diff --git a/DEPS b/DEPS
index 4796328..cfb218fd 100644
--- a/DEPS
+++ b/DEPS
@@ -195,11 +195,11 @@
   # 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': '21efb7c7ddbc911adb60228934c0cdf14085f0ce',
+  'skia_revision': '10f019c5068f9ff070c16e3571209012c78826e3',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling V8
   # and whatever else without interference from each other.
-  'v8_revision': 'a8084f7d644a4e257d20d9414d831af8db8947c0',
+  'v8_revision': 'ce4c3060f6fbfd5ed410de314f7e83a66fd792d9',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling swarming_client
   # and whatever else without interference from each other.
@@ -207,11 +207,11 @@
   # 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': 'd41280a787d1594470053dfa15b3330822049612',
+  'angle_revision': 'adc250c38976277a15bd91605d06e1613bfdac86',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling SwiftShader
   # and whatever else without interference from each other.
-  'swiftshader_revision': 'a3e03e1086d5c85bd8435ff0b6f48666a8042603',
+  'swiftshader_revision': 'be7c55a2a8cebd46ba6912e6b7d4dae8353d154c',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling PDFium
   # and whatever else without interference from each other.
@@ -258,7 +258,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling catapult
   # and whatever else without interference from each other.
-  'catapult_revision': 'cf93e1de9eb8b7073397eb5aac874fd67e06b1a5',
+  'catapult_revision': 'ac60992d41b7007e9c6c4329e5bbfc02c912f314',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling libFuzzer
   # and whatever else without interference from each other.
@@ -266,7 +266,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': '97713d6f163e086aad0560246b1e68e3b673115f',
+  'devtools_frontend_revision': '580a4b1d52b65febb22c804a036d972898026b86',
   # 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.
@@ -318,11 +318,11 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling feed
   # and whatever else without interference from each other.
-  'dawn_revision': 'e84a1b13760d6ca251819569af8684af56fdee19',
+  'dawn_revision': '0a4342793e0e31ed36e59d2eef36817c82ede555',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling feed
   # and whatever else without interference from each other.
-  'quiche_revision': 'dd5382a4449c31e40aafc2ec60cd80041c32bdc0',
+  'quiche_revision': '1b6221e157c76e14b4041f961f2e4c3a5f82673c',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling ios_webkit
   # and whatever else without interference from each other.
@@ -545,7 +545,7 @@
   },
 
   'src/ios/third_party/material_components_ios/src': {
-      'url': Var('chromium_git') + '/external/github.com/material-components/material-components-ios.git' + '@' + '0dd6376c6de22ff059f68023f0e876d0e275aeb1',
+      'url': Var('chromium_git') + '/external/github.com/material-components/material-components-ios.git' + '@' + '8dac13ae51e05e60b394ac81dffa40cfcfd05bec',
       'condition': 'checkout_ios',
   },
 
@@ -895,7 +895,7 @@
   },
 
   'src/third_party/depot_tools':
-    Var('chromium_git') + '/chromium/tools/depot_tools.git' + '@' + 'a0c3f906bc6fdd7cfdc892e4e1c4a89ed97d5d5a',
+    Var('chromium_git') + '/chromium/tools/depot_tools.git' + '@' + '486f1812ef65fde35ad0c63e3939e89dc1c876a3',
 
   'src/third_party/devtools-frontend/src':
     Var('chromium_git') + '/devtools/devtools-frontend' + '@' + Var('devtools_frontend_revision'),
@@ -1248,7 +1248,7 @@
   },
 
   'src/third_party/perfetto':
-    Var('android_git') + '/platform/external/perfetto.git' + '@' + '06e30bae5cbff2a72e6840bf6f845237187c44b0',
+    Var('android_git') + '/platform/external/perfetto.git' + '@' + 'f8ccdc63a188fd977953864bab7490eac9f9852f',
 
   'src/third_party/perl': {
       'url': Var('chromium_git') + '/chromium/deps/perl.git' + '@' + '6f3e5028eb65d0b4c5fdd792106ac4c84eee1eb3',
@@ -1537,7 +1537,7 @@
     Var('chromium_git') + '/v8/v8.git' + '@' +  Var('v8_revision'),
 
   'src-internal': {
-    'url': 'https://chrome-internal.googlesource.com/chrome/src-internal.git@c6116bf6ce82d05a73126691f52c1ca1f9f4848b',
+    'url': 'https://chrome-internal.googlesource.com/chrome/src-internal.git@d24a19d429243e3ae7fa087b2c1ed9b9cf921863',
     'condition': 'checkout_src_internal',
   },
 
diff --git a/PRESUBMIT.py b/PRESUBMIT.py
index 13fb2d09..6532a0d 100644
--- a/PRESUBMIT.py
+++ b/PRESUBMIT.py
@@ -2162,28 +2162,6 @@
           long_text=error.output)]
 
 
-def _CheckTeamTags(input_api, output_api):
-  """Checks that OWNERS files have consistent TEAM and COMPONENT tags."""
-  checkteamtags_tool = input_api.os_path.join(
-      input_api.PresubmitLocalPath(),
-      'tools', 'checkteamtags', 'checkteamtags.py')
-  args = [input_api.python_executable, checkteamtags_tool,
-          '--root', input_api.change.RepositoryRoot()]
-  files = [f.LocalPath() for f in input_api.AffectedFiles(include_deletes=False)
-           if input_api.os_path.basename(f.AbsoluteLocalPath()).upper() ==
-           'OWNERS']
-  try:
-    if files:
-      warnings = input_api.subprocess.check_output(args + files).splitlines()
-      if warnings:
-        return [output_api.PresubmitPromptWarning(warnings[0], warnings[1:])]
-    return []
-  except input_api.subprocess.CalledProcessError as error:
-    return [output_api.PresubmitError(
-        'checkteamtags.py failed:',
-        long_text=error.output)]
-
-
 def _CheckNoAuraWindowPropertyHInHeaders(input_api, output_api):
   """Makes sure we don't include ui/aura/window_property.h
   in header files.
@@ -4425,7 +4403,6 @@
   results.extend(_CheckNoTrinaryTrueFalse(input_api, output_api))
   results.extend(_CheckUnwantedDependencies(input_api, output_api))
   results.extend(_CheckFilePermissions(input_api, output_api))
-  results.extend(_CheckTeamTags(input_api, output_api))
   results.extend(_CheckNoAuraWindowPropertyHInHeaders(input_api, output_api))
   results.extend(_CheckForVersionControlConflicts(input_api, output_api))
   results.extend(_CheckPatchFiles(input_api, output_api))
@@ -4471,6 +4448,15 @@
   results.extend(_CheckPythonDevilInit(input_api, output_api))
   results.extend(_CheckStableMojomChanges(input_api, output_api))
 
+  dirmd_bin = input_api.os_path.join(
+      input_api.PresubmitLocalPath(), 'third_party', 'depot_tools', 'dirmd')
+  results.extend(input_api.RunTests(
+      input_api.canned_checks.CheckDirMetadataFormat(
+          input_api, output_api, dirmd_bin)))
+  results.extend(
+      input_api.canned_checks.CheckOwnersDirMetadataExclusive(
+          input_api, output_api))
+
   for f in input_api.AffectedFiles():
     path, name = input_api.os_path.split(f.LocalPath())
     if name == 'PRESUBMIT.py':
diff --git a/android_webview/glue/BUILD.gn b/android_webview/glue/BUILD.gn
index fddb8aae..10bf86b2 100644
--- a/android_webview/glue/BUILD.gn
+++ b/android_webview/glue/BUILD.gn
@@ -45,6 +45,7 @@
     "java/src/com/android/webview/chromium/GlueApiHelperForOMR1.java",
     "java/src/com/android/webview/chromium/GlueApiHelperForP.java",
     "java/src/com/android/webview/chromium/GlueApiHelperForQ.java",
+    "java/src/com/android/webview/chromium/GlueApiHelperForR.java",
     "java/src/com/android/webview/chromium/GraphicsUtils.java",
     "java/src/com/android/webview/chromium/MonochromeLibraryPreloader.java",
     "java/src/com/android/webview/chromium/PacProcessorImpl.java",
diff --git a/android_webview/glue/java/src/com/android/webview/chromium/GlueApiHelperForR.java b/android_webview/glue/java/src/com/android/webview/chromium/GlueApiHelperForR.java
new file mode 100644
index 0000000..f3aee53
--- /dev/null
+++ b/android_webview/glue/java/src/com/android/webview/chromium/GlueApiHelperForR.java
@@ -0,0 +1,28 @@
+// 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.
+
+package com.android.webview.chromium;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.webkit.PacProcessor;
+
+import org.chromium.base.annotations.VerifiesOnR;
+
+/**
+ * Utility class to use new APIs that were added in R (API level 30). These need to exist in a
+ * separate class so that Android framework can successfully verify glue layer classes without
+ * encountering the new APIs. Note that GlueApiHelper is only for APIs that cannot go to ApiHelper
+ * in base/, for reasons such as using system APIs or instantiating an adapter class that is
+ * specific to glue layer.
+ */
+@VerifiesOnR
+@TargetApi(Build.VERSION_CODES.R)
+public final class GlueApiHelperForR {
+    private GlueApiHelperForR() {}
+
+    public static PacProcessor getPacProcessor() {
+        return PacProcessorImpl.getInstance();
+    }
+}
diff --git a/android_webview/glue/java/src/com/android/webview/chromium/WebViewChromiumFactoryProvider.java b/android_webview/glue/java/src/com/android/webview/chromium/WebViewChromiumFactoryProvider.java
index 5c5c68b..682856ac 100644
--- a/android_webview/glue/java/src/com/android/webview/chromium/WebViewChromiumFactoryProvider.java
+++ b/android_webview/glue/java/src/com/android/webview/chromium/WebViewChromiumFactoryProvider.java
@@ -17,6 +17,7 @@
 import android.view.ViewGroup;
 import android.webkit.CookieManager;
 import android.webkit.GeolocationPermissions;
+import android.webkit.PacProcessor;
 import android.webkit.ServiceWorkerController;
 import android.webkit.TokenBindingService;
 import android.webkit.TracingController;
@@ -715,4 +716,9 @@
             getSingleton().getBrowserContextOnUiThread().setWebLayerRunningInSameProcess();
         });
     }
+
+    @Override
+    public PacProcessor getPacProcessor() {
+        return GlueApiHelperForR.getPacProcessor();
+    }
 }
diff --git a/android_webview/glue/java/src/com/android/webview/chromium/WebViewChromiumFactoryProviderForR.java b/android_webview/glue/java/src/com/android/webview/chromium/WebViewChromiumFactoryProviderForR.java
index f06585a..925594aa 100644
--- a/android_webview/glue/java/src/com/android/webview/chromium/WebViewChromiumFactoryProviderForR.java
+++ b/android_webview/glue/java/src/com/android/webview/chromium/WebViewChromiumFactoryProviderForR.java
@@ -4,8 +4,6 @@
 
 package com.android.webview.chromium;
 
-import android.webkit.PacProcessor;
-
 class WebViewChromiumFactoryProviderForR extends WebViewChromiumFactoryProvider {
     public static WebViewChromiumFactoryProvider create(android.webkit.WebViewDelegate delegate) {
         return new WebViewChromiumFactoryProviderForR(delegate);
@@ -14,9 +12,4 @@
     protected WebViewChromiumFactoryProviderForR(android.webkit.WebViewDelegate delegate) {
         super(delegate);
     }
-
-    @Override
-    public PacProcessor getPacProcessor() {
-        return PacProcessorImpl.getInstance();
-    }
 }
diff --git a/android_webview/javatests/src/org/chromium/android_webview/test/SafeBrowsingTest.java b/android_webview/javatests/src/org/chromium/android_webview/test/SafeBrowsingTest.java
index 61f09b2..985755d1 100644
--- a/android_webview/javatests/src/org/chromium/android_webview/test/SafeBrowsingTest.java
+++ b/android_webview/javatests/src/org/chromium/android_webview/test/SafeBrowsingTest.java
@@ -47,6 +47,7 @@
 import org.chromium.base.task.PostTask;
 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.Feature;
 import org.chromium.base.test.util.InMemorySharedPreferences;
 import org.chromium.components.safe_browsing.SafeBrowsingApiBridge;
@@ -615,6 +616,7 @@
     }
 
     @Test
+    @DisabledTest(message = "Wait for interstitial is flaky. crbug.com/1107540")
     @SmallTest
     @Feature({"AndroidWebView"})
     public void testSafeBrowsingShowsInterstitialForSubresource() throws Throwable {
@@ -643,6 +645,7 @@
     }
 
     @Test
+    @DisabledTest(message = "Wait for interstitial is flaky. crbug.com/1107540")
     @SmallTest
     @Feature({"AndroidWebView"})
     public void testSafeBrowsingProceedThroughInterstitialForSubresource() throws Throwable {
@@ -692,6 +695,7 @@
     }
 
     @Test
+    @DisabledTest(message = "Wait for interstitial is flaky. crbug.com/1107540")
     @SmallTest
     @Feature({"AndroidWebView"})
     public void testSafeBrowsingDontProceedNavigatesBackForSubResource() throws Throwable {
diff --git a/ash/BUILD.gn b/ash/BUILD.gn
index b9f43d5..bd44b65 100644
--- a/ash/BUILD.gn
+++ b/ash/BUILD.gn
@@ -1883,6 +1883,7 @@
     "power/hfp_battery_listener_unittest.cc",
     "power/hid_battery_listener_unittest.cc",
     "power/hid_battery_util_unittest.cc",
+    "quick_answers/quick_answers_ui_controller_unittest.cc",
     "quick_answers/ui/quick_answers_view_unittest.cc",
     "root_window_controller_unittest.cc",
     "rotator/screen_rotation_animation_unittest.cc",
diff --git a/ash/ambient/ambient_constants.h b/ash/ambient/ambient_constants.h
index c9fb294..993a0dc 100644
--- a/ash/ambient/ambient_constants.h
+++ b/ash/ambient/ambient_constants.h
@@ -17,7 +17,7 @@
 // The default interval to refresh photos.
 // TODO(b/139953713): Change to a correct time interval.
 constexpr base::TimeDelta kPhotoRefreshInterval =
-    base::TimeDelta::FromSeconds(5);
+    base::TimeDelta::FromSeconds(60);
 
 // Directory name of ambient mode.
 constexpr char kAmbientModeDirectoryName[] = "ambient-mode";
diff --git a/ash/ambient/ambient_photo_controller.cc b/ash/ambient/ambient_photo_controller.cc
index 6a3e4fd..a36df7d 100644
--- a/ash/ambient/ambient_photo_controller.cc
+++ b/ash/ambient/ambient_photo_controller.cc
@@ -55,10 +55,8 @@
 
 // The upper bound of delay to the fetch topics. An random value will be
 // generated in the range of |kTopicFetchDelayMax|/2 to |kTopicFetchDelayMax|.
-
-// TODO(b/139953713): Change to a correct time interval.
-// E.g. it will be max 36 seconds if we want to fetch 50 batches in 30 mins.
-constexpr base::TimeDelta kTopicFetchDelayMax = base::TimeDelta::FromSeconds(3);
+constexpr base::TimeDelta kTopicFetchDelayMax =
+    base::TimeDelta::FromSeconds(36);
 
 constexpr int kMaxImageSizeInBytes = 5 * 1024 * 1024;
 
diff --git a/ash/clipboard/clipboard_history_controller.cc b/ash/clipboard/clipboard_history_controller.cc
index 73fe8c9..ba0b9724 100644
--- a/ash/clipboard/clipboard_history_controller.cc
+++ b/ash/clipboard/clipboard_history_controller.cc
@@ -23,6 +23,7 @@
 #include "ui/base/ime/text_input_client.h"
 #include "ui/base/models/image_model.h"
 #include "ui/base/models/menu_separator_types.h"
+#include "ui/base/models/simple_menu_model.h"
 #include "ui/base/resource/resource_bundle.h"
 #include "ui/display/screen.h"
 #include "ui/events/event.h"
@@ -69,35 +70,18 @@
   clipboard->WriteClipboardData(std::make_unique<ui::ClipboardData>(data));
 }
 
-class ClipboardHistoryMenuDelegate : public ui::SimpleMenuModel::Delegate {
- public:
-  ClipboardHistoryMenuDelegate(ClipboardHistoryController* controller)
-      : controller_(controller) {}
-  ClipboardHistoryMenuDelegate(const ClipboardHistoryMenuDelegate&) = delete;
-  ClipboardHistoryMenuDelegate& operator=(const ClipboardHistoryMenuDelegate&) =
-      delete;
-
-  // ui::SimpleMenuModel::Delegate:
-  void ExecuteCommand(int command_id, int event_flags) override {
-    controller_->MenuOptionSelected(/*index=*/command_id);
-  }
-
- private:
-  // The controller responsible for showing the Clipboard History menu.
-  ClipboardHistoryController* const controller_;
-};
-
 }  // namespace
 
-class ClipboardHistoryAcceleratorTarget : public ui::AcceleratorTarget {
+// ClipboardHistoryController::AcceleratorTarget -------------------------------
+
+class ClipboardHistoryController::AcceleratorTarget
+    : public ui::AcceleratorTarget {
  public:
-  ClipboardHistoryAcceleratorTarget(ClipboardHistoryController* controller)
+  explicit AcceleratorTarget(ClipboardHistoryController* controller)
       : controller_(controller) {}
-  ClipboardHistoryAcceleratorTarget(const ClipboardHistoryAcceleratorTarget&) =
-      delete;
-  ClipboardHistoryAcceleratorTarget& operator=(
-      const ClipboardHistoryAcceleratorTarget&) = delete;
-  ~ClipboardHistoryAcceleratorTarget() override = default;
+  AcceleratorTarget(const AcceleratorTarget&) = delete;
+  AcceleratorTarget& operator=(const AcceleratorTarget&) = delete;
+  ~AcceleratorTarget() override = default;
 
   void Init() {
     ui::Accelerator show_menu_combo(ui::VKEY_V, ui::EF_COMMAND_DOWN);
@@ -111,23 +95,47 @@
  private:
   // ui::AcceleratorTarget:
   bool AcceleratorPressed(const ui::Accelerator& accelerator) override {
-    controller_->ShowMenu();
+    if (controller_->IsMenuShowing())
+      controller_->ExecuteSelectedMenuItem();
+    else
+      controller_->ShowMenu();
     return true;
   }
 
   bool CanHandleAccelerators() const override {
-    return controller_->CanShowMenu();
+    return controller_->IsMenuShowing() || controller_->CanShowMenu();
   }
 
   // The controller responsible for showing the Clipboard History menu.
   ClipboardHistoryController* const controller_;
 };
 
+// ClipboardHistoryController::MenuDelegate ------------------------------------
+
+class ClipboardHistoryController::MenuDelegate
+    : public ui::SimpleMenuModel::Delegate {
+ public:
+  explicit MenuDelegate(ClipboardHistoryController* controller)
+      : controller_(controller) {}
+  MenuDelegate(const MenuDelegate&) = delete;
+  MenuDelegate& operator=(const MenuDelegate&) = delete;
+
+  // ui::SimpleMenuModel::Delegate:
+  void ExecuteCommand(int command_id, int event_flags) override {
+    controller_->MenuOptionSelected(/*index=*/command_id);
+  }
+
+ private:
+  // The controller responsible for showing the Clipboard History menu.
+  ClipboardHistoryController* const controller_;
+};
+
+// ClipboardHistoryController --------------------------------------------------
+
 ClipboardHistoryController::ClipboardHistoryController()
     : clipboard_history_(std::make_unique<ClipboardHistory>()),
-      accelerator_target_(
-          std::make_unique<ClipboardHistoryAcceleratorTarget>(this)),
-      menu_delegate_(std::make_unique<ClipboardHistoryMenuDelegate>(this)) {}
+      accelerator_target_(std::make_unique<AcceleratorTarget>(this)),
+      menu_delegate_(std::make_unique<MenuDelegate>(this)) {}
 
 ClipboardHistoryController::~ClipboardHistoryController() = default;
 
@@ -135,12 +143,35 @@
   accelerator_target_->Init();
 }
 
+bool ClipboardHistoryController::IsMenuShowing() const {
+  return context_menu_ && context_menu_->IsRunning();
+}
+
+gfx::Rect ClipboardHistoryController::GetMenuBoundsInScreenForTest() const {
+  return context_menu_->GetMenuBoundsInScreenForTest();
+}
+
 bool ClipboardHistoryController::CanShowMenu() const {
   return !clipboard_history_->IsEmpty();
 }
 
+void ClipboardHistoryController::ExecuteSelectedMenuItem() {
+  DCHECK(IsMenuShowing());
+  auto command = context_menu_->GetSelectedMenuItemCommand();
+
+  // TODO(crbug.com/1106849): Update once sequential paste is supported.
+  // Force close the context menu. Failure to do so before dispatching our
+  // synthetic key event will result in the context menu consuming the event.
+  // Currently we don't support sequential copy-paste. Once we do, we'll have to
+  // update this logic.
+  context_menu_->Cancel();
+
+  // If no menu item is currently selected, we'll fallback to the first item.
+  menu_delegate_->ExecuteCommand(command.value_or(0), ui::EF_NONE);
+}
+
 void ClipboardHistoryController::ShowMenu() {
-  if (!CanShowMenu())
+  if (IsMenuShowing() || !CanShowMenu())
     return;
 
   clipboard_items_ =
@@ -205,20 +236,24 @@
   // back onto the clipboard.
   base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
       FROM_HERE,
-      base::BindOnce(&WriteClipboardDataToClipboard,
-                     *(clipboard_items_.begin())),
+      base::BindOnce(
+          [](const base::WeakPtr<ClipboardHistoryController>& weak_ptr,
+             ui::ClipboardData clipboard_data) {
+            // When restoring the original item back on top of the clipboard we
+            // need to pause clipboard history. Failure to do so will result in
+            // the original item being re-recorded when this restoration step
+            // should actually be opaque to the user.
+            std::unique_ptr<ClipboardHistory::ScopedPause> scoped_pause;
+            if (weak_ptr) {
+              scoped_pause = std::make_unique<ClipboardHistory::ScopedPause>(
+                  weak_ptr->clipboard_history_.get());
+            }
+            WriteClipboardDataToClipboard(clipboard_data);
+          },
+          weak_ptr_factory_.GetWeakPtr(), *clipboard_items_.begin()),
       base::TimeDelta::FromMilliseconds(100));
 }
 
-bool ClipboardHistoryController::IsMenuShowing() const {
-  return context_menu_ && context_menu_->IsRunning();
-}
-
-gfx::Rect ClipboardHistoryController::GetClipboardHistoryMenuBoundsForTest()
-    const {
-  return context_menu_->GetClipboardHistoryMenuBoundsForTest();
-}
-
 gfx::Rect ClipboardHistoryController::CalculateAnchorRect() const {
   display::Display display = display::Screen::GetScreen()->GetPrimaryDisplay();
   auto* host = ash::GetWindowTreeHostForDisplay(display.id());
diff --git a/ash/clipboard/clipboard_history_controller.h b/ash/clipboard/clipboard_history_controller.h
index 662d6a00..84e61ce 100644
--- a/ash/clipboard/clipboard_history_controller.h
+++ b/ash/clipboard/clipboard_history_controller.h
@@ -10,15 +10,18 @@
 
 #include "ash/ash_export.h"
 #include "base/memory/weak_ptr.h"
-#include "ui/base/models/simple_menu_model.h"
+
+namespace gfx {
+class Rect;
+}  // namespace gfx
 
 namespace ui {
 class ClipboardData;
 }  // namespace ui
 
 namespace ash {
+
 class ClipboardHistory;
-class ClipboardHistoryAcceleratorTarget;
 class ClipboardHistoryMenuModelAdapter;
 
 // Shows a menu with the last few things saved in the clipboard when the
@@ -33,23 +36,24 @@
 
   void Init();
 
-  // Whether a menu can be shown.
-  bool CanShowMenu() const;
-
-  // Shows a menu with the last few items copied. Executing one of the menu
-  // options results in that item being pasted into the active window.
-  void ShowMenu();
-
-  // Called when a menu option is selected.
-  void MenuOptionSelected(int index);
-
+  // Returns if the contextual menu is currently showing.
   bool IsMenuShowing() const;
 
-  gfx::Rect GetClipboardHistoryMenuBoundsForTest() const;
+  // Returns bounds for the contextual menu in screen coordinates.
+  gfx::Rect GetMenuBoundsInScreenForTest() const;
 
-  ClipboardHistory* clipboard_history() { return clipboard_history_.get(); }
+  // Returns the history which tracks what is being copied to the clipboard.
+  const ClipboardHistory* history() const { return clipboard_history_.get(); }
 
  private:
+  class AcceleratorTarget;
+  class MenuDelegate;
+
+  bool CanShowMenu() const;
+  void ShowMenu();
+  void ExecuteSelectedMenuItem();
+  void MenuOptionSelected(int index);
+
   gfx::Rect CalculateAnchorRect() const;
 
   // The menu being shown.
@@ -57,8 +61,9 @@
   // Used to keep track of what is being copied to the clipboard.
   std::unique_ptr<ClipboardHistory> clipboard_history_;
   // Detects the search+v key combo.
-  std::unique_ptr<ClipboardHistoryAcceleratorTarget> accelerator_target_;
-  std::unique_ptr<ui::SimpleMenuModel::Delegate> menu_delegate_;
+  std::unique_ptr<AcceleratorTarget> accelerator_target_;
+  // Handles events on the contextual menu.
+  std::unique_ptr<MenuDelegate> menu_delegate_;
   // The items we show in the contextual menu. Saved so we can paste them later.
   std::vector<ui::ClipboardData> clipboard_items_;
 
diff --git a/ash/clipboard/clipboard_history_menu_model_adapter.cc b/ash/clipboard/clipboard_history_menu_model_adapter.cc
index d287c36..971abed 100644
--- a/ash/clipboard/clipboard_history_menu_model_adapter.cc
+++ b/ash/clipboard/clipboard_history_menu_model_adapter.cc
@@ -38,8 +38,22 @@
   return menu_runner_ && menu_runner_->IsRunning();
 }
 
-gfx::Rect
-ClipboardHistoryMenuModelAdapter::GetClipboardHistoryMenuBoundsForTest() const {
+void ClipboardHistoryMenuModelAdapter::Cancel() {
+  DCHECK(menu_runner_);
+  menu_runner_->Cancel();
+}
+
+base::Optional<int>
+ClipboardHistoryMenuModelAdapter::GetSelectedMenuItemCommand() const {
+  DCHECK(root_view_);
+  auto* menu_item = root_view_->GetMenuController()->GetSelectedMenuItem();
+  return menu_item ? base::make_optional(menu_item->GetCommand())
+                   : base::nullopt;
+}
+
+gfx::Rect ClipboardHistoryMenuModelAdapter::GetMenuBoundsInScreenForTest()
+    const {
+  DCHECK(root_view_);
   return root_view_->GetSubmenu()->GetBoundsInScreen();
 }
 
diff --git a/ash/clipboard/clipboard_history_menu_model_adapter.h b/ash/clipboard/clipboard_history_menu_model_adapter.h
index e5d5012..db5e0ef 100644
--- a/ash/clipboard/clipboard_history_menu_model_adapter.h
+++ b/ash/clipboard/clipboard_history_menu_model_adapter.h
@@ -7,6 +7,7 @@
 
 #include <memory>
 
+#include "base/optional.h"
 #include "ui/views/controls/menu/menu_model_adapter.h"
 
 namespace gfx {
@@ -39,9 +40,18 @@
   // Shows the menu, anchored below |anchor_rect|.
   void Run(const gfx::Rect& anchor_rect);
 
+  // Returns if the menu is currently running.
   bool IsRunning() const;
 
-  gfx::Rect GetClipboardHistoryMenuBoundsForTest() const;
+  // Hides and cancels the menu.
+  void Cancel();
+
+  // Returns the command of the currently selected menu item. If no menu item is
+  // currently selected, returns |base::nullopt|.
+  base::Optional<int> GetSelectedMenuItemCommand() const;
+
+  // Returns menu bounds in screen coordinates.
+  gfx::Rect GetMenuBoundsInScreenForTest() const;
 
  private:
   // The model which holds the contents of the menu.
diff --git a/ash/clipboard/clipboard_history_unittest.cc b/ash/clipboard/clipboard_history_unittest.cc
index 8e6e5fd..581f1c90 100644
--- a/ash/clipboard/clipboard_history_unittest.cc
+++ b/ash/clipboard/clipboard_history_unittest.cc
@@ -33,8 +33,8 @@
     scoped_feature_list_.InitWithFeatures(
         {chromeos::features::kClipboardHistory}, {});
     AshTestBase::SetUp();
-    clipboard_history_ =
-        Shell::Get()->clipboard_history_controller()->clipboard_history();
+    clipboard_history_ = const_cast<ClipboardHistory*>(
+        Shell::Get()->clipboard_history_controller()->history());
   }
 
   const std::list<ui::ClipboardData>& GetClipboardHistoryData() {
diff --git a/ash/quick_answers/quick_answers_ui_controller.cc b/ash/quick_answers/quick_answers_ui_controller.cc
index 2ce205c..5b8964f 100644
--- a/ash/quick_answers/quick_answers_ui_controller.cc
+++ b/ash/quick_answers/quick_answers_ui_controller.cc
@@ -28,8 +28,8 @@
     : controller_(controller) {}
 
 QuickAnswersUiController::~QuickAnswersUiController() {
-  CloseQuickAnswersView();
-  CloseUserConsentView();
+  quick_answers_view_ = nullptr;
+  user_consent_view_ = nullptr;
 }
 
 void QuickAnswersUiController::CreateQuickAnswersView(
diff --git a/ash/quick_answers/quick_answers_ui_controller_unittest.cc b/ash/quick_answers/quick_answers_ui_controller_unittest.cc
new file mode 100644
index 0000000..1c6e3ad
--- /dev/null
+++ b/ash/quick_answers/quick_answers_ui_controller_unittest.cc
@@ -0,0 +1,66 @@
+// 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 "ash/quick_answers/ui/quick_answers_view.h"
+
+#include "ash/quick_answers/quick_answers_controller_impl.h"
+#include "ash/quick_answers/quick_answers_ui_controller.h"
+#include "ash/test/ash_test_base.h"
+#include "base/test/scoped_feature_list.h"
+#include "chromeos/constants/chromeos_features.h"
+
+namespace ash {
+
+namespace {
+
+constexpr gfx::Rect kDefaultAnchorBoundsInScreen =
+    gfx::Rect(gfx::Point(500, 250), gfx::Size(80, 140));
+
+}  // namespace
+
+class QuickAnswersUiControllerTest : public AshTestBase {
+ protected:
+  QuickAnswersUiControllerTest() {
+    scoped_feature_list_.InitWithFeatures(
+        {chromeos::features::kQuickAnswers,
+         chromeos::features::kQuickAnswersRichUi},
+        {});
+  }
+  QuickAnswersUiControllerTest(const QuickAnswersUiControllerTest&) = delete;
+  QuickAnswersUiControllerTest& operator=(const QuickAnswersUiControllerTest&) =
+      delete;
+  ~QuickAnswersUiControllerTest() override = default;
+
+  // AshTestBase:
+  void SetUp() override {
+    AshTestBase::SetUp();
+
+    ui_controller_ =
+        static_cast<QuickAnswersControllerImpl*>(QuickAnswersController::Get())
+            ->quick_answers_ui_controller();
+  }
+
+  // Currently instantiated QuickAnswersView instance.
+  QuickAnswersUiController* ui_controller() { return ui_controller_; }
+
+ private:
+  QuickAnswersUiController* ui_controller_ = nullptr;
+  base::test::ScopedFeatureList scoped_feature_list_;
+};
+
+TEST_F(QuickAnswersUiControllerTest, TearDownWhileQuickAnswersViewShowing) {
+  EXPECT_FALSE(ui_controller()->is_showing_quick_answers_view());
+  ui_controller()->CreateQuickAnswersView(kDefaultAnchorBoundsInScreen,
+                                          "default_title", "default_query");
+  EXPECT_TRUE(ui_controller()->is_showing_quick_answers_view());
+}
+
+TEST_F(QuickAnswersUiControllerTest, TearDownWhileConsentViewShowing) {
+  EXPECT_FALSE(ui_controller()->is_showing_user_consent_view());
+  ui_controller()->CreateUserConsentView(kDefaultAnchorBoundsInScreen,
+                                         base::string16(), base::string16());
+  EXPECT_TRUE(ui_controller()->is_showing_user_consent_view());
+}
+
+}  // namespace ash
diff --git a/ash/resources/vector_icons/BUILD.gn b/ash/resources/vector_icons/BUILD.gn
index 17d947d..0fade24d 100644
--- a/ash/resources/vector_icons/BUILD.gn
+++ b/ash/resources/vector_icons/BUILD.gn
@@ -30,6 +30,7 @@
     "battery.icon",
     "bitmap.icon",
     "captive_portal.icon",
+    "capture_mode.icon",
     "check_circle.icon",
     "custom_data.icon",
     "delete.icon",
diff --git a/ash/resources/vector_icons/capture_mode.icon b/ash/resources/vector_icons/capture_mode.icon
new file mode 100644
index 0000000..684abb8
--- /dev/null
+++ b/ash/resources/vector_icons/capture_mode.icon
@@ -0,0 +1,29 @@
+// 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.
+
+CANVAS_DIMENSIONS, 20,
+MOVE_TO, 13, 10,
+R_ARC_TO, 3, 3, 0, 1, 1, -6, 0,
+R_ARC_TO, 3, 3, 0, 0, 1, 6, 0,
+CLOSE,
+R_MOVE_TO, -2, 0,
+R_ARC_TO, 1, 1, 0, 1, 1, -2, 0,
+R_ARC_TO, 1, 1, 0, 0, 1, 2, 0,
+CLOSE,
+MOVE_TO, 3, 5,
+R_ARC_TO, 2, 2, 0, 0, 1, 2, -2,
+R_H_LINE_TO, 10,
+R_ARC_TO, 2, 2, 0, 0, 1, 2, 2,
+R_V_LINE_TO, 10,
+R_ARC_TO, 2, 2, 0, 0, 1, -2, 2,
+H_LINE_TO, 5,
+R_ARC_TO, 2, 2, 0, 0, 1, -2, -2,
+V_LINE_TO, 5,
+CLOSE,
+R_MOVE_TO, 2, 0,
+R_H_LINE_TO, 10,
+R_V_LINE_TO, 10,
+H_LINE_TO, 5,
+V_LINE_TO, 5,
+CLOSE
diff --git a/ash/shelf/shelf_focus_cycler.cc b/ash/shelf/shelf_focus_cycler.cc
index a55202b4..a3aaf44 100644
--- a/ash/shelf/shelf_focus_cycler.cc
+++ b/ash/shelf/shelf_focus_cycler.cc
@@ -65,7 +65,7 @@
     FocusOut(last_element, SourceView::kShelfNavigationView);
     return;
   }
-  navigation_widget->SetDefaultLastFocusableChild(last_element);
+  navigation_widget->PrepareForGettingFocus(last_element);
   Shell::Get()->focus_cycler()->FocusWidget(navigation_widget);
 }
 
diff --git a/ash/shelf/shelf_layout_manager_unittest.cc b/ash/shelf/shelf_layout_manager_unittest.cc
index 1db7b17..11e7a28 100644
--- a/ash/shelf/shelf_layout_manager_unittest.cc
+++ b/ash/shelf/shelf_layout_manager_unittest.cc
@@ -577,6 +577,26 @@
             GetShelfWidget()->GetBackgroundType());
 }
 
+// Verifies that the hidden shelf shows after triggering the FOCUS_SHELF
+// accelerator (https://crbug.com/1111426).
+TEST_F(ShelfLayoutManagerTest, ShowHiddenShelfByFocusShelfAccelerator) {
+  // Open a window so that the shelf will auto-hide.
+  std::unique_ptr<aura::Window> window(CreateTestWindow());
+  window->Show();
+  Shelf* shelf = GetPrimaryShelf();
+  shelf->SetAutoHideBehavior(ShelfAutoHideBehavior::kAlways);
+  EXPECT_EQ(SHELF_AUTO_HIDE, shelf->GetVisibilityState());
+  EXPECT_EQ(SHELF_AUTO_HIDE_HIDDEN, shelf->GetAutoHideState());
+
+  // Focus on the shelf by accelerator.
+  Shell::Get()->accelerator_controller()->PerformActionIfEnabled(FOCUS_SHELF,
+                                                                 {});
+
+  // Shelf should be visible.
+  EXPECT_EQ(SHELF_AUTO_HIDE, shelf->GetVisibilityState());
+  EXPECT_EQ(SHELF_AUTO_HIDE_SHOWN, shelf->GetAutoHideState());
+}
+
 TEST_F(ShelfLayoutManagerTest, ShelfDoesNotAutoHideWithVoxAndTabletMode) {
   TabletModeControllerTestApi().EnterTabletMode();
   // Open a window so that the shelf will auto-hide.
diff --git a/ash/shelf/shelf_navigation_widget.cc b/ash/shelf/shelf_navigation_widget.cc
index 962e32f4..987836b 100644
--- a/ash/shelf/shelf_navigation_widget.cc
+++ b/ash/shelf/shelf_navigation_widget.cc
@@ -666,6 +666,21 @@
   return gfx::Rect(target_bounds_.origin(), clip_rect_.size());
 }
 
+void ShelfNavigationWidget::PrepareForGettingFocus(bool last_element) {
+  SetDefaultLastFocusableChild(last_element);
+
+  // The native view of the navigation widget is not activatable when its target
+  // visibility is false. So show the widget before setting focus.
+
+  // Layer opacity should be set first. Because it is not allowed that a window
+  // is visible but the layers alpha is fully transparent.
+  ui::Layer* layer = GetLayer();
+  if (layer->GetTargetOpacity() != 1.f)
+    GetLayer()->SetOpacity(1.f);
+  if (!IsVisible())
+    ShowInactive();
+}
+
 void ShelfNavigationWidget::HandleLocaleChange() {
   delegate_->home_button()->HandleLocaleChange();
   delegate_->back_button()->HandleLocaleChange();
diff --git a/ash/shelf/shelf_navigation_widget.h b/ash/shelf/shelf_navigation_widget.h
index d25943b..af6c1b2 100644
--- a/ash/shelf/shelf_navigation_widget.h
+++ b/ash/shelf/shelf_navigation_widget.h
@@ -84,6 +84,9 @@
   // Returns the visible part's bounds in screen coordinates.
   gfx::Rect GetVisibleBounds() const;
 
+  // Do preparations before setting focus on the navigation widget.
+  void PrepareForGettingFocus(bool last_element);
+
   // Called when shelf layout manager detects a locale change. Reloads the
   // home and back button tooltips and accessibility name strings.
   void HandleLocaleChange();
diff --git a/base/BUILD.gn b/base/BUILD.gn
index fda1a07..e4ca016c 100644
--- a/base/BUILD.gn
+++ b/base/BUILD.gn
@@ -1816,11 +1816,6 @@
       "strings/string16.cc",
     ]
 
-    # winternl.h and NTSecAPI.h have different definitions of UNICODE_STRING.
-    # There's only one client of NTSecAPI.h in base but several of winternl.h,
-    # so exclude the NTSecAPI.h one.
-    jumbo_excluded_sources = [ "rand_util_win.cc" ]
-
     deps += [ "//base/win:base_win_buildflags" ]
 
     data_deps += [ "//build/win:runtime_libs" ]
@@ -3556,6 +3551,7 @@
       "android/java/src/org/chromium/base/annotations/VerifiesOnOMR1.java",
       "android/java/src/org/chromium/base/annotations/VerifiesOnP.java",
       "android/java/src/org/chromium/base/annotations/VerifiesOnQ.java",
+      "android/java/src/org/chromium/base/annotations/VerifiesOnR.java",
       "android/java/src/org/chromium/base/compat/ApiHelperForM.java",
       "android/java/src/org/chromium/base/compat/ApiHelperForN.java",
       "android/java/src/org/chromium/base/compat/ApiHelperForO.java",
diff --git a/base/android/java/src/org/chromium/base/annotations/VerifiesOnR.java b/base/android/java/src/org/chromium/base/annotations/VerifiesOnR.java
new file mode 100644
index 0000000..6274f3d
--- /dev/null
+++ b/base/android/java/src/org/chromium/base/annotations/VerifiesOnR.java
@@ -0,0 +1,21 @@
+// 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.
+
+package org.chromium.base.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * The annotated method or class verifies on R, but not below.
+ *
+ * The annotated method (or methods on the annotated class) are guaranteed to not be inlined by R8
+ * on builds targeted below R. This prevents class verification errors (which results in a very slow
+ * retry-verification-at-runtime) from spreading into other classes on these lower versions.
+ */
+@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.CLASS)
+public @interface VerifiesOnR {}
diff --git a/base/profiler/module_cache_posix.cc b/base/profiler/module_cache_posix.cc
index ca1b77e9..e38facd3 100644
--- a/base/profiler/module_cache_posix.cc
+++ b/base/profiler/module_cache_posix.cc
@@ -55,6 +55,20 @@
   return max_offset;
 }
 
+FilePath GetDebugBasenameForModule(const Dl_info& dl_info) {
+#if defined(OS_ANDROID)
+  // Preferentially identify the library using its soname on Android. Libraries
+  // mapped directly from apks have the apk filename in |dl_info.dli_fname|, and
+  // this doesn't distinguish the particular library.
+  Optional<StringPiece> library_name =
+      debug::ReadElfLibraryName(dl_info.dli_fbase);
+  if (library_name)
+    return FilePath(*library_name);
+#endif
+
+  return FilePath(dl_info.dli_fname).BaseName();
+}
+
 class PosixModule : public ModuleCache::Module {
  public:
   PosixModule(const Dl_info& dl_info);
@@ -79,7 +93,7 @@
 PosixModule::PosixModule(const Dl_info& dl_info)
     : base_address_(reinterpret_cast<uintptr_t>(dl_info.dli_fbase)),
       id_(GetUniqueBuildId(dl_info.dli_fbase)),
-      debug_basename_(FilePath(dl_info.dli_fname).BaseName()),
+      debug_basename_(GetDebugBasenameForModule(dl_info)),
       size_(GetLastExecutableOffset(dl_info.dli_fbase)) {}
 
 }  // namespace
diff --git a/base/profiler/module_cache_unittest.cc b/base/profiler/module_cache_unittest.cc
index 9d8cddb..fc9bee8 100644
--- a/base/profiler/module_cache_unittest.cc
+++ b/base/profiler/module_cache_unittest.cc
@@ -92,6 +92,21 @@
 #define MAYBE_TEST(TestSuite, TestName) TEST(TestSuite, DISABLED_##TestName)
 #endif
 
+MAYBE_TEST(ModuleCacheTest, GetDebugBasename) {
+  ModuleCache cache;
+  const ModuleCache::Module* module =
+      cache.GetModuleForAddress(reinterpret_cast<uintptr_t>(&AFunctionForTest));
+  ASSERT_NE(nullptr, module);
+#if defined(OS_ANDROID)
+  EXPECT_EQ("libbase_unittests__library.so",
+            module->GetDebugBasename().value());
+#elif defined(OS_POSIX)
+  EXPECT_EQ("base_unittests", module->GetDebugBasename().value());
+#elif defined(OS_WIN)
+  EXPECT_EQ(L"base_unittests.exe.pdb", module->GetDebugBasename().value());
+#endif
+}
+
 // Checks that ModuleCache returns the same module instance for
 // addresses within the module.
 MAYBE_TEST(ModuleCacheTest, LookupCodeAddresses) {
diff --git a/build/android/gyp/proguard.py b/build/android/gyp/proguard.py
index 3242e31f..15e4756 100755
--- a/build/android/gyp/proguard.py
+++ b/build/android/gyp/proguard.py
@@ -27,6 +27,7 @@
     (27, 'OMR1'),
     (28, 'P'),
     (29, 'Q'),
+    (30, 'R'),
 ]
 _CHECKDISCARD_RE = re.compile(r'^\s*-checkdiscard[\s\S]*?}', re.MULTILINE)
 _DIRECTIVE_RE = re.compile(r'^\s*-', re.MULTILINE)
diff --git a/build/android/pylib/gtest/gtest_test_instance.py b/build/android/pylib/gtest/gtest_test_instance.py
index 49d4892..b12bca9 100644
--- a/build/android/pylib/gtest/gtest_test_instance.py
+++ b/build/android/pylib/gtest/gtest_test_instance.py
@@ -29,6 +29,9 @@
     'weblayer_browsertests',
 ]
 
+# The max number of tests to run on a shard during the test run.
+MAX_SHARDS = 256
+
 RUN_IN_SUB_THREAD_TEST_SUITES = [
     # Multiprocess tests should be run outside of the main thread.
     'base_unittests',  # file_locking_unittest.cc uses a child process.
@@ -301,6 +304,11 @@
     if args.test_apk_incremental_install_json:
       incremental_part = '_incremental'
 
+    self._test_launcher_batch_limit = MAX_SHARDS
+    if (args.test_launcher_batch_limit
+        and 0 < args.test_launcher_batch_limit < MAX_SHARDS):
+      self._test_launcher_batch_limit = args.test_launcher_batch_limit
+
     apk_path = os.path.join(
         constants.GetOutDirectory(), '%s_apk' % self._suite,
         '%s-debug%s.apk' % (self._suite, incremental_part))
@@ -455,6 +463,10 @@
     return self._test_apk_incremental_install_json
 
   @property
+  def test_launcher_batch_limit(self):
+    return self._test_launcher_batch_limit
+
+  @property
   def total_external_shards(self):
     return self._total_external_shards
 
diff --git a/build/android/pylib/local/device/local_device_gtest_run.py b/build/android/pylib/local/device/local_device_gtest_run.py
index d6da1a41..0ae9a1b 100644
--- a/build/android/pylib/local/device/local_device_gtest_run.py
+++ b/build/android/pylib/local/device/local_device_gtest_run.py
@@ -51,7 +51,6 @@
     'org.chromium.native_test.NativeTestInstrumentationTestRunner'
         '.TestList')
 
-_MAX_SHARD_SIZE = 256
 _SECONDS_TO_NANOS = int(1e9)
 
 # The amount of time a test executable may run before it gets killed.
@@ -487,10 +486,14 @@
     # Delete suspect testcase from tests.
     tests = [test for test in tests if not test in self._crashes]
 
+    batch_size = self._test_instance.test_launcher_batch_limit
+
     for i in xrange(0, device_count):
       unbounded_shard = tests[i::device_count]
-      shards += [unbounded_shard[j:j+_MAX_SHARD_SIZE]
-                 for j in xrange(0, len(unbounded_shard), _MAX_SHARD_SIZE)]
+      shards += [
+          unbounded_shard[j:j + batch_size]
+          for j in xrange(0, len(unbounded_shard), batch_size)
+      ]
     return shards
 
   #override
diff --git a/build/android/test_runner.py b/build/android/test_runner.py
index e4cd353..2d97a24 100755
--- a/build/android/test_runner.py
+++ b/build/android/test_runner.py
@@ -389,6 +389,12 @@
       '--test-apk-incremental-install-json',
       type=os.path.realpath,
       help='Path to install json for the test apk.')
+  parser.add_argument('--test-launcher-batch-limit',
+                      dest='test_launcher_batch_limit',
+                      type=int,
+                      help='The max number of tests to run in a shard. '
+                      'Ignores non-positive ints and those greater than '
+                      'MAX_SHARDS')
   parser.add_argument(
       '-w', '--wait-for-java-debugger', action='store_true',
       help='Wait for java debugger to attach before running any application '
diff --git a/chrome/VERSION b/chrome/VERSION
index 3fdc4ad2..88eb593 100644
--- a/chrome/VERSION
+++ b/chrome/VERSION
@@ -1,4 +1,4 @@
 MAJOR=86
 MINOR=0
-BUILD=4223
+BUILD=4224
 PATCH=0
diff --git a/chrome/android/chrome_java_sources.gni b/chrome/android/chrome_java_sources.gni
index f359f4c..5d310a2 100644
--- a/chrome/android/chrome_java_sources.gni
+++ b/chrome/android/chrome_java_sources.gni
@@ -32,6 +32,7 @@
   "java/src/org/chromium/chrome/browser/ChromeVersionInfo.java",
   "java/src/org/chromium/chrome/browser/ChromeWindow.java",
   "java/src/org/chromium/chrome/browser/DefaultBrowserInfo.java",
+  "java/src/org/chromium/chrome/browser/DefaultBrowserInfo2.java",
   "java/src/org/chromium/chrome/browser/DeferredStartupHandler.java",
   "java/src/org/chromium/chrome/browser/DelayedScreenLockIntentHandler.java",
   "java/src/org/chromium/chrome/browser/DevToolsServer.java",
diff --git a/chrome/android/expectations/monochrome_public_bundle.proguard_flags.expected b/chrome/android/expectations/monochrome_public_bundle.proguard_flags.expected
index 0600ac76..7b28e62 100644
--- a/chrome/android/expectations/monochrome_public_bundle.proguard_flags.expected
+++ b/chrome/android/expectations/monochrome_public_bundle.proguard_flags.expected
@@ -784,3 +784,13 @@
 -keepclassmembers,allowobfuscation class ** {
   @org.chromium.base.annotations.VerifiesOnQ <methods>;
 }
+-keep @interface org.chromium.base.annotations.VerifiesOnR
+-if @org.chromium.base.annotations.VerifiesOnR class * {
+    *** *(...);
+}
+-keep,allowobfuscation class <1> {
+    *** <2>(...);
+}
+-keepclassmembers,allowobfuscation class ** {
+  @org.chromium.base.annotations.VerifiesOnR <methods>;
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ChromeTabbedActivity.java b/chrome/android/java/src/org/chromium/chrome/browser/ChromeTabbedActivity.java
index 7b83efb..2136442f 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ChromeTabbedActivity.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ChromeTabbedActivity.java
@@ -112,6 +112,7 @@
 import org.chromium.chrome.browser.partnercustomizations.PartnerBrowserCustomizations;
 import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
 import org.chromium.chrome.browser.profiles.Profile;
+import org.chromium.chrome.browser.reengagement.ReengagementNotificationController;
 import org.chromium.chrome.browser.search_engines.SearchEngineChoiceNotification;
 import org.chromium.chrome.browser.suggestions.SuggestionsEventReporterBridge;
 import org.chromium.chrome.browser.suggestions.SuggestionsMetrics;
@@ -1037,15 +1038,19 @@
         //                values of this depending on when it is called after the activity was
         //                shown.
 
-        if (mCallbackController != null) {
-            new OneShotCallback<>(
-                    mTabModelProfileSupplier, mCallbackController.makeCancelable(profile -> {
-                        assert profile != null : "Unexpectedly null profile from TabModel.";
-                        if (profile == null) return;
+        // Temporary safety check to make sure none of this code runs if the feature is
+        // disabled.
+        if (ReengagementNotificationController.isEnabled()) {
+            if (mCallbackController != null) {
+                new OneShotCallback<>(
+                        mTabModelProfileSupplier, mCallbackController.makeCancelable(profile -> {
+                            assert profile != null : "Unexpectedly null profile from TabModel.";
+                            if (profile == null) return;
 
-                        TrackerFactory.getTrackerForProfile(profile).notifyEvent(
-                                EventConstants.STARTED_FROM_MAIN_INTENT);
-                    }));
+                            TrackerFactory.getTrackerForProfile(profile).notifyEvent(
+                                    EventConstants.STARTED_FROM_MAIN_INTENT);
+                        }));
+            }
         }
 
         mMainIntentMetrics.onMainIntentWithNative(
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/DefaultBrowserInfo.java b/chrome/android/java/src/org/chromium/chrome/browser/DefaultBrowserInfo.java
index 526736e7..173a1bd3 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/DefaultBrowserInfo.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/DefaultBrowserInfo.java
@@ -11,22 +11,17 @@
 import android.text.TextUtils;
 
 import androidx.annotation.IntDef;
-import androidx.annotation.VisibleForTesting;
 
 import org.chromium.base.BuildInfo;
-import org.chromium.base.Callback;
 import org.chromium.base.ContextUtils;
-import org.chromium.base.ObserverList;
 import org.chromium.base.PackageManagerUtils;
-import org.chromium.base.ThreadUtils;
 import org.chromium.base.metrics.RecordHistogram;
 import org.chromium.base.task.AsyncTask;
 import org.chromium.base.task.BackgroundOnlyAsyncTask;
-import org.chromium.base.task.PostTask;
 import org.chromium.chrome.R;
 import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
 import org.chromium.chrome.browser.preferences.SharedPreferencesManager;
-import org.chromium.content_public.browser.UiThreadTaskTraits;
+import org.chromium.content_public.browser.BrowserStartupController;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -36,10 +31,10 @@
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.RejectedExecutionException;
-import java.util.concurrent.atomic.AtomicReference;
 
 /**
- * A utility class for querying information about the default browser setting.
+ * A utility class for querying information about the default browser setting.\
+ * TODO(crbug.com/1112519): Remove this and replace with DefaultBrowserInfo2.
  */
 public final class DefaultBrowserInfo {
     /**
@@ -62,40 +57,19 @@
         int NUM_ENTRIES = 5;
     }
 
-    /** Contains all status related to the default browser state on the device. */
-    public static class DefaultInfo {
-        /** Whether or not Chrome is the system browser. */
-        public final boolean isChromeSystem;
-
-        /** Whether or not Chrome is the default browser. */
-        public final boolean isChromeDefault;
-
-        /** Whether or not the default browser is the system browser. */
-        public final boolean isDefaultSystem;
-
-        /** Whether or not the user has set a default browser. */
-        public final boolean hasDefault;
-
-        /** The number of browsers installed on this device. */
-        public final int browserCount;
-
-        /** The number of system browsers installed on this device. */
-        public final int systemCount;
-
-        /** Creates an instance of the {@link DefaultInfo} class. */
-        public DefaultInfo(boolean isChromeSystem, boolean isChromeDefault, boolean isDefaultSystem,
-                boolean hasDefault, int browserCount, int systemCount) {
-            this.isChromeSystem = isChromeSystem;
-            this.isChromeDefault = isChromeDefault;
-            this.isDefaultSystem = isDefaultSystem;
-            this.hasDefault = hasDefault;
-            this.browserCount = browserCount;
-            this.systemCount = systemCount;
-        }
+    /**
+     * Helper class for passing information about the system's default browser settings back from a
+     * worker task.
+     */
+    private static class DefaultInfo {
+        public boolean isChromeSystem;
+        public boolean isChromeDefault;
+        public boolean isDefaultSystem;
+        public boolean hasDefault;
+        public int browserCount;
+        public int systemCount;
     }
 
-    private static DefaultInfoTask sDefaultInfoTask;
-
     /** A lock to synchronize background tasks to retrieve browser information. */
     private static final Object sDirCreationLock = new Object();
 
@@ -108,8 +82,6 @@
      * Initialize an AsyncTask for getting menu title of opening a link in default browser.
      */
     public static void initBrowserFetcher() {
-        // TODO(crbug.com/1107527): Make this depend on getDefaultBrowserInfo instead of rolling
-        //  it's own AsyncTask.  Potentially update DefaultInfo to include extra data if necessary.
         synchronized (sDirCreationLock) {
             if (sDefaultBrowserFetcher == null) {
                 sDefaultBrowserFetcher = new BackgroundOnlyAsyncTask<ArrayList<String>>() {
@@ -127,7 +99,7 @@
                         // Caches whether Chrome is set as a default browser on the device.
                         boolean isDefault = info != null && info.match != 0
                                 && TextUtils.equals(
-                                           context.getPackageName(), info.activityInfo.packageName);
+                                        context.getPackageName(), info.activityInfo.packageName);
                         SharedPreferencesManager.getInstance().writeBoolean(
                                 ChromePreferenceKeys.CHROME_DEFAULT_BROWSER, isDefault);
 
@@ -172,146 +144,61 @@
     }
 
     /**
-     * Determines various information about browsers on the system.
-     * @param callback To be called with a {@link DefaultInfo} instance if possible.  Can be {@code
-     *         null}.
-     * @see DefaultInfo
-     */
-    public static void getDefaultBrowserInfo(Callback<DefaultInfo> callback) {
-        ThreadUtils.checkUiThread();
-        if (sDefaultInfoTask == null) sDefaultInfoTask = new DefaultInfoTask();
-        sDefaultInfoTask.get(callback);
-    }
-
-    /**
      * Log statistics about the current default browser to UMA.
      */
     public static void logDefaultBrowserStats() {
-        getDefaultBrowserInfo(info -> {
-            if (info == null) return;
+        assert BrowserStartupController.getInstance().isFullBrowserStarted();
 
-            RecordHistogram.recordCount100Histogram(
-                    getSystemBrowserCountUmaName(info), info.systemCount);
-            RecordHistogram.recordCount100Histogram(
-                    getDefaultBrowserCountUmaName(info), info.browserCount);
-            RecordHistogram.recordEnumeratedHistogram("Mobile.DefaultBrowser.State",
-                    getDefaultBrowserUmaState(info), MobileDefaultBrowserState.NUM_ENTRIES);
-        });
-    }
+        try {
+            new AsyncTask<DefaultInfo>() {
+                @Override
+                protected DefaultInfo doInBackground() {
+                    Context context = ContextUtils.getApplicationContext();
 
-    @VisibleForTesting
-    public static void setDefaultInfoForTests(DefaultInfo info) {
-        DefaultInfoTask.setDefaultInfoForTests(info);
-    }
+                    DefaultInfo info = new DefaultInfo();
 
-    @VisibleForTesting
-    public static void clearDefaultInfoForTests() {
-        DefaultInfoTask.clearDefaultInfoForTests();
-    }
-
-    private static class DefaultInfoTask extends AsyncTask<DefaultInfo> {
-        private static AtomicReference<DefaultInfo> sTestInfo;
-
-        private final ObserverList<Callback<DefaultInfo>> mObservers = new ObserverList<>();
-
-        @VisibleForTesting
-        public static void setDefaultInfoForTests(DefaultInfo info) {
-            sTestInfo = new AtomicReference<DefaultInfo>(info);
-        }
-
-        public static void clearDefaultInfoForTests() {
-            sTestInfo = null;
-        }
-
-        /**
-         *  Queues up {@code callback} to be notified of the result of this {@link AsyncTask}.  If
-         *  the task has not been started, this will start it.  If the task is finished, this will
-         *  send the result.  If the task is running this will queue the callback up until the task
-         *  is done.
-         *
-         * @param callback The {@link Callback} to notify with the right {@link DefaultInfo}.
-         */
-        public void get(Callback<DefaultInfo> callback) {
-            ThreadUtils.checkUiThread();
-
-            if (getStatus() == Status.FINISHED) {
-                DefaultInfo info = null;
-                try {
-                    info = sTestInfo == null ? get() : sTestInfo.get();
-                } catch (InterruptedException | ExecutionException e) {
-                    // Fail silently here since this is not a critical task.
-                }
-
-                final DefaultInfo postInfo = info;
-                PostTask.postTask(UiThreadTaskTraits.DEFAULT, () -> callback.onResult(postInfo));
-            } else {
-                if (getStatus() == Status.PENDING) {
-                    try {
-                        executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
-                    } catch (RejectedExecutionException e) {
-                        // Fail silently here since this is not a critical task.
-                        PostTask.postTask(
-                                UiThreadTaskTraits.DEFAULT, () -> callback.onResult(null));
-                        return;
+                    // Query the default handler first.
+                    ResolveInfo defaultRi = PackageManagerUtils.resolveDefaultWebBrowserActivity();
+                    if (defaultRi != null && defaultRi.match != 0) {
+                        info.hasDefault = true;
+                        info.isChromeDefault = isSamePackage(context, defaultRi);
+                        info.isDefaultSystem = isSystemPackage(defaultRi);
                     }
-                }
-                mObservers.addObserver(callback);
-            }
-        }
 
-        @Override
-        protected DefaultInfo doInBackground() {
-            Context context = ContextUtils.getApplicationContext();
+                    // Query all other intent handlers.
+                    Set<String> uniquePackages = new HashSet<>();
+                    List<ResolveInfo> ris = PackageManagerUtils.queryAllWebBrowsersInfo();
+                    if (ris != null) {
+                        for (ResolveInfo ri : ris) {
+                            String packageName = ri.activityInfo.applicationInfo.packageName;
+                            if (!uniquePackages.add(packageName)) continue;
 
-            boolean isChromeSystem = false;
-            boolean isChromeDefault = false;
-            boolean isDefaultSystem = false;
-            boolean hasDefault = false;
-            int browserCount = 0;
-            int systemCount = 0;
-
-            // Query the default handler first.
-            ResolveInfo defaultRi = PackageManagerUtils.resolveDefaultWebBrowserActivity();
-            if (defaultRi != null && defaultRi.match != 0) {
-                hasDefault = true;
-                isChromeDefault = isSamePackage(context, defaultRi);
-                isDefaultSystem = isSystemPackage(defaultRi);
-            }
-
-            // Query all other intent handlers.
-            Set<String> uniquePackages = new HashSet<>();
-            List<ResolveInfo> ris = PackageManagerUtils.queryAllWebBrowsersInfo();
-            if (ris != null) {
-                for (ResolveInfo ri : ris) {
-                    String packageName = ri.activityInfo.applicationInfo.packageName;
-                    if (!uniquePackages.add(packageName)) continue;
-
-                    if (isSystemPackage(ri)) {
-                        if (isSamePackage(context, ri)) isChromeSystem = true;
-                        systemCount++;
+                            if (isSystemPackage(ri)) {
+                                if (isSamePackage(context, ri)) info.isChromeSystem = true;
+                                info.systemCount++;
+                            }
+                        }
                     }
+
+                    info.browserCount = uniquePackages.size();
+
+                    return info;
                 }
-            }
 
-            browserCount = uniquePackages.size();
+                @Override
+                protected void onPostExecute(DefaultInfo info) {
+                    if (info == null) return;
 
-            return new DefaultInfo(isChromeSystem, isChromeDefault, isDefaultSystem, hasDefault,
-                    browserCount, systemCount);
-        }
-
-        @Override
-        protected void onPostExecute(DefaultInfo defaultInfo) {
-            flushCallbacks(sTestInfo == null ? defaultInfo : sTestInfo.get());
-        }
-
-        @Override
-        protected void onCancelled() {
-            flushCallbacks(null);
-        }
-
-        private void flushCallbacks(DefaultInfo info) {
-            for (Callback<DefaultInfo> callback : mObservers) callback.onResult(info);
-            mObservers.clear();
+                    RecordHistogram.recordCount100Histogram(
+                            getSystemBrowserCountUmaName(info), info.systemCount);
+                    RecordHistogram.recordCount100Histogram(
+                            getDefaultBrowserCountUmaName(info), info.browserCount);
+                    RecordHistogram.recordEnumeratedHistogram("Mobile.DefaultBrowser.State",
+                            getDefaultBrowserUmaState(info), MobileDefaultBrowserState.NUM_ENTRIES);
+                }
+            }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+        } catch (RejectedExecutionException ex) {
+            // Fail silently here since this is not a critical task.
         }
     }
 
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/DefaultBrowserInfo2.java b/chrome/android/java/src/org/chromium/chrome/browser/DefaultBrowserInfo2.java
new file mode 100644
index 0000000..f3a2462
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/DefaultBrowserInfo2.java
@@ -0,0 +1,208 @@
+// 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.
+
+package org.chromium.chrome.browser;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.ResolveInfo;
+import android.text.TextUtils;
+
+import androidx.annotation.VisibleForTesting;
+
+import org.chromium.base.Callback;
+import org.chromium.base.ContextUtils;
+import org.chromium.base.ObserverList;
+import org.chromium.base.PackageManagerUtils;
+import org.chromium.base.ThreadUtils;
+import org.chromium.base.task.AsyncTask;
+import org.chromium.base.task.PostTask;
+import org.chromium.content_public.browser.UiThreadTaskTraits;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * A utility class for querying information about the default browser setting.
+ * TODO(crbug.com/1112519): Remove DefaultBrowserInfo and replace with this.
+ */
+public final class DefaultBrowserInfo2 {
+    /** Contains all status related to the default browser state on the device. */
+    public static class DefaultInfo {
+        /** Whether or not Chrome is the system browser. */
+        public final boolean isChromeSystem;
+
+        /** Whether or not Chrome is the default browser. */
+        public final boolean isChromeDefault;
+
+        /** Whether or not the default browser is the system browser. */
+        public final boolean isDefaultSystem;
+
+        /** Whether or not the user has set a default browser. */
+        public final boolean hasDefault;
+
+        /** The number of browsers installed on this device. */
+        public final int browserCount;
+
+        /** The number of system browsers installed on this device. */
+        public final int systemCount;
+
+        /** Creates an instance of the {@link DefaultInfo} class. */
+        public DefaultInfo(boolean isChromeSystem, boolean isChromeDefault, boolean isDefaultSystem,
+                boolean hasDefault, int browserCount, int systemCount) {
+            this.isChromeSystem = isChromeSystem;
+            this.isChromeDefault = isChromeDefault;
+            this.isDefaultSystem = isDefaultSystem;
+            this.hasDefault = hasDefault;
+            this.browserCount = browserCount;
+            this.systemCount = systemCount;
+        }
+    }
+
+    private static DefaultInfoTask sDefaultInfoTask;
+
+    /** Don't instantiate me. */
+    private DefaultBrowserInfo2() {}
+
+    /**
+     * Determines various information about browsers on the system.
+     * @param callback To be called with a {@link DefaultInfo} instance if possible.  Can be {@code
+     *         null}.
+     * @see DefaultInfo
+     */
+    public static void getDefaultBrowserInfo(Callback<DefaultInfo> callback) {
+        ThreadUtils.checkUiThread();
+        if (sDefaultInfoTask == null) sDefaultInfoTask = new DefaultInfoTask();
+        sDefaultInfoTask.get(callback);
+    }
+
+    @VisibleForTesting
+    public static void setDefaultInfoForTests(DefaultInfo info) {
+        DefaultInfoTask.setDefaultInfoForTests(info);
+    }
+
+    @VisibleForTesting
+    public static void clearDefaultInfoForTests() {
+        DefaultInfoTask.clearDefaultInfoForTests();
+    }
+
+    private static class DefaultInfoTask extends AsyncTask<DefaultInfo> {
+        private static AtomicReference<DefaultInfo> sTestInfo;
+
+        private final ObserverList<Callback<DefaultInfo>> mObservers = new ObserverList<>();
+
+        @VisibleForTesting
+        public static void setDefaultInfoForTests(DefaultInfo info) {
+            sTestInfo = new AtomicReference<DefaultInfo>(info);
+        }
+
+        public static void clearDefaultInfoForTests() {
+            sTestInfo = null;
+        }
+
+        /**
+         *  Queues up {@code callback} to be notified of the result of this {@link AsyncTask}.  If
+         *  the task has not been started, this will start it.  If the task is finished, this will
+         *  send the result.  If the task is running this will queue the callback up until the task
+         *  is done.
+         *
+         * @param callback The {@link Callback} to notify with the right {@link DefaultInfo}.
+         */
+        public void get(Callback<DefaultInfo> callback) {
+            ThreadUtils.checkUiThread();
+
+            if (getStatus() == Status.FINISHED) {
+                DefaultInfo info = null;
+                try {
+                    info = sTestInfo == null ? get() : sTestInfo.get();
+                } catch (InterruptedException | ExecutionException e) {
+                    // Fail silently here since this is not a critical task.
+                }
+
+                final DefaultInfo postInfo = info;
+                PostTask.postTask(UiThreadTaskTraits.DEFAULT, () -> callback.onResult(postInfo));
+            } else {
+                if (getStatus() == Status.PENDING) {
+                    try {
+                        executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+                    } catch (RejectedExecutionException e) {
+                        // Fail silently here since this is not a critical task.
+                        PostTask.postTask(
+                                UiThreadTaskTraits.DEFAULT, () -> callback.onResult(null));
+                        return;
+                    }
+                }
+                mObservers.addObserver(callback);
+            }
+        }
+
+        @Override
+        protected DefaultInfo doInBackground() {
+            Context context = ContextUtils.getApplicationContext();
+
+            boolean isChromeSystem = false;
+            boolean isChromeDefault = false;
+            boolean isDefaultSystem = false;
+            boolean hasDefault = false;
+            int browserCount = 0;
+            int systemCount = 0;
+
+            // Query the default handler first.
+            ResolveInfo defaultRi = PackageManagerUtils.resolveDefaultWebBrowserActivity();
+            if (defaultRi != null && defaultRi.match != 0) {
+                hasDefault = true;
+                isChromeDefault = isSamePackage(context, defaultRi);
+                isDefaultSystem = isSystemPackage(defaultRi);
+            }
+
+            // Query all other intent handlers.
+            Set<String> uniquePackages = new HashSet<>();
+            List<ResolveInfo> ris = PackageManagerUtils.queryAllWebBrowsersInfo();
+            if (ris != null) {
+                for (ResolveInfo ri : ris) {
+                    String packageName = ri.activityInfo.applicationInfo.packageName;
+                    if (!uniquePackages.add(packageName)) continue;
+
+                    if (isSystemPackage(ri)) {
+                        if (isSamePackage(context, ri)) isChromeSystem = true;
+                        systemCount++;
+                    }
+                }
+            }
+
+            browserCount = uniquePackages.size();
+
+            return new DefaultInfo(isChromeSystem, isChromeDefault, isDefaultSystem, hasDefault,
+                    browserCount, systemCount);
+        }
+
+        @Override
+        protected void onPostExecute(DefaultInfo defaultInfo) {
+            flushCallbacks(sTestInfo == null ? defaultInfo : sTestInfo.get());
+        }
+
+        @Override
+        protected void onCancelled() {
+            flushCallbacks(null);
+        }
+
+        private void flushCallbacks(DefaultInfo info) {
+            for (Callback<DefaultInfo> callback : mObservers) callback.onResult(info);
+            mObservers.clear();
+        }
+    }
+
+    private static boolean isSamePackage(Context context, ResolveInfo info) {
+        return TextUtils.equals(
+                context.getPackageName(), info.activityInfo.applicationInfo.packageName);
+    }
+
+    private static boolean isSystemPackage(ResolveInfo info) {
+        return (info.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
+    }
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/reengagement/ReengagementNotificationController.java b/chrome/android/java/src/org/chromium/chrome/browser/reengagement/ReengagementNotificationController.java
index db28e8027..7b6908a3 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/reengagement/ReengagementNotificationController.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/reengagement/ReengagementNotificationController.java
@@ -15,7 +15,7 @@
 
 import org.chromium.base.Callback;
 import org.chromium.chrome.R;
-import org.chromium.chrome.browser.DefaultBrowserInfo;
+import org.chromium.chrome.browser.DefaultBrowserInfo2;
 import org.chromium.chrome.browser.flags.ChromeFeatureList;
 import org.chromium.chrome.browser.notifications.NotificationUmaTracker;
 import org.chromium.chrome.browser.notifications.NotificationUmaTracker.SystemNotificationType;
@@ -79,8 +79,8 @@
     }
 
     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-    protected void getDefaultBrowserInfo(Callback<DefaultBrowserInfo.DefaultInfo> callback) {
-        DefaultBrowserInfo.getDefaultBrowserInfo(callback);
+    protected void getDefaultBrowserInfo(Callback<DefaultBrowserInfo2.DefaultInfo> callback) {
+        DefaultBrowserInfo2.getDefaultBrowserInfo(callback);
     }
 
     private boolean showNotification(String feature) {
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/app/appmenu/OWNERS b/chrome/android/javatests/src/org/chromium/chrome/browser/app/appmenu/OWNERS
index 4e1c69c..33e91e60 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/app/appmenu/OWNERS
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/app/appmenu/OWNERS
@@ -1 +1 @@
-file://chrome/browser/ui/android/appmenu/OWNERS
+file://chrome/android/java/src/org/chromium/chrome/browser/app/appmenu/OWNERS
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/page_info/PageInfoViewTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/page_info/PageInfoViewTest.java
index cf4f1fcf..44df2a0 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/page_info/PageInfoViewTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/page_info/PageInfoViewTest.java
@@ -25,6 +25,7 @@
 import org.junit.runner.RunWith;
 
 import org.chromium.base.test.util.CommandLineFlags;
+import org.chromium.base.test.util.DisabledTest;
 import org.chromium.base.test.util.Feature;
 import org.chromium.chrome.R;
 import org.chromium.chrome.browser.ChromeActivity;
@@ -61,6 +62,7 @@
         ContentSwitches.HOST_RESOLVER_RULES + "=MAP * 127.0.0.1"})
 @Features.
 EnableFeatures(ContentSettingsFeatureList.IMPROVED_COOKIE_CONTROLS_FOR_THIRD_PARTY_COOKIE_BLOCKING)
+@DisabledTest(message = "crbug.com/1112985")
 public class PageInfoViewTest {
     @Rule
     public ChromeActivityTestRule<ChromeActivity> mActivityTestRule =
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/reengagement/ReengagementNotificationControllerIntegrationTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/reengagement/ReengagementNotificationControllerIntegrationTest.java
index 52e2e0d..2b0cd07 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/reengagement/ReengagementNotificationControllerIntegrationTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/reengagement/ReengagementNotificationControllerIntegrationTest.java
@@ -37,9 +37,10 @@
 import org.chromium.base.FeatureList;
 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.UrlUtils;
 import org.chromium.chrome.R;
-import org.chromium.chrome.browser.DefaultBrowserInfo;
+import org.chromium.chrome.browser.DefaultBrowserInfo2;
 import org.chromium.chrome.browser.app.reengagement.ReengagementActivity;
 import org.chromium.chrome.browser.customtabs.CustomTabActivityTestRule;
 import org.chromium.chrome.browser.customtabs.CustomTabsTestUtils;
@@ -92,7 +93,7 @@
     @After
     public void tearDown() {
         TrackerFactory.setTrackerForTests(null);
-        DefaultBrowserInfo.clearDefaultInfoForTests();
+        DefaultBrowserInfo2.clearDefaultInfoForTests();
         FeatureList.resetTestCanUseDefaultsForTesting();
         FeatureList.setTestFeatures(null);
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) closeReengagementNotifications();
@@ -101,7 +102,7 @@
     @Test
     @MediumTest
     public void testReengagementNotificationSent() {
-        DefaultBrowserInfo.setDefaultInfoForTests(
+        DefaultBrowserInfo2.setDefaultInfoForTests(
                 createDefaultInfo(/* passesPrecondition = */ true));
         doReturn(true).when(mTracker).shouldTriggerHelpUI(
                 FeatureConstants.CHROME_REENGAGEMENT_NOTIFICATION_1_FEATURE);
@@ -121,7 +122,7 @@
     @Test
     @MediumTest
     public void testReengagementDifferentNotificationSent() {
-        DefaultBrowserInfo.setDefaultInfoForTests(
+        DefaultBrowserInfo2.setDefaultInfoForTests(
                 createDefaultInfo(/* passesPrecondition = */ true));
         doReturn(true).when(mTracker).shouldTriggerHelpUI(
                 FeatureConstants.CHROME_REENGAGEMENT_NOTIFICATION_2_FEATURE);
@@ -141,7 +142,7 @@
     @Test
     @MediumTest
     public void testReengagementNotificationNotSentDueToIPH() {
-        DefaultBrowserInfo.setDefaultInfoForTests(
+        DefaultBrowserInfo2.setDefaultInfoForTests(
                 createDefaultInfo(/* passesPrecondition = */ true));
         mCustomTabActivityTestRule.startCustomTabActivityWithIntent(
                 CustomTabsTestUtils.createMinimalCustomTabIntent(
@@ -165,7 +166,7 @@
     @Test
     @MediumTest
     public void testReengagementNotificationNotSentDueToPreconditions() {
-        DefaultBrowserInfo.setDefaultInfoForTests(
+        DefaultBrowserInfo2.setDefaultInfoForTests(
                 createDefaultInfo(/* passesPrecondition = */ false));
         mCustomTabActivityTestRule.startCustomTabActivityWithIntent(
                 CustomTabsTestUtils.createMinimalCustomTabIntent(
@@ -189,7 +190,7 @@
     @Test
     @MediumTest
     public void testReengagementNotificationNotSentDueToUnavailablePreconditions() {
-        DefaultBrowserInfo.setDefaultInfoForTests(null);
+        DefaultBrowserInfo2.setDefaultInfoForTests(null);
         mCustomTabActivityTestRule.startCustomTabActivityWithIntent(
                 CustomTabsTestUtils.createMinimalCustomTabIntent(
                         InstrumentationRegistry.getTargetContext(),
@@ -228,6 +229,7 @@
 
     @Test
     @SmallTest
+    @DisabledTest(message = "crbug.com/1112519 - Disabled while safety guard is in place.")
     public void testEngagementTrackedWhenDisabled() {
         setReengagementNotificationEnabled(false);
         mTabbedActivityTestRule.startMainActivityFromLauncher();
@@ -246,7 +248,7 @@
     @MediumTest
     public void testEngagementNotificationNotSentDueToDisabled() {
         setReengagementNotificationEnabled(false);
-        DefaultBrowserInfo.setDefaultInfoForTests(
+        DefaultBrowserInfo2.setDefaultInfoForTests(
                 createDefaultInfo(/* passesPrecondition = */ true));
         mCustomTabActivityTestRule.startCustomTabActivityWithIntent(
                 CustomTabsTestUtils.createMinimalCustomTabIntent(
@@ -361,9 +363,9 @@
                         ReengagementNotificationController.NOTIFICATION_ID);
     }
 
-    private DefaultBrowserInfo.DefaultInfo createDefaultInfo(boolean passesPrecondition) {
+    private DefaultBrowserInfo2.DefaultInfo createDefaultInfo(boolean passesPrecondition) {
         int browserCount = passesPrecondition ? 2 : 1;
-        return new DefaultBrowserInfo.DefaultInfo(/* isChromeSystem = */ true,
+        return new DefaultBrowserInfo2.DefaultInfo(/* isChromeSystem = */ true,
                 /* isChromeDefault = */ true,
                 /* isDefaultSystem = */ true, /* hasDefault = */ true, browserCount,
                 /* systemCount = */ 0);
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/app/appmenu/OWNERS b/chrome/android/junit/src/org/chromium/chrome/browser/app/appmenu/OWNERS
new file mode 100644
index 0000000..33e91e60
--- /dev/null
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/app/appmenu/OWNERS
@@ -0,0 +1 @@
+file://chrome/android/java/src/org/chromium/chrome/browser/app/appmenu/OWNERS
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/reengagement/ReengagementNotificationControllerTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/reengagement/ReengagementNotificationControllerTest.java
index 52dfba61..058e8b2a 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/reengagement/ReengagementNotificationControllerTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/reengagement/ReengagementNotificationControllerTest.java
@@ -36,7 +36,7 @@
 import org.chromium.base.metrics.test.ShadowRecordHistogram;
 import org.chromium.base.test.BaseRobolectricTestRunner;
 import org.chromium.chrome.R;
-import org.chromium.chrome.browser.DefaultBrowserInfo;
+import org.chromium.chrome.browser.DefaultBrowserInfo2;
 import org.chromium.chrome.browser.flags.ChromeFeatureList;
 import org.chromium.chrome.browser.notifications.NotificationUmaTracker;
 import org.chromium.components.feature_engagement.FeatureConstants;
@@ -59,15 +59,15 @@
 
     private class TestingReengagementNotificationController
             extends ReengagementNotificationController {
-        private DefaultBrowserInfo.DefaultInfo mInfo;
+        private DefaultBrowserInfo2.DefaultInfo mInfo;
 
-        TestingReengagementNotificationController(DefaultBrowserInfo.DefaultInfo info) {
+        TestingReengagementNotificationController(DefaultBrowserInfo2.DefaultInfo info) {
             super(mContext, mTracker, Activity.class);
             mInfo = info;
         }
 
         @Override
-        protected void getDefaultBrowserInfo(Callback<DefaultBrowserInfo.DefaultInfo> callback) {
+        protected void getDefaultBrowserInfo(Callback<DefaultBrowserInfo2.DefaultInfo> callback) {
             new Handler().post(() -> callback.onResult(mInfo));
         }
     }
@@ -269,9 +269,9 @@
         return null;
     }
 
-    private DefaultBrowserInfo.DefaultInfo createDefaultInfo(boolean passesPrecondition) {
+    private DefaultBrowserInfo2.DefaultInfo createDefaultInfo(boolean passesPrecondition) {
         int browserCount = passesPrecondition ? 2 : 1;
-        return new DefaultBrowserInfo.DefaultInfo(/* isChromeSystem = */ true,
+        return new DefaultBrowserInfo2.DefaultInfo(/* isChromeSystem = */ true,
                 /* isChromeDefault = */ true,
                 /* isDefaultSystem = */ true, /* hasDefault = */ true, browserCount,
                 /* systemCount = */ 0);
diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
index 58bc8f5..4ebfb5e1 100644
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -3176,6 +3176,10 @@
      flag_descriptions::kAndroidPictureInPictureAPIName,
      flag_descriptions::kAndroidPictureInPictureAPIDescription, kOsAndroid,
      FEATURE_VALUE_TYPE(media::kPictureInPictureAPI)},
+    {"reengagement-notification",
+     flag_descriptions::kReengagementNotificationName,
+     flag_descriptions::kReengagementNotificationDescription, kOsAndroid,
+     FEATURE_VALUE_TYPE(chrome::android::kReengagementNotification)},
 #endif  // OS_ANDROID
     {"disallow-doc-written-script-loads",
      flag_descriptions::kDisallowDocWrittenScriptsUiName,
@@ -5102,6 +5106,10 @@
      flag_descriptions::kDisplayAlignmentAssistanceName,
      flag_descriptions::kDisplayAlignmentAssistanceDescription, kOsCrOS,
      FEATURE_VALUE_TYPE(ash::features::kDisplayAlignAssist)},
+
+    {"print-save-to-drive", flag_descriptions::kPrintSaveToDriveName,
+     flag_descriptions::kPrintSaveToDriveDescription, kOsCrOS,
+     FEATURE_VALUE_TYPE(chromeos::features::kPrintSaveToDrive)},
 #endif  // OS_CHROMEOS
 
     {"autofill-off-no-server-data",
diff --git a/chrome/browser/android/vr/arcore_device/ar_image_transport.cc b/chrome/browser/android/vr/arcore_device/ar_image_transport.cc
index f607441a..e22108c 100644
--- a/chrome/browser/android/vr/arcore_device/ar_image_transport.cc
+++ b/chrome/browser/android/vr/arcore_device/ar_image_transport.cc
@@ -172,7 +172,8 @@
   buffer->mailbox_holder = mailbox_bridge_->CreateSharedImage(
       buffer->gmb.get(), gfx::ColorSpace(), shared_image_usage);
   DVLOG(2) << ": CreateSharedImage, mailbox="
-           << buffer->mailbox_holder.mailbox.ToDebugString();
+           << buffer->mailbox_holder.mailbox.ToDebugString() << ", SyncToken="
+           << buffer->mailbox_holder.sync_token.ToDebugString();
 
   auto img = base::MakeRefCounted<gl::GLImageAHardwareBuffer>(size);
 
@@ -230,6 +231,8 @@
   // that it's transitioned through "processing" and "rendering" states back
   // to "animating".
   DCHECK(shared_buffer->mailbox_holder.sync_token.HasData());
+  DVLOG(2) << ": SyncToken="
+           << shared_buffer->mailbox_holder.sync_token.ToDebugString();
 
   return shared_buffer->mailbox_holder;
 }
diff --git a/chrome/browser/autofill/autofill_interactive_uitest.cc b/chrome/browser/autofill/autofill_interactive_uitest.cc
index ac87aa55..dfd89cc6 100644
--- a/chrome/browser/autofill/autofill_interactive_uitest.cc
+++ b/chrome/browser/autofill/autofill_interactive_uitest.cc
@@ -393,7 +393,8 @@
     ASSERT_TRUE(content::ExecuteScriptAndExtractString(
         GetWebContents(),
         "window.domAutomationController.send("
-        "    document.getElementById('" + field_name + "').value);",
+        "    document.getElementById('" +
+            field_name + "').value);",
         &value));
     EXPECT_EQ(expected_value, value) << "for field " << field_name;
   }
@@ -416,7 +417,7 @@
         GetWebContents(),
         "window.domAutomationController.send("
         "    document.defaultView.getComputedStyle(document.getElementById('" +
-        field_name + "')).backgroundColor);",
+            field_name + "')).backgroundColor);",
         color));
   }
 
@@ -1256,6 +1257,35 @@
   EXPECT_TRUE(test_delegate()->Wait());
 }
 
+// Makes sure that clicking a field while there is no enough height in the
+// content area for at least one suggestion, won't show the autofill popup. This
+// is a regression test for crbug.com/1108181
+IN_PROC_BROWSER_TEST_F(AutofillInteractiveTest,
+                       DontAutofillShowPopupWhenNoEnoughHeightInContentArea) {
+  // This firstname field starts at y=-100px and has a height of 5120px. There
+  // is no enough space to show at least one row of the autofill popup and hence
+  // the autofill shouldn't be shown.
+  static const char kTestFormWithLargeInputField[] =
+      "<form action=\"http://www.example.com/\" method=\"POST\">"
+      "<label for=\"firstname\">First name:</label>"
+      " <input type=\"text\" id=\"firstname\" style=\"position:fixed; "
+      "top:-100px;height:5120px\"><br>"
+      "<label for=\"lastname\">Last name:</label>"
+      " <input type=\"text\" id=\"lastname\"><br>"
+      "<label for=\"city\">City:</label>"
+      " <input type=\"text\" id=\"city\"><br>"
+      "</form>";
+
+  CreateTestProfile();
+  SetTestUrlResponse(kTestFormWithLargeInputField);
+
+  ui_test_utils::NavigateToURL(browser(), GetTestUrl());
+
+  FocusFirstNameField();
+  SendKeyToPage(GetWebContents(), ui::DomKey::ARROW_DOWN);
+  ASSERT_NO_FATAL_FAILURE(MakeSurePopupDoesntAppear());
+}
+
 // Test that a field is still autofillable after the previously autofilled
 // value is deleted.
 IN_PROC_BROWSER_TEST_F(AutofillInteractiveTest, OnDeleteValueAfterAutofill) {
@@ -2156,7 +2186,7 @@
   PopulateForm("NAME_FIRST");
 
   ASSERT_TRUE(content::ExecuteScript(
-       GetWebContents(), "document.getElementById('testform').reset()"));
+      GetWebContents(), "document.getElementById('testform').reset()"));
 
   PopulateForm("NAME_FIRST");
 
@@ -3379,7 +3409,7 @@
   // The fields that were initially filled and not reset should still be filled.
   ExpectFieldValue("firstname", "");  // That field value was reset dynamically.
   ExpectFieldValue("address1", "4120 Freidrich Lane");
-  ExpectFieldValue("state", "CA");   // Default value.
+  ExpectFieldValue("state", "CA");  // Default value.
   ExpectFieldValue("city", "Austin");
   ExpectFieldValue("company", company_name_enabled_ ? "Initech" : "");
   ExpectFieldValue("email", "red.swingline@initech.com");
diff --git a/chrome/browser/chrome_browser_interface_binders.cc b/chrome/browser/chrome_browser_interface_binders.cc
index e2afbb6c..3074a26 100644
--- a/chrome/browser/chrome_browser_interface_binders.cc
+++ b/chrome/browser/chrome_browser_interface_binders.cc
@@ -110,6 +110,7 @@
 #include "chrome/browser/media/kaleidoscope/kaleidoscope_ui.h"
 #include "chrome/browser/media/kaleidoscope/mojom/kaleidoscope.mojom.h"
 #include "chrome/browser/payments/payment_request_factory.h"
+#include "chrome/browser/promo_browser_command/promo_browser_command.mojom.h"
 #include "chrome/browser/speech/speech_recognition_service.h"
 #include "chrome/browser/speech/speech_recognition_service_factory.h"
 #include "chrome/browser/ui/webui/downloads/downloads.mojom.h"
@@ -524,6 +525,9 @@
   RegisterWebUIControllerInterfaceBinder<
       new_tab_page::mojom::PageHandlerFactory, NewTabPageUI>(map);
 
+  RegisterWebUIControllerInterfaceBinder<
+      promo_browser_command::mojom::CommandHandler, NewTabPageUI>(map);
+
   RegisterWebUIControllerInterfaceBinder<media_feeds::mojom::MediaFeedsStore,
                                          MediaFeedsUI>(map);
 
diff --git a/chrome/browser/chromeos/arc/session/arc_session_manager.cc b/chrome/browser/chromeos/arc/session/arc_session_manager.cc
index 53bb6ca..1c777ab 100644
--- a/chrome/browser/chromeos/arc/session/arc_session_manager.cc
+++ b/chrome/browser/chromeos/arc/session/arc_session_manager.cc
@@ -327,7 +327,7 @@
           SIGN_IN_CLOUD_PROVISION_FLOW_ACCOUNT_MISSING_ERROR;
 
     case mojom::CloudProvisionFlowError::ERROR_ACCOUNT_NOT_READY:
-    case mojom::CloudProvisionFlowError::ERROR_ACCOUNT_NOT_WHITELISTED:
+    case mojom::CloudProvisionFlowError::ERROR_ACCOUNT_NOT_ALLOWLISTED:
     case mojom::CloudProvisionFlowError::ERROR_DPC_SUPPORT:
     case mojom::CloudProvisionFlowError::ERROR_ENTERPRISE_INVALID:
       return ArcSupportHost::Error::
diff --git a/chrome/browser/chromeos/arc/test/test_arc_session_manager.cc b/chrome/browser/chromeos/arc/test/test_arc_session_manager.cc
index 1eb6a00..a8f42fa2 100644
--- a/chrome/browser/chromeos/arc/test/test_arc_session_manager.cc
+++ b/chrome/browser/chromeos/arc/test/test_arc_session_manager.cc
@@ -22,7 +22,8 @@
   // Create empty prop files so ArcSessionManager's property expansion code
   // works like production.
   for (const char* filename :
-       {"default.prop", "build.prop", "vendor_build.prop"}) {
+       {"default.prop", "build.prop", "vendor_build.prop",
+        "system_ext_build.prop", "product_build.prop", "odm_build.prop"}) {
     if (base::WriteFile(source_dir->Append(filename), "", 1) != 1)
       return false;
   }
diff --git a/chrome/browser/chromeos/extensions/file_manager/private_api_sharesheet.cc b/chrome/browser/chromeos/extensions/file_manager/private_api_sharesheet.cc
index 8605bd8..03096797 100644
--- a/chrome/browser/chromeos/extensions/file_manager/private_api_sharesheet.cc
+++ b/chrome/browser/chromeos/extensions/file_manager/private_api_sharesheet.cc
@@ -13,6 +13,7 @@
 #include "chrome/common/extensions/api/file_manager_private.h"
 #include "chrome/common/extensions/api/file_manager_private_internal.h"
 #include "components/services/app_service/public/cpp/intent_util.h"
+#include "content/public/browser/web_contents.h"
 #include "extensions/browser/api/file_handlers/directory_util.h"
 #include "extensions/browser/api/file_handlers/mime_util.h"
 #include "storage/browser/file_system/file_system_context.h"
@@ -68,11 +69,18 @@
 
 void FileManagerPrivateInternalSharesheetHasTargetsFunction::
     OnMimeTypesCollected(std::unique_ptr<std::vector<std::string>> mime_types) {
-  auto* sharesheet_service =
+  sharesheet::SharesheetService* sharesheet_service =
       sharesheet::SharesheetServiceFactory::GetForProfile(
           chrome_details_.GetProfile());
 
-  bool result;
+  bool result = false;
+
+  if (!sharesheet_service) {
+    LOG(ERROR) << "Couldn't get Sharesheet Service for profile";
+    Respond(ArgumentList(extensions::api::file_manager_private_internal::
+                             SharesheetHasTargets::Results::Create(result)));
+  }
+
   result = sharesheet_service->HasShareTargets(
       apps_util::CreateShareIntentFromFiles(urls_, *mime_types));
 
@@ -80,4 +88,66 @@
                            SharesheetHasTargets::Results::Create(result)));
 }
 
+FileManagerPrivateInternalInvokeSharesheetFunction::
+    FileManagerPrivateInternalInvokeSharesheetFunction()
+    : chrome_details_(this) {}
+
+FileManagerPrivateInternalInvokeSharesheetFunction::
+    ~FileManagerPrivateInternalInvokeSharesheetFunction() = default;
+
+ExtensionFunction::ResponseAction
+FileManagerPrivateInternalInvokeSharesheetFunction::Run() {
+  using extensions::api::file_manager_private_internal::InvokeSharesheet::
+      Params;
+  const std::unique_ptr<Params> params(Params::Create(*args_));
+  EXTENSION_FUNCTION_VALIDATE(params);
+
+  if (params->urls.empty())
+    return RespondNow(Error("No URLs provided"));
+
+  const scoped_refptr<storage::FileSystemContext> file_system_context =
+      file_manager::util::GetFileSystemContextForRenderFrameHost(
+          chrome_details_.GetProfile(), render_frame_host());
+
+  std::vector<storage::FileSystemURL> file_system_urls;
+  // Collect all the URLs, convert them to GURLs, and crack all the urls into
+  // file paths.
+  for (size_t i = 0; i < params->urls.size(); ++i) {
+    const GURL url(params->urls[i]);
+    storage::FileSystemURL file_system_url(file_system_context->CrackURL(url));
+    if (!chromeos::FileSystemBackend::CanHandleURL(file_system_url))
+      continue;
+    urls_.push_back(url);
+    file_system_urls.push_back(file_system_url);
+  }
+
+  mime_type_collector_ =
+      std::make_unique<app_file_handler_util::MimeTypeCollector>(
+          chrome_details_.GetProfile());
+  mime_type_collector_->CollectForURLs(
+      file_system_urls,
+      base::BindOnce(&FileManagerPrivateInternalInvokeSharesheetFunction::
+                         OnMimeTypesCollected,
+                     this));
+
+  return RespondLater();
+}
+
+void FileManagerPrivateInternalInvokeSharesheetFunction::OnMimeTypesCollected(
+    std::unique_ptr<std::vector<std::string>> mime_types) {
+  // On button press show sharesheet bubble.
+  auto* profile = chrome_details_.GetProfile();
+  sharesheet::SharesheetService* sharesheet_service =
+      sharesheet::SharesheetServiceFactory::GetForProfile(profile);
+  if (!sharesheet_service) {
+    Respond(Error("Cannot find sharesheet service"));
+    return;
+  }
+  sharesheet_service->ShowBubble(
+      GetSenderWebContents(),
+      apps_util::CreateShareIntentFromFiles(urls_, *mime_types));
+
+  Respond(NoArguments());
+}
+
 }  // namespace extensions
diff --git a/chrome/browser/chromeos/extensions/file_manager/private_api_sharesheet.h b/chrome/browser/chromeos/extensions/file_manager/private_api_sharesheet.h
index d4dd6c1..4611706c 100644
--- a/chrome/browser/chromeos/extensions/file_manager/private_api_sharesheet.h
+++ b/chrome/browser/chromeos/extensions/file_manager/private_api_sharesheet.h
@@ -47,6 +47,31 @@
   const ChromeExtensionFunctionDetails chrome_details_;
 };
 
+// Implements the chrome.fileManagerPrivateInternal.invokeSharesheet method.
+class FileManagerPrivateInternalInvokeSharesheetFunction
+    : public LoggedExtensionFunction {
+ public:
+  FileManagerPrivateInternalInvokeSharesheetFunction();
+
+  DECLARE_EXTENSION_FUNCTION("fileManagerPrivateInternal.invokeSharesheet",
+                             FILEMANAGERPRIVATEINTERNAL_INVOKESHARESHEET)
+
+ protected:
+  ~FileManagerPrivateInternalInvokeSharesheetFunction() override;
+
+  // ExtensionFunction overrides.
+  ResponseAction Run() override;
+
+ private:
+  void OnMimeTypesCollected(
+      std::unique_ptr<std::vector<std::string>> mime_types);
+
+  std::unique_ptr<app_file_handler_util::MimeTypeCollector>
+      mime_type_collector_;
+  std::vector<GURL> urls_;
+  const ChromeExtensionFunctionDetails chrome_details_;
+};
+
 }  // namespace extensions
 
 #endif  // CHROME_BROWSER_CHROMEOS_EXTENSIONS_FILE_MANAGER_PRIVATE_API_SHARESHEET_H_
diff --git a/chrome/browser/chromeos/file_manager/file_manager_browsertest.cc b/chrome/browser/chromeos/file_manager/file_manager_browsertest.cc
index e4be506..878d0ba 100644
--- a/chrome/browser/chromeos/file_manager/file_manager_browsertest.cc
+++ b/chrome/browser/chromeos/file_manager/file_manager_browsertest.cc
@@ -920,6 +920,7 @@
         TestCase("recentsCrostiniMounted"),
         TestCase("recentsDownloadsAndDrive"),
         TestCase("recentsDownloadsAndDriveWithOverlap"),
+        TestCase("recentsNested"),
         TestCase("recentAudioDownloads").EnableUnifiedMediaView(),
         TestCase("recentAudioDownloadsAndDrive").EnableUnifiedMediaView(),
         TestCase("recentImagesDownloads").EnableUnifiedMediaView(),
diff --git a/chrome/browser/chromeos/web_applications/terminal_source.cc b/chrome/browser/chromeos/web_applications/terminal_source.cc
index 8420679..76aaa2bf 100644
--- a/chrome/browser/chromeos/web_applications/terminal_source.cc
+++ b/chrome/browser/chromeos/web_applications/terminal_source.cc
@@ -97,12 +97,12 @@
   auto* webui_allowlist = WebUIAllowlist::GetOrCreate(profile);
   const url::Origin terminal_origin = url::Origin::Create(GURL(source));
   CHECK(!terminal_origin.opaque());
-  webui_allowlist->RegisterAutoGrantedPermission(
-      terminal_origin, ContentSettingsType::NOTIFICATIONS);
-  webui_allowlist->RegisterAutoGrantedPermission(
-      terminal_origin, ContentSettingsType::CLIPBOARD_READ_WRITE);
-  webui_allowlist->RegisterAutoGrantedPermission(terminal_origin,
-                                                 ContentSettingsType::COOKIES);
+  for (auto permission :
+       {ContentSettingsType::JAVASCRIPT, ContentSettingsType::NOTIFICATIONS,
+        ContentSettingsType::CLIPBOARD_READ_WRITE, ContentSettingsType::COOKIES,
+        ContentSettingsType::IMAGES, ContentSettingsType::SOUND}) {
+    webui_allowlist->RegisterAutoGrantedPermission(terminal_origin, permission);
+  }
 }
 
 TerminalSource::~TerminalSource() = default;
diff --git a/chrome/browser/data_saver/lite_video_browsertest.cc b/chrome/browser/data_saver/lite_video_browsertest.cc
index 3dece8ee..098391c2 100644
--- a/chrome/browser/data_saver/lite_video_browsertest.cc
+++ b/chrome/browser/data_saver/lite_video_browsertest.cc
@@ -8,7 +8,11 @@
 #include "base/test/scoped_feature_list.h"
 #include "build/build_config.h"
 #include "chrome/browser/browser_process.h"
+#include "chrome/browser/lite_video/lite_video_features.h"
+#include "chrome/browser/lite_video/lite_video_hint.h"
+#include "chrome/browser/lite_video/lite_video_navigation_metrics.h"
 #include "chrome/browser/lite_video/lite_video_switches.h"
+#include "chrome/browser/lite_video/lite_video_user_blocklist.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/browser.h"
 #include "chrome/browser/ui/tabs/tab_strip_model.h"
@@ -16,6 +20,7 @@
 #include "chrome/test/base/in_process_browser_test.h"
 #include "chrome/test/base/ui_test_utils.h"
 #include "components/metrics/content/subprocess_metrics_provider.h"
+#include "components/ukm/test_ukm_recorder.h"
 #include "content/public/common/content_features.h"
 #include "content/public/common/content_switches.h"
 #include "content/public/test/browser_test.h"
@@ -23,6 +28,8 @@
 #include "media/base/media_switches.h"
 #include "media/base/test_data_util.h"
 #include "net/test/embedded_test_server/embedded_test_server.h"
+#include "services/metrics/public/cpp/ukm_builders.h"
+#include "services/metrics/public/cpp/ukm_source.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "url/gurl.h"
 #include "url/url_util.h"
@@ -123,15 +130,18 @@
     std::string query = media::GetURLQueryString(query_params);
     content::TitleWatcher title_watcher(
         browser()->tab_strip_model()->GetActiveWebContents(), expected_title);
-    EXPECT_TRUE(ui_test_utils::NavigateToURL(
-        browser(), http_server_.GetURL("/" + html_page + "?" + query)));
+    media_url_ = http_server_.GetURL("/" + html_page + "?" + query);
+    EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), media_url_));
     EXPECT_EQ(expected_title, title_watcher.WaitAndGetTitle());
   }
 
   const base::HistogramTester& histogram_tester() { return histogram_tester_; }
 
+  GURL media_url() { return media_url_; }
+
  private:
   bool enable_lite_mode_;  // Whether LiteMode is enabled.
+  GURL media_url_;
   base::test::ScopedFeatureList scoped_feature_list_;
   net::EmbeddedTestServer http_server_;
   base::HistogramTester histogram_tester_;
@@ -150,6 +160,7 @@
 
 IN_PROC_BROWSER_TEST_F(LiteVideoBrowserTest,
                        DISABLE_ON_WIN_MAC_CHROMEOS(SimplePlayback)) {
+  ukm::TestAutoSetUkmRecorder ukm_recorder;
   TestMSEPlayback("bear-vp9.webm", "2000", "2000", false);
 
   RetryForHistogramUntilCountReached(histogram_tester(),
@@ -161,6 +172,26 @@
   // made and the hint is available.
   RetryForHistogramUntilCountReached(histogram_tester(),
                                      "LiteVideo.URLLoader.ThrottleLatency", 1);
+
+  // Close the tab to flush the UKM metrics.
+  browser()->tab_strip_model()->GetActiveWebContents()->Close();
+  auto entries =
+      ukm_recorder.GetEntriesByName(ukm::builders::LiteVideo::kEntryName);
+  ASSERT_EQ(1u, entries.size());
+  auto* entry = entries[0];
+  ukm_recorder.ExpectEntrySourceHasUrl(entry, media_url());
+  ukm_recorder.ExpectEntryMetric(
+      entry, ukm::builders::LiteVideo::kThrottlingStartDecisionName,
+      static_cast<int>(lite_video::LiteVideoDecision::kAllowed));
+  // Blocklist reason is unknown due to force overriding the decision logic
+  // for testing.
+  ukm_recorder.ExpectEntryMetric(
+      entry, ukm::builders::LiteVideo::kBlocklistReasonName,
+      static_cast<int>(lite_video::LiteVideoBlocklistReason::kUnknown));
+  ukm_recorder.ExpectEntryMetric(
+      entry, ukm::builders::LiteVideo::kThrottlingResultName,
+      static_cast<int>(
+          lite_video::LiteVideoThrottleResult::kThrottledWithoutStop));
 }
 
 class LiteVideoWithLiteModeDisabledBrowserTest : public LiteVideoBrowserTest {
@@ -172,6 +203,7 @@
 
 IN_PROC_BROWSER_TEST_F(LiteVideoWithLiteModeDisabledBrowserTest,
                        VideoThrottleDisabled) {
+  ukm::TestAutoSetUkmRecorder ukm_recorder;
   TestMSEPlayback("bear-vp9.webm", "2000", "2000", false);
 
   RetryForHistogramUntilCountReached(histogram_tester(),
@@ -179,10 +211,17 @@
 
   histogram_tester().ExpectTotalCount("LiteVideo.HintAgent.HasHint", 0);
   histogram_tester().ExpectTotalCount("LiteVideo.URLLoader.ThrottleLatency", 0);
+
+  // Close the tab to flush the UKM metrics.
+  browser()->tab_strip_model()->GetActiveWebContents()->Close();
+  auto entries =
+      ukm_recorder.GetEntriesByName(ukm::builders::LiteVideo::kEntryName);
+  ASSERT_EQ(0u, entries.size());
 }
 
 IN_PROC_BROWSER_TEST_F(LiteVideoBrowserTest,
                        MSEPlaybackStalledDueToBufferUnderflow) {
+  ukm::TestAutoSetUkmRecorder ukm_recorder;
   TestMSEPlayback("bear-vp9.webm", "2700", "500", false);
 
   RetryForHistogramUntilCountReached(histogram_tester(),
@@ -197,10 +236,31 @@
   EXPECT_GE(1U, histogram_tester()
                     .GetAllSamples("LiteVideo.HintsAgent.StopThrottling")
                     .size());
+  // Close the tab to flush the UKM metrics.
+  browser()->tab_strip_model()->GetActiveWebContents()->Close();
+
+  auto entries =
+      ukm_recorder.GetEntriesByName(ukm::builders::LiteVideo::kEntryName);
+  ASSERT_EQ(1u, entries.size());
+  auto* entry = entries[0];
+  ukm_recorder.ExpectEntrySourceHasUrl(entry, media_url());
+  ukm_recorder.ExpectEntryMetric(
+      entry, ukm::builders::LiteVideo::kThrottlingStartDecisionName,
+      static_cast<int>(lite_video::LiteVideoDecision::kAllowed));
+  // Blocklist reason is unknown due to force overriding the decision logic
+  // for testing.
+  ukm_recorder.ExpectEntryMetric(
+      entry, ukm::builders::LiteVideo::kBlocklistReasonName,
+      static_cast<int>(lite_video::LiteVideoBlocklistReason::kUnknown));
+  ukm_recorder.ExpectEntryMetric(
+      entry, ukm::builders::LiteVideo::kThrottlingResultName,
+      static_cast<int>(
+          lite_video::LiteVideoThrottleResult::kThrottleStoppedOnRebuffer));
 }
 
 IN_PROC_BROWSER_TEST_F(LiteVideoBrowserTest,
                        MSEPlaybackStalledDueToBufferUnderflow_WithSubframe) {
+  ukm::TestAutoSetUkmRecorder ukm_recorder;
   TestMSEPlayback("bear-vp9.webm", "2700", "500", true);
 
   RetryForHistogramUntilCountReached(histogram_tester(),
@@ -217,6 +277,27 @@
   EXPECT_GE(2U, histogram_tester()
                     .GetAllSamples("LiteVideo.HintsAgent.StopThrottling")
                     .size());
+  // Close the tab to flush the UKM metrics.
+  browser()->tab_strip_model()->GetActiveWebContents()->Close();
+
+  auto entries =
+      ukm_recorder.GetEntriesByName(ukm::builders::LiteVideo::kEntryName);
+  // Only 1 UKM entry logged, tied to the mainframe navigation.
+  ASSERT_EQ(1u, entries.size());
+  auto* entry = entries[0];
+  ukm_recorder.ExpectEntrySourceHasUrl(entry, media_url());
+  ukm_recorder.ExpectEntryMetric(
+      entry, ukm::builders::LiteVideo::kThrottlingStartDecisionName,
+      static_cast<int>(lite_video::LiteVideoDecision::kAllowed));
+  // Blocklist reason is unknown due to force overriding the decision logic
+  // for testing.
+  ukm_recorder.ExpectEntryMetric(
+      entry, ukm::builders::LiteVideo::kBlocklistReasonName,
+      static_cast<int>(lite_video::LiteVideoBlocklistReason::kUnknown));
+  ukm_recorder.ExpectEntryMetric(
+      entry, ukm::builders::LiteVideo::kThrottlingResultName,
+      static_cast<int>(
+          lite_video::LiteVideoThrottleResult::kThrottleStoppedOnRebuffer));
 }
 
 class LiteVideoAndLiteModeDisabledBrowserTest : public LiteVideoBrowserTest {
@@ -228,6 +309,7 @@
 
 IN_PROC_BROWSER_TEST_F(LiteVideoAndLiteModeDisabledBrowserTest,
                        VideoThrottleDisabled) {
+  ukm::TestAutoSetUkmRecorder ukm_recorder;
   TestMSEPlayback("bear-vp9.webm", "2000", "2000", false);
 
   RetryForHistogramUntilCountReached(histogram_tester(),
@@ -235,6 +317,11 @@
 
   histogram_tester().ExpectTotalCount("LiteVideo.HintAgent.HasHint", 0);
   histogram_tester().ExpectTotalCount("LiteVideo.URLLoader.ThrottleLatency", 0);
+  // Close the tab to flush the UKM metrics.
+  browser()->tab_strip_model()->GetActiveWebContents()->Close();
+  auto entries =
+      ukm_recorder.GetEntriesByName(ukm::builders::LiteVideo::kEntryName);
+  ASSERT_EQ(0u, entries.size());
 }
 
 }  // namespace
diff --git a/chrome/browser/extensions/api/debugger/debugger_api.cc b/chrome/browser/extensions/api/debugger/debugger_api.cc
index 73456ea..f957e78 100644
--- a/chrome/browser/extensions/api/debugger/debugger_api.cc
+++ b/chrome/browser/extensions/api/debugger/debugger_api.cc
@@ -416,12 +416,9 @@
         *debuggee_.tab_id, browser_context(), include_incognito_information(),
         &web_contents);
     if (result && web_contents) {
-      // TODO(rdevlin.cronin) This should definitely be GetLastCommittedURL().
-      GURL url = web_contents->GetVisibleURL();
-
       if (!ExtensionCanAttachToURL(
-              *extension(), url, Profile::FromBrowserContext(browser_context()),
-              error)) {
+              *extension(), web_contents->GetLastCommittedURL(),
+              Profile::FromBrowserContext(browser_context()), error)) {
         return false;
       }
 
diff --git a/chrome/browser/extensions/api/messaging/native_messaging_apitest.cc b/chrome/browser/extensions/api/messaging/native_messaging_apitest.cc
index 24e38b2..3f87fc8e 100644
--- a/chrome/browser/extensions/api/messaging/native_messaging_apitest.cc
+++ b/chrome/browser/extensions/api/messaging/native_messaging_apitest.cc
@@ -78,12 +78,16 @@
                          NativeMessagingLazyApiTest,
                          ::testing::Values(ContextType::kServiceWorker));
 
-IN_PROC_BROWSER_TEST_P(NativeMessagingLazyApiTest, NativeMessagingBasic) {
+// Flaky test: http://crbug.com/1111536
+IN_PROC_BROWSER_TEST_P(NativeMessagingLazyApiTest,
+                       DISABLED_NativeMessagingBasic) {
   ASSERT_NO_FATAL_FAILURE(test_host_.RegisterTestHost(false));
   ASSERT_TRUE(RunLazyTest("native_messaging_lazy")) << message_;
 }
 
-IN_PROC_BROWSER_TEST_P(NativeMessagingLazyApiTest, UserLevelNativeMessaging) {
+// Flaky test: http://crbug.com/1111337
+IN_PROC_BROWSER_TEST_P(NativeMessagingLazyApiTest,
+                       DISABLED_UserLevelNativeMessaging) {
   ASSERT_NO_FATAL_FAILURE(test_host_.RegisterTestHost(true));
   ASSERT_TRUE(RunLazyTest("native_messaging_lazy")) << message_;
 }
diff --git a/chrome/browser/extensions/api/tabs/tabs_api.cc b/chrome/browser/extensions/api/tabs/tabs_api.cc
index 584ae172..005f8e0df 100644
--- a/chrome/browser/extensions/api/tabs/tabs_api.cc
+++ b/chrome/browser/extensions/api/tabs/tabs_api.cc
@@ -1596,6 +1596,9 @@
   return RespondNow(NoArguments());
 }
 
+TabsRemoveFunction::TabsRemoveFunction() = default;
+TabsRemoveFunction::~TabsRemoveFunction() = default;
+
 ExtensionFunction::ResponseAction TabsRemoveFunction::Run() {
   std::unique_ptr<tabs::Remove::Params> params(
       tabs::Remove::Params::Create(*args_));
@@ -1613,7 +1616,16 @@
     if (!RemoveTab(*params->tab_ids.as_integer, &error))
       return RespondNow(Error(std::move(error)));
   }
-  return RespondNow(NoArguments());
+  triggered_all_tab_removals_ = true;
+  DCHECK(!did_respond());
+  // WebContentsDestroyed will return the response in most cases, except when
+  // the last tab closed immediately (it won't return a response because
+  // |triggered_all_tab_removals_| will still be false). In this case we should
+  // return the response from here.
+  if (remaining_tabs_count_ == 0) {
+    return RespondNow(NoArguments());
+  }
+  return RespondLater();
 }
 
 bool TabsRemoveFunction::RemoveTab(int tab_id, std::string* error) {
@@ -1629,6 +1641,17 @@
     *error = tabs_constants::kTabStripNotEditableError;
     return false;
   }
+  // The tab might not immediately close after calling Close() below, so we
+  // should wait until WebContentsDestroyed is called before responding.
+  web_contents_destroyed_observers_.push_back(
+      std::make_unique<WebContentsDestroyedObserver>(this, contents));
+  // Ensure that we're going to keep this class alive until
+  // |remaining_tabs_count| reaches zero. This relies on WebContents::Close()
+  // always (eventually) resulting in a WebContentsDestroyed() call; otherwise,
+  // this function will never respond and may leak.
+  AddRef();
+  remaining_tabs_count_++;
+
   // There's a chance that the tab is being dragged, or we're in some other
   // nested event loop. This code path ensures that the tab is safely closed
   // under such circumstances, whereas |TabStripModel::CloseWebContentsAt()|
@@ -1637,6 +1660,40 @@
   return true;
 }
 
+void TabsRemoveFunction::TabDestroyed() {
+  DCHECK_GT(remaining_tabs_count_, 0);
+  // One of the tabs we wanted to remove had been destroyed.
+  remaining_tabs_count_--;
+  // If we've triggered all the tab removals we need, and this is the last tab
+  // we're waiting for and we haven't sent a response (it's possible that we've
+  // responded earlier in case of errors, etc.), send a response.
+  if (triggered_all_tab_removals_ && remaining_tabs_count_ == 0 &&
+      !did_respond()) {
+    Respond(NoArguments());
+  }
+  Release();
+}
+
+class TabsRemoveFunction::WebContentsDestroyedObserver
+    : public content::WebContentsObserver {
+ public:
+  WebContentsDestroyedObserver(extensions::TabsRemoveFunction* owner,
+                               content::WebContents* watched_contents)
+      : content::WebContentsObserver(watched_contents), owner_(owner) {}
+
+  ~WebContentsDestroyedObserver() override = default;
+  WebContentsDestroyedObserver(const WebContentsDestroyedObserver&) = delete;
+  WebContentsDestroyedObserver& operator=(const WebContentsDestroyedObserver&) =
+      delete;
+
+  // WebContentsObserver
+  void WebContentsDestroyed() override { owner_->TabDestroyed(); }
+
+ private:
+  // Guaranteed to outlive this object.
+  extensions::TabsRemoveFunction* owner_;
+};
+
 TabsCaptureVisibleTabFunction::TabsCaptureVisibleTabFunction()
     : chrome_details_(this) {
 }
diff --git a/chrome/browser/extensions/api/tabs/tabs_api.h b/chrome/browser/extensions/api/tabs/tabs_api.h
index ed3aa53..a3df97cb 100644
--- a/chrome/browser/extensions/api/tabs/tabs_api.h
+++ b/chrome/browser/extensions/api/tabs/tabs_api.h
@@ -24,7 +24,6 @@
 class GURL;
 class SkBitmap;
 class TabStripModel;
-
 namespace content {
 class WebContents;
 }
@@ -175,9 +174,20 @@
   DECLARE_EXTENSION_FUNCTION("tabs.reload", TABS_RELOAD)
 };
 class TabsRemoveFunction : public ExtensionFunction {
-  ~TabsRemoveFunction() override {}
+ public:
+  TabsRemoveFunction();
+  void TabDestroyed();
+
+ private:
+  class WebContentsDestroyedObserver;
+  ~TabsRemoveFunction() override;
   ResponseAction Run() override;
   bool RemoveTab(int tab_id, std::string* error);
+
+  int remaining_tabs_count_ = 0;
+  bool triggered_all_tab_removals_ = false;
+  std::vector<std::unique_ptr<WebContentsDestroyedObserver>>
+      web_contents_destroyed_observers_;
   DECLARE_EXTENSION_FUNCTION("tabs.remove", TABS_REMOVE)
 };
 class TabsDetectLanguageFunction
diff --git a/chrome/browser/extensions/extension_tabs_apitest.cc b/chrome/browser/extensions/extension_tabs_apitest.cc
index 0da92c4..f5d6979 100644
--- a/chrome/browser/extensions/extension_tabs_apitest.cc
+++ b/chrome/browser/extensions/extension_tabs_apitest.cc
@@ -119,6 +119,15 @@
   ASSERT_TRUE(RunExtensionSubtest("tabs/basics", "opener.html")) << message_;
 }
 
+IN_PROC_BROWSER_TEST_F(ExtensionApiTabTest, TabRemove) {
+  ASSERT_TRUE(RunExtensionSubtest("tabs/basics", "remove.html")) << message_;
+}
+
+IN_PROC_BROWSER_TEST_F(ExtensionApiTabTest, TabRemoveMultiple) {
+  ASSERT_TRUE(RunExtensionSubtest("tabs/basics", "remove-multiple.html"))
+      << message_;
+}
+
 IN_PROC_BROWSER_TEST_F(ExtensionApiTabTest, TabGetCurrent) {
   ASSERT_TRUE(RunExtensionTest("tabs/get_current")) << message_;
 }
diff --git a/chrome/browser/flag-metadata.json b/chrome/browser/flag-metadata.json
index 211318c6..5711b2ec 100644
--- a/chrome/browser/flag-metadata.json
+++ b/chrome/browser/flag-metadata.json
@@ -52,7 +52,7 @@
   {
     "name": "align-font-display-auto-lcp",
     "owners": [ "xiaochengh" ],
-    "expiry_milestone": 85
+    "expiry_milestone": 87
   },
   {
     "name": "allow-disable-mouse-acceleration",
@@ -2838,6 +2838,11 @@
     "expiry_milestone": 87
   },
   {
+    "name": "legacy-tls-interstitial",
+    "owners": [ "cthomp" ],
+    "expiry_milestone": 92
+  },
+  {
     "name": "list-all-display-modes",
     "owners": [ "//ui/display/OWNERS" ],
     // This flag is used for debugging and development purposes to list all
@@ -3588,6 +3593,11 @@
     "expiry_milestone": 88
   },
   {
+    "name": "print-save-to-drive",
+    "owners": [ "gavinwill", "cros-peripherals@google.com" ],
+    "expiry_milestone": 90
+  },
+  {
     "name": "print-with-reduced-rasterization",
     "owners": [ "thestig" ],
     "expiry_milestone": 89
@@ -3698,6 +3708,11 @@
     "expiry_milestone": 87
   },
   {
+    "name": "reengagement-notification",
+    "owners": [ "dtrainor", "xingliu" ],
+    "expiry_milestone": 90
+  },
+  {
     "name": "related-searches",
     "owners": [ "ayman", "donnd" ],
     "expiry_milestone": 85
diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc
index feb2a418..7c8cc58 100644
--- a/chrome/browser/flag_descriptions.cc
+++ b/chrome/browser/flag_descriptions.cc
@@ -1848,6 +1848,10 @@
     "Enables the print management app that allows Chrome OS users to view "
     "and manage their native print jobs.";
 
+const char kPrintSaveToDriveName[] = "Print Save to Drive locally";
+const char kPrintSaveToDriveDescription[] =
+    "Modifies Print Preview Save to Drive to use locally mounted Drive";
+
 const char kPrivacyElevatedAndroidName[] =
     "Elevate Privacy in Settings on Android";
 const char kPrivacyElevatedAndroidDescription[] =
@@ -2957,6 +2961,12 @@
     "Enables showing UI which allows for easy reverting of the decision to "
     "never save passwords on a certain webiste";
 
+const char kReengagementNotificationName[] =
+    "Enable re-engagement notifications";
+const char kReengagementNotificationDescription[] =
+    "Enables Chrome to use the in-product help system to decide when "
+    "to show re-engagement notifications.";
+
 const char kRelatedSearchesName[] =
     "Enables an experiment for Related Searches on Android";
 const char kRelatedSearchesDescription[] =
diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h
index 900d203..03e8ff1 100644
--- a/chrome/browser/flag_descriptions.h
+++ b/chrome/browser/flag_descriptions.h
@@ -1069,6 +1069,9 @@
 extern const char kPrintJobManagementAppName[];
 extern const char kPrintJobManagementAppDescription[];
 
+extern const char kPrintSaveToDriveName[];
+extern const char kPrintSaveToDriveDescription[];
+
 extern const char kPrivacyElevatedAndroidName[];
 extern const char kPrivacyElevatedAndroidDescription[];
 
@@ -1704,6 +1707,9 @@
 extern const char kRecoverFromNeverSaveAndroidName[];
 extern const char kRecoverFromNeverSaveAndroidDescription[];
 
+extern const char kReengagementNotificationName[];
+extern const char kReengagementNotificationDescription[];
+
 extern const char kRelatedSearchesName[];
 extern const char kRelatedSearchesDescription[];
 
diff --git a/chrome/browser/lite_video/lite_video_decider.cc b/chrome/browser/lite_video/lite_video_decider.cc
index 0a3fd95..7c16124 100644
--- a/chrome/browser/lite_video/lite_video_decider.cc
+++ b/chrome/browser/lite_video/lite_video_decider.cc
@@ -171,8 +171,14 @@
     return base::nullopt;
 
   // The navigation will have the LiteVideo optimization triggered so
-  // update the navigation blocklist.
+  // update the blocklist.
   user_blocklist_->AddNavigationToBlocklist(navigation_handle, false);
+
+  navigation_handle->IsInMainFrame()
+      ? DidMediaRebuffer(navigation_handle->GetURL(), base::nullopt, false)
+      : DidMediaRebuffer(
+            navigation_handle->GetWebContents()->GetLastCommittedURL(),
+            navigation_handle->GetURL(), false);
   return hint;
 }
 
@@ -210,4 +216,12 @@
   LOCAL_HISTOGRAM_BOOLEAN("LiteVideo.UserBlocklist.ClearBlocklist", true);
 }
 
+void LiteVideoDecider::DidMediaRebuffer(const GURL& mainframe_url,
+                                        base::Optional<GURL> subframe_url,
+                                        bool opt_out) {
+  if (user_blocklist_)
+    user_blocklist_->AddRebufferToBlocklist(mainframe_url, subframe_url,
+                                            opt_out);
+}
+
 }  // namespace lite_video
diff --git a/chrome/browser/lite_video/lite_video_decider.h b/chrome/browser/lite_video/lite_video_decider.h
index 1f4f49a..faf01ca 100644
--- a/chrome/browser/lite_video/lite_video_decider.h
+++ b/chrome/browser/lite_video/lite_video_decider.h
@@ -70,6 +70,12 @@
   void ClearBlocklist(const base::Time& delete_begin,
                       const base::Time& delete_end);
 
+  // Update |user_blocklist_| that a rebuffer event consided an opt-out on the
+  // mainframe and subframe URLs occurred.
+  void DidMediaRebuffer(const GURL& mainframe_url,
+                        base::Optional<GURL> subframe_url,
+                        bool opt_out);
+
  private:
   // The hint cache that holds LiteVideoHints that specify the parameters
   // for throttling media requests for that navigation.
diff --git a/chrome/browser/lite_video/lite_video_keyed_service_browsertest.cc b/chrome/browser/lite_video/lite_video_keyed_service_browsertest.cc
index 215367c..02a7970 100644
--- a/chrome/browser/lite_video/lite_video_keyed_service_browsertest.cc
+++ b/chrome/browser/lite_video/lite_video_keyed_service_browsertest.cc
@@ -246,6 +246,10 @@
   ukm_recorder.ExpectEntryMetric(
       entry, ukm::builders::LiteVideo::kBlocklistReasonName,
       static_cast<int>(lite_video::LiteVideoBlocklistReason::kAllowed));
+  ukm_recorder.ExpectEntryMetric(
+      entry, ukm::builders::LiteVideo::kThrottlingResultName,
+      static_cast<int>(
+          lite_video::LiteVideoThrottleResult::kThrottledWithoutStop));
 }
 
 IN_PROC_BROWSER_TEST_F(LiteVideoKeyedServiceBrowserTest,
@@ -288,6 +292,10 @@
   ukm_recorder.ExpectEntryMetric(
       entry, ukm::builders::LiteVideo::kBlocklistReasonName,
       static_cast<int>(lite_video::LiteVideoBlocklistReason::kAllowed));
+  ukm_recorder.ExpectEntryMetric(
+      entry, ukm::builders::LiteVideo::kThrottlingResultName,
+      static_cast<int>(
+          lite_video::LiteVideoThrottleResult::kThrottledWithoutStop));
 }
 
 IN_PROC_BROWSER_TEST_F(LiteVideoKeyedServiceBrowserTest,
@@ -347,6 +355,10 @@
       entry, ukm::builders::LiteVideo::kBlocklistReasonName,
       static_cast<int>(
           lite_video::LiteVideoBlocklistReason::kNavigationReload));
+  ukm_recorder.ExpectEntryMetric(
+      entry, ukm::builders::LiteVideo::kThrottlingResultName,
+      static_cast<int>(
+          lite_video::LiteVideoThrottleResult::kThrottledWithoutStop));
 
   entry = entries[1];
   ukm_recorder.ExpectEntrySourceHasUrl(entry, url);
@@ -357,6 +369,10 @@
       entry, ukm::builders::LiteVideo::kBlocklistReasonName,
       static_cast<int>(
           lite_video::LiteVideoBlocklistReason::kNavigationBlocklisted));
+  ukm_recorder.ExpectEntryMetric(
+      entry, ukm::builders::LiteVideo::kThrottlingResultName,
+      static_cast<int>(
+          lite_video::LiteVideoThrottleResult::kThrottledWithoutStop));
 }
 
 IN_PROC_BROWSER_TEST_F(LiteVideoKeyedServiceBrowserTest,
@@ -416,6 +432,10 @@
       entry, ukm::builders::LiteVideo::kBlocklistReasonName,
       static_cast<int>(
           lite_video::LiteVideoBlocklistReason::kNavigationForwardBack));
+  ukm_recorder.ExpectEntryMetric(
+      entry, ukm::builders::LiteVideo::kThrottlingResultName,
+      static_cast<int>(
+          lite_video::LiteVideoThrottleResult::kThrottledWithoutStop));
 
   entry = entries[1];
   ukm_recorder.ExpectEntrySourceHasUrl(entry, url);
@@ -426,6 +446,10 @@
       entry, ukm::builders::LiteVideo::kBlocklistReasonName,
       static_cast<int>(
           lite_video::LiteVideoBlocklistReason::kNavigationBlocklisted));
+  ukm_recorder.ExpectEntryMetric(
+      entry, ukm::builders::LiteVideo::kThrottlingResultName,
+      static_cast<int>(
+          lite_video::LiteVideoThrottleResult::kThrottledWithoutStop));
 }
 
 IN_PROC_BROWSER_TEST_F(LiteVideoKeyedServiceBrowserTest,
@@ -483,6 +507,10 @@
     ukm_recorder.ExpectEntryMetric(
         entry, ukm::builders::LiteVideo::kBlocklistReasonName,
         static_cast<int>(lite_video::LiteVideoBlocklistReason::kAllowed));
+    ukm_recorder.ExpectEntryMetric(
+        entry, ukm::builders::LiteVideo::kThrottlingResultName,
+        static_cast<int>(
+            lite_video::LiteVideoThrottleResult::kThrottledWithoutStop));
   }
 }
 
@@ -669,6 +697,10 @@
   ukm_recorder.ExpectEntryMetric(
       entry, ukm::builders::LiteVideo::kBlocklistReasonName,
       static_cast<int>(lite_video::LiteVideoBlocklistReason::kAllowed));
+  ukm_recorder.ExpectEntryMetric(
+      entry, ukm::builders::LiteVideo::kThrottlingResultName,
+      static_cast<int>(
+          lite_video::LiteVideoThrottleResult::kThrottledWithoutStop));
 }
 
 class LiteVideoKeyedServiceCoinflipBrowserTest
diff --git a/chrome/browser/lite_video/lite_video_navigation_metrics.cc b/chrome/browser/lite_video/lite_video_navigation_metrics.cc
index 4c8da07..09add76 100644
--- a/chrome/browser/lite_video/lite_video_navigation_metrics.cc
+++ b/chrome/browser/lite_video/lite_video_navigation_metrics.cc
@@ -9,15 +9,18 @@
 LiteVideoNavigationMetrics::LiteVideoNavigationMetrics(
     int64_t nav_id,
     LiteVideoDecision decision,
-    LiteVideoBlocklistReason blocklist_reason)
+    LiteVideoBlocklistReason blocklist_reason,
+    LiteVideoThrottleResult throttle_result)
     : nav_id_(nav_id),
       decision_(decision),
-      blocklist_reason_(blocklist_reason) {}
+      blocklist_reason_(blocklist_reason),
+      throttle_result_(throttle_result) {}
 
 LiteVideoNavigationMetrics::~LiteVideoNavigationMetrics() = default;
 
-LiteVideoBlocklistReason LiteVideoNavigationMetrics::blocklist_reason() const {
-  return blocklist_reason_;
+void LiteVideoNavigationMetrics::SetThrottleResult(
+    LiteVideoThrottleResult throttle_result) {
+  throttle_result_ = throttle_result;
 }
 
 }  // namespace lite_video
diff --git a/chrome/browser/lite_video/lite_video_navigation_metrics.h b/chrome/browser/lite_video/lite_video_navigation_metrics.h
index dd3962e..d9bf552 100644
--- a/chrome/browser/lite_video/lite_video_navigation_metrics.h
+++ b/chrome/browser/lite_video/lite_video_navigation_metrics.h
@@ -25,21 +25,44 @@
   kMaxValue = kHoldback,
 };
 
+// The result of throttling on a navigation.
+// This should be kept in sync with LiteVideoThrottleResult in enums.xml.
+enum class LiteVideoThrottleResult {
+  kUnknown,
+  // LiteVideos were enabled to throttle media requests on the navigation
+  // and they were not stopped due to rebuffering events.
+  kThrottledWithoutStop,
+  // LiteVideos were enabled to throttle media requests on the navigation
+  // but they were stopped due to rebuffering events.
+  kThrottleStoppedOnRebuffer,
+
+  // Insert new values before this line.
+  kMaxValue = kThrottleStoppedOnRebuffer,
+};
+
 class LiteVideoNavigationMetrics {
  public:
   LiteVideoNavigationMetrics(int64_t nav_id,
                              LiteVideoDecision decision,
-                             LiteVideoBlocklistReason blocklist_reason);
+                             LiteVideoBlocklistReason blocklist_reason,
+                             LiteVideoThrottleResult throttle_result);
   ~LiteVideoNavigationMetrics();
 
   int64_t nav_id() const { return nav_id_; }
   LiteVideoDecision decision() const { return decision_; }
-  LiteVideoBlocklistReason blocklist_reason() const;
+  LiteVideoBlocklistReason blocklist_reason() const {
+    return blocklist_reason_;
+  }
+  LiteVideoThrottleResult throttle_result() const { return throttle_result_; }
+
+  // Update the throttling result of the current navigation.
+  void SetThrottleResult(LiteVideoThrottleResult throttle_result);
 
  private:
   int64_t nav_id_;
   LiteVideoDecision decision_;
   LiteVideoBlocklistReason blocklist_reason_;
+  LiteVideoThrottleResult throttle_result_;
 };
 
 }  // namespace lite_video
diff --git a/chrome/browser/lite_video/lite_video_observer.cc b/chrome/browser/lite_video/lite_video_observer.cc
index 978fb5b..c790692 100644
--- a/chrome/browser/lite_video/lite_video_observer.cc
+++ b/chrome/browser/lite_video/lite_video_observer.cc
@@ -12,6 +12,7 @@
 #include "chrome/browser/lite_video/lite_video_hint.h"
 #include "chrome/browser/lite_video/lite_video_keyed_service.h"
 #include "chrome/browser/lite_video/lite_video_keyed_service_factory.h"
+#include "chrome/browser/lite_video/lite_video_navigation_metrics.h"
 #include "chrome/browser/lite_video/lite_video_switches.h"
 #include "chrome/browser/lite_video/lite_video_user_blocklist.h"
 #include "chrome/browser/lite_video/lite_video_util.h"
@@ -91,7 +92,8 @@
   if (navigation_handle->IsInMainFrame()) {
     FlushUKMMetrics();
     nav_metrics_ = lite_video::LiteVideoNavigationMetrics(
-        navigation_handle->GetNavigationId(), decision, blocklist_reason);
+        navigation_handle->GetNavigationId(), decision, blocklist_reason,
+        lite_video::LiteVideoThrottleResult::kThrottledWithoutStop);
   }
 
   LOCAL_HISTOGRAM_BOOLEAN("LiteVideo.Navigation.HasHint", hint ? true : false);
@@ -140,6 +142,7 @@
   ukm::builders::LiteVideo builder(ukm_source_id);
   builder.SetThrottlingStartDecision(static_cast<int>(nav_metrics_->decision()))
       .SetBlocklistReason(static_cast<int>(nav_metrics_->blocklist_reason()))
+      .SetThrottlingResult(static_cast<int>(nav_metrics_->throttle_result()))
       .Record(ukm::UkmRecorder::Get());
   nav_metrics_.reset();
 }
@@ -171,12 +174,26 @@
         &loading_hints_agent);
     loading_hints_agent->StopThrottlingMediaRequests();
   }
+  // Only consider a rebuffer event related to LiteVideos if they
+  // were allowed on current navigation.
+  if (!nav_metrics_ ||
+      nav_metrics_->decision() != lite_video::LiteVideoDecision::kAllowed) {
+    return;
+  }
 
-  // TODO(crbug/1101563 Update the user blocklist. This needs additional
-  // work to operate on local state mapping the current render frame id
-  // to the navigation's origin.
+  nav_metrics_->SetThrottleResult(
+      lite_video::LiteVideoThrottleResult::kThrottleStoppedOnRebuffer);
 
-  // TODO(crbug/1097792): Flush a UKM event for this render frame host.
+  if (!lite_video_decider_)
+    return;
+
+  // Determine if the rebuffer happened in the mainframe.
+  render_frame_host->GetMainFrame() == render_frame_host
+      ? lite_video_decider_->DidMediaRebuffer(
+            render_frame_host->GetLastCommittedURL(), base::nullopt, true)
+      : lite_video_decider_->DidMediaRebuffer(
+            render_frame_host->GetMainFrame()->GetLastCommittedURL(),
+            render_frame_host->GetLastCommittedURL(), true);
 }
 
 WEB_CONTENTS_USER_DATA_KEY_IMPL(LiteVideoObserver)
diff --git a/chrome/browser/lite_video/lite_video_user_blocklist.cc b/chrome/browser/lite_video/lite_video_user_blocklist.cc
index 7368e0a..afc9f2a 100644
--- a/chrome/browser/lite_video/lite_video_user_blocklist.cc
+++ b/chrome/browser/lite_video/lite_video_user_blocklist.cc
@@ -139,4 +139,17 @@
            static_cast<int>(LiteVideoBlocklistType::kNavigationBlocklist));
 }
 
+void LiteVideoUserBlocklist::AddRebufferToBlocklist(
+    const GURL& mainframe_url,
+    base::Optional<GURL> subframe_url,
+    bool opt_out) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  base::Optional<std::string> rebuffer_key =
+      GetRebufferBlocklistKey(mainframe_url, subframe_url);
+  if (rebuffer_key) {
+    AddEntry(*rebuffer_key, opt_out,
+             static_cast<int>(LiteVideoBlocklistType::kRebufferBlocklist));
+  }
+}
+
 }  // namespace lite_video
diff --git a/chrome/browser/lite_video/lite_video_user_blocklist.h b/chrome/browser/lite_video/lite_video_user_blocklist.h
index bfdd7bb..14de369 100644
--- a/chrome/browser/lite_video/lite_video_user_blocklist.h
+++ b/chrome/browser/lite_video/lite_video_user_blocklist.h
@@ -86,6 +86,12 @@
   void AddNavigationToBlocklist(content::NavigationHandle* navigation_handle,
                                 bool opt_out);
 
+  // Update the entry within the RebufferBlocklistType for the
+  // mainframe and subframe urls based on whether it was an opt-out or not.
+  void AddRebufferToBlocklist(const GURL& mainframe_url,
+                              base::Optional<GURL> subframe_url,
+                              bool opt_out);
+
  protected:
   // OptOutBlocklist:
   bool ShouldUseSessionPolicy(base::TimeDelta* duration,
diff --git a/chrome/browser/lite_video/lite_video_user_blocklist_unittest.cc b/chrome/browser/lite_video/lite_video_user_blocklist_unittest.cc
index 4b2fd53..d1d6748 100644
--- a/chrome/browser/lite_video/lite_video_user_blocklist_unittest.cc
+++ b/chrome/browser/lite_video/lite_video_user_blocklist_unittest.cc
@@ -182,7 +182,7 @@
        MainframeNavigationBlocklistedByRebufferBlocklist) {
   ConfigBlocklistParamsForTesting();
   GURL url("https://test.com");
-  SeedBlocklist(url.host() + "_", LiteVideoBlocklistType::kRebufferBlocklist);
+  blocklist()->AddRebufferToBlocklist(url, base::nullopt, true);
   EXPECT_EQ(CheckBlocklistForMainframeNavigation(url),
             LiteVideoBlocklistReason::kRebufferingBlocklisted);
 }
@@ -199,8 +199,7 @@
   ConfigBlocklistParamsForTesting();
   GURL mainframe_url("https://test.com");
   GURL subframe_url("https://subframe.com");
-  SeedBlocklist(mainframe_url.host() + "_" + subframe_url.host(),
-                LiteVideoBlocklistType::kRebufferBlocklist);
+  blocklist()->AddRebufferToBlocklist(mainframe_url, subframe_url, true);
   EXPECT_EQ(CheckBlocklistForSubframeNavigation(mainframe_url, subframe_url),
             LiteVideoBlocklistReason::kRebufferingBlocklisted);
 }
diff --git a/chrome/browser/nearby_sharing/nearby_process_manager_unittest.cc b/chrome/browser/nearby_sharing/nearby_process_manager_unittest.cc
index 7151481..847cd96 100644
--- a/chrome/browser/nearby_sharing/nearby_process_manager_unittest.cc
+++ b/chrome/browser/nearby_sharing/nearby_process_manager_unittest.cc
@@ -47,7 +47,7 @@
 
   // sharing::mojom::Sharing:
   void CreateSharingWebRtcConnection(
-      mojo::PendingRemote<sharing::mojom::SignallingSender>,
+      mojo::PendingRemote<sharing::mojom::SignalingSender>,
       mojo::PendingReceiver<sharing::mojom::SignallingReceiver>,
       mojo::PendingRemote<sharing::mojom::SharingWebRtcConnectionDelegate>,
       mojo::PendingReceiver<sharing::mojom::SharingWebRtcConnection>,
diff --git a/chrome/browser/optimization_guide/hints_fetcher_browsertest.cc b/chrome/browser/optimization_guide/hints_fetcher_browsertest.cc
index 76ae61a..683d759 100644
--- a/chrome/browser/optimization_guide/hints_fetcher_browsertest.cc
+++ b/chrome/browser/optimization_guide/hints_fetcher_browsertest.cc
@@ -751,18 +751,6 @@
       1);
   histogram_tester->ExpectTotalCount(
       "OptimizationGuide.HintsFetcher.GetHintsRequest.HintCount", 0);
-
-  LoadHintsForUrl(https_url());
-
-  ui_test_utils::NavigateToURL(browser(), https_url());
-
-  // Verifies that no Fetched Hint was added to the store, only the
-  // Component hint is loaded.
-  histogram_tester->ExpectUniqueSample(
-      "OptimizationGuide.HintCache.HintType.Loaded",
-      static_cast<int>(optimization_guide::OptimizationGuideStore::
-                           StoreEntryType::kComponentHint),
-      1);
 }
 
 IN_PROC_BROWSER_TEST_F(
diff --git a/chrome/browser/optimization_guide/optimization_guide_hints_manager.cc b/chrome/browser/optimization_guide/optimization_guide_hints_manager.cc
index 7493218..f7da3b77 100644
--- a/chrome/browser/optimization_guide/optimization_guide_hints_manager.cc
+++ b/chrome/browser/optimization_guide/optimization_guide_hints_manager.cc
@@ -238,11 +238,14 @@
       profile_(profile),
       pref_service_(pref_service),
       hint_cache_(std::make_unique<optimization_guide::HintCache>(
-          std::make_unique<optimization_guide::OptimizationGuideStore>(
-              database_provider,
-              profile_path.AddExtensionASCII(
-                  optimization_guide::kOptimizationGuideHintStore),
-              background_task_runner_))),
+          optimization_guide::features::ShouldPersistHintsToDisk()
+              ? std::make_unique<optimization_guide::OptimizationGuideStore>(
+                    database_provider,
+                    profile_path.AddExtensionASCII(
+                        optimization_guide::kOptimizationGuideHintStore),
+                    background_task_runner_)
+              : nullptr,
+          optimization_guide::features::MaxHostKeyedHintCacheSize())),
       page_navigation_hints_fetchers_(
           optimization_guide::features::MaxConcurrentPageNavigationFetches()),
       hints_fetcher_factory_(
@@ -359,6 +362,11 @@
                              config->optimization_blacklists(),
                              registered_optimization_types);
 
+  // TODO(crbug/1112500): Figure out what to do with component hints if there
+  // isn't a persistent store. Right now, it doesn't really matter since there
+  // aren't hints sent down via the component, but we need to figure out
+  // threading since these hints are now stored in memory prior to being
+  // persisted.
   if (update_data) {
     bool did_process_hints = hint_cache_->ProcessAndCacheHints(
         config->mutable_hints(), update_data.get());
@@ -679,6 +687,14 @@
   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
   LOCAL_HISTOGRAM_BOOLEAN("OptimizationGuide.FetchedHints.Stored", true);
 
+  if (!optimization_guide::features::ShouldPersistHintsToDisk()) {
+    // If we aren't persisting hints to disk, there's no point in purging
+    // hints from disk or starting a new fetch since at this point we should
+    // just be fetching everything on page navigation and only storing
+    // in-memory.
+    return;
+  }
+
   hint_cache_->PurgeExpiredFetchedHints();
 
   top_hosts_hints_fetch_timer_.Stop();
diff --git a/chrome/browser/optimization_guide/optimization_guide_hints_manager_unittest.cc b/chrome/browser/optimization_guide/optimization_guide_hints_manager_unittest.cc
index 01ed3e69..03339c8 100644
--- a/chrome/browser/optimization_guide/optimization_guide_hints_manager_unittest.cc
+++ b/chrome/browser/optimization_guide/optimization_guide_hints_manager_unittest.cc
@@ -266,7 +266,11 @@
 class OptimizationGuideHintsManagerTest
     : public optimization_guide::ProtoDatabaseProviderTestBase {
  public:
-  OptimizationGuideHintsManagerTest() = default;
+  OptimizationGuideHintsManagerTest() {
+    scoped_feature_list_.InitAndEnableFeatureWithParameters(
+        optimization_guide::features::kOptimizationHints,
+        {{"max_host_keyed_hint_cache_size", "1"}});
+  }
   ~OptimizationGuideHintsManagerTest() override = default;
 
   void SetUp() override {
@@ -364,6 +368,14 @@
     optimization_guide::proto::PreviewsMetadata* default_opt_metadata =
         default_opt->mutable_previews_metadata();
     default_opt_metadata->set_inflation_percent(1234);
+    // Add another hint so somedomain.org hint is not in-memory initially.
+    optimization_guide::proto::Hint* hint2 = config.add_hints();
+    hint2->set_key("somedomain2.org");
+    hint2->set_key_representation(optimization_guide::proto::HOST);
+    hint2->set_version("someversion");
+    optimization_guide::proto::Optimization* opt =
+        hint2->add_whitelisted_optimizations();
+    opt->set_optimization_type(optimization_guide::proto::NOSCRIPT);
 
     ProcessHints(config, version);
   }
@@ -438,6 +450,7 @@
   content::BrowserTaskEnvironment task_environment_{
       base::test::TaskEnvironment::MainThreadType::UI,
       base::test::TaskEnvironment::TimeSource::MOCK_TIME};
+  base::test::ScopedFeatureList scoped_feature_list_;
   TestingProfile testing_profile_;
   std::unique_ptr<content::TestWebContentsFactory> web_contents_factory_;
   std::unique_ptr<OptimizationGuideHintsManager> hints_manager_;
diff --git a/chrome/browser/optimization_guide/optimization_guide_keyed_service.cc b/chrome/browser/optimization_guide/optimization_guide_keyed_service.cc
index 30ef51f..01ae1f8 100644
--- a/chrome/browser/optimization_guide/optimization_guide_keyed_service.cc
+++ b/chrome/browser/optimization_guide/optimization_guide_keyed_service.cc
@@ -23,6 +23,7 @@
 #include "components/optimization_guide/optimization_guide_decider.h"
 #include "components/optimization_guide/optimization_guide_features.h"
 #include "components/optimization_guide/optimization_guide_service.h"
+#include "components/optimization_guide/proto/models.pb.h"
 #include "components/optimization_guide/top_host_provider.h"
 #include "content/public/browser/browser_context.h"
 #include "content/public/browser/browser_thread.h"
@@ -68,6 +69,25 @@
   }
 }
 
+// Logs |optimization_target_decision| for |optimization_target| in a histogram
+// and passes the corresponding OptimizationGuideDecision to |callback|.
+void LogOptimizationTargetDecisionAndPassOptimizationGuideDecision(
+    optimization_guide::proto::OptimizationTarget optimization_target,
+    optimization_guide::OptimizationGuideTargetDecisionCallback callback,
+    optimization_guide::OptimizationTargetDecision
+        optimization_target_decision) {
+  base::UmaHistogramExactLinear(
+      "OptimizationGuide.TargetDecision." +
+          GetStringNameForOptimizationTarget(optimization_target),
+      static_cast<int>(optimization_target_decision),
+      static_cast<int>(
+          optimization_guide::OptimizationTargetDecision::kMaxValue));
+
+  std::move(callback).Run(
+      GetOptimizationGuideDecisionFromOptimizationTargetDecision(
+          optimization_target_decision));
+}
+
 }  // namespace
 
 OptimizationGuideKeyedService::OptimizationGuideKeyedService(
@@ -177,18 +197,11 @@
     return;
   }
 
-  optimization_guide::OptimizationTargetDecision optimization_target_decision =
-      prediction_manager_->ShouldTargetNavigation(
-          navigation_handle, optimization_target, client_model_feature_values);
-  base::UmaHistogramExactLinear(
-      "OptimizationGuide.TargetDecision." +
-          GetStringNameForOptimizationTarget(optimization_target),
-      static_cast<int>(optimization_target_decision),
-      static_cast<int>(
-          optimization_guide::OptimizationTargetDecision::kMaxValue));
-  std::move(callback).Run(
-      GetOptimizationGuideDecisionFromOptimizationTargetDecision(
-          optimization_target_decision));
+  prediction_manager_->ShouldTargetNavigationAsync(
+      navigation_handle, optimization_target, client_model_feature_values,
+      base::BindOnce(
+          &LogOptimizationTargetDecisionAndPassOptimizationGuideDecision,
+          optimization_target, std::move(callback)));
 }
 
 void OptimizationGuideKeyedService::RegisterOptimizationTypes(
diff --git a/chrome/browser/optimization_guide/optimization_guide_keyed_service.h b/chrome/browser/optimization_guide/optimization_guide_keyed_service.h
index 978654b0..dad095f7 100644
--- a/chrome/browser/optimization_guide/optimization_guide_keyed_service.h
+++ b/chrome/browser/optimization_guide/optimization_guide_keyed_service.h
@@ -36,7 +36,7 @@
 class OptimizationGuideService;
 class TopHostProvider;
 class PredictionManager;
-class PredictionManagerBrowserTest;
+class PredictionManagerBrowserTestBase;
 }  // namespace optimization_guide
 
 class GURL;
@@ -95,7 +95,7 @@
   friend class OptimizationGuideKeyedServiceBrowserTest;
   friend class OptimizationGuideWebContentsObserver;
   friend class ProfileManager;
-  friend class optimization_guide::PredictionManagerBrowserTest;
+  friend class optimization_guide::PredictionManagerBrowserTestBase;
   friend class optimization_guide::android::OptimizationGuideBridge;
 
   // Initializes the service. |optimization_guide_service| is the
diff --git a/chrome/browser/optimization_guide/prediction/prediction_manager.cc b/chrome/browser/optimization_guide/prediction/prediction_manager.cc
index c5ea5d3a..e042c99 100644
--- a/chrome/browser/optimization_guide/prediction/prediction_manager.cc
+++ b/chrome/browser/optimization_guide/prediction/prediction_manager.cc
@@ -164,6 +164,36 @@
 
 namespace optimization_guide {
 
+struct PredictionDecisionParams {
+  PredictionDecisionParams(
+      base::WeakPtr<OptimizationGuideNavigationData> navigation_data,
+      proto::OptimizationTarget optimization_target,
+      OptimizationTargetDecisionCallback callback,
+      int64_t version,
+      base::TimeTicks model_evaluation_start_time)
+      : navigation_data(navigation_data),
+        optimization_target(optimization_target),
+        callback(std::move(callback)),
+        version(version),
+        model_evaluation_start_time(model_evaluation_start_time) {}
+
+  ~PredictionDecisionParams() = default;
+
+  PredictionDecisionParams(const PredictionDecisionParams&) = delete;
+  PredictionDecisionParams& operator=(const PredictionDecisionParams&) = delete;
+
+  // Will store relevant prediction results, if not null.
+  base::WeakPtr<OptimizationGuideNavigationData> navigation_data;
+  // Target of the prediction.
+  proto::OptimizationTarget optimization_target;
+  // Callback to be invoked once a OptimizationTargetDecision is made.
+  OptimizationTargetDecisionCallback callback;
+  // Model version.
+  int64_t version;
+  // Time when the model evaluation is initiated.
+  base::TimeTicks model_evaluation_start_time;
+};
+
 PredictionManager::PredictionManager(
     const std::vector<optimization_guide::proto::OptimizationTarget>&
         optimization_targets_at_initialization,
@@ -483,6 +513,147 @@
   return target_decision;
 }
 
+void PredictionManager::ShouldTargetNavigationAsync(
+    content::NavigationHandle* navigation_handle,
+    proto::OptimizationTarget optimization_target,
+    const base::flat_map<proto::ClientModelFeature, float>&
+        override_client_model_feature_values,
+    OptimizationTargetDecisionCallback callback) {
+  SEQUENCE_CHECKER(sequence_checker_);
+  DCHECK(navigation_handle->GetURL().SchemeIsHTTPOrHTTPS());
+
+  OptimizationGuideNavigationData* navigation_data =
+      OptimizationGuideNavigationData::GetFromNavigationHandle(
+          navigation_handle);
+  if (navigation_data) {
+    base::Optional<optimization_guide::OptimizationTargetDecision>
+        optimization_target_decision =
+            navigation_data->GetDecisionForOptimizationTarget(
+                optimization_target);
+    if (optimization_target_decision.has_value() &&
+        ShouldUseCurrentOptimizationTargetDecision(
+            *optimization_target_decision)) {
+      std::move(callback).Run(*optimization_target_decision);
+      return;
+    }
+  }
+
+  if (!registered_optimization_targets_.contains(optimization_target)) {
+    std::move(callback).Run(OptimizationTargetDecision::kUnknown);
+    return;
+  }
+
+  // Use the synchronous code path if ML Service is not enabled.
+  if (!features::ShouldUseMLServiceForPrediction()) {
+    std::move(callback).Run(
+        ShouldTargetNavigation(navigation_handle, optimization_target,
+                               override_client_model_feature_values));
+    return;
+  }
+
+  ScopedPredictionManagerModelStatusRecorder model_status_recorder(
+      optimization_target);
+  auto it =
+      optimization_target_remote_model_predictor_map_.find(optimization_target);
+  if (it == optimization_target_remote_model_predictor_map_.end()) {
+    if (store_is_ready_ && model_and_features_store_) {
+      OptimizationGuideStore::EntryKey model_entry_key;
+      if (model_and_features_store_->FindPredictionModelEntryKey(
+              optimization_target, &model_entry_key)) {
+        model_status_recorder.set_status(
+            PredictionManagerModelStatus::kStoreAvailableModelNotLoaded);
+      } else {
+        model_status_recorder.set_status(
+            PredictionManagerModelStatus::kStoreAvailableNoModelForTarget);
+      }
+    } else {
+      model_status_recorder.set_status(
+          PredictionManagerModelStatus::kStoreUnavailableModelUnknown);
+    }
+    std::move(callback).Run(
+        OptimizationTargetDecision::kModelNotAvailableOnClient);
+    return;
+  }
+
+  RemoteDecisionTreePredictor* predictor = it->second.get();
+
+  if (!predictor->Get() || !predictor->IsConnected()) {
+    // Connection to remote model is no longer valid.
+    model_status_recorder.set_status(
+        optimization_guide::PredictionManagerModelStatus::
+            kStoreAvailableModelNotLoaded);
+    optimization_target_remote_model_predictor_map_.erase(it);
+    std::move(callback).Run(
+        OptimizationTargetDecision::kModelNotAvailableOnClient);
+    return;
+  }
+
+  model_status_recorder.set_status(
+      PredictionManagerModelStatus::kModelAvailable);
+
+  base::flat_map<std::string, float> feature_map =
+      BuildFeatureMap(navigation_handle, predictor->model_features(),
+                      override_client_model_feature_values);
+
+  predictor->Get()->Predict(
+      feature_map,
+      base::BindOnce(&PredictionManager::OnModelEvaluated,
+                     ui_weak_ptr_factory_.GetWeakPtr(),
+                     std::make_unique<PredictionDecisionParams>(
+                         navigation_data->GetWeakPtr(), optimization_target,
+                         std::move(callback), predictor->version(),
+                         base::TimeTicks::Now())));
+}
+
+void PredictionManager::OnModelEvaluated(
+    std::unique_ptr<PredictionDecisionParams> params,
+    machine_learning::mojom::DecisionTreePredictionResult result,
+    double prediction_score) {
+  SEQUENCE_CHECKER(sequence_checker_);
+
+  if (result !=
+      machine_learning::mojom::DecisionTreePredictionResult::kUnknown) {
+    UmaHistogramTimes(
+        "OptimizationGuide.PredictionModelEvaluationLatency." +
+            GetStringNameForOptimizationTarget(params->optimization_target),
+        base::TimeTicks::Now() - params->model_evaluation_start_time);
+  }
+
+  if (params->navigation_data) {
+    params->navigation_data->SetModelVersionForOptimizationTarget(
+        params->optimization_target, params->version);
+    params->navigation_data->SetModelPredictionScoreForOptimizationTarget(
+        params->optimization_target, prediction_score);
+  }
+
+  if (optimization_guide::features::
+          ShouldOverrideOptimizationTargetDecisionForMetricsPurposes(
+              params->optimization_target)) {
+    std::move(params->callback)
+        .Run(optimization_guide::OptimizationTargetDecision::
+                 kModelPredictionHoldback);
+    return;
+  }
+
+  optimization_guide::OptimizationTargetDecision target_decision;
+  switch (result) {
+    case machine_learning::mojom::DecisionTreePredictionResult::kTrue:
+      target_decision =
+          optimization_guide::OptimizationTargetDecision::kPageLoadMatches;
+      break;
+    case machine_learning::mojom::DecisionTreePredictionResult::kFalse:
+      target_decision =
+          optimization_guide::OptimizationTargetDecision::kPageLoadDoesNotMatch;
+      break;
+    case machine_learning::mojom::DecisionTreePredictionResult::kUnknown:
+      target_decision =
+          optimization_guide::OptimizationTargetDecision::kUnknown;
+      break;
+  }
+
+  std::move(params->callback).Run(target_decision);
+}
+
 void PredictionManager::OnEffectiveConnectionTypeChanged(
     net::EffectiveConnectionType effective_connection_type) {
   SEQUENCE_CHECKER(sequence_checker_);
@@ -497,6 +668,16 @@
   return nullptr;
 }
 
+RemoteDecisionTreePredictor*
+PredictionManager::GetRemoteDecisionTreePredictorForTesting(
+    proto::OptimizationTarget optimization_target) const {
+  auto it =
+      optimization_target_remote_model_predictor_map_.find(optimization_target);
+  if (it != optimization_target_remote_model_predictor_map_.end())
+    return it->second.get();
+  return nullptr;
+}
+
 const HostModelFeaturesMRUCache*
 PredictionManager::GetHostModelFeaturesForTesting() const {
   return &host_model_features_cache_;
@@ -853,10 +1034,11 @@
 
     auto* service_connection =
         machine_learning::ServiceConnection::GetInstance();
+    auto pending_receiver = predictor_handle->BindNewPipeAndPassReceiver();
+    std::string model_string = model->SerializeAsString();
     service_connection->LoadDecisionTreeModel(
-        machine_learning::mojom::DecisionTreeModelSpec::New(
-            model->SerializeAsString()),
-        predictor_handle->BindNewPipeAndPassReceiver(),
+        machine_learning::mojom::DecisionTreeModelSpec::New(model_string),
+        std::move(pending_receiver),
         base::BindOnce(&PredictionManager::OnPredictionModelSentToMLService,
                        ui_weak_ptr_factory_.GetWeakPtr(), std::move(callback),
                        std::move(model), std::move(predictor_handle)));
@@ -870,6 +1052,7 @@
     std::unique_ptr<proto::PredictionModel> model,
     std::unique_ptr<RemoteDecisionTreePredictor> predictor_handle,
     machine_learning::mojom::LoadModelResult result) {
+  SEQUENCE_CHECKER(sequence_checker_);
   proto::OptimizationTarget target = model->model_info().optimization_target();
   ScopedPredictionModelConstructionAndValidationRecorder
       prediction_model_recorder(target);
diff --git a/chrome/browser/optimization_guide/prediction/prediction_manager.h b/chrome/browser/optimization_guide/prediction/prediction_manager.h
index 31f5c30b..4095549e 100644
--- a/chrome/browser/optimization_guide/prediction/prediction_manager.h
+++ b/chrome/browser/optimization_guide/prediction/prediction_manager.h
@@ -54,6 +54,10 @@
 class TopHostProvider;
 class RemoteDecisionTreePredictor;
 
+// Parameters to be passed to PredictionManager::OnModelEvaluated for post
+// processing after the model prediction decision and score are obtained.
+struct PredictionDecisionParams;
+
 using HostModelFeaturesMRUCache =
     base::HashingMRUCache<std::string, base::flat_map<std::string, float>>;
 
@@ -98,7 +102,7 @@
   // Determine if the navigation matches the criteria for
   // |optimization_target|. Return kUnknown if a PredictionModel for the
   // optimization target is not registered and kModelNotAvailableOnClient if the
-  // if model for the optimization target is not currently on the client.
+  // model for the optimization target is not currently on the client.
   // If the model for the optimization target requires a client model feature
   // that is present in |override_client_model_feature_values|, the value from
   // |override_client_model_feature_values| will be used. The client will
@@ -112,6 +116,22 @@
       const base::flat_map<proto::ClientModelFeature, float>&
           override_client_model_feature_values);
 
+  // Invokes |callback| with the decision for whether the navigation matches the
+  // criteria for |optimization_target|. Passes kUnknown if a PredictionModel
+  // for the optimization target is not registered
+  // and kModelNotAvailableOnClient if the model for the optimization target is
+  // not currently on the client.
+  //
+  // Values provided in |client_model_feature_values| will be used over any
+  // values for features required by the model that may be calculated by the
+  // Optimization Guide.
+  void ShouldTargetNavigationAsync(
+      content::NavigationHandle* navigation_handle,
+      proto::OptimizationTarget optimization_target,
+      const base::flat_map<proto::ClientModelFeature, float>&
+          override_client_model_feature_values,
+      OptimizationTargetDecisionCallback callback);
+
   // Update |session_fcp_| and |previous_fcp_| with |fcp|.
   void UpdateFCPSessionStatistics(base::TimeDelta fcp);
 
@@ -161,6 +181,11 @@
   PredictionModel* GetPredictionModelForTesting(
       proto::OptimizationTarget optimization_target) const;
 
+  // Return the remote model predictor handle for the optimization target used
+  // by this PredictionManager for testing.
+  RemoteDecisionTreePredictor* GetRemoteDecisionTreePredictorForTesting(
+      proto::OptimizationTarget optimization_target) const;
+
   // Return the host model features for all hosts used by this
   // PredictionManager for testing.
   const HostModelFeaturesMRUCache* GetHostModelFeaturesForTesting() const;
@@ -308,6 +333,14 @@
   bool ProcessAndStoreHostModelFeatures(
       const proto::HostModelFeatures& host_model_features);
 
+  // Callback to be passed to the ML Service via the predictor handle and to
+  // retrieve |result| and |prediction_score|. Performs post processing using
+  // information passed via |params|.
+  void OnModelEvaluated(
+      std::unique_ptr<PredictionDecisionParams> params,
+      machine_learning::mojom::DecisionTreePredictionResult result,
+      double prediction_score);
+
   // Return the time when a prediction model and host model features fetch was
   // last attempted.
   base::Time GetLastFetchAttemptTime() const;
diff --git a/chrome/browser/optimization_guide/prediction/prediction_manager_browsertest.cc b/chrome/browser/optimization_guide/prediction/prediction_manager_browsertest.cc
index 6b660f19..8bfe3553 100644
--- a/chrome/browser/optimization_guide/prediction/prediction_manager_browsertest.cc
+++ b/chrome/browser/optimization_guide/prediction/prediction_manager_browsertest.cc
@@ -191,7 +191,7 @@
 class OptimizationGuideConsumerWebContentsObserver
     : public content::WebContentsObserver {
  public:
-  OptimizationGuideConsumerWebContentsObserver(
+  explicit OptimizationGuideConsumerWebContentsObserver(
       content::WebContents* web_contents)
       : content::WebContentsObserver(web_contents) {}
   ~OptimizationGuideConsumerWebContentsObserver() override = default;
@@ -260,39 +260,27 @@
 
 namespace optimization_guide {
 
-class PredictionManagerBrowserTest
-    : public InProcessBrowserTest,
-      public ::testing::WithParamInterface<bool> {
+// Abstract base class for browser testing Prediction Manager.
+// Actual class fixtures should implement InitializeFeatureList to set up
+// features used in tests.
+class PredictionManagerBrowserTestBase : public InProcessBrowserTest {
  public:
-  PredictionManagerBrowserTest() : using_ml_service_(GetParam()) {}
-  ~PredictionManagerBrowserTest() override = default;
+  PredictionManagerBrowserTestBase() = default;
+  ~PredictionManagerBrowserTestBase() override = default;
 
-  PredictionManagerBrowserTest(const PredictionManagerBrowserTest&) = delete;
-  PredictionManagerBrowserTest& operator=(const PredictionManagerBrowserTest&) =
+  PredictionManagerBrowserTestBase(const PredictionManagerBrowserTestBase&) =
       delete;
+  PredictionManagerBrowserTestBase& operator=(
+      const PredictionManagerBrowserTestBase&) = delete;
 
   void SetUp() override {
-    if (using_ml_service_) {
-      scoped_feature_list_.InitWithFeatures(
-          {optimization_guide::features::kOptimizationHints,
-           optimization_guide::features::kRemoteOptimizationGuideFetching,
-           optimization_guide::features::kOptimizationTargetPrediction,
-           optimization_guide::features::
-               kOptimizationTargetPredictionUsingMLService},
-          {});
-    } else {
-      scoped_feature_list_.InitWithFeatures(
-          {optimization_guide::features::kOptimizationHints,
-           optimization_guide::features::kRemoteOptimizationGuideFetching,
-           optimization_guide::features::kOptimizationTargetPrediction},
-          {});
-    }
+    InitializeFeatureList();
 
     models_server_ = std::make_unique<net::EmbeddedTestServer>(
         net::EmbeddedTestServer::TYPE_HTTPS);
     models_server_->ServeFilesFromSourceDirectory("chrome/test/data/previews");
     models_server_->RegisterRequestHandler(base::BindRepeating(
-        &PredictionManagerBrowserTest::HandleGetModelsRequest,
+        &PredictionManagerBrowserTestBase::HandleGetModelsRequest,
         base::Unretained(this)));
 
     ASSERT_TRUE(models_server_->Start());
@@ -302,16 +290,16 @@
   void SetUpOnMainThread() override {
     content::NetworkConnectionChangeSimulator().SetConnectionType(
         network::mojom::ConnectionType::CONNECTION_2G);
-    https_server_.reset(
-        new net::EmbeddedTestServer(net::EmbeddedTestServer::TYPE_HTTPS));
+    https_server_ = std::make_unique<net::EmbeddedTestServer>(
+        net::EmbeddedTestServer::TYPE_HTTPS);
     https_server_->ServeFilesFromSourceDirectory(GetChromeTestDataDir());
     ASSERT_TRUE(https_server_->Start());
     https_url_with_content_ = https_server_->GetURL("/english_page.html");
     https_url_without_content_ = https_server_->GetURL("/empty.html");
 
     // Set up an OptimizationGuideKeyedService consumer.
-    consumer_.reset(new OptimizationGuideConsumerWebContentsObserver(
-        browser()->tab_strip_model()->GetActiveWebContents()));
+    consumer_ = std::make_unique<OptimizationGuideConsumerWebContentsObserver>(
+        browser()->tab_strip_model()->GetActiveWebContents());
 
     InProcessBrowserTest::SetUpOnMainThread();
   }
@@ -387,6 +375,11 @@
   GURL https_url_without_content() { return https_url_without_content_; }
 
  protected:
+  // Virtualize for testing different feature configurations.
+  virtual void InitializeFeatureList() = 0;
+
+  base::test::ScopedFeatureList scoped_feature_list_;
+
   // Feature that the model server should return in response to
   // GetModelsRequest.
   proto::ClientModelFeature client_model_feature_ =
@@ -428,13 +421,48 @@
   GURL https_url_with_content_, https_url_without_content_;
   std::unique_ptr<net::EmbeddedTestServer> https_server_;
   std::unique_ptr<net::EmbeddedTestServer> models_server_;
-  base::test::ScopedFeatureList scoped_feature_list_;
   PredictionModelsFetcherRemoteResponseType response_type_ =
       PredictionModelsFetcherRemoteResponseType::
           kSuccessfulWithModelsAndFeatures;
   std::unique_ptr<OptimizationGuideConsumerWebContentsObserver> consumer_;
 };
 
+// Parametrized on whether the ML Service path is enabled.
+class PredictionManagerBrowserTest
+    : public PredictionManagerBrowserTestBase,
+      public ::testing::WithParamInterface<bool> {
+ public:
+  PredictionManagerBrowserTest() : using_ml_service_(GetParam()) {}
+  ~PredictionManagerBrowserTest() override = default;
+
+  PredictionManagerBrowserTest(const PredictionManagerBrowserTest&) = delete;
+  PredictionManagerBrowserTest& operator=(const PredictionManagerBrowserTest&) =
+      delete;
+
+  bool using_ml_service() const { return using_ml_service_; }
+
+ private:
+  void InitializeFeatureList() override {
+    if (using_ml_service_) {
+      scoped_feature_list_.InitWithFeatures(
+          {optimization_guide::features::kOptimizationHints,
+           optimization_guide::features::kRemoteOptimizationGuideFetching,
+           optimization_guide::features::kOptimizationTargetPrediction,
+           optimization_guide::features::
+               kOptimizationTargetPredictionUsingMLService},
+          {});
+    } else {
+      scoped_feature_list_.InitWithFeatures(
+          {optimization_guide::features::kOptimizationHints,
+           optimization_guide::features::kRemoteOptimizationGuideFetching,
+           optimization_guide::features::kOptimizationTargetPrediction},
+          {});
+    }
+  }
+
+  bool using_ml_service_ = false;
+};
+
 #if defined(OS_WIN) || defined(OS_MAC) || defined(OS_CHROMEOS)
 #define DISABLE_ON_WIN_MAC_CHROMEOS(x) DISABLED_##x
 #else
@@ -443,7 +471,8 @@
 
 INSTANTIATE_TEST_SUITE_P(UsingMLService,
                          PredictionManagerBrowserTest,
-                         ::testing::Bool());
+                         ::testing::Bool(),
+                         ::testing::PrintToStringParamName());
 
 IN_PROC_BROWSER_TEST_P(
     PredictionManagerBrowserTest,
@@ -596,14 +625,6 @@
 IN_PROC_BROWSER_TEST_P(
     PredictionManagerBrowserTest,
     DISABLE_ON_WIN_MAC_CHROMEOS(HostModelFeaturesClearedOnHistoryClear)) {
-  if (using_ml_service()) {
-    // Skipped for the ML Service path because ShouldTargetNavigation has not
-    // been migrated.
-    // TODO(crbug/1099371): Enable this after adding ML Service integration to
-    // ShouldTargetNavigation.
-    GTEST_SKIP();
-  }
-
   base::HistogramTester histogram_tester;
   MLServiceProcessObserver ml_service_observer;
 
@@ -661,19 +682,17 @@
   }
 };
 
-// Disabled for the ML Service path because ShouldTargetNavigation has not
-// been migrated.
-// TODO(crbug/1099371): Enable this after adding ML Service integration to
-// ShouldTargetNavigation.
 INSTANTIATE_TEST_SUITE_P(UsingMLService,
                          PredictionManagerBrowserSameOriginTest,
-                         ::testing::Values(false));
+                         ::testing::Bool(),
+                         ::testing::PrintToStringParamName());
 
 // Regression test for https://crbug.com/1037945. Tests that the origin of the
 // previous navigation is computed correctly.
 IN_PROC_BROWSER_TEST_P(PredictionManagerBrowserSameOriginTest,
                        DISABLE_ON_WIN_MAC_CHROMEOS(IsSameOriginNavigation)) {
   base::HistogramTester histogram_tester;
+  MLServiceProcessObserver ml_service_observer;
 
   RegisterWithKeyedService();
 
@@ -691,6 +710,12 @@
       &histogram_tester,
       "OptimizationGuide.PredictionManager.PredictionModelsStored", 1);
 
+  RetryForHistogramUntilCountReached(
+      &histogram_tester,
+      "OptimizationGuide.PredictionModelLoadedVersion.PainfulPageLoad", 1);
+
+  EXPECT_EQ(ml_service_observer.IsLaunched(), using_ml_service());
+
   SetCallbackOnConsumer(base::DoNothing());
   ui_test_utils::NavigateToURL(browser(), https_url_with_content());
   RetryForHistogramUntilCountReached(
@@ -725,6 +750,7 @@
     PredictionManagerBrowserSameOriginTest,
     DISABLE_ON_WIN_MAC_CHROMEOS(ShouldTargetNavigationAsync)) {
   base::HistogramTester histogram_tester;
+  MLServiceProcessObserver ml_service_observer;
 
   RegisterWithKeyedService();
 
@@ -742,6 +768,12 @@
       &histogram_tester,
       "OptimizationGuide.PredictionManager.PredictionModelsStored", 1);
 
+  RetryForHistogramUntilCountReached(
+      &histogram_tester,
+      "OptimizationGuide.PredictionModelLoadedVersion.PainfulPageLoad", 1);
+
+  EXPECT_EQ(ml_service_observer.IsLaunched(), using_ml_service());
+
   std::unique_ptr<base::RunLoop> run_loop = std::make_unique<base::RunLoop>();
   SetCallbackOnConsumer(base::BindOnce(
       [](base::RunLoop* run_loop,
@@ -759,4 +791,119 @@
   run_loop->Run();
 }
 
+class PredictionManagerUsingMLServiceBrowserSameOriginTest
+    : public PredictionManagerBrowserSameOriginTest {};
+
+// Only instantiate with ML Service enabled.
+INSTANTIATE_TEST_SUITE_P(UsingMLService,
+                         PredictionManagerUsingMLServiceBrowserSameOriginTest,
+                         ::testing::Values(true),
+                         ::testing::PrintToStringParamName());
+
+IN_PROC_BROWSER_TEST_P(
+    PredictionManagerUsingMLServiceBrowserSameOriginTest,
+    DISABLE_ON_WIN_MAC_CHROMEOS(
+        ShouldTargetNavigationAsyncWithServiceDisconnection)) {
+  base::HistogramTester histogram_tester;
+  MLServiceProcessObserver ml_service_observer;
+
+  RegisterWithKeyedService();
+
+  // Wait until histograms have been updated before performing checks for
+  // correct behavior based on the response.
+  RetryForHistogramUntilCountReached(
+      &histogram_tester,
+      "OptimizationGuide.PredictionModelFetcher.GetModelsResponse.Status", 1);
+
+  RetryForHistogramUntilCountReached(
+      &histogram_tester,
+      "OptimizationGuide.PredictionManager.HostModelFeaturesStored", 1);
+
+  RetryForHistogramUntilCountReached(
+      &histogram_tester,
+      "OptimizationGuide.PredictionManager.PredictionModelsStored", 1);
+
+  RetryForHistogramUntilCountReached(
+      &histogram_tester,
+      "OptimizationGuide.PredictionModelLoadedVersion.PainfulPageLoad", 1);
+
+  EXPECT_TRUE(ml_service_observer.IsLaunched());
+
+  // Force termination of the service: model predictors will become invalid.
+  machine_learning::ServiceConnection::GetInstance()->ResetServiceForTesting();
+
+  SetCallbackOnConsumer(base::BindOnce(
+      [](OptimizationGuideConsumerWebContentsObserver* consumer,
+         optimization_guide::OptimizationGuideDecision decision) {
+        EXPECT_EQ(decision,
+                  optimization_guide::OptimizationGuideDecision::kUnknown);
+      },
+      consumer()));
+
+  ui_test_utils::NavigateToURL(browser(), https_url_with_content());
+}
+
+class PredictionManagerUsingMLServiceMetricsOnlyBrowserTest
+    : public PredictionManagerBrowserTestBase {
+ public:
+  PredictionManagerUsingMLServiceMetricsOnlyBrowserTest() = default;
+  ~PredictionManagerUsingMLServiceMetricsOnlyBrowserTest() override = default;
+
+ private:
+  void InitializeFeatureList() override {
+    scoped_feature_list_.InitWithFeaturesAndParameters(
+        {{optimization_guide::features::kOptimizationHints, {}},
+         {optimization_guide::features::kRemoteOptimizationGuideFetching, {}},
+         {optimization_guide::features::kOptimizationTargetPrediction,
+          {{"painful_page_load_metrics_only", "true"}}},
+         {optimization_guide::features::
+              kOptimizationTargetPredictionUsingMLService,
+          {}}},
+        {});
+  }
+};
+
+IN_PROC_BROWSER_TEST_F(
+    PredictionManagerUsingMLServiceMetricsOnlyBrowserTest,
+    DISABLE_ON_WIN_MAC_CHROMEOS(ShouldTargetNavigationAsync)) {
+  base::HistogramTester histogram_tester;
+  MLServiceProcessObserver ml_service_observer;
+
+  EXPECT_TRUE(
+      features::ShouldOverrideOptimizationTargetDecisionForMetricsPurposes(
+          proto::OptimizationTarget::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD));
+
+  RegisterWithKeyedService();
+
+  // Wait until histograms have been updated before performing checks for
+  // correct behavior based on the response.
+  RetryForHistogramUntilCountReached(
+      &histogram_tester,
+      "OptimizationGuide.PredictionModelFetcher.GetModelsResponse.Status", 1);
+
+  RetryForHistogramUntilCountReached(
+      &histogram_tester,
+      "OptimizationGuide.PredictionManager.HostModelFeaturesStored", 1);
+
+  RetryForHistogramUntilCountReached(
+      &histogram_tester,
+      "OptimizationGuide.PredictionManager.PredictionModelsStored", 1);
+
+  RetryForHistogramUntilCountReached(
+      &histogram_tester,
+      "OptimizationGuide.PredictionModelLoadedVersion.PainfulPageLoad", 1);
+
+  EXPECT_TRUE(ml_service_observer.IsLaunched());
+
+  SetCallbackOnConsumer(base::BindOnce(
+      [](OptimizationGuideConsumerWebContentsObserver* consumer,
+         optimization_guide::OptimizationGuideDecision decision) {
+        EXPECT_EQ(decision,
+                  optimization_guide::OptimizationGuideDecision::kFalse);
+      },
+      consumer()));
+
+  ui_test_utils::NavigateToURL(browser(), https_url_with_content());
+}
+
 }  // namespace optimization_guide
diff --git a/chrome/browser/optimization_guide/prediction/prediction_manager_unittest.cc b/chrome/browser/optimization_guide/prediction/prediction_manager_unittest.cc
index ee03b9d..14c7275d1 100644
--- a/chrome/browser/optimization_guide/prediction/prediction_manager_unittest.cc
+++ b/chrome/browser/optimization_guide/prediction/prediction_manager_unittest.cc
@@ -18,6 +18,9 @@
 #include "chrome/browser/optimization_guide/optimization_guide_util.h"
 #include "chrome/browser/optimization_guide/optimization_guide_web_contents_observer.h"
 #include "chrome/browser/optimization_guide/prediction/prediction_model_fetcher.h"
+#include "chrome/browser/optimization_guide/prediction/remote_decision_tree_predictor.h"
+#include "chrome/services/machine_learning/public/cpp/test_support/fake_service_connection.h"
+#include "chrome/services/machine_learning/public/mojom/decision_tree.mojom.h"
 #include "chrome/test/base/testing_profile.h"
 #include "components/leveldb_proto/testing/fake_db.h"
 #include "components/optimization_guide/optimization_guide_features.h"
@@ -320,6 +323,7 @@
   bool WasHostModelFeaturesLoaded() const {
     return host_model_features_loaded_;
   }
+
  private:
   base::OnceClosure init_callback_;
   base::OnceClosure update_host_models_callback_;
@@ -369,6 +373,7 @@
   using PredictionManager::GetHostModelFeaturesForHost;
   using PredictionManager::GetHostModelFeaturesForTesting;
   using PredictionManager::GetPredictionModelForTesting;
+  using PredictionManager::GetRemoteDecisionTreePredictorForTesting;
 
   std::unique_ptr<OptimizationGuideStore>
   CreateModelAndHostModelFeaturesStore() {
@@ -396,12 +401,14 @@
 };
 
 class PredictionManagerTest
-    : public optimization_guide::ProtoDatabaseProviderTestBase,
-      public testing::WithParamInterface<proto::ClientModelFeature> {
+    : public optimization_guide::ProtoDatabaseProviderTestBase {
  public:
   PredictionManagerTest() = default;
   ~PredictionManagerTest() override = default;
 
+  PredictionManagerTest(const PredictionManagerTest&) = delete;
+  PredictionManagerTest& operator=(const PredictionManagerTest&) = delete;
+
   void SetUp() override {
     optimization_guide::ProtoDatabaseProviderTestBase::SetUp();
     web_contents_factory_ = std::make_unique<content::TestWebContentsFactory>();
@@ -464,14 +471,6 @@
     return navigation_handle;
   }
 
-  bool IsSameOriginNavigationFeature() {
-    return GetParam() == proto::CLIENT_MODEL_FEATURE_SAME_ORIGIN_NAVIGATION;
-  }
-
-  bool IsUnknownFeature() {
-    return GetParam() == proto::CLIENT_MODEL_FEATURE_UNKNOWN;
-  }
-
   void TearDown() override {
     optimization_guide::ProtoDatabaseProviderTestBase::TearDown();
   }
@@ -533,8 +532,6 @@
   TestingProfile testing_profile_;
   std::unique_ptr<TestingPrefServiceSimple> pref_service_;
   std::unique_ptr<content::TestWebContentsFactory> web_contents_factory_;
-
-  DISALLOW_COPY_AND_ASSIGN(PredictionManagerTest);
 };
 
 // No support for Mac, Windows or ChromeOS.
@@ -544,6 +541,64 @@
 #define DISABLE_ON_WIN_MAC_CHROMEOS(x) x
 #endif
 
+class PredictionManagerMLServiceTest
+    : public PredictionManagerTest,
+      public testing::WithParamInterface<bool> {
+ public:
+  PredictionManagerMLServiceTest() = default;
+  ~PredictionManagerMLServiceTest() override = default;
+
+  PredictionManagerMLServiceTest(const PredictionManagerMLServiceTest&) =
+      delete;
+  PredictionManagerMLServiceTest& operator=(
+      const PredictionManagerMLServiceTest&) = delete;
+
+  void SetUp() override {
+    service_connection_ =
+        std::make_unique<machine_learning::testing::FakeServiceConnection>();
+    service_connection_->SetAsyncModeForTesting(false);
+
+    PredictionManagerTest::SetUp();
+  }
+
+  void TearDown() override {
+    PredictionManagerTest::TearDown();
+    service_connection_.reset();
+  }
+
+  bool UsingMLService() const { return GetParam(); }
+
+  void SetLoadModelResult(machine_learning::mojom::LoadModelResult result) {
+    if (UsingMLService())
+      service_connection_->SetLoadModelResult(result);
+  }
+
+  void SetDecisionTreePredictionResult(
+      machine_learning::mojom::DecisionTreePredictionResult result,
+      double score) {
+    if (UsingMLService())
+      service_connection_->SetDecisionTreePredictionResult(result, score);
+  }
+
+  void RunScheduledCalls() {
+    if (UsingMLService())
+      service_connection_->RunScheduledCalls();
+  }
+
+  void ResetMLService() {
+    if (UsingMLService())
+      service_connection_->ResetServiceForTesting();
+  }
+
+ protected:
+  std::unique_ptr<machine_learning::testing::FakeServiceConnection>
+      service_connection_;
+};
+
+INSTANTIATE_TEST_SUITE_P(UsingMLService,
+                         PredictionManagerMLServiceTest,
+                         ::testing::Bool());
+
 TEST_F(PredictionManagerTest,
        OptimizationTargetProvidedAtInitializationIsRegistered) {
   CreatePredictionManager(
@@ -552,7 +607,18 @@
   EXPECT_FALSE(prediction_manager()->registered_optimization_targets().empty());
 }
 
-TEST_F(PredictionManagerTest, OptimizationTargetNotRegisteredForNavigation) {
+TEST_P(PredictionManagerMLServiceTest,
+       OptimizationTargetNotRegisteredForNavigation) {
+  base::test::ScopedFeatureList scoped_feature_list;
+  if (UsingMLService()) {
+    scoped_feature_list.InitWithFeatures(
+        {optimization_guide::features::
+             kOptimizationTargetPredictionUsingMLService},
+        {});
+
+    SetLoadModelResult(machine_learning::mojom::LoadModelResult::kOk);
+  }
+
   base::HistogramTester histogram_tester;
   std::unique_ptr<content::MockNavigationHandle> navigation_handle =
       CreateMockNavigationHandleWithOptimizationGuideWebContentsObserver(
@@ -571,10 +637,22 @@
 
   EXPECT_TRUE(prediction_model_fetcher()->models_fetched());
 
-  EXPECT_EQ(
-      OptimizationTargetDecision::kUnknown,
-      prediction_manager()->ShouldTargetNavigation(
-          navigation_handle.get(), proto::OPTIMIZATION_TARGET_UNKNOWN, {}));
+  if (UsingMLService()) {
+    prediction_manager()->ShouldTargetNavigationAsync(
+        navigation_handle.get(), proto::OPTIMIZATION_TARGET_UNKNOWN, {},
+        base::BindOnce([](OptimizationTargetDecision decision) {
+          EXPECT_EQ(OptimizationTargetDecision::kUnknown, decision);
+        }));
+
+    // Flush the Service connection pipe.
+    RunUntilIdle();
+  } else {
+    EXPECT_EQ(
+        OptimizationTargetDecision::kUnknown,
+        prediction_manager()->ShouldTargetNavigation(
+            navigation_handle.get(), proto::OPTIMIZATION_TARGET_UNKNOWN, {}));
+  }
+
   // OptimizationGuideNavData should not be populated.
   OptimizationGuideNavigationData* nav_data =
       OptimizationGuideNavigationData::GetFromNavigationHandle(
@@ -599,19 +677,40 @@
       0);
 }
 
-TEST_F(PredictionManagerTest,
+TEST_P(PredictionManagerMLServiceTest,
        DISABLE_ON_WIN_MAC_CHROMEOS(
            NoPredictionModelForRegisteredOptimizationTarget)) {
+  base::test::ScopedFeatureList scoped_feature_list;
+  if (UsingMLService()) {
+    scoped_feature_list.InitWithFeatures(
+        {optimization_guide::features::
+             kOptimizationTargetPredictionUsingMLService},
+        {});
+  }
+
   base::HistogramTester histogram_tester;
   std::unique_ptr<content::MockNavigationHandle> navigation_handle =
       CreateMockNavigationHandleWithOptimizationGuideWebContentsObserver(
           GURL("https://foo.com"));
 
   CreatePredictionManager({proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD});
-  EXPECT_EQ(OptimizationTargetDecision::kModelNotAvailableOnClient,
-            prediction_manager()->ShouldTargetNavigation(
-                navigation_handle.get(),
-                proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD, {}));
+
+  if (UsingMLService()) {
+    prediction_manager()->ShouldTargetNavigationAsync(
+        navigation_handle.get(), proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD,
+        {}, base::BindOnce([](OptimizationTargetDecision decision) {
+          EXPECT_EQ(OptimizationTargetDecision::kModelNotAvailableOnClient,
+                    decision);
+        }));
+
+    // Flush the Service connection pipe.
+    RunUntilIdle();
+  } else {
+    EXPECT_EQ(OptimizationTargetDecision::kModelNotAvailableOnClient,
+              prediction_manager()->ShouldTargetNavigation(
+                  navigation_handle.get(),
+                  proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD, {}));
+  }
 
   // OptimizationGuideNavData should not be populated.
   OptimizationGuideNavigationData* nav_data =
@@ -635,7 +734,17 @@
       0);
 }
 
-TEST_F(PredictionManagerTest, EvaluatePredictionModel) {
+TEST_P(PredictionManagerMLServiceTest, EvaluatePredictionModel) {
+  base::test::ScopedFeatureList scoped_feature_list;
+  if (UsingMLService()) {
+    scoped_feature_list.InitWithFeatures(
+        {optimization_guide::features::
+             kOptimizationTargetPredictionUsingMLService},
+        {});
+
+    SetLoadModelResult(machine_learning::mojom::LoadModelResult::kOk);
+  }
+
   base::HistogramTester histogram_tester;
   std::unique_ptr<content::MockNavigationHandle> navigation_handle =
       CreateMockNavigationHandleWithOptimizationGuideWebContentsObserver(
@@ -652,17 +761,31 @@
   SetStoreInitialized();
   EXPECT_TRUE(prediction_model_fetcher()->models_fetched());
 
-  EXPECT_EQ(OptimizationTargetDecision::kPageLoadMatches,
-            prediction_manager()->ShouldTargetNavigation(
-                navigation_handle.get(),
-                proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD, {}));
+  if (UsingMLService()) {
+    SetDecisionTreePredictionResult(
+        machine_learning::mojom::DecisionTreePredictionResult::kTrue,
+        /* score */ 0.6);
 
-  TestPredictionModel* test_prediction_model =
-      static_cast<TestPredictionModel*>(
-          prediction_manager()->GetPredictionModelForTesting(
-              proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD));
-  EXPECT_TRUE(test_prediction_model);
-  EXPECT_TRUE(test_prediction_model->WasModelEvaluated());
+    prediction_manager()->ShouldTargetNavigationAsync(
+        navigation_handle.get(), proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD,
+        {}, base::BindOnce([](OptimizationTargetDecision decision) {
+          EXPECT_EQ(OptimizationTargetDecision::kPageLoadMatches, decision);
+        }));
+
+    // Flush the Service connection pipe.
+    RunUntilIdle();
+  } else {
+    EXPECT_EQ(OptimizationTargetDecision::kPageLoadMatches,
+              prediction_manager()->ShouldTargetNavigation(
+                  navigation_handle.get(),
+                  proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD, {}));
+    TestPredictionModel* test_prediction_model =
+        static_cast<TestPredictionModel*>(
+            prediction_manager()->GetPredictionModelForTesting(
+                proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD));
+    EXPECT_TRUE(test_prediction_model);
+    EXPECT_TRUE(test_prediction_model->WasModelEvaluated());
+  }
 
   histogram_tester.ExpectTotalCount(
       "OptimizationGuide.PredictionModelEvaluationLatency." +
@@ -688,7 +811,18 @@
       "OptimizationGuide.PredictionModelValidationLatency", 1);
 }
 
-TEST_F(PredictionManagerTest, UpdatePredictionModelsWithInvalidModel) {
+TEST_P(PredictionManagerMLServiceTest, UpdatePredictionModelsWithInvalidModel) {
+  base::test::ScopedFeatureList scoped_feature_list;
+  if (UsingMLService()) {
+    scoped_feature_list.InitWithFeatures(
+        {optimization_guide::features::
+             kOptimizationTargetPredictionUsingMLService},
+        {});
+
+    SetLoadModelResult(
+        machine_learning::mojom::LoadModelResult::kLoadModelError);
+  }
+
   base::HistogramTester histogram_tester;
   CreatePredictionManager({});
   prediction_manager()->SetPredictionModelFetcherForTesting(
@@ -718,7 +852,17 @@
       "OptimizationGuide.PredictionModelLoadedVersion.PainfulPageLoad", 0);
 }
 
-TEST_F(PredictionManagerTest, UpdateModelWithSameVersion) {
+TEST_P(PredictionManagerMLServiceTest, UpdateModelWithSameVersion) {
+  base::test::ScopedFeatureList scoped_feature_list;
+  if (UsingMLService()) {
+    scoped_feature_list.InitWithFeatures(
+        {optimization_guide::features::
+             kOptimizationTargetPredictionUsingMLService},
+        {});
+
+    SetLoadModelResult(machine_learning::mojom::LoadModelResult::kOk);
+  }
+
   base::HistogramTester histogram_tester;
   CreatePredictionManager({});
   prediction_manager()->SetPredictionModelFetcherForTesting(
@@ -748,19 +892,36 @@
   prediction_manager()->UpdatePredictionModelsForTesting(
       get_models_response.get());
 
-  TestPredictionModel* stored_prediction_model =
-      static_cast<TestPredictionModel*>(
-          prediction_manager()->GetPredictionModelForTesting(
-              proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD));
-  EXPECT_TRUE(stored_prediction_model);
-  EXPECT_EQ(3, stored_prediction_model->GetVersion());
-
+  if (UsingMLService()) {
+    RemoteDecisionTreePredictor* stored_predictor_handle =
+        prediction_manager()->GetRemoteDecisionTreePredictorForTesting(
+            proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD);
+    EXPECT_TRUE(stored_predictor_handle);
+    EXPECT_EQ(3, stored_predictor_handle->version());
+  } else {
+    TestPredictionModel* stored_prediction_model =
+        static_cast<TestPredictionModel*>(
+            prediction_manager()->GetPredictionModelForTesting(
+                proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD));
+    EXPECT_TRUE(stored_prediction_model);
+    EXPECT_EQ(3, stored_prediction_model->GetVersion());
+  }
   histogram_tester.ExpectBucketCount("OptimizationGuide.IsPredictionModelValid",
                                      true, 2);
 }
 
-TEST_F(PredictionManagerTest,
+TEST_P(PredictionManagerMLServiceTest,
        EvaluatePredictionModelUsesDecisionFromPostiveEvalIfModelWasEvaluated) {
+  base::test::ScopedFeatureList scoped_feature_list;
+  if (UsingMLService()) {
+    scoped_feature_list.InitWithFeatures(
+        {optimization_guide::features::
+             kOptimizationTargetPredictionUsingMLService},
+        {});
+
+    SetLoadModelResult(machine_learning::mojom::LoadModelResult::kOk);
+  }
+
   std::unique_ptr<content::MockNavigationHandle> navigation_handle =
       CreateMockNavigationHandleWithOptimizationGuideWebContentsObserver(
           GURL("https://foo.com"));
@@ -784,23 +945,54 @@
       proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD,
       OptimizationTargetDecision::kPageLoadMatches);
 
-  TestPredictionModel* test_prediction_model =
-      static_cast<TestPredictionModel*>(
-          prediction_manager()->GetPredictionModelForTesting(
-              proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD));
-  EXPECT_TRUE(test_prediction_model);
+  if (UsingMLService()) {
+    RemoteDecisionTreePredictor* predictor_handle =
+        prediction_manager()->GetRemoteDecisionTreePredictorForTesting(
+            proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD);
+    EXPECT_TRUE(predictor_handle);
 
-  // Make sure the cached decision is returned and that the model was not
-  // evaluated.
-  EXPECT_EQ(OptimizationTargetDecision::kPageLoadMatches,
-            prediction_manager()->ShouldTargetNavigation(
-                navigation_handle.get(),
-                proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD, {}));
-  EXPECT_FALSE(test_prediction_model->WasModelEvaluated());
+    // Set ML prediction result to False to ensure the actual model is not run.
+    SetDecisionTreePredictionResult(
+        machine_learning::mojom::DecisionTreePredictionResult::kUnknown,
+        /* score */ 0.0);
+
+    prediction_manager()->ShouldTargetNavigationAsync(
+        navigation_handle.get(), proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD,
+        {}, base::BindOnce([](OptimizationTargetDecision decision) {
+          EXPECT_EQ(OptimizationTargetDecision::kPageLoadMatches, decision);
+        }));
+
+    // Flush the Service connection pipe.
+    RunUntilIdle();
+  } else {
+    TestPredictionModel* test_prediction_model =
+        static_cast<TestPredictionModel*>(
+            prediction_manager()->GetPredictionModelForTesting(
+                proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD));
+    EXPECT_TRUE(test_prediction_model);
+
+    // Make sure the cached decision is returned and that the model was not
+    // evaluated.
+    EXPECT_EQ(OptimizationTargetDecision::kPageLoadMatches,
+              prediction_manager()->ShouldTargetNavigation(
+                  navigation_handle.get(),
+                  proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD, {}));
+    EXPECT_FALSE(test_prediction_model->WasModelEvaluated());
+  }
 }
 
-TEST_F(PredictionManagerTest,
+TEST_P(PredictionManagerMLServiceTest,
        EvaluatePredictionModelUsesDecisionFromNegativeEvalIfModelWasEvaluated) {
+  base::test::ScopedFeatureList scoped_feature_list;
+  if (UsingMLService()) {
+    scoped_feature_list.InitWithFeatures(
+        {optimization_guide::features::
+             kOptimizationTargetPredictionUsingMLService},
+        {});
+
+    SetLoadModelResult(machine_learning::mojom::LoadModelResult::kOk);
+  }
+
   base::HistogramTester histogram_tester;
   std::unique_ptr<content::MockNavigationHandle> navigation_handle =
       CreateMockNavigationHandleWithOptimizationGuideWebContentsObserver(
@@ -825,25 +1017,58 @@
       proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD,
       OptimizationTargetDecision::kPageLoadDoesNotMatch);
 
-  TestPredictionModel* test_prediction_model =
-      static_cast<TestPredictionModel*>(
-          prediction_manager()->GetPredictionModelForTesting(
-              proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD));
-  EXPECT_TRUE(test_prediction_model);
+  if (UsingMLService()) {
+    RemoteDecisionTreePredictor* predictor_handle =
+        prediction_manager()->GetRemoteDecisionTreePredictorForTesting(
+            proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD);
+    EXPECT_TRUE(predictor_handle);
 
-  // Make sure the previous decision is reused and that the model was not
-  // evaluated.
-  EXPECT_EQ(OptimizationTargetDecision::kPageLoadDoesNotMatch,
-            prediction_manager()->ShouldTargetNavigation(
-                navigation_handle.get(),
-                proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD, {}));
-  EXPECT_FALSE(test_prediction_model->WasModelEvaluated());
+    // Set ML prediction result to Unknown to ensure the actual model is not
+    // run.
+    SetDecisionTreePredictionResult(
+        machine_learning::mojom::DecisionTreePredictionResult::kUnknown,
+        /* score */ 0.0);
+
+    prediction_manager()->ShouldTargetNavigationAsync(
+        navigation_handle.get(), proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD,
+        {}, base::BindOnce([](OptimizationTargetDecision decision) {
+          EXPECT_EQ(OptimizationTargetDecision::kPageLoadDoesNotMatch,
+                    decision);
+        }));
+
+    // Flush the Service connection pipe.
+    RunUntilIdle();
+  } else {
+    TestPredictionModel* test_prediction_model =
+        static_cast<TestPredictionModel*>(
+            prediction_manager()->GetPredictionModelForTesting(
+                proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD));
+    EXPECT_TRUE(test_prediction_model);
+
+    // Make sure the previous decision is reused and that the model was not
+    // evaluated.
+    EXPECT_EQ(OptimizationTargetDecision::kPageLoadDoesNotMatch,
+              prediction_manager()->ShouldTargetNavigation(
+                  navigation_handle.get(),
+                  proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD, {}));
+    EXPECT_FALSE(test_prediction_model->WasModelEvaluated());
+  }
   histogram_tester.ExpectTotalCount(
       "OptimizationGuide.ShouldTargetNavigation.PredictionModelStatus", 0);
 }
 
-TEST_F(PredictionManagerTest,
+TEST_P(PredictionManagerMLServiceTest,
        EvaluatePredictionModelUsesDecisionFromHoldbackEvalIfModelWasEvaluated) {
+  base::test::ScopedFeatureList scoped_feature_list;
+  if (UsingMLService()) {
+    scoped_feature_list.InitWithFeatures(
+        {optimization_guide::features::
+             kOptimizationTargetPredictionUsingMLService},
+        {});
+
+    SetLoadModelResult(machine_learning::mojom::LoadModelResult::kOk);
+  }
+
   std::unique_ptr<content::MockNavigationHandle> navigation_handle =
       CreateMockNavigationHandleWithOptimizationGuideWebContentsObserver(
           GURL("https://foo.com"));
@@ -867,22 +1092,56 @@
       proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD,
       OptimizationTargetDecision::kModelPredictionHoldback);
 
-  TestPredictionModel* test_prediction_model =
-      static_cast<TestPredictionModel*>(
-          prediction_manager()->GetPredictionModelForTesting(
-              proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD));
-  EXPECT_TRUE(test_prediction_model);
+  if (UsingMLService()) {
+    RemoteDecisionTreePredictor* predictor_handle =
+        prediction_manager()->GetRemoteDecisionTreePredictorForTesting(
+            proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD);
+    EXPECT_TRUE(predictor_handle);
 
-  // Make sure the cached decision is returned and that the model was not
-  // evaluated.
-  EXPECT_EQ(OptimizationTargetDecision::kModelPredictionHoldback,
-            prediction_manager()->ShouldTargetNavigation(
-                navigation_handle.get(),
-                proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD, {}));
-  EXPECT_FALSE(test_prediction_model->WasModelEvaluated());
+    // Set ML prediction result to Unknown to ensure the actual model is not
+    // run.
+    SetDecisionTreePredictionResult(
+        machine_learning::mojom::DecisionTreePredictionResult::kUnknown,
+        /* score */ 0.0);
+
+    prediction_manager()->ShouldTargetNavigationAsync(
+        navigation_handle.get(), proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD,
+        {}, base::BindOnce([](OptimizationTargetDecision decision) {
+          EXPECT_EQ(OptimizationTargetDecision::kModelPredictionHoldback,
+                    decision);
+        }));
+
+    // Flush the Service connection pipe.
+    RunUntilIdle();
+  } else {
+    TestPredictionModel* test_prediction_model =
+        static_cast<TestPredictionModel*>(
+            prediction_manager()->GetPredictionModelForTesting(
+                proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD));
+    EXPECT_TRUE(test_prediction_model);
+
+    // Make sure the cached decision is returned and that the model was not
+    // evaluated.
+    EXPECT_EQ(OptimizationTargetDecision::kModelPredictionHoldback,
+              prediction_manager()->ShouldTargetNavigation(
+                  navigation_handle.get(),
+                  proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD, {}));
+    EXPECT_FALSE(test_prediction_model->WasModelEvaluated());
+  }
 }
 
-TEST_F(PredictionManagerTest, EvaluatePredictionModelPopulatesNavData) {
+TEST_P(PredictionManagerMLServiceTest,
+       EvaluatePredictionModelPopulatesNavData) {
+  base::test::ScopedFeatureList scoped_feature_list;
+  if (UsingMLService()) {
+    scoped_feature_list.InitWithFeatures(
+        {optimization_guide::features::
+             kOptimizationTargetPredictionUsingMLService},
+        {});
+
+    SetLoadModelResult(machine_learning::mojom::LoadModelResult::kOk);
+  }
+
   base::HistogramTester histogram_tester;
   std::unique_ptr<content::MockNavigationHandle> navigation_handle =
       CreateMockNavigationHandleWithOptimizationGuideWebContentsObserver(
@@ -913,19 +1172,36 @@
       proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD,
       OptimizationTargetDecision::kModelNotAvailableOnClient);
 
-  // Make sure model gets evaluated despite there already being a decision in
-  // the navigation data.
-  EXPECT_EQ(OptimizationTargetDecision::kPageLoadMatches,
-            prediction_manager()->ShouldTargetNavigation(
-                navigation_handle.get(),
-                proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD, {}));
+  if (UsingMLService()) {
+    // Set ML prediction result to True to ensure the actual model is evaluated
+    // despite there already being a decision in the navigation data.
+    SetDecisionTreePredictionResult(
+        machine_learning::mojom::DecisionTreePredictionResult::kTrue,
+        /* score */ 0.6);
 
-  TestPredictionModel* test_prediction_model =
-      static_cast<TestPredictionModel*>(
-          prediction_manager()->GetPredictionModelForTesting(
-              proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD));
-  EXPECT_TRUE(test_prediction_model);
-  EXPECT_TRUE(test_prediction_model->WasModelEvaluated());
+    prediction_manager()->ShouldTargetNavigationAsync(
+        navigation_handle.get(), proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD,
+        {}, base::BindOnce([](OptimizationTargetDecision decision) {
+          EXPECT_EQ(OptimizationTargetDecision::kPageLoadMatches, decision);
+        }));
+
+    // Flush the Service connection pipe.
+    RunUntilIdle();
+  } else {
+    // Make sure model gets evaluated despite there already being a decision in
+    // the navigation data.
+    EXPECT_EQ(OptimizationTargetDecision::kPageLoadMatches,
+              prediction_manager()->ShouldTargetNavigation(
+                  navigation_handle.get(),
+                  proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD, {}));
+
+    TestPredictionModel* test_prediction_model =
+        static_cast<TestPredictionModel*>(
+            prediction_manager()->GetPredictionModelForTesting(
+                proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD));
+    EXPECT_TRUE(test_prediction_model);
+    EXPECT_TRUE(test_prediction_model->WasModelEvaluated());
+  }
 
   EXPECT_EQ(2, *nav_data->GetModelVersionForOptimizationTarget(
                    proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD));
@@ -933,15 +1209,27 @@
                      proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD));
 }
 
-TEST_F(PredictionManagerTest,
+TEST_P(PredictionManagerMLServiceTest,
        EvaluatePredictionModelPopulatesNavDataEvenWithHoldback) {
-  base::HistogramTester histogram_tester;
   base::test::ScopedFeatureList scoped_feature_list;
-  scoped_feature_list.InitWithFeaturesAndParameters(
-      {base::test::ScopedFeatureList::FeatureAndParams(
-          features::kOptimizationTargetPrediction,
-          {{"painful_page_load_metrics_only", "true"}})},
-      {});
+  if (UsingMLService()) {
+    scoped_feature_list.InitWithFeaturesAndParameters(
+        {{features::kOptimizationTargetPrediction,
+          {{"painful_page_load_metrics_only", "true"}}},
+         {optimization_guide::features::
+              kOptimizationTargetPredictionUsingMLService,
+          {}}},
+        {});
+
+    SetLoadModelResult(machine_learning::mojom::LoadModelResult::kOk);
+  } else {
+    scoped_feature_list.InitWithFeaturesAndParameters(
+        {{features::kOptimizationTargetPrediction,
+          {{"painful_page_load_metrics_only", "true"}}}},
+        {});
+  }
+
+  base::HistogramTester histogram_tester;
 
   std::unique_ptr<content::MockNavigationHandle> navigation_handle =
       CreateMockNavigationHandleWithOptimizationGuideWebContentsObserver(
@@ -960,17 +1248,34 @@
   EXPECT_TRUE(prediction_model_fetcher()->models_fetched());
   models_and_features_store()->RunUpdateHostModelFeaturesCallback();
 
-  EXPECT_EQ(OptimizationTargetDecision::kModelPredictionHoldback,
-            prediction_manager()->ShouldTargetNavigation(
-                navigation_handle.get(),
-                proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD, {}));
+  if (UsingMLService()) {
+    // Set ML prediction result to True to ensure the actual model is evaluated.
+    SetDecisionTreePredictionResult(
+        machine_learning::mojom::DecisionTreePredictionResult::kTrue,
+        /* score */ 0.6);
 
-  TestPredictionModel* test_prediction_model =
-      static_cast<TestPredictionModel*>(
-          prediction_manager()->GetPredictionModelForTesting(
-              proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD));
-  EXPECT_TRUE(test_prediction_model);
-  EXPECT_TRUE(test_prediction_model->WasModelEvaluated());
+    prediction_manager()->ShouldTargetNavigationAsync(
+        navigation_handle.get(), proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD,
+        {}, base::BindOnce([](OptimizationTargetDecision decision) {
+          EXPECT_EQ(OptimizationTargetDecision::kModelPredictionHoldback,
+                    decision);
+        }));
+
+    // Flush the Service connection pipe.
+    RunUntilIdle();
+  } else {
+    EXPECT_EQ(OptimizationTargetDecision::kModelPredictionHoldback,
+              prediction_manager()->ShouldTargetNavigation(
+                  navigation_handle.get(),
+                  proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD, {}));
+
+    TestPredictionModel* test_prediction_model =
+        static_cast<TestPredictionModel*>(
+            prediction_manager()->GetPredictionModelForTesting(
+                proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD));
+    EXPECT_TRUE(test_prediction_model);
+    EXPECT_TRUE(test_prediction_model->WasModelEvaluated());
+  }
 
   OptimizationGuideNavigationData* nav_data =
       OptimizationGuideNavigationData::GetFromNavigationHandle(
@@ -991,7 +1296,16 @@
       PredictionManagerModelStatus::kModelAvailable, 1);
 }
 
-TEST_F(PredictionManagerTest, ShouldTargetNavigationStoreAvailableNoModel) {
+TEST_P(PredictionManagerMLServiceTest,
+       ShouldTargetNavigationStoreAvailableNoModel) {
+  base::test::ScopedFeatureList scoped_feature_list;
+  if (UsingMLService()) {
+    scoped_feature_list.InitWithFeatures(
+        {optimization_guide::features::
+             kOptimizationTargetPredictionUsingMLService},
+        {});
+  }
+
   base::HistogramTester histogram_tester;
   std::unique_ptr<content::MockNavigationHandle> navigation_handle =
       CreateMockNavigationHandleWithOptimizationGuideWebContentsObserver(
@@ -1005,14 +1319,26 @@
   prediction_manager()->RegisterOptimizationTargets(
       {proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD});
 
-  SetStoreInitialized(/*load_models=*/false,
-                      /*load_host_model_features=*/true,
-                      /*have_models_in_store=)*/ false);
+  SetStoreInitialized(/* load_models= */ false,
+                      /* load_host_model_features= */ true,
+                      /* have_models_in_store= */ false);
 
-  EXPECT_EQ(OptimizationTargetDecision::kModelNotAvailableOnClient,
-            prediction_manager()->ShouldTargetNavigation(
-                navigation_handle.get(),
-                proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD, {}));
+  if (UsingMLService()) {
+    prediction_manager()->ShouldTargetNavigationAsync(
+        navigation_handle.get(), proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD,
+        {}, base::BindOnce([](OptimizationTargetDecision decision) {
+          EXPECT_EQ(OptimizationTargetDecision::kModelNotAvailableOnClient,
+                    decision);
+        }));
+
+    // Flush the Service connection pipe.
+    RunUntilIdle();
+  } else {
+    EXPECT_EQ(OptimizationTargetDecision::kModelNotAvailableOnClient,
+              prediction_manager()->ShouldTargetNavigation(
+                  navigation_handle.get(),
+                  proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD, {}));
+  }
 
   histogram_tester.ExpectBucketCount(
       "OptimizationGuide.ShouldTargetNavigation.PredictionModelStatus",
@@ -1025,8 +1351,15 @@
       PredictionManagerModelStatus::kStoreAvailableNoModelForTarget, 1);
 }
 
-TEST_F(PredictionManagerTest,
+TEST_P(PredictionManagerMLServiceTest,
        ShouldTargetNavigationStoreAvailableModelNotLoaded) {
+  base::test::ScopedFeatureList scoped_feature_list;
+  if (UsingMLService()) {
+    scoped_feature_list.InitWithFeatures(
+        {optimization_guide::features::
+             kOptimizationTargetPredictionUsingMLService},
+        {});
+  }
   base::HistogramTester histogram_tester;
   std::unique_ptr<content::MockNavigationHandle> navigation_handle =
       CreateMockNavigationHandleWithOptimizationGuideWebContentsObserver(
@@ -1040,14 +1373,26 @@
   prediction_manager()->RegisterOptimizationTargets(
       {proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD});
 
-  SetStoreInitialized(/*load_models=*/false,
-                      /*load_host_model_features=*/true,
-                      /*have_models_in_store=)*/ true);
+  SetStoreInitialized(/* load_models= */ false,
+                      /* load_host_model_features= */ true,
+                      /* have_models_in_store= */ true);
 
-  EXPECT_EQ(OptimizationTargetDecision::kModelNotAvailableOnClient,
-            prediction_manager()->ShouldTargetNavigation(
-                navigation_handle.get(),
-                proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD, {}));
+  if (UsingMLService()) {
+    prediction_manager()->ShouldTargetNavigationAsync(
+        navigation_handle.get(), proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD,
+        {}, base::BindOnce([](OptimizationTargetDecision decision) {
+          EXPECT_EQ(OptimizationTargetDecision::kModelNotAvailableOnClient,
+                    decision);
+        }));
+
+    // Flush the Service connection pipe.
+    RunUntilIdle();
+  } else {
+    EXPECT_EQ(OptimizationTargetDecision::kModelNotAvailableOnClient,
+              prediction_manager()->ShouldTargetNavigation(
+                  navigation_handle.get(),
+                  proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD, {}));
+  }
 
   histogram_tester.ExpectBucketCount(
       "OptimizationGuide.ShouldTargetNavigation.PredictionModelStatus",
@@ -1063,9 +1408,16 @@
       "OptimizationGuide.PredictionModelLoadedVersion.PainfulPageLoad", 0);
 }
 
-TEST_F(PredictionManagerTest,
+TEST_P(PredictionManagerMLServiceTest,
        DISABLE_ON_WIN_MAC_CHROMEOS(
            ShouldTargetNavigationStoreUnavailableModelUnknown)) {
+  base::test::ScopedFeatureList scoped_feature_list;
+  if (UsingMLService()) {
+    scoped_feature_list.InitWithFeatures(
+        {optimization_guide::features::
+             kOptimizationTargetPredictionUsingMLService},
+        {});
+  }
   base::HistogramTester histogram_tester;
   std::unique_ptr<content::MockNavigationHandle> navigation_handle =
       CreateMockNavigationHandleWithOptimizationGuideWebContentsObserver(
@@ -1079,10 +1431,22 @@
   prediction_manager()->RegisterOptimizationTargets(
       {proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD});
 
-  EXPECT_EQ(OptimizationTargetDecision::kModelNotAvailableOnClient,
-            prediction_manager()->ShouldTargetNavigation(
-                navigation_handle.get(),
-                proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD, {}));
+  if (UsingMLService()) {
+    prediction_manager()->ShouldTargetNavigationAsync(
+        navigation_handle.get(), proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD,
+        {}, base::BindOnce([](OptimizationTargetDecision decision) {
+          EXPECT_EQ(OptimizationTargetDecision::kModelNotAvailableOnClient,
+                    decision);
+        }));
+
+    // Flush the Service connection pipe.
+    RunUntilIdle();
+  } else {
+    EXPECT_EQ(OptimizationTargetDecision::kModelNotAvailableOnClient,
+              prediction_manager()->ShouldTargetNavigation(
+                  navigation_handle.get(),
+                  proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD, {}));
+  }
 
   histogram_tester.ExpectBucketCount(
       "OptimizationGuide.ShouldTargetNavigation.PredictionModelStatus",
@@ -1095,7 +1459,16 @@
       PredictionManagerModelStatus::kStoreUnavailableModelUnknown, 1);
 }
 
-TEST_F(PredictionManagerTest, UpdateModelForUnregisteredTarget) {
+TEST_P(PredictionManagerMLServiceTest, UpdateModelForUnregisteredTarget) {
+  base::test::ScopedFeatureList scoped_feature_list;
+  if (UsingMLService()) {
+    scoped_feature_list.InitWithFeatures(
+        {optimization_guide::features::
+             kOptimizationTargetPredictionUsingMLService},
+        {});
+    SetLoadModelResult(machine_learning::mojom::LoadModelResult::kOk);
+  }
+
   base::HistogramTester histogram_tester;
   CreatePredictionManager({});
   prediction_manager()->SetPredictionModelFetcherForTesting(
@@ -1114,11 +1487,19 @@
   prediction_manager()->UpdatePredictionModelsForTesting(
       get_models_response.get());
 
-  TestPredictionModel* test_prediction_model =
-      static_cast<TestPredictionModel*>(
-          prediction_manager()->GetPredictionModelForTesting(
-              proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD));
-  EXPECT_FALSE(test_prediction_model);
+  if (UsingMLService()) {
+    RemoteDecisionTreePredictor* predictor =
+        prediction_manager()->GetRemoteDecisionTreePredictorForTesting(
+            proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD);
+    EXPECT_FALSE(predictor);
+  } else {
+    TestPredictionModel* test_prediction_model =
+        static_cast<TestPredictionModel*>(
+            prediction_manager()->GetPredictionModelForTesting(
+                proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD));
+    EXPECT_FALSE(test_prediction_model);
+  }
+
   histogram_tester.ExpectTotalCount(
       "OptimizationGuide.PredictionManager.PredictionModelsStored", 1);
   histogram_tester.ExpectTotalCount(
@@ -1127,9 +1508,17 @@
       "OptimizationGuide.PredictionModelLoadedVersion.PainfulPageLoad", 0);
 }
 
-TEST_F(
-    PredictionManagerTest,
+TEST_P(
+    PredictionManagerMLServiceTest,
     DISABLE_ON_WIN_MAC_CHROMEOS(UpdateModelWithUnsupportedOptimizationTarget)) {
+  base::test::ScopedFeatureList scoped_feature_list;
+  if (UsingMLService()) {
+    scoped_feature_list.InitWithFeatures(
+        {optimization_guide::features::
+             kOptimizationTargetPredictionUsingMLService},
+        {});
+  }
+
   std::unique_ptr<content::MockNavigationHandle> navigation_handle =
       CreateMockNavigationHandleWithOptimizationGuideWebContentsObserver(
           GURL("https://foo.com"));
@@ -1153,19 +1542,108 @@
   prediction_manager()->UpdatePredictionModelsForTesting(
       get_models_response.get());
 
-  EXPECT_EQ(OptimizationTargetDecision::kModelNotAvailableOnClient,
-            prediction_manager()->ShouldTargetNavigation(
-                navigation_handle.get(),
-                proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD, {}));
+  if (UsingMLService()) {
+    prediction_manager()->ShouldTargetNavigationAsync(
+        navigation_handle.get(), proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD,
+        {}, base::BindOnce([](OptimizationTargetDecision decision) {
+          EXPECT_EQ(OptimizationTargetDecision::kModelNotAvailableOnClient,
+                    decision);
+        }));
 
-  TestPredictionModel* test_prediction_model =
-      static_cast<TestPredictionModel*>(
-          prediction_manager()->GetPredictionModelForTesting(
-              proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD));
-  EXPECT_FALSE(test_prediction_model);
+    // Flush the Service connection pipe.
+    RunUntilIdle();
+
+    RemoteDecisionTreePredictor* predictor =
+        prediction_manager()->GetRemoteDecisionTreePredictorForTesting(
+            proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD);
+    EXPECT_FALSE(predictor);
+  } else {
+    EXPECT_EQ(OptimizationTargetDecision::kModelNotAvailableOnClient,
+              prediction_manager()->ShouldTargetNavigation(
+                  navigation_handle.get(),
+                  proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD, {}));
+
+    TestPredictionModel* test_prediction_model =
+        static_cast<TestPredictionModel*>(
+            prediction_manager()->GetPredictionModelForTesting(
+                proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD));
+    EXPECT_FALSE(test_prediction_model);
+  }
   EXPECT_FALSE(models_and_features_store()->WasModelLoaded());
 }
 
+class PredictionManagerMLServiceEnabledTest
+    : public PredictionManagerMLServiceTest {
+ public:
+  void SetUp() override {
+    PredictionManagerMLServiceTest::SetUp();
+    service_connection_->SetAsyncModeForTesting(true);
+  }
+};
+
+INSTANTIATE_TEST_SUITE_P(MLServiceEnabled,
+                         PredictionManagerMLServiceEnabledTest,
+                         ::testing::Values(true));
+
+TEST_P(PredictionManagerMLServiceEnabledTest,
+       ServiceDisconnectedAtModelEvaluation) {
+  base::test::ScopedFeatureList scoped_feature_list;
+  scoped_feature_list.InitWithFeatures(
+      {optimization_guide::features::
+           kOptimizationTargetPredictionUsingMLService},
+      {});
+
+  base::HistogramTester histogram_tester;
+  std::unique_ptr<content::MockNavigationHandle> navigation_handle =
+      CreateMockNavigationHandleWithOptimizationGuideWebContentsObserver(
+          GURL("https://foo.com"));
+
+  CreatePredictionManager({});
+  // The model will be loaded from the store.
+  prediction_manager()->SetPredictionModelFetcherForTesting(
+      BuildTestPredictionModelFetcher(
+          PredictionModelFetcherEndState::kFetchSuccessWithEmptyResponse));
+
+  prediction_manager()->RegisterOptimizationTargets(
+      {proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD});
+  SetStoreInitialized();
+  EXPECT_TRUE(prediction_model_fetcher()->models_fetched());
+
+  SetLoadModelResult(machine_learning::mojom::LoadModelResult::kOk);
+  RunScheduledCalls();
+
+  // Reset the service to cause disconnection.
+  ResetMLService();
+
+  // Still sets the evaluation result
+  SetDecisionTreePredictionResult(
+      machine_learning::mojom::DecisionTreePredictionResult::kTrue,
+      /* score */ 0.6);
+
+  prediction_manager()->ShouldTargetNavigationAsync(
+      navigation_handle.get(), proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD, {},
+      base::BindOnce([](OptimizationTargetDecision decision) {
+        EXPECT_EQ(OptimizationTargetDecision::kModelNotAvailableOnClient,
+                  decision);
+      }));
+
+  // Flush the Service connection pipe.
+  RunUntilIdle();
+  RunScheduledCalls();
+
+  histogram_tester.ExpectTotalCount(
+      "OptimizationGuide.PredictionModelEvaluationLatency." +
+          GetStringNameForOptimizationTarget(
+              optimization_guide::proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD),
+      0);
+
+  histogram_tester.ExpectUniqueSample(
+      "OptimizationGuide.IsPredictionModelValid." +
+          GetStringNameForOptimizationTarget(
+              optimization_guide::proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD),
+      true, 1);
+}
+
 TEST_F(PredictionManagerTest, HasHostModelFeaturesForHost) {
   base::HistogramTester histogram_tester;
 
@@ -1237,7 +1715,6 @@
 }
 
 TEST_F(PredictionManagerTest, UpdateHostModelFeaturesMissingHost) {
-
   CreatePredictionManager({});
   prediction_manager()->SetPredictionModelFetcherForTesting(
       BuildTestPredictionModelFetcher(
@@ -1262,7 +1739,6 @@
 }
 
 TEST_F(PredictionManagerTest, UpdateHostModelFeaturesNoFeature) {
-
   CreatePredictionManager({});
   prediction_manager()->SetPredictionModelFetcherForTesting(
       BuildTestPredictionModelFetcher(
@@ -1286,7 +1762,6 @@
 }
 
 TEST_F(PredictionManagerTest, UpdateHostModelFeaturesNoFeatureName) {
-
   CreatePredictionManager({});
   prediction_manager()->SetPredictionModelFetcherForTesting(
       BuildTestPredictionModelFetcher(
@@ -1338,7 +1813,6 @@
 }
 
 TEST_F(PredictionManagerTest, UpdateHostModelFeaturesIntValue) {
-
   CreatePredictionManager({});
   prediction_manager()->SetPredictionModelFetcherForTesting(
       BuildTestPredictionModelFetcher(
@@ -1462,7 +1936,25 @@
   EXPECT_EQ(6.0, (*host_model_features)["host_feat_added"]);
 }
 
-TEST_P(PredictionManagerTest, ClientFeature) {
+class PredictionManagerClientFeatureTest
+    : public PredictionManagerTest,
+      public testing::WithParamInterface<proto::ClientModelFeature> {
+ public:
+  bool IsSameOriginNavigationFeature() {
+    return GetParam() == proto::CLIENT_MODEL_FEATURE_SAME_ORIGIN_NAVIGATION;
+  }
+
+  bool IsUnknownFeature() {
+    return GetParam() == proto::CLIENT_MODEL_FEATURE_UNKNOWN;
+  }
+};
+
+INSTANTIATE_TEST_SUITE_P(ClientFeature,
+                         PredictionManagerClientFeatureTest,
+                         testing::Range(proto::ClientModelFeature_MIN,
+                                        proto::ClientModelFeature_MAX));
+
+TEST_P(PredictionManagerClientFeatureTest, ClientFeature) {
   base::HistogramTester histogram_tester;
   std::unique_ptr<content::MockNavigationHandle> navigation_handle =
       CreateMockNavigationHandleWithOptimizationGuideWebContentsObserver(
@@ -1513,11 +2005,6 @@
   }
 }
 
-INSTANTIATE_TEST_SUITE_P(ClientFeature,
-                         PredictionManagerTest,
-                         testing::Range(proto::ClientModelFeature_MIN,
-                                        proto::ClientModelFeature_MAX));
-
 TEST_F(PredictionManagerTest, PreviousSessionStatisticsUsed) {
   base::HistogramTester histogram_tester;
   GURL previous_url = GURL("https://foo.com");
diff --git a/chrome/browser/optimization_guide/prediction/remote_decision_tree_predictor.cc b/chrome/browser/optimization_guide/prediction/remote_decision_tree_predictor.cc
index a60ac5c..86778eca 100644
--- a/chrome/browser/optimization_guide/prediction/remote_decision_tree_predictor.cc
+++ b/chrome/browser/optimization_guide/prediction/remote_decision_tree_predictor.cc
@@ -43,6 +43,14 @@
   return remote_.get();
 }
 
+bool RemoteDecisionTreePredictor::IsConnected() const {
+  return remote_.is_connected();
+}
+
+void RemoteDecisionTreePredictor::FlushForTesting() {
+  remote_.FlushForTesting();
+}
+
 mojo::PendingReceiver<machine_learning::mojom::DecisionTreePredictor>
 RemoteDecisionTreePredictor::BindNewPipeAndPassReceiver() {
   return remote_.BindNewPipeAndPassReceiver();
diff --git a/chrome/browser/optimization_guide/prediction/remote_decision_tree_predictor.h b/chrome/browser/optimization_guide/prediction/remote_decision_tree_predictor.h
index 6487c1d..44a9286 100644
--- a/chrome/browser/optimization_guide/prediction/remote_decision_tree_predictor.h
+++ b/chrome/browser/optimization_guide/prediction/remote_decision_tree_predictor.h
@@ -32,6 +32,12 @@
   // receiver. Returns nullptr if |remote_| is unbound.
   machine_learning::mojom::DecisionTreePredictorProxy* Get() const;
 
+  // Whether |remote_| is connected.
+  bool IsConnected() const;
+
+  // Flushes |remote_| for testing purpose.
+  void FlushForTesting();
+
   // Calls the |BindNewPipeAndPassReceiver| method of the |remote_|. Must only
   // be called on a bound |remote_|.
   mojo::PendingReceiver<machine_learning::mojom::DecisionTreePredictor>
diff --git a/chrome/browser/optimization_guide/prediction/remote_decision_tree_predictor_unittest.cc b/chrome/browser/optimization_guide/prediction/remote_decision_tree_predictor_unittest.cc
index bad16db..f1c5eba 100644
--- a/chrome/browser/optimization_guide/prediction/remote_decision_tree_predictor_unittest.cc
+++ b/chrome/browser/optimization_guide/prediction/remote_decision_tree_predictor_unittest.cc
@@ -55,6 +55,12 @@
   auto pending_receiver = predictor.BindNewPipeAndPassReceiver();
   EXPECT_TRUE(predictor.Get());
   EXPECT_TRUE(pending_receiver);
+  EXPECT_TRUE(predictor.IsConnected());
+
+  pending_receiver.reset();
+  predictor.FlushForTesting();
+  EXPECT_TRUE(predictor.Get());
+  EXPECT_FALSE(predictor.IsConnected());
 }
 
 }  // namespace optimization_guide
diff --git a/chrome/browser/resources/BUILD.gn b/chrome/browser/resources/BUILD.gn
index 2ad95bd7..150c221 100644
--- a/chrome/browser/resources/BUILD.gn
+++ b/chrome/browser/resources/BUILD.gn
@@ -260,6 +260,7 @@
     }
 
     deps = [
+      "//chrome/browser/promo_browser_command:mojo_bindings_js",
       "//chrome/browser/ui/webui/new_tab_page:mojo_bindings_js",
       "//skia/public/mojom:mojom_js",
     ]
diff --git a/chrome/browser/resources/nearby_internals/contact_tab.js b/chrome/browser/resources/nearby_internals/contact_tab.js
index 40943b0..5e4b298 100644
--- a/chrome/browser/resources/nearby_internals/contact_tab.js
+++ b/chrome/browser/resources/nearby_internals/contact_tab.js
@@ -86,6 +86,6 @@
    */
   onContactUpdateAdded_(contacts) {
     contacts.unshift('contactList_');
-    this.push.apply(this, contacts);
+    this.unshift.apply(this, contacts);
   },
 });
diff --git a/chrome/browser/resources/nearby_internals/http_message_object.html b/chrome/browser/resources/nearby_internals/http_message_object.html
index 0167286..d2879dc 100644
--- a/chrome/browser/resources/nearby_internals/http_message_object.html
+++ b/chrome/browser/resources/nearby_internals/http_message_object.html
@@ -4,9 +4,7 @@
   }
 
   #item {
-    border-left: var(--standard-border);
-    border-right: var(--standard-border);
-    border-top: var(--standard-border);
+    border: var(--standard-border);
   }
 
   #header {
@@ -48,7 +46,7 @@
       [[rpcToString_(item.rpc)]]:[[directionToString_(item.direction)]]
     </span>
     <div id="flex"></div>
-    <span id="time">[[item.time]]</span>
+    <span id="time">[[formatTime_(item.time)]]</span>
     <cr-expand-button id="expandContent" class="cr-row"
           expanded="{{contentExpanded_}}">
     </cr-expand-button>
diff --git a/chrome/browser/resources/nearby_internals/http_message_object.js b/chrome/browser/resources/nearby_internals/http_message_object.js
index 58e7d99..4e7ba40 100644
--- a/chrome/browser/resources/nearby_internals/http_message_object.js
+++ b/chrome/browser/resources/nearby_internals/http_message_object.js
@@ -2,6 +2,9 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'chrome://resources/cr_elements/cr_expand_button/cr_expand_button.m.js';
+import 'chrome://resources/polymer/v3_0/iron-collapse/iron-collapse.js';
+
 import {html, Polymer} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 import {Direction, Rpc} from './types.js';
 
@@ -79,4 +82,15 @@
         break;
     }
   },
+
+  /**
+   * Sets the string representation of time.
+   * @private
+   * @param {number} time
+   * @return
+   */
+  formatTime_(time) {
+    const d = new Date(time);
+    return d.toLocaleTimeString();
+  },
 });
diff --git a/chrome/browser/resources/nearby_internals/http_tab.html b/chrome/browser/resources/nearby_internals/http_tab.html
index 6d9c3f3b..561db62 100644
--- a/chrome/browser/resources/nearby_internals/http_tab.html
+++ b/chrome/browser/resources/nearby_internals/http_tab.html
@@ -3,10 +3,6 @@
     --standard-border: 1px solid black;
   }
 
-  #http-list:last-child {
-    border-bottom: var(--standard-border);
-  }
-
   #clearButton {
     float: right;
   }
@@ -25,10 +21,10 @@
  Clear Messages
 </cr-button>
 
-<iron-list items="[[httpMessageList_]]" as="http-message" id="http-list"
+<dom-repeat items="[[httpMessageList_]]" as="http-message"
     hidden="[[!httpMessageList_.length]]">
   <template>
     <http-message-object item="[[http-message]]">
     </http-message-object>
   </template>
-</iron-list>
+</dom-repeat>
diff --git a/chrome/browser/resources/nearby_internals/http_tab.js b/chrome/browser/resources/nearby_internals/http_tab.js
index 74d02d5..c40a949b 100644
--- a/chrome/browser/resources/nearby_internals/http_tab.js
+++ b/chrome/browser/resources/nearby_internals/http_tab.js
@@ -104,6 +104,6 @@
    */
   parseAndAddMessages_(messages) {
     messages.unshift('httpMessageList_');
-    this.push.apply(this, messages);
+    this.unshift.apply(this, messages);
   },
 });
diff --git a/chrome/browser/resources/nearby_internals/types.js b/chrome/browser/resources/nearby_internals/types.js
index e61b65a..384e10e 100644
--- a/chrome/browser/resources/nearby_internals/types.js
+++ b/chrome/browser/resources/nearby_internals/types.js
@@ -53,7 +53,7 @@
  * The HTTP request/response object, sent by NearbyInternalsHttpHandler
  * chrome/browser/ui/webui/nearby_internals/nearby_internals_http_handler.cc.
  * @typedef {{body: string,
- *            time: string,
+ *            time: number,
  *            rpc: !Rpc,
  *            direction: !Direction}}
  */
diff --git a/chrome/browser/resources/new_tab_page/BUILD.gn b/chrome/browser/resources/new_tab_page/BUILD.gn
index ba975ca..298e94fd 100644
--- a/chrome/browser/resources/new_tab_page/BUILD.gn
+++ b/chrome/browser/resources/new_tab_page/BUILD.gn
@@ -23,6 +23,7 @@
     ":module_registry",
     ":modules",
     ":one_google_bar_api",
+    ":promo_browser_command_proxy",
     ":realbox",
     ":realbox_button",
     ":realbox_dropdown",
@@ -51,12 +52,20 @@
     ":modules",
     ":most_visited",
     ":one_google_bar_api",
+    ":promo_browser_command_proxy",
     "//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
     "//ui/webui/resources/js:event_tracker.m",
     "//ui/webui/resources/js:load_time_data.m",
   ]
 }
 
+js_library("promo_browser_command_proxy") {
+  deps = [
+    "//chrome/browser/promo_browser_command:mojo_bindings_js_library_for_compile",
+    "//ui/webui/resources/js:cr.m",
+  ]
+}
+
 js_library("most_visited") {
   deps = [
     ":browser_proxy",
@@ -271,6 +280,7 @@
     source = "new_tab_page_resources.grd"
 
     deps = [
+      "//chrome/browser/promo_browser_command:mojo_bindings_js",
       "//chrome/browser/resources/new_tab_page:web_components",
       "//chrome/browser/ui/webui/new_tab_page:mojo_bindings_js",
       "//skia/public/mojom:mojom_js",
@@ -296,6 +306,7 @@
     deps = [ ":unoptimized_resources" ]
 
     excludes = [
+      "../../promo_browser_command/promo_browser_command.mojom-lite.js",
       "../../ui/webui/new_tab_page/new_tab_page.mojom-lite.js",
       "../../../common/search/omnibox.mojom-lite.js",
       "../../../../skia/public/mojom/skcolor.mojom-lite.js",
@@ -322,6 +333,7 @@
       "chrome://resources/mojo/url/mojom/url.mojom-lite.js",
       "new_tab_page.mojom-lite.js",
       "omnibox.mojom-lite.js",
+      "promo_browser_command.mojom-lite.js",
     ]
   }
 }
diff --git a/chrome/browser/resources/new_tab_page/app.js b/chrome/browser/resources/new_tab_page/app.js
index 227da8f..4c50b3c 100644
--- a/chrome/browser/resources/new_tab_page/app.js
+++ b/chrome/browser/resources/new_tab_page/app.js
@@ -24,8 +24,17 @@
 import {BackgroundSelection, BackgroundSelectionType} from './customize_dialog.js';
 import {registry} from './modules/modules.js';
 import {oneGoogleBarApi} from './one_google_bar_api.js';
+import {PromoBrowserCommandProxy} from './promo_browser_command_proxy.js';
 import {$$, hexColorToSkColor, skColorToRgba} from './utils.js';
 
+/**
+ * @typedef {{
+ *   commandId: promoBrowserCommand.mojom.Command<number>,
+ *   clickInfo: !promoBrowserCommand.mojom.ClickInfo
+ * }}
+ */
+let CommandData;
+
 class AppElement extends PolymerElement {
   static get is() {
     return 'ntp-app';
@@ -227,7 +236,9 @@
           performance.measure('theme-set');
           this.theme_ = theme;
         });
-    this.eventTracker_.add(window, 'message', ({data}) => {
+    this.eventTracker_.add(window, 'message', (event) => {
+      /** @type {!Object} */
+      const data = event.data;
       // Something in OneGoogleBar is sending a message that is received here.
       // Need to ignore it.
       if (typeof data !== 'object') {
@@ -235,9 +246,9 @@
       }
       if ('frameType' in data) {
         if (data.frameType === 'promo') {
-          this.handlePromoMessage_(data);
+          this.handlePromoMessage_(event);
         } else if (data.frameType === 'one-google-bar') {
-          this.handleOneGoogleBarMessage_(data);
+          this.handleOneGoogleBarMessage_(event);
         }
       }
     });
@@ -640,6 +651,32 @@
   }
 
   /**
+   * Sends the command and the accompanying mouse click info received from the
+   * promo of the given source and origin to the browser. Relays the execution
+   * status response back to the source promo frame. |commandSource| and
+   * |commandOrigin| are used only to send the execution status response back to
+   * the source promo frame and should not be used for anything else.
+   * @param {!CommandData} commandData Command and mouse click info.
+   * @param {Window} commandSource Source promo frame.
+   * @param {string} commandOrigin Origin of the source promo frame.
+   * @private
+   */
+  executePromoBrowserCommand_(commandData, commandSource, commandOrigin) {
+    // Make sure we don't send unsupported commands to the browser.
+    /** @type {!promoBrowserCommand.mojom.Command} */
+    const commandId = Object.values(promoBrowserCommand.mojom.Command)
+                          .includes(commandData.commandId) ?
+        commandData.commandId :
+        promoBrowserCommand.mojom.Command.kUnknownCommand;
+
+    PromoBrowserCommandProxy.getInstance()
+        .handler.executeCommand(commandId, commandData.clickInfo)
+        .then(({commandExecuted}) => {
+          commandSource.postMessage(commandExecuted, commandOrigin);
+        });
+  }
+
+  /**
    * Handles messages from the OneGoogleBar iframe. The messages that are
    * handled include show bar on load and overlay updates.
    *
@@ -649,10 +686,12 @@
    * When modal overlays are enabled, activate/deactivate controls if the
    * OneGoogleBar is layered on top of #content with a backdrop. This would
    * happen when OneGoogleBar has an overlay open.
-   * @param {!Object} data
+   * @param {!MessageEvent} event
    * @private
    */
-  handleOneGoogleBarMessage_(data) {
+  handleOneGoogleBarMessage_(event) {
+    /** @type {!Object} */
+    const data = event.data;
     if (data.messageType === 'loaded') {
       if (!this.oneGoogleBarModalOverlaysEnabled_) {
         const oneGoogleBar = $$(this, '#oneGoogleBar');
@@ -683,6 +722,9 @@
     } else if (data.messageType === 'deactivate') {
       this.$.oneGoogleBarOverlayBackdrop.toggleAttribute('show', false);
       $$(this, '#oneGoogleBar').style.zIndex = '0';
+    } else if (data.messageType === 'execute-browser-command') {
+      this.executePromoBrowserCommand_(
+          /** @type CommandData */ (data), event.source, event.origin);
     }
   }
 
@@ -690,10 +732,12 @@
    * Handle messages from promo iframe. This shows the promo on load and sets
    * up the show/hide logic (in case there is an overlap with most-visited
    * tiles).
-   * @param {!Object} data
+   * @param {!MessageEvent} event
    * @private
    */
-  handlePromoMessage_(data) {
+  handlePromoMessage_(event) {
+    /** @type {!Object} */
+    const data = event.data;
     if (data.messageType === 'loaded') {
       this.promoLoaded_ = true;
       const onResize = () => {
@@ -706,6 +750,9 @@
       this.pageHandler_.onPromoRendered(BrowserProxy.getInstance().now());
     } else if (data.messageType === 'link-clicked') {
       this.pageHandler_.onPromoLinkClicked();
+    } else if (data.messageType === 'execute-browser-command') {
+      this.executePromoBrowserCommand_(
+          /** @type CommandData */ (data), event.source, event.origin);
     }
   }
 
diff --git a/chrome/browser/resources/new_tab_page/new_tab_page.js b/chrome/browser/resources/new_tab_page/new_tab_page.js
index ad1f3bc..ac41a8d 100644
--- a/chrome/browser/resources/new_tab_page/new_tab_page.js
+++ b/chrome/browser/resources/new_tab_page/new_tab_page.js
@@ -17,5 +17,6 @@
 export {BackgroundSelectionType} from './customize_dialog.js';
 export {dummyDescriptor} from './modules/dummy/module.js';
 export {ModuleRegistry} from './modules/module_registry.js';
+export {PromoBrowserCommandProxy} from './promo_browser_command_proxy.js';
 export {NO_SUGGESTION_GROUP_ID} from './realbox_dropdown.js';
 export {$$, createScrollBorders, decodeString16, hexColorToSkColor, mojoString16, skColorToRgba} from './utils.js';
diff --git a/chrome/browser/resources/new_tab_page/new_tab_page_resources_common.grdp b/chrome/browser/resources/new_tab_page/new_tab_page_resources_common.grdp
index 1b52ac2..8d0effd 100644
--- a/chrome/browser/resources/new_tab_page/new_tab_page_resources_common.grdp
+++ b/chrome/browser/resources/new_tab_page/new_tab_page_resources_common.grdp
@@ -6,6 +6,9 @@
   <include name="IDR_NEW_TAB_PAGE_OMNIBOX_MOJO_LITE_JS"
       file="${root_gen_dir}/chrome/common/search/omnibox.mojom-lite.js"
       use_base_dir="false" type="BINDATA" />
+  <include name="IDR_NEW_TAB_PAGE_PROMO_BROWSER_COMMAND_MOJO_LITE_JS"
+      file="${root_gen_dir}/chrome/browser/promo_browser_command/promo_browser_command.mojom-lite.js"
+      use_base_dir="false" type="BINDATA" />
   <include name="IDR_NEW_TAB_PAGE_ACCOUNT_CIRCLE_SVG"
       file="icons/account_circle.svg" type="BINDATA" />
   <include name="IDR_NEW_TAB_PAGE_BRUSH_ICON_SVG"
@@ -64,4 +67,6 @@
       file="untrusted/background_image.js" type="BINDATA" />
   <include name="IDR_NEW_TAB_PAGE_ONE_GOOGLE_BAR_API_JS"
       file="one_google_bar_api.js" type="BINDATA" compress="false" />
+  <include name="IDR_NEW_TAB_PAGE_PROMO_BROWSER_COMMAND_PROXY_JS"
+      file="promo_browser_command_proxy.js" type="BINDATA" compress="false" />
 </grit-part>
diff --git a/chrome/browser/resources/new_tab_page/promo_browser_command_proxy.js b/chrome/browser/resources/new_tab_page/promo_browser_command_proxy.js
new file mode 100644
index 0000000..2abad05
--- /dev/null
+++ b/chrome/browser/resources/new_tab_page/promo_browser_command_proxy.js
@@ -0,0 +1,22 @@
+// 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.
+
+import './promo_browser_command.mojom-lite.js';
+
+import {addSingletonGetter} from 'chrome://resources/js/cr.m.js';
+
+/**
+ * @fileoverview This file provides a class that exposes the Mojo handler
+ * interface used for sending the NTP promos browser commands to the browser and
+ * receiving the browser response.
+ */
+
+export class PromoBrowserCommandProxy {
+  constructor() {
+    /** @type {!promoBrowserCommand.mojom.CommandHandlerRemote} */
+    this.handler = promoBrowserCommand.mojom.CommandHandler.getRemote();
+  }
+}
+
+addSingletonGetter(PromoBrowserCommandProxy);
diff --git a/chrome/browser/resources/new_tab_page/untrusted/promo.js b/chrome/browser/resources/new_tab_page/untrusted/promo.js
index 454df0f2..2faa373 100644
--- a/chrome/browser/resources/new_tab_page/untrusted/promo.js
+++ b/chrome/browser/resources/new_tab_page/untrusted/promo.js
@@ -16,7 +16,27 @@
     if (el.target !== '_blank') {
       el.target = '_top';
     }
-    el.addEventListener('click', () => {
+    el.addEventListener('click', (event) => {
+      const browserCommandFound =
+          event.target.getAttribute('href').match(/^command:([1-9][0-9]*)$/);
+      if (browserCommandFound) {
+        event.preventDefault();  // Prevent navigation attempt.
+        window.parent.postMessage(
+            {
+              frameType: 'promo',
+              messageType: 'execute-browser-command',
+              commandId: parseInt(browserCommandFound[1], 10),
+              clickInfo: {
+                middleButton: event.button === 1,
+                altKey: event.altKey,
+                ctrlKey: event.ctrlKey,
+                metaKey: event.metaKey,
+                shiftKey: event.shiftKey
+              }
+            },
+            'chrome://new-tab-page');
+      }
+
       window.parent.postMessage(
           {frameType: 'promo', messageType: 'link-clicked'},
           'chrome://new-tab-page');
diff --git a/chrome/browser/resources/settings/chrome_cleanup_page/chrome_cleanup_page.html b/chrome/browser/resources/settings/chrome_cleanup_page/chrome_cleanup_page.html
index a10ab1e0..1f99140 100644
--- a/chrome/browser/resources/settings/chrome_cleanup_page/chrome_cleanup_page.html
+++ b/chrome/browser/resources/settings/chrome_cleanup_page/chrome_cleanup_page.html
@@ -5,8 +5,8 @@
 
       #waiting-spinner {
         flex-shrink: 0;
-        height: 20px;
-        width: 20px;
+        height: 2.0em;
+        width: 2.0em;
       }
 
       /* Apply a fixed height to the <svg> tag inside #powered-by-logo.
diff --git a/chrome/browser/resources/settings/chromeos/BUILD.gn b/chrome/browser/resources/settings/chromeos/BUILD.gn
index 814ccfa..6f3187a 100644
--- a/chrome/browser/resources/settings/chromeos/BUILD.gn
+++ b/chrome/browser/resources/settings/chromeos/BUILD.gn
@@ -253,7 +253,8 @@
     "bluetooth_page:closure_compile_module",
 
     #"crostini_page:closure_compile_module",
-    #"date_time_page:closure_compile_module",
+    "date_time_page:closure_compile_module",
+
     #"device_page:closure_compile_module",
     #"google_assistant_page:closure_compile_module",
     #"internet_page:closure_compile_module",
diff --git a/chrome/browser/resources/settings/chromeos/date_time_page/BUILD.gn b/chrome/browser/resources/settings/chromeos/date_time_page/BUILD.gn
index 3bfa5759..e6e792803 100644
--- a/chrome/browser/resources/settings/chromeos/date_time_page/BUILD.gn
+++ b/chrome/browser/resources/settings/chromeos/date_time_page/BUILD.gn
@@ -3,6 +3,7 @@
 # found in the LICENSE file.
 
 import("//third_party/closure_compiler/compile_js.gni")
+import("../os_settings.gni")
 
 js_type_check("closure_compile") {
   deps = [
@@ -39,7 +40,6 @@
 
 js_library("timezone_selector") {
   deps = [
-    ":date_time_types",
     "../../controls:settings_dropdown_menu",
     "../../prefs:prefs_behavior",
     "//ui/webui/resources/js:cr",
@@ -57,17 +57,18 @@
   ]
 }
 
-# TODO: Uncomment as the Polymer3 migration makes progress.
-#js_type_check("closure_compile_module") {
-#  is_polymer3 = true
-#  deps = [
-#    ":date_time_page.m",
-#    ":date_time_types.m",
-#    ":timezone_browser_proxy.m",
-#    ":timezone_selector.m",
-#    ":timezone_subpage.m"
-#  ]
-#}
+js_type_check("closure_compile_module") {
+  is_polymer3 = true
+  deps = [
+    #   ":date_time_page.m",
+    #   ":date_time_types.m",
+
+    #   ":timezone_browser_proxy.m",
+    ":timezone_selector.m",
+
+    #   ":timezone_subpage.m"
+  ]
+}
 
 js_library("date_time_page.m") {
   sources = [ "$root_gen_dir/chrome/browser/resources/settings/chromeos/date_time_page/date_time_page.m.js" ]
@@ -96,7 +97,11 @@
 js_library("timezone_selector.m") {
   sources = [ "$root_gen_dir/chrome/browser/resources/settings/chromeos/date_time_page/timezone_selector.m.js" ]
   deps = [
-    # TODO: Fill those in.
+    "../../controls:settings_dropdown_menu.m",
+    "../../prefs:prefs_behavior.m",
+    "../../prefs:prefs_types.m",
+    "//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
+    "//ui/webui/resources/js:load_time_data.m",
   ]
   extra_deps = [ ":timezone_selector_module" ]
 }
@@ -137,6 +142,9 @@
   js_file = "timezone_selector.js"
   html_file = "timezone_selector.html"
   html_type = "dom-module"
+  migrated_imports = os_settings_migrated_imports
+  namespace_rewrites = os_settings_namespace_rewrites
+  auto_imports = os_settings_auto_imports
 }
 
 polymer_modulizer("timezone_subpage") {
diff --git a/chrome/browser/resources/settings/chromeos/date_time_page/date_time_types.js b/chrome/browser/resources/settings/chromeos/date_time_page/date_time_types.js
index 87b2d761..c6e060c4 100644
--- a/chrome/browser/resources/settings/chromeos/date_time_page/date_time_types.js
+++ b/chrome/browser/resources/settings/chromeos/date_time_page/date_time_types.js
@@ -4,17 +4,6 @@
 
 cr.define('settings', function() {
   /**
-   * Describes the effective policy restriction on time zone automatic
-   * detection.
-   * @enum {number}
-   */
-  const TimeZoneAutoDetectPolicyRestriction = {
-    NONE: 0,
-    FORCED_ON: 1,
-    FORCED_OFF: 2,
-  };
-
-  /**
    * Describes values of
    * prefs.generated.resolve_timezone_by_geolocation_method_short. Must be kept
    * in sync with TimeZoneResolverManager::TimeZoneResolveMethod enum.
@@ -28,5 +17,5 @@
   };
 
   // #cr_define_end
-  return {TimeZoneAutoDetectPolicyRestriction, TimeZoneAutoDetectMethod};
+  return {TimeZoneAutoDetectMethod};
 });
diff --git a/chrome/browser/resources/settings/chromeos/date_time_page/timezone_selector.html b/chrome/browser/resources/settings/chromeos/date_time_page/timezone_selector.html
index e48bc3a0..3dc2133 100644
--- a/chrome/browser/resources/settings/chromeos/date_time_page/timezone_selector.html
+++ b/chrome/browser/resources/settings/chromeos/date_time_page/timezone_selector.html
@@ -1,11 +1,12 @@
 <link rel="import" href="chrome://resources/html/polymer.html">
 
+<link rel="import" href="chrome://resources/html/cr.html">
 <link rel="import" href="chrome://resources/html/i18n_behavior.html">
 <link rel="import" href="../../controls/settings_dropdown_menu.html">
 <link rel="import" href="../../i18n_setup.html">
 <link rel="import" href="../../prefs/prefs_behavior.html">
+<link rel="import" href="../../prefs/prefs_types.html">
 <link rel="import" href="../../settings_shared_css.html">
-<link rel="import" href="date_time_types.html">
 
 <dom-module id="timezone-selector">
   <template>
diff --git a/chrome/browser/resources/settings/chromeos/date_time_page/timezone_selector.js b/chrome/browser/resources/settings/chromeos/date_time_page/timezone_selector.js
index 6a8bbe1..09a74deb 100644
--- a/chrome/browser/resources/settings/chromeos/date_time_page/timezone_selector.js
+++ b/chrome/browser/resources/settings/chromeos/date_time_page/timezone_selector.js
@@ -5,13 +5,11 @@
 /**
  * @fileoverview 'timezone-selector' is the time zone selector dropdown.
  */
-(function() {
-'use strict';
 
 Polymer({
   is: 'timezone-selector',
 
-  behaviors: [I18nBehavior, PrefsBehavior],
+  behaviors: [PrefsBehavior],
 
   properties: {
     /**
@@ -179,4 +177,3 @@
         prefResolveOnOffValue;
   },
 });
-})();
diff --git a/chrome/browser/resources/settings/chromeos/os_settings.gni b/chrome/browser/resources/settings/chromeos/os_settings.gni
index 2350de3..0b07244 100644
--- a/chrome/browser/resources/settings/chromeos/os_settings.gni
+++ b/chrome/browser/resources/settings/chromeos/os_settings.gni
@@ -7,44 +7,43 @@
 import("//ui/webui/resources/cr_elements/chromeos/os_cr_elements.gni")
 import("../settings.gni")
 
-os_settings_namespace_rewrites =
-    settings_namespace_rewrites + cr_components_chromeos_namespace_rewrites +
-    cr_elements_chromeos_namespace_rewrites +
-    [
-      "parental_controls.ParentalControlsBrowserProxy|ParentalControlsBrowserProxy",
-      "settings.AccountManagerBrowserProxy|AccountManagerBrowserProxy",
-      "settings.AmbientModeBrowserProxy|AmbientModeBrowserProxy",
-      "settings.ChangePictureBrowserProxy|ChangePictureBrowserProxy",
-      "settings.DefaultImage|DefaultImage",
-      "settings.KerberosAccountsBrowserProxy|KerberosAccountsBrowserProxy",
-      "settings.KerberosErrorType|KerberosErrorType",
-      "settings.KerberosConfigErrorCode|KerberosConfigErrorCode",
-      "settings.MultiDeviceBrowserProxy|MultiDeviceBrowserProxy",
-      "settings.MultiDeviceSettingsMode|MultiDeviceSettingsMode",
-      "settings.MultiDeviceFeature|MultiDeviceFeature",
-      "settings.MultiDeviceFeatureState|MultiDeviceFeatureState",
-      "settings.MultiDevicePageContentData|MultiDevicePageContentData",
-      "settings.LanguagesMetricsProxy|LanguagesMetricsProxy",
-      "settings.LanguagesPageInteraction|LanguagesPageInteraction",
-      "settings.OsResetBrowserProxy|OsResetBrowserProxy",
-      "settings.RouteObserverBehavior|RouteObserverBehavior",
-      "settings.Route|Route",
-      "settings.routes|routes",
-      "settings.recordSettingChange|recordSettingChange",
-      "settings.SmartLockSignInEnabledState|SmartLockSignInEnabledState",
-      "settings.WallpaperBrowserProxy|WallpaperBrowserProxy",
-      "settings.FingerprintBrowserProxy|FingerprintBrowserProxy",
-      "settings.FingerprintResultType|FingerprintResultType",
-      "settings.recordLockScreenProgress|recordLockScreenProgress",
-      "settings.OsSyncBrowserProxy|OsSyncBrowserProxy",
-      "settings.FingerprintInfo|FingerprintInfo",
-      "settings.FingerprintSetupStep|FingerprintSetupStep",
-      "settings.FingerprintAttempt|FingerprintAttempt",
-      "settings.FingerprintScan|FingerprintScan",
-      "settings.KerberosAccount|KerberosAccount",
-      "settings.OsSyncPrefs|OsSyncPrefs",
-      "settings.ValidateKerberosConfigResult|ValidateKerberosConfigResult",
-    ]
+os_settings_namespace_rewrites = settings_namespace_rewrites +
+                                 cr_components_chromeos_namespace_rewrites +
+                                 cr_elements_chromeos_namespace_rewrites + [
+                                   "parental_controls.ParentalControlsBrowserProxy|ParentalControlsBrowserProxy",
+                                   "settings.AccountManagerBrowserProxy|AccountManagerBrowserProxy",
+                                   "settings.AmbientModeBrowserProxy|AmbientModeBrowserProxy",
+                                   "settings.ChangePictureBrowserProxy|ChangePictureBrowserProxy",
+                                   "settings.DefaultImage|DefaultImage",
+                                   "settings.KerberosAccountsBrowserProxy|KerberosAccountsBrowserProxy",
+                                   "settings.KerberosErrorType|KerberosErrorType",
+                                   "settings.KerberosConfigErrorCode|KerberosConfigErrorCode",
+                                   "settings.MultiDeviceBrowserProxy|MultiDeviceBrowserProxy",
+                                   "settings.MultiDeviceSettingsMode|MultiDeviceSettingsMode",
+                                   "settings.MultiDeviceFeature|MultiDeviceFeature",
+                                   "settings.MultiDeviceFeatureState|MultiDeviceFeatureState",
+                                   "settings.MultiDevicePageContentData|MultiDevicePageContentData",
+                                   "settings.LanguagesMetricsProxy|LanguagesMetricsProxy",
+                                   "settings.LanguagesPageInteraction|LanguagesPageInteraction",
+                                   "settings.OsResetBrowserProxy|OsResetBrowserProxy",
+                                   "settings.RouteObserverBehavior|RouteObserverBehavior",
+                                   "settings.Route|Route",
+                                   "settings.routes|routes",
+                                   "settings.recordSettingChange|recordSettingChange",
+                                   "settings.SmartLockSignInEnabledState|SmartLockSignInEnabledState",
+                                   "settings.WallpaperBrowserProxy|WallpaperBrowserProxy",
+                                   "settings.FingerprintBrowserProxy|FingerprintBrowserProxy",
+                                   "settings.FingerprintResultType|FingerprintResultType",
+                                   "settings.recordLockScreenProgress|recordLockScreenProgress",
+                                   "settings.OsSyncBrowserProxy|OsSyncBrowserProxy",
+                                   "settings.FingerprintInfo|FingerprintInfo",
+                                   "settings.FingerprintSetupStep|FingerprintSetupStep",
+                                   "settings.FingerprintAttempt|FingerprintAttempt",
+                                   "settings.FingerprintScan|FingerprintScan",
+                                   "settings.KerberosAccount|KerberosAccount",
+                                   "settings.OsSyncPrefs|OsSyncPrefs",
+                                   "settings.ValidateKerberosConfigResult|ValidateKerberosConfigResult",
+                                 ]
 
 os_settings_auto_imports = settings_auto_imports +
                            cr_components_chromeos_auto_imports +
@@ -68,6 +67,7 @@
                              "chrome/browser/resources/settings/chromeos/personalization_page/wallpaper_browser_proxy.html|WallpaperBrowserProxy,WallpaperBrowserProxyImpl",
                              "chrome/browser/resources/settings/chromeos/parental_controls_page/parental_controls_browser_proxy.html|ParentalControlsBrowserProxy,ParentalControlsBrowserProxyImpl",
                              "chrome/browser/resources/settings/chromeos/route_origin_behavior.html|RouteOriginBehaviorImpl,RouteOriginBehavior",
+                             "chrome/browser/resources/settings/controls/settings_dropdown_menu.html|DropdownMenuOptionList",
                              "chrome/browser/resources/settings/lifetime_browser_proxy.html|LifetimeBrowserProxy,LifetimeBrowserProxyImpl",
                              "chrome/browser/resources/settings/people_page/account_manager_browser_proxy.html|AccountManagerBrowserProxy,AccountManagerBrowserProxyImpl,Account",
                              "chrome/browser/resources/settings/people_page/profile_info_browser_proxy.html|ProfileInfoBrowserProxyImpl,ProfileInfoBrowserProxy,ProfileInfo",
diff --git a/chrome/browser/resources/settings/chromeos/os_settings.js b/chrome/browser/resources/settings/chromeos/os_settings.js
index 9804189..070cb54 100644
--- a/chrome/browser/resources/settings/chromeos/os_settings.js
+++ b/chrome/browser/resources/settings/chromeos/os_settings.js
@@ -19,6 +19,7 @@
 import './parental_controls_page/parental_controls_page.m.js';
 import './os_people_page/os_people_page.m.js';
 import './os_privacy_page/os_privacy_page.m.js';
+import './date_time_page/timezone_selector.m.js';
 
 export {LifetimeBrowserProxy, LifetimeBrowserProxyImpl} from '../lifetime_browser_proxy.m.js';
 export {dataUsageStringToEnum, NearbyShareDataUsage} from '../nearby_share_page/types.m.js';
diff --git a/chrome/browser/resources/settings/chromeos/os_settings_resources_v3.grdp b/chrome/browser/resources/settings/chromeos/os_settings_resources_v3.grdp
index 8d1583645..c020c33 100644
--- a/chrome/browser/resources/settings/chromeos/os_settings_resources_v3.grdp
+++ b/chrome/browser/resources/settings/chromeos/os_settings_resources_v3.grdp
@@ -62,6 +62,11 @@
            use_base_dir="false"
            compress="false"
            type="BINDATA" />
+  <include name="IDR_OS_SETTINGS_SETTINGS_DROPDOWN_MENU_M_JS"
+           file="${root_gen_dir}/chrome/browser/resources/settings/controls/settings_dropdown_menu.m.js"
+           use_base_dir="false"
+           compress="false"
+           type="BINDATA" />
   <include name="IDR_OS_SETTINGS_PEOPLE_PAGE_ACCOUNT_MANAGER_BROWSER_PROXY_M_JS"
            file="${root_gen_dir}/chrome/browser/resources/settings/people_page/account_manager_browser_proxy.m.js"
            use_base_dir="false"
@@ -455,4 +460,14 @@
            file="chromeos/os_settings.js"
            compress="false"
            type="BINDATA" />
+  <include name="IDR_OS_SETTINGS_SETTINGS_DATE_TIME_PAGE_DATE_TIME_TYPES_M_JS"
+           file="${root_gen_dir}/chrome/browser/resources/settings/chromeos/date_time_page/date_time_types.m.js"
+           use_base_dir="false"
+           compress="false"
+           type="BINDATA" />
+  <include name="IDR_OS_SETTINGS_SETTINGS_DATE_TIME_PAGE_TIMEZONE_SELECTOR_M_JS"
+           file="${root_gen_dir}/chrome/browser/resources/settings/chromeos/date_time_page/timezone_selector.m.js"
+           use_base_dir="false"
+           compress="false"
+           type="BINDATA" />
 </grit-part>
diff --git a/chrome/browser/safe_browsing/cloud_content_scanning/binary_upload_service.cc b/chrome/browser/safe_browsing/cloud_content_scanning/binary_upload_service.cc
index 11a7030..3327ac76 100644
--- a/chrome/browser/safe_browsing/cloud_content_scanning/binary_upload_service.cc
+++ b/chrome/browser/safe_browsing/cloud_content_scanning/binary_upload_service.cc
@@ -303,7 +303,7 @@
         request->deep_scanning_request());
   } else {
     WebUIInfoSingleton::GetInstance()->AddToDeepScanRequests(
-        request->content_analysis_request());
+        request->tab_url(), request->content_analysis_request());
   }
 
   // |request| might have been deleted by the call to Start() in tests, so don't
@@ -471,7 +471,7 @@
   // We add the request here in case we never actually uploaded anything, so it
   // wasn't added in OnGetRequestData
   WebUIInfoSingleton::GetInstance()->AddToDeepScanRequests(
-      request->content_analysis_request());
+      request->tab_url(), request->content_analysis_request());
   WebUIInfoSingleton::GetInstance()->AddToDeepScanResponses(
       active_tokens_[request], ResultToString(result), response);
 
@@ -608,6 +608,16 @@
 
 BinaryUploadService::Request::~Request() = default;
 
+void BinaryUploadService::Request::set_tab_url(const GURL& tab_url) {
+  DCHECK(!use_legacy_proto_);
+  tab_url_ = tab_url;
+}
+
+const GURL& BinaryUploadService::Request::tab_url() const {
+  DCHECK(!use_legacy_proto_);
+  return tab_url_;
+}
+
 void BinaryUploadService::Request::set_request_dlp_scan(
     DlpDeepScanningClientRequest dlp_request) {
   DCHECK(use_legacy_proto_);
diff --git a/chrome/browser/safe_browsing/cloud_content_scanning/binary_upload_service.h b/chrome/browser/safe_browsing/cloud_content_scanning/binary_upload_service.h
index 09e5a2d..e0eb274e 100644
--- a/chrome/browser/safe_browsing/cloud_content_scanning/binary_upload_service.h
+++ b/chrome/browser/safe_browsing/cloud_content_scanning/binary_upload_service.h
@@ -148,6 +148,9 @@
 
     bool use_legacy_proto() const { return use_legacy_proto_; }
 
+    void set_tab_url(const GURL& tab_url);
+    const GURL& tab_url() const;
+
     // Methods for modifying the DeepScanningClientRequest.
     void set_request_dlp_scan(DlpDeepScanningClientRequest dlp_request);
     void set_request_malware_scan(
@@ -198,6 +201,8 @@
     ContentAnalysisCallback content_analysis_callback_;
 
     GURL url_;
+    // The URL of the page that initially triggered the scan.
+    GURL tab_url_;
   };
 
   // Upload the given file contents for deep scanning if the browser is
diff --git a/chrome/browser/safe_browsing/cloud_content_scanning/deep_scanning_dialog_delegate.cc b/chrome/browser/safe_browsing/cloud_content_scanning/deep_scanning_dialog_delegate.cc
index 098f465..4c7bb9b 100644
--- a/chrome/browser/safe_browsing/cloud_content_scanning/deep_scanning_dialog_delegate.cc
+++ b/chrome/browser/safe_browsing/cloud_content_scanning/deep_scanning_dialog_delegate.cc
@@ -786,6 +786,7 @@
   request->set_analysis_connector(connector);
   request->set_email(GetProfileEmail(profile));
   request->set_url(data_.url.spec());
+  request->set_tab_url(data_.url);
   for (const std::string& tag : data_.settings.tags)
     request->add_tag(tag);
 }
diff --git a/chrome/browser/safe_browsing/download_protection/deep_scanning_request.cc b/chrome/browser/safe_browsing/download_protection/deep_scanning_request.cc
index a140bbf..cbbd33b 100644
--- a/chrome/browser/safe_browsing/download_protection/deep_scanning_request.cc
+++ b/chrome/browser/safe_browsing/download_protection/deep_scanning_request.cc
@@ -351,6 +351,9 @@
   if (item_->GetURL().is_valid())
     request->set_url(item_->GetURL().spec());
 
+  if (item_->GetTabUrl().is_valid())
+    request->set_tab_url(item_->GetTabUrl());
+
   for (const std::string& tag : analysis_settings_.tags)
     request->add_tag(tag);
 }
diff --git a/chrome/browser/sharesheet/sharesheet_service.cc b/chrome/browser/sharesheet/sharesheet_service.cc
index c185a9b..72884e4f 100644
--- a/chrome/browser/sharesheet/sharesheet_service.cc
+++ b/chrome/browser/sharesheet/sharesheet_service.cc
@@ -42,24 +42,19 @@
   auto sharesheet_service_delegate =
       std::make_unique<SharesheetServiceDelegate>(
           delegate_counter_++, std::move(bubble_anchor_view), this);
+  ShowBubbleWithDelegate(std::move(sharesheet_service_delegate),
+                         std::move(intent));
+}
 
-  std::vector<TargetInfo> targets;
-  auto& actions = sharesheet_action_cache_->GetShareActions();
-  auto iter = actions.begin();
-  while (iter != actions.end()) {
-    targets.emplace(targets.begin(), TargetType::kAction,
-                    (*iter)->GetActionIcon(), (*iter)->GetActionName(),
-                    (*iter)->GetActionName());
-    ++iter;
-  }
-
-  std::vector<apps::AppIdAndActivityName> app_id_and_activities =
-      app_service_proxy_->GetAppsForIntent(intent);
-  LoadAppIcons(std::move(app_id_and_activities), std::move(targets), 0,
-               base::BindOnce(&SharesheetService::OnAppIconsLoaded,
-                              weak_factory_.GetWeakPtr(),
-                              std::move(sharesheet_service_delegate),
-                              std::move(intent)));
+void SharesheetService::ShowBubble(content::WebContents* web_contents,
+                                   apps::mojom::IntentPtr intent) {
+  DCHECK(intent->action == apps_util::kIntentActionSend ||
+         intent->action == apps_util::kIntentActionSendMultiple);
+  auto sharesheet_service_delegate =
+      std::make_unique<SharesheetServiceDelegate>(delegate_counter_++,
+                                                  web_contents, this);
+  ShowBubbleWithDelegate(std::move(sharesheet_service_delegate),
+                         std::move(intent));
 }
 
 // Cleanup delegate when bubble closes.
@@ -121,7 +116,7 @@
   return nullptr;
 }
 
-bool SharesheetService::HasShareTargets(apps::mojom::IntentPtr intent) {
+bool SharesheetService::HasShareTargets(const apps::mojom::IntentPtr& intent) {
   auto& actions = sharesheet_action_cache_->GetShareActions();
   std::vector<apps::AppIdAndActivityName> app_id_and_activities =
       app_service_proxy_->GetAppsForIntent(intent);
@@ -175,8 +170,28 @@
     apps::mojom::IntentPtr intent,
     std::vector<TargetInfo> targets) {
   delegate->ShowBubble(std::move(targets), std::move(intent));
-
   active_delegates_.push_back(std::move(delegate));
 }
 
+void SharesheetService::ShowBubbleWithDelegate(
+    std::unique_ptr<SharesheetServiceDelegate> delegate,
+    apps::mojom::IntentPtr intent) {
+  std::vector<TargetInfo> targets;
+  auto& actions = sharesheet_action_cache_->GetShareActions();
+  auto iter = actions.begin();
+  while (iter != actions.end()) {
+    targets.emplace(targets.begin(), TargetType::kAction,
+                    (*iter)->GetActionIcon(), (*iter)->GetActionName(),
+                    (*iter)->GetActionName());
+    ++iter;
+  }
+
+  std::vector<apps::AppIdAndActivityName> app_id_and_activities =
+      app_service_proxy_->GetAppsForIntent(intent);
+  LoadAppIcons(std::move(app_id_and_activities), std::move(targets), 0,
+               base::BindOnce(&SharesheetService::OnAppIconsLoaded,
+                              weak_factory_.GetWeakPtr(), std::move(delegate),
+                              std::move(intent)));
+}
+
 }  // namespace sharesheet
diff --git a/chrome/browser/sharesheet/sharesheet_service.h b/chrome/browser/sharesheet/sharesheet_service.h
index 829ac36..d4abbba 100644
--- a/chrome/browser/sharesheet/sharesheet_service.h
+++ b/chrome/browser/sharesheet/sharesheet_service.h
@@ -27,6 +27,10 @@
 class View;
 }
 
+namespace content {
+class WebContents;
+}
+
 namespace sharesheet {
 
 class SharesheetServiceDelegate;
@@ -41,8 +45,13 @@
   SharesheetService(const SharesheetService&) = delete;
   SharesheetService& operator=(const SharesheetService&) = delete;
 
+  // Displays the dialog (aka bubble) for sharing content (or files) with
+  // other applications and targets. |intent| contains the list of the
+  // files/content to be shared.
   void ShowBubble(views::View* bubble_anchor_view,
                   apps::mojom::IntentPtr intent);
+  void ShowBubble(content::WebContents* web_contents,
+                  apps::mojom::IntentPtr intent);
   void OnBubbleClosed(uint32_t id, const base::string16& active_action);
   void OnTargetSelected(uint32_t delegate_id,
                         const base::string16& target_name,
@@ -50,7 +59,7 @@
                         apps::mojom::IntentPtr intent,
                         views::View* share_action_view);
   SharesheetServiceDelegate* GetDelegate(uint32_t delegate_id);
-  bool HasShareTargets(apps::mojom::IntentPtr intent);
+  bool HasShareTargets(const apps::mojom::IntentPtr& intent);
 
  private:
   using SharesheetServiceIconLoaderCallback =
@@ -73,6 +82,10 @@
                         apps::mojom::IntentPtr intent,
                         std::vector<TargetInfo> targets);
 
+  void ShowBubbleWithDelegate(
+      std::unique_ptr<SharesheetServiceDelegate> delegate,
+      apps::mojom::IntentPtr intent);
+
   uint32_t delegate_counter_ = 0;
   std::unique_ptr<SharesheetActionCache> sharesheet_action_cache_;
   apps::AppServiceProxy* app_service_proxy_;
diff --git a/chrome/browser/sharesheet/sharesheet_service_delegate.cc b/chrome/browser/sharesheet/sharesheet_service_delegate.cc
index be53dd7..1d7a34a 100644
--- a/chrome/browser/sharesheet/sharesheet_service_delegate.cc
+++ b/chrome/browser/sharesheet/sharesheet_service_delegate.cc
@@ -11,6 +11,7 @@
 #include "chrome/browser/sharesheet/sharesheet_service.h"
 #include "chrome/browser/sharesheet/sharesheet_service_factory.h"
 #include "chrome/browser/ui/views/sharesheet_bubble_view.h"
+#include "content/public/browser/web_contents.h"
 #include "ui/views/view.h"
 
 namespace sharesheet {
@@ -23,6 +24,14 @@
       sharesheet_bubble_view_(
           std::make_unique<SharesheetBubbleView>(bubble_anchor_view, this)),
       sharesheet_service_(sharesheet_service) {}
+SharesheetServiceDelegate::SharesheetServiceDelegate(
+    uint32_t id,
+    content::WebContents* web_contents,
+    SharesheetService* sharesheet_service)
+    : id_(id),
+      sharesheet_bubble_view_(
+          std::make_unique<SharesheetBubbleView>(web_contents, this)),
+      sharesheet_service_(sharesheet_service) {}
 
 SharesheetServiceDelegate::~SharesheetServiceDelegate() = default;
 
diff --git a/chrome/browser/sharesheet/sharesheet_service_delegate.h b/chrome/browser/sharesheet/sharesheet_service_delegate.h
index a4b86c1..7715c11 100644
--- a/chrome/browser/sharesheet/sharesheet_service_delegate.h
+++ b/chrome/browser/sharesheet/sharesheet_service_delegate.h
@@ -18,6 +18,10 @@
 class View;
 }
 
+namespace content {
+class WebContents;
+}
+
 namespace sharesheet {
 
 class SharesheetService;
@@ -26,9 +30,12 @@
 // business logic in the sharesheet.
 class SharesheetServiceDelegate : public SharesheetController {
  public:
-  explicit SharesheetServiceDelegate(uint32_t id,
-                                     views::View* bubble_anchor_view,
-                                     SharesheetService* sharesheet_service);
+  SharesheetServiceDelegate(uint32_t id,
+                            views::View* bubble_anchor_view,
+                            SharesheetService* sharesheet_service);
+  SharesheetServiceDelegate(uint32_t id,
+                            content::WebContents* web_contents,
+                            SharesheetService* sharesheet_service);
   ~SharesheetServiceDelegate() override;
   SharesheetServiceDelegate(const SharesheetServiceDelegate&) = delete;
   SharesheetServiceDelegate& operator=(const SharesheetServiceDelegate&) =
diff --git a/chrome/browser/sharing/webrtc/sharing_service_host.cc b/chrome/browser/sharing/webrtc/sharing_service_host.cc
index abee3a43..aed679f8 100644
--- a/chrome/browser/sharing/webrtc/sharing_service_host.cc
+++ b/chrome/browser/sharing/webrtc/sharing_service_host.cc
@@ -148,7 +148,7 @@
 }  // namespace
 
 struct SharingWebRtcMojoPipes {
-  MojoPipe<sharing::mojom::SignallingSender> signalling_sender;
+  MojoPipe<sharing::mojom::SignalingSender> signaling_sender;
   MojoPipe<sharing::mojom::SignallingReceiver> signalling_receiver;
   MojoPipe<sharing::mojom::SharingWebRtcConnectionDelegate> delegate;
   MojoPipe<sharing::mojom::SharingWebRtcConnection> connection;
@@ -266,7 +266,7 @@
   auto pipes = std::make_unique<SharingWebRtcMojoPipes>();
 
   auto signalling_host = std::make_unique<WebRtcSignallingHostFCM>(
-      std::move(pipes->signalling_sender.receiver),
+      std::move(pipes->signaling_sender.receiver),
       std::move(pipes->signalling_receiver.remote), message_sender_,
       CreateDeviceInfo(device_guid, fcm_configuration, device_source_));
 
@@ -312,7 +312,7 @@
   }
 
   sharing_utility_service_->CreateSharingWebRtcConnection(
-      std::move(pipes->signalling_sender.remote),
+      std::move(pipes->signaling_sender.remote),
       std::move(pipes->signalling_receiver.receiver),
       std::move(pipes->delegate.remote), std::move(pipes->connection.receiver),
       std::move(pipes->socket_manager.remote),
diff --git a/chrome/browser/sharing/webrtc/sharing_service_host_unittest.cc b/chrome/browser/sharing/webrtc/sharing_service_host_unittest.cc
index 01c5a73f..be23cc3 100644
--- a/chrome/browser/sharing/webrtc/sharing_service_host_unittest.cc
+++ b/chrome/browser/sharing/webrtc/sharing_service_host_unittest.cc
@@ -38,7 +38,7 @@
 
   // sharing::mojom::Sharing:
   void CreateSharingWebRtcConnection(
-      mojo::PendingRemote<sharing::mojom::SignallingSender> signalling_sender,
+      mojo::PendingRemote<sharing::mojom::SignalingSender> signaling_sender,
       mojo::PendingReceiver<sharing::mojom::SignallingReceiver>
           signalling_receiver,
       mojo::PendingRemote<sharing::mojom::SharingWebRtcConnectionDelegate>
diff --git a/chrome/browser/sharing/webrtc/sharing_webrtc_connection_host_unittest.cc b/chrome/browser/sharing/webrtc/sharing_webrtc_connection_host_unittest.cc
index 05f0cf1..f29611d 100644
--- a/chrome/browser/sharing/webrtc/sharing_webrtc_connection_host_unittest.cc
+++ b/chrome/browser/sharing/webrtc/sharing_webrtc_connection_host_unittest.cc
@@ -52,7 +52,7 @@
  public:
   MockSignallingHost()
       : WebRtcSignallingHostFCM(
-            mojo::PendingReceiver<sharing::mojom::SignallingSender>(),
+            mojo::PendingReceiver<sharing::mojom::SignalingSender>(),
             mojo::PendingRemote<sharing::mojom::SignallingReceiver>(),
             /*message_sender=*/nullptr,
             CreateFakeDeviceInfo("id", "name")) {}
diff --git a/chrome/browser/sharing/webrtc/webrtc_signalling_host_fcm.cc b/chrome/browser/sharing/webrtc/webrtc_signalling_host_fcm.cc
index 2667fe5..b790dbb 100644
--- a/chrome/browser/sharing/webrtc/webrtc_signalling_host_fcm.cc
+++ b/chrome/browser/sharing/webrtc/webrtc_signalling_host_fcm.cc
@@ -12,13 +12,13 @@
 #include "chrome/browser/sharing/sharing_send_message_result.h"
 
 WebRtcSignallingHostFCM::WebRtcSignallingHostFCM(
-    mojo::PendingReceiver<sharing::mojom::SignallingSender> signalling_sender,
+    mojo::PendingReceiver<sharing::mojom::SignalingSender> signaling_sender,
     mojo::PendingRemote<sharing::mojom::SignallingReceiver> signalling_receiver,
     SharingMessageSender* message_sender,
     std::unique_ptr<syncer::DeviceInfo> device_info)
     : message_sender_(message_sender),
       device_info_(std::move(device_info)),
-      signalling_sender_(this, std::move(signalling_sender)),
+      signaling_sender_(this, std::move(signaling_sender)),
       signalling_receiver_(std::move(signalling_receiver)) {
   DCHECK(device_info_);
 }
diff --git a/chrome/browser/sharing/webrtc/webrtc_signalling_host_fcm.h b/chrome/browser/sharing/webrtc/webrtc_signalling_host_fcm.h
index 6af4852..b1693d3 100644
--- a/chrome/browser/sharing/webrtc/webrtc_signalling_host_fcm.h
+++ b/chrome/browser/sharing/webrtc/webrtc_signalling_host_fcm.h
@@ -19,12 +19,12 @@
 #include "mojo/public/cpp/bindings/receiver.h"
 #include "mojo/public/cpp/bindings/remote.h"
 
-// Handles the signalling part of a WebRTC connection via FCM. The signalling
+// Handles the signaling part of a WebRTC connection via FCM. The signaling
 // messages are sent via the |message_sender| to |device_info| through FCM.
-class WebRtcSignallingHostFCM : public sharing::mojom::SignallingSender {
+class WebRtcSignallingHostFCM : public sharing::mojom::SignalingSender {
  public:
   WebRtcSignallingHostFCM(
-      mojo::PendingReceiver<sharing::mojom::SignallingSender> signalling_sender,
+      mojo::PendingReceiver<sharing::mojom::SignalingSender> signaling_sender,
       mojo::PendingRemote<sharing::mojom::SignallingReceiver>
           signalling_receiver,
       SharingMessageSender* message_sender,
@@ -33,7 +33,7 @@
   WebRtcSignallingHostFCM& operator=(const WebRtcSignallingHostFCM&) = delete;
   ~WebRtcSignallingHostFCM() override;
 
-  // sharing::mojom::SignallingSender:
+  // sharing::mojom::SignalingSender:
   void SendOffer(const std::string& offer, SendOfferCallback callback) override;
   void SendIceCandidates(
       std::vector<sharing::mojom::IceCandidatePtr> ice_candidates) override;
@@ -53,7 +53,7 @@
   SharingMessageSender* message_sender_;
   std::unique_ptr<syncer::DeviceInfo> device_info_;
 
-  mojo::Receiver<sharing::mojom::SignallingSender> signalling_sender_;
+  mojo::Receiver<sharing::mojom::SignalingSender> signaling_sender_;
   mojo::Remote<sharing::mojom::SignallingReceiver> signalling_receiver_;
 
   base::WeakPtrFactory<WebRtcSignallingHostFCM> weak_ptr_factory_{this};
diff --git a/chrome/browser/sharing/webrtc/webrtc_signalling_host_fcm_unittest.cc b/chrome/browser/sharing/webrtc/webrtc_signalling_host_fcm_unittest.cc
index 0d472849..e0ce580 100644
--- a/chrome/browser/sharing/webrtc/webrtc_signalling_host_fcm_unittest.cc
+++ b/chrome/browser/sharing/webrtc/webrtc_signalling_host_fcm_unittest.cc
@@ -29,7 +29,7 @@
   MOCK_METHOD1(OnIceCandidatesReceived,
                void(std::vector<sharing::mojom::IceCandidatePtr>));
 
-  mojo::Remote<sharing::mojom::SignallingSender> signalling_sender;
+  mojo::Remote<sharing::mojom::SignalingSender> signaling_sender;
   mojo::Receiver<sharing::mojom::SignallingReceiver> signalling_receiver{this};
 };
 
@@ -73,7 +73,7 @@
   MockSharingMessageSender message_sender_;
   MockSignallingService signalling_service_;
   WebRtcSignallingHostFCM signalling_host_{
-      signalling_service_.signalling_sender.BindNewPipeAndPassReceiver(),
+      signalling_service_.signaling_sender.BindNewPipeAndPassReceiver(),
       signalling_service_.signalling_receiver.BindNewPipeAndPassRemote(),
       &message_sender_, CreateFakeDeviceInfo("id", "name")};
 
diff --git a/chrome/browser/ui/BUILD.gn b/chrome/browser/ui/BUILD.gn
index 38980e3..4a9044dd 100644
--- a/chrome/browser/ui/BUILD.gn
+++ b/chrome/browser/ui/BUILD.gn
@@ -1488,7 +1488,10 @@
       "//chrome/app/vector_icons",
       "//chrome/browser:theme_properties",
       "//chrome/browser/media/router",
+      "//chrome/browser/nearby_sharing/certificates",
+      "//chrome/browser/nearby_sharing/client",
       "//chrome/browser/nearby_sharing/contacts",
+      "//chrome/browser/nearby_sharing/local_device_data",
       "//chrome/browser/nearby_sharing/logging",
       "//chrome/browser/nearby_sharing/logging:util",
       "//chrome/browser/nearby_sharing/proto",
diff --git a/chrome/browser/ui/android/appmenu/OWNERS b/chrome/browser/ui/android/appmenu/OWNERS
index 3feaf49..8e0277c4 100644
--- a/chrome/browser/ui/android/appmenu/OWNERS
+++ b/chrome/browser/ui/android/appmenu/OWNERS
@@ -1,3 +1,5 @@
 twellington@chromium.org
 
-# COMPONENT: UI>Browser>Mobile
+# COMPONENT: UI>Browser>Mobile>AppMenu
+# TEAM: chrome-android-app@chromium.org
+# OS: Android
diff --git a/chrome/browser/ui/ash/clipboard_history_browsertest.cc b/chrome/browser/ui/ash/clipboard_history_browsertest.cc
index 676b80c..d49ed18c 100644
--- a/chrome/browser/ui/ash/clipboard_history_browsertest.cc
+++ b/chrome/browser/ui/ash/clipboard_history_browsertest.cc
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 #include <list>
+#include <memory>
 
 #include "ash/clipboard/clipboard_history.h"
 #include "ash/clipboard/clipboard_history_controller.h"
@@ -17,9 +18,22 @@
 #include "ui/base/clipboard/scoped_clipboard_writer.h"
 #include "ui/events/test/event_generator.h"
 #include "ui/views/controls/menu/menu_config.h"
+#include "ui/views/controls/textfield/textfield.h"
+#include "ui/views/widget/widget.h"
 
 namespace {
 
+std::unique_ptr<views::Widget> CreateTestWidget() {
+  auto widget = std::make_unique<views::Widget>();
+
+  views::Widget::InitParams params;
+  params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
+  params.type = views::Widget::InitParams::TYPE_WINDOW_FRAMELESS;
+  widget->Init(std::move(params));
+
+  return widget;
+}
+
 void SetClipboardText(const std::string& text) {
   ui::ScopedClipboardWriter(ui::ClipboardBuffer::kCopyPaste)
       .WriteText(base::ASCIIToUTF16(text));
@@ -35,12 +49,11 @@
 }
 
 const std::list<ui::ClipboardData>& GetClipboardData() {
-  return GetClipboardHistoryController()->clipboard_history()->GetItems();
+  return GetClipboardHistoryController()->history()->GetItems();
 }
 
-gfx::Rect GetClipboardHistoryMenuScreenBounds() {
-  return GetClipboardHistoryController()
-      ->GetClipboardHistoryMenuBoundsForTest();
+gfx::Rect GetClipboardHistoryMenuBoundsInScreen() {
+  return GetClipboardHistoryController()->GetMenuBoundsInScreenForTest();
 }
 
 class ClipboardTestHelper {
@@ -55,11 +68,9 @@
 
   ui::test::EventGenerator* event_generator() { return event_generator_.get(); }
 
-  void ShowContextMenuViaAccelerator() {
-    event_generator_->PressKey(ui::KeyboardCode::VKEY_COMMAND, ui::EF_NONE);
-    event_generator_->PressKey(ui::KeyboardCode::VKEY_V, ui::EF_COMMAND_DOWN);
-    event_generator_->ReleaseKey(ui::KeyboardCode::VKEY_V, ui::EF_COMMAND_DOWN);
-    event_generator_->ReleaseKey(ui::KeyboardCode::VKEY_COMMAND, ui::EF_NONE);
+  void PressAndRelease(ui::KeyboardCode key, int modifiers) {
+    event_generator_->PressKey(key, modifiers);
+    event_generator_->ReleaseKey(key, modifiers);
   }
 
  private:
@@ -87,8 +98,12 @@
   }
 
  protected:
+  void PressAndRelease(ui::KeyboardCode key, int modifiers = ui::EF_NONE) {
+    test_helper_->PressAndRelease(key, modifiers);
+  }
+
   void ShowContextMenuViaAccelerator() {
-    test_helper_->ShowContextMenuViaAccelerator();
+    PressAndRelease(ui::KeyboardCode::VKEY_V, ui::EF_COMMAND_DOWN);
   }
 
   void SetUpOnMainThread() override {
@@ -183,9 +198,83 @@
 
   // Verifies that the menu is anchored at the cursor's location.
   ASSERT_TRUE(GetClipboardHistoryController()->IsMenuShowing());
-  const gfx::Point menu_origin = GetClipboardHistoryMenuScreenBounds().origin();
+  const gfx::Point menu_origin =
+      GetClipboardHistoryMenuBoundsInScreen().origin();
   EXPECT_EQ(mouse_location.x() +
                 views::MenuConfig::instance().touchable_anchor_offset,
             menu_origin.x());
   EXPECT_EQ(mouse_location.y(), menu_origin.y());
 }
+
+IN_PROC_BROWSER_TEST_F(ClipboardHistoryWithMultiProfileBrowserTest,
+                       ShouldPasteHistoryViaKeyboard) {
+  LoginUser(account_id1_);
+  CloseAllBrowsers();
+
+  // Create a widget containing a single, focusable textfield.
+  auto widget = CreateTestWidget();
+  auto* textfield =
+      widget->SetContentsView(std::make_unique<views::Textfield>());
+  textfield->SetAccessibleName(base::UTF8ToUTF16("Textfield"));
+  textfield->SetFocusBehavior(views::View::FocusBehavior::ALWAYS);
+
+  // Show the widget.
+  widget->SetBounds(gfx::Rect(0, 0, 100, 100));
+  widget->Show();
+  EXPECT_TRUE(widget->IsActive());
+
+  // Focus the textfield and confirm initial state.
+  textfield->RequestFocus();
+  EXPECT_TRUE(textfield->HasFocus());
+  EXPECT_TRUE(textfield->GetText().empty());
+
+  // Write some things to the clipboard.
+  SetClipboardText("A");
+  SetClipboardText("B");
+  SetClipboardText("C");
+
+  // Verify we can paste the first history item via the ENTER key.
+  PressAndRelease(ui::KeyboardCode::VKEY_V, ui::EF_COMMAND_DOWN);
+  EXPECT_TRUE(GetClipboardHistoryController()->IsMenuShowing());
+  PressAndRelease(ui::KeyboardCode::VKEY_DOWN);
+  PressAndRelease(ui::KeyboardCode::VKEY_RETURN);
+  EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing());
+  EXPECT_EQ("C", base::UTF16ToUTF8(textfield->GetText()));
+
+  textfield->SetText(base::string16());
+  EXPECT_TRUE(textfield->GetText().empty());
+
+  // Verify we can paste the first history item via the COMMAND+V shortcut.
+  PressAndRelease(ui::KeyboardCode::VKEY_V, ui::EF_COMMAND_DOWN);
+  EXPECT_TRUE(GetClipboardHistoryController()->IsMenuShowing());
+  PressAndRelease(ui::KeyboardCode::VKEY_DOWN);
+  PressAndRelease(ui::KeyboardCode::VKEY_V, ui::EF_COMMAND_DOWN);
+  EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing());
+  EXPECT_EQ("C", base::UTF16ToUTF8(textfield->GetText()));
+
+  textfield->SetText(base::string16());
+  EXPECT_TRUE(textfield->GetText().empty());
+
+  // Verify we can paste the last history item via the ENTER key.
+  PressAndRelease(ui::KeyboardCode::VKEY_V, ui::EF_COMMAND_DOWN);
+  EXPECT_TRUE(GetClipboardHistoryController()->IsMenuShowing());
+  PressAndRelease(ui::KeyboardCode::VKEY_DOWN);
+  PressAndRelease(ui::KeyboardCode::VKEY_DOWN);
+  PressAndRelease(ui::KeyboardCode::VKEY_DOWN);
+  PressAndRelease(ui::KeyboardCode::VKEY_RETURN);
+  EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing());
+  EXPECT_EQ("A", base::UTF16ToUTF8(textfield->GetText()));
+
+  textfield->SetText(base::string16());
+  EXPECT_TRUE(textfield->GetText().empty());
+
+  // Verify we can paste the last history item via the COMMAND+V shortcut.
+  PressAndRelease(ui::KeyboardCode::VKEY_V, ui::EF_COMMAND_DOWN);
+  EXPECT_TRUE(GetClipboardHistoryController()->IsMenuShowing());
+  PressAndRelease(ui::KeyboardCode::VKEY_DOWN);
+  PressAndRelease(ui::KeyboardCode::VKEY_DOWN);
+  PressAndRelease(ui::KeyboardCode::VKEY_DOWN);
+  PressAndRelease(ui::KeyboardCode::VKEY_V, ui::EF_COMMAND_DOWN);
+  EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing());
+  EXPECT_EQ("A", base::UTF16ToUTF8(textfield->GetText()));
+}
diff --git a/chrome/browser/ui/views/autofill/autofill_popup_base_view.cc b/chrome/browser/ui/views/autofill/autofill_popup_base_view.cc
index 2a75d79..cb0c7e0 100644
--- a/chrome/browser/ui/views/autofill/autofill_popup_base_view.cc
+++ b/chrome/browser/ui/views/autofill/autofill_popup_base_view.cc
@@ -17,10 +17,11 @@
 #include "chrome/browser/ui/browser_window.h"
 #include "chrome/browser/ui/views/autofill/autofill_popup_view_utils.h"
 #include "chrome/browser/ui/views/chrome_layout_provider.h"
+#include "chrome/browser/ui/views/frame/browser_view.h"
+#include "chrome/browser/ui/views/frame/contents_web_view.h"
 #include "components/strings/grit/components_strings.h"
 #include "ui/accessibility/ax_enums.mojom.h"
 #include "ui/accessibility/platform/ax_platform_node.h"
-#include "ui/base/buildflags.h"
 #include "ui/base/l10n/l10n_util.h"
 #include "ui/gfx/color_palette.h"
 #include "ui/native_theme/native_theme.h"
@@ -117,7 +118,11 @@
   }
 
   GetWidget()->GetRootView()->SetBorder(CreateBorder());
-  DoUpdateBoundsAndRedrawPopup();
+  bool enough_height = DoUpdateBoundsAndRedrawPopup();
+  // If there is insufficient height, DoUpdateBoundsAndRedrawPopup() hides and
+  // thus deletes |this|. Hence, there is nothing else to do.
+  if (!enough_height)
+    return;
   GetWidget()->Show();
 
   // Showing the widget can change native focus (which would result in an
@@ -218,25 +223,31 @@
 }
 
 gfx::Rect AutofillPopupBaseView::GetWindowBounds() const {
-// The call to FindBrowserWithWindow will fail on Android, so we use
-// platform-specific calls.
-#if defined(OS_ANDROID)
-  return delegate()->container_view()->GetWindowAndroid()->bounds();
-#else
   views::Widget* widget = views::Widget::GetTopLevelWidgetForNativeView(
       delegate()->container_view());
   if (widget)
     return widget->GetWindowBoundsInScreen();
 
-  // If the browser is null, simply return an empty rect. The most common reason
+  // If the widget is null, simply return an empty rect. The most common reason
   // to end up here is that the NativeView has been destroyed externally, which
   // can happen at any time. This happens fairly commonly on Windows (e.g., at
   // shutdown) in particular.
   return gfx::Rect();
-#endif
 }
 
-void AutofillPopupBaseView::DoUpdateBoundsAndRedrawPopup() {
+gfx::Rect AutofillPopupBaseView::GetContentAreaBounds() const {
+  content::WebContents* web_contents = delegate()->GetWebContents();
+  if (web_contents)
+    return web_contents->GetContainerBounds();
+
+  // If the |web_contents| is null, simply return an empty rect. The most common
+  // reason to end up here is that the |web_contents| has been destroyed
+  // externally, which can happen at any time. This happens fairly commonly on
+  // Windows (e.g., at shutdown) in particular.
+  return gfx::Rect();
+}
+
+bool AutofillPopupBaseView::DoUpdateBoundsAndRedrawPopup() {
   gfx::Size preferred_size = GetPreferredSize();
 
   // When a bubble border is shown, the contents area (inside the shadow) is
@@ -244,6 +255,16 @@
   gfx::Rect element_bounds = gfx::ToEnclosingRect(delegate()->element_bounds());
   element_bounds.Inset(/*horizontal=*/0, /*vertical=*/-kElementBorderPadding);
 
+  // At least one row of the popup should be shown in the bounds of the content
+  // area so that the user notices the presence of the popup.
+  int item_height =
+      children().size() > 0 ? children()[0]->GetPreferredSize().height() : 0;
+  if (!HasEnoughHeightForOneRow(item_height, GetContentAreaBounds(),
+                                element_bounds)) {
+    HideController(PopupHidingReason::kInsufficientSpace);
+    return false;
+  }
+
   gfx::Rect popup_bounds = CalculatePopupBounds(
       preferred_size, GetWindowBounds(), element_bounds, delegate()->IsRTL());
   // Account for the scroll view's border so that the content has enough space.
@@ -253,6 +274,7 @@
   Layout();
   UpdateClipPath();
   SchedulePaint();
+  return true;
 }
 
 void AutofillPopupBaseView::OnNativeFocusChanged(gfx::NativeView focused_now) {
diff --git a/chrome/browser/ui/views/autofill/autofill_popup_base_view.h b/chrome/browser/ui/views/autofill/autofill_popup_base_view.h
index 9d8116f..5876a6c4 100644
--- a/chrome/browser/ui/views/autofill/autofill_popup_base_view.h
+++ b/chrome/browser/ui/views/autofill/autofill_popup_base_view.h
@@ -70,8 +70,13 @@
   // Returns the bounds of the containing window in screen space.
   gfx::Rect GetWindowBounds() const;
 
-  // Update size of popup and paint (virtual for testing).
-  virtual void DoUpdateBoundsAndRedrawPopup();
+  // Returns the bounds of the content area in screen space.
+  gfx::Rect GetContentAreaBounds() const;
+
+  // Update size of popup and paint. If there is insufficient height to draw the
+  // popup, it hides and thus deletes |this| and returns false. (virtual for
+  // testing).
+  virtual bool DoUpdateBoundsAndRedrawPopup();
 
   const AutofillPopupViewDelegate* delegate() const { return delegate_; }
 
diff --git a/chrome/browser/ui/views/autofill/autofill_popup_view_native_views.cc b/chrome/browser/ui/views/autofill/autofill_popup_view_native_views.cc
index e75759e..4a9504e 100644
--- a/chrome/browser/ui/views/autofill/autofill_popup_view_native_views.cc
+++ b/chrome/browser/ui/views/autofill/autofill_popup_view_native_views.cc
@@ -1211,7 +1211,7 @@
   return width;
 }
 
-void AutofillPopupViewNativeViews::DoUpdateBoundsAndRedrawPopup() {
+bool AutofillPopupViewNativeViews::DoUpdateBoundsAndRedrawPopup() {
   gfx::Size preferred_size = CalculatePreferredSize();
   gfx::Rect popup_bounds;
 
@@ -1226,6 +1226,16 @@
   // look too close to the element.
   element_bounds.Inset(/*horizontal=*/0, /*vertical=*/-kElementBorderPadding);
 
+  int item_height =
+      body_container_->children().size() > 0
+          ? body_container_->children()[0]->GetPreferredSize().height()
+          : 0;
+  if (!HasEnoughHeightForOneRow(item_height, GetContentAreaBounds(),
+                                element_bounds)) {
+    controller_->Hide(PopupHidingReason::kInsufficientSpace);
+    return false;
+  }
+
   CalculatePopupYAndHeight(preferred_size.height(), window_bounds,
                            element_bounds, &popup_bounds);
 
@@ -1253,6 +1263,7 @@
   UpdateClipPath();
 
   SchedulePaint();
+  return true;
 }
 
 // static
diff --git a/chrome/browser/ui/views/autofill/autofill_popup_view_native_views.h b/chrome/browser/ui/views/autofill/autofill_popup_view_native_views.h
index 2a77c71..7176240 100644
--- a/chrome/browser/ui/views/autofill/autofill_popup_view_native_views.h
+++ b/chrome/browser/ui/views/autofill/autofill_popup_view_native_views.h
@@ -109,7 +109,7 @@
   int AdjustWidth(int width) const;
 
   // AutofillPopupBaseView:
-  void DoUpdateBoundsAndRedrawPopup() override;
+  bool DoUpdateBoundsAndRedrawPopup() override;
 
   // Controller for this view.
   AutofillPopupController* controller_ = nullptr;
diff --git a/chrome/browser/ui/views/autofill/autofill_popup_view_utils.cc b/chrome/browser/ui/views/autofill/autofill_popup_view_utils.cc
index 2011853..e95fe36 100644
--- a/chrome/browser/ui/views/autofill/autofill_popup_view_utils.cc
+++ b/chrome/browser/ui/views/autofill/autofill_popup_view_utils.cc
@@ -86,3 +86,17 @@
 
   return popup_bounds;
 }
+
+bool HasEnoughHeightForOneRow(int item_height,
+                              const gfx::Rect& content_area_bounds,
+                              const gfx::Rect& element_bounds) {
+  // Ensure that at least one row of the popup can be displayed within the
+  // bounds of the content area so that the user notices the presence of the
+  // popup.
+  bool enough_space_for_one_item_in_content_area_above_element =
+      element_bounds.y() - content_area_bounds.y() >= item_height;
+  bool enough_space_for_one_item_in_content_area_below_element =
+      content_area_bounds.bottom() - element_bounds.bottom() >= item_height;
+  return enough_space_for_one_item_in_content_area_above_element ||
+         enough_space_for_one_item_in_content_area_below_element;
+}
diff --git a/chrome/browser/ui/views/autofill/autofill_popup_view_utils.h b/chrome/browser/ui/views/autofill/autofill_popup_view_utils.h
index 88af63ce..990d67c 100644
--- a/chrome/browser/ui/views/autofill/autofill_popup_view_utils.h
+++ b/chrome/browser/ui/views/autofill/autofill_popup_view_utils.h
@@ -33,4 +33,10 @@
                                const gfx::Rect& element_bounds,
                                bool is_rtl);
 
+// Returns whether there is enough height within |content_area_bounds| above or
+// below |element_bounds| to display |item_height|.
+bool HasEnoughHeightForOneRow(int item_height,
+                              const gfx::Rect& content_area_bounds,
+                              const gfx::Rect& element_bounds);
+
 #endif  // CHROME_BROWSER_UI_VIEWS_AUTOFILL_AUTOFILL_POPUP_VIEW_UTILS_H_
diff --git a/chrome/browser/ui/views/autofill/autofill_popup_view_utils_unittest.cc b/chrome/browser/ui/views/autofill/autofill_popup_view_utils_unittest.cc
index 9fd284990..78fa8a09 100644
--- a/chrome/browser/ui/views/autofill/autofill_popup_view_utils_unittest.cc
+++ b/chrome/browser/ui/views/autofill/autofill_popup_view_utils_unittest.cc
@@ -93,3 +93,29 @@
         << "Popup bounds failed to match for rtl test " << i;
   }
 }
+
+TEST(AutofillPopupViewUtilsTest, NotEnoughHeightForAnItem) {
+  // In this test, each row of the popup has a height of 8 pixels, and there is
+  // no enough height in the content area to show one row.
+  //
+  //  |---------------------|    ---> y = 5
+  //  |       Window        |
+  //  | |-----------------| |    ---> y = 7
+  //  | |   Content Area  | |
+  //  | | |-------------| | |    ---> y = 8
+  //  | | |   Element   | | |
+  //  |-|-|-------------|-|-|    ---> y = 15
+
+  constexpr int item_height = 8;
+  constexpr int window_y = 5;
+  constexpr int x = 10;
+  constexpr int width = 5;
+  constexpr int height = 10;
+
+  gfx::Rect content_area_bounds(x, window_y + 2, width, height - 2);
+  gfx::Rect element_bounds(x, window_y + 3, width, height - 3);
+
+  bool enough_height_for_item = HasEnoughHeightForOneRow(
+      item_height, content_area_bounds, element_bounds);
+  EXPECT_FALSE(enough_height_for_item);
+}
diff --git a/chrome/browser/ui/views/omnibox/omnibox_result_view.cc b/chrome/browser/ui/views/omnibox/omnibox_result_view.cc
index a8b8b143..ccf243a 100644
--- a/chrome/browser/ui/views/omnibox/omnibox_result_view.cc
+++ b/chrome/browser/ui/views/omnibox/omnibox_result_view.cc
@@ -123,8 +123,7 @@
 
   suggestion_view_->OnMatchUpdate(this, match_);
   keyword_view_->OnMatchUpdate(this, match_);
-  suggestion_tab_switch_button_->SetVisible(
-      match.ShouldShowTabMatchButtonInlineInResultView());
+  suggestion_tab_switch_button_->SetVisible(ShouldShowTabMatchButtonInline());
   UpdateRemoveSuggestionVisibility();
 
   suggestion_view_->content()->SetText(match_.contents, match_.contents_class);
@@ -242,7 +241,7 @@
     // TODO(orinj): Eventually the deep digging in this class should get
     //  replaced with a single local point of access to all selection state.
     ShowKeyword(popup_contents_view_->model()->selection().state ==
-                OmniboxPopupModel::KEYWORD);
+                OmniboxPopupModel::KEYWORD_MODE);
   } else {
     ShowKeyword(false);
   }
@@ -336,7 +335,7 @@
                                          button_size.height());
   }
 
-  if (match_.ShouldShowTabMatchButtonInlineInResultView()) {
+  if (ShouldShowTabMatchButtonInline()) {
     suggestion_tab_switch_button_->ProvideWidthHint(suggestion_width);
     const gfx::Size ts_button_size =
         suggestion_tab_switch_button_->GetPreferredSize();
@@ -516,6 +515,13 @@
   ApplyThemeAndRefreshIcons();
 }
 
+bool OmniboxResultView::ShouldShowTabMatchButtonInline() {
+  return !OmniboxFieldTrial::IsSuggestionButtonRowEnabled() &&
+         popup_contents_view_->model()->IsControlPresentOnMatch(
+             OmniboxPopupModel::Selection(
+                 model_index_, OmniboxPopupModel::FOCUSED_BUTTON_TAB_SWITCH));
+}
+
 void OmniboxResultView::UpdateRemoveSuggestionVisibility() {
   bool old_visibility = remove_suggestion_button_->GetVisible();
   bool new_visibility =
diff --git a/chrome/browser/ui/views/omnibox/omnibox_result_view.h b/chrome/browser/ui/views/omnibox/omnibox_result_view.h
index e09c1ba..5458d56 100644
--- a/chrome/browser/ui/views/omnibox/omnibox_result_view.h
+++ b/chrome/browser/ui/views/omnibox/omnibox_result_view.h
@@ -115,6 +115,13 @@
   // controls that are only visible on row hover.
   void UpdateHoverState();
 
+  // This returns true if the match has a matching tab and will use a
+  // switch-to-tab button inline in Result View. It returns false, for
+  // example, when the switch button is not shown because a keyword match is
+  // taking precedence or when Suggestion Button Row is enabled, as the
+  // Switch-to-tab button will appear in the button row.
+  bool ShouldShowTabMatchButtonInline();
+
   // Sets the visibility of the |remove_suggestion_button_| based on the current
   // state.
   void UpdateRemoveSuggestionVisibility();
diff --git a/chrome/browser/ui/views/sharesheet_bubble_view.cc b/chrome/browser/ui/views/sharesheet_bubble_view.cc
index d8a1912..7905e7f2 100644
--- a/chrome/browser/ui/views/sharesheet_bubble_view.cc
+++ b/chrome/browser/ui/views/sharesheet_bubble_view.cc
@@ -10,8 +10,10 @@
 #include "base/strings/string16.h"
 #include "base/strings/utf_string_conversions.h"
 #include "chrome/browser/sharesheet/sharesheet_service_delegate.h"
-#include "chrome/browser/ui/views/chrome_layout_provider.h"
+#include "content/public/browser/web_contents.h"
 #include "third_party/skia/include/core/SkColor.h"
+#include "ui/gfx/color_palette.h"
+#include "ui/gfx/font_list.h"
 #include "ui/gfx/geometry/insets.h"
 #include "ui/gfx/geometry/size.h"
 #include "ui/gfx/image/image_skia.h"
@@ -23,16 +25,27 @@
 #include "ui/views/controls/label.h"
 #include "ui/views/controls/styled_label.h"
 #include "ui/views/layout/box_layout.h"
+#include "ui/views/layout/grid_layout.h"
 #include "ui/views/widget/widget.h"
 
 namespace {
 
-constexpr int kButtonSize = 64;
+// Sizes are in px.
+constexpr int kButtonWidth = 92;
+constexpr int kButtonHeight = 104;
+constexpr int kButtonLineHeight = 20;
+constexpr int kButtonPadding = 8;
+
 constexpr int kCornerRadius = 12;
-constexpr int kMaxTargetRowSize = 4;
+constexpr int kMaxTargetsPerRow = 4;
+constexpr int kBubbleWidth = 416;
 constexpr int kSpacing = 24;
+constexpr int kTitleLineHeight = 24;
 constexpr char kTitle[] = "Share";
 
+constexpr SkColor kShareTitleColor = gfx::kGoogleGrey900;
+constexpr SkColor kShareTargetTitleColor = gfx::kGoogleGrey700;
+
 enum { COLUMN_SET_ID_TITLE, COLUMN_SET_ID_TARGETS };
 
 }  // namespace
@@ -46,10 +59,13 @@
                          const base::string16& display_name,
                          const gfx::ImageSkia* icon)
       : Button(listener) {
-    SetLayoutManager(std::make_unique<views::BoxLayout>(
-        views::BoxLayout::Orientation::kVertical, gfx::Insets(),
-        ChromeLayoutProvider::Get()->GetDistanceMetric(
-            views::DISTANCE_RELATED_CONTROL_VERTICAL)));
+    auto* layout = SetLayoutManager(std::make_unique<views::BoxLayout>(
+        views::BoxLayout::Orientation::kVertical, gfx::Insets(kButtonPadding),
+        kButtonPadding, true));
+    layout->set_main_axis_alignment(
+        views::BoxLayout::MainAxisAlignment::kStart);
+    layout->set_cross_axis_alignment(
+        views::BoxLayout::CrossAxisAlignment::kCenter);
 
     auto* image = AddChildView(std::make_unique<views::ImageView>());
     image->set_can_process_events_within_subtree(false);
@@ -59,9 +75,13 @@
     }
 
     auto* label = AddChildView(std::make_unique<views::Label>(display_name));
+    label->SetFontList(gfx::FontList("Roboto, Medium, 14px"));
+    label->SetLineHeight(kButtonLineHeight);
     label->SetBackgroundColor(SK_ColorTRANSPARENT);
-    label->SetHandlesTooltips(false);
-    label->SetMultiLine(true);
+    label->SetEnabledColor(kShareTargetTitleColor);
+    label->SetHandlesTooltips(true);
+    label->SetTooltipText(display_name);
+    label->SetMultiLine(false);
     label->SetAutoColorReadabilityEnabled(false);
     label->SetHorizontalAlignment(gfx::ALIGN_CENTER);
 
@@ -71,8 +91,9 @@
   ShareSheetTargetButton(const ShareSheetTargetButton&) = delete;
   ShareSheetTargetButton& operator=(const ShareSheetTargetButton&) = delete;
 
+  // Button is 76px width x 88px height + 8px padding along all sides.
   gfx::Size CalculatePreferredSize() const override {
-    return gfx::Size(kButtonSize, kButtonSize);
+    return gfx::Size(kButtonWidth, kButtonHeight);
   }
 };
 
@@ -80,30 +101,18 @@
     views::View* anchor_view,
     sharesheet::SharesheetServiceDelegate* delegate)
     : delegate_(delegate) {
-  SetButtons(ui::DIALOG_BUTTON_NONE);
-
   SetAnchorView(anchor_view);
+  CreateBubble();
+}
 
-  SetLayoutManager(std::make_unique<views::BoxLayout>(
-      views::BoxLayout::Orientation::kVertical, gfx::Insets(),
-      ChromeLayoutProvider::Get()->GetDistanceMetric(
-          views::DISTANCE_RELATED_CONTROL_VERTICAL)));
-
-  auto root_view = std::make_unique<views::View>();
-  root_view->SetLayoutManager(std::make_unique<views::BoxLayout>(
-      views::BoxLayout::Orientation::kVertical, gfx::Insets(),
-      ChromeLayoutProvider::Get()->GetDistanceMetric(
-          views::DISTANCE_RELATED_CONTROL_VERTICAL)));
-  root_view_ = AddChildView(std::move(root_view));
-
-  auto main_view = std::make_unique<views::View>();
-  main_view_ = root_view_->AddChildView(std::move(main_view));
-
-  auto share_action_view = std::make_unique<views::View>();
-  share_action_view->SetLayoutManager(std::make_unique<views::BoxLayout>(
-      views::BoxLayout::Orientation::kVertical, gfx::Insets(), 0, true));
-  share_action_view_ = root_view_->AddChildView(std::move(share_action_view));
-  share_action_view_->SetVisible(false);
+SharesheetBubbleView::SharesheetBubbleView(
+    content::WebContents* web_contents,
+    sharesheet::SharesheetServiceDelegate* delegate)
+    : delegate_(delegate) {
+  // TODO(crbug.com/1097623): Make the bubble located in the center of the
+  // invoke window.
+  set_parent_window(web_contents->GetNativeView());
+  CreateBubble();
 }
 
 SharesheetBubbleView::~SharesheetBubbleView() = default;
@@ -119,44 +128,33 @@
   // Set up columnsets
   views::ColumnSet* cs = main_layout->AddColumnSet(COLUMN_SET_ID_TITLE);
   cs->AddColumn(/* h_align */ views::GridLayout::FILL,
-                /* v_align */ views::GridLayout::CENTER,
-                /* resize_percent */ 1.0,
+                /* v_align */ views::GridLayout::LEADING,
+                /* resize_percent */ 0,
                 views::GridLayout::ColumnSize::kUsePreferred,
                 /* fixed_width */ 0, /*min_width*/ 0);
 
   views::ColumnSet* cs_buttons =
       main_layout->AddColumnSet(COLUMN_SET_ID_TARGETS);
-  cs_buttons->AddPaddingColumn(/* resize_percent */ 1, kSpacing);
-  cs_buttons->AddColumn(views::GridLayout::CENTER, views::GridLayout::CENTER,
-                        1.0, views::GridLayout::ColumnSize::kFixed, kButtonSize,
-                        0);
-  cs_buttons->AddPaddingColumn(/* resize_percent */ 1, kSpacing);
-  cs_buttons->AddColumn(views::GridLayout::CENTER, views::GridLayout::CENTER,
-                        1.0, views::GridLayout::ColumnSize::kFixed, kButtonSize,
-                        0);
-  cs_buttons->AddPaddingColumn(/* resize_percent */ 1, kSpacing);
-  cs_buttons->AddColumn(views::GridLayout::CENTER, views::GridLayout::CENTER,
-                        1.0, views::GridLayout::ColumnSize::kFixed, kButtonSize,
-                        0);
-  cs_buttons->AddPaddingColumn(/* resize_percent */ 1, kSpacing);
-  cs_buttons->AddColumn(views::GridLayout::CENTER, views::GridLayout::CENTER,
-                        1.0, views::GridLayout::ColumnSize::kFixed, kButtonSize,
-                        0);
-  cs_buttons->AddPaddingColumn(/* resize_percent */ 1, kSpacing);
+  for (int i = 0; i < kMaxTargetsPerRow; i++) {
+    cs_buttons->AddColumn(views::GridLayout::CENTER, views::GridLayout::CENTER,
+                          0, views::GridLayout::ColumnSize::kFixed,
+                          kButtonWidth, 0);
+  }
 
   // Add Title label
-  main_layout->AddPaddingRow(views::GridLayout::kFixedSize, kSpacing);
   main_layout->StartRow(views::GridLayout::kFixedSize, COLUMN_SET_ID_TITLE,
-                        /* height */ kSpacing);
+                        kTitleLineHeight);
   auto* title = main_layout->AddView(std::make_unique<views::Label>(
-      base::UTF8ToUTF16(base::StringPiece(kTitle)),
-      views::style::CONTEXT_DIALOG_TITLE, views::style::STYLE_PRIMARY));
-  title->SetHorizontalAlignment(gfx::ALIGN_CENTER);
+      base::UTF8ToUTF16(base::StringPiece(kTitle))));
+  title->SetFontList(gfx::FontList("GoogleSans, Medium, 24px"));
+  title->SetLineHeight(kTitleLineHeight);
+  title->SetEnabledColor(kShareTitleColor);
+  title->SetHorizontalAlignment(gfx::ALIGN_LEFT);
 
   // Add Targets
   size_t i = 0;
   for (const auto& target : targets_) {
-    if (i % kMaxTargetRowSize == 0) {
+    if (i % kMaxTargetsPerRow == 0) {
       main_layout->AddPaddingRow(views::GridLayout::kFixedSize, kSpacing);
       main_layout->StartRow(views::GridLayout::kFixedSize,
                             COLUMN_SET_ID_TARGETS);
@@ -212,9 +210,27 @@
 }
 
 gfx::Size SharesheetBubbleView::CalculatePreferredSize() const {
-  const int width = ChromeLayoutProvider::Get()->GetDistanceMetric(
-                        DISTANCE_BUBBLE_PREFERRED_WIDTH) -
-                    margins().width();
-  gfx::Size size = gfx::Size(width, GetHeightForWidth(width));
-  return size;
+  return gfx::Size(kBubbleWidth, GetHeightForWidth(kBubbleWidth));
+}
+
+void SharesheetBubbleView::CreateBubble() {
+  SetButtons(ui::DIALOG_BUTTON_NONE);
+
+  SetLayoutManager(std::make_unique<views::BoxLayout>(
+      views::BoxLayout::Orientation::kVertical));
+
+  auto root_view = std::make_unique<views::View>();
+  root_view->SetLayoutManager(std::make_unique<views::BoxLayout>(
+      views::BoxLayout::Orientation::kVertical, gfx::Insets(kSpacing), 0,
+      true));
+  root_view_ = AddChildView(std::move(root_view));
+
+  auto main_view = std::make_unique<views::View>();
+  main_view_ = root_view_->AddChildView(std::move(main_view));
+
+  auto share_action_view = std::make_unique<views::View>();
+  share_action_view->SetLayoutManager(std::make_unique<views::BoxLayout>(
+      views::BoxLayout::Orientation::kVertical, gfx::Insets(), 0, true));
+  share_action_view_ = root_view_->AddChildView(std::move(share_action_view));
+  share_action_view_->SetVisible(false);
 }
diff --git a/chrome/browser/ui/views/sharesheet_bubble_view.h b/chrome/browser/ui/views/sharesheet_bubble_view.h
index 53739be..a3f629f0 100644
--- a/chrome/browser/ui/views/sharesheet_bubble_view.h
+++ b/chrome/browser/ui/views/sharesheet_bubble_view.h
@@ -16,6 +16,10 @@
 class SharesheetServiceDelegate;
 }
 
+namespace content {
+class WebContents;
+}
+
 class SharesheetBubbleView : public views::BubbleDialogDelegateView,
                              public views::ButtonListener {
  public:
@@ -23,6 +27,8 @@
 
   SharesheetBubbleView(views::View* anchor_view,
                        sharesheet::SharesheetServiceDelegate* delegate);
+  SharesheetBubbleView(content::WebContents* web_contents,
+                       sharesheet::SharesheetServiceDelegate* delegate);
   SharesheetBubbleView(const SharesheetBubbleView&) = delete;
   SharesheetBubbleView& operator=(const SharesheetBubbleView&) = delete;
   ~SharesheetBubbleView() override;
@@ -44,6 +50,7 @@
   void OnWidgetDestroyed(views::Widget* widget) override;
 
  private:
+  void CreateBubble();
   // Owns this class.
   sharesheet::SharesheetServiceDelegate* delegate_;
   std::vector<TargetInfo> targets_;
diff --git a/chrome/browser/ui/views/tabs/tab_drag_controller_interactive_uitest.cc b/chrome/browser/ui/views/tabs/tab_drag_controller_interactive_uitest.cc
index 1cefa66..914a788 100644
--- a/chrome/browser/ui/views/tabs/tab_drag_controller_interactive_uitest.cc
+++ b/chrome/browser/ui/views/tabs/tab_drag_controller_interactive_uitest.cc
@@ -651,6 +651,14 @@
     observer.Wait();
   }
 
+  // Helper method to click the first tab. Used to ensure no additional widgets
+  // are in focus. For example, the tab editor bubble  is automatically opened
+  // upon creating a new group.
+  void EnsureFocusToTabStrip(TabStrip* tab_strip) {
+    ASSERT_TRUE(PressInput(GetCenterInScreenCoordinates(tab_strip->tab_at(0))));
+    ASSERT_TRUE(ReleaseInput());
+  }
+
   Browser* browser() const { return InProcessBrowserTest::browser(); }
 
  private:
@@ -841,6 +849,7 @@
   tab_groups::TabGroupId group1 = model->AddToNewGroup({0});
   model->AddToNewGroup({2});
   StopAnimating(tab_strip);
+  EnsureFocusToTabStrip(tab_strip);
 
   // Dragging the tab in the first index toward the tab in the zero-th index
   // switches the tabs and adds the dragged tab to the group.
@@ -899,6 +908,7 @@
   tab_groups::TabGroupId group1 = model->AddToNewGroup({3});
   model->AddToNewGroup({1});
   StopAnimating(tab_strip);
+  EnsureFocusToTabStrip(tab_strip);
 
   // Dragging the tab in the second index to the tab in the third index switches
   // the tabs and adds the dragged tab to the group.
@@ -951,6 +961,7 @@
   model->AddToNewGroup({2});
   tab_groups::TabGroupId group4 = model->AddToNewGroup({3});
   StopAnimating(tab_strip);
+  EnsureFocusToTabStrip(tab_strip);
 
   // Click the first tab and select third tab so both first and third tabs are
   // selected.
@@ -1005,6 +1016,7 @@
   tab_groups::TabGroupId group3 = model->AddToNewGroup({2});
   model->AddToNewGroup({3});
   StopAnimating(tab_strip);
+  EnsureFocusToTabStrip(tab_strip);
 
   // Click the second tab and select fourth tab so both second and fourth tabs
   // are selected.
@@ -1117,6 +1129,7 @@
   AddTabsAndResetBrowser(browser(), 3);
   tab_groups::TabGroupId group = model->AddToNewGroup({1, 2});
   StopAnimating(tab_strip);
+  EnsureFocusToTabStrip(tab_strip);
 
   ASSERT_EQ(4, model->count());
   ASSERT_EQ(2u, group_model->GetTabGroup(group)->ListTabs().size());
@@ -2130,6 +2143,7 @@
   AddTabsAndResetBrowser(browser(), 3);
   tab_groups::TabGroupId group = model->AddToNewGroup({0, 1});
   StopAnimating(tab_strip);
+  EnsureFocusToTabStrip(tab_strip);
 
   TabGroupHeader* group_header = tab_strip->group_header(group);
   EXPECT_FALSE(group_header->dragging());
@@ -2160,6 +2174,7 @@
   AddTabsAndResetBrowser(browser(), 3);
   tab_groups::TabGroupId group = model->AddToNewGroup({2, 3});
   StopAnimating(tab_strip);
+  EnsureFocusToTabStrip(tab_strip);
 
   TabGroupHeader* group_header = tab_strip->group_header(group);
   EXPECT_FALSE(group_header->dragging());
@@ -2217,14 +2232,13 @@
   AddTabsAndResetBrowser(browser(), 1);
   tab_groups::TabGroupId group = model->AddToNewGroup({0});
   StopAnimating(tab_strip);
+  EnsureFocusToTabStrip(tab_strip);
 
   DragGroupAndNotify(tab_strip,
                      base::BindOnce(&PressEscapeWhileDetachedHeaderStep2, this),
                      group);
 
-  TabGroupHeader* group_header =
-      GetTabStripForBrowser(browser())->group_header(group);
-  EXPECT_FALSE(group_header->dragging());
+  EXPECT_FALSE(tab_strip->group_header(group)->dragging());
 
   ASSERT_FALSE(tab_strip->GetDragContext()->IsDragSessionActive());
   ASSERT_FALSE(TabDragController::IsActive());
@@ -2330,6 +2344,8 @@
   AddTabsAndResetBrowser(browser(), 1);
   tab_groups::TabGroupId group = model->AddToNewGroup({0});
   EXPECT_FALSE(model->IsGroupCollapsed(group));
+  EnsureFocusToTabStrip(tab_strip);
+
   tab_strip->controller()->ToggleTabGroupCollapsedState(group);
   StopAnimating(tab_strip);
   EXPECT_TRUE(model->IsGroupCollapsed(group));
diff --git a/chrome/browser/ui/views/tabs/tab_group_editor_bubble_view_browsertest.cc b/chrome/browser/ui/views/tabs/tab_group_editor_bubble_view_browsertest.cc
index 17cd3c1..fac137c 100644
--- a/chrome/browser/ui/views/tabs/tab_group_editor_bubble_view_browsertest.cc
+++ b/chrome/browser/ui/views/tabs/tab_group_editor_bubble_view_browsertest.cc
@@ -29,18 +29,6 @@
     BrowserView* browser_view = static_cast<BrowserView*>(browser()->window());
     TabGroupHeader* header = browser_view->tabstrip()->group_header(group);
     ASSERT_NE(nullptr, header);
-    ASSERT_FALSE(header->editor_bubble_tracker_.is_open());
-
-    ui::MouseEvent pressed_event(ui::ET_MOUSE_PRESSED, gfx::PointF(),
-                                 gfx::PointF(), base::TimeTicks(), 0, 0);
-    header->OnMousePressed(pressed_event);
-
-    ASSERT_FALSE(header->editor_bubble_tracker_.is_open());
-
-    ui::MouseEvent released_event(ui::ET_MOUSE_RELEASED, gfx::PointF(),
-                                  gfx::PointF(), base::TimeTicks(), 0, 0);
-    header->OnMouseReleased(released_event);
-
     ASSERT_TRUE(header->editor_bubble_tracker_.is_open());
   }
 
diff --git a/chrome/browser/ui/views/tabs/tab_strip.cc b/chrome/browser/ui/views/tabs/tab_strip.cc
index 4a208dd..460b2537 100644
--- a/chrome/browser/ui/views/tabs/tab_strip.cc
+++ b/chrome/browser/ui/views/tabs/tab_strip.cc
@@ -1341,6 +1341,13 @@
   auto group_view = std::make_unique<TabGroupViews>(this, group);
   layout_helper_->InsertGroupHeader(group, group_view->header());
   group_views_[group] = std::move(group_view);
+
+  // The context menu relys on a Browser object which is not provided in
+  // TabStripTest.
+  if (this->controller()->GetBrowser()) {
+    group_views_[group]->header()->ShowContextMenuForViewImpl(
+        this, gfx::Point(), ui::MENU_SOURCE_NONE);
+  }
 }
 
 void TabStrip::OnGroupContentsChanged(const tab_groups::TabGroupId& group) {
diff --git a/chrome/browser/ui/webui/nearby_internals/nearby_internals_http_handler.cc b/chrome/browser/ui/webui/nearby_internals/nearby_internals_http_handler.cc
index 1dc65012..c2d6bd2 100644
--- a/chrome/browser/ui/webui/nearby_internals/nearby_internals_http_handler.cc
+++ b/chrome/browser/ui/webui/nearby_internals/nearby_internals_http_handler.cc
@@ -5,7 +5,17 @@
 #include "chrome/browser/ui/webui/nearby_internals/nearby_internals_http_handler.h"
 
 #include "base/bind.h"
+#include "base/json/json_writer.h"
+#include "base/time/time.h"
 #include "base/values.h"
+#include "chrome/browser/nearby_sharing/certificates/nearby_share_certificate_manager.h"
+#include "chrome/browser/nearby_sharing/client/nearby_share_http_notifier.h"
+#include "chrome/browser/nearby_sharing/contacts/nearby_share_contact_manager.h"
+#include "chrome/browser/nearby_sharing/local_device_data/nearby_share_local_device_data_manager.h"
+#include "chrome/browser/nearby_sharing/logging/logging.h"
+#include "chrome/browser/nearby_sharing/logging/proto_to_dictionary_conversion.h"
+#include "chrome/browser/nearby_sharing/nearby_sharing_service.h"
+#include "chrome/browser/nearby_sharing/nearby_sharing_service_factory.h"
 
 namespace {
 
@@ -24,9 +34,44 @@
   kResponse = 1
 };
 
+std::string FormatAsJSON(const base::Value& value) {
+  std::string json;
+  base::JSONWriter::WriteWithOptions(
+      value, base::JSONWriter::OPTIONS_PRETTY_PRINT, &json);
+  return json;
+}
+
+base::Value GetJavascriptTimestamp() {
+  return base::Value(base::Time::Now().ToJsTimeIgnoringNull());
+}
+
+// FireWebUIListener message to notify the JavaScript of HTTP message addition.
+const char kHttpMessageAdded[] = "http-message-added";
+
+// Keys in the JSON representation of a Http Message
+const char kHttpMessageBodyKey[] = "body";
+const char kHttpMessageTimeKey[] = "time";
+const char kHttpMessageRpcKey[] = "rpc";
+const char kHttpMessageDirectionKey[] = "direction";
+
+// Converts a RPC request/response to a raw dictionary value used as a
+// JSON argument to JavaScript functions.
+base::Value HttpMessageToDictionary(const base::Value& message,
+                                    Direction dir,
+                                    Rpc rpc) {
+  base::Value dictionary(base::Value::Type::DICTIONARY);
+  dictionary.SetStringKey(kHttpMessageBodyKey, FormatAsJSON(message));
+  dictionary.SetKey(kHttpMessageTimeKey, GetJavascriptTimestamp());
+  dictionary.SetIntKey(kHttpMessageRpcKey, static_cast<int>(rpc));
+  dictionary.SetIntKey(kHttpMessageDirectionKey, static_cast<int>(dir));
+  return dictionary;
+}
+
 }  // namespace
 
-NearbyInternalsHttpHandler::NearbyInternalsHttpHandler() = default;
+NearbyInternalsHttpHandler::NearbyInternalsHttpHandler(
+    content::BrowserContext* context)
+    : context_(context) {}
 
 NearbyInternalsHttpHandler::~NearbyInternalsHttpHandler() = default;
 
@@ -49,9 +94,19 @@
                           base::Unretained(this)));
 }
 
-void NearbyInternalsHttpHandler::OnJavascriptAllowed() {}
+void NearbyInternalsHttpHandler::OnJavascriptAllowed() {
+  NearbySharingService* service_ =
+      NearbySharingServiceFactory::GetForBrowserContext(context_);
+  if (service_) {
+    observer_.Add(service_->GetHttpNotifier());
+  } else {
+    NS_LOG(ERROR) << "No NearbyShareService instance to call.";
+  }
+}
 
-void NearbyInternalsHttpHandler::OnJavascriptDisallowed() {}
+void NearbyInternalsHttpHandler::OnJavascriptDisallowed() {
+  observer_.RemoveAll();
+}
 
 void NearbyInternalsHttpHandler::InitializeContents(
     const base::ListValue* args) {
@@ -59,19 +114,96 @@
 }
 
 void NearbyInternalsHttpHandler::UpdateDevice(const base::ListValue* args) {
-  // TODO(julietlevesque): Add functionality for Update Device call, which
-  // responds to javascript callback from chrome://nearby-internals HTTP
-  // Messages tab.
+  NearbySharingService* service_ =
+      NearbySharingServiceFactory::GetForBrowserContext(context_);
+  if (service_) {
+    service_->GetLocalDeviceDataManager()->DownloadDeviceData();
+  } else {
+    NS_LOG(ERROR) << "No NearbyShareService instance to call.";
+  }
 }
+
 void NearbyInternalsHttpHandler::ListPublicCertificates(
     const base::ListValue* args) {
-  // TODO(julietlevesque): Add functionality for List Public Certificates call,
-  // which responds to javascript callback from chrome://nearby-internals HTTP
-  // Messages tab.
+  NearbySharingService* service_ =
+      NearbySharingServiceFactory::GetForBrowserContext(context_);
+  if (service_) {
+    service_->GetCertificateManager()->DownloadPublicCertificates();
+  } else {
+    NS_LOG(ERROR) << "No NearbyShareService instance to call.";
+  }
 }
+
 void NearbyInternalsHttpHandler::ListContactPeople(
     const base::ListValue* args) {
-  // TODO(julietlevesque): Add functionality for List ContactPeople call, which
-  // responds to javascript callback from chrome://nearby-internals HTTP
-  // Messages tab.
+  NearbySharingService* service_ =
+      NearbySharingServiceFactory::GetForBrowserContext(context_);
+  if (service_) {
+    service_->GetContactManager()->DownloadContacts(
+        /*only_download_if_contacts_changed=*/false);
+  } else {
+    NS_LOG(ERROR) << "No NearbyShareService instance to call.";
+  }
+}
+
+void NearbyInternalsHttpHandler::OnUpdateDeviceRequest(
+    const nearbyshare::proto::UpdateDeviceRequest& request) {
+  FireWebUIListener(
+      kHttpMessageAdded,
+      HttpMessageToDictionary(UpdateDeviceRequestToReadableDictionary(request),
+                              Direction::kRequest, Rpc::kDevice));
+}
+
+void NearbyInternalsHttpHandler::OnUpdateDeviceResponse(
+    const nearbyshare::proto::UpdateDeviceResponse& response) {
+  FireWebUIListener(kHttpMessageAdded,
+                    HttpMessageToDictionary(
+                        UpdateDeviceResponseToReadableDictionary(response),
+                        Direction::kResponse, Rpc::kDevice));
+}
+
+void NearbyInternalsHttpHandler::OnListContactPeopleRequest(
+    const nearbyshare::proto::ListContactPeopleRequest& request) {
+  FireWebUIListener(kHttpMessageAdded,
+                    HttpMessageToDictionary(
+                        ListContactPeopleRequestToReadableDictionary(request),
+                        Direction::kRequest, Rpc::kContact));
+}
+
+void NearbyInternalsHttpHandler::OnListContactPeopleResponse(
+    const nearbyshare::proto::ListContactPeopleResponse& response) {
+  FireWebUIListener(kHttpMessageAdded,
+                    HttpMessageToDictionary(
+                        ListContactPeopleResponseToReadableDictionary(response),
+                        Direction::kResponse, Rpc::kContact));
+}
+
+void NearbyInternalsHttpHandler::OnListPublicCertificatesRequest(
+    const nearbyshare::proto::ListPublicCertificatesRequest& request) {
+  FireWebUIListener(
+      kHttpMessageAdded,
+      HttpMessageToDictionary(
+          ListPublicCertificatesRequestToReadableDictionary(request),
+          Direction::kRequest, Rpc::kCertificate));
+}
+
+void NearbyInternalsHttpHandler::OnListPublicCertificatesResponse(
+    const nearbyshare::proto::ListPublicCertificatesResponse& response) {
+  FireWebUIListener(
+      kHttpMessageAdded,
+      HttpMessageToDictionary(
+          ListPublicCertificatesResponseToReadableDictionary(response),
+          Direction::kResponse, Rpc::kCertificate));
+}
+
+void NearbyInternalsHttpHandler::OnCheckContactsReachabilityRequest(
+    const nearbyshare::proto::CheckContactsReachabilityRequest& request) {
+  // No FireWebUIListener() because internal debug page does not support
+  // CheckContactsReachabilityRequest.
+}
+
+void NearbyInternalsHttpHandler::OnCheckContactsReachabilityResponse(
+    const nearbyshare::proto::CheckContactsReachabilityResponse& response) {
+  // No FireWebUIListener() because internal debug page does not support
+  // CheckContactsReachabilityResponse.
 }
diff --git a/chrome/browser/ui/webui/nearby_internals/nearby_internals_http_handler.h b/chrome/browser/ui/webui/nearby_internals/nearby_internals_http_handler.h
index a791f9e3..d892a937 100644
--- a/chrome/browser/ui/webui/nearby_internals/nearby_internals_http_handler.h
+++ b/chrome/browser/ui/webui/nearby_internals/nearby_internals_http_handler.h
@@ -6,27 +6,59 @@
 #define CHROME_BROWSER_UI_WEBUI_NEARBY_INTERNALS_NEARBY_INTERNALS_HTTP_HANDLER_H_
 
 #include "base/memory/weak_ptr.h"
+#include "base/scoped_observer.h"
+#include "chrome/browser/nearby_sharing/client/nearby_share_http_notifier.h"
+#include "chrome/browser/nearby_sharing/proto/certificate_rpc.pb.h"
+#include "chrome/browser/nearby_sharing/proto/contact_rpc.pb.h"
+#include "chrome/browser/nearby_sharing/proto/device_rpc.pb.h"
 #include "content/public/browser/web_ui_message_handler.h"
 
 namespace base {
 class ListValue;
 }  // namespace base
 
+namespace content {
+class BrowserContext;
+}  // namespace content
+
 // WebUIMessageHandler for HTTP Messages to pass messages to the
 // chrome://nearby-internals HTTP tab.
-class NearbyInternalsHttpHandler : public content::WebUIMessageHandler {
+class NearbyInternalsHttpHandler : public content::WebUIMessageHandler,
+                                   public NearbyShareHttpNotifier::Observer {
  public:
-  NearbyInternalsHttpHandler();
+  explicit NearbyInternalsHttpHandler(content::BrowserContext* context);
   NearbyInternalsHttpHandler(const NearbyInternalsHttpHandler&) = delete;
   NearbyInternalsHttpHandler& operator=(const NearbyInternalsHttpHandler&) =
       delete;
   ~NearbyInternalsHttpHandler() override;
 
-  // content::WebUIMessageHandler
+  // content::WebUIMessageHandler:
   void RegisterMessages() override;
   void OnJavascriptAllowed() override;
   void OnJavascriptDisallowed() override;
 
+  // NearbyShareHttpNotifier::Observer:
+  void OnUpdateDeviceRequest(
+      const nearbyshare::proto::UpdateDeviceRequest& request) override;
+  void OnUpdateDeviceResponse(
+      const nearbyshare::proto::UpdateDeviceResponse& response) override;
+  void OnListContactPeopleRequest(
+      const nearbyshare::proto::ListContactPeopleRequest& request) override;
+  void OnListContactPeopleResponse(
+      const nearbyshare::proto::ListContactPeopleResponse& response) override;
+  void OnListPublicCertificatesRequest(
+      const nearbyshare::proto::ListPublicCertificatesRequest& request)
+      override;
+  void OnListPublicCertificatesResponse(
+      const nearbyshare::proto::ListPublicCertificatesResponse& response)
+      override;
+  void OnCheckContactsReachabilityRequest(
+      const nearbyshare::proto::CheckContactsReachabilityRequest& request)
+      override;
+  void OnCheckContactsReachabilityResponse(
+      const nearbyshare::proto::CheckContactsReachabilityResponse& response)
+      override;
+
  private:
   // Message handler callback that initializes JavaScript.
   void InitializeContents(const base::ListValue* args);
@@ -40,6 +72,9 @@
   // Message handler callback that calls List Contacts RPC.
   void ListContactPeople(const base::ListValue* args);
 
+  content::BrowserContext* const context_;
+  ScopedObserver<NearbyShareHttpNotifier, NearbyShareHttpNotifier::Observer>
+      observer_{this};
   base::WeakPtrFactory<NearbyInternalsHttpHandler> weak_ptr_factory_{this};
 };
 
diff --git a/chrome/browser/ui/webui/nearby_internals/nearby_internals_ui.cc b/chrome/browser/ui/webui/nearby_internals/nearby_internals_ui.cc
index af4a9b4..9b36cd5 100644
--- a/chrome/browser/ui/webui/nearby_internals/nearby_internals_ui.cc
+++ b/chrome/browser/ui/webui/nearby_internals/nearby_internals_ui.cc
@@ -48,9 +48,10 @@
       web_ui->GetWebContents()->GetBrowserContext();
 
   web_ui->AddMessageHandler(std::make_unique<NearbyInternalsLogsHandler>());
-  web_ui->AddMessageHandler(std::make_unique<NearbyInternalsHttpHandler>());
   web_ui->AddMessageHandler(
       std::make_unique<NearbyInternalsContactHandler>(context));
+  web_ui->AddMessageHandler(
+      std::make_unique<NearbyInternalsHttpHandler>(context));
 }
 
 NearbyInternalsUI::~NearbyInternalsUI() = default;
diff --git a/chrome/browser/ui/webui/new_tab_page/new_tab_page_ui.cc b/chrome/browser/ui/webui/new_tab_page/new_tab_page_ui.cc
index bd7d211..bd3ef6b9 100644
--- a/chrome/browser/ui/webui/new_tab_page/new_tab_page_ui.cc
+++ b/chrome/browser/ui/webui/new_tab_page/new_tab_page_ui.cc
@@ -16,6 +16,7 @@
 #include "chrome/browser/ui/search/omnibox_mojo_utils.h"
 #include "chrome/browser/ui/webui/favicon_source.h"
 #include "chrome/browser/ui/webui/new_tab_page/new_tab_page_handler.h"
+#include "chrome/browser/ui/webui/new_tab_page/promo_browser_command/promo_browser_command_handler.h"
 #include "chrome/browser/ui/webui/new_tab_page/untrusted_source.h"
 #include "chrome/browser/ui/webui/theme_source.h"
 #include "chrome/browser/ui/webui/webui_util.h"
@@ -199,6 +200,8 @@
                           IDR_NEW_TAB_PAGE_MOJO_LITE_JS);
   source->AddResourcePath("omnibox.mojom-lite.js",
                           IDR_NEW_TAB_PAGE_OMNIBOX_MOJO_LITE_JS);
+  source->AddResourcePath("promo_browser_command.mojom-lite.js",
+                          IDR_NEW_TAB_PAGE_PROMO_BROWSER_COMMAND_MOJO_LITE_JS);
 #if BUILDFLAG(OPTIMIZE_WEBUI)
   source->AddResourcePath("new_tab_page.js", IDR_NEW_TAB_PAGE_NEW_TAB_PAGE_JS);
 #endif  // BUILDFLAG(OPTIMIZE_WEBUI)
@@ -280,6 +283,13 @@
   page_factory_receiver_.Bind(std::move(pending_receiver));
 }
 
+void NewTabPageUI::BindInterface(
+    mojo::PendingReceiver<promo_browser_command::mojom::CommandHandler>
+        pending_page_handler) {
+  promo_browser_command_handler_ = std::make_unique<PromoBrowserCommandHandler>(
+      std::move(pending_page_handler), profile_);
+}
+
 void NewTabPageUI::CreatePageHandler(
     mojo::PendingRemote<new_tab_page::mojom::Page> pending_page,
     mojo::PendingReceiver<new_tab_page::mojom::PageHandler>
diff --git a/chrome/browser/ui/webui/new_tab_page/new_tab_page_ui.h b/chrome/browser/ui/webui/new_tab_page/new_tab_page_ui.h
index 5cbe503..e37d8c27 100644
--- a/chrome/browser/ui/webui/new_tab_page/new_tab_page_ui.h
+++ b/chrome/browser/ui/webui/new_tab_page/new_tab_page_ui.h
@@ -6,6 +6,7 @@
 #define CHROME_BROWSER_UI_WEBUI_NEW_TAB_PAGE_NEW_TAB_PAGE_UI_H_
 
 #include "base/macros.h"
+#include "chrome/browser/promo_browser_command/promo_browser_command.mojom-forward.h"
 #include "chrome/browser/search/instant_service_observer.h"
 #include "chrome/browser/ui/webui/new_tab_page/new_tab_page.mojom.h"
 #include "content/public/browser/web_contents_observer.h"
@@ -14,6 +15,7 @@
 #include "mojo/public/cpp/bindings/receiver.h"
 #include "ui/webui/mojo_web_ui_controller.h"
 
+class PromoBrowserCommandHandler;
 namespace content {
 class NavigationHandle;
 class WebContents;
@@ -40,6 +42,13 @@
       mojo::PendingReceiver<new_tab_page::mojom::PageHandlerFactory>
           pending_receiver);
 
+  // Instantiates the implementor of the
+  // promo_browser_command::mojom::CommandHandler mojo interface passing the
+  // pending receiver that will be internally bound.
+  void BindInterface(
+      mojo::PendingReceiver<promo_browser_command::mojom::CommandHandler>
+          pending_receiver);
+
  private:
   // new_tab_page::mojom::PageHandlerFactory:
   void CreatePageHandler(
@@ -63,6 +72,7 @@
   std::unique_ptr<NewTabPageHandler> page_handler_;
   mojo::Receiver<new_tab_page::mojom::PageHandlerFactory>
       page_factory_receiver_;
+  std::unique_ptr<PromoBrowserCommandHandler> promo_browser_command_handler_;
   Profile* profile_;
   InstantService* instant_service_;
   content::WebContents* web_contents_;
diff --git a/chrome/browser/web_applications/components/web_app_run_on_os_login.cc b/chrome/browser/web_applications/components/web_app_run_on_os_login.cc
index b18f20c7..f50b0fb 100644
--- a/chrome/browser/web_applications/components/web_app_run_on_os_login.cc
+++ b/chrome/browser/web_applications/components/web_app_run_on_os_login.cc
@@ -27,7 +27,6 @@
       FROM_HERE,
       base::BindOnce(std::move(callback), run_on_os_login_registered));
 }
-
 }  // namespace
 
 namespace internals {
@@ -41,8 +40,10 @@
 
 // TODO(crbug.com/897302): This boilerplate function is used for platforms
 // other than Windows, currently the feature is only supported in Windows.
-void UnregisterRunOnOsLogin(const base::FilePath& profile_path,
-                            const base::string16& shortcut_title) {}
+bool UnregisterRunOnOsLogin(const base::FilePath& profile_path,
+                            const base::string16& shortcut_title) {
+  return true;
+}
 #endif
 
 }  // namespace internals
@@ -56,4 +57,16 @@
       std::move(shortcut_info));
 }
 
+void ScheduleUnregisterRunOnOsLogin(const base::FilePath& profile_path,
+                                    const base::string16& shortcut_title,
+                                    UnregisterRunOnOsLoginCallback callback) {
+  DCHECK_CURRENTLY_ON(BrowserThread::UI);
+
+  internals::GetShortcutIOTaskRunner()->PostTaskAndReplyWithResult(
+      FROM_HERE,
+      base::BindOnce(&internals::UnregisterRunOnOsLogin, profile_path,
+                     shortcut_title),
+      std::move(callback));
+}
+
 }  // namespace web_app
diff --git a/chrome/browser/web_applications/components/web_app_run_on_os_login.h b/chrome/browser/web_applications/components/web_app_run_on_os_login.h
index e91425a..53f5221 100644
--- a/chrome/browser/web_applications/components/web_app_run_on_os_login.h
+++ b/chrome/browser/web_applications/components/web_app_run_on_os_login.h
@@ -25,6 +25,10 @@
 // registered.
 using RegisterRunOnOsLoginCallback = base::OnceCallback<void(bool success)>;
 
+// Callback made when UnregisterRunOnOslogin has finished indicating whether or
+// not it was successfully unregistered
+using UnregisterRunOnOsLoginCallback = base::OnceCallback<void(bool success)>;
+
 namespace internals {
 
 // Registers the app with the OS to run on OS login. Platform specific
@@ -35,7 +39,7 @@
 // Unregisters the app with the OS from running on startup. Platform specific
 // implementations are required for this.
 // See web_app_shortcut_win.cc for Windows.
-void UnregisterRunOnOsLogin(const base::FilePath& profile_path,
+bool UnregisterRunOnOsLogin(const base::FilePath& profile_path,
                             const base::string16& shortcut_title);
 
 }  // namespace internals
@@ -46,6 +50,9 @@
 void ScheduleRegisterRunOnOsLogin(std::unique_ptr<ShortcutInfo> shortcut_info,
                                   RegisterRunOnOsLoginCallback callback);
 
+void ScheduleUnregisterRunOnOsLogin(const base::FilePath& profile_path,
+                                    const base::string16& shortcut_title,
+                                    UnregisterRunOnOsLoginCallback callback);
 }  // namespace web_app
 
 #endif  // CHROME_BROWSER_WEB_APPLICATIONS_COMPONENTS_WEB_APP_RUN_ON_OS_LOGIN_H_
diff --git a/chrome/browser/web_applications/components/web_app_run_on_os_login_win.cc b/chrome/browser/web_applications/components/web_app_run_on_os_login_win.cc
index 71cbfe4..69a52c7 100644
--- a/chrome/browser/web_applications/components/web_app_run_on_os_login_win.cc
+++ b/chrome/browser/web_applications/components/web_app_run_on_os_login_win.cc
@@ -24,22 +24,24 @@
                                  SHORTCUT_CREATION_BY_USER, shortcut_info);
 }
 
-void UnregisterRunOnOsLogin(const base::FilePath& profile_path,
+bool UnregisterRunOnOsLogin(const base::FilePath& profile_path,
                             const base::string16& shortcut_title) {
   web_app::ShortcutLocations all_shortcut_locations;
   all_shortcut_locations.in_startup = true;
   std::vector<base::FilePath> all_paths =
       GetShortcutPaths(all_shortcut_locations);
-
+  bool result = true;
   // Only Startup folder is the expected path to be returned in all_paths.
   for (const auto& path : all_paths) {
     // Find all app's shortcuts in Startup folder to delete.
     std::vector<base::FilePath> shortcut_files =
         FindAppShortcutsByProfileAndTitle(path, profile_path, shortcut_title);
     for (const auto& shortcut_file : shortcut_files) {
-      base::DeleteFile(shortcut_file);
+      if (!base::DeleteFile(shortcut_file))
+        result = false;
     }
   }
+  return result;
 }
 
 }  // namespace internals
diff --git a/chrome/browser/web_applications/components/web_app_shortcut.cc b/chrome/browser/web_applications/components/web_app_shortcut.cc
index 6befe144..bf53d71 100644
--- a/chrome/browser/web_applications/components/web_app_shortcut.cc
+++ b/chrome/browser/web_applications/components/web_app_shortcut.cc
@@ -71,6 +71,16 @@
       FROM_HERE, base::BindOnce(std::move(callback), shortcut_created));
 }
 
+void DeletePlatformShortcutsAndPostCallback(
+    const base::FilePath& shortcut_data_path,
+    CreateShortcutsCallback callback,
+    const ShortcutInfo& shortcut_info) {
+  bool shortcut_deleted =
+      internals::DeletePlatformShortcuts(shortcut_data_path, shortcut_info);
+  content::GetUIThreadTaskRunner({})->PostTask(
+      FROM_HERE, base::BindOnce(std::move(callback), shortcut_deleted));
+}
+
 }  // namespace
 
 ShortcutInfo::ShortcutInfo() = default;
@@ -166,6 +176,17 @@
                      std::move(shortcut_info));
 }
 
+void ScheduleDeletePlatformShortcuts(
+    const base::FilePath& shortcut_data_path,
+    std::unique_ptr<ShortcutInfo> shortcut_info,
+    DeleteShortcutsCallback callback) {
+  DCHECK_CURRENTLY_ON(BrowserThread::UI);
+
+  PostShortcutIOTask(base::BindOnce(&DeletePlatformShortcutsAndPostCallback,
+                                    shortcut_data_path, std::move(callback)),
+                     std::move(shortcut_info));
+}
+
 void PostShortcutIOTaskAndReply(
     base::OnceCallback<void(const ShortcutInfo&)> task,
     std::unique_ptr<ShortcutInfo> shortcut_info,
diff --git a/chrome/browser/web_applications/components/web_app_shortcut.h b/chrome/browser/web_applications/components/web_app_shortcut.h
index 74693de..5235fe6 100644
--- a/chrome/browser/web_applications/components/web_app_shortcut.h
+++ b/chrome/browser/web_applications/components/web_app_shortcut.h
@@ -123,6 +123,10 @@
 // platform shortcuts indicating whether or not they were successfully
 // created.
 using CreateShortcutsCallback = base::OnceCallback<void(bool shortcut_created)>;
+// Callback made when DeletePlatformShortcuts has finished trying to delete the
+// platform shortcuts indicating whether or not they were successfully
+// deleted.
+using DeleteShortcutsCallback = base::OnceCallback<void(bool shortcut_deleted)>;
 
 // Returns an array of desired icon sizes (in px) to be contained in an app OS
 // shortcut, sorted in ascending order (biggest desired icon size is last).
@@ -155,10 +159,15 @@
     std::unique_ptr<ShortcutInfo> shortcut_info,
     CreateShortcutsCallback callback);
 
+void ScheduleDeletePlatformShortcuts(
+    const base::FilePath& shortcut_data_path,
+    std::unique_ptr<ShortcutInfo> shortcut_info,
+    DeleteShortcutsCallback callback);
+
 // Delete all the shortcuts we have added for this extension. This is the
 // platform specific implementation of the DeleteAllShortcuts function, and
 // is executed on the FILE thread.
-void DeletePlatformShortcuts(const base::FilePath& shortcut_data_path,
+bool DeletePlatformShortcuts(const base::FilePath& shortcut_data_path,
                              const ShortcutInfo& shortcut_info);
 
 // Delete the multi-profile (non-profile_scoped) shortcuts for the specified
diff --git a/chrome/browser/web_applications/components/web_app_shortcut_chromeos.cc b/chrome/browser/web_applications/components/web_app_shortcut_chromeos.cc
index 620b12a..e79a248 100644
--- a/chrome/browser/web_applications/components/web_app_shortcut_chromeos.cc
+++ b/chrome/browser/web_applications/components/web_app_shortcut_chromeos.cc
@@ -15,8 +15,10 @@
   return true;
 }
 
-void DeletePlatformShortcuts(const base::FilePath& web_app_path,
-                             const ShortcutInfo& shortcut_info) {}
+bool DeletePlatformShortcuts(const base::FilePath& web_app_path,
+                             const ShortcutInfo& shortcut_info) {
+  return true;
+}
 
 void UpdatePlatformShortcuts(const base::FilePath& web_app_path,
                              const base::string16& old_app_title,
diff --git a/chrome/browser/web_applications/components/web_app_shortcut_linux.cc b/chrome/browser/web_applications/components/web_app_shortcut_linux.cc
index c12b00f..9b3ea69 100644
--- a/chrome/browser/web_applications/components/web_app_shortcut_linux.cc
+++ b/chrome/browser/web_applications/components/web_app_shortcut_linux.cc
@@ -281,13 +281,15 @@
   return base::FilePath(filename.append(".desktop"));
 }
 
-void DeleteShortcutOnDesktop(const base::FilePath& shortcut_filename) {
+bool DeleteShortcutOnDesktop(const base::FilePath& shortcut_filename) {
   base::FilePath desktop_path;
+  bool result = false;
   if (base::PathService::Get(base::DIR_USER_DESKTOP, &desktop_path))
-    base::DeleteFile(desktop_path.Append(shortcut_filename));
+    result = base::DeleteFile(desktop_path.Append(shortcut_filename));
+  return result;
 }
 
-void DeleteShortcutInApplicationsMenu(
+bool DeleteShortcutInApplicationsMenu(
     const base::FilePath& shortcut_filename,
     const base::FilePath& directory_filename) {
   std::vector<std::string> argv;
@@ -306,7 +308,7 @@
     argv.push_back(directory_filename.value());
   argv.push_back(shortcut_filename.value());
   int exit_code;
-  shell_integration_linux::LaunchXdgUtility(argv, &exit_code);
+  return shell_integration_linux::LaunchXdgUtility(argv, &exit_code);
 }
 
 bool CreateDesktopShortcut(const ShortcutInfo& shortcut_info,
@@ -447,7 +449,7 @@
   return locations;
 }
 
-void DeleteDesktopShortcuts(const base::FilePath& profile_path,
+bool DeleteDesktopShortcuts(const base::FilePath& profile_path,
                             const std::string& extension_id) {
   base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
                                                 base::BlockingType::MAY_BLOCK);
@@ -456,21 +458,22 @@
       GetAppShortcutFilename(profile_path, extension_id);
   DCHECK(!shortcut_filename.empty());
 
-  DeleteShortcutOnDesktop(shortcut_filename);
+  bool deleted_from_desktop = DeleteShortcutOnDesktop(shortcut_filename);
   // Delete shortcuts from |kDirectoryFilename|.
   // Note that it is possible that shortcuts were not created in the Chrome Apps
   // directory. It doesn't matter: this will still delete the shortcut even if
   // it isn't in the directory.
-  DeleteShortcutInApplicationsMenu(shortcut_filename,
-                                   base::FilePath(kDirectoryFilename));
+  bool deleted_from_application_menu = DeleteShortcutInApplicationsMenu(
+      shortcut_filename, base::FilePath(kDirectoryFilename));
+  return (deleted_from_desktop && deleted_from_application_menu);
 }
 
-void DeleteAllDesktopShortcuts(const base::FilePath& profile_path) {
+bool DeleteAllDesktopShortcuts(const base::FilePath& profile_path) {
   base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
                                                 base::BlockingType::MAY_BLOCK);
 
   std::unique_ptr<base::Environment> env(base::Environment::Create());
-
+  bool result = true;
   // Delete shortcuts from Desktop.
   base::FilePath desktop_path;
   if (base::PathService::Get(base::DIR_USER_DESKTOP, &desktop_path)) {
@@ -478,7 +481,8 @@
         shell_integration_linux::GetExistingProfileShortcutFilenames(
             profile_path, desktop_path);
     for (const auto& shortcut : shortcut_filenames_desktop) {
-      DeleteShortcutOnDesktop(shortcut);
+      if (!DeleteShortcutOnDesktop(shortcut))
+        result = false;
     }
   }
 
@@ -490,8 +494,12 @@
       shell_integration_linux::GetExistingProfileShortcutFilenames(
           profile_path, applications_menu);
   for (const auto& menu : shortcut_filenames_app_menu) {
-    DeleteShortcutInApplicationsMenu(menu, base::FilePath(kDirectoryFilename));
+    if (!DeleteShortcutInApplicationsMenu(menu,
+                                          base::FilePath(kDirectoryFilename))) {
+      result = false;
+    }
   }
+  return result;
 }
 
 namespace internals {
@@ -509,12 +517,13 @@
 #endif
 }
 
-void DeletePlatformShortcuts(const base::FilePath& web_app_path,
+bool DeletePlatformShortcuts(const base::FilePath& web_app_path,
                              const ShortcutInfo& shortcut_info) {
 #if !defined(OS_CHROMEOS)
-  DeleteDesktopShortcuts(shortcut_info.profile_path,
-                         shortcut_info.extension_id);
+  return DeleteDesktopShortcuts(shortcut_info.profile_path,
+                                shortcut_info.extension_id);
 #endif
+  return true;
 }
 
 void UpdatePlatformShortcuts(const base::FilePath& web_app_path,
diff --git a/chrome/browser/web_applications/components/web_app_shortcut_linux.h b/chrome/browser/web_applications/components/web_app_shortcut_linux.h
index a9fa20a3..c6e5844 100644
--- a/chrome/browser/web_applications/components/web_app_shortcut_linux.h
+++ b/chrome/browser/web_applications/components/web_app_shortcut_linux.h
@@ -49,13 +49,14 @@
     const base::FilePath& desktop_path);
 
 // Delete any desktop shortcuts on desktop or in the application menu that have
-// been added for the extension with |extension_id| in |profile_path|.
-void DeleteDesktopShortcuts(const base::FilePath& profile_path,
+// been added for the extension with |extension_id| in |profile_path|. Returns
+// true on successful deletion.
+bool DeleteDesktopShortcuts(const base::FilePath& profile_path,
                             const std::string& extension_id);
 
 // Delete any desktop shortcuts on desktop or in the application menu that have
-// for the profile in |profile_path|.
-void DeleteAllDesktopShortcuts(const base::FilePath& profile_path);
+// for the profile in |profile_path|. Returns true on successful deletion.
+bool DeleteAllDesktopShortcuts(const base::FilePath& profile_path);
 
 }  // namespace web_app
 
diff --git a/chrome/browser/web_applications/components/web_app_shortcut_mac.mm b/chrome/browser/web_applications/components/web_app_shortcut_mac.mm
index 696bcd7..8bba0ceb 100644
--- a/chrome/browser/web_applications/components/web_app_shortcut_mac.mm
+++ b/chrome/browser/web_applications/components/web_app_shortcut_mac.mm
@@ -1328,15 +1328,19 @@
   return shortcut_creator.CreateShortcuts(creation_reason, creation_locations);
 }
 
-void DeletePlatformShortcuts(const base::FilePath& app_data_path,
+bool DeletePlatformShortcuts(const base::FilePath& app_data_path,
                              const ShortcutInfo& shortcut_info) {
   base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
                                                 base::BlockingType::MAY_BLOCK);
   const std::string bundle_id = GetBundleIdentifier(shortcut_info.extension_id,
                                                     shortcut_info.profile_path);
   auto bundle_infos = SearchForBundlesById(bundle_id);
-  for (const auto& bundle_info : bundle_infos)
-    base::DeletePathRecursively(bundle_info.bundle_path());
+  bool result = true;
+  for (const auto& bundle_info : bundle_infos) {
+    if (!base::DeletePathRecursively(bundle_info.bundle_path()))
+      result = false;
+  }
+  return result;
 }
 
 void DeleteMultiProfileShortcutsForApp(const std::string& app_id) {
diff --git a/chrome/browser/web_applications/components/web_app_shortcut_win.cc b/chrome/browser/web_applications/components/web_app_shortcut_win.cc
index 08df698..bb6b775c 100644
--- a/chrome/browser/web_applications/components/web_app_shortcut_win.cc
+++ b/chrome/browser/web_applications/components/web_app_shortcut_win.cc
@@ -229,12 +229,14 @@
 // for this app were found (and deleted). This will delete duplicate shortcuts,
 // but only return each path once, even if it contained multiple deleted
 // shortcuts. Both of these may be NULL.
-void GetShortcutLocationsAndDeleteShortcuts(
+bool GetShortcutLocationsAndDeleteShortcuts(
     const base::FilePath& web_app_path,
     const base::FilePath& profile_path,
     const base::string16& title,
     bool* was_pinned_to_taskbar,
     std::vector<base::FilePath>* shortcut_paths) {
+  bool result = true;
+
   // Get all possible locations for shortcuts.
   web_app::ShortcutLocations all_shortcut_locations;
   all_shortcut_locations.in_quick_launch_bar = true;
@@ -275,9 +277,11 @@
       // Any shortcut could have been pinned, either by chrome or the user, so
       // they are all unpinned.
       base::win::UnpinShortcutFromTaskbar(*j);
-      base::DeleteFile(*j);
+      if (base::DeleteFile(*j))
+        result = false;
     }
   }
+  return result;
 }
 
 void CreateIconAndSetRelaunchDetails(
@@ -472,11 +476,11 @@
   CheckAndSaveIcon(icon_file, shortcut_info.favicon, true);
 }
 
-void DeletePlatformShortcuts(const base::FilePath& web_app_path,
+bool DeletePlatformShortcuts(const base::FilePath& web_app_path,
                              const ShortcutInfo& shortcut_info) {
-  GetShortcutLocationsAndDeleteShortcuts(web_app_path,
-                                         shortcut_info.profile_path,
-                                         shortcut_info.title, NULL, NULL);
+  bool result = GetShortcutLocationsAndDeleteShortcuts(
+      web_app_path, shortcut_info.profile_path, shortcut_info.title, nullptr,
+      nullptr);
 
   // If there are no more shortcuts in the Chrome Apps subdirectory, remove it.
   base::FilePath chrome_apps_dir;
@@ -488,7 +492,9 @@
   }
 
   // Delete downloaded shortcut icons for the web app.
-  web_app::internals::DeleteShortcutsMenuIcons(web_app_path);
+  if (!web_app::internals::DeleteShortcutsMenuIcons(web_app_path))
+    result = false;
+  return result;
 }
 
 void DeleteAllShortcutsForProfile(const base::FilePath& profile_path) {
diff --git a/chrome/browser/web_applications/components/web_app_shortcuts_menu.cc b/chrome/browser/web_applications/components/web_app_shortcuts_menu.cc
index d62687a4a..3733d29b 100644
--- a/chrome/browser/web_applications/components/web_app_shortcuts_menu.cc
+++ b/chrome/browser/web_applications/components/web_app_shortcuts_menu.cc
@@ -25,10 +25,12 @@
   DCHECK(ShouldRegisterShortcutsMenuWithOs());
 }
 
-void UnregisterShortcutsMenuWithOs(const AppId& app_id,
+bool UnregisterShortcutsMenuWithOs(const AppId& app_id,
                                    const base::FilePath& profile_path) {
   NOTIMPLEMENTED();
   DCHECK(ShouldRegisterShortcutsMenuWithOs());
+
+  return true;
 }
 #endif  // !defined(OS_WIN)
 
diff --git a/chrome/browser/web_applications/components/web_app_shortcuts_menu.h b/chrome/browser/web_applications/components/web_app_shortcuts_menu.h
index 69a5062..4af43b6 100644
--- a/chrome/browser/web_applications/components/web_app_shortcuts_menu.h
+++ b/chrome/browser/web_applications/components/web_app_shortcuts_menu.h
@@ -30,8 +30,8 @@
     const ShortcutsMenuIconsBitmaps& shortcuts_menu_icons_bitmaps);
 
 // Deletes the ShortcutsMenu from the OS. This should be called during the
-// uninstallation process.
-void UnregisterShortcutsMenuWithOs(const AppId& app_id,
+// uninstallation process. Returns true on successful deletion.
+bool UnregisterShortcutsMenuWithOs(const AppId& app_id,
                                    const base::FilePath& profile_path);
 
 }  // namespace web_app
diff --git a/chrome/browser/web_applications/components/web_app_shortcuts_menu_win.cc b/chrome/browser/web_applications/components/web_app_shortcuts_menu_win.cc
index 3dfc97fa..4c54093 100644
--- a/chrome/browser/web_applications/components/web_app_shortcuts_menu_win.cc
+++ b/chrome/browser/web_applications/components/web_app_shortcuts_menu_win.cc
@@ -211,22 +211,23 @@
                      shortcuts_menu_icons_bitmaps));
 }
 
-void UnregisterShortcutsMenuWithOs(const AppId& app_id,
+bool UnregisterShortcutsMenuWithOs(const AppId& app_id,
                                    const base::FilePath& profile_path) {
   if (!JumpListUpdater::DeleteJumpList(
           GenerateAppUserModelId(profile_path, app_id))) {
     RecordUnregistration(UnregistrationResult::kFailedToDeleteJumpList);
-    return;
+    return false;
   }
   RecordUnregistration(UnregistrationResult::kSuccess);
+  return true;
 }
 
 namespace internals {
 
-void DeleteShortcutsMenuIcons(const base::FilePath& shortcut_data_dir) {
+bool DeleteShortcutsMenuIcons(const base::FilePath& shortcut_data_dir) {
   base::FilePath shortcuts_menu_icons_path =
       GetShortcutsMenuIconsDirectory(shortcut_data_dir);
-  base::DeletePathRecursively(shortcuts_menu_icons_path);
+  return base::DeletePathRecursively(shortcuts_menu_icons_path);
 }
 
 }  // namespace internals
diff --git a/chrome/browser/web_applications/components/web_app_shortcuts_menu_win.h b/chrome/browser/web_applications/components/web_app_shortcuts_menu_win.h
index e7dd94c2..b369716e 100644
--- a/chrome/browser/web_applications/components/web_app_shortcuts_menu_win.h
+++ b/chrome/browser/web_applications/components/web_app_shortcuts_menu_win.h
@@ -13,7 +13,7 @@
 
 // Deletes all .ico shortcuts menu icons that were written to disk at PWA
 // install time. Call this when PWA is uninstalled on Windows.
-void DeleteShortcutsMenuIcons(const base::FilePath& web_app_path);
+bool DeleteShortcutsMenuIcons(const base::FilePath& web_app_path);
 
 }  // namespace internals
 
diff --git a/chrome/browser/web_applications/extensions/bookmark_app_registrar.cc b/chrome/browser/web_applications/extensions/bookmark_app_registrar.cc
index 6fb7f5a..c472edfb 100644
--- a/chrome/browser/web_applications/extensions/bookmark_app_registrar.cc
+++ b/chrome/browser/web_applications/extensions/bookmark_app_registrar.cc
@@ -81,7 +81,7 @@
     NotifyWebAppUninstalled(extension->id());
     web_app::WebAppProviderBase::GetProviderBase(profile())
         ->os_integration_manager()
-        .UninstallOsHooks(extension->id());
+        .UninstallOsHooks(extension->id(), base::DoNothing());
 
     bookmark_app_being_observed_ = nullptr;
   }
@@ -104,7 +104,7 @@
     NotifyWebAppProfileWillBeDeleted(extension->id());
     web_app::WebAppProviderBase::GetProviderBase(profile())
         ->os_integration_manager()
-        .UninstallOsHooks(extension->id());
+        .UninstallOsHooks(extension->id(), base::DoNothing());
   }
 
   bookmark_app_being_observed_ = nullptr;
diff --git a/chrome/browser/web_applications/extensions/web_app_extension_shortcut.cc b/chrome/browser/web_applications/extensions/web_app_extension_shortcut.cc
index b2275a4..cea641d 100644
--- a/chrome/browser/web_applications/extensions/web_app_extension_shortcut.cc
+++ b/chrome/browser/web_applications/extensions/web_app_extension_shortcut.cc
@@ -268,9 +268,8 @@
       ShortcutInfoForExtensionAndProfile(app, profile));
   base::FilePath shortcut_data_dir =
       internals::GetShortcutDataDir(*shortcut_info);
-  internals::PostShortcutIOTask(
-      base::BindOnce(&internals::DeletePlatformShortcuts, shortcut_data_dir),
-      std::move(shortcut_info));
+  internals::ScheduleDeletePlatformShortcuts(
+      shortcut_data_dir, std::move(shortcut_info), base::DoNothing());
 }
 
 void UpdateAllShortcuts(const base::string16& old_app_title,
diff --git a/chrome/browser/web_applications/os_integration_manager.cc b/chrome/browser/web_applications/os_integration_manager.cc
index d4a4a61..4e057f2 100644
--- a/chrome/browser/web_applications/os_integration_manager.cc
+++ b/chrome/browser/web_applications/os_integration_manager.cc
@@ -30,11 +30,11 @@
   explicit OsHooksBarrierInfo(InstallOsHooksCallback done_callback)
       : done_callback_(std::move(done_callback)) {}
 
-  void Run(OsHookType::Type os_hook, bool created) {
+  void Run(OsHookType::Type os_hook, bool completed) {
     DCHECK(!os_hooks_called_[os_hook]);
 
     os_hooks_called_[os_hook] = true;
-    os_hooks_results_[os_hook] = created;
+    os_hooks_results_[os_hook] = completed;
 
     if (os_hooks_called_.all()) {
       std::move(done_callback_).Run(os_hooks_results_);
@@ -103,7 +103,7 @@
   // callback for every type. Developers should double check that Run is
   // called for every OsHookType::Type. If there is any missing type, the
   // InstallOsHooksCallback will not get run.
-  base::RepeatingCallback<void(OsHookType::Type os_hook, bool created)>
+  base::RepeatingCallback<void(OsHookType::Type os_hook, bool completed)>
       barrier = base::BindRepeating(
           &OsHooksBarrierInfo::Run,
           base::Owned(new OsHooksBarrierInfo(std::move(callback))));
@@ -127,12 +127,24 @@
   }
 }
 
-void OsIntegrationManager::UninstallOsHooks(const AppId& app_id) {
+void OsIntegrationManager::UninstallOsHooks(const AppId& app_id,
+                                            UninstallOsHooksCallback callback) {
+  DCHECK(shortcut_manager_);
+
   if (suppress_os_hooks_for_testing_)
     return;
 
-  if (ShouldRegisterShortcutsMenuWithOs())
-    UnregisterShortcutsMenuWithOs(app_id, profile_->GetPath());
+  base::RepeatingCallback<void(OsHookType::Type os_hook, bool completed)>
+      barrier = base::BindRepeating(
+          &OsHooksBarrierInfo::Run,
+          base::Owned(new OsHooksBarrierInfo(std::move(callback))));
+
+  if (ShouldRegisterShortcutsMenuWithOs()) {
+    barrier.Run(OsHookType::kShortcutsMenu,
+                UnregisterShortcutsMenuWithOs(app_id, profile_->GetPath()));
+  } else {
+    barrier.Run(OsHookType::kShortcutsMenu, /*completed=*/true);
+  }
 
   std::unique_ptr<ShortcutInfo> shortcut_info =
       shortcut_manager_->BuildShortcutInfo(app_id);
@@ -140,17 +152,19 @@
       internals::GetShortcutDataDir(*shortcut_info);
 
   if (base::FeatureList::IsEnabled(features::kDesktopPWAsRunOnOsLogin)) {
-    internals::GetShortcutIOTaskRunner()->PostTask(
-        FROM_HERE,
-        base::BindOnce(&internals::UnregisterRunOnOsLogin,
-                       shortcut_info->profile_path, shortcut_info->title));
+    ScheduleUnregisterRunOnOsLogin(
+        shortcut_info->profile_path, shortcut_info->title,
+        base::BindOnce(barrier, OsHookType::kRunOnOsLogin));
   }
 
-  internals::PostShortcutIOTask(
-      base::BindOnce(&internals::DeletePlatformShortcuts, shortcut_data_dir),
-      std::move(shortcut_info));
+  internals::ScheduleDeletePlatformShortcuts(
+      shortcut_data_dir, std::move(shortcut_info),
+      base::BindOnce(barrier, OsHookType::kShortcuts));
 
+  // TODO(https://crbug.com/1108109) we should return the result of file handler
+  // unregistration and record errors during unregistration.
   file_handler_manager_->DisableAndUnregisterOsFileHandlers(app_id);
+  barrier.Run(OsHookType::kFileHandlers, /*completed=*/true);
 
   DeleteSharedAppShims(app_id);
 }
@@ -169,12 +183,12 @@
   DCHECK(file_handler_manager_);
   DCHECK(ui_manager_);
 
-  barrier_callback.Run(OsHookType::kShortcuts, true);
+  barrier_callback.Run(OsHookType::kShortcuts, /*completed=*/true);
 
   // TODO(crbug.com/1087219): callback should be run after all hooks are
   // deployed, need to refactor filehandler to allow this.
   file_handler_manager_->EnableAndRegisterOsFileHandlers(app_id);
-  barrier_callback.Run(OsHookType::kFileHandlers, true);
+  barrier_callback.Run(OsHookType::kFileHandlers, /*completed=*/true);
 
   if (options.add_to_quick_launch_bar &&
       ui_manager_->CanAddAppToQuickLaunchBar()) {
@@ -187,13 +201,13 @@
           web_app_info->shortcuts_menu_icons_bitmaps);
       // TODO(https://crbug.com/1098471): fix RegisterShortcutsMenuWithOs to
       // take callback.
-      barrier_callback.Run(OsHookType::kShortcutsMenu, true);
+      barrier_callback.Run(OsHookType::kShortcutsMenu, /*completed=*/true);
     } else {
       shortcut_manager_->ReadAllShortcutsMenuIconsAndRegisterShortcutsMenu(
           app_id, base::BindOnce(barrier_callback, OsHookType::kShortcutsMenu));
     }
   } else {
-    barrier_callback.Run(OsHookType::kShortcutsMenu, false);
+    barrier_callback.Run(OsHookType::kShortcutsMenu, /*completed=*/false);
   }
 
   if (base::FeatureList::IsEnabled(features::kDesktopPWAsRunOnOsLogin) &&
diff --git a/chrome/browser/web_applications/os_integration_manager.h b/chrome/browser/web_applications/os_integration_manager.h
index 4a77c83a..aee7dac 100644
--- a/chrome/browser/web_applications/os_integration_manager.h
+++ b/chrome/browser/web_applications/os_integration_manager.h
@@ -34,11 +34,14 @@
   bool run_on_os_login = false;
 };
 
-// Callback made when InstallOsHooks has finished trying to deploy all
-// needed OS hooks.
+// Callback made after InstallOsHooks is finished.
 using InstallOsHooksCallback =
     base::OnceCallback<void(OsHooksResults os_hooks_info)>;
 
+// Callback made after UninstallOsHooks is finished.
+using UninstallOsHooksCallback =
+    base::OnceCallback<void(OsHooksResults os_hooks_info)>;
+
 // OsIntegrationManager is responsible of creating/updating/deleting
 // all OS hooks during Web App lifecycle.
 // It contains individual OS integration managers and takes
@@ -67,7 +70,8 @@
   // Uninstall all OS hooks for the web app.
   // TODO(https://crbug.com/1108109) we should record uninstall result and allow
   // callback. virtual for testing
-  virtual void UninstallOsHooks(const AppId& app_id);
+  virtual void UninstallOsHooks(const AppId& app_id,
+                                UninstallOsHooksCallback callback);
 
   void SuppressOsHooksForTesting();
 
diff --git a/chrome/browser/web_applications/test/test_os_integration_manager.cc b/chrome/browser/web_applications/test/test_os_integration_manager.cc
index 0bdefbf..45bb01d 100644
--- a/chrome/browser/web_applications/test/test_os_integration_manager.cc
+++ b/chrome/browser/web_applications/test/test_os_integration_manager.cc
@@ -57,7 +57,9 @@
       base::BindOnce(std::move(callback), std::move(os_hooks_results)));
 }
 
-void TestOsIntegrationManager::UninstallOsHooks(const AppId& app_id) {
+void TestOsIntegrationManager::UninstallOsHooks(
+    const AppId& app_id,
+    UninstallOsHooksCallback callback) {
   NOTIMPLEMENTED();
 }
 
diff --git a/chrome/browser/web_applications/test/test_os_integration_manager.h b/chrome/browser/web_applications/test/test_os_integration_manager.h
index fe3706c..96b08af 100644
--- a/chrome/browser/web_applications/test/test_os_integration_manager.h
+++ b/chrome/browser/web_applications/test/test_os_integration_manager.h
@@ -23,7 +23,8 @@
                       InstallOsHooksCallback callback,
                       std::unique_ptr<WebApplicationInfo> web_app_info,
                       InstallOsHooksOptions options) override;
-  void UninstallOsHooks(const AppId& app_id) override;
+  void UninstallOsHooks(const AppId& app_id,
+                        UninstallOsHooksCallback callback) override;
 
   size_t num_create_shortcuts_calls() const {
     return num_create_shortcuts_calls_;
diff --git a/chrome/browser/web_applications/web_app_install_finalizer.cc b/chrome/browser/web_applications/web_app_install_finalizer.cc
index e44a6775..24bf0b0 100644
--- a/chrome/browser/web_applications/web_app_install_finalizer.cc
+++ b/chrome/browser/web_applications/web_app_install_finalizer.cc
@@ -384,7 +384,7 @@
   registrar().NotifyWebAppUninstalled(app_id);
   WebAppProviderBase::GetProviderBase(profile_)
       ->os_integration_manager()
-      .UninstallOsHooks(app_id);
+      .UninstallOsHooks(app_id, base::DoNothing());
 
   ScopedRegistryUpdate update(registry_controller().AsWebAppSyncBridge());
   update->DeleteApp(app_id);
diff --git a/chrome/browser/web_applications/web_app_registrar.cc b/chrome/browser/web_applications/web_app_registrar.cc
index 2b77a2b..237d86e 100644
--- a/chrome/browser/web_applications/web_app_registrar.cc
+++ b/chrome/browser/web_applications/web_app_registrar.cc
@@ -191,7 +191,7 @@
     NotifyWebAppProfileWillBeDeleted(app.app_id());
     WebAppProviderBase::GetProviderBase(profile())
         ->os_integration_manager()
-        .UninstallOsHooks(app.app_id());
+        .UninstallOsHooks(app.app_id(), base::DoNothing());
   }
   // We can't do registry_.clear() here because it makes in-memory registry
   // diverged from the sync server registry and from the on-disk registry
diff --git a/chrome/browser/web_applications/web_app_sync_bridge.cc b/chrome/browser/web_applications/web_app_sync_bridge.cc
index 47187ab..4d6f347 100644
--- a/chrome/browser/web_applications/web_app_sync_bridge.cc
+++ b/chrome/browser/web_applications/web_app_sync_bridge.cc
@@ -513,7 +513,7 @@
     registrar_->NotifyWebAppUninstalled(app_id);
     WebAppProviderBase::GetProviderBase(profile())
         ->os_integration_manager()
-        .UninstallOsHooks(app_id);
+        .UninstallOsHooks(app_id, base::DoNothing());
   }
 
   std::vector<WebApp*> apps_to_install;
diff --git a/chrome/build/win32.pgo.txt b/chrome/build/win32.pgo.txt
index 5b90f8583..df2b786 100644
--- a/chrome/build/win32.pgo.txt
+++ b/chrome/build/win32.pgo.txt
@@ -1 +1 @@
-chrome-win32-master-1596549454-7f993c1fb49036aa3a3eb3f7c44ae7cd093767bd.profdata
+chrome-win32-master-1596571199-429c5d3274b1adcffd4054f095f3cc2a5ada36c8.profdata
diff --git a/chrome/build/win64.pgo.txt b/chrome/build/win64.pgo.txt
index d1d7bc19..e8bf390 100644
--- a/chrome/build/win64.pgo.txt
+++ b/chrome/build/win64.pgo.txt
@@ -1 +1 @@
-chrome-win64-master-1596542250-4b9669504bdebb7cf76d0d2b9c7cc833bb153daa.profdata
+chrome-win64-master-1596562698-892fa89488e0e796ec09ee348ea5782de7379276.profdata
diff --git a/chrome/common/extensions/api/_features.md b/chrome/common/extensions/api/_features.md
index c15412d8..520a1875 100644
--- a/chrome/common/extensions/api/_features.md
+++ b/chrome/common/extensions/api/_features.md
@@ -312,8 +312,8 @@
 The `platforms` property specifies the properties the feature should be
 available on.
 
-The accepted values are lists of strings from `chromeos`, `mac`, `linux`, and
-`win`.
+The accepted values are lists of strings from `chromeos`, `mac`, 'lacros',
+`linux`, and `win`.
 
 ### session\_types
 
diff --git a/chrome/common/extensions/api/enterprise_platform_keys.idl b/chrome/common/extensions/api/enterprise_platform_keys.idl
index 39c98d6f..14a0de2f 100644
--- a/chrome/common/extensions/api/enterprise_platform_keys.idl
+++ b/chrome/common/extensions/api/enterprise_platform_keys.idl
@@ -7,7 +7,7 @@
 // certificates will be managed by the platform and can be used for TLS
 // authentication, network access or by other extension through
 // $(ref:platformKeys chrome.platformKeys).
-[platforms = ("chromeos")]
+[platforms = ("chromeos", "lacros")]
 namespace enterprise.platformKeys {
   [nocompile, noinline_doc] dictionary Token {
     // Uniquely identifies this <code>Token</code>.
diff --git a/chrome/common/extensions/api/file_manager_private.idl b/chrome/common/extensions/api/file_manager_private.idl
index c57fdcc..a170272 100644
--- a/chrome/common/extensions/api/file_manager_private.idl
+++ b/chrome/common/extensions/api/file_manager_private.idl
@@ -1218,10 +1218,19 @@
 
   // Return true if sharesheet contains share targets for entries.
   // |entries| Array of selected entries
-  // |callback|
+  // |callback| is called with error in case of failure and with no arguments
+  // if successfully launched the Sharesheet dialog, but before user has
+  // finished the sharing.
   [nocompile]
   static void sharesheetHasTargets([instanceof=Entry] object[] entries,
                            BooleanCallback callback);
+
+  // Invoke Sharesheet for selected files.
+  // |entries| Array of selected entries.
+  // |callback|
+  [nocompile]
+  static void invokeSharesheet([instanceof=Entry] object[] entries,
+                                     SimpleCallback callback);
 };
 
 interface Events {
diff --git a/chrome/common/extensions/api/file_manager_private_internal.idl b/chrome/common/extensions/api/file_manager_private_internal.idl
index 8394d8e..7cbd331 100644
--- a/chrome/common/extensions/api/file_manager_private_internal.idl
+++ b/chrome/common/extensions/api/file_manager_private_internal.idl
@@ -110,5 +110,6 @@
                              boolean cropToSquare,
                              GetThumbnailCallback callback);
     static void sharesheetHasTargets(DOMString[] urls, BooleanCallback callback);
+    static void invokeSharesheet(DOMString[] urls, SimpleCallback callback);
   };
 };
diff --git a/chrome/credential_provider/gaiacp/gaia_credential_base.cc b/chrome/credential_provider/gaiacp/gaia_credential_base.cc
index cba85d8..81e29dd 100644
--- a/chrome/credential_provider/gaiacp/gaia_credential_base.cc
+++ b/chrome/credential_provider/gaiacp/gaia_credential_base.cc
@@ -2455,8 +2455,9 @@
   }
 
   base::string16 sid = OLE2CW(user_sid_);
-  if (UserPoliciesManager::Get()->GetTimeDeltaSinceLastPolicyFetch(sid) >
-      kMaxTimeDeltaSinceLastUserPolicyRefresh) {
+  if (UserPoliciesManager::Get()->CloudPoliciesEnabled() &&
+      UserPoliciesManager::Get()->GetTimeDeltaSinceLastPolicyFetch(sid) >
+          kMaxTimeDeltaSinceLastUserPolicyRefresh) {
     // TODO(crbug.com/976744) Use downscoped token here.
     base::string16 access_token = GetDictString(*properties, kKeyAccessToken);
     HRESULT hr = UserPoliciesManager::Get()->FetchAndStoreCloudUserPolicies(
diff --git a/chrome/credential_provider/gaiacp/gaia_credential_base_unittests.cc b/chrome/credential_provider/gaiacp/gaia_credential_base_unittests.cc
index 390a6eb..49e19e0 100644
--- a/chrome/credential_provider/gaiacp/gaia_credential_base_unittests.cc
+++ b/chrome/credential_provider/gaiacp/gaia_credential_base_unittests.cc
@@ -3233,14 +3233,17 @@
 //                 http server.
 // 2. bool  true:  Policies were fetched recently and don't need refreshing.
 //          false: Policies were never fetched or are very old.
+// 3. bool :       Whether cloud policies feature is enabled.
 class GcpGaiaCredentialBaseFetchCloudPoliciesTest
     : public GcpGaiaCredentialBaseTest,
-      public ::testing::WithParamInterface<std::tuple<bool, bool>> {};
+      public ::testing::WithParamInterface<std::tuple<bool, bool, bool>> {};
 
 TEST_P(GcpGaiaCredentialBaseFetchCloudPoliciesTest, FetchAndStore) {
   bool fail_fetch_policies = std::get<0>(GetParam());
   bool policy_refreshed_recently = std::get<1>(GetParam());
+  bool cloud_policies_enabled = std::get<2>(GetParam());
 
+  FakeUserPoliciesManager fake_user_policies_manager(cloud_policies_enabled);
   GoogleMdmEnrolledStatusForTesting force_success(true);
 
   // Create a fake user associated to a gaia id.
@@ -3251,30 +3254,32 @@
                 base::UTF8ToUTF16(kDefaultGaiaId), base::string16(), &sid_str));
   base::string16 sid = OLE2W(sid_str);
 
-  base::string16 fetch_time_millis = L"0";
-  if (policy_refreshed_recently) {
-    fetch_time_millis = base::NumberToString16(
-        base::Time::Now().ToDeltaSinceWindowsEpoch().InMilliseconds());
+  if (cloud_policies_enabled) {
+    base::string16 fetch_time_millis = L"0";
+    if (policy_refreshed_recently) {
+      fetch_time_millis = base::NumberToString16(
+          base::Time::Now().ToDeltaSinceWindowsEpoch().InMilliseconds());
+    }
+    ASSERT_EQ(S_OK, SetUserProperty(sid, L"last_policy_refresh_time",
+                                    fetch_time_millis));
+
+    std::string expected_response;
+    if (fail_fetch_policies) {
+      expected_response = "Invalid json response";
+    } else {
+      UserPolicies policies;
+      base::Value policies_value = policies.ToValue();
+      base::JSONWriter::Write(policies_value, &expected_response);
+    }
+
+    fake_http_url_fetcher_factory()->SetFakeResponse(
+        UserPoliciesManager::Get()->GetGcpwServiceUserPoliciesUrl(sid),
+        FakeWinHttpUrlFetcher::Headers(), expected_response);
   }
-  ASSERT_EQ(S_OK, SetUserProperty(sid, L"last_policy_refresh_time",
-                                  fetch_time_millis));
 
   // Change token response to an valid one.
   SetDefaultTokenHandleResponse(kDefaultValidTokenHandleResponse);
 
-  std::string expected_response;
-  if (fail_fetch_policies) {
-    expected_response = "Invalid json response";
-  } else {
-    UserPolicies policies;
-    base::Value policies_value = policies.ToValue();
-    base::JSONWriter::Write(policies_value, &expected_response);
-  }
-
-  fake_http_url_fetcher_factory()->SetFakeResponse(
-      UserPoliciesManager::Get()->GetGcpwServiceUserPoliciesUrl(sid),
-      FakeWinHttpUrlFetcher::Headers(), expected_response);
-
   // Create provider and start logon.
   Microsoft::WRL::ComPtr<ICredentialProviderCredential> cred;
 
@@ -3288,12 +3293,18 @@
   base::TimeDelta time_since_last_fetch =
       UserPoliciesManager::Get()->GetTimeDeltaSinceLastPolicyFetch(sid);
 
+  if (cloud_policies_enabled && !policy_refreshed_recently) {
+    ASSERT_EQ(1, fake_user_policies_manager.GetNumTimesFetchAndStoreCalled());
+  } else {
+    ASSERT_EQ(0, fake_user_policies_manager.GetNumTimesFetchAndStoreCalled());
+  }
+
   // Expected number of HTTP calls when not fetching user policies since upload
   // device details is always called.
   const size_t base_num_http_requests = 1;
   const size_t requests_created =
       fake_http_url_fetcher_factory()->requests_created();
-  if (policy_refreshed_recently) {
+  if (!cloud_policies_enabled || policy_refreshed_recently) {
     // No new requests for fetching policies.
     ASSERT_EQ(base_num_http_requests, requests_created);
   } else {
@@ -3321,6 +3332,7 @@
 INSTANTIATE_TEST_SUITE_P(All,
                          GcpGaiaCredentialBaseFetchCloudPoliciesTest,
                          ::testing::Combine(::testing::Bool(),
+                                            ::testing::Bool(),
                                             ::testing::Bool()));
 
 }  // namespace testing
diff --git a/chrome/credential_provider/gaiacp/user_policies_manager.h b/chrome/credential_provider/gaiacp/user_policies_manager.h
index 16cbd66..3021199 100644
--- a/chrome/credential_provider/gaiacp/user_policies_manager.h
+++ b/chrome/credential_provider/gaiacp/user_policies_manager.h
@@ -25,8 +25,9 @@
   // Fetch the policies for the user from GCPW backend with |sid| using
   // |access_token| for authentication and authorization and saves it in file
   // storage replacing any previously fetched versions.
-  HRESULT FetchAndStoreCloudUserPolicies(const base::string16& sid,
-                                         const std::string& access_token);
+  virtual HRESULT FetchAndStoreCloudUserPolicies(
+      const base::string16& sid,
+      const std::string& access_token);
 
   // Return the elapsed time delta since the last time the policies were
   // successfully fetched for the user with |sid|.
diff --git a/chrome/credential_provider/test/gcp_fakes.cc b/chrome/credential_provider/test/gcp_fakes.cc
index cafb3764..5113a1d 100644
--- a/chrome/credential_provider/test/gcp_fakes.cc
+++ b/chrome/credential_provider/test/gcp_fakes.cc
@@ -1166,6 +1166,15 @@
   *GetInstanceStorage() = original_manager_;
 }
 
+HRESULT FakeUserPoliciesManager::FetchAndStoreCloudUserPolicies(
+    const base::string16& sid,
+    const std::string& access_token) {
+  ++num_times_fetch_called_;
+  fetch_status_ =
+      original_manager_->FetchAndStoreCloudUserPolicies(sid, access_token);
+  return fetch_status_;
+}
+
 void FakeUserPoliciesManager::SetUserPolicies(const base::string16& sid,
                                               const UserPolicies& policies) {
   user_policies_[sid] = policies;
@@ -1181,6 +1190,10 @@
   return false;
 }
 
+int FakeUserPoliciesManager::GetNumTimesFetchAndStoreCalled() const {
+  return num_times_fetch_called_;
+}
+
 ///////////////////////////////////////////////////////////////////////////////
 
 FakeDevicePoliciesManager::FakeDevicePoliciesManager(
diff --git a/chrome/credential_provider/test/gcp_fakes.h b/chrome/credential_provider/test/gcp_fakes.h
index 6f5e4aef..c4ccbafd 100644
--- a/chrome/credential_provider/test/gcp_fakes.h
+++ b/chrome/credential_provider/test/gcp_fakes.h
@@ -595,15 +595,24 @@
   explicit FakeUserPoliciesManager(bool cloud_policies_enabled);
   ~FakeUserPoliciesManager() override;
 
+  HRESULT FetchAndStoreCloudUserPolicies(
+      const base::string16& sid,
+      const std::string& access_token) override;
+
   // Specify the policy to use for a user.
   void SetUserPolicies(const base::string16& sid, const UserPolicies& policies);
 
   bool GetUserPolicies(const base::string16& sid,
                        UserPolicies* policies) override;
 
+  // Returns the number of times FetchAndStoreCloudUserPolicies method was
+  // called.
+  int GetNumTimesFetchAndStoreCalled() const;
+
  private:
   UserPoliciesManager* original_manager_ = nullptr;
   std::map<base::string16, UserPolicies> user_policies_;
+  int num_times_fetch_called_ = 0;
 };
 
 ///////////////////////////////////////////////////////////////////////////////
diff --git a/chrome/installer/setup/install_worker.cc b/chrome/installer/setup/install_worker.cc
index 7ad0077..c4b7b71 100644
--- a/chrome/installer/setup/install_worker.cc
+++ b/chrome/installer/setup/install_worker.cc
@@ -395,7 +395,7 @@
       install_static::GetElevationServiceDisplayName(),
       base::CommandLine(elevation_service_path),
       install_static::GetClientStateKeyPath(),
-      install_static::GetElevatorClsid(), install_static::GetElevatorIid());
+      {install_static::GetElevatorClsid()}, {install_static::GetElevatorIid()});
   install_service_work_item->set_best_effort(true);
   list->AddWorkItem(install_service_work_item);
 }
diff --git a/chrome/installer/setup/uninstall.cc b/chrome/installer/setup/uninstall.cc
index 56e29e5..8d3b5f3 100644
--- a/chrome/installer/setup/uninstall.cc
+++ b/chrome/installer/setup/uninstall.cc
@@ -679,8 +679,8 @@
     if (!InstallServiceWorkItem::DeleteService(
             install_static::GetElevationServiceName(),
             install_static::GetClientStateKeyPath(),
-            install_static::GetElevatorClsid(),
-            install_static::GetElevatorIid())) {
+            {install_static::GetElevatorClsid()},
+            {install_static::GetElevatorIid()})) {
       LOG(WARNING) << "Failed to delete "
                    << install_static::GetElevationServiceName();
     }
diff --git a/chrome/installer/util/install_service_work_item.cc b/chrome/installer/util/install_service_work_item.cc
index ffee605a..8f9e70a 100644
--- a/chrome/installer/util/install_service_work_item.cc
+++ b/chrome/installer/util/install_service_work_item.cc
@@ -14,14 +14,14 @@
     const base::string16& display_name,
     const base::CommandLine& service_cmd_line,
     const base::string16& registry_path,
-    const GUID& clsid,
-    const GUID& iid)
+    const std::vector<GUID>& clsids,
+    const std::vector<GUID>& iids)
     : impl_(std::make_unique<InstallServiceWorkItemImpl>(service_name,
                                                          display_name,
                                                          service_cmd_line,
                                                          registry_path,
-                                                         clsid,
-                                                         iid)) {}
+                                                         clsids,
+                                                         iids)) {}
 
 InstallServiceWorkItem::~InstallServiceWorkItem() = default;
 
@@ -36,12 +36,12 @@
 // static
 bool InstallServiceWorkItem::DeleteService(const base::string16& service_name,
                                            const base::string16& registry_path,
-                                           const GUID& clsid,
-                                           const GUID& iid) {
+                                           const std::vector<GUID>& clsids,
+                                           const std::vector<GUID>& iids) {
   return InstallServiceWorkItemImpl(
              service_name, base::string16(),
              base::CommandLine(base::CommandLine::NO_PROGRAM), registry_path,
-             clsid, iid)
+             clsids, iids)
       .DeleteServiceImpl();
 }
 
diff --git a/chrome/installer/util/install_service_work_item.h b/chrome/installer/util/install_service_work_item.h
index 5a2fe889..b3726ab5 100644
--- a/chrome/installer/util/install_service_work_item.h
+++ b/chrome/installer/util/install_service_work_item.h
@@ -13,6 +13,7 @@
 #define CHROME_INSTALLER_UTIL_INSTALL_SERVICE_WORK_ITEM_H_
 
 #include <memory>
+#include <vector>
 
 #include "base/macros.h"
 #include "base/strings/string16.h"
@@ -47,25 +48,22 @@
   // persist a versioned service name. An example |registry_path| is
   // "Software\ProductFoo".
   //
-  // |clsid| is the CLSID and AppId to register.
-  // If COM CLSID/AppId registration is not required, pass in GUID_NULL for
-  // |clsid|.
-  // |iid| is the Interface and Typelib to register.
-  // If COM Interface/Typelib registration is not required, pass in
-  // GUID_NULL for |iid|.
+  // If COM CLSID/AppId registration is required, |clsids| should contain the
+  // CLSIDs and AppIds to register. If COM Interface/Typelib registration is
+  // required, |iids| should contain the Interfaces and Typelibs to register.
   InstallServiceWorkItem(const base::string16& service_name,
                          const base::string16& display_name,
                          const base::CommandLine& service_cmd_line,
                          const base::string16& registry_path,
-                         const GUID& clsid,
-                         const GUID& iid);
+                         const std::vector<GUID>& clsids,
+                         const std::vector<GUID>& iids);
 
   ~InstallServiceWorkItem() override;
 
   static bool DeleteService(const base::string16& service_name,
                             const base::string16& registry_path,
-                            const GUID& clsid,
-                            const GUID& iid);
+                            const std::vector<GUID>& clsids,
+                            const std::vector<GUID>& iids);
 
  private:
   friend class InstallServiceWorkItemTest;
diff --git a/chrome/installer/util/install_service_work_item_impl.cc b/chrome/installer/util/install_service_work_item_impl.cc
index c37faaf..9c1fd44 100644
--- a/chrome/installer/util/install_service_work_item_impl.cc
+++ b/chrome/installer/util/install_service_work_item_impl.cc
@@ -147,15 +147,15 @@
     const base::string16& display_name,
     const base::CommandLine& service_cmd_line,
     const base::string16& registry_path,
-    const GUID& clsid,
-    const GUID& iid)
+    const std::vector<GUID>& clsids,
+    const std::vector<GUID>& iids)
     : com_registration_work_items_(WorkItem::CreateWorkItemList()),
       service_name_(service_name),
       display_name_(display_name),
       service_cmd_line_(service_cmd_line),
       registry_path_(registry_path),
-      clsid_(clsid),
-      iid_(iid),
+      clsids_(clsids),
+      iids_(iids),
       rollback_existing_service_(false),
       rollback_new_service_(false),
       original_service_still_exists_(false) {}
@@ -232,15 +232,15 @@
 }
 
 bool InstallServiceWorkItemImpl::DoComRegistration() {
-  if (clsid_ != GUID_NULL) {
-    const base::string16 clsid_reg_path = GetComClsidRegistryPath(clsid_);
-    const base::string16 appid_reg_path = GetComAppidRegistryPath(clsid_);
+  for (const auto& clsid : clsids_) {
+    const base::string16 clsid_reg_path = GetComClsidRegistryPath(clsid);
+    const base::string16 appid_reg_path = GetComAppidRegistryPath(clsid);
 
     com_registration_work_items_->AddCreateRegKeyWorkItem(
         HKEY_LOCAL_MACHINE, clsid_reg_path, WorkItem::kWow64Default);
     com_registration_work_items_->AddSetRegValueWorkItem(
         HKEY_LOCAL_MACHINE, clsid_reg_path, WorkItem::kWow64Default, L"AppID",
-        base::win::WStringFromGUID(clsid_), true);
+        base::win::WStringFromGUID(clsid), true);
     com_registration_work_items_->AddCreateRegKeyWorkItem(
         HKEY_LOCAL_MACHINE, appid_reg_path, WorkItem::kWow64Default);
     com_registration_work_items_->AddSetRegValueWorkItem(
@@ -248,9 +248,9 @@
         L"LocalService", GetCurrentServiceName(), true);
   }
 
-  if (iid_ != GUID_NULL) {
-    const base::string16 iid_reg_path = GetComIidRegistryPath(iid_);
-    const base::string16 typelib_reg_path = GetComTypeLibRegistryPath(iid_);
+  for (const auto& iid : iids_) {
+    const base::string16 iid_reg_path = GetComIidRegistryPath(iid);
+    const base::string16 typelib_reg_path = GetComTypeLibRegistryPath(iid);
 
     // Registering the Ole Automation marshaler with the CLSID
     // {00020424-0000-0000-C000-000000000046} as the proxy/stub for the
@@ -269,7 +269,7 @@
         WorkItem::kWow64Default);
     com_registration_work_items_->AddSetRegValueWorkItem(
         HKEY_LOCAL_MACHINE, iid_reg_path + L"\\TypeLib",
-        WorkItem::kWow64Default, L"", base::win::WStringFromGUID(iid_), true);
+        WorkItem::kWow64Default, L"", base::win::WStringFromGUID(iid), true);
     com_registration_work_items_->AddSetRegValueWorkItem(
         HKEY_LOCAL_MACHINE, iid_reg_path + L"\\TypeLib",
         WorkItem::kWow64Default, L"Version", L"1.0", true);
@@ -359,20 +359,22 @@
 
 bool InstallServiceWorkItemImpl::DeleteServiceImpl() {
   // Uninstall the elevation service.
-  if (clsid_ != GUID_NULL) {
+  for (const auto& clsid : clsids_) {
     for (const auto& reg_path :
-         {GetComClsidRegistryPath(clsid_), GetComAppidRegistryPath(clsid_)}) {
+         {GetComClsidRegistryPath(clsid), GetComAppidRegistryPath(clsid)}) {
       InstallUtil::DeleteRegistryKey(HKEY_LOCAL_MACHINE, reg_path,
                                      WorkItem::kWow64Default);
     }
   }
-  if (iid_ != GUID_NULL) {
+
+  for (const auto& iid : iids_) {
     for (const auto& reg_path :
-         {GetComIidRegistryPath(iid_), GetComTypeLibRegistryPath(iid_)}) {
+         {GetComIidRegistryPath(iid), GetComTypeLibRegistryPath(iid)}) {
       InstallUtil::DeleteRegistryKey(HKEY_LOCAL_MACHINE, reg_path,
                                      WorkItem::kWow64Default);
     }
   }
+
   scm_.Set(::OpenSCManager(nullptr, nullptr, SC_MANAGER_CONNECT));
   if (!scm_.IsValid()) {
     DPLOG(ERROR) << "::OpenSCManager Failed";
diff --git a/chrome/installer/util/install_service_work_item_impl.h b/chrome/installer/util/install_service_work_item_impl.h
index d4125191..8ff5626 100644
--- a/chrome/installer/util/install_service_work_item_impl.h
+++ b/chrome/installer/util/install_service_work_item_impl.h
@@ -54,8 +54,8 @@
                              const base::string16& display_name,
                              const base::CommandLine& service_cmd_line,
                              const base::string16& registry_path,
-                             const GUID& clsid,
-                             const GUID& iid);
+                             const std::vector<GUID>& clsids,
+                             const std::vector<GUID>& iids);
 
   ~InstallServiceWorkItemImpl();
 
@@ -170,13 +170,12 @@
   // to the 32-bit view of the registry.
   const base::string16 registry_path_;
 
-  // If COM CLSID/AppId registration is required, |clsid| would contain a valid
-  // CLSID.
-  const GUID clsid_;
+  // If COM CLSID/AppId registration is required, |clsids_| would be populated.
+  const std::vector<GUID> clsids_;
 
-  // If COM Interface/Typelib registration is required, |iid| would contain a
-  // valid IID.
-  const GUID iid_;
+  // If COM Interface/Typelib registration is required, |iids_| would be
+  // populated.
+  const std::vector<GUID> iids_;
 
   ScopedScHandle scm_;
   ScopedScHandle service_;
diff --git a/chrome/installer/util/install_service_work_item_unittest.cc b/chrome/installer/util/install_service_work_item_unittest.cc
index 5736557b..0bd18db 100644
--- a/chrome/installer/util/install_service_work_item_unittest.cc
+++ b/chrome/installer/util/install_service_work_item_unittest.cc
@@ -36,6 +36,8 @@
                          0x9c33,
                          0x4a09,
                          {0x9b, 0x3a, 0x3b, 0x88, 0xd, 0xf6, 0x44, 0x40}};
+const std::vector<GUID> kClsids = {kClsid};
+
 constexpr base::char16 kClsidRegPath[] =
     L"Software\\Classes\\CLSID\\{76EDE292-9C33-4A09-9B3A-3B880DF64440}";
 constexpr base::char16 kAppidRegPath[] =
@@ -46,6 +48,8 @@
                        0xa94a,
                        0x4c0a,
                        {0x93, 0xc7, 0x81, 0x33, 0x5, 0x26, 0xac, 0x7b}};
+const std::vector<GUID> kIids = {kIid};
+
 #define IID_REGISTRY_PATH \
   L"Software\\Classes\\Interface\\{0F9A0C1C-A94A-4C0A-93C7-81330526AC7B}"
 constexpr base::char16 kIidPSRegPath[] =
@@ -116,7 +120,7 @@
   auto item = std::make_unique<InstallServiceWorkItem>(
       kServiceName, kServiceDisplayName,
       base::CommandLine(base::FilePath(kServiceProgramPath)), kProductRegPath,
-      kClsid, kIid);
+      kClsids, kIids);
 
   ASSERT_TRUE(item->Do());
   EXPECT_TRUE(GetImpl(item.get())->OpenService());
@@ -177,21 +181,21 @@
   auto item = std::make_unique<InstallServiceWorkItem>(
       kServiceName, kServiceDisplayName,
       base::CommandLine(base::FilePath(kServiceProgramPath)), kProductRegPath,
-      kClsid, kIid);
+      kClsids, kIids);
 
   ASSERT_TRUE(item->Do());
   EXPECT_TRUE(GetImpl(item.get())->OpenService());
   EXPECT_TRUE(IsServiceCorrectlyConfigured(item.get()));
 
   EXPECT_TRUE(InstallServiceWorkItem::DeleteService(
-      kServiceName, kProductRegPath, kClsid, kIid));
+      kServiceName, kProductRegPath, kClsids, kIids));
 }
 
 TEST_F(InstallServiceWorkItemTest, Do_UpgradeNoChanges) {
   auto item = std::make_unique<InstallServiceWorkItem>(
       kServiceName, kServiceDisplayName,
       base::CommandLine(base::FilePath(kServiceProgramPath)), kProductRegPath,
-      kClsid, kIid);
+      kClsids, kIids);
   ASSERT_TRUE(item->Do());
 
   EXPECT_TRUE(IsServiceCorrectlyConfigured(item.get()));
@@ -200,7 +204,7 @@
   auto item_upgrade = std::make_unique<InstallServiceWorkItem>(
       kServiceName, kServiceDisplayName,
       base::CommandLine(base::FilePath(kServiceProgramPath)), kProductRegPath,
-      kClsid, kIid);
+      kClsids, kIids);
   EXPECT_TRUE(item_upgrade->Do());
 
   item_upgrade->Rollback();
@@ -213,7 +217,7 @@
   auto item = std::make_unique<InstallServiceWorkItem>(
       kServiceName, kServiceDisplayName,
       base::CommandLine(base::FilePath(kServiceProgramPath)), kProductRegPath,
-      kClsid, kIid);
+      kClsids, kIids);
   ASSERT_TRUE(item->Do());
 
   EXPECT_TRUE(IsServiceCorrectlyConfigured(item.get()));
@@ -222,7 +226,7 @@
   auto item_upgrade = std::make_unique<InstallServiceWorkItem>(
       kServiceName, kServiceDisplayName,
       base::CommandLine::FromString(L"NewCmd.exe arg1 arg2"), kProductRegPath,
-      kClsid, kIid);
+      kClsids, kIids);
   EXPECT_TRUE(item_upgrade->Do());
 
   item_upgrade->Rollback();
@@ -238,7 +242,7 @@
   auto item = std::make_unique<InstallServiceWorkItem>(
       kServiceName, kServiceDisplayName,
       base::CommandLine(base::FilePath(kServiceProgramPath)), kProductRegPath,
-      kClsid, kIid);
+      kClsids, kIids);
 
   EXPECT_STREQ(kServiceName,
                GetImpl(item.get())->GetCurrentServiceName().c_str());
diff --git a/chrome/renderer/resources/extensions/file_manager_private_custom_bindings.js b/chrome/renderer/resources/extensions/file_manager_private_custom_bindings.js
index 3e1d23f..69f1602 100644
--- a/chrome/renderer/resources/extensions/file_manager_private_custom_bindings.js
+++ b/chrome/renderer/resources/extensions/file_manager_private_custom_bindings.js
@@ -267,6 +267,14 @@
         });
         fileManagerPrivateInternal.sharesheetHasTargets(urls, callback);
       });
+
+  apiFunctions.setHandleRequest(
+      'invokeSharesheet', function(entries, callback) {
+        var urls = entries.map(function(entry) {
+          return getEntryURL(entry);
+        });
+        fileManagerPrivateInternal.invokeSharesheet(urls, callback);
+      });
 });
 
 bindingUtil.registerEventArgumentMassager(
diff --git a/chrome/services/machine_learning/BUILD.gn b/chrome/services/machine_learning/BUILD.gn
index 897ca2fc..fefc19f 100644
--- a/chrome/services/machine_learning/BUILD.gn
+++ b/chrome/services/machine_learning/BUILD.gn
@@ -48,6 +48,7 @@
     "public/cpp/decision_tree/decision_tree_prediction_model_unittest.cc",
     "public/cpp/decision_tree/prediction_model_unittest.cc",
     "public/cpp/decision_tree_model_unittest.cc",
+    "public/cpp/test_support/fake_service_connection_unittest.cc",
   ]
 
   if (build_with_tflite_lib) {
diff --git a/chrome/services/machine_learning/public/cpp/BUILD.gn b/chrome/services/machine_learning/public/cpp/BUILD.gn
index b9b5a25..cba969eb 100644
--- a/chrome/services/machine_learning/public/cpp/BUILD.gn
+++ b/chrome/services/machine_learning/public/cpp/BUILD.gn
@@ -39,11 +39,18 @@
 }
 
 source_set("test_support") {
-  public = [ "test_support/machine_learning_test_utils.h" ]
+  public = [
+    "test_support/fake_service_connection.h",
+    "test_support/machine_learning_test_utils.h",
+  ]
 
-  sources = [ "test_support/machine_learning_test_utils.cc" ]
+  sources = [
+    "test_support/fake_service_connection.cc",
+    "test_support/machine_learning_test_utils.cc",
+  ]
 
   public_deps = [
+    ":cpp",
     "//base",
     "//chrome/services/machine_learning/public/mojom",
     "//components/optimization_guide",
diff --git a/chrome/services/machine_learning/public/cpp/test_support/fake_service_connection.cc b/chrome/services/machine_learning/public/cpp/test_support/fake_service_connection.cc
new file mode 100644
index 0000000..67e9ef0
--- /dev/null
+++ b/chrome/services/machine_learning/public/cpp/test_support/fake_service_connection.cc
@@ -0,0 +1,137 @@
+// 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 "chrome/services/machine_learning/public/cpp/test_support/fake_service_connection.h"
+
+#include <string>
+#include <utility>
+#include "base/bind.h"
+#include "base/callback_forward.h"
+#include "base/containers/flat_map.h"
+#include "chrome/services/machine_learning/public/mojom/decision_tree.mojom.h"
+#include "chrome/services/machine_learning/public/mojom/machine_learning_service.mojom.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+
+namespace machine_learning {
+namespace testing {
+
+FakeServiceConnection::FakeServiceConnection() {
+  ServiceConnection::SetServiceConnectionForTesting(this);
+}
+
+FakeServiceConnection::~FakeServiceConnection() {
+  FakeServiceConnection::SetServiceConnectionForTesting(nullptr);
+}
+
+void FakeServiceConnection::ScheduleCall(base::OnceClosure callback) {
+  if (!is_async_) {
+    std::move(callback).Run();
+  }
+
+  pending_calls_.emplace_back(std::move(callback));
+}
+
+void FakeServiceConnection::RunScheduledCalls() {
+  for (auto& call : pending_calls_) {
+    std::move(call).Run();
+  }
+
+  pending_calls_.clear();
+}
+
+void FakeServiceConnection::SetLoadModelResult(mojom::LoadModelResult result) {
+  load_model_result_ = result;
+}
+
+void FakeServiceConnection::SetDecisionTreePredictionResult(
+    mojom::DecisionTreePredictionResult result,
+    double prediction_score) {
+  if (result == mojom::DecisionTreePredictionResult::kUnknown)
+    prediction_score = 0.0;
+
+  decision_tree_prediction_result_ = result;
+  decision_tree_prediction_score_ = prediction_score;
+}
+
+void FakeServiceConnection::SetAsyncModeForTesting(bool is_async) {
+  is_async_ = is_async;
+}
+
+bool FakeServiceConnection::is_service_running() const {
+  return is_service_running_;
+}
+
+mojom::MachineLearningService* FakeServiceConnection::GetService() {
+  if (!is_service_running_) {
+    is_service_running_ = true;
+  }
+
+  return this;
+}
+
+void FakeServiceConnection::ResetServiceForTesting() {
+  if (is_service_running_) {
+    is_service_running_ = false;
+    decision_tree_receivers_.Clear();
+    pending_calls_.clear();
+    load_model_result_ = mojom::LoadModelResult::kLoadModelError;
+    decision_tree_prediction_result_ =
+        mojom::DecisionTreePredictionResult::kUnknown;
+    decision_tree_prediction_score_ = 0.0;
+  }
+}
+
+void FakeServiceConnection::LoadDecisionTreeModel(
+    mojom::DecisionTreeModelSpecPtr spec,
+    mojo::PendingReceiver<mojom::DecisionTreePredictor> receiver,
+    LoadDecisionTreeCallback callback) {
+  GetService();
+  LoadDecisionTree(std::move(spec), std::move(receiver), std::move(callback));
+}
+
+void FakeServiceConnection::LoadDecisionTree(
+    mojom::DecisionTreeModelSpecPtr spec,
+    mojo::PendingReceiver<mojom::DecisionTreePredictor> receiver,
+    LoadDecisionTreeCallback callback) {
+  if (!is_service_running_)
+    return;
+
+  ScheduleCall(base::BindOnce(&FakeServiceConnection::HandleLoadDecisionTree,
+                              base::Unretained(this), std::move(receiver),
+                              std::move(callback)));
+}
+
+void FakeServiceConnection::HandleLoadDecisionTree(
+    mojo::PendingReceiver<mojom::DecisionTreePredictor> receiver,
+    LoadDecisionTreeCallback callback) {
+  if (!is_service_running_)
+    return;
+
+  if (load_model_result_ == mojom::LoadModelResult::kOk)
+    decision_tree_receivers_.Add(this, std::move(receiver));
+
+  std::move(callback).Run(load_model_result_);
+}
+
+void FakeServiceConnection::Predict(
+    const base::flat_map<std::string, float>& model_features,
+    PredictCallback callback) {
+  if (!is_service_running_)
+    return;
+
+  ScheduleCall(base::BindOnce(&FakeServiceConnection::HandleDecisionTreePredict,
+                              base::Unretained(this), std::move(callback)));
+}
+
+void FakeServiceConnection::HandleDecisionTreePredict(
+    PredictCallback callback) {
+  if (!is_service_running_)
+    return;
+
+  std::move(callback).Run(decision_tree_prediction_result_,
+                          decision_tree_prediction_score_);
+}
+
+}  // namespace testing
+}  // namespace machine_learning
diff --git a/chrome/services/machine_learning/public/cpp/test_support/fake_service_connection.h b/chrome/services/machine_learning/public/cpp/test_support/fake_service_connection.h
new file mode 100644
index 0000000..b4896c1c
--- /dev/null
+++ b/chrome/services/machine_learning/public/cpp/test_support/fake_service_connection.h
@@ -0,0 +1,100 @@
+// 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 CHROME_SERVICES_MACHINE_LEARNING_PUBLIC_CPP_TEST_SUPPORT_FAKE_SERVICE_CONNECTION_H_
+#define CHROME_SERVICES_MACHINE_LEARNING_PUBLIC_CPP_TEST_SUPPORT_FAKE_SERVICE_CONNECTION_H_
+
+#include <string>
+#include <vector>
+
+#include "base/callback_forward.h"
+#include "chrome/services/machine_learning/public/cpp/service_connection.h"
+#include "chrome/services/machine_learning/public/mojom/decision_tree.mojom.h"
+#include "chrome/services/machine_learning/public/mojom/machine_learning_service.mojom.h"
+#include "mojo/public/cpp/bindings/receiver_set.h"
+
+namespace machine_learning {
+namespace testing {
+
+// Fake implementation of machine_learning::ServiceConnection.
+// - Replaces the actual ServiceConnection singleton on initialization.
+// - Handles LoadDecisionTreeModel by returning the value specified by a
+//   previous call to SetLoadModelResults. Binds to itself if result is set to
+//   kOk.
+// - Handles DecisionTreePredictor::Predict by returning the values specified by
+//   a previous call to SetDecisionTreePredictionResult.
+class FakeServiceConnection : public ServiceConnection,
+                              public mojom::MachineLearningService,
+                              public mojom::DecisionTreePredictor {
+ public:
+  FakeServiceConnection();
+  ~FakeServiceConnection() override;
+
+  FakeServiceConnection(const FakeServiceConnection&) = delete;
+  FakeServiceConnection& operator=(const FakeServiceConnection&) = delete;
+
+  // Runs all scheduled callbacks and removes them from schedule.
+  void RunScheduledCalls();
+
+  // Sets the return value for model loading.
+  void SetLoadModelResult(mojom::LoadModelResult result);
+
+  // Sets the return value for decision tree prediction.
+  void SetDecisionTreePredictionResult(
+      mojom::DecisionTreePredictionResult result,
+      double prediction_score);
+
+  // Whether the service is running.
+  bool is_service_running() const;
+
+  // ServiceConnection implementations.
+  mojom::MachineLearningService* GetService() override;
+  void ResetServiceForTesting() override;
+  void LoadDecisionTreeModel(
+      mojom::DecisionTreeModelSpecPtr spec,
+      mojo::PendingReceiver<mojom::DecisionTreePredictor> receiver,
+      mojom::MachineLearningService::LoadDecisionTreeCallback callback)
+      override;
+
+  // Sets whether calls are handled async for testing purposes.
+  void SetAsyncModeForTesting(bool is_async);
+
+ private:
+  // Store |callback| to be run when RunScheduledCalls is invoked.
+  void ScheduleCall(base::OnceClosure callback);
+
+  // mojom::MachineLearningService implementations.
+  void LoadDecisionTree(
+      mojom::DecisionTreeModelSpecPtr spec,
+      mojo::PendingReceiver<mojom::DecisionTreePredictor> receiver,
+      LoadDecisionTreeCallback callback) override;
+
+  // Callback used to handle LoadDecisionTree calls.
+  void HandleLoadDecisionTree(
+      mojo::PendingReceiver<mojom::DecisionTreePredictor> receiver,
+      LoadDecisionTreeCallback callback);
+
+  // mojom::DecisionTreePredictor implementations.
+  void Predict(const base::flat_map<std::string, float>& model_features,
+               PredictCallback callback) override;
+
+  // Callback used to handle Predict calls.
+  void HandleDecisionTreePredict(PredictCallback callback);
+
+  bool is_service_running_ = false;
+  bool is_async_ = true;
+
+  mojo::ReceiverSet<mojom::DecisionTreePredictor> decision_tree_receivers_;
+  std::vector<base::OnceClosure> pending_calls_;
+  mojom::LoadModelResult load_model_result_ =
+      mojom::LoadModelResult::kLoadModelError;
+  mojom::DecisionTreePredictionResult decision_tree_prediction_result_ =
+      mojom::DecisionTreePredictionResult::kUnknown;
+  double decision_tree_prediction_score_ = 0.0;
+};
+
+}  // namespace testing
+}  // namespace machine_learning
+
+#endif  // CHROME_SERVICES_MACHINE_LEARNING_PUBLIC_CPP_TEST_SUPPORT_FAKE_SERVICE_CONNECTION_H_
diff --git a/chrome/services/machine_learning/public/cpp/test_support/fake_service_connection_unittest.cc b/chrome/services/machine_learning/public/cpp/test_support/fake_service_connection_unittest.cc
new file mode 100644
index 0000000..6f9e6ce8
--- /dev/null
+++ b/chrome/services/machine_learning/public/cpp/test_support/fake_service_connection_unittest.cc
@@ -0,0 +1,133 @@
+// 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 "chrome/services/machine_learning/public/cpp/test_support/fake_service_connection.h"
+
+#include <memory>
+#include <utility>
+
+#include "base/bind.h"
+#include "base/run_loop.h"
+#include "base/test/task_environment.h"
+#include "chrome/services/machine_learning/machine_learning_service.h"
+#include "chrome/services/machine_learning/public/cpp/test_support/machine_learning_test_utils.h"
+#include "chrome/services/machine_learning/public/mojom/decision_tree.mojom-shared.h"
+#include "chrome/services/machine_learning/public/mojom/decision_tree.mojom.h"
+#include "chrome/services/machine_learning/public/mojom/machine_learning_service.mojom-shared.h"
+#include "chrome/services/machine_learning/public/mojom/machine_learning_service.mojom.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "mojo/public/cpp/bindings/receiver.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace machine_learning {
+
+TEST(FakeServiceConnectionTest, MultipleLaunchesReusesSameService) {
+  testing::FakeServiceConnection service_connection;
+
+  ASSERT_EQ(&service_connection, ServiceConnection::GetInstance());
+
+  auto* service_ptr1 = service_connection.GetService();
+  EXPECT_TRUE(service_ptr1);
+  EXPECT_TRUE(service_connection.is_service_running());
+
+  auto* service_ptr2 = service_connection.GetService();
+  EXPECT_EQ(service_ptr1, service_ptr2);
+
+  service_connection.ResetServiceForTesting();
+  EXPECT_FALSE(service_connection.is_service_running());
+}
+
+TEST(FakeServiceConnectionTest, LoadInvalidDecisionTree) {
+  base::test::SingleThreadTaskEnvironment task_environment;
+  testing::FakeServiceConnection service_connection;
+
+  mojo::Remote<mojom::DecisionTreePredictor> predictor;
+  mojom::LoadModelResult result = mojom::LoadModelResult::kLoadModelError;
+
+  service_connection.LoadDecisionTreeModel(
+      mojom::DecisionTreeModelSpec::New("some model string"),
+      predictor.BindNewPipeAndPassReceiver(),
+      base::BindOnce([](mojom::LoadModelResult* p_result,
+                        mojom::LoadModelResult result) { *p_result = result; },
+                     &result));
+
+  service_connection.SetLoadModelResult(
+      mojom::LoadModelResult::kModelSpecError);
+  service_connection.RunScheduledCalls();
+
+  EXPECT_TRUE(service_connection.is_service_running());
+  EXPECT_EQ(mojom::LoadModelResult::kModelSpecError, result);
+
+  predictor.FlushForTesting();
+  // Invalid models doesn't lead to a predictor connection.
+  EXPECT_FALSE(predictor.is_connected());
+}
+
+TEST(FakeServiceConnectionTest, LoadValidDecisionTreeAndPredict) {
+  base::test::SingleThreadTaskEnvironment task_environment;
+  testing::FakeServiceConnection service_connection;
+
+  // Making an actual model for consistency even though we are mocking the
+  // result.
+  auto model_proto = testing::GetModelProtoForPredictionResult(
+      mojom::DecisionTreePredictionResult::kTrue);
+
+  mojo::Remote<mojom::DecisionTreePredictor> predictor;
+  mojom::LoadModelResult result = mojom::LoadModelResult::kLoadModelError;
+
+  service_connection.LoadDecisionTreeModel(
+      mojom::DecisionTreeModelSpec::New(model_proto->SerializeAsString()),
+      predictor.BindNewPipeAndPassReceiver(),
+      base::BindOnce([](mojom::LoadModelResult* p_result,
+                        mojom::LoadModelResult result) { *p_result = result; },
+                     &result));
+
+  service_connection.SetLoadModelResult(mojom::LoadModelResult::kOk);
+  service_connection.RunScheduledCalls();
+
+  EXPECT_TRUE(service_connection.is_service_running());
+  EXPECT_EQ(mojom::LoadModelResult::kOk, result);
+
+  predictor.FlushForTesting();
+  // Valid models leads to a predictor connection.
+  EXPECT_TRUE(predictor.is_connected());
+
+  // Normal prediction call.
+  const mojom::DecisionTreePredictionResult prediction_result_expected =
+      mojom::DecisionTreePredictionResult::kTrue;
+  const double prediction_score_expected = 2.33;
+
+  predictor->Predict(
+      {}, base::BindOnce(
+              [](mojom::DecisionTreePredictionResult result_expected,
+                 double score_expected,
+                 mojom::DecisionTreePredictionResult result, double score) {
+                EXPECT_EQ(result_expected, result);
+                EXPECT_EQ(score_expected, score);
+              },
+              prediction_result_expected, prediction_score_expected));
+  predictor.FlushForTesting();
+
+  service_connection.SetDecisionTreePredictionResult(prediction_result_expected,
+                                                     prediction_score_expected);
+  service_connection.RunScheduledCalls();
+
+  // Predictor launches a call but the service got disconnected.
+  predictor->Predict(
+      {}, base::BindOnce(
+              [](mojom::DecisionTreePredictionResult result, double score) {
+                // Callback should not run.
+                FAIL();
+              }));
+  predictor.FlushForTesting();
+
+  service_connection.ResetServiceForTesting();
+  service_connection.RunScheduledCalls();
+
+  predictor.FlushForTesting();
+  EXPECT_FALSE(predictor.is_connected());
+}
+
+}  // namespace machine_learning
diff --git a/chrome/services/sharing/public/mojom/sharing.mojom b/chrome/services/sharing/public/mojom/sharing.mojom
index e704b64..2f54ad1 100644
--- a/chrome/services/sharing/public/mojom/sharing.mojom
+++ b/chrome/services/sharing/public/mojom/sharing.mojom
@@ -14,8 +14,8 @@
 // and is used by the browser process to offload unsafe protocol parsing.
 interface Sharing {
   // Creates a new Sharing connection via WebRTC.
-  // Signalling of WebRTC is done OOB by the browser using the
-  // |signalling_sender| and |signalling_receiver| interfaces.
+  // Signaling of WebRTC is done OOB by the browser using the |signaling_sender|
+  // and |signalling_receiver| interfaces.
   // WebRTC messages to and from the browser process are handled via the
   // |delegate| and |connection| interfaces.
   // All network communication is handled by the |socket_manager| and
@@ -23,7 +23,7 @@
   // |ice_servers| contains the list of ICE servers to be used to establish a
   // connection.
   CreateSharingWebRtcConnection(
-      pending_remote<SignallingSender> signalling_sender,
+      pending_remote<SignalingSender> signaling_sender,
       pending_receiver<SignallingReceiver> signalling_receiver,
       pending_remote<SharingWebRtcConnectionDelegate> delegate,
       pending_receiver<SharingWebRtcConnection> connection,
diff --git a/chrome/services/sharing/public/mojom/webrtc.mojom b/chrome/services/sharing/public/mojom/webrtc.mojom
index 1124521d..2330119 100644
--- a/chrome/services/sharing/public/mojom/webrtc.mojom
+++ b/chrome/services/sharing/public/mojom/webrtc.mojom
@@ -7,7 +7,7 @@
 import "url/mojom/url.mojom";
 
 // Represents an ICE candidate as defined in RFC 5245. These will be exchanged
-// OOB via the SignallingSender and SignallingReceiver interfaces.
+// OOB via the SignalingSender and SignallingReceiver interfaces.
 struct IceCandidate {
   // Represents the remote address to connect to as defined in RFC 5245 # 15.1.
   string candidate;
@@ -42,9 +42,9 @@
   GetIceServers() => (array<IceServer> ice_servers);
 };
 
-// Signalling sender interface used to exchange offer / answer pairs and a list
+// Signaling sender interface used to exchange offer / answer pairs and a list
 // of ICE candidates OOB. Implemented in the browser process.
-interface SignallingSender {
+interface SignalingSender {
   // Sends an offer to the remote expecting an answer in response. The content
   // of both is in SDP format.
   SendOffer(string offer) => (string answer);
diff --git a/chrome/services/sharing/sharing_impl.cc b/chrome/services/sharing/sharing_impl.cc
index 21cdae8..98274014 100644
--- a/chrome/services/sharing/sharing_impl.cc
+++ b/chrome/services/sharing/sharing_impl.cc
@@ -23,7 +23,7 @@
 SharingImpl::~SharingImpl() = default;
 
 void SharingImpl::CreateSharingWebRtcConnection(
-    mojo::PendingRemote<mojom::SignallingSender> signalling_sender,
+    mojo::PendingRemote<mojom::SignalingSender> signaling_sender,
     mojo::PendingReceiver<mojom::SignallingReceiver> signalling_receiver,
     mojo::PendingRemote<mojom::SharingWebRtcConnectionDelegate> delegate,
     mojo::PendingReceiver<mojom::SharingWebRtcConnection> connection,
@@ -36,7 +36,7 @@
   // base::Unretained is safe as the |peer_connection| is owned by |this|.
   auto sharing_connection = std::make_unique<SharingWebRtcConnection>(
       webrtc_peer_connection_factory_.get(), std::move(ice_servers),
-      std::move(signalling_sender), std::move(signalling_receiver),
+      std::move(signaling_sender), std::move(signalling_receiver),
       std::move(delegate), std::move(connection), std::move(socket_manager),
       std::move(mdns_responder),
       base::BindOnce(&SharingImpl::SharingWebRtcConnectionDisconnected,
diff --git a/chrome/services/sharing/sharing_impl.h b/chrome/services/sharing/sharing_impl.h
index e148347..17764182f 100644
--- a/chrome/services/sharing/sharing_impl.h
+++ b/chrome/services/sharing/sharing_impl.h
@@ -53,7 +53,7 @@
 
   // mojom::Sharing:
   void CreateSharingWebRtcConnection(
-      mojo::PendingRemote<mojom::SignallingSender> signalling_sender,
+      mojo::PendingRemote<mojom::SignalingSender> signaling_sender,
       mojo::PendingReceiver<mojom::SignallingReceiver> signalling_receiver,
       mojo::PendingRemote<mojom::SharingWebRtcConnectionDelegate> delegate,
       mojo::PendingReceiver<mojom::SharingWebRtcConnection> connection,
diff --git a/chrome/services/sharing/sharing_impl_unittest.cc b/chrome/services/sharing/sharing_impl_unittest.cc
index 4b2db20b..0632638 100644
--- a/chrome/services/sharing/sharing_impl_unittest.cc
+++ b/chrome/services/sharing/sharing_impl_unittest.cc
@@ -41,7 +41,7 @@
   std::unique_ptr<MockSharingConnectionHost> CreateWebRtcConnection() {
     auto connection = std::make_unique<MockSharingConnectionHost>();
     service_->CreateSharingWebRtcConnection(
-        connection->signalling_sender.BindNewPipeAndPassRemote(),
+        connection->signaling_sender.BindNewPipeAndPassRemote(),
         connection->signalling_receiver.BindNewPipeAndPassReceiver(),
         connection->delegate.BindNewPipeAndPassRemote(),
         connection->connection.BindNewPipeAndPassReceiver(),
diff --git a/chrome/services/sharing/webrtc/sharing_webrtc_connection.cc b/chrome/services/sharing/webrtc/sharing_webrtc_connection.cc
index 7140265..080402c8 100644
--- a/chrome/services/sharing/webrtc/sharing_webrtc_connection.cc
+++ b/chrome/services/sharing/webrtc/sharing_webrtc_connection.cc
@@ -149,7 +149,7 @@
 SharingWebRtcConnection::SharingWebRtcConnection(
     webrtc::PeerConnectionFactoryInterface* connection_factory,
     const std::vector<mojom::IceServerPtr>& ice_servers,
-    mojo::PendingRemote<mojom::SignallingSender> signalling_sender,
+    mojo::PendingRemote<mojom::SignalingSender> signaling_sender,
     mojo::PendingReceiver<mojom::SignallingReceiver> signalling_receiver,
     mojo::PendingRemote<mojom::SharingWebRtcConnectionDelegate> delegate,
     mojo::PendingReceiver<mojom::SharingWebRtcConnection> connection,
@@ -157,7 +157,7 @@
     mojo::PendingRemote<network::mojom::MdnsResponder> mdns_responder,
     base::OnceCallback<void(SharingWebRtcConnection*)> on_disconnect)
     : signalling_receiver_(this, std::move(signalling_receiver)),
-      signalling_sender_(std::move(signalling_sender)),
+      signaling_sender_(std::move(signaling_sender)),
       connection_(this, std::move(connection)),
       delegate_(std::move(delegate)),
       p2p_socket_manager_(std::move(socket_manager)),
@@ -175,7 +175,7 @@
     rtc_config.servers.push_back(ice_turn_server);
   }
 
-  signalling_sender_.set_disconnect_handler(
+  signaling_sender_.set_disconnect_handler(
       base::BindOnce(&SharingWebRtcConnection::CloseConnection,
                      weak_ptr_factory_.GetWeakPtr()));
   delegate_.set_disconnect_handler(
@@ -559,7 +559,7 @@
 
   timing_recorder_.LogEvent(WebRtcTimingEvent::kOfferCreated);
 
-  signalling_sender_->SendOffer(
+  signaling_sender_->SendOffer(
       sdp, base::BindOnce(&SharingWebRtcConnection::OnAnswerReceived,
                           weak_ptr_factory_.GetWeakPtr()));
 }
@@ -641,7 +641,7 @@
     queued_messages_.pop();
   }
 
-  signalling_sender_.reset();
+  signaling_sender_.reset();
   delegate_.reset();
 
   // Close DataChannel if necessary.
@@ -718,7 +718,7 @@
   }
 
   if (!local_ice_candidates_.empty()) {
-    signalling_sender_->SendIceCandidates(std::move(local_ice_candidates_));
+    signaling_sender_->SendIceCandidates(std::move(local_ice_candidates_));
     local_ice_candidates_.clear();
   }
 }
diff --git a/chrome/services/sharing/webrtc/sharing_webrtc_connection.h b/chrome/services/sharing/webrtc/sharing_webrtc_connection.h
index 748011c..16de8e5 100644
--- a/chrome/services/sharing/webrtc/sharing_webrtc_connection.h
+++ b/chrome/services/sharing/webrtc/sharing_webrtc_connection.h
@@ -34,7 +34,7 @@
 
 class IpcPacketSocketFactory;
 
-// Manages a WebRTC PeerConnection. Signalling is handled via the passed sender
+// Manages a WebRTC PeerConnection. Signaling is handled via the passed sender
 // and receiver. All network communication is handled by the network service via
 // the passed P2PSocketManager and MdnsResponder pipes. All methods of this
 // class are called on the same thread and the PeerConnectionFactory is setup to
@@ -47,7 +47,7 @@
   SharingWebRtcConnection(
       webrtc::PeerConnectionFactoryInterface* connection_factory,
       const std::vector<mojom::IceServerPtr>& ice_servers,
-      mojo::PendingRemote<mojom::SignallingSender> signalling_sender,
+      mojo::PendingRemote<mojom::SignalingSender> signaling_sender,
       mojo::PendingReceiver<mojom::SignallingReceiver> signalling_receiver,
       mojo::PendingRemote<mojom::SharingWebRtcConnectionDelegate> delegate,
       mojo::PendingReceiver<mojom::SharingWebRtcConnection> connection,
@@ -132,7 +132,7 @@
       const rtc::scoped_refptr<const webrtc::RTCStatsReport>& report);
 
   mojo::Receiver<mojom::SignallingReceiver> signalling_receiver_;
-  mojo::Remote<mojom::SignallingSender> signalling_sender_;
+  mojo::Remote<mojom::SignalingSender> signaling_sender_;
   mojo::Receiver<mojom::SharingWebRtcConnection> connection_;
   mojo::Remote<mojom::SharingWebRtcConnectionDelegate> delegate_;
   mojo::Remote<network::mojom::P2PSocketManager> p2p_socket_manager_;
diff --git a/chrome/services/sharing/webrtc/sharing_webrtc_connection_integration_test.cc b/chrome/services/sharing/webrtc/sharing_webrtc_connection_integration_test.cc
index 11a82d2..6c39111f 100644
--- a/chrome/services/sharing/webrtc/sharing_webrtc_connection_integration_test.cc
+++ b/chrome/services/sharing/webrtc/sharing_webrtc_connection_integration_test.cc
@@ -35,7 +35,7 @@
       base::OnceCallback<void(sharing::SharingWebRtcConnection*)> on_disconnect)
       : connection_(pc_factory,
                     /*ice_servers=*/{},
-                    host_.signalling_sender.BindNewPipeAndPassRemote(),
+                    host_.signaling_sender.BindNewPipeAndPassRemote(),
                     host_.signalling_receiver.BindNewPipeAndPassReceiver(),
                     host_.delegate.BindNewPipeAndPassRemote(),
                     host_.connection.BindNewPipeAndPassReceiver(),
diff --git a/chrome/services/sharing/webrtc/sharing_webrtc_connection_unittest.cc b/chrome/services/sharing/webrtc/sharing_webrtc_connection_unittest.cc
index 77eb358..6ad65ef 100644
--- a/chrome/services/sharing/webrtc/sharing_webrtc_connection_unittest.cc
+++ b/chrome/services/sharing/webrtc/sharing_webrtc_connection_unittest.cc
@@ -173,7 +173,7 @@
 
     connection_ = std::make_unique<SharingWebRtcConnection>(
         mock_webrtc_pc_factory_.get(), std::vector<mojom::IceServerPtr>(),
-        connection_host_.signalling_sender.BindNewPipeAndPassRemote(),
+        connection_host_.signaling_sender.BindNewPipeAndPassRemote(),
         connection_host_.signalling_receiver.BindNewPipeAndPassReceiver(),
         connection_host_.delegate.BindNewPipeAndPassRemote(),
         connection_host_.connection.BindNewPipeAndPassReceiver(),
diff --git a/chrome/services/sharing/webrtc/test/mock_sharing_connection_host.h b/chrome/services/sharing/webrtc/test/mock_sharing_connection_host.h
index 8bda29d..e2c5fa0 100644
--- a/chrome/services/sharing/webrtc/test/mock_sharing_connection_host.h
+++ b/chrome/services/sharing/webrtc/test/mock_sharing_connection_host.h
@@ -14,7 +14,7 @@
 
 namespace sharing {
 
-class MockSharingConnectionHost : public mojom::SignallingSender,
+class MockSharingConnectionHost : public mojom::SignalingSender,
                                   public mojom::SharingWebRtcConnectionDelegate,
                                   public network::mojom::P2PSocketManager,
                                   public network::mojom::MdnsResponder {
@@ -25,7 +25,7 @@
       delete;
   ~MockSharingConnectionHost() override;
 
-  // mojom::SignallingSender:
+  // mojom::SignalingSender:
   MOCK_METHOD2(SendOffer, void(const std::string&, SendOfferCallback));
   MOCK_METHOD1(SendIceCandidates, void(std::vector<mojom::IceCandidatePtr>));
 
@@ -52,7 +52,7 @@
   MOCK_METHOD2(RemoveNameForAddress,
                void(const ::net::IPAddress&, RemoveNameForAddressCallback));
 
-  mojo::Receiver<mojom::SignallingSender> signalling_sender{this};
+  mojo::Receiver<mojom::SignalingSender> signaling_sender{this};
   mojo::Remote<mojom::SignallingReceiver> signalling_receiver;
   mojo::Receiver<mojom::SharingWebRtcConnectionDelegate> delegate{this};
   mojo::Remote<mojom::SharingWebRtcConnection> connection;
diff --git a/chrome/test/data/extensions/api_test/bindings/uncaught_exception_logging/test.js b/chrome/test/data/extensions/api_test/bindings/uncaught_exception_logging/test.js
index 58acb63..bfcdafd2 100644
--- a/chrome/test/data/extensions/api_test/bindings/uncaught_exception_logging/test.js
+++ b/chrome/test/data/extensions/api_test/bindings/uncaught_exception_logging/test.js
@@ -2,49 +2,49 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-function createTestFunction(expected_message) {
-  return function(tab) {
-    function onDebuggerEvent(debuggee, method, params) {
-      if (debuggee.tabId == tab.id && method == 'Runtime.exceptionThrown') {
-        var exception = params.exceptionDetails.exception;
-        if (exception.value.indexOf(expected_message) > -1) {
-          chrome.debugger.onEvent.removeListener(onDebuggerEvent);
-          chrome.test.succeed();
-        }
+function verifyException(expectedMessage, tabId) {
+  function onDebuggerEvent(debuggee, method, params) {
+    if (debuggee.tabId == tabId && method == 'Runtime.exceptionThrown') {
+      var exception = params.exceptionDetails.exception;
+      if (exception.value.indexOf(expectedMessage) > -1) {
+        chrome.debugger.onEvent.removeListener(onDebuggerEvent);
+        chrome.test.succeed();
       }
-    };
-    chrome.debugger.onEvent.addListener(onDebuggerEvent);
-    chrome.debugger.attach({ tabId: tab.id }, "1.1", function() {
-      // Enabling console provides both stored and new messages via the
-      // Console.messageAdded event.
-      chrome.debugger.sendCommand({ tabId: tab.id }, "Runtime.enable");
-    });
-  }
+    }
+  };
+  chrome.debugger.onEvent.addListener(onDebuggerEvent);
+  chrome.debugger.attach({ tabId: tabId }, "1.1", function() {
+    // Enabling console provides both stored and new messages via the
+    // Console.messageAdded event.
+    chrome.debugger.sendCommand({ tabId: tabId }, "Runtime.enable");
+  });
 }
 
+let openTab;
+
 chrome.test.runTests([
-  function testExceptionInExtensionPage() {
-    chrome.tabs.create(
-        {url: chrome.runtime.getURL('extension_page.html')},
-        createTestFunction('Exception thrown in extension page.'));
+  async function testExceptionInExtensionPage() {
+    ({openTab} = await import('/_test_resources/test_util/tabs_util.js'));
+    const tab = await openTab(chrome.runtime.getURL('extension_page.html'));
+    verifyException('Exception thrown in extension page.', tab.id);
   },
 
-  function testExceptionInInjectedScript() {
+  async function testExceptionInInjectedScript() {
     function injectScriptAndSendMessage(tab) {
       chrome.tabs.executeScript(
           tab.id,
           { file: 'content_script.js' },
           function() {
-            createTestFunction('Exception thrown in injected script.')(tab);
+            verifyException('Exception thrown in injected script.', tab.id);
           });
     }
 
-    chrome.test.getConfig(function(config) {
-      var test_url =
-          'http://localhost:PORT/extensions/test_file.html'
-              .replace(/PORT/, config.testServer.port);
-
-      chrome.tabs.create({ url: test_url }, injectScriptAndSendMessage);
+    chrome.test.getConfig(async function(config) {
+      const testUrl =
+          `http://localhost:${config.testServer.port}/` +
+          'extensions/test_file.html';
+      const tab = await openTab(testUrl);
+      injectScriptAndSendMessage(tab);
     });
   }
 ]);
diff --git a/chrome/test/data/extensions/api_test/debugger/background.js b/chrome/test/data/extensions/api_test/debugger/background.js
index 49ec84f..7675e92b 100644
--- a/chrome/test/data/extensions/api_test/debugger/background.js
+++ b/chrome/test/data/extensions/api_test/debugger/background.js
@@ -16,6 +16,8 @@
     "'silent-debugger-extension-api' flag is enabled.";
 var DETACHED_WHILE_HANDLING = "Detached while handling command.";
 
+let openTab;
+
 chrome.test.getConfig(config => chrome.test.runTests([
 
   function attachMalformedVersion() {
@@ -92,86 +94,83 @@
         fail("Debugger is not attached to the tab with id: " + tabId + "."));
   },
 
-  function closeTab() {
-    chrome.tabs.create({url:"inspected.html"}, function(tab) {
-      function onDetach(debuggee, reason) {
-        chrome.test.assertEq(tab.id, debuggee.tabId);
-        chrome.test.assertEq("target_closed", reason);
-        chrome.debugger.onDetach.removeListener(onDetach);
-        chrome.test.succeed();
-      }
+  async function closeTab() {
+    ({openTab} = await import('/_test_resources/test_util/tabs_util.js'));
+    const tab = await openTab(chrome.runtime.getURL('inspected.html'));
+    function onDetach(debuggee, reason) {
+      chrome.test.assertEq(tab.id, debuggee.tabId);
+      chrome.test.assertEq("target_closed", reason);
+      chrome.debugger.onDetach.removeListener(onDetach);
+      chrome.test.succeed();
+    }
 
-      var debuggee2 = {tabId: tab.id};
-      chrome.debugger.attach(debuggee2, protocolVersion, function() {
-        chrome.debugger.onDetach.addListener(onDetach);
-        chrome.tabs.remove(tab.id);
-      });
-    });
-  },
-
-  function attachToWebUI() {
-    chrome.tabs.create({url:"chrome://version"}, function(tab) {
-      var debuggee = {tabId: tab.id};
-      chrome.debugger.attach(debuggee, protocolVersion,
-          fail("Cannot access a chrome:// URL"));
+    const debuggee2 = {tabId: tab.id};
+    chrome.debugger.attach(debuggee2, protocolVersion, function() {
+      chrome.debugger.onDetach.addListener(onDetach);
       chrome.tabs.remove(tab.id);
     });
   },
 
-  function navigateToWebUI() {
-    chrome.tabs.create({url:"inspected.html"}, function(tab) {
-      var debuggee = {tabId: tab.id};
-      chrome.debugger.attach(debuggee, protocolVersion, function() {
-        var responded = false;
+  async function attachToWebUI() {
+    const tab = await openTab('chrome://version');
+    const debuggee = {tabId: tab.id};
+    chrome.debugger.attach(debuggee, protocolVersion,
+        fail("Cannot access a chrome:// URL"));
+    chrome.tabs.remove(tab.id);
+  },
 
-        function onResponse() {
-          chrome.test.assertLastError(DETACHED_WHILE_HANDLING);
-          responded = true;
-        }
+  async function navigateToWebUI() {
+    const tab = await openTab(chrome.runtime.getURL('inspected.html'));
+    const debuggee = {tabId: tab.id};
+    chrome.debugger.attach(debuggee, protocolVersion, function() {
+      var responded = false;
 
-        function onDetach(from, reason) {
-          chrome.debugger.onDetach.removeListener(onDetach);
-          chrome.test.assertTrue(responded);
-          chrome.test.assertEq(debuggee.tabId, from.tabId);
-          chrome.test.assertEq("target_closed", reason);
-          chrome.tabs.remove(tab.id, function() {
-            chrome.test.assertNoLastError();
-            chrome.test.succeed();
-          });
-        }
+      function onResponse() {
+        chrome.test.assertLastError(DETACHED_WHILE_HANDLING);
+        responded = true;
+      }
 
-        chrome.test.assertNoLastError();
-        chrome.debugger.onDetach.addListener(onDetach);
-        chrome.debugger.sendCommand(
-          debuggee, "Page.navigate", {url: "chrome://version"}, onResponse);
-      });
+      function onDetach(from, reason) {
+        chrome.debugger.onDetach.removeListener(onDetach);
+        chrome.test.assertTrue(responded);
+        chrome.test.assertEq(debuggee.tabId, from.tabId);
+        chrome.test.assertEq("target_closed", reason);
+        chrome.tabs.remove(tab.id, function() {
+          chrome.test.assertNoLastError();
+          chrome.test.succeed();
+        });
+      }
+
+      chrome.test.assertNoLastError();
+      chrome.debugger.onDetach.addListener(onDetach);
+      chrome.debugger.sendCommand(
+        debuggee, "Page.navigate", {url: "chrome://version"}, onResponse);
     });
   },
 
-  function detachDuringCommand() {
-    chrome.tabs.create({url:"inspected.html"}, function(tab) {
-      var debuggee = {tabId: tab.id};
-      chrome.debugger.attach(debuggee, protocolVersion, function() {
-        var responded = false;
+  async function detachDuringCommand() {
+    const tab = await openTab(chrome.runtime.getURL('inspected.html'));
+    const debuggee = {tabId: tab.id};
+    chrome.debugger.attach(debuggee, protocolVersion, function() {
+      var responded = false;
 
-        function onResponse() {
-          chrome.test.assertLastError(DETACHED_WHILE_HANDLING);
-          responded = true;
-        }
+      function onResponse() {
+        chrome.test.assertLastError(DETACHED_WHILE_HANDLING);
+        responded = true;
+      }
 
-        function onDetach() {
-          chrome.debugger.onDetach.removeListener(onDetach);
-          chrome.test.assertTrue(responded);
-          chrome.tabs.remove(tab.id, function() {
-            chrome.test.assertNoLastError();
-            chrome.test.succeed();
-          });
-        }
+      function onDetach() {
+        chrome.debugger.onDetach.removeListener(onDetach);
+        chrome.test.assertTrue(responded);
+        chrome.tabs.remove(tab.id, function() {
+          chrome.test.assertNoLastError();
+          chrome.test.succeed();
+        });
+      }
 
-        chrome.test.assertNoLastError();
-        chrome.debugger.sendCommand(debuggee, "command", null, onResponse);
-        chrome.debugger.detach(debuggee, onDetach);
-      });
+      chrome.test.assertNoLastError();
+      chrome.debugger.sendCommand(debuggee, "command", null, onResponse);
+      chrome.debugger.detach(debuggee, onDetach);
     });
   },
 
@@ -205,28 +204,22 @@
     chrome.debugger.detach(debuggee, pass());
   },
 
-  function createAndDiscoverTab() {
-    function onUpdated(tabId, changeInfo) {
-      if (changeInfo.status == 'loading')
-        return;
-      chrome.tabs.onUpdated.removeListener(onUpdated);
-      chrome.debugger.getTargets(function(targets) {
-        var page = targets.filter(
-            function(t) {
-              return t.type == 'page' &&
-                     t.tabId == tabId &&
-                     t.title == 'Test page';
-            })[0];
-        if (page) {
-          chrome.debugger.attach(
-              {targetId: page.id}, protocolVersion, pass());
-        } else {
-          chrome.test.fail("Cannot discover a newly created tab");
-        }
-      });
-    }
-    chrome.tabs.onUpdated.addListener(onUpdated);
-    chrome.tabs.create({url: "inspected.html"});
+  async function createAndDiscoverTab() {
+    const tab = await openTab(chrome.runtime.getURL('inspected.html'));
+    chrome.debugger.getTargets(function(targets) {
+      var page = targets.filter(
+          function(t) {
+            return t.type == 'page' &&
+                   t.tabId == tab.id &&
+                   t.title == 'Test page';
+          })[0];
+      if (page) {
+        chrome.debugger.attach(
+            {targetId: page.id}, protocolVersion, pass());
+      } else {
+        chrome.test.fail("Cannot discover a newly created tab");
+      }
+    });
   },
 
   function discoverWorker() {
@@ -250,56 +243,54 @@
     chrome.debugger.detach(debuggee, pass());
   },
 
-  function sendCommandDuringNavigation() {
-    chrome.tabs.create({url:"inspected.html"}, function(tab) {
-      var debuggee = {tabId: tab.id};
+  async function sendCommandDuringNavigation() {
+    const tab = await openTab(chrome.runtime.getURL('inspected.html'));
+    const debuggee = {tabId: tab.id};
 
-      function checkError() {
-        if (chrome.runtime.lastError) {
-          chrome.test.fail(chrome.runtime.lastError.message);
-        } else {
-          chrome.tabs.remove(tab.id);
-          chrome.test.succeed();
-        }
+    function checkError() {
+      if (chrome.runtime.lastError) {
+        chrome.test.fail(chrome.runtime.lastError.message);
+      } else {
+        chrome.tabs.remove(tab.id);
+        chrome.test.succeed();
       }
+    }
 
-      function onNavigateDone() {
-        chrome.debugger.sendCommand(debuggee, "Page.disable", null, checkError);
-      }
+    function onNavigateDone() {
+      chrome.debugger.sendCommand(debuggee, "Page.disable", null, checkError);
+    }
 
-      function onAttach() {
-        chrome.debugger.sendCommand(debuggee, "Page.enable");
-        chrome.debugger.sendCommand(
-            debuggee, "Page.navigate", {url:"about:blank"}, onNavigateDone);
-      }
+    function onAttach() {
+      chrome.debugger.sendCommand(debuggee, "Page.enable");
+      chrome.debugger.sendCommand(
+          debuggee, "Page.navigate", {url:"about:blank"}, onNavigateDone);
+    }
 
-      chrome.debugger.attach(debuggee, protocolVersion, onAttach);
-    });
+    chrome.debugger.attach(debuggee, protocolVersion, onAttach);
   },
 
-  function sendCommandToDataUri() {
-    chrome.tabs.create({url:"data:text/html,<h1>hi</h1>"}, function(tab) {
-      var debuggee = {tabId: tab.id};
+  async function sendCommandToDataUri() {
+    const tab = await openTab('data:text/html,<h1>hi</h1>');
+    const debuggee = {tabId: tab.id};
 
-      function checkError() {
-        if (chrome.runtime.lastError) {
-          chrome.test.fail(chrome.runtime.lastError.message);
-        } else {
-          chrome.tabs.remove(tab.id);
-          chrome.test.succeed();
-        }
+    function checkError() {
+      if (chrome.runtime.lastError) {
+        chrome.test.fail(chrome.runtime.lastError.message);
+      } else {
+        chrome.tabs.remove(tab.id);
+        chrome.test.succeed();
       }
+    }
 
-      function onAttach() {
-        chrome.debugger.sendCommand(debuggee, "Page.enable", null, checkError);
-      }
+    function onAttach() {
+      chrome.debugger.sendCommand(debuggee, "Page.enable", null, checkError);
+    }
 
-      chrome.debugger.attach(debuggee, protocolVersion, onAttach);
-    });
+    chrome.debugger.attach(debuggee, protocolVersion, onAttach);
   },
 
   // http://crbug.com/824174
-  function getResponseBodyInvalidChar() {
+  async function getResponseBodyInvalidChar() {
     let requestId;
 
     function onEvent(debuggeeId, message, params) {
@@ -319,105 +310,103 @@
     }
 
     chrome.debugger.onEvent.addListener(onEvent);
-    chrome.tabs.create({url: 'inspected.html'}, function(tab) {
-      const debuggee = {tabId: tab.id};
-      chrome.debugger.attach(debuggee, protocolVersion, function() {
-        chrome.debugger.sendCommand(
-            debuggee, 'Network.enable', null, function() {
-              chrome.debugger.sendCommand(
-                  debuggee, 'Page.enable', null, function() {
-                    // Navigate to a new page after attaching so we don't miss
-                    // any protocol events that we might have missed while
-                    // attaching to the first page.
-                    chrome.debugger.sendCommand(
-                        debuggee, 'Page.navigate',
-                        {url: window.location.origin + '/fetch.html'});
-                  });
-            });
-      });
+    const tab = await openTab(chrome.runtime.getURL('inspected.html'));
+    const debuggee = {tabId: tab.id};
+    chrome.debugger.attach(debuggee, protocolVersion, function() {
+      chrome.debugger.sendCommand(
+          debuggee, 'Network.enable', null, function() {
+            chrome.debugger.sendCommand(
+                debuggee, 'Page.enable', null, function() {
+                  // Navigate to a new page after attaching so we don't miss
+                  // any protocol events that we might have missed while
+                  // attaching to the first page.
+                  chrome.debugger.sendCommand(
+                      debuggee, 'Page.navigate',
+                      {url: window.location.origin + '/fetch.html'});
+                });
+          });
     });
   },
 
-  function offlineErrorPage() {
+  async function offlineErrorPage() {
     const url = 'http://127.0.0.1//extensions/api_test/debugger/inspected.html';
-    chrome.tabs.create({url: url}, function(tab) {
-      var debuggee = {tabId: tab.id};
-      var finished = false;
-      var failure = '';
-      var expectingFrameNavigated = false;
+    const tab = await openTab(url);
+    const debuggee = {tabId: tab.id};
+    var finished = false;
+    var failure = '';
+    var expectingFrameNavigated = false;
 
-      function finishIfError() {
-        if (chrome.runtime.lastError) {
-          failure = chrome.runtime.lastError.message;
-          finish(true);
-          return true;
-        }
-        return false;
-      }
-
-      function onAttach() {
-        chrome.debugger.sendCommand(debuggee, 'Network.enable', null,
-            finishIfError);
-        chrome.debugger.sendCommand(debuggee, 'Page.enable', null,
-            finishIfError);
-        var offlineParams = { offline: true, latency: 0,
-            downloadThroughput: 0, uploadThroughput: 0 };
-        chrome.debugger.sendCommand(debuggee,
-            'Network.emulateNetworkConditions',
-            offlineParams, onOffline);
-      }
-
-      function onOffline() {
-        if (finishIfError())
-          return;
-        expectingFrameNavigated = true;
-        chrome.debugger.sendCommand(debuggee, 'Page.reload', null,
-            finishIfError);
-      }
-
-      function finish(detach) {
-        if (finished)
-          return;
-        finished = true;
-        chrome.debugger.onDetach.removeListener(onDetach);
-        chrome.debugger.onEvent.removeListener(onEvent);
-        if (detach)
-          chrome.debugger.detach(debuggee);
-        chrome.tabs.remove(tab.id, () => {
-          if (failure)
-            chrome.test.fail(failure);
-          else
-            chrome.test.succeed();
-        });
-      }
-
-      function onDetach() {
-        failure = 'Detached before navigated to error page';
-        finish(false);
-      }
-
-      function onEvent(_, method, params) {
-        if (!expectingFrameNavigated || method !== 'Page.frameNavigated')
-          return;
-
-        if (finishIfError())
-          return;
-
-        expectingFrameNavigated = false;
-        chrome.debugger.sendCommand(
-            debuggee, 'Page.navigate', {url: 'about:blank'}, onNavigateDone);
-      }
-
-      function onNavigateDone() {
-        if (finishIfError())
-          return;
+    function finishIfError() {
+      if (chrome.runtime.lastError) {
+        failure = chrome.runtime.lastError.message;
         finish(true);
+        return true;
       }
+      return false;
+    }
 
-      chrome.debugger.onDetach.addListener(onDetach);
-      chrome.debugger.onEvent.addListener(onEvent);
-      chrome.debugger.attach(debuggee, protocolVersion, onAttach);
-    });
+    function onAttach() {
+      chrome.debugger.sendCommand(debuggee, 'Network.enable', null,
+          finishIfError);
+      chrome.debugger.sendCommand(debuggee, 'Page.enable', null,
+          finishIfError);
+      var offlineParams = { offline: true, latency: 0,
+          downloadThroughput: 0, uploadThroughput: 0 };
+      chrome.debugger.sendCommand(debuggee,
+          'Network.emulateNetworkConditions',
+          offlineParams, onOffline);
+    }
+
+    function onOffline() {
+      if (finishIfError())
+        return;
+      expectingFrameNavigated = true;
+      chrome.debugger.sendCommand(debuggee, 'Page.reload', null,
+          finishIfError);
+    }
+
+    function finish(detach) {
+      if (finished)
+        return;
+      finished = true;
+      chrome.debugger.onDetach.removeListener(onDetach);
+      chrome.debugger.onEvent.removeListener(onEvent);
+      if (detach)
+        chrome.debugger.detach(debuggee);
+      chrome.tabs.remove(tab.id, () => {
+        if (failure)
+          chrome.test.fail(failure);
+        else
+          chrome.test.succeed();
+      });
+    }
+
+    function onDetach() {
+      failure = 'Detached before navigated to error page';
+      finish(false);
+    }
+
+    function onEvent(_, method, params) {
+      if (!expectingFrameNavigated || method !== 'Page.frameNavigated')
+        return;
+
+      if (finishIfError())
+        return;
+
+      expectingFrameNavigated = false;
+      chrome.debugger.sendCommand(
+          debuggee, 'Page.navigate', {url: 'about:blank'}, onNavigateDone);
+    }
+
+    function onNavigateDone() {
+      if (finishIfError())
+        return;
+      finish(true);
+    }
+
+    chrome.debugger.onDetach.addListener(onDetach);
+    chrome.debugger.onEvent.addListener(onEvent);
+    chrome.debugger.attach(debuggee, protocolVersion, onAttach);
   },
 
   function autoAttachToOOPIF() {
diff --git a/chrome/test/data/extensions/api_test/debugger_extension/background.js b/chrome/test/data/extensions/api_test/debugger_extension/background.js
index 318d6dae..6f9bb7dc 100644
--- a/chrome/test/data/extensions/api_test/debugger_extension/background.js
+++ b/chrome/test/data/extensions/api_test/debugger_extension/background.js
@@ -10,13 +10,13 @@
 
 chrome.test.runTests([
 
-  function attachToWebUI() {
-    chrome.tabs.create({url:"chrome://version"}, function(tab) {
-      var debuggee = {tabId: tab.id};
-      chrome.debugger.attach(debuggee, protocolVersion,
-          fail("Cannot attach to this target."));
-      chrome.tabs.remove(tab.id);
-    });
+  async function attachToWebUI() {
+    const {openTab} = await import('/_test_resources/test_util/tabs_util.js');
+    const tab = await openTab('chrome://version');
+    const debuggee = {tabId: tab.id};
+    chrome.debugger.attach(debuggee, protocolVersion,
+                           fail("Cannot attach to this target."));
+    chrome.tabs.remove(tab.id);
   },
 
   function attach() {
diff --git a/chrome/test/data/extensions/api_test/debugger_file_access/background.js b/chrome/test/data/extensions/api_test/debugger_file_access/background.js
index ddf0eee..bebf2eb 100644
--- a/chrome/test/data/extensions/api_test/debugger_file_access/background.js
+++ b/chrome/test/data/extensions/api_test/debugger_file_access/background.js
@@ -10,29 +10,30 @@
                        new URL(actual).href);
 }
 
-function runNotAllowedTest(method, params, expectAllowed) {
-  const NOT_ALLOWED = "Not allowed";
-  chrome.tabs.create({url: 'dummy.html'}, function(tab) {
-    var debuggee = {tabId: tab.id};
-    chrome.debugger.attach(debuggee, '1.2', function() {
-      chrome.test.assertNoLastError();
-      chrome.debugger.sendCommand(debuggee, method, params, onResponse);
+let openTab;
 
-      function onResponse() {
-        var message;
-        try {
-          message = JSON.parse(chrome.runtime.lastError.message).message;
-        } catch (e) {
-        }
-        chrome.debugger.detach(debuggee, () => {
-          const allowed = message !== NOT_ALLOWED;
-          if (allowed === expectAllowed)
-            chrome.test.succeed();
-          else
-            chrome.test.fail('' + message);
-        });
+async function runNotAllowedTest(method, params, expectAllowed) {
+  const NOT_ALLOWED = "Not allowed";
+  const tab = await openTab(chrome.runtime.getURL('dummy.html'));
+  const debuggee = {tabId: tab.id};
+  chrome.debugger.attach(debuggee, '1.2', function() {
+    chrome.test.assertNoLastError();
+    chrome.debugger.sendCommand(debuggee, method, params, onResponse);
+
+    function onResponse() {
+      var message;
+      try {
+        message = JSON.parse(chrome.runtime.lastError.message).message;
+      } catch (e) {
       }
-    });
+      chrome.debugger.detach(debuggee, () => {
+        const allowed = message !== NOT_ALLOWED;
+        if (allowed === expectAllowed)
+          chrome.test.succeed();
+        else
+          chrome.test.fail('' + message);
+      });
+    }
   });
 }
 
@@ -42,7 +43,8 @@
                  });
   const fileUrl = config.testDataDirectory + '/../body1.html';
   const expectFileAccess = !!config.customArg;
-  const { openTab } = await import('/_test_resources/test_util/tabs_util.js');
+
+  ({ openTab } = await import('/_test_resources/test_util/tabs_util.js'));
 
   console.log(fileUrl);
 
diff --git a/chrome/test/data/extensions/api_test/debugger_inspect_worker/background.js b/chrome/test/data/extensions/api_test/debugger_inspect_worker/background.js
index c104c77..6fc91d0 100644
--- a/chrome/test/data/extensions/api_test/debugger_inspect_worker/background.js
+++ b/chrome/test/data/extensions/api_test/debugger_inspect_worker/background.js
@@ -6,8 +6,8 @@
 
 chrome.test.getConfig(config => chrome.test.runTests([
   async function testInspectWorkerForbidden() {
-    const tab = await new Promise(resolve =>
-        chrome.tabs.create({url: config.customArg}, resolve));
+    const {openTab} = await import('/_test_resources/test_util/tabs_util.js');
+    const tab = await openTab(config.customArg);
     const debuggee = {tabId: tab.id};
     await new Promise(resolve =>
         chrome.debugger.attach(debuggee, protocolVersion, resolve));
diff --git a/chrome/test/data/extensions/api_test/tabs/basics/remove-multiple.html b/chrome/test/data/extensions/api_test/tabs/basics/remove-multiple.html
new file mode 100644
index 0000000..c112ae4
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/tabs/basics/remove-multiple.html
@@ -0,0 +1,8 @@
+<!--
+ * 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.
+-->
+
+<script src="tabs_util.js"></script>
+<script src="remove-multiple.js"></script>
diff --git a/chrome/test/data/extensions/api_test/tabs/basics/remove-multiple.js b/chrome/test/data/extensions/api_test/tabs/basics/remove-multiple.js
new file mode 100644
index 0000000..08786ae
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/tabs/basics/remove-multiple.js
@@ -0,0 +1,74 @@
+// 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.
+
+var firstTabId;
+var secondTabId;
+var thirdTabId;
+var fourthTabId;
+
+function createTab(createParams) {
+  return new Promise((resolve) => {
+    chrome.tabs.create(createParams, (tab) => {
+      var createdTabId = tab.id;
+      chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
+        // Wait for the tab to finish loading.
+        if (tabId == createdTabId && changeInfo.status == 'complete') {
+          resolve(tab);
+        }
+      });
+    });
+  });
+}
+
+chrome.test.runTests([
+  function getFirstTabId() {
+    chrome.tabs.query({ windowId: chrome.windows.WINDOW_ID_CURRENT },
+      (tabs) => {
+        // Make sure we start the test with one tab.
+        assertEq(1, tabs.length);
+        firstTabId = tabs[0].id;
+        chrome.test.succeed();
+    })
+  },
+  function createTabs() {
+    // Create a second tab that has an unload handler.
+    createTab({index: 1, active: false, url: 'unload-storage-1.html'})
+      .then((tab) => {
+        secondTabId = tab.id;
+        assertFalse(tab.active);
+        assertEq(1, tab.index);
+        // Create and switch to a third tab that has an unload handler.
+        return createTab(
+          {index: 2, active: true, url: 'unload-storage-2.html'});
+      }).then((tab) => {
+        thirdTabId = tab.id;
+        assertTrue(tab.active);
+        assertEq(2, tab.index);
+        // Create a fourth tab that does not have an unload handler (it will
+        // open the default New Tab Page).
+        return createTab({index: 3, active: false });
+      }).then((tab) => {
+        fourthTabId = tab.id;
+        assertFalse(tab.active);
+        assertEq(3, tab.index);
+        chrome.test.succeed();
+      });
+  },
+  function removeCreatedTabs() {
+    chrome.tabs.remove([secondTabId, thirdTabId, fourthTabId], () => {
+      // The tabs should've set the 'did_run_unload_1' and
+      // 'did_run_unload_2' values to 'yes' from their unload handler,
+      //  which are accessible from the first tab.
+      assertEq('yes', localStorage.getItem('did_run_unload_1'));
+      assertEq('yes', localStorage.getItem('did_run_unload_2'));
+      chrome.tabs.query({ windowId: chrome.windows.WINDOW_ID_CURRENT },
+        (tabs) => {
+          // Make sure we only have one tab left (the first tab) in the window.
+          assertEq(1, tabs.length);
+          assertEq(firstTabId, tabs[0].id);
+          chrome.test.succeed();
+      });
+    });
+  }
+]);
diff --git a/chrome/test/data/extensions/api_test/tabs/basics/remove.html b/chrome/test/data/extensions/api_test/tabs/basics/remove.html
new file mode 100644
index 0000000..d4daf0d0
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/tabs/basics/remove.html
@@ -0,0 +1,8 @@
+<!--
+ * 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.
+-->
+
+<script src="tabs_util.js"></script>
+<script src="remove.js"></script>
diff --git a/chrome/test/data/extensions/api_test/tabs/basics/remove.js b/chrome/test/data/extensions/api_test/tabs/basics/remove.js
new file mode 100644
index 0000000..ed1486f8
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/tabs/basics/remove.js
@@ -0,0 +1,30 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+var secondTabId;
+chrome.test.runTests([
+  function createSecondTab() {
+    // Create and switch to a second tab that has an unload handler.
+    chrome.tabs.create({index: 1, active: true, url: 'unload-storage-1.html'},
+      (tab) => {
+        secondTabId = tab.id;
+        assertTrue(tab.active);
+        assertEq(1, tab.index);
+        chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
+          // Wait for the second tab to finish loading before moving on.
+          if (tabId == secondTabId && changeInfo.status == 'complete') {
+            chrome.test.succeed();
+          }
+        });
+      });
+  },
+  function removeSecondTab() {
+    chrome.tabs.remove(secondTabId, () => {
+      // The second tab should've set the 'did_run_unload_1' value from
+      // its unload handler, which is accessible from the first tab too.
+      assertEq('yes', localStorage.getItem('did_run_unload_1'));
+      chrome.test.succeed();
+    });
+  }
+]);
diff --git a/chrome/test/data/extensions/api_test/tabs/basics/unload-storage-1.html b/chrome/test/data/extensions/api_test/tabs/basics/unload-storage-1.html
new file mode 100644
index 0000000..369137a4
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/tabs/basics/unload-storage-1.html
@@ -0,0 +1,10 @@
+<!--
+ * 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.
+-->
+
+<script src="unload-storage-1.js"></script>
+<body>
+  Page with unload handler that stores {"did_run_unload_1": "yes"}.
+</body>
diff --git a/chrome/test/data/extensions/api_test/tabs/basics/unload-storage-1.js b/chrome/test/data/extensions/api_test/tabs/basics/unload-storage-1.js
new file mode 100644
index 0000000..5e6c804e
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/tabs/basics/unload-storage-1.js
@@ -0,0 +1,7 @@
+// 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.
+
+window.addEventListener('unload', (e) => {
+  localStorage.setItem('did_run_unload_1', 'yes');
+});
diff --git a/chrome/test/data/extensions/api_test/tabs/basics/unload-storage-2.html b/chrome/test/data/extensions/api_test/tabs/basics/unload-storage-2.html
new file mode 100644
index 0000000..d20fd9f
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/tabs/basics/unload-storage-2.html
@@ -0,0 +1,10 @@
+<!--
+ * 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.
+-->
+
+<script src="unload-storage-2.js"></script>
+<body>
+  Page with unload handler that stores {"did_run_unload_2": "yes"}.
+</body>
diff --git a/chrome/test/data/extensions/api_test/tabs/basics/unload-storage-2.js b/chrome/test/data/extensions/api_test/tabs/basics/unload-storage-2.js
new file mode 100644
index 0000000..e918ced
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/tabs/basics/unload-storage-2.js
@@ -0,0 +1,7 @@
+// 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.
+
+window.addEventListener('unload', (e) => {
+  localStorage.setItem('did_run_unload_2', 'yes');
+});
diff --git a/chrome/test/data/webui/BUILD.gn b/chrome/test/data/webui/BUILD.gn
index 447aa94..42f4524a 100644
--- a/chrome/test/data/webui/BUILD.gn
+++ b/chrome/test/data/webui/BUILD.gn
@@ -262,6 +262,7 @@
         "$root_gen_dir/chrome/test/data/webui/settings/chromeos/test_multidevice_browser_proxy.m.js",
         "$root_gen_dir/chrome/test/data/webui/settings/chromeos/personalization_page_test.m.js",
         "$root_gen_dir/chrome/test/data/webui/settings/chromeos/test_wallpaper_browser_proxy.m.js",
+        "$root_gen_dir/chrome/test/data/webui/settings/chromeos/timezone_selector_test.m.js",
       ]
     }
     defines = [ "HAS_OUT_OF_PROC_TEST_RUNNER" ]
diff --git a/chrome/test/data/webui/new_tab_page/app_test.js b/chrome/test/data/webui/new_tab_page/app_test.js
index 2019358..66a81d0 100644
--- a/chrome/test/data/webui/new_tab_page/app_test.js
+++ b/chrome/test/data/webui/new_tab_page/app_test.js
@@ -2,12 +2,12 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import {$$, BackgroundManager, BackgroundSelectionType, BrowserProxy} from 'chrome://new-tab-page/new_tab_page.js';
+import {$$, BackgroundManager, BackgroundSelectionType, BrowserProxy, PromoBrowserCommandProxy} from 'chrome://new-tab-page/new_tab_page.js';
 import {isMac} from 'chrome://resources/js/cr.m.js';
 import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
 import {assertNotStyle, assertStyle, createTestProxy, createTheme} from 'chrome://test/new_tab_page/test_support.js';
 import {TestBrowserProxy} from 'chrome://test/test_browser_proxy.m.js';
-import {flushTasks} from 'chrome://test/test_util.m.js';
+import {eventToPromise, flushTasks} from 'chrome://test/test_util.m.js';
 
 suite('NewTabPageAppTest', () => {
   /** @type {!AppElement} */
@@ -389,4 +389,38 @@
     await testProxy.callbackRouterRemote.$.flushForTesting();
     assertTrue(app.$.mostVisited.hasAttribute('use-title-pill'));
   });
+
+  test('executes promo browser command', async () => {
+    const testProxy = PromoBrowserCommandProxy.getInstance();
+    testProxy.handler = TestBrowserProxy.fromClass(
+        promoBrowserCommand.mojom.CommandHandlerRemote);
+    testProxy.handler.setResultFor(
+        'executeCommand', Promise.resolve({commandExecuted: true}));
+
+    const commandId = 123;  // Unsupported command.
+    const clickInfo = {middleButton: true};
+    window.dispatchEvent(new MessageEvent('message', {
+      data: {
+        frameType: 'one-google-bar',
+        messageType: 'execute-browser-command',
+        commandId,
+        clickInfo,
+      },
+      source: window,
+      origin: window.origin,
+    }));
+
+    // Make sure the command and click information are sent to the browser.
+    const [expectedCommandId, expectedClickInfo] =
+        await testProxy.handler.whenCalled('executeCommand');
+    // Unsupported commands get resolved to the default command before being
+    // sent to the browser.
+    assertEquals(
+        promoBrowserCommand.mojom.Command.kUnknownCommand, expectedCommandId);
+    assertEquals(clickInfo, expectedClickInfo);
+
+    // Make sure the promo frame gets notified whether the command was executed.
+    const {data: commandExecuted} = await eventToPromise('message', window);
+    assertTrue(commandExecuted);
+  });
 });
diff --git a/chrome/test/data/webui/settings/chromeos/BUILD.gn b/chrome/test/data/webui/settings/chromeos/BUILD.gn
index b74589e92..418e979 100644
--- a/chrome/test/data/webui/settings/chromeos/BUILD.gn
+++ b/chrome/test/data/webui/settings/chromeos/BUILD.gn
@@ -58,6 +58,7 @@
     "test_multidevice_browser_proxy.js",
     "test_wallpaper_browser_proxy.js",
     "test_os_sync_browser_proxy.js",
+    "timezone_selector_test.js",
   ]
   namespace_rewrites =
       os_settings_namespace_rewrites + os_test_namespace_rewrites
diff --git a/chrome/test/data/webui/settings/chromeos/os_settings_browsertest.js b/chrome/test/data/webui/settings/chromeos/os_settings_browsertest.js
index 723f5fb..e202f1c8 100644
--- a/chrome/test/data/webui/settings/chromeos/os_settings_browsertest.js
+++ b/chrome/test/data/webui/settings/chromeos/os_settings_browsertest.js
@@ -1531,4 +1531,23 @@
   mocha.run();
 });
 
+// Tests for the Date Time timezone selector
+// eslint-disable-next-line no-var
+var OSSettingsTimezoneSelectorTest = class extends OSSettingsBrowserTest {
+  /** @override */
+  get browsePreload() {
+    return super.browsePreload +
+        'chromeos/date_time_page/timezone_selector.html';
+  }
+
+  /** @override */
+  get extraLibraries() {
+    return super.extraLibraries.concat(['timezone_selector_test.js']);
+  }
+};
+
+TEST_F('OSSettingsTimezoneSelectorTest', 'AllJsTests', () => {
+  mocha.run();
+});
+
 GEN('#endif  // defined(NDEBUG)');
diff --git a/chrome/test/data/webui/settings/chromeos/os_settings_v3_browsertest.js b/chrome/test/data/webui/settings/chromeos/os_settings_v3_browsertest.js
index f3f8f01..b510a5f 100644
--- a/chrome/test/data/webui/settings/chromeos/os_settings_v3_browsertest.js
+++ b/chrome/test/data/webui/settings/chromeos/os_settings_v3_browsertest.js
@@ -54,6 +54,7 @@
  ['PeoplePageChangePicture', 'people_page_change_picture_test.m.js'],
  ['PeoplePageKerberosAccounts', 'people_page_kerberos_accounts_test.m.js'],
  ['PrivacyPage', 'os_privacy_page_test.m.js'],
+ ['TimezoneSelector', 'timezone_selector_test.m.js'],
 ].forEach(test => registerTest(...test));
 
 function registerTest(testName, module, caseName) {
diff --git a/chrome/test/data/webui/settings/chromeos/timezone_selector_test.js b/chrome/test/data/webui/settings/chromeos/timezone_selector_test.js
new file mode 100644
index 0000000..45bb1cf
--- /dev/null
+++ b/chrome/test/data/webui/settings/chromeos/timezone_selector_test.js
@@ -0,0 +1,64 @@
+// 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.
+
+// clang-format off
+// #import 'chrome://os-settings/chromeos/os_settings.js';
+
+// #import {flush} from'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+// #import {assertEquals} from '../../chai_assert.js';
+// #import {assert} from 'chrome://resources/js/assert.m.js';
+// clang-format on
+
+suite('TimezoneSelectorTests', function() {
+  /** @type {TimezoneSelector} */
+  let timezoneSelector = null;
+
+  setup(function() {
+    PolymerTest.clearBody();
+  });
+
+  teardown(function() {
+    timezoneSelector.remove();
+  });
+
+  test('Per-user timezone disabled', async () => {
+    timezoneSelector = document.createElement('timezone-selector');
+    timezoneSelector.prefs = {
+      'cros': {
+        'flags': {
+          'per_user_timezone_enabled': {
+            value: false,
+          }
+        }
+      }
+    };
+    document.body.appendChild(timezoneSelector);
+
+    Polymer.dom.flush();
+
+    assertEquals(null, timezoneSelector.$$('#userTimeZoneSelector'));
+    assertEquals(null, timezoneSelector.$$('#systemTimezoneSelector'));
+  });
+
+  test('Per-user timezone enabled', async () => {
+    timezoneSelector = document.createElement('timezone-selector');
+    timezoneSelector.prefs = {
+      'cros': {
+        'flags': {
+          'per_user_timezone_enabled': {
+            value: true,
+          }
+        }
+      }
+    };
+    document.body.appendChild(timezoneSelector);
+
+    Polymer.dom.flush();
+
+    const userTimezoneSelector =
+        assert(timezoneSelector.$$('#userTimeZoneSelector'));
+    const systemTimezoneSelector =
+        assert(timezoneSelector.$$('#systemTimezoneSelector'));
+  });
+});
diff --git a/chrome/test/media_router/media_router_integration_browsertest.cc b/chrome/test/media_router/media_router_integration_browsertest.cc
index 36282fc..3483370e 100644
--- a/chrome/test/media_router/media_router_integration_browsertest.cc
+++ b/chrome/test/media_router/media_router_integration_browsertest.cc
@@ -500,6 +500,14 @@
       web_contents->GetDelegate()->IsFullscreenForTabOrPending(web_contents));
 }
 
+// Flaky on MSan bots: http://crbug.com/879885
+#if defined(MEMORY_SANITIZER)
+#define MAYBE_OpenLocalMediaFileCastFailNoFullscreen \
+  DISABLED_OpenLocalMediaFileCastFailNoFullscreen
+#else
+#define MAYBE_OpenLocalMediaFileCastFailNoFullscreen \
+  OpenLocalMediaFileCastFailNoFullscreen
+#endif
 // Tests that failed route creation of local file does not enter fullscreen.
 IN_PROC_BROWSER_TEST_F(MediaRouterIntegrationBrowserTest,
                        OpenLocalMediaFileCastFailNoFullscreen) {
diff --git a/chrome/updater/win/setup/setup.cc b/chrome/updater/win/setup/setup.cc
index bd4cc8db..81bc5592 100644
--- a/chrome/updater/win/setup/setup.cc
+++ b/chrome/updater/win/setup/setup.cc
@@ -93,7 +93,7 @@
   list->AddWorkItem(new installer::InstallServiceWorkItem(
       kWindowsServiceName, kWindowsServiceName,
       base::CommandLine(com_service_path), base::ASCIIToUTF16(UPDATER_KEY),
-      CLSID_UpdaterServiceClass, GUID_NULL));
+      {CLSID_UpdaterServiceClass}, {}));
 }
 
 // Adds work items to register the COM Interfaces with Windows.
diff --git a/chrome/updater/win/setup/uninstall.cc b/chrome/updater/win/setup/uninstall.cc
index fe98068..e3b54d5 100644
--- a/chrome/updater/win/setup/uninstall.cc
+++ b/chrome/updater/win/setup/uninstall.cc
@@ -50,7 +50,7 @@
                                  WorkItem::kWow64Default);
   if (!installer::InstallServiceWorkItem::DeleteService(
           kWindowsServiceName, base::ASCIIToUTF16(UPDATER_KEY),
-          CLSID_UpdaterServiceClass, GUID_NULL))
+          {CLSID_UpdaterServiceClass}, {}))
     LOG(WARNING) << "DeleteService failed.";
 }
 
diff --git a/chromecast/media/audio/BUILD.gn b/chromecast/media/audio/BUILD.gn
index be15aa3..c871565 100644
--- a/chromecast/media/audio/BUILD.gn
+++ b/chromecast/media/audio/BUILD.gn
@@ -13,6 +13,15 @@
   default_output_buffer_size_in_frames = 2048
 }
 
+cast_source_set("audio_log") {
+  sources = [
+    "audio_log.cc",
+    "audio_log.h",
+  ]
+
+  deps = [ "//base" ]
+}
+
 cast_source_set("audio_io_thread") {
   sources = [
     "audio_io_thread.cc",
diff --git a/chromecast/media/audio/audio_log.cc b/chromecast/media/audio/audio_log.cc
new file mode 100644
index 0000000..bd6c64f
--- /dev/null
+++ b/chromecast/media/audio/audio_log.cc
@@ -0,0 +1,175 @@
+// 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 "chromecast/media/audio/audio_log.h"
+
+#include <algorithm>
+
+#include "base/bind.h"
+#include "base/callback.h"
+#include "base/location.h"
+#include "base/memory/scoped_refptr.h"
+#include "base/no_destructor.h"
+#include "base/sequenced_task_runner.h"
+#include "base/synchronization/lock.h"
+#include "base/thread_annotations.h"
+#include "base/threading/sequenced_task_runner_handle.h"
+
+namespace logging {
+
+namespace {
+constexpr int kBufferSize = 256;
+constexpr int kMaxBuffers = 16;
+}  // namespace
+
+class AudioLogMessage::StreamBuf : public std::streambuf {
+ public:
+  StreamBuf() = default;
+
+  StreamBuf(const StreamBuf&) = delete;
+  StreamBuf& operator=(const StreamBuf&) = delete;
+
+  void Initialize(const char* file, int line, LogSeverity severity) {
+    file_ = file;
+    line_ = line;
+    severity_ = severity;
+    setp(buffer_, buffer_ + kBufferSize);
+  }
+
+  void Log() {
+    ::logging::LogMessage message(file_, line_, severity_);
+    int size = pptr() - pbase();
+    message.stream().write(buffer_, size);
+  }
+
+ private:
+  const char* file_ = nullptr;
+  int line_;
+  LogSeverity severity_;
+  char buffer_[kBufferSize];
+};
+
+namespace {
+
+class BufferManager {
+ public:
+  static BufferManager* Get();
+
+  void Setup() {
+    base::AutoLock lock(lock_);
+    if (ready_) {
+      return;
+    }
+
+    task_runner_ = base::SequencedTaskRunnerHandle::Get();
+    dispose_callback_ = base::BindRepeating(
+        &BufferManager::HandleDisposedBuffers, base::Unretained(this));
+
+    for (int i = 0; i < kMaxBuffers; ++i) {
+      free_buffers_[i] = &buffers_[i];
+    }
+    num_free_buffers_ = kMaxBuffers;
+    ready_ = true;
+  }
+
+  AudioLogMessage::StreamBuf* GetBuffer(const char* file,
+                                        int line,
+                                        LogSeverity severity) {
+    AudioLogMessage::StreamBuf* buffer;
+    {
+      base::AutoLock lock(lock_);
+      if (num_free_buffers_ == 0) {
+        ++num_missing_buffers_;
+        return nullptr;
+      }
+
+      --num_free_buffers_;
+      buffer = free_buffers_[num_free_buffers_];
+    }
+    buffer->Initialize(file, line, severity);
+    return buffer;
+  }
+
+  void Dispose(AudioLogMessage::StreamBuf* buffer) {
+    if (!buffer) {
+      return;
+    }
+
+    {
+      base::AutoLock lock(lock_);
+      DCHECK_LT(num_disposed_buffers_, kMaxBuffers);
+      disposed_buffers_[num_disposed_buffers_] = buffer;
+      ++num_disposed_buffers_;
+    }
+    DCHECK(task_runner_);
+    task_runner_->PostTask(FROM_HERE, dispose_callback_);
+  }
+
+ private:
+  void HandleDisposedBuffers() {
+    AudioLogMessage::StreamBuf* buffers[kMaxBuffers];
+    int num_buffers;
+    int num_missing;
+    {
+      base::AutoLock lock(lock_);
+      std::copy_n(disposed_buffers_, num_disposed_buffers_, buffers);
+      num_buffers = num_disposed_buffers_;
+      num_disposed_buffers_ = 0;
+
+      num_missing = num_missing_buffers_;
+      num_missing_buffers_ = 0;
+    }
+
+    for (int i = 0; i < num_buffers; ++i) {
+      buffers[i]->Log();
+      {
+        base::AutoLock lock(lock_);
+        free_buffers_[num_free_buffers_] = buffers[i];
+        ++num_free_buffers_;
+      }
+    }
+
+    LOG_IF(ERROR, num_missing > 0)
+        << num_missing << " log messages lost due to lack of buffers";
+  }
+
+  AudioLogMessage::StreamBuf buffers_[kMaxBuffers];
+
+  base::Lock lock_;
+  bool ready_ GUARDED_BY(lock_) = false;
+  AudioLogMessage::StreamBuf* free_buffers_[kMaxBuffers] GUARDED_BY(lock_);
+  int num_free_buffers_ GUARDED_BY(lock_) = 0;
+
+  AudioLogMessage::StreamBuf* disposed_buffers_[kMaxBuffers] GUARDED_BY(lock_);
+  int num_disposed_buffers_ GUARDED_BY(lock_) = 0;
+
+  int num_missing_buffers_ GUARDED_BY(lock_) = 0;
+
+  scoped_refptr<base::SequencedTaskRunner> task_runner_;
+  base::RepeatingClosure dispose_callback_;
+};
+
+// static
+BufferManager* BufferManager::Get() {
+  static base::NoDestructor<BufferManager> g_buffer_manager;
+  return g_buffer_manager.get();
+}
+
+}  // namespace
+
+AudioLogMessage::AudioLogMessage(const char* file,
+                                 int line,
+                                 LogSeverity severity)
+    : buffer_(BufferManager::Get()->GetBuffer(file, line, severity)),
+      stream_(buffer_) {}
+
+AudioLogMessage::~AudioLogMessage() {
+  BufferManager::Get()->Dispose(buffer_);
+}
+
+void InitializeAudioLog() {
+  BufferManager::Get()->Setup();
+}
+
+}  // namespace logging
diff --git a/chromecast/media/audio/audio_log.h b/chromecast/media/audio/audio_log.h
new file mode 100644
index 0000000..3968336b
--- /dev/null
+++ b/chromecast/media/audio/audio_log.h
@@ -0,0 +1,47 @@
+// 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 CHROMECAST_MEDIA_AUDIO_AUDIO_LOG_H_
+#define CHROMECAST_MEDIA_AUDIO_AUDIO_LOG_H_
+
+#include <ostream>
+
+#include "base/logging.h"
+
+namespace logging {
+
+#define AUDIO_LOG_STREAM(severity) \
+  COMPACT_GOOGLE_LOG_EX_##severity(AudioLogMessage).stream()
+
+#define AUDIO_LOG(severity) \
+  LAZY_STREAM(AUDIO_LOG_STREAM(severity), LOG_IS_ON(severity))
+
+#define AUDIO_LOG_IF(severity, condition) \
+  LAZY_STREAM(AUDIO_LOG_STREAM(severity), LOG_IS_ON(severity) && (condition))
+
+class AudioLogMessage {
+ public:
+  class StreamBuf;
+
+  AudioLogMessage(const char* file, int line, LogSeverity severity);
+  ~AudioLogMessage();
+
+  AudioLogMessage(const AudioLogMessage&) = delete;
+  AudioLogMessage& operator=(const AudioLogMessage&) = delete;
+
+  std::ostream& stream() { return stream_; }
+
+ private:
+  StreamBuf* buffer_;
+  std::ostream stream_;
+};
+
+// Should be called on a lower-priority thread. Actual output of log messages
+// will be done on this thread. Note that any use of AudioLogMessage prior to
+// InitializeAudioLog() will not produce any output.
+void InitializeAudioLog();
+
+}  // namespace logging
+
+#endif  // CHROMECAST_MEDIA_AUDIO_AUDIO_LOG_H_
diff --git a/chromecast/media/audio/cast_audio_manager.cc b/chromecast/media/audio/cast_audio_manager.cc
index 5bf2f2f..c2d7bf6 100644
--- a/chromecast/media/audio/cast_audio_manager.cc
+++ b/chromecast/media/audio/cast_audio_manager.cc
@@ -21,10 +21,6 @@
 #include "chromecast/public/media/media_pipeline_backend.h"
 #include "media/audio/audio_device_description.h"
 
-#if defined(OS_ANDROID)
-#include "media/audio/android/audio_track_output_stream.h"
-#endif  // defined(OS_ANDROID)
-
 namespace {
 // TODO(alokp): Query the preferred value from media backend.
 const int kDefaultSampleRate = 48000;
@@ -197,20 +193,6 @@
   }
 }
 
-::media::AudioOutputStream* CastAudioManager::MakeBitstreamOutputStream(
-    const ::media::AudioParameters& params,
-    const std::string& device_id,
-    const ::media::AudioManager::LogCallback& log_callback) {
-#if defined(OS_ANDROID)
-  DCHECK(params.IsBitstreamFormat());
-  return new ::media::AudioTrackOutputStream(this, params);
-#else
-  NOTREACHED() << " Not implemented on non-android platform.";
-  return ::media::AudioManagerBase::MakeBitstreamOutputStream(params, device_id,
-                                                              log_callback);
-#endif  // defined(OS_ANDROID)
-}
-
 ::media::AudioInputStream* CastAudioManager::MakeLinearInputStream(
     const ::media::AudioParameters& params,
     const std::string& device_id,
@@ -281,16 +263,5 @@
   return !use_cma_backend;
 }
 
-#if defined(OS_ANDROID)
-::media::AudioOutputStream* CastAudioManager::MakeAudioOutputStreamProxy(
-    const ::media::AudioParameters& params,
-    const std::string& device_id) {
-  // Override to use MakeAudioOutputStream to prevent the audio output stream
-  // from closing during pause/stop.
-  return MakeAudioOutputStream(params, device_id,
-                               /*log_callback, not used*/ base::DoNothing());
-}
-#endif  // defined(OS_ANDROID)
-
 }  // namespace media
 }  // namespace chromecast
diff --git a/chromecast/media/audio/cast_audio_manager.h b/chromecast/media/audio/cast_audio_manager.h
index 0998c62..60f1351 100644
--- a/chromecast/media/audio/cast_audio_manager.h
+++ b/chromecast/media/audio/cast_audio_manager.h
@@ -88,10 +88,6 @@
       const ::media::AudioParameters& params,
       const std::string& device_id_or_group_id,
       const ::media::AudioManager::LogCallback& log_callback) override;
-  ::media::AudioOutputStream* MakeBitstreamOutputStream(
-      const ::media::AudioParameters& params,
-      const std::string& device_id,
-      const ::media::AudioManager::LogCallback& log_callback) override;
   ::media::AudioInputStream* MakeLinearInputStream(
       const ::media::AudioParameters& params,
       const std::string& device_id,
@@ -108,12 +104,6 @@
   virtual ::media::AudioOutputStream* MakeMixerOutputStream(
       const ::media::AudioParameters& params);
 
-#if defined(OS_ANDROID)
-  ::media::AudioOutputStream* MakeAudioOutputStreamProxy(
-      const ::media::AudioParameters& params,
-      const std::string& device_id) override;
-#endif
-
  private:
   FRIEND_TEST_ALL_PREFIXES(CastAudioManagerTest, CanMakeStreamProxy);
   friend class CastAudioMixer;
diff --git a/chromecast/media/audio/cast_audio_manager_android.cc b/chromecast/media/audio/cast_audio_manager_android.cc
index e882618c..0638fee 100644
--- a/chromecast/media/audio/cast_audio_manager_android.cc
+++ b/chromecast/media/audio/cast_audio_manager_android.cc
@@ -9,6 +9,7 @@
 #include "base/logging.h"
 #include "chromecast/media/audio/audio_buildflags.h"
 #include "chromecast/media/audio/cast_audio_input_stream.h"
+#include "media/audio/android/audio_track_output_stream.h"
 #include "media/audio/audio_device_name.h"
 #include "media/base/audio_parameters.h"
 #include "media/base/channel_layout.h"
@@ -106,5 +107,22 @@
   return nullptr;
 }
 
+::media::AudioOutputStream* CastAudioManagerAndroid::MakeBitstreamOutputStream(
+    const ::media::AudioParameters& params,
+    const std::string& device_id,
+    const ::media::AudioManager::LogCallback& log_callback) {
+  DCHECK(params.IsBitstreamFormat());
+  return new ::media::AudioTrackOutputStream(this, params);
+}
+
+::media::AudioOutputStream* CastAudioManagerAndroid::MakeAudioOutputStreamProxy(
+    const ::media::AudioParameters& params,
+    const std::string& device_id) {
+  // Override to use MakeAudioOutputStream to prevent the audio output stream
+  // from closing during pause/stop.
+  return MakeAudioOutputStream(params, device_id,
+                               /*log_callback, not used*/ base::DoNothing());
+}
+
 }  // namespace media
 }  // namespace chromecast
diff --git a/chromecast/media/audio/cast_audio_manager_android.h b/chromecast/media/audio/cast_audio_manager_android.h
index a2a81ce..7afe47d 100644
--- a/chromecast/media/audio/cast_audio_manager_android.h
+++ b/chromecast/media/audio/cast_audio_manager_android.h
@@ -28,6 +28,15 @@
       bool use_mixer);
   ~CastAudioManagerAndroid() override;
 
+  // AudioManager implementation.
+  ::media::AudioOutputStream* MakeAudioOutputStreamProxy(
+      const ::media::AudioParameters& params,
+      const std::string& device_id) override;
+  ::media::AudioOutputStream* MakeBitstreamOutputStream(
+      const ::media::AudioParameters& params,
+      const std::string& device_id,
+      const ::media::AudioManager::LogCallback& log_callback) override;
+
   // CastAudioManager implementation.
   bool HasAudioInputDevices() override;
   void GetAudioInputDeviceNames(
diff --git a/chromecast/media/cma/backend/mixer/BUILD.gn b/chromecast/media/cma/backend/mixer/BUILD.gn
index c3870f8..cd11be8 100644
--- a/chromecast/media/cma/backend/mixer/BUILD.gn
+++ b/chromecast/media/cma/backend/mixer/BUILD.gn
@@ -72,6 +72,7 @@
     "//chromecast/base:chromecast_switches",
     "//chromecast/base:thread_health_checker",
     "//chromecast/media/audio:audio_io_thread",
+    "//chromecast/media/audio:audio_log",
     "//chromecast/media/audio:interleaved_channel_mixer",
     "//chromecast/media/audio:libcast_external_audio_pipeline_1.0",
     "//chromecast/media/audio:processing",
diff --git a/chromecast/media/cma/backend/mixer/audio_output_redirector.cc b/chromecast/media/cma/backend/mixer/audio_output_redirector.cc
index 684258bc..5c42e678 100644
--- a/chromecast/media/cma/backend/mixer/audio_output_redirector.cc
+++ b/chromecast/media/cma/backend/mixer/audio_output_redirector.cc
@@ -16,6 +16,7 @@
 #include "base/strings/pattern.h"
 #include "base/threading/sequenced_task_runner_handle.h"
 #include "chromecast/media/audio/audio_fader.h"
+#include "chromecast/media/audio/audio_log.h"
 #include "chromecast/media/audio/mixer_service/conversions.h"
 #include "chromecast/media/audio/mixer_service/mixer_service.pb.h"
 #include "chromecast/media/audio/mixer_service/mixer_socket.h"
@@ -183,8 +184,9 @@
   DCHECK(mixer_input_);
 
   if (mixer_input_->num_channels() != num_output_channels_) {
-    LOG(INFO) << "Remixing channels for " << mixer_input_->source() << " from "
-              << mixer_input_->num_channels() << " to " << num_output_channels_;
+    AUDIO_LOG(INFO) << "Remixing channels for " << mixer_input_->source()
+                    << " from " << mixer_input_->num_channels() << " to "
+                    << num_output_channels_;
     channel_mixer_ = std::make_unique<::media::ChannelMixer>(
         mixer::CreateAudioParametersForChannelMixer(
             mixer_input_->channel_layout(), mixer_input_->num_channels()),
diff --git a/chromecast/media/cma/backend/mixer/filter_group.cc b/chromecast/media/cma/backend/mixer/filter_group.cc
index 1534499..114b64e 100644
--- a/chromecast/media/cma/backend/mixer/filter_group.cc
+++ b/chromecast/media/cma/backend/mixer/filter_group.cc
@@ -11,6 +11,7 @@
 #include "base/numerics/ranges.h"
 #include "base/time/time.h"
 #include "base/values.h"
+#include "chromecast/media/audio/audio_log.h"
 #include "chromecast/media/audio/interleaved_channel_mixer.h"
 #include "chromecast/media/cma/backend/mixer/channel_layout.h"
 #include "chromecast/media/cma/backend/mixer/mixer_input.h"
@@ -124,15 +125,16 @@
   // Get default limits.
   if (ParseVolumeLimit(volume_limits, &default_volume_min_,
                        &default_volume_max_)) {
-    LOG(INFO) << "Default volume limits for '" << name_ << "' group: ["
-              << default_volume_min_ << ", " << default_volume_max_ << "]";
+    AUDIO_LOG(INFO) << "Default volume limits for '" << name_ << "' group: ["
+                    << default_volume_min_ << ", " << default_volume_max_
+                    << "]";
   }
 
   float min, max;
   for (const auto& item : volume_limits->DictItems()) {
     if (ParseVolumeLimit(&item.second, &min, &max)) {
-      LOG(INFO) << "Volume limits for device ID '" << item.first << "' = ["
-                << min << ", " << max << "]";
+      AUDIO_LOG(INFO) << "Volume limits for device ID '" << item.first
+                      << "' = [" << min << ", " << max << "]";
       volume_limits_.insert({item.first, {min, max}});
     }
   }
@@ -290,8 +292,8 @@
 
 void FilterGroup::UpdatePlayoutChannel(int playout_channel) {
   if (playout_channel >= num_channels_) {
-    LOG(ERROR) << "only " << num_channels_ << " present, wanted channel #"
-               << playout_channel;
+    AUDIO_LOG(ERROR) << "only " << num_channels_ << " present, wanted channel #"
+                     << playout_channel;
     return;
   }
   post_processing_pipeline_->UpdatePlayoutChannel(playout_channel);
diff --git a/chromecast/media/cma/backend/mixer/mixer_input.cc b/chromecast/media/cma/backend/mixer/mixer_input.cc
index b897058..c9befac 100644
--- a/chromecast/media/cma/backend/mixer/mixer_input.cc
+++ b/chromecast/media/cma/backend/mixer/mixer_input.cc
@@ -15,6 +15,7 @@
 #include "base/logging.h"
 #include "base/numerics/ranges.h"
 #include "chromecast/media/audio/audio_fader.h"
+#include "chromecast/media/audio/audio_log.h"
 #include "chromecast/media/cma/backend/mixer/audio_output_redirector_input.h"
 #include "chromecast/media/cma/backend/mixer/channel_layout.h"
 #include "chromecast/media/cma/backend/mixer/filter_group.h"
@@ -117,8 +118,9 @@
     if (filter_group->num_channels() == num_channels_) {
       channel_mixer_.reset();
     } else {
-      LOG(INFO) << "Remixing channels for " << source_ << " from "
-                << num_channels_ << " to " << filter_group->num_channels();
+      AUDIO_LOG(INFO) << "Remixing channels for " << source_ << " from "
+                      << num_channels_ << " to "
+                      << filter_group->num_channels();
       channel_mixer_ = std::make_unique<::media::ChannelMixer>(
           mixer::CreateAudioParametersForChannelMixer(channel_layout_,
                                                       num_channels_),
@@ -131,7 +133,8 @@
 
 void MixerInput::AddAudioOutputRedirector(
     AudioOutputRedirectorInput* redirector) {
-  LOG(INFO) << "Add redirector to " << device_id_ << "(" << source_ << ")";
+  AUDIO_LOG(INFO) << "Add redirector to " << device_id_ << "(" << source_
+                  << ")";
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   DCHECK(redirector);
   audio_output_redirectors_.insert(
@@ -146,7 +149,8 @@
 
 void MixerInput::RemoveAudioOutputRedirector(
     AudioOutputRedirectorInput* redirector) {
-  LOG(INFO) << "Remove redirector from " << device_id_ << "(" << source_ << ")";
+  AUDIO_LOG(INFO) << "Remove redirector from " << device_id_ << "(" << source_
+                  << ")";
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   DCHECK(redirector);
   audio_output_redirectors_.erase(
@@ -323,9 +327,9 @@
   float old_target_volume = TargetVolume();
   stream_volume_multiplier_ = std::max(0.0f, multiplier);
   float target_volume = TargetVolume();
-  LOG(INFO) << device_id_ << "(" << source_
-            << "): stream volume = " << stream_volume_multiplier_
-            << ", effective multiplier = " << target_volume;
+  AUDIO_LOG(INFO) << device_id_ << "(" << source_
+                  << "): stream volume = " << stream_volume_multiplier_
+                  << ", effective multiplier = " << target_volume;
   if (target_volume != old_target_volume) {
     slew_volume_.SetMaxSlewTimeMs(kDefaultSlewTimeMs);
     slew_volume_.SetVolume(target_volume);
@@ -339,9 +343,9 @@
   float old_target_volume = TargetVolume();
   type_volume_multiplier_ = volume;
   float target_volume = TargetVolume();
-  LOG(INFO) << device_id_ << "(" << source_
-            << "): type volume = " << type_volume_multiplier_
-            << ", effective multiplier = " << target_volume;
+  AUDIO_LOG(INFO) << device_id_ << "(" << source_
+                  << "): type volume = " << type_volume_multiplier_
+                  << ", effective multiplier = " << target_volume;
   if (target_volume != old_target_volume) {
     slew_volume_.SetMaxSlewTimeMs(kDefaultSlewTimeMs);
     slew_volume_.SetVolume(target_volume);
@@ -354,8 +358,8 @@
   volume_min_ = volume_min;
   volume_max_ = volume_max;
   float target_volume = TargetVolume();
-  LOG(INFO) << device_id_ << "(" << source_ << "): set volume limits to ["
-            << volume_min_ << ", " << volume_max_ << "]";
+  AUDIO_LOG(INFO) << device_id_ << "(" << source_ << "): set volume limits to ["
+                  << volume_min_ << ", " << volume_max_ << "]";
   if (target_volume != old_target_volume) {
     slew_volume_.SetMaxSlewTimeMs(kDefaultSlewTimeMs);
     slew_volume_.SetVolume(target_volume);
@@ -367,13 +371,13 @@
   float old_target_volume = TargetVolume();
   output_volume_limit_ = limit;
   float target_volume = TargetVolume();
-  LOG(INFO) << device_id_ << "(" << source_
-            << "): output limit = " << output_volume_limit_
-            << ", effective multiplier = " << target_volume;
+  AUDIO_LOG(INFO) << device_id_ << "(" << source_
+                  << "): output limit = " << output_volume_limit_
+                  << ", effective multiplier = " << target_volume;
   if (fade_ms < 0) {
     fade_ms = kDefaultSlewTimeMs;
   } else {
-    LOG(INFO) << "Fade over " << fade_ms << " ms";
+    AUDIO_LOG(INFO) << "Fade over " << fade_ms << " ms";
   }
   if (target_volume != old_target_volume) {
     slew_volume_.SetMaxSlewTimeMs(fade_ms);
@@ -388,9 +392,9 @@
   float old_target_volume = TargetVolume();
   mute_volume_multiplier_ = muted ? 0.0f : 1.0f;
   float target_volume = TargetVolume();
-  LOG(INFO) << device_id_ << "(" << source_
-            << "): mute volume = " << mute_volume_multiplier_
-            << ", effective multiplier = " << target_volume;
+  AUDIO_LOG(INFO) << device_id_ << "(" << source_
+                  << "): mute volume = " << mute_volume_multiplier_
+                  << ", effective multiplier = " << target_volume;
   if (target_volume != old_target_volume) {
     slew_volume_.SetMaxSlewTimeMs(kDefaultSlewTimeMs);
     slew_volume_.SetVolume(target_volume);
diff --git a/chromecast/media/cma/backend/mixer/mixer_input_connection.cc b/chromecast/media/cma/backend/mixer/mixer_input_connection.cc
index 414d720..04dc882 100644
--- a/chromecast/media/cma/backend/mixer/mixer_input_connection.cc
+++ b/chromecast/media/cma/backend/mixer/mixer_input_connection.cc
@@ -19,6 +19,7 @@
 #include "base/threading/thread_task_runner_handle.h"
 #include "base/time/time.h"
 #include "chromecast/base/chromecast_switches.h"
+#include "chromecast/media/audio/audio_log.h"
 #include "chromecast/media/audio/mixer_service/conversions.h"
 #include "chromecast/media/audio/mixer_service/mixer_service.pb.h"
 #include "chromecast/media/cma/backend/mixer/channel_layout.h"
@@ -583,7 +584,7 @@
 int64_t MixerInputConnection::QueueData(scoped_refptr<net::IOBuffer> data) {
   int frames = GetFrameCount(data.get());
   if (frames == 0) {
-    LOG(INFO) << "End of stream for " << this;
+    AUDIO_LOG(INFO) << "End of stream for " << this;
     state_ = State::kGotEos;
     if (!started_) {
       io_task_runner_->PostTask(FROM_HERE, ready_for_playback_task_);
@@ -656,8 +657,8 @@
     mixer_read_size_ = read_size;
     if (start_threshold_frames_ == 0) {
       start_threshold_frames_ = read_size + fill_size_;
-      LOG(INFO) << this
-                << " Updated start threshold: " << start_threshold_frames_;
+      AUDIO_LOG(INFO) << this << " Updated start threshold: "
+                      << start_threshold_frames_;
     }
     mixer_rendering_delay_ = initial_rendering_delay;
     if (state_ == State::kUninitialized) {
@@ -687,8 +688,8 @@
   const int frames_needed_to_start = std::max(
       start_threshold_frames_, fader_.FramesNeededFromSource(num_frames));
   if (max_queued_frames_ < frames_needed_to_start) {
-    LOG(INFO) << "Boost queue size to " << frames_needed_to_start
-              << " to allow stream to start";
+    AUDIO_LOG(INFO) << "Boost queue size to " << frames_needed_to_start
+                    << " to allow stream to start";
     max_queued_frames_ = frames_needed_to_start;
   }
   const bool have_enough_queued_frames =
@@ -700,7 +701,7 @@
   remaining_silence_frames_ = 0;
   if (!use_start_timestamp_ || (queue_.empty() && state_ == State::kGotEos)) {
     // No start timestamp, so start as soon as there are enough queued frames.
-    LOG(INFO) << "Start " << this;
+    AUDIO_LOG(INFO) << "Start " << this;
     started_ = true;
     return;
   }
@@ -726,7 +727,7 @@
   int64_t drop_us = (desired_pts_now - actual_pts_now) / playback_rate_;
 
   if (drop_us >= 0) {
-    LOG(INFO) << this << " Dropping audio, duration = " << drop_us;
+    AUDIO_LOG(INFO) << this << " Dropping audio, duration = " << drop_us;
     DropAudio(::media::AudioTimestampHelper::TimeToFrames(
         base::TimeDelta::FromMicroseconds(drop_us), input_samples_per_second_));
     // Only start if we still have enough data to do so.
@@ -738,20 +739,22 @@
                           SamplesToMicroseconds(current_buffer_offset_,
                                                 input_samples_per_second_) *
                               playback_rate_;
-      LOG(INFO) << this << " Start playback of PTS " << start_pts << " at "
-                << playback_absolute_timestamp;
+      AUDIO_LOG(INFO) << this << " Start playback of PTS " << start_pts
+                      << " at " << playback_absolute_timestamp;
     }
   } else {
     int64_t silence_duration = -drop_us;
-    LOG(INFO) << this << " Adding silence. Duration = " << silence_duration;
+    AUDIO_LOG(INFO) << this
+                    << " Adding silence. Duration = " << silence_duration;
     remaining_silence_frames_ = ::media::AudioTimestampHelper::TimeToFrames(
         base::TimeDelta::FromMicroseconds(silence_duration),
         input_samples_per_second_);
     // Round to nearest multiple of 4 to preserve buffer alignment.
     remaining_silence_frames_ = ((remaining_silence_frames_ + 2) / 4) * 4;
     started_ = true;
-    LOG(INFO) << this << " Should start playback of PTS " << actual_pts_now
-              << " at " << (playback_absolute_timestamp + silence_duration);
+    AUDIO_LOG(INFO) << this << " Should start playback of PTS "
+                    << actual_pts_now << " at "
+                    << (playback_absolute_timestamp + silence_duration);
   }
 }
 
@@ -774,7 +777,8 @@
   }
 
   if (frames_to_drop > 0) {
-    LOG(INFO) << this << " Still need to drop " << frames_to_drop << " frames";
+    AUDIO_LOG(INFO) << this << " Still need to drop " << frames_to_drop
+                    << " frames";
   }
 }
 
@@ -814,11 +818,11 @@
     // full request. This will allow us to buffer up more data so we can fully
     // fade in.
     if (state_ == State::kNormalPlayback && !can_complete_fill) {
-      LOG_IF(INFO, !zero_fader_frames_) << "Stream underrun for " << this;
+      AUDIO_LOG_IF(INFO, !zero_fader_frames_) << "Stream underrun for " << this;
       zero_fader_frames_ = true;
       underrun = true;
     } else {
-      LOG_IF(INFO, started_ && zero_fader_frames_)
+      AUDIO_LOG_IF(INFO, started_ && zero_fader_frames_)
           << "Stream underrun recovered for " << this;
       zero_fader_frames_ = false;
       if (!skip_next_fill_for_rate_change_) {
@@ -932,8 +936,9 @@
   }
 
   if (rate_shifter_output_->frames() < needed_frames) {
-    LOG(WARNING) << "Rate shifter output is too small; "
-                 << rate_shifter_output_->frames() << " < " << needed_frames;
+    AUDIO_LOG(WARNING) << "Rate shifter output is too small; "
+                       << rate_shifter_output_->frames() << " < "
+                       << needed_frames;
     auto output = ::media::AudioBus::Create(num_channels_, needed_frames);
     rate_shifter_output_->CopyPartialFramesTo(0, rate_shifted_offset_, 0,
                                               output.get());
@@ -1044,7 +1049,7 @@
   if (audio_ready_for_playback_fired_) {
     return;
   }
-  LOG(INFO) << this << " ready for playback";
+  AUDIO_LOG(INFO) << this << " ready for playback";
 
   mixer_service::Generic message;
   auto* ready_for_playback = message.mutable_ready_for_playback();
@@ -1075,8 +1080,8 @@
 
 void MixerInputConnection::OnAudioPlaybackError(MixerError error) {
   if (error == MixerError::kInputIgnored) {
-    LOG(INFO) << "Mixer input " << this
-              << " now being ignored due to output sample rate change";
+    AUDIO_LOG(INFO) << "Mixer input " << this
+                    << " now being ignored due to output sample rate change";
   }
 
   io_task_runner_->PostTask(
diff --git a/chromecast/media/cma/backend/mixer/stream_mixer.cc b/chromecast/media/cma/backend/mixer/stream_mixer.cc
index deac9fc..60f6e3bb 100644
--- a/chromecast/media/cma/backend/mixer/stream_mixer.cc
+++ b/chromecast/media/cma/backend/mixer/stream_mixer.cc
@@ -25,6 +25,7 @@
 #include "chromecast/base/serializers.h"
 #include "chromecast/base/thread_health_checker.h"
 #include "chromecast/media/audio/audio_io_thread.h"
+#include "chromecast/media/audio/audio_log.h"
 #include "chromecast/media/audio/interleaved_channel_mixer.h"
 #include "chromecast/media/audio/mixer_service/loopback_interrupt_reason.h"
 #include "chromecast/media/base/audio_device_ids.h"
@@ -220,6 +221,9 @@
     io_task_runner_ = mixer_task_runner_;
   }
 
+  io_task_runner_->PostTask(FROM_HERE,
+                            base::BindOnce(&logging::InitializeAudioLog));
+
   if (fixed_output_sample_rate_ != MixerOutputStream::kInvalidSampleRate) {
     LOG(INFO) << "Setting fixed sample rate to " << fixed_output_sample_rate_;
   }
@@ -307,8 +311,8 @@
   // Attempt to fall back to built-in cast_audio.json, unless we were reset with
   // an override config.
   if (!mixer_pipeline_ && override_config.empty()) {
-    LOG(WARNING) << "Invalid cast_audio.json config loaded. Retrying with "
-                    "read-only config";
+    AUDIO_LOG(WARNING) << "Invalid cast_audio.json config loaded. Retrying with"
+                          " read-only config";
     callback(false,
              "Unable to build pipeline.");  // TODO(bshaya): Send more specific
                                             // error message.
@@ -325,10 +329,11 @@
       fixed_num_output_channels_ != mixer_pipeline_->GetOutputChannelCount()) {
     // Just log a warning, but this is still fine because we will remap the
     // channels prior to output.
-    LOG(WARNING) << "PostProcessor configuration output channel count does not "
-                 << "match command line flag: "
-                 << mixer_pipeline_->GetOutputChannelCount() << " vs "
-                 << fixed_num_output_channels_ << ". Channels will be remapped";
+    AUDIO_LOG(WARNING) << "PostProcessor configuration output channel count"
+                       << " does not match command line flag: "
+                       << mixer_pipeline_->GetOutputChannelCount() << " vs "
+                       << fixed_num_output_channels_
+                       << ". Channels will be remapped";
   }
 
   if (state_ == kStateRunning) {
@@ -397,7 +402,7 @@
 }
 
 void StreamMixer::SetNumOutputChannelsOnThread(int num_channels) {
-  LOG(INFO) << "Set the number of output channels to " << num_channels;
+  AUDIO_LOG(INFO) << "Set the number of output channels to " << num_channels;
   enable_dynamic_channel_count_ = true;
   fixed_num_output_channels_ = num_channels;
 
@@ -408,7 +413,7 @@
 }
 
 void StreamMixer::Start() {
-  LOG(INFO) << __func__ << " with " << inputs_.size() << " active inputs";
+  AUDIO_LOG(INFO) << __func__ << " with " << inputs_.size() << " active inputs";
   DCHECK(mixer_task_runner_->BelongsToCurrentThread());
   DCHECK(state_ == kStateStopped);
 
@@ -458,9 +463,9 @@
 
   num_output_channels_ = output_->GetNumChannels();
   output_samples_per_second_ = output_->GetSampleRate();
-  LOG(INFO) << "Output " << num_output_channels_ << " "
-            << ChannelString(num_output_channels_) << " at "
-            << output_samples_per_second_ << " samples per second";
+  AUDIO_LOG(INFO) << "Output " << num_output_channels_ << " "
+                  << ChannelString(num_output_channels_) << " at "
+                  << output_samples_per_second_ << " samples per second";
   // Make sure the number of frames meets the filter alignment requirements.
   frames_per_write_ =
       output_->OptimalWriteFramesCount() & ~(filter_frame_alignment_ - 1);
@@ -476,8 +481,8 @@
   if (!enable_dynamic_channel_count_ && num_output_channels_ == 1) {
     num_loopback_channels = 1;
   }
-  LOG(INFO) << "Using " << num_loopback_channels << " loopback "
-            << ChannelString(num_loopback_channels);
+  AUDIO_LOG(INFO) << "Using " << num_loopback_channels << " loopback "
+                  << ChannelString(num_loopback_channels);
   loopback_channel_mixer_ = std::make_unique<InterleavedChannelMixer>(
       mixer::GuessChannelLayout(mixer_pipeline_->GetLoopbackChannelCount()),
       mixer_pipeline_->GetLoopbackChannelCount(),
@@ -533,7 +538,7 @@
 }
 
 void StreamMixer::Stop(LoopbackInterruptReason reason) {
-  LOG(INFO) << __func__;
+  AUDIO_LOG(INFO) << __func__;
   DCHECK(mixer_task_runner_->BelongsToCurrentThread());
 
   weak_factory_.InvalidateWeakPtrs();
@@ -596,9 +601,10 @@
 }
 
 int StreamMixer::GetEffectiveChannelCount(MixerInput::Source* input_source) {
-  LOG(INFO) << "Input source channel count = " << input_source->num_channels();
+  AUDIO_LOG(INFO) << "Input source channel count = "
+                  << input_source->num_channels();
   if (!enable_dynamic_channel_count_) {
-    LOG(INFO) << "Dynamic channel count not enabled; using stereo";
+    AUDIO_LOG(INFO) << "Dynamic channel count not enabled; using stereo";
     return kDefaultInputChannels;
   }
 
@@ -631,10 +637,11 @@
   DCHECK(input_group) << "Could not find a processor for "
                       << input_source->device_id();
 
-  LOG(INFO) << "Add input " << input_source << " to " << input_group->name()
-            << " @ " << input_group->GetInputSampleRate()
-            << " samples per second. Is primary source? = "
-            << input_source->primary();
+  AUDIO_LOG(INFO) << "Add input " << input_source << " to "
+                  << input_group->name() << " @ "
+                  << input_group->GetInputSampleRate()
+                  << " samples per second. Is primary source? = "
+                  << input_source->primary();
 
   auto input = std::make_unique<MixerInput>(input_source, input_group);
   if (state_ != kStateRunning) {
@@ -673,7 +680,7 @@
   DCHECK(mixer_task_runner_->BelongsToCurrentThread());
   DCHECK(input_source);
 
-  LOG(INFO) << "Remove input " << input_source;
+  AUDIO_LOG(INFO) << "Remove input " << input_source;
 
   auto it = inputs_.find(input_source);
   if (it != inputs_.end()) {
@@ -719,7 +726,7 @@
   }
 
   DCHECK(playout_channel == kChannelAll || playout_channel >= 0);
-  LOG(INFO) << "Update playout channel: " << playout_channel;
+  AUDIO_LOG(INFO) << "Update playout channel: " << playout_channel;
   playout_channel_ = playout_channel;
   mixer_pipeline_->SetPlayoutChannel(playout_channel_);
 }
@@ -764,7 +771,7 @@
   DCHECK(mixer_task_runner_->BelongsToCurrentThread());
   if (inputs_.empty() && base::TimeTicks::Now() >= close_timestamp_ &&
       !mixer_pipeline_->IsRinging()) {
-    LOG(INFO) << "Close timeout";
+    AUDIO_LOG(INFO) << "Close timeout";
     Stop(LoopbackInterruptReason::kOutputStopped);
     return;
   }
@@ -843,7 +850,7 @@
 void StreamMixer::AddAudioOutputRedirector(
     std::unique_ptr<AudioOutputRedirector> redirector) {
   MAKE_SURE_MIXER_THREAD(AddAudioOutputRedirector, std::move(redirector));
-  LOG(INFO) << __func__;
+  AUDIO_LOG(INFO) << __func__;
   DCHECK(redirector);
 
   AudioOutputRedirector* key = redirector.get();
@@ -867,7 +874,7 @@
 void StreamMixer::RemoveAudioOutputRedirectorOnThread(
     AudioOutputRedirector* redirector) {
   DCHECK(mixer_task_runner_->BelongsToCurrentThread());
-  LOG(INFO) << __func__;
+  AUDIO_LOG(INFO) << __func__;
   audio_output_redirectors_.erase(redirector);
 }
 
@@ -908,7 +915,7 @@
   MAKE_SURE_MIXER_THREAD(SetOutputLimit, type, limit);
   DCHECK(type != AudioContentType::kOther);
 
-  LOG(INFO) << "Set volume limit for " << type << " to " << limit;
+  AUDIO_LOG(INFO) << "Set volume limit for " << type << " to " << limit;
   volume_info_[type].limit = limit;
   int fade_ms = kUseDefaultFade;
   if (type == AudioContentType::kMedia) {
diff --git a/chromeos/components/media_app_ui/resources/js/launch.js b/chromeos/components/media_app_ui/resources/js/launch.js
index 707c4ca..115add9 100644
--- a/chromeos/components/media_app_ui/resources/js/launch.js
+++ b/chromeos/components/media_app_ui/resources/js/launch.js
@@ -195,6 +195,55 @@
   await saveBlobToFile(handle, blob);
 });
 
+guestMessagePipe.registerHandler(Message.SAVE_AS, async (message) => {
+  const {blob, oldFileToken, pickedFileToken} =
+      /** @type {!SaveAsMessage} */ (message);
+  const oldFileDescriptor = currentFiles.find(fd => fd.token === oldFileToken);
+  /** @type {!FileDescriptor} */
+  const pickedFileDescriptor = {
+    // We silently take over the old file's file descriptor by taking its token,
+    // note we can be passed an undefined token if the file we are saving was
+    // dragged into the media app.
+    token: oldFileToken || tokenGenerator.next().value,
+    file: null,
+    handle: tokenMap.get(pickedFileToken)
+  };
+  const oldFileIndex = currentFiles.findIndex(fd => fd.token === oldFileToken);
+  tokenMap.set(pickedFileDescriptor.token, pickedFileDescriptor.handle);
+  // Give the old file a new token, if we couldn't find the old file we assume
+  // its been deleted (or pasted/dragged into the media app) and skip this
+  // step.
+  if (oldFileDescriptor) {
+    oldFileDescriptor.token = generateToken(oldFileDescriptor.handle);
+  }
+  try {
+    // Note `pickedFileHandle` could be the same as a `FileSystemFileHandle`
+    // that exists in `tokenMap`. Possibly even the `File` currently open. But
+    // that's OK. E.g. the next overwrite-file request will just invoke
+    // `saveBlobToFile` in the same way. Note there may be no currently writable
+    // file (e.g. save from clipboard).
+    await saveBlobToFile(pickedFileDescriptor.handle, blob);
+  } catch (/** @type {!DOMException} */ e) {
+    // If something went wrong revert the token back to its original
+    // owner so future file actions function correctly.
+    if (oldFileDescriptor && oldFileToken) {
+      oldFileDescriptor.token = oldFileToken;
+      tokenMap.set(oldFileToken, oldFileDescriptor.handle);
+    }
+    throw e;
+  }
+
+  // Note: oldFileIndex may be `-1` here which causes the new file to be added
+  // to the start of the array, this is WAI.
+  currentFiles.splice(oldFileIndex + 1, 0, pickedFileDescriptor);
+  // Silently update entry index without triggering a reload of the media app.
+  entryIndex = oldFileIndex + 1;
+
+  /** @type {!SaveAsResponse} */
+  const response = {newFilename: pickedFileDescriptor.handle.name};
+  return response;
+});
+
 /**
  * Shows a file picker to get a writable file.
  * @param {string} suggestedName
@@ -231,7 +280,8 @@
     assertCast(crypto).getRandomValues(randomBuffer);
     for (let i = 0; i < randomBuffer.length; ++i) {
       const token = randomBuffer[i];
-      if (!tokenMap.has(token)) {
+      // Disallow "0" as a token.
+      if (token && !tokenMap.has(token)) {
         yield Number(token);
       }
     }
diff --git a/chromeos/components/media_app_ui/resources/js/media_app.externs.js b/chromeos/components/media_app_ui/resources/js/media_app.externs.js
index 1bdb833..42762dfd 100644
--- a/chromeos/components/media_app_ui/resources/js/media_app.externs.js
+++ b/chromeos/components/media_app_ui/resources/js/media_app.externs.js
@@ -76,6 +76,14 @@
  * @type {function(string): !Promise<number>|undefined}
  */
 mediaApp.AbstractFile.prototype.renameOriginalFile;
+/**
+ * A function that will save the provided blob in the file pointed to by
+ * pickedFileToken. Once saved the new file takes over this.token and becomes
+ * currently writable. The original file is given a new token
+ * and pushed forward in the navigation order.
+ * @type {function(!Blob, number): !Promise<undefined>|undefined}
+ */
+mediaApp.AbstractFile.prototype.saveAs;
 
 /**
  * Wraps an HTML FileList object.
diff --git a/chromeos/components/media_app_ui/resources/js/message_types.js b/chromeos/components/media_app_ui/resources/js/message_types.js
index 7b5b2b42..ae5fe48 100644
--- a/chromeos/components/media_app_ui/resources/js/message_types.js
+++ b/chromeos/components/media_app_ui/resources/js/message_types.js
@@ -21,6 +21,7 @@
   OVERWRITE_FILE: 'overwrite-file',
   RENAME_FILE: 'rename-file',
   REQUEST_SAVE_FILE: 'request-save-file',
+  SAVE_AS: 'save-as',
   SAVE_COPY: 'save-copy'
 };
 
@@ -138,3 +139,22 @@
  * @typedef {{blob: !Blob, token: number}}
  */
 let SaveCopyMessage;
+
+/**
+ * Message sent by the unprivileged context to the privileged context requesting
+ * for the provided blob to be saved in the location specified by
+ * `pickedFileToken`. Once saved the new file takes over oldFileToken if it is
+ * provided, else it gives itself a fresh token, then it becomes currently
+ * writable. The file specified by oldFileToken is given a new token and pushed
+ * forward in the navigation order. This method can be called with any file, not
+ * just the currently writable file.
+ * @typedef {{blob: !Blob, oldFileToken: ?number, pickedFileToken: number}}
+ */
+let SaveAsMessage;
+
+/**
+ * Response message sent by the privileged context with the name of the new
+ * current file.
+ * @typedef {{newFilename: string}}
+ */
+let SaveAsResponse;
diff --git a/chromeos/components/media_app_ui/resources/js/receiver.js b/chromeos/components/media_app_ui/resources/js/receiver.js
index 6f81e02..da47c0e 100644
--- a/chromeos/components/media_app_ui/resources/js/receiver.js
+++ b/chromeos/components/media_app_ui/resources/js/receiver.js
@@ -70,6 +70,23 @@
             Message.RENAME_FILE, {token: this.token, newFilename: newName}));
     return renameResponse.renameResult;
   }
+
+  /**
+   * @override
+   * @param {!Blob} blob
+   * @param {number} pickedFileToken
+   * @return {!Promise<undefined>}
+   */
+  async saveAs(blob, pickedFileToken) {
+    /** @type {!SaveAsMessage} */
+    const message = {blob, oldFileToken: this.token, pickedFileToken};
+    const result = /** @type {!SaveAsResponse} */ (
+        await parentMessagePipe.sendMessage(Message.SAVE_AS, message));
+    this.name = result.newFilename;
+    this.blob = blob;
+    this.size = blob.size;
+    this.mimeType = blob.type;
+  }
 }
 
 /**
diff --git a/chromeos/components/media_app_ui/test/driver_api.js b/chromeos/components/media_app_ui/test/driver_api.js
index ffab3aa71..f1b0ac2 100644
--- a/chromeos/components/media_app_ui/test/driver_api.js
+++ b/chromeos/components/media_app_ui/test/driver_api.js
@@ -23,7 +23,7 @@
  *     renameLastFile: (string|undefined),
  *     requestFullscreen: (boolean|undefined),
  *     requestSaveFile: (boolean|undefined),
- *     saveCopy: (boolean|undefined),
+ *     saveAs: (string|undefined),
  *     testQuery: string,
  * }}
  */
diff --git a/chromeos/components/media_app_ui/test/guest_query_receiver.js b/chromeos/components/media_app_ui/test/guest_query_receiver.js
index c152e35..cc46c058 100644
--- a/chromeos/components/media_app_ui/test/guest_query_receiver.js
+++ b/chromeos/components/media_app_ui/test/guest_query_receiver.js
@@ -97,15 +97,23 @@
           existingFile.name, existingFile.mimeType);
       result = token.toString();
     }
-  } else if (data.saveCopy) {
+  } else if (data.saveAs) {
     const existingFile = assertCast(lastReceivedFileList).item(0);
     if (!existingFile) {
-      result = 'saveCopy failed, no file loaded';
+      result = 'saveAs failed, no file loaded';
     } else {
-      const token = await DELEGATE.requestSaveFile(
-          existingFile.name, existingFile.mimeType);
-      await DELEGATE.saveCopy(existingFile, token);
-      result = 'file successfully saved';
+      const file = firstReceivedItem();
+      try {
+        const token = await DELEGATE.requestSaveFile(
+            existingFile.name, existingFile.mimeType);
+        const testBlob = new Blob([data.saveAs]);
+        await assertCast(file.saveAs).call(file, testBlob, token);
+        result = file.name;
+        extraResultData = {blobText: await file.blob.text()};
+      } catch (/** @type{!Error} */ error) {
+        result = `saveAs failed Error: ${error}`;
+        extraResultData = {filename: file.name};
+      }
     }
   } else if (data.getFileErrors) {
     result =
diff --git a/chromeos/components/media_app_ui/test/media_app_ui_browsertest.js b/chromeos/components/media_app_ui/test/media_app_ui_browsertest.js
index 51ad118..0c006a5 100644
--- a/chromeos/components/media_app_ui/test/media_app_ui_browsertest.js
+++ b/chromeos/components/media_app_ui/test/media_app_ui_browsertest.js
@@ -445,10 +445,10 @@
   assertEquals(result, '222');
   assertEquals(currentFiles.length, 3);
 
-  // The error stays on the third, now unopenable. But, since we've advanced,
-  // it has now rotated into the second slot. But! Also we don't validate it
-  // until it rotates into the first slot, so the error won't be present yet.
-  // If we implement pre-loading, this expectation can change to
+  // The error stays on the third, now unopenable. But, since we've advanced, it
+  // has now rotated into the second slot. But! Also we don't validate it until
+  // it rotates into the first slot, so the error won't be present yet. If we
+  // implement pre-loading, this expectation can change to
   // ',NotAllowedError,'.
   assertEquals(await getFileErrors(), ',,');
 
@@ -841,20 +841,68 @@
   testDone();
 });
 
-// Tests the IPC behind the saveCopy delegate function.
-TEST_F('MediaAppUIBrowserTest', 'SaveCopyIPC', async () => {
+// Tests the IPC behind the saveAs function on received files.
+TEST_F('MediaAppUIBrowserTest', 'SaveAsIPC', async () => {
   // Mock out choose file system entries since it can only be interacted with
   // via trusted user gestures.
-  const newFileHandle = new FakeFileSystemFileHandle();
+  const newFileHandle = new FakeFileSystemFileHandle('new_file.jpg');
   window.showSaveFilePicker = () => Promise.resolve(newFileHandle);
   const testImage = await createTestImageFile(10, 10);
-  await loadFile(testImage, new FakeFileSystemFileHandle());
+  const testHandle = new FakeFileSystemFileHandle('original_file.jpg');
+  await loadFile(testImage, testHandle);
+  const originalFileToken = currentFiles[0].token;
+  assertEquals(entryIndex, 0);
 
-  const result = await sendTestMessage({saveCopy: true});
-  assertEquals(result.testQueryResult, 'file successfully saved');
+  const result = await sendTestMessage({saveAs: 'foo'});
 
+  // Make sure the receivedFile object has the correct state.
+  assertEquals(result.testQueryResult, 'new_file.jpg');
+  assertEquals(await result.testQueryResultData['blobText'], 'foo');
+  // Confirm the right string was written to the new file.
   const writeResult = await newFileHandle.lastWritable.closePromise;
-  assertEquals(await writeResult.text(), await testImage.text());
+  assertEquals(await writeResult.text(), 'foo');
+  // Make sure we have created a new file descriptor, and that
+  // the original file is still available.
+  assertEquals(entryIndex, 1);
+  assertEquals(currentFiles[0].handle, testHandle);
+  assertEquals(currentFiles[0].handle.name, 'original_file.jpg');
+  assertNotEquals(currentFiles[0].token, originalFileToken);
+  assertEquals(currentFiles[1].handle, newFileHandle);
+  assertEquals(currentFiles[1].handle.name, 'new_file.jpg');
+  assertEquals(currentFiles[1].token, originalFileToken);
+  assertEquals(tokenMap.get(currentFiles[0].token), currentFiles[0].handle);
+  assertEquals(tokenMap.get(currentFiles[1].token), currentFiles[1].handle);
+  testDone();
+});
+
+// Tests the error handling behind the saveAs function on received files.
+TEST_F('MediaAppUIBrowserTest', 'SaveAsErrorHandling', async () => {
+  // Prevent the trusted context from throwing errors which cause the test to
+  // fail.
+  guestMessagePipe.logClientError = error => console.log(JSON.stringify(error));
+  guestMessagePipe.rethrowErrors = false;
+  const newFileHandle = new FakeFileSystemFileHandle('new_file.jpg');
+  newFileHandle.nextCreateWritableError =
+      new DOMException('Fake exception', 'FakeError');
+  window.showSaveFilePicker = () => Promise.resolve(newFileHandle);
+  const testImage = await createTestImageFile(10, 10);
+  const testHandle = new FakeFileSystemFileHandle('original_file.jpg');
+  await loadFile(testImage, testHandle);
+  const originalFileToken = currentFiles[0].token;
+
+  const result = await sendTestMessage({saveAs: 'foo'});
+
+  // Make sure we revert back to our original state.
+  assertEquals(
+      result.testQueryResult,
+      'saveAs failed Error: FakeError: save-as: Fake exception');
+  assertEquals(result.testQueryResultData['filename'], 'original_file.jpg');
+  assertEquals(entryIndex, 0);
+  assertEquals(currentFiles.length, 1);
+  assertEquals(currentFiles[0].handle, testHandle);
+  assertEquals(currentFiles[0].handle.name, 'original_file.jpg');
+  assertEquals(currentFiles[0].token, originalFileToken);
+  assertEquals(tokenMap.get(currentFiles[0].token), currentFiles[0].handle);
   testDone();
 });
 
diff --git a/chromeos/constants/chromeos_features.cc b/chromeos/constants/chromeos_features.cc
index ad8922a8..2cf5a888 100644
--- a/chromeos/constants/chromeos_features.cc
+++ b/chromeos/constants/chromeos_features.cc
@@ -362,6 +362,10 @@
 const base::Feature kPrintJobManagementApp{"PrintJobManagementApp",
                                            base::FEATURE_ENABLED_BY_DEFAULT};
 
+// Changes Print Preview Save to Drive to use local Drive.
+const base::Feature kPrintSaveToDrive{"PrintSaveToDrive",
+                                      base::FEATURE_DISABLED_BY_DEFAULT};
+
 // Controls whether to enable quick answers.
 const base::Feature kQuickAnswers{"QuickAnswers",
                                   base::FEATURE_DISABLED_BY_DEFAULT};
diff --git a/chromeos/constants/chromeos_features.h b/chromeos/constants/chromeos_features.h
index 411dcd7..37b4855 100644
--- a/chromeos/constants/chromeos_features.h
+++ b/chromeos/constants/chromeos_features.h
@@ -157,6 +157,8 @@
 COMPONENT_EXPORT(CHROMEOS_CONSTANTS)
 extern const base::Feature kPrintJobManagementApp;
 COMPONENT_EXPORT(CHROMEOS_CONSTANTS)
+extern const base::Feature kPrintSaveToDrive;
+COMPONENT_EXPORT(CHROMEOS_CONSTANTS)
 extern const base::Feature kPrinterStatus;
 COMPONENT_EXPORT(CHROMEOS_CONSTANTS) extern const base::Feature kQuickAnswers;
 COMPONENT_EXPORT(CHROMEOS_CONSTANTS)
diff --git a/components/arc/mojom/audio.mojom b/components/arc/mojom/audio.mojom
index 6d90a02..eb9a344c7 100644
--- a/components/arc/mojom/audio.mojom
+++ b/components/arc/mojom/audio.mojom
@@ -19,7 +19,7 @@
   ShowVolumeControls@0();
 
   // Request that the volume be changed to |volume|.
-  // This is a privileged API and should only be used on whitelisted cases.
+  // This is a privileged API and should only be used on allowlisted cases.
   // |percent| is of the range [0, 100].
   [MinVersion=3] OnSystemVolumeUpdateRequest@1(int32 percent);
 };
diff --git a/components/arc/mojom/auth.mojom b/components/arc/mojom/auth.mojom
index 8045fa16..d51576f 100644
--- a/components/arc/mojom/auth.mojom
+++ b/components/arc/mojom/auth.mojom
@@ -132,8 +132,8 @@
     ERROR_ACCOUNT_NOT_READY = 14,
     // Checkin failed.
     ERROR_CHECKIN_FAILED = 15,
-    // Issues with whitelisting work account.
-    ERROR_ACCOUNT_NOT_WHITELISTED = 16,
+    // Issues with allowlisting work account.
+    ERROR_ACCOUNT_NOT_ALLOWLISTED = 16,
     // Error parsing JSON, most likely policy JSON
     ERROR_JSON = 17,
     // ManagedProvisioning failed.
diff --git a/components/arc/mojom/cert_store.mojom b/components/arc/mojom/cert_store.mojom
index 24ec3fb3..984ffdd 100644
--- a/components/arc/mojom/cert_store.mojom
+++ b/components/arc/mojom/cert_store.mojom
@@ -75,7 +75,7 @@
 interface CertStoreHost {
   // The helper method, which does not correspond to keymaster interface.
   // It returns a list of Chrome OS corporate usage client certificates if
-  // any Android app is whitelisted to use them, otherwise returns an
+  // any Android app is allowlisted to use them, otherwise returns an
   // empty list.
   ListCertificates@0() => (array<Certificate> certs);
 
diff --git a/components/arc/mojom/intent_helper.mojom b/components/arc/mojom/intent_helper.mojom
index 697d6c8..989195a 100644
--- a/components/arc/mojom/intent_helper.mojom
+++ b/components/arc/mojom/intent_helper.mojom
@@ -392,8 +392,8 @@
   // specified.  Data can be sent as extras by including a JSON map string which
   // will be automatically converted to a bundle accessible by the receiver.
   //
-  // Note: Broadcasts can only be sent to whitelisted packages.  Packages can be
-  // added to the whitelist in ArcBridgeService.java in the Android source.
+  // Note: Broadcasts can only be sent to allowlisted packages.  Packages can be
+  // added to the allowlist in ArcBridgeService.java in the Android source.
   [MinVersion=1] SendBroadcast@1(string action,
                                  string package_name,
                                  string cls,
diff --git a/components/arc/session/arc_property_util.cc b/components/arc/session/arc_property_util.cc
index ccdca60f..d1ee992 100644
--- a/components/arc/session/arc_property_util.cc
+++ b/components/arc/session/arc_property_util.cc
@@ -15,6 +15,7 @@
 #include "base/process/launch.h"
 #include "base/strings/string_split.h"
 #include "base/strings/string_util.h"
+#include "base/strings/stringprintf.h"
 #include "chromeos/constants/chromeos_switches.h"
 
 namespace brillo {
@@ -40,11 +41,12 @@
 constexpr char kPAIRegionsPropertyName[] = "pai-regions";
 
 // Properties related to dynamically adding native bridge 64 bit support.
-constexpr char kAbilistPropertyPrefix[] = "ro.product.cpu.abilist=";
+constexpr char kAbilistPropertyPrefixTemplate[] = "ro.%sproduct.cpu.abilist=";
 constexpr char kAbilistPropertyExpected[] = "x86_64,x86,armeabi-v7a,armeabi";
 constexpr char kAbilistPropertyReplacement[] =
     "x86_64,x86,arm64-v8a,armeabi-v7a,armeabi";
-constexpr char kAbilist64PropertyPrefix[] = "ro.product.cpu.abilist64=";
+constexpr char kAbilist64PropertyPrefixTemplate[] =
+    "ro.%sproduct.cpu.abilist64=";
 constexpr char kAbilist64PropertyExpected[] = "x86_64";
 constexpr char kAbilist64PropertyReplacement[] = "x86_64,arm64-v8a";
 constexpr char kDalvikVmIsaArm64[] = "ro.dalvik.vm.isa.arm64=x86_64";
@@ -158,7 +160,9 @@
 bool ExpandPropertyContents(const std::string& content,
                             brillo::CrosConfigInterface* config,
                             std::string* expanded_content,
-                            bool add_native_bridge_64bit_support) {
+                            bool add_native_bridge_64bit_support,
+                            bool append_dalvik_isa,
+                            const std::string& partition_name) {
   const std::vector<std::string> lines = base::SplitString(
       content, "\n", base::WhitespaceHandling::KEEP_WHITESPACE,
       base::SplitResult::SPLIT_WANT_ALL);
@@ -204,27 +208,30 @@
         expanded += line.substr(prev_match);
       line = expanded;
     } while (inserted);
+
     if (add_native_bridge_64bit_support) {
-      // Special-case ro.product.cpu.abilist / ro.product.cpu.abilist64 to add
-      // ARM64.
+      // Special-case ro.<partition>.product.cpu.abilist and
+      // ro.<partition>.product.cpu.abilist64 to add ARM64.
+      std::string prefix = base::StringPrintf(kAbilistPropertyPrefixTemplate,
+                                              partition_name.c_str());
       std::string value;
-      if (FindProperty(kAbilistPropertyPrefix, &value, line)) {
+      if (FindProperty(prefix, &value, line)) {
         if (value == kAbilistPropertyExpected) {
-          line = std::string(kAbilistPropertyPrefix) +
-                 std::string(kAbilistPropertyReplacement);
+          line = prefix + std::string(kAbilistPropertyReplacement);
         } else {
-          LOG(ERROR) << "Found unexpected value for " << kAbilistPropertyPrefix
-                     << ", value " << value;
+          LOG(ERROR) << "Found unexpected value for " << prefix << ", value "
+                     << value;
           return false;
         }
       }
-      if (FindProperty(kAbilist64PropertyPrefix, &value, line)) {
+      prefix = base::StringPrintf(kAbilist64PropertyPrefixTemplate,
+                                  partition_name.c_str());
+      if (FindProperty(prefix, &value, line)) {
         if (value == kAbilist64PropertyExpected) {
-          line = std::string(kAbilist64PropertyPrefix) +
-                 std::string(kAbilist64PropertyReplacement);
+          line = prefix + std::string(kAbilist64PropertyReplacement);
         } else {
-          LOG(ERROR) << "Found unexpected value for "
-                     << kAbilist64PropertyPrefix << ", value " << value;
+          LOG(ERROR) << "Found unexpected value for " << prefix << ", value "
+                     << value;
           return false;
         }
       }
@@ -248,7 +255,7 @@
     }
   }
 
-  if (add_native_bridge_64bit_support) {
+  if (append_dalvik_isa) {
     // Special-case to add ro.dalvik.vm.isa.arm64.
     new_properties += std::string(kDalvikVmIsaArm64) + "\n";
   }
@@ -261,7 +268,9 @@
                         const base::FilePath& output,
                         CrosConfig* config,
                         bool append,
-                        bool add_native_bridge_64bit_support) {
+                        bool add_native_bridge_64bit_support,
+                        bool append_dalvik_isa,
+                        const std::string& partition_name) {
   std::string content;
   std::string expanded;
   if (!base::ReadFileToString(input, &content)) {
@@ -269,7 +278,8 @@
     return false;
   }
   if (!ExpandPropertyContents(content, config, &expanded,
-                              add_native_bridge_64bit_support))
+                              add_native_bridge_64bit_support,
+                              append_dalvik_isa, partition_name))
     return false;
   if (append && base::PathExists(output)) {
     if (!base::AppendToFile(output, expanded.data(), expanded.size())) {
@@ -330,7 +340,8 @@
                                       brillo::CrosConfigInterface* config,
                                       std::string* expanded_content) {
   return ExpandPropertyContents(content, config, expanded_content,
-                                /*add_native_bridge_64bit_support=*/false);
+                                /*add_native_bridge_64bit_support=*/false,
+                                false, "");
 }
 
 bool TruncateAndroidPropertyForTesting(const std::string& line,
@@ -342,7 +353,8 @@
                                   const base::FilePath& output,
                                   CrosConfig* config) {
   return ExpandPropertyFile(input, output, config, /*append=*/false,
-                            /*add_native_bridge_64bit_support=*/false);
+                            /*add_native_bridge_64bit_support=*/false, false,
+                            "");
 }
 
 bool ExpandPropertyFiles(const base::FilePath& source_path,
@@ -355,13 +367,24 @@
 
   // default.prop may not exist. Silently skip it if not found.
   for (const auto& tuple :
-       {std::tuple<const char*, bool, bool>{"default.prop", true, false},
-        {"build.prop", false, true},
-        {"vendor_build.prop", false, false}}) {
+       // The order has to match the one in PropertyLoadBootDefaults() in
+       // system/core/init/property_service.cpp.
+       // Note: Our vendor image doesn't have /vendor/default.prop although
+       // PropertyLoadBootDefaults() tries to open it.
+       {std::tuple<const char*, bool, bool, const char*>{"default.prop", true,
+                                                         false, ""},
+        {"build.prop", false, true, ""},
+        {"system_ext_build.prop", true, false, "system_ext."},
+        {"vendor_build.prop", false, false, "vendor."},
+        {"odm_build.prop", true, false, "odm."},
+        {"product_build.prop", true, false, "product."}}) {
     const char* file = std::get<0>(tuple);
     const bool is_optional = std::get<1>(tuple);
-    const bool add_native_bridge_properties =
+    // When true, unconditionally add |kDalvikVmIsaArm64| property.
+    const bool append_dalvik_isa =
         std::get<2>(tuple) && add_native_bridge_64bit_support;
+    // Search for ro.<partition_name>product.cpu.abilist* properties.
+    const char* partition_name = std::get<3>(tuple);
 
     if (is_optional && !base::PathExists(source_path.Append(file)))
       continue;
@@ -369,7 +392,8 @@
     if (!ExpandPropertyFile(
             source_path.Append(file),
             single_file ? dest_path : dest_path.Append(file), &config,
-            /*append=*/single_file, add_native_bridge_properties)) {
+            /*append=*/single_file, add_native_bridge_64bit_support,
+            append_dalvik_isa, partition_name)) {
       LOG(ERROR) << "Failed to expand " << source_path.Append(file);
       return false;
     }
diff --git a/components/arc/session/arc_property_util_unittest.cc b/components/arc/session/arc_property_util_unittest.cc
index 55126dd..fec2e8ee 100644
--- a/components/arc/session/arc_property_util_unittest.cc
+++ b/components/arc/session/arc_property_util_unittest.cc
@@ -379,54 +379,88 @@
   EXPECT_FALSE(ExpandPropertyFiles(source_dir, dest_prop_file, true, false));
 
   // Add default.prop to the source, but not build.prop.
-  base::FilePath default_prop = source_dir.Append("default.prop");
+  const base::FilePath default_prop = source_dir.Append("default.prop");
   constexpr const char kDefaultProp[] = "ro.foo=bar\n";
   base::WriteFile(default_prop, kDefaultProp, strlen(kDefaultProp));
   EXPECT_FALSE(ExpandPropertyFiles(source_dir, dest_prop_file, true, false));
 
   // Add build.prop too. The call should not succeed still.
-  base::FilePath build_prop = source_dir.Append("build.prop");
+  const base::FilePath build_prop = source_dir.Append("build.prop");
   constexpr const char kBuildProp[] = "ro.baz=boo\n";
   base::WriteFile(build_prop, kBuildProp, strlen(kBuildProp));
   EXPECT_FALSE(ExpandPropertyFiles(source_dir, dest_prop_file, true, false));
 
   // Add vendor_build.prop too. Then the call should succeed.
-  base::FilePath vendor_build_prop = source_dir.Append("vendor_build.prop");
+  const base::FilePath vendor_build_prop =
+      source_dir.Append("vendor_build.prop");
   constexpr const char kVendorBuildProp[] = "ro.a=b\n";
   base::WriteFile(vendor_build_prop, kVendorBuildProp,
                   strlen(kVendorBuildProp));
   EXPECT_TRUE(ExpandPropertyFiles(source_dir, dest_prop_file, true, false));
 
+  // Add other optional files too. Then the call should succeed.
+  const base::FilePath system_ext_build_prop =
+      source_dir.Append("system_ext_build.prop");
+  constexpr const char kSystemExtBuildProp[] = "ro.c=d\n";
+  base::WriteFile(system_ext_build_prop, kSystemExtBuildProp,
+                  strlen(kSystemExtBuildProp));
+  EXPECT_TRUE(ExpandPropertyFiles(source_dir, dest_prop_file, true, false));
+
+  const base::FilePath odm_build_prop = source_dir.Append("odm_build.prop");
+  constexpr const char kOdmBuildProp[] = "ro.e=f\n";
+  base::WriteFile(odm_build_prop, kOdmBuildProp, strlen(kOdmBuildProp));
+  EXPECT_TRUE(ExpandPropertyFiles(source_dir, dest_prop_file, true, false));
+
+  const base::FilePath product_build_prop =
+      source_dir.Append("product_build.prop");
+  constexpr const char kProductBuildProp[] = "ro.g=h\n";
+  base::WriteFile(product_build_prop, kProductBuildProp,
+                  strlen(kProductBuildProp));
+  EXPECT_TRUE(ExpandPropertyFiles(source_dir, dest_prop_file, true, false));
+
   // Verify only one dest file exists.
   EXPECT_FALSE(
       base::PathExists(dest_prop_file.DirName().Append("default.prop")));
   EXPECT_FALSE(base::PathExists(dest_prop_file.DirName().Append("build.prop")));
   EXPECT_FALSE(
       base::PathExists(dest_prop_file.DirName().Append("vendor_build.prop")));
+  EXPECT_FALSE(base::PathExists(
+      dest_prop_file.DirName().Append("system_ext_build.prop")));
+  EXPECT_FALSE(
+      base::PathExists(dest_prop_file.DirName().Append("odm_build.prop")));
+  EXPECT_FALSE(
+      base::PathExists(dest_prop_file.DirName().Append("product_build.prop")));
   EXPECT_TRUE(base::PathExists(dest_prop_file));
 
   // Verify the content.
   // Note: ExpandPropertyFileForTesting() adds a trailing LF.
   std::string content;
   EXPECT_TRUE(base::ReadFileToString(dest_prop_file, &content));
-  EXPECT_EQ(base::StringPrintf("%s\n%s\n%s\n", kDefaultProp, kBuildProp,
-                               kVendorBuildProp),
-            content);
+  EXPECT_EQ(
+      base::StringPrintf("%s\n%s\n%s\n%s\n%s\n%s\n", kDefaultProp, kBuildProp,
+                         kSystemExtBuildProp, kVendorBuildProp, kOdmBuildProp,
+                         kProductBuildProp),
+      content);
 
   // Expand it again, verify the previous result is cleared.
   EXPECT_TRUE(ExpandPropertyFiles(source_dir, dest_prop_file, true, false));
   EXPECT_TRUE(base::ReadFileToString(dest_prop_file, &content));
-  EXPECT_EQ(base::StringPrintf("%s\n%s\n%s\n", kDefaultProp, kBuildProp,
-                               kVendorBuildProp),
-            content);
+  EXPECT_EQ(
+      base::StringPrintf("%s\n%s\n%s\n%s\n%s\n%s\n", kDefaultProp, kBuildProp,
+                         kSystemExtBuildProp, kVendorBuildProp, kOdmBuildProp,
+                         kProductBuildProp),
+      content);
 
-  // If default.prop does not exist in the source path, it should still process
-  // the other files.
+  // If optional ones e.g. default.prop does not exist in the source path, it
+  // should still process the other files.
   base::DeleteFile(source_dir.Append("default.prop"));
+  base::DeleteFile(source_dir.Append("odm_build.prop"));
   EXPECT_TRUE(ExpandPropertyFiles(source_dir, dest_prop_file, true, false));
   EXPECT_TRUE(base::ReadFileToString(dest_prop_file, &content));
-  EXPECT_EQ(base::StringPrintf("%s\n%s\n", kBuildProp, kVendorBuildProp),
-            content);
+  EXPECT_EQ(
+      base::StringPrintf("%s\n%s\n%s\n%s\n", kBuildProp, kSystemExtBuildProp,
+                         kVendorBuildProp, kProductBuildProp),
+      content);
 
   // Finally, test the case where source is valid but the dest is not.
   EXPECT_FALSE(ExpandPropertyFiles(source_dir, base::FilePath("/nonexistent"),
@@ -454,7 +488,10 @@
   base::WriteFile(build_prop, kBuildProp, strlen(kBuildProp));
 
   base::FilePath vendor_build_prop = source_dir.Append("vendor_build.prop");
-  constexpr const char kVendorBuildProp[] = "ro.a=b\n";
+  constexpr const char kVendorBuildProp[] =
+      "ro.a=b\n"
+      "ro.vendor.product.cpu.abilist=x86_64,x86,armeabi-v7a,armeabi\n"
+      "ro.vendor.product.cpu.abilist64=x86_64\n";
   base::WriteFile(vendor_build_prop, kVendorBuildProp,
                   strlen(kVendorBuildProp));
 
@@ -485,7 +522,11 @@
   EXPECT_EQ(std::string(kBuildPropModified) + "\n", content);
   EXPECT_TRUE(
       base::ReadFileToString(dest_dir.Append("vendor_build.prop"), &content));
-  EXPECT_EQ(std::string(kVendorBuildProp) + "\n", content);
+  constexpr const char kVendorBuildPropModified[] =
+      "ro.a=b\n"
+      "ro.vendor.product.cpu.abilist=x86_64,x86,arm64-v8a,armeabi-v7a,armeabi\n"
+      "ro.vendor.product.cpu.abilist64=x86_64,arm64-v8a\n";
+  EXPECT_EQ(std::string(kVendorBuildPropModified) + "\n", content);
 
   // Expand to a single file with experiment on, verify properties are added /
   // modified as expected.
@@ -498,7 +539,7 @@
   // Verify the contents.
   EXPECT_TRUE(base::ReadFileToString(dest_prop_file, &content));
   EXPECT_EQ(base::StringPrintf("%s\n%s\n%s\n", kDefaultProp, kBuildPropModified,
-                               kVendorBuildProp),
+                               kVendorBuildPropModified),
             content);
 
   // Verify that unexpected property values generate an error.
diff --git a/components/autofill/core/browser/ui/popup_types.h b/components/autofill/core/browser/ui/popup_types.h
index 1656116..21918b9 100644
--- a/components/autofill/core/browser/ui/popup_types.h
+++ b/components/autofill/core/browser/ui/popup_types.h
@@ -34,6 +34,9 @@
   kUserAborted,    // The user explicitly dismissed the popup (e.g. ESC key).
   kViewDestroyed,  // The popup view (or its controller) goes out of scope.
   kWidgetChanged,  // The platform-native UI changed (e.g. window resize).
+  kInsufficientSpace,  // Not enough space in content area to display an display
+                       // at least one row of the popup within the bounds of the
+                       // content area.
 };
 
 }  // namespace autofill
diff --git a/components/feed/core/v2/enums.cc b/components/feed/core/v2/enums.cc
index a675de4..3dd3af87 100644
--- a/components/feed/core/v2/enums.cc
+++ b/components/feed/core/v2/enums.cc
@@ -33,8 +33,10 @@
       return out << "kDataInStoreIsStale";
     case LoadStreamStatus::kDataInStoreIsStaleTimestampInFuture:
       return out << "kDataInStoreIsStaleTimestampInFuture";
-    case LoadStreamStatus::kCannotLoadFromNetworkSupressedForHistoryDelete:
-      return out << "kCannotLoadFromNetworkSupressedForHistoryDelete";
+    case LoadStreamStatus::
+        kCannotLoadFromNetworkSupressedForHistoryDelete_DEPRECATED:
+      return out
+             << "kCannotLoadFromNetworkSupressedForHistoryDelete_DEPRECATED";
     case LoadStreamStatus::kCannotLoadFromNetworkOffline:
       return out << "kCannotLoadFromNetworkOffline";
     case LoadStreamStatus::kCannotLoadFromNetworkThrottled:
diff --git a/components/feed/core/v2/enums.h b/components/feed/core/v2/enums.h
index ae0c95c..dfb9544 100644
--- a/components/feed/core/v2/enums.h
+++ b/components/feed/core/v2/enums.h
@@ -31,7 +31,7 @@
   // The timestamp for stored data is in the future, so we're treating stored
   // data as it it is stale.
   kDataInStoreIsStaleTimestampInFuture = 9,
-  kCannotLoadFromNetworkSupressedForHistoryDelete = 10,
+  kCannotLoadFromNetworkSupressedForHistoryDelete_DEPRECATED = 10,
   kCannotLoadFromNetworkOffline = 11,
   kCannotLoadFromNetworkThrottled = 12,
   kLoadNotAllowedEulaNotAccepted = 13,
diff --git a/components/feed/core/v2/feed_network.h b/components/feed/core/v2/feed_network.h
index 9e585ec..40d93da 100644
--- a/components/feed/core/v2/feed_network.h
+++ b/components/feed/core/v2/feed_network.h
@@ -49,6 +49,7 @@
   // |CancelRequests()|.
   virtual void SendQueryRequest(
       const feedwire::Request& request,
+      bool force_signed_out_request,
       base::OnceCallback<void(QueryRequestResult)> callback) = 0;
 
   // Send a feedwire::FeedActionRequest, and receive the response in |callback|.
diff --git a/components/feed/core/v2/feed_network_impl.cc b/components/feed/core/v2/feed_network_impl.cc
index 433500e..9ed2346a 100644
--- a/components/feed/core/v2/feed_network_impl.cc
+++ b/components/feed/core/v2/feed_network_impl.cc
@@ -146,6 +146,7 @@
   NetworkFetch(const GURL& url,
                const std::string& request_type,
                std::string request_body,
+               bool force_signed_out_request,
                signin::IdentityManager* identity_manager,
                network::SharedURLLoaderFactory* loader_factory,
                const std::string& api_key,
@@ -154,6 +155,7 @@
       : url_(url),
         request_type_(request_type),
         request_body_(std::move(request_body)),
+        force_signed_out_request_(force_signed_out_request),
         identity_manager_(identity_manager),
         loader_factory_(loader_factory),
         api_key_(api_key),
@@ -182,7 +184,7 @@
   void Start(base::OnceCallback<void(RawResponse)> done_callback) {
     done_callback_ = std::move(done_callback);
 
-    if (!identity_manager_->HasPrimaryAccount()) {
+    if (force_signed_out_request_ || !identity_manager_->HasPrimaryAccount()) {
       StartLoader();
       return;
     }
@@ -381,6 +383,7 @@
   const std::string request_type_;
   std::string access_token_;
   const std::string request_body_;
+  bool force_signed_out_request_;
   signin::IdentityManager* const identity_manager_;
   std::unique_ptr<signin::PrimaryAccountAccessTokenFetcher> token_fetcher_;
   std::unique_ptr<network::SimpleURLLoader> simple_loader_;
@@ -417,6 +420,7 @@
 
 void FeedNetworkImpl::SendQueryRequest(
     const feedwire::Request& request,
+    bool force_signed_out_request,
     base::OnceCallback<void(QueryRequestResult)> callback) {
   std::string binary_proto;
   request.SerializeToString(&binary_proto);
@@ -432,7 +436,7 @@
 
   AddMothershipPayloadQueryParams(base64proto, delegate_->GetLanguageTag(),
                                   url);
-  Send(url, "GET", /*request_body=*/{},
+  Send(url, "GET", /*request_body=*/{}, force_signed_out_request,
        base::BindOnce(&ParseAndForwardResponse<QueryRequestResult,
                                                NetworkRequestType::kFeedQuery>,
                       std::move(callback)));
@@ -445,6 +449,7 @@
   request.SerializeToString(&binary_proto);
 
   Send(GURL(kUploadActionUrl), "POST", std::move(binary_proto),
+       /*force_signed_out_request=*/false,
        base::BindOnce(
            &ParseAndForwardResponse<ActionRequestResult,
                                     NetworkRequestType::kUploadActions>,
@@ -458,10 +463,12 @@
 void FeedNetworkImpl::Send(const GURL& url,
                            const std::string& request_type,
                            std::string request_body,
+                           bool force_signed_out_request,
                            base::OnceCallback<void(RawResponse)> callback) {
   auto fetch = std::make_unique<NetworkFetch>(
-      url, request_type, std::move(request_body), identity_manager_,
-      loader_factory_.get(), api_key_, tick_clock_, pref_service_);
+      url, request_type, std::move(request_body), force_signed_out_request,
+      identity_manager_, loader_factory_.get(), api_key_, tick_clock_,
+      pref_service_);
   NetworkFetch* fetch_unowned = fetch.get();
   pending_requests_.emplace(std::move(fetch));
 
diff --git a/components/feed/core/v2/feed_network_impl.h b/components/feed/core/v2/feed_network_impl.h
index 660780e..c8d487d 100644
--- a/components/feed/core/v2/feed_network_impl.h
+++ b/components/feed/core/v2/feed_network_impl.h
@@ -53,6 +53,7 @@
 
   void SendQueryRequest(
       const feedwire::Request& request,
+      bool force_signed_out_request,
       base::OnceCallback<void(QueryRequestResult)> callback) override;
 
   void SendActionRequest(
@@ -71,6 +72,7 @@
   void Send(const GURL& url,
             const std::string& request_type,
             std::string request_body,
+            bool force_signed_out_request,
             base::OnceCallback<void(FeedNetworkImpl::RawResponse)> callback);
 
   void SendComplete(NetworkFetch* fetch,
diff --git a/components/feed/core/v2/feed_network_impl_unittest.cc b/components/feed/core/v2/feed_network_impl_unittest.cc
index c86564d..69d4cce 100644
--- a/components/feed/core/v2/feed_network_impl_unittest.cc
+++ b/components/feed/core/v2/feed_network_impl_unittest.cc
@@ -189,7 +189,7 @@
 
 TEST_F(FeedNetworkTest, SendQueryRequestEmpty) {
   CallbackReceiver<QueryRequestResult> receiver;
-  feed_network()->SendQueryRequest(feedwire::Request(), receiver.Bind());
+  feed_network()->SendQueryRequest(feedwire::Request(), false, receiver.Bind());
 
   ASSERT_TRUE(receiver.GetResult());
   const QueryRequestResult& result = *receiver.GetResult();
@@ -199,7 +199,8 @@
 
 TEST_F(FeedNetworkTest, SendQueryRequestSendsValidRequest) {
   CallbackReceiver<QueryRequestResult> receiver;
-  feed_network()->SendQueryRequest(GetTestFeedRequest(), receiver.Bind());
+  feed_network()->SendQueryRequest(GetTestFeedRequest(), false,
+                                   receiver.Bind());
   network::ResourceRequest resource_request =
       RespondToQueryRequest("", net::HTTP_OK);
 
@@ -217,9 +218,24 @@
       "ContentSuggestions.Feed.Network.ResponseStatus.FeedQuery", 200, 1);
 }
 
+TEST_F(FeedNetworkTest, SendQueryRequestForceSignedOut) {
+  CallbackReceiver<QueryRequestResult> receiver;
+  feed_network()->SendQueryRequest(
+      GetTestFeedRequest(), /*force_signed_out_request=*/true, receiver.Bind());
+  network::ResourceRequest resource_request =
+      RespondToQueryRequest("", net::HTTP_OK);
+
+  EXPECT_EQ(
+      "https://www.google.com/httpservice/retry/TrellisClankService/"
+      "FeedQuery?reqpld=CAHCPgQSAggB&fmt=bin&hl=en&key=dummy_api_key",
+      resource_request.url);
+  EXPECT_FALSE(resource_request.headers.HasHeader("Authorization"));
+}
+
 TEST_F(FeedNetworkTest, SendQueryRequestInvalidResponse) {
   CallbackReceiver<QueryRequestResult> receiver;
-  feed_network()->SendQueryRequest(GetTestFeedRequest(), receiver.Bind());
+  feed_network()->SendQueryRequest(GetTestFeedRequest(), false,
+                                   receiver.Bind());
   RespondToQueryRequest("invalid", net::HTTP_OK);
 
   ASSERT_TRUE(receiver.GetResult());
@@ -230,7 +246,8 @@
 
 TEST_F(FeedNetworkTest, SendQueryRequestReceivesResponse) {
   CallbackReceiver<QueryRequestResult> receiver;
-  feed_network()->SendQueryRequest(GetTestFeedRequest(), receiver.Bind());
+  feed_network()->SendQueryRequest(GetTestFeedRequest(), false,
+                                   receiver.Bind());
   RespondToQueryRequest(GetTestFeedResponse(), net::HTTP_OK);
 
   ASSERT_TRUE(receiver.GetResult());
@@ -246,7 +263,8 @@
 
 TEST_F(FeedNetworkTest, SendQueryRequestIgnoresBodyForNon200Response) {
   CallbackReceiver<QueryRequestResult> receiver;
-  feed_network()->SendQueryRequest(GetTestFeedRequest(), receiver.Bind());
+  feed_network()->SendQueryRequest(GetTestFeedRequest(), false,
+                                   receiver.Bind());
   RespondToQueryRequest(GetTestFeedResponse(), net::HTTP_FORBIDDEN);
 
   ASSERT_TRUE(receiver.GetResult());
@@ -260,7 +278,8 @@
 
 TEST_F(FeedNetworkTest, CancelRequest) {
   CallbackReceiver<QueryRequestResult> receiver;
-  feed_network()->SendQueryRequest(GetTestFeedRequest(), receiver.Bind());
+  feed_network()->SendQueryRequest(GetTestFeedRequest(), false,
+                                   receiver.Bind());
   feed_network()->CancelRequests();
   task_environment_.FastForwardUntilNoTasksRemain();
 
@@ -270,7 +289,8 @@
 TEST_F(FeedNetworkTest, RequestTimeout) {
   base::HistogramTester histogram_tester;
   CallbackReceiver<QueryRequestResult> receiver;
-  feed_network()->SendQueryRequest(GetTestFeedRequest(), receiver.Bind());
+  feed_network()->SendQueryRequest(GetTestFeedRequest(), false,
+                                   receiver.Bind());
   task_environment_.FastForwardBy(TimeDelta::FromSeconds(30));
 
   ASSERT_TRUE(receiver.GetResult());
@@ -283,11 +303,12 @@
 
 TEST_F(FeedNetworkTest, ParallelRequests) {
   CallbackReceiver<QueryRequestResult> receiver1, receiver2;
-  feed_network()->SendQueryRequest(GetTestFeedRequest(), receiver1.Bind());
+  feed_network()->SendQueryRequest(GetTestFeedRequest(), false,
+                                   receiver1.Bind());
   // Make another request with a different URL so Respond() won't affect both
   // requests.
   feed_network()->SendQueryRequest(
-      GetTestFeedRequest(feedwire::FeedQuery::NEXT_PAGE_SCROLL),
+      GetTestFeedRequest(feedwire::FeedQuery::NEXT_PAGE_SCROLL), false,
       receiver2.Bind());
 
   // Respond to both requests, avoiding FastForwardUntilNoTasksRemain until
@@ -310,7 +331,8 @@
 TEST_F(FeedNetworkTest, ShouldReportResponseStatusCode) {
   CallbackReceiver<QueryRequestResult> receiver;
   base::HistogramTester histogram_tester;
-  feed_network()->SendQueryRequest(GetTestFeedRequest(), receiver.Bind());
+  feed_network()->SendQueryRequest(GetTestFeedRequest(), false,
+                                   receiver.Bind());
   RespondToQueryRequest(GetTestFeedResponse(), net::HTTP_FORBIDDEN);
 
   EXPECT_THAT(
@@ -324,7 +346,8 @@
   CallbackReceiver<QueryRequestResult> receiver;
   base::HistogramTester histogram_tester;
 
-  feed_network()->SendQueryRequest(GetTestFeedRequest(), receiver.Bind());
+  feed_network()->SendQueryRequest(GetTestFeedRequest(), false,
+                                   receiver.Bind());
   identity_env()->WaitForAccessTokenRequestIfNecessaryAndRespondWithError(
       GoogleServiceAuthError(
           GoogleServiceAuthError::State::INVALID_GAIA_CREDENTIALS));
@@ -349,7 +372,8 @@
 TEST_F(FeedNetworkTest, ShouldIncludeAPIKeyForNoSignedInUser) {
   identity_env()->ClearPrimaryAccount();
   CallbackReceiver<QueryRequestResult> receiver;
-  feed_network()->SendQueryRequest(GetTestFeedRequest(), receiver.Bind());
+  feed_network()->SendQueryRequest(GetTestFeedRequest(), false,
+                                   receiver.Bind());
 
   network::ResourceRequest resource_request =
       RespondToQueryRequest(GetTestFeedResponse(), net::HTTP_OK);
@@ -364,7 +388,8 @@
   CallbackReceiver<QueryRequestResult> receiver;
   const TimeDelta kDuration = TimeDelta::FromMilliseconds(12345);
 
-  feed_network()->SendQueryRequest(GetTestFeedRequest(), receiver.Bind());
+  feed_network()->SendQueryRequest(GetTestFeedRequest(), false,
+                                   receiver.Bind());
   task_environment_.FastForwardBy(kDuration);
   RespondToQueryRequest(GetTestFeedResponse(), net::HTTP_OK);
 
@@ -379,7 +404,8 @@
   CallbackReceiver<QueryRequestResult> receiver;
   profile_prefs().SetString(feed::prefs::kHostOverrideHost,
                             "http://www.newhost.com/");
-  feed_network()->SendQueryRequest(GetTestFeedRequest(), receiver.Bind());
+  feed_network()->SendQueryRequest(GetTestFeedRequest(), false,
+                                   receiver.Bind());
 
   response_headers_ = base::MakeRefCounted<net::HttpResponseHeaders>(
       net::HttpUtil::AssembleRawHeaders(
diff --git a/components/feed/core/v2/feed_stream.cc b/components/feed/core/v2/feed_stream.cc
index 6b5a4aa..68e604b 100644
--- a/components/feed/core/v2/feed_stream.cc
+++ b/components/feed/core/v2/feed_stream.cc
@@ -467,16 +467,6 @@
     }
   }
 
-  // TODO(harringtond): |suppress_refreshes_until_| was historically used
-  // for privacy purposes after clearing data to make sure sync data made it
-  // to the server. I'm not sure we need this now. But also, it was
-  // documented as not affecting manually triggered refreshes, but coded in
-  // a way that it does. I've tried to keep the same functionality as the
-  // old feed code, but we should revisit this.
-  if (tick_clock_->NowTicks() < suppress_refreshes_until_) {
-    return LoadStreamStatus::kCannotLoadFromNetworkSupressedForHistoryDelete;
-  }
-
   if (delegate_->IsOffline()) {
     return LoadStreamStatus::kCannotLoadFromNetworkOffline;
   }
@@ -489,6 +479,10 @@
   return LoadStreamStatus::kNoStatus;
 }
 
+bool FeedStream::ShouldForceSignedOutFeedQueryRequest() const {
+  return base::TimeTicks::Now() < signed_out_refreshes_until_;
+}
+
 RequestMetadata FeedStream::GetRequestMetadata() {
   RequestMetadata result;
   result.chrome_info = chrome_info_;
@@ -503,11 +497,10 @@
     TriggerStreamLoad();
 }
 
-void FeedStream::OnHistoryDeleted() {
-  // Due to privacy, we should not fetch for a while (unless the user
-  // explicitly asks for new suggestions) to give sync the time to propagate
-  // the changes in history to the server.
-  suppress_refreshes_until_ =
+void FeedStream::OnAllHistoryDeleted() {
+  // Give sync the time to propagate the changes in history to the server.
+  // In the interim, only send signed-out FeedQuery requests.
+  signed_out_refreshes_until_ =
       tick_clock_->NowTicks() + kSuppressRefreshDuration;
   ClearAll();
 }
diff --git a/components/feed/core/v2/feed_stream.h b/components/feed/core/v2/feed_stream.h
index 8b4ae07..19c8f3b 100644
--- a/components/feed/core/v2/feed_stream.h
+++ b/components/feed/core/v2/feed_stream.h
@@ -175,8 +175,8 @@
   void OnSignedIn();
   // The user signed out of Chrome.
   void OnSignedOut();
-  // The user has deleted their Chrome history.
-  void OnHistoryDeleted();
+  // The user has deleted all browsing history.
+  void OnAllHistoryDeleted();
   // Chrome's cached data was cleared.
   void OnCacheDataCleared();
 
@@ -216,6 +216,10 @@
   LoadStreamStatus ShouldMakeFeedQueryRequest(bool is_load_more = false,
                                               bool consume_quota = true);
 
+  // Returns true if a FeedQuery request made right now should be made without
+  // user credentials.
+  bool ShouldForceSignedOutFeedQueryRequest() const;
+
   // Unloads the model. Surfaces are not updated, and will remain frozen until a
   // model load is requested.
   void UnloadModel();
@@ -302,7 +306,7 @@
 
   // Mutable state.
   RequestThrottler request_throttler_;
-  base::TimeTicks suppress_refreshes_until_;
+  base::TimeTicks signed_out_refreshes_until_;
   std::vector<base::OnceCallback<void(bool)>> load_more_complete_callbacks_;
   Metadata metadata_;
   int unload_on_detach_sequence_number_ = 0;
diff --git a/components/feed/core/v2/feed_stream_unittest.cc b/components/feed/core/v2/feed_stream_unittest.cc
index 12946ed1..e6402844 100644
--- a/components/feed/core/v2/feed_stream_unittest.cc
+++ b/components/feed/core/v2/feed_stream_unittest.cc
@@ -259,7 +259,9 @@
   // FeedNetwork implementation.
   void SendQueryRequest(
       const feedwire::Request& request,
+      bool force_signed_out_request,
       base::OnceCallback<void(QueryRequestResult)> callback) override {
+    forced_signed_out_request = force_signed_out_request;
     ++send_query_call_count;
     // Emulate a successful response.
     // The response body is currently an empty message, because most of the
@@ -330,6 +332,7 @@
   base::Optional<feedwire::FeedActionRequest> action_request_sent;
   int action_request_call_count = 0;
   std::string consistency_token;
+  bool forced_signed_out_request = false;
 
  private:
   base::Optional<feedwire::Response> injected_response_;
@@ -1010,26 +1013,28 @@
   EXPECT_EQ("loading -> 2 slices", surface.DescribeUpdates());
 }
 
-TEST_F(FeedStreamTest, DoNotLoadFromNetworkAfterHistoryIsDeleted) {
-  stream_->OnHistoryDeleted();
+TEST_F(FeedStreamTest, ForceSignedOutRequestAfterHistoryIsDeleted) {
+  stream_->OnAllHistoryDeleted();
   task_environment_.FastForwardBy(kSuppressRefreshDuration -
                                   base::TimeDelta::FromSeconds(1));
   response_translator_.InjectResponse(MakeTypicalInitialModelState());
   TestSurface surface(stream_.get());
   WaitForIdleTaskQueue();
 
-  EXPECT_EQ("loading -> no-cards", surface.DescribeUpdates());
+  EXPECT_EQ("loading -> 2 slices", surface.DescribeUpdates());
+  EXPECT_TRUE(network_.forced_signed_out_request);
+}
 
-  EXPECT_EQ(LoadStreamStatus::kCannotLoadFromNetworkSupressedForHistoryDelete,
-            metrics_reporter_->load_stream_status);
-
-  surface.Detach();
-  task_environment_.FastForwardBy(base::TimeDelta::FromSeconds(2));
-  surface.Clear();
-  surface.Attach(stream_.get());
+TEST_F(FeedStreamTest, AllowSignedInRequestAfterHistoryIsDeletedAfterDelay) {
+  stream_->OnAllHistoryDeleted();
+  task_environment_.FastForwardBy(kSuppressRefreshDuration +
+                                  base::TimeDelta::FromSeconds(1));
+  response_translator_.InjectResponse(MakeTypicalInitialModelState());
+  TestSurface surface(stream_.get());
   WaitForIdleTaskQueue();
 
   EXPECT_EQ("loading -> 2 slices", surface.DescribeUpdates());
+  EXPECT_FALSE(network_.forced_signed_out_request);
 }
 
 TEST_F(FeedStreamTest, ShouldMakeFeedQueryRequestConsumesQuota) {
diff --git a/components/feed/core/v2/public/feed_service.cc b/components/feed/core/v2/public/feed_service.cc
index 04795aa..5912974 100644
--- a/components/feed/core/v2/public/feed_service.cc
+++ b/components/feed/core/v2/public/feed_service.cc
@@ -43,14 +43,8 @@
 
 namespace internal {
 bool ShouldClearFeed(const history::DeletionInfo& deletion_info) {
-  // We ignore expirations since they're not user-initiated.
-  if (deletion_info.is_from_expiration())
-    return false;
-
-  // If a user deletes a single URL, we don't consider this a clear user
-  // intent to clear our data.
-  return deletion_info.IsAllHistory() ||
-         deletion_info.deleted_rows().size() > 1;
+  // Only clear the feed if all history is deleted.
+  return deletion_info.IsAllHistory();
 }
 }  // namespace internal
 
@@ -71,7 +65,7 @@
   void OnURLsDeleted(history::HistoryService* history_service,
                      const history::DeletionInfo& deletion_info) override {
     if (internal::ShouldClearFeed(deletion_info))
-      feed_stream_->OnHistoryDeleted();
+      feed_stream_->OnAllHistoryDeleted();
   }
 
  private:
diff --git a/components/feed/core/v2/public/feed_service_unittest.cc b/components/feed/core/v2/public/feed_service_unittest.cc
index 50fa7e27..8a0bd9ed 100644
--- a/components/feed/core/v2/public/feed_service_unittest.cc
+++ b/components/feed/core/v2/public/feed_service_unittest.cc
@@ -14,16 +14,10 @@
 
 TEST(ShouldClearFeed, ShouldClearFeed) {
   EXPECT_TRUE(ShouldClearFeed(DeletionInfo::ForAllHistory()));
-  EXPECT_TRUE(ShouldClearFeed(DeletionInfo::ForUrls(
-      {
-          history::URLRow(GURL("http://url1")),
-          history::URLRow(GURL("http://url2")),
-      },
-      /*favicon_urls=*/{})));
-
   EXPECT_FALSE(ShouldClearFeed(DeletionInfo::ForUrls(
       {
           history::URLRow(GURL("http://url1")),
+          history::URLRow(GURL("http://url2")),
       },
       /*favicon_urls=*/{})));
 }
diff --git a/components/feed/core/v2/surface_updater.cc b/components/feed/core/v2/surface_updater.cc
index fb35e9c7..4854079b 100644
--- a/components/feed/core/v2/surface_updater.cc
+++ b/components/feed/core/v2/surface_updater.cc
@@ -146,7 +146,8 @@
     case LoadStreamStatus::kModelAlreadyLoaded:
     case LoadStreamStatus::kDataInStoreIsStale:
     case LoadStreamStatus::kDataInStoreIsStaleTimestampInFuture:
-    case LoadStreamStatus::kCannotLoadFromNetworkSupressedForHistoryDelete:
+    case LoadStreamStatus::
+        kCannotLoadFromNetworkSupressedForHistoryDelete_DEPRECATED:
     case LoadStreamStatus::kLoadNotAllowedEulaNotAccepted:
     case LoadStreamStatus::kLoadNotAllowedArticlesListHidden:
     case LoadStreamStatus::kCannotParseNetworkResponseBody:
diff --git a/components/feed/core/v2/tasks/load_more_task.cc b/components/feed/core/v2/tasks/load_more_task.cc
index ad6aac4..dd5c513 100644
--- a/components/feed/core/v2/tasks/load_more_task.cc
+++ b/components/feed/core/v2/tasks/load_more_task.cc
@@ -55,6 +55,7 @@
           stream_->GetRequestMetadata(),
           stream_->GetMetadata()->GetConsistencyToken(),
           stream_->GetModel()->GetNextPageToken()),
+      stream_->ShouldForceSignedOutFeedQueryRequest(),
       base::BindOnce(&LoadMoreTask::QueryRequestComplete, GetWeakPtr()));
 }
 
diff --git a/components/feed/core/v2/tasks/load_stream_task.cc b/components/feed/core/v2/tasks/load_stream_task.cc
index fdf0e66..df119df 100644
--- a/components/feed/core/v2/tasks/load_stream_task.cc
+++ b/components/feed/core/v2/tasks/load_stream_task.cc
@@ -120,6 +120,7 @@
       CreateFeedQueryRefreshRequest(
           GetRequestReason(load_type_), stream_->GetRequestMetadata(),
           stream_->GetMetadata()->GetConsistencyToken()),
+      stream_->ShouldForceSignedOutFeedQueryRequest(),
       base::BindOnce(&LoadStreamTask::QueryRequestComplete, GetWeakPtr()));
 }
 
diff --git a/components/ntp_tiles/most_visited_sites_unittest.cc b/components/ntp_tiles/most_visited_sites_unittest.cc
index 7efb2e5..56b566f 100644
--- a/components/ntp_tiles/most_visited_sites_unittest.cc
+++ b/components/ntp_tiles/most_visited_sites_unittest.cc
@@ -21,6 +21,7 @@
 #include "base/run_loop.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/task/cancelable_task_tracker.h"
+#include "base/test/gmock_callback_support.h"
 #include "base/test/scoped_feature_list.h"
 #include "base/test/task_environment.h"
 #include "base/threading/thread_task_runner_handle.h"
@@ -58,9 +59,10 @@
 using suggestions::ChromeSuggestion;
 using suggestions::SuggestionsProfile;
 using suggestions::SuggestionsService;
-using testing::AtLeast;
+using testing::_;
 using testing::AllOf;
 using testing::AnyNumber;
+using testing::AtLeast;
 using testing::ByMove;
 using testing::Contains;
 using testing::ElementsAre;
@@ -78,7 +80,6 @@
 using testing::SaveArg;
 using testing::SizeIs;
 using testing::StrictMock;
-using testing::_;
 
 const char kHomepageUrl[] = "http://homepa.ge/";
 const char kHomepageTitle[] = "Homepage";
@@ -134,15 +135,6 @@
          tiles[0].url == GURL(url) && tiles[0].source == source;
 }
 
-// testing::InvokeArgument<N> does not work with base::Callback, fortunately
-// gmock makes it simple to create action templates that do for the various
-// possible numbers of arguments.
-ACTION_TEMPLATE(InvokeCallbackArgument,
-                HAS_1_TEMPLATE_PARAMS(int, k),
-                AND_1_VALUE_PARAMS(p0)) {
-  std::move(std::get<k>(args)).Run(p0);
-}
-
 NTPTile MakeTile(const std::string& title,
                  const std::string& url,
                  TileSource source) {
@@ -181,10 +173,7 @@
 class MockTopSites : public TopSites {
  public:
   MOCK_METHOD0(ShutdownOnUIThread, void());
-  void GetMostVisitedURLs(GetMostVisitedURLsCallback callback) override {
-    GetMostVisitedURLs_(callback);
-  }
-  MOCK_METHOD1(GetMostVisitedURLs_, void(GetMostVisitedURLsCallback& callback));
+  MOCK_METHOD1(GetMostVisitedURLs, void(GetMostVisitedURLsCallback callback));
   MOCK_METHOD0(SyncWithHistory, void());
   MOCK_CONST_METHOD0(HasBlockedUrls, bool());
   MOCK_METHOD1(AddBlockedUrl, void(const GURL& url));
@@ -408,7 +397,7 @@
 // implementation in TopSites (which doesn't use base::CallbackList).
 class TopSitesCallbackList {
  public:
-  void Add(TopSites::GetMostVisitedURLsCallback& callback) {
+  void Add(TopSites::GetMostVisitedURLsCallback callback) {
     callbacks_.push_back(std::move(callback));
   }
 
@@ -593,8 +582,8 @@
   FakeHomepageClient* homepage_client = RegisterNewHomepageClient();
   homepage_client->SetHomepageTileEnabled(true);
   DisableRemoteSuggestions();
-  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs_(_))
-      .WillRepeatedly(InvokeCallbackArgument<0>(MostVisitedURLList{}));
+  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs(_))
+      .WillRepeatedly(base::test::RunOnceCallback<0>(MostVisitedURLList{}));
   EXPECT_CALL(*mock_top_sites_, SyncWithHistory());
   EXPECT_CALL(*mock_top_sites_, IsBlocked(Eq(GURL(kHomepageUrl))))
       .Times(AnyNumber())
@@ -608,8 +597,8 @@
 
 TEST_P(MostVisitedSitesTest, ShouldNotIncludeHomepageWithoutClient) {
   DisableRemoteSuggestions();
-  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs_(_))
-      .WillRepeatedly(InvokeCallbackArgument<0>(MostVisitedURLList{}));
+  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs(_))
+      .WillRepeatedly(base::test::RunOnceCallback<0>(MostVisitedURLList{}));
   EXPECT_CALL(*mock_top_sites_, SyncWithHistory());
   EXPECT_CALL(mock_observer_,
               OnURLsAvailable(Contains(
@@ -629,8 +618,8 @@
   homepage_client->SetHomepageTileEnabled(true);
   homepage_client->SetHomepageTitle(base::UTF8ToUTF16(kHomepageTitle));
   DisableRemoteSuggestions();
-  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs_(_))
-      .WillRepeatedly(InvokeCallbackArgument<0>(MostVisitedURLList{}));
+  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs(_))
+      .WillRepeatedly(base::test::RunOnceCallback<0>(MostVisitedURLList{}));
   EXPECT_CALL(*mock_top_sites_, SyncWithHistory());
   EXPECT_CALL(*mock_top_sites_, IsBlocked(Eq(GURL(kHomepageUrl))))
       .Times(AnyNumber())
@@ -659,8 +648,8 @@
   DisableRemoteSuggestions();
 
   // Ensure that home tile is available as usual.
-  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs_(_))
-      .WillRepeatedly(InvokeCallbackArgument<0>(MostVisitedURLList{}));
+  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs(_))
+      .WillRepeatedly(base::test::RunOnceCallback<0>(MostVisitedURLList{}));
   EXPECT_CALL(*mock_top_sites_, SyncWithHistory());
   EXPECT_CALL(*mock_top_sites_, IsBlocked(Eq(GURL(kHomepageUrl))))
       .Times(AnyNumber())
@@ -675,8 +664,8 @@
   // Disable home page and rebuild _without_ Resync. The tile should be gone.
   homepage_client->SetHomepageTileEnabled(false);
   DisableRemoteSuggestions();
-  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs_(_))
-      .WillRepeatedly(InvokeCallbackArgument<0>(MostVisitedURLList{}));
+  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs(_))
+      .WillRepeatedly(base::test::RunOnceCallback<0>(MostVisitedURLList{}));
   EXPECT_CALL(*mock_top_sites_, SyncWithHistory()).Times(0);
   EXPECT_CALL(mock_observer_, OnURLsAvailable(Not(FirstPersonalizedTileIs(
                                   "", kHomepageUrl, TileSource::HOMEPAGE))));
@@ -688,8 +677,8 @@
   FakeHomepageClient* homepage_client = RegisterNewHomepageClient();
   homepage_client->SetHomepageTileEnabled(true);
   DisableRemoteSuggestions();
-  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs_(_))
-      .WillRepeatedly(InvokeCallbackArgument<0>(MostVisitedURLList{}));
+  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs(_))
+      .WillRepeatedly(base::test::RunOnceCallback<0>(MostVisitedURLList{}));
   EXPECT_CALL(*mock_top_sites_, SyncWithHistory());
   EXPECT_CALL(*mock_top_sites_, IsBlocked(Eq(GURL(kHomepageUrl))))
       .Times(AnyNumber())
@@ -706,8 +695,8 @@
   FakeHomepageClient* homepage_client = RegisterNewHomepageClient();
   homepage_client->SetHomepageTileEnabled(true);
   DisableRemoteSuggestions();
-  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs_(_))
-      .WillRepeatedly(InvokeCallbackArgument<0>(
+  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs(_))
+      .WillRepeatedly(base::test::RunOnceCallback<0>(
           (MostVisitedURLList{MakeMostVisitedURL("Site 1", "http://site1/")})));
   EXPECT_CALL(*mock_top_sites_, SyncWithHistory());
   EXPECT_CALL(*mock_top_sites_, IsBlocked(Eq(GURL(kHomepageUrl))))
@@ -727,8 +716,8 @@
   FakeHomepageClient* homepage_client = RegisterNewHomepageClient();
   homepage_client->SetHomepageTileEnabled(true);
   DisableRemoteSuggestions();
-  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs_(_))
-      .WillRepeatedly(InvokeCallbackArgument<0>((MostVisitedURLList{
+  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs(_))
+      .WillRepeatedly(base::test::RunOnceCallback<0>((MostVisitedURLList{
           MakeMostVisitedURL("Site 1", "http://site1/"),
           MakeMostVisitedURL("Site 2", "http://site2/"),
           MakeMostVisitedURL("Site 3", "http://site3/"),
@@ -756,8 +745,8 @@
   FakeHomepageClient* homepage_client = RegisterNewHomepageClient();
   homepage_client->SetHomepageTileEnabled(true);
   DisableRemoteSuggestions();
-  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs_(_))
-      .WillRepeatedly(InvokeCallbackArgument<0>((MostVisitedURLList{
+  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs(_))
+      .WillRepeatedly(base::test::RunOnceCallback<0>((MostVisitedURLList{
           MakeMostVisitedURL("Site 1", "http://site1/"),
           MakeMostVisitedURL("Site 2", "http://site2/"),
           MakeMostVisitedURL("Site 3", "http://site3/"),
@@ -785,8 +774,8 @@
   FakeHomepageClient* homepage_client = RegisterNewHomepageClient();
   homepage_client->SetHomepageTileEnabled(true);
   DisableRemoteSuggestions();
-  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs_(_))
-      .WillRepeatedly(InvokeCallbackArgument<0>(
+  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs(_))
+      .WillRepeatedly(base::test::RunOnceCallback<0>(
           (MostVisitedURLList{MakeMostVisitedURL("Site 1", "http://site1/"),
                               MakeMostVisitedURL("", kHomepageUrl)})));
   EXPECT_CALL(*mock_top_sites_, SyncWithHistory());
@@ -809,8 +798,8 @@
   FakeHomepageClient* homepage_client = RegisterNewHomepageClient();
   homepage_client->SetHomepageTileEnabled(false);
   DisableRemoteSuggestions();
-  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs_(_))
-      .WillRepeatedly(InvokeCallbackArgument<0>(MostVisitedURLList{}));
+  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs(_))
+      .WillRepeatedly(base::test::RunOnceCallback<0>(MostVisitedURLList{}));
   EXPECT_CALL(*mock_top_sites_, SyncWithHistory());
   EXPECT_CALL(*mock_top_sites_, IsBlocked(Eq(GURL(kHomepageUrl))))
       .Times(AnyNumber())
@@ -831,8 +820,8 @@
   homepage_client->SetHomepageTileEnabled(true);
   homepage_client->SetHomepageUrl(GURL(kEmptyHomepageUrl));
   DisableRemoteSuggestions();
-  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs_(_))
-      .WillRepeatedly(InvokeCallbackArgument<0>(MostVisitedURLList{}));
+  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs(_))
+      .WillRepeatedly(base::test::RunOnceCallback<0>(MostVisitedURLList{}));
   EXPECT_CALL(*mock_top_sites_, SyncWithHistory());
   EXPECT_CALL(*mock_top_sites_, IsBlocked(Eq(kEmptyHomepageUrl)))
       .Times(AnyNumber())
@@ -849,8 +838,8 @@
   FakeHomepageClient* homepage_client = RegisterNewHomepageClient();
   homepage_client->SetHomepageTileEnabled(true);
   DisableRemoteSuggestions();
-  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs_(_))
-      .WillRepeatedly(InvokeCallbackArgument<0>(
+  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs(_))
+      .WillRepeatedly(base::test::RunOnceCallback<0>(
           (MostVisitedURLList{MakeMostVisitedURL("", kHomepageUrl)})));
   EXPECT_CALL(*mock_top_sites_, SyncWithHistory());
   EXPECT_CALL(*mock_top_sites_, IsBlocked(Eq(GURL(kHomepageUrl))))
@@ -876,8 +865,8 @@
   homepage_client->SetHomepageTileEnabled(true);
 
   DisableRemoteSuggestions();
-  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs_(_))
-      .WillOnce(InvokeCallbackArgument<0>(
+  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs(_))
+      .WillOnce(base::test::RunOnceCallback<0>(
           (MostVisitedURLList{MakeMostVisitedURL("", kHomepageUrl)})));
   EXPECT_CALL(*mock_top_sites_, SyncWithHistory());
   EXPECT_CALL(*mock_top_sites_, IsBlocked(Eq(GURL(kHomepageUrl))))
@@ -895,8 +884,8 @@
   VerifyAndClearExpectations();
 
   DisableRemoteSuggestions();
-  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs_(_))
-      .WillOnce(InvokeCallbackArgument<0>(MostVisitedURLList{}));
+  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs(_))
+      .WillOnce(base::test::RunOnceCallback<0>(MostVisitedURLList{}));
   EXPECT_CALL(*mock_top_sites_, IsBlocked(Eq(GURL(kHomepageUrl))))
       .Times(AtLeast(1))
       .WillRepeatedly(Return(false));
@@ -914,8 +903,8 @@
   // Does not register an explore sites client.
 
   DisableRemoteSuggestions();
-  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs_(_))
-      .WillRepeatedly(InvokeCallbackArgument<0>(MostVisitedURLList{
+  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs(_))
+      .WillRepeatedly(base::test::RunOnceCallback<0>(MostVisitedURLList{
           MakeMostVisitedURL("ESPN", "http://espn.com/"),
           MakeMostVisitedURL("Mobile", "http://m.mobile.de/"),
           MakeMostVisitedURL("Google", "http://www.google.com/")}));
@@ -936,8 +925,8 @@
 TEST_P(MostVisitedSitesTest, ShouldIncludeTileForExploreSites) {
   RegisterNewExploreSitesClient();
   DisableRemoteSuggestions();
-  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs_(_))
-      .WillRepeatedly(InvokeCallbackArgument<0>(MostVisitedURLList{
+  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs(_))
+      .WillRepeatedly(base::test::RunOnceCallback<0>(MostVisitedURLList{
           MakeMostVisitedURL("ESPN", "http://espn.com/"),
           MakeMostVisitedURL("Mobile", "http://m.mobile.de/"),
           MakeMostVisitedURL("Google", "http://www.google.com/")}));
@@ -955,8 +944,8 @@
 TEST_P(MostVisitedSitesTest, RemovesPersonalSiteIfExploreSitesTilePresent) {
   RegisterNewExploreSitesClient();
   DisableRemoteSuggestions();
-  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs_(_))
-      .WillRepeatedly(InvokeCallbackArgument<0>(MostVisitedURLList{
+  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs(_))
+      .WillRepeatedly(base::test::RunOnceCallback<0>(MostVisitedURLList{
           MakeMostVisitedURL("ESPN", "http://espn.com/"),
           MakeMostVisitedURL("Mobile", "http://m.mobile.de/"),
           MakeMostVisitedURL("Google", "http://www.google.com/")}));
@@ -995,8 +984,8 @@
   pref_service_.SetString(prefs::kPopularSitesOverrideCountry, "US");
   RecreateMostVisitedSites();  // Refills cache with ESPN and Google News.
   DisableRemoteSuggestions();
-  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs_(_))
-      .WillRepeatedly(InvokeCallbackArgument<0>(MostVisitedURLList{
+  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs(_))
+      .WillRepeatedly(base::test::RunOnceCallback<0>(MostVisitedURLList{
           MakeMostVisitedURL("ESPN", "http://espn.com/"),
           MakeMostVisitedURL("Mobile", "http://m.mobile.de/"),
           MakeMostVisitedURL("Google", "http://www.google.com/")}));
@@ -1031,8 +1020,8 @@
 TEST_P(MostVisitedSitesTest, ShouldHandleTopSitesCacheHit) {
   // If cached, TopSites returns the tiles synchronously, running the callback
   // even before the function returns.
-  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs_(_))
-      .WillRepeatedly(InvokeCallbackArgument<0>(
+  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs(_))
+      .WillRepeatedly(base::test::RunOnceCallback<0>(
           MostVisitedURLList{MakeMostVisitedURL("Site 1", "http://site1/")}));
 
   InSequence seq;
@@ -1070,8 +1059,8 @@
   CHECK(top_sites_callbacks_.empty());
 
   // Update by TopSites is propagated.
-  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs_(_))
-      .WillOnce(InvokeCallbackArgument<0>(
+  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs(_))
+      .WillOnce(base::test::RunOnceCallback<0>(
           MostVisitedURLList{MakeMostVisitedURL("Site 2", "http://site2/")}));
   if (IsPopularSitesFeatureEnabled()) {
     EXPECT_CALL(*mock_top_sites_, IsBlocked(_)).WillRepeatedly(Return(false));
@@ -1133,8 +1122,8 @@
   void ExpectBuildWithTopSites(
       const MostVisitedURLList& expected_list,
       std::map<SectionType, NTPTilesVector>* sections) {
-    EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs_(_))
-        .WillRepeatedly(InvokeCallbackArgument<0>(expected_list));
+    EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs(_))
+        .WillRepeatedly(base::test::RunOnceCallback<0>(expected_list));
     EXPECT_CALL(*mock_top_sites_, SyncWithHistory());
     EXPECT_CALL(*mock_custom_links_, IsInitialized())
         .WillRepeatedly(Return(false));
@@ -1205,8 +1194,8 @@
 
   // Uninitialize custom links and rebuild tiles. Tiles should be Top Sites.
   EXPECT_CALL(*mock_custom_links_, Uninitialize());
-  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs_(_))
-      .WillRepeatedly(InvokeCallbackArgument<0>(
+  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs(_))
+      .WillRepeatedly(base::test::RunOnceCallback<0>(
           MostVisitedURLList{MakeMostVisitedURL(kTestTitle, kTestUrl)}));
   EXPECT_CALL(*mock_custom_links_, IsInitialized())
       .WillRepeatedly(Return(false));
@@ -1523,8 +1512,8 @@
   // Undo the action. This should uninitialize custom links.
   EXPECT_CALL(*mock_custom_links_, UndoAction()).Times(0);
   EXPECT_CALL(*mock_custom_links_, Uninitialize());
-  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs_(_))
-      .WillRepeatedly(InvokeCallbackArgument<0>(
+  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs(_))
+      .WillRepeatedly(base::test::RunOnceCallback<0>(
           MostVisitedURLList{MakeMostVisitedURL(kTestTitle, kTestUrl)}));
   EXPECT_CALL(*mock_custom_links_, IsInitialized())
       .WillRepeatedly(Return(false));
@@ -1705,8 +1694,8 @@
   // tiles with Top Sites.
   EXPECT_CALL(*mock_custom_links_, IsInitialized())
       .WillRepeatedly(Return(false));
-  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs_(_))
-      .WillRepeatedly(InvokeCallbackArgument<0>(
+  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs(_))
+      .WillRepeatedly(base::test::RunOnceCallback<0>(
           MostVisitedURLList{MakeMostVisitedURL(kTestTitle1, kTestUrl1)}));
   EXPECT_CALL(*mock_custom_links_, IsInitialized())
       .WillRepeatedly(Return(false));
@@ -1847,7 +1836,7 @@
 
 TEST_P(MostVisitedSitesWithCacheHitTest,
        ShouldSwitchToTopSitesIfEmptyUpdateBySuggestionsService) {
-  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs_(_))
+  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs(_))
       .WillOnce(Invoke(&top_sites_callbacks_, &TopSitesCallbackList::Add));
   suggestions_service_callbacks_.Notify(SuggestionsProfile());
   VerifyAndClearExpectations();
@@ -1899,7 +1888,7 @@
                          &SuggestionsService::ResponseCallbackList::Add));
     EXPECT_CALL(mock_suggestions_service_, GetSuggestionsDataFromCache())
         .WillOnce(Return(SuggestionsProfile()));  // Empty cache.
-    EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs_(_))
+    EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs(_))
         .WillOnce(Invoke(&top_sites_callbacks_, &TopSitesCallbackList::Add));
     EXPECT_CALL(*mock_top_sites_, SyncWithHistory());
     EXPECT_CALL(mock_suggestions_service_, FetchSuggestionsData())
@@ -2079,8 +2068,8 @@
   EXPECT_TRUE(top_sites_callbacks_.empty());
 
   // Update from top sites is propagated to observer.
-  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs_(_))
-      .WillOnce(InvokeCallbackArgument<0>(
+  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs(_))
+      .WillOnce(base::test::RunOnceCallback<0>(
           MostVisitedURLList{MakeMostVisitedURL("Site 4", "http://site4/"),
                              MakeMostVisitedURL("Site 5", "http://site5/"),
                              MakeMostVisitedURL("Site 6", "http://site6/")}));
@@ -2142,7 +2131,7 @@
   base::RunLoop().RunUntilIdle();
 
   for (int i = 0; i < 4; ++i) {
-    EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs_(_))
+    EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs(_))
         .WillOnce(Invoke(&top_sites_callbacks_, &TopSitesCallbackList::Add));
     mock_top_sites_->NotifyTopSitesChanged(
         history::TopSitesObserver::ChangeReason::MOST_VISITED);
diff --git a/components/omnibox/browser/autocomplete_match.cc b/components/omnibox/browser/autocomplete_match.cc
index a90880d..4915ef3e 100644
--- a/components/omnibox/browser/autocomplete_match.cc
+++ b/components/omnibox/browser/autocomplete_match.cc
@@ -1103,14 +1103,6 @@
   return res;
 }
 
-bool AutocompleteMatch::ShouldShowTabMatchButtonInlineInResultView() const {
-  // TODO(pkasting): This kind of presentational logic does not belong on
-  // AutocompleteMatch and should be e.g. a static method in
-  // OmniboxMatchCellView that takes an AutocompleteMatch.
-  return has_tab_match && !associated_keyword &&
-         !OmniboxFieldTrial::IsSuggestionButtonRowEnabled();
-}
-
 void AutocompleteMatch::UpgradeMatchWithPropertiesFrom(
     const AutocompleteMatch& duplicate_match) {
   // For Entity Matches, absorb the duplicate match's |allowed_to_be_default|
diff --git a/components/omnibox/browser/autocomplete_match.h b/components/omnibox/browser/autocomplete_match.h
index 72e3d69..78c398f 100644
--- a/components/omnibox/browser/autocomplete_match.h
+++ b/components/omnibox/browser/autocomplete_match.h
@@ -420,14 +420,6 @@
   // See base/trace_event/memory_usage_estimator.h for more info.
   size_t EstimateMemoryUsage() const;
 
-  // Not to be confused with |has_tab_match|, this returns true if the match
-  // has a matching tab and will use a switch-to-tab button inline in Result
-  // View. It returns false, for example, when the switch button is not shown
-  // because a keyword match is taking precedence. It also returns false when
-  // Suggestion Button Row is enabled, as the Switch-to-tab button will appear
-  // in the button row.
-  bool ShouldShowTabMatchButtonInlineInResultView() const;
-
   // Upgrades this match by absorbing the best properties from
   // |duplicate_match|. For instance: if |duplicate_match| has a higher
   // relevance score, this match's own relevance score will be upgraded.
diff --git a/components/omnibox/browser/omnibox_controller.cc b/components/omnibox/browser/omnibox_controller.cc
index d824f5f..b76109b 100644
--- a/components/omnibox/browser/omnibox_controller.cc
+++ b/components/omnibox/browser/omnibox_controller.cc
@@ -88,7 +88,7 @@
 void OmniboxController::ClearPopupKeywordMode() const {
   // |popup_| can be nullptr in tests.
   if (popup_ && popup_->IsOpen() &&
-      popup_->selected_line_state() == OmniboxPopupModel::KEYWORD) {
+      popup_->selected_line_state() == OmniboxPopupModel::KEYWORD_MODE) {
     popup_->SetSelectedLineState(OmniboxPopupModel::NORMAL);
   }
 }
diff --git a/components/omnibox/browser/omnibox_edit_model.cc b/components/omnibox/browser/omnibox_edit_model.cc
index 61d593e..a90e884 100644
--- a/components/omnibox/browser/omnibox_edit_model.cc
+++ b/components/omnibox/browser/omnibox_edit_model.cc
@@ -914,7 +914,7 @@
   user_text_ = MaybeStripKeyword(user_text_);
 
   if (PopupIsOpen())
-    popup_model()->SetSelectedLineState(OmniboxPopupModel::KEYWORD);
+    popup_model()->SetSelectedLineState(OmniboxPopupModel::KEYWORD_MODE);
   else
     StartAutocomplete(false, true);
 
@@ -973,7 +973,7 @@
   // difference, as we'll need it below. popup_model() may be nullptr in tests.
   bool was_toggled_into_keyword_mode =
       popup_model() &&
-      popup_model()->selected_line_state() == OmniboxPopupModel::KEYWORD;
+      popup_model()->selected_line_state() == OmniboxPopupModel::KEYWORD_MODE;
 
   bool entry_by_tab = keyword_mode_entry_method_ == OmniboxEventProto::TAB;
 
@@ -1552,9 +1552,10 @@
     } else if (popup_model()->selected_line() != OmniboxPopupModel::kNoMatch) {
       const AutocompleteMatch& selected_match =
           result().match_at(popup_model()->selected_line());
-      *match =
-          (popup_model()->selected_line_state() == OmniboxPopupModel::KEYWORD) ?
-              *selected_match.associated_keyword : selected_match;
+      *match = (popup_model()->selected_line_state() ==
+                OmniboxPopupModel::KEYWORD_MODE)
+                   ? *selected_match.associated_keyword
+                   : selected_match;
       found_match_for_text = true;
     }
     if (found_match_for_text && alternate_nav_url &&
diff --git a/components/omnibox/browser/omnibox_popup_model.cc b/components/omnibox/browser/omnibox_popup_model.cc
index 0cca6c1..6efe67e 100644
--- a/components/omnibox/browser/omnibox_popup_model.cc
+++ b/components/omnibox/browser/omnibox_popup_model.cc
@@ -47,11 +47,11 @@
 }
 
 bool OmniboxPopupModel::Selection::IsChangeToKeyword(Selection from) const {
-  return state == KEYWORD && from.state != KEYWORD;
+  return state == KEYWORD_MODE && from.state != KEYWORD_MODE;
 }
 
 bool OmniboxPopupModel::Selection::IsButtonFocused() const {
-  return state != NORMAL && state != KEYWORD;
+  return state != NORMAL && state != KEYWORD_MODE;
 }
 
 ///////////////////////////////////////////////////////////////////////////////
@@ -157,7 +157,7 @@
     return;
 
   const AutocompleteMatch& match = result().match_at(selection_.line);
-  DCHECK((selection_.state != KEYWORD) || match.associated_keyword.get());
+  DCHECK((selection_.state != KEYWORD_MODE) || match.associated_keyword.get());
   if (selection_.IsButtonFocused()) {
     old_focused_url_ = match.destination_url;
     edit_model_->SetAccessibilityLabel(match);
@@ -178,10 +178,11 @@
                                     base::string16(), keyword, is_keyword_hint,
                                     base::string16());
   } else if (old_selection.line != selection_.line ||
-             old_selection.IsButtonFocused()) {
+             (old_selection.IsButtonFocused() &&
+              new_selection.state != KEYWORD_MODE)) {
     // Otherwise, only update the edit model for line number changes, or
-    // when the old selection was a button. Updating the edit model for every
-    // state change breaks keyword mode.
+    // when the old selection was a button and we're not entering keyword mode.
+    // Updating the edit model for every state change breaks keyword mode.
     if (reset_to_default) {
       edit_model_->OnPopupDataChanged(
           match.inline_autocompletion,
@@ -357,7 +358,7 @@
       // Keyword mode is only accessible by Tabbing forward.
       if (direction == kForward) {
         if (step == kStateOrLine) {
-          all_states.push_back(KEYWORD);
+          all_states.push_back(KEYWORD_MODE);
         }
       }
       all_states.push_back(FOCUSED_BUTTON_TAB_SWITCH);
@@ -496,7 +497,7 @@
     }
     case NORMAL:
       return true;
-    case KEYWORD:
+    case KEYWORD_MODE:
       return match.associated_keyword != nullptr;
     case FOCUSED_BUTTON_KEYWORD:
       return match.associated_keyword != nullptr;
@@ -506,7 +507,7 @@
       if (OmniboxFieldTrial::IsSuggestionButtonRowEnabled())
         return match.has_tab_match;
       else
-        return match.ShouldShowTabMatchButtonInlineInResultView();
+        return match.has_tab_match && !match.associated_keyword;
     case FOCUSED_BUTTON_PEDAL:
       return match.pedal != nullptr;
     case FOCUSED_BUTTON_REMOVE_SUGGESTION:
@@ -515,8 +516,7 @@
       if (OmniboxFieldTrial::IsSuggestionButtonRowEnabled())
         return match.SupportsDeletion();
       else
-        return !match.associated_keyword &&
-               !match.ShouldShowTabMatchButtonInlineInResultView() &&
+        return !match.associated_keyword && !match.has_tab_match &&
                match.SupportsDeletion();
     default:
       break;
@@ -614,7 +614,7 @@
       // Don't add an additional message for removable suggestions without
       // button focus, since they are relatively common.
       break;
-    case KEYWORD:
+    case KEYWORD_MODE:
       // TODO(tommycli): Investigate whether the accessibility messaging for
       // Keyword mode belongs here.
       break;
diff --git a/components/omnibox/browser/omnibox_popup_model.h b/components/omnibox/browser/omnibox_popup_model.h
index 4de17a8..01eb9629 100644
--- a/components/omnibox/browser/omnibox_popup_model.h
+++ b/components/omnibox/browser/omnibox_popup_model.h
@@ -61,14 +61,14 @@
     // NORMAL means the row is focused, and Enter key navigates to the match.
     NORMAL = 1,
 
-    // KEYWORD state means actually in keyword mode, as distinct from the
-    // FOCUSED_BUTTON_KEYWORD state, which is only for button focus.
-    KEYWORD = 2,
-
     // FOCUSED_BUTTON_KEYWORD is used when the keyword button is in focus, not
     // actually in Keyword Mode. This is currently only used if deciated button
     // row is enabled
-    FOCUSED_BUTTON_KEYWORD = 3,
+    FOCUSED_BUTTON_KEYWORD = 2,
+
+    // KEYWORD_MODE state means actually in keyword mode, as distinct from the
+    // FOCUSED_BUTTON_KEYWORD state, which is only for button focus.
+    KEYWORD_MODE = 3,
 
     // FOCUSED_BUTTON_TAB_SWITCH state means the Switch Tab button is focused.
     // Pressing enter will switch to the tab match.
diff --git a/components/omnibox/browser/omnibox_popup_model_unittest.cc b/components/omnibox/browser/omnibox_popup_model_unittest.cc
index 9a12709..421e36a 100644
--- a/components/omnibox/browser/omnibox_popup_model_unittest.cc
+++ b/components/omnibox/browser/omnibox_popup_model_unittest.cc
@@ -233,7 +233,7 @@
            Selection(1, OmniboxPopupModel::NORMAL),
            Selection(1, OmniboxPopupModel::FOCUSED_BUTTON_REMOVE_SUGGESTION),
            Selection(2, OmniboxPopupModel::NORMAL),
-           Selection(2, OmniboxPopupModel::KEYWORD),
+           Selection(2, OmniboxPopupModel::KEYWORD_MODE),
            Selection(3, OmniboxPopupModel::FOCUSED_BUTTON_HEADER),
            Selection(3, OmniboxPopupModel::NORMAL),
            Selection(0, OmniboxPopupModel::NORMAL),
diff --git a/components/omnibox/browser/url_index_private_data.cc b/components/omnibox/browser/url_index_private_data.cc
index 04288e1..f685ca4 100644
--- a/components/omnibox/browser/url_index_private_data.cc
+++ b/components/omnibox/browser/url_index_private_data.cc
@@ -523,8 +523,6 @@
 
 HistoryIDVector URLIndexPrivateData::HistoryIDsFromWords(
     const String16Vector& unsorted_words) {
-  // This histogram name reflects the historic name of this function.
-  SCOPED_UMA_HISTOGRAM_TIMER("Omnibox.HistoryQuickHistoryIDSetFromWords");
   // Break the terms down into individual terms (words), get the candidate
   // set for each term, and intersect each to get a final candidate list.
   // Note that a single 'term' from the user's perspective might be
diff --git a/components/optimization_guide/hint_cache.cc b/components/optimization_guide/hint_cache.cc
index c7b883a..b3027be 100644
--- a/components/optimization_guide/hint_cache.cc
+++ b/components/optimization_guide/hint_cache.cc
@@ -15,66 +15,63 @@
 
 namespace optimization_guide {
 
-namespace {
-
-// The default number of host-keyed hints retained within the host keyed cache.
-// When the limit is exceeded, the least recently used hint is purged from
-// |host_keyed_cache_|.
-const size_t kDefaultMaxMemoryCacheHostKeyedHints = 20;
-
-}  // namespace
-
 HintCache::HintCache(
     std::unique_ptr<OptimizationGuideStore> optimization_guide_store,
-    base::Optional<int>
-        max_memory_cache_host_keyed_hints /*= base::Optional<int>()*/)
+    int max_memory_cache_host_keyed_hints)
     : optimization_guide_store_(std::move(optimization_guide_store)),
-      host_keyed_cache_(std::max(max_memory_cache_host_keyed_hints.value_or(
-                                     kDefaultMaxMemoryCacheHostKeyedHints),
-                                 1)),
+      host_keyed_cache_(max_memory_cache_host_keyed_hints),
       url_keyed_hint_cache_(features::MaxURLKeyedHintCacheSize()),
-      clock_(base::DefaultClock::GetInstance()) {
-  DCHECK(optimization_guide_store_);
-}
+      clock_(base::DefaultClock::GetInstance()) {}
 
 HintCache::~HintCache() = default;
 
 void HintCache::Initialize(bool purge_existing_data,
                            base::OnceClosure callback) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  optimization_guide_store_->Initialize(
-      purge_existing_data,
-      base::BindOnce(&HintCache::OnStoreInitialized, base::Unretained(this),
-                     std::move(callback)));
+
+  if (optimization_guide_store_) {
+    optimization_guide_store_->Initialize(
+        purge_existing_data,
+        base::BindOnce(&HintCache::OnStoreInitialized, base::Unretained(this),
+                       std::move(callback)));
+    return;
+  }
+  std::move(callback).Run();
 }
 
 std::unique_ptr<StoreUpdateData>
 HintCache::MaybeCreateUpdateDataForComponentHints(
     const base::Version& version) const {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  return optimization_guide_store_->MaybeCreateUpdateDataForComponentHints(
-      version);
+  if (optimization_guide_store_) {
+    return optimization_guide_store_->MaybeCreateUpdateDataForComponentHints(
+        version);
+  }
+  return nullptr;
 }
 
 std::unique_ptr<StoreUpdateData> HintCache::CreateUpdateDataForFetchedHints(
     base::Time update_time) const {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  return optimization_guide_store_->CreateUpdateDataForFetchedHints(
-      update_time);
+  if (optimization_guide_store_) {
+    return optimization_guide_store_->CreateUpdateDataForFetchedHints(
+        update_time);
+  }
+  return nullptr;
 }
 
 void HintCache::UpdateComponentHints(
     std::unique_ptr<StoreUpdateData> component_data,
     base::OnceClosure callback) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  DCHECK(component_data);
 
-  // Clear the host-keyed cache prior to updating the store with the new
-  // component data.
-  host_keyed_cache_.Clear();
-
-  optimization_guide_store_->UpdateComponentHints(std::move(component_data),
-                                                  std::move(callback));
+  if (optimization_guide_store_) {
+    DCHECK(component_data);
+    optimization_guide_store_->UpdateComponentHints(std::move(component_data),
+                                                    std::move(callback));
+    return;
+  }
+  std::move(callback).Run();
 }
 
 void HintCache::UpdateFetchedHints(
@@ -93,29 +90,34 @@
 
   ProcessAndCacheHints(get_hints_response.get()->mutable_hints(),
                        fetched_hints_update_data.get());
-  optimization_guide_store_->UpdateFetchedHints(
-      std::move(fetched_hints_update_data), std::move(callback));
+
+  if (optimization_guide_store_) {
+    optimization_guide_store_->UpdateFetchedHints(
+        std::move(fetched_hints_update_data), std::move(callback));
+  } else {
+    std::move(callback).Run();
+  }
 }
 
 void HintCache::PurgeExpiredFetchedHints() {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  DCHECK(optimization_guide_store_);
 
-  optimization_guide_store_->PurgeExpiredFetchedHints();
+  if (optimization_guide_store_)
+    optimization_guide_store_->PurgeExpiredFetchedHints();
 }
 
 void HintCache::ClearFetchedHints() {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  DCHECK(optimization_guide_store_);
   url_keyed_hint_cache_.Clear();
   ClearHostKeyedHints();
 }
 
 void HintCache::ClearHostKeyedHints() {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  DCHECK(optimization_guide_store_);
   host_keyed_cache_.Clear();
-  optimization_guide_store_->ClearFetchedHintsFromDatabase();
+
+  if (optimization_guide_store_)
+    optimization_guide_store_->ClearFetchedHintsFromDatabase();
 }
 
 bool HintCache::HasHint(const std::string& host) {
@@ -123,9 +125,12 @@
 
   auto hint_it = host_keyed_cache_.Get(host);
   if (hint_it == host_keyed_cache_.end()) {
-    // If not in-memory, check database.
-    OptimizationGuideStore::EntryKey hint_entry_key;
-    return optimization_guide_store_->FindHintEntryKey(host, &hint_entry_key);
+    if (optimization_guide_store_) {
+      // If not in-memory, check database.
+      OptimizationGuideStore::EntryKey hint_entry_key;
+      return optimization_guide_store_->FindHintEntryKey(host, &hint_entry_key);
+    }
+    return false;
   }
 
   // The hint for |host| was requested but no hint was returned.
@@ -148,6 +153,11 @@
   // there, then asynchronously load it from the store and return.
   auto hint_it = host_keyed_cache_.Get(host);
   if (hint_it == host_keyed_cache_.end()) {
+    if (!optimization_guide_store_) {
+      std::move(callback).Run(nullptr);
+      return;
+    }
+
     OptimizationGuideStore::EntryKey hint_entry_key;
     if (!optimization_guide_store_->FindHintEntryKey(host, &hint_entry_key)) {
       std::move(callback).Run(nullptr);
@@ -242,14 +252,14 @@
 }
 
 base::Time HintCache::GetFetchedHintsUpdateTime() const {
-  if (!optimization_guide_store_) {
-    return base::Time();
-  }
-  return optimization_guide_store_->GetFetchedHintsUpdateTime();
+  if (optimization_guide_store_)
+    return optimization_guide_store_->GetFetchedHintsUpdateTime();
+  return base::Time();
 }
 
 void HintCache::OnStoreInitialized(base::OnceClosure callback) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  DCHECK(optimization_guide_store_);
   std::move(callback).Run();
 }
 
@@ -281,9 +291,11 @@
 bool HintCache::ProcessAndCacheHints(
     google::protobuf::RepeatedPtrField<proto::Hint>* hints,
     optimization_guide::StoreUpdateData* update_data) {
-  // If there's no update data, then there's nothing to do.
-  if (!update_data)
+  if (optimization_guide_store_ && !update_data) {
+    // If there's no update data, then there's nothing to do.
     return false;
+  }
+
   bool processed_hints_to_store = false;
   // Process each hint in the the hint configuration. The hints are mutable
   // because once processing is completed on each individual hint, it is moved
@@ -313,7 +325,8 @@
             std::make_unique<MemoryHint>(
                 expiry_time,
                 std::make_unique<optimization_guide::proto::Hint>(hint)));
-        update_data->MoveHintIntoUpdateData(std::move(hint));
+        if (update_data)
+          update_data->MoveHintIntoUpdateData(std::move(hint));
         processed_hints_to_store = true;
         break;
       case proto::FULL_URL:
@@ -347,8 +360,11 @@
 
 bool HintCache::IsHintStoreAvailable() const {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  DCHECK(optimization_guide_store_);
-  return optimization_guide_store_->IsAvailable();
+
+  if (optimization_guide_store_)
+    return optimization_guide_store_->IsAvailable();
+
+  return false;
 }
 
 }  // namespace optimization_guide
diff --git a/components/optimization_guide/hint_cache.h b/components/optimization_guide/hint_cache.h
index 3cbd625..a003d394 100644
--- a/components/optimization_guide/hint_cache.h
+++ b/components/optimization_guide/hint_cache.h
@@ -33,18 +33,17 @@
 // synchronously retrieve recently loaded hints keyed by URL or host.
 class HintCache {
  public:
-  // Construct the HintCache with a backing store and an optional max host-keyed
-  // cache size. While |optimization_guide_store| is required,
-  // |max_memory_cache_hints| is optional and the default max size will be used
-  // if it is not provided.
+  // Construct the HintCache with an optional backing store and max host-keyed
+  // cache size. If a backing store is not provided, all hints will only be
+  // stored in-memory.
   explicit HintCache(
       std::unique_ptr<OptimizationGuideStore> optimization_guide_store,
-      base::Optional<int> max_memory_cache_hints = base::Optional<int>());
+      int max_host_keyed_memory_cache_size);
   ~HintCache();
 
-  // Initializes the backing store contained within the hint cache and
-  // asynchronously runs the callback after initialization is complete.
-  // If |purge_existing_data| is set to true, then the cache will purge any
+  // Initializes the backing store contained within the hint cache, if provided,
+  // and asynchronously runs the callback after initialization is complete. If
+  // |purge_existing_data| is set to true, then the cache will purge any
   // pre-existing data and begin in a clean state.
   void Initialize(bool purge_existing_data, base::OnceClosure callback);
 
diff --git a/components/optimization_guide/hint_cache_unittest.cc b/components/optimization_guide/hint_cache_unittest.cc
index 51a0d394..725f9d0 100644
--- a/components/optimization_guide/hint_cache_unittest.cc
+++ b/components/optimization_guide/hint_cache_unittest.cc
@@ -31,7 +31,8 @@
   return "host.domain" + base::NumberToString(index) + ".org";
 }
 
-class HintCacheTest : public ProtoDatabaseProviderTestBase {
+class HintCacheTest : public ProtoDatabaseProviderTestBase,
+                      public testing::WithParamInterface<bool> {
  public:
   HintCacheTest() : loaded_hint_(nullptr) {}
 
@@ -52,8 +53,10 @@
     auto database_path = temp_dir_.GetPath();
     auto database_task_runner = task_environment_.GetMainThreadTaskRunner();
     hint_cache_ = std::make_unique<HintCache>(
-        std::make_unique<OptimizationGuideStore>(
-            db_provider_.get(), database_path, database_task_runner),
+        IsBackedByPersistentStore()
+            ? std::make_unique<OptimizationGuideStore>(
+                  db_provider_.get(), database_path, database_task_runner)
+            : nullptr,
         memory_cache_size);
     is_store_initialized_ = false;
     hint_cache_->Initialize(purge_existing_data,
@@ -149,6 +152,8 @@
     base::RunLoop().RunUntilIdle();
   }
 
+  bool IsBackedByPersistentStore() const { return GetParam(); }
+
  private:
   void OnStoreInitialized() { is_store_initialized_ = true; }
   void OnUpdateComponentHints() { are_component_hints_updated_ = true; }
@@ -171,7 +176,14 @@
   DISALLOW_COPY_AND_ASSIGN(HintCacheTest);
 };
 
-TEST_F(HintCacheTest, ComponentUpdate) {
+INSTANTIATE_TEST_SUITE_P(WithPersistentStore,
+                         HintCacheTest,
+                         testing::Values(true, false));
+
+TEST_P(HintCacheTest, ComponentUpdate) {
+  if (!IsBackedByPersistentStore())
+    return;
+
   const int kMemoryCacheSize = 5;
   CreateAndInitializeHintCache(kMemoryCacheSize);
 
@@ -206,7 +218,10 @@
   EXPECT_TRUE(hint_cache()->HasHint("subdomain.domain.org"));
 }
 
-TEST_F(HintCacheTest, ComponentUpdateWithSameVersionIgnored) {
+TEST_P(HintCacheTest, ComponentUpdateWithSameVersionIgnored) {
+  if (!IsBackedByPersistentStore())
+    return;
+
   const int kMemoryCacheSize = 5;
   CreateAndInitializeHintCache(kMemoryCacheSize);
 
@@ -220,7 +235,10 @@
   EXPECT_FALSE(hint_cache()->MaybeCreateUpdateDataForComponentHints(version));
 }
 
-TEST_F(HintCacheTest, ComponentUpdateWithEarlierVersionIgnored) {
+TEST_P(HintCacheTest, ComponentUpdateWithEarlierVersionIgnored) {
+  if (!IsBackedByPersistentStore())
+    return;
+
   const int kMemoryCacheSize = 5;
   CreateAndInitializeHintCache(kMemoryCacheSize);
 
@@ -236,7 +254,10 @@
   EXPECT_FALSE(hint_cache()->MaybeCreateUpdateDataForComponentHints(version_1));
 }
 
-TEST_F(HintCacheTest, ComponentUpdateWithLaterVersionProcessed) {
+TEST_P(HintCacheTest, ComponentUpdateWithLaterVersionProcessed) {
+  if (!IsBackedByPersistentStore())
+    return;
+
   const int kMemoryCacheSize = 5;
   CreateAndInitializeHintCache(kMemoryCacheSize);
 
@@ -305,7 +326,10 @@
   EXPECT_TRUE(hint_cache()->HasHint("host.domain2.org"));
 }
 
-TEST_F(HintCacheTest, ComponentHintsAvailableAfterRestart) {
+TEST_P(HintCacheTest, ComponentHintsAvailableAfterRestart) {
+  if (!IsBackedByPersistentStore())
+    return;
+
   for (int i = 0; i < 2; ++i) {
     const int kMemoryCacheSize = 5;
     CreateAndInitializeHintCache(kMemoryCacheSize,
@@ -350,7 +374,10 @@
   }
 }
 
-TEST_F(HintCacheTest, ComponentHintsUpdatableAfterRestartWithPurge) {
+TEST_P(HintCacheTest, ComponentHintsUpdatableAfterRestartWithPurge) {
+  if (!IsBackedByPersistentStore())
+    return;
+
   for (int i = 0; i < 2; ++i) {
     const int kMemoryCacheSize = 5;
     CreateAndInitializeHintCache(kMemoryCacheSize,
@@ -391,7 +418,10 @@
   }
 }
 
-TEST_F(HintCacheTest, ComponentHintsNotRetainedAfterRestartWithPurge) {
+TEST_P(HintCacheTest, ComponentHintsNotRetainedAfterRestartWithPurge) {
+  if (!IsBackedByPersistentStore())
+    return;
+
   for (int i = 0; i < 2; ++i) {
     const int kMemoryCacheSize = 5;
     CreateAndInitializeHintCache(kMemoryCacheSize,
@@ -438,7 +468,10 @@
   }
 }
 
-TEST_F(HintCacheTest, TestMemoryCacheLeastRecentlyUsedPurge) {
+TEST_P(HintCacheTest, TestMemoryCacheLeastRecentlyUsedPurge) {
+  if (!IsBackedByPersistentStore())
+    return;
+
   const int kTestHintCount = 10;
   const int kMemoryCacheSize = 5;
   CreateAndInitializeHintCache(kMemoryCacheSize);
@@ -478,7 +511,9 @@
   }
 }
 
-TEST_F(HintCacheTest, TestHostNotInCache) {
+TEST_P(HintCacheTest, TestHostNotInCache) {
+  if (!IsBackedByPersistentStore())
+    return;
   const int kTestHintCount = 10;
   const int kMemoryCacheSize = 5;
   CreateAndInitializeHintCache(kMemoryCacheSize);
@@ -500,7 +535,10 @@
   EXPECT_FALSE(hint_cache()->HasHint(GetHostDomainOrg(kTestHintCount)));
 }
 
-TEST_F(HintCacheTest, TestMemoryCacheLoadCallback) {
+TEST_P(HintCacheTest, TestMemoryCacheLoadCallback) {
+  if (!IsBackedByPersistentStore())
+    return;
+
   const int kMemoryCacheSize = 5;
   CreateAndInitializeHintCache(kMemoryCacheSize);
 
@@ -525,7 +563,13 @@
   EXPECT_EQ(hint_key, GetLoadedHint()->key());
 }
 
-TEST_F(HintCacheTest, StoreValidFetchedHints) {
+TEST_P(HintCacheTest, StoreValidFetchedHints) {
+  if (!IsBackedByPersistentStore()) {
+    // Checking the fetched hints update time is not relevant when we don't have
+    // a backing store.
+    return;
+  }
+
   const int kMemoryCacheSize = 5;
   CreateAndInitializeHintCache(kMemoryCacheSize);
 
@@ -550,7 +594,7 @@
   EXPECT_EQ(hint_cache()->GetFetchedHintsUpdateTime(), stored_time);
 }
 
-TEST_F(HintCacheTest, ParseEmptyFetchedHints) {
+TEST_P(HintCacheTest, ParseEmptyFetchedHints) {
   const int kMemoryCacheSize = 5;
   CreateAndInitializeHintCache(kMemoryCacheSize);
 
@@ -559,12 +603,18 @@
       std::make_unique<proto::GetHintsResponse>();
 
   UpdateFetchedHintsAndWait(std::move(get_hints_response), stored_time, {}, {});
-  // Empty Fetched Hints causes the metadata entry to be updated.
+  // Empty Fetched Hints causes the metadata entry to be updated if store is
+  // available.
   EXPECT_TRUE(are_fetched_hints_updated());
-  EXPECT_EQ(hint_cache()->GetFetchedHintsUpdateTime(), stored_time);
+
+  if (IsBackedByPersistentStore()) {
+    EXPECT_EQ(hint_cache()->GetFetchedHintsUpdateTime(), stored_time);
+  } else {
+    EXPECT_EQ(hint_cache()->GetFetchedHintsUpdateTime(), base::Time());
+  }
 }
 
-TEST_F(HintCacheTest, StoreValidFetchedHintsWithServerProvidedExpiryTime) {
+TEST_P(HintCacheTest, StoreValidFetchedHintsWithServerProvidedExpiryTime) {
   const int kMemoryCacheSize = 5;
   const int kFetchedHintExpirationSecs = 60;
   CreateAndInitializeHintCache(kMemoryCacheSize);
@@ -589,8 +639,12 @@
                             {"host.domain.org"}, {navigation_url});
   EXPECT_TRUE(are_fetched_hints_updated());
 
-  // Next update time for hints should be updated.
-  EXPECT_EQ(hint_cache()->GetFetchedHintsUpdateTime(), stored_time);
+  if (IsBackedByPersistentStore()) {
+    // Next update time for hints should be updated.
+    EXPECT_EQ(hint_cache()->GetFetchedHintsUpdateTime(), stored_time);
+  } else {
+    EXPECT_EQ(hint_cache()->GetFetchedHintsUpdateTime(), base::Time());
+  }
 
   // Should be loaded right when response is received.
   EXPECT_TRUE(hint_cache()->GetHostKeyedHintIfLoaded("host.domain.org"));
@@ -601,7 +655,7 @@
   EXPECT_FALSE(hint_cache()->GetHostKeyedHintIfLoaded("host.domain.org"));
 }
 
-TEST_F(HintCacheTest, StoreValidFetchedHintsWithDefaultExpiryTime) {
+TEST_P(HintCacheTest, StoreValidFetchedHintsWithDefaultExpiryTime) {
   const int kMemoryCacheSize = 5;
   CreateAndInitializeHintCache(kMemoryCacheSize);
 
@@ -622,8 +676,12 @@
                             {"host.domain.org"}, {});
   EXPECT_TRUE(are_fetched_hints_updated());
 
-  // Next update time for hints should be updated.
-  EXPECT_EQ(hint_cache()->GetFetchedHintsUpdateTime(), stored_time);
+  if (IsBackedByPersistentStore()) {
+    // Next update time for hints should be updated.
+    EXPECT_EQ(hint_cache()->GetFetchedHintsUpdateTime(), stored_time);
+  } else {
+    EXPECT_EQ(hint_cache()->GetFetchedHintsUpdateTime(), base::Time());
+  }
 
   // Should be loaded right when response is received.
   EXPECT_TRUE(hint_cache()->GetHostKeyedHintIfLoaded("host.domain.org"));
@@ -635,13 +693,13 @@
   EXPECT_FALSE(hint_cache()->GetHostKeyedHintIfLoaded("host.domain.org"));
 }
 
-TEST_F(HintCacheTest, CacheValidURLKeyedHint) {
+TEST_P(HintCacheTest, CacheValidURLKeyedHint) {
   const int kMemoryCacheSize = 5;
   CreateAndInitializeHintCache(kMemoryCacheSize);
 
   std::unique_ptr<StoreUpdateData> update_data =
       hint_cache()->CreateUpdateDataForFetchedHints(base::Time());
-  ASSERT_TRUE(update_data);
+  ASSERT_EQ(update_data != nullptr, IsBackedByPersistentStore());
 
   GURL url("https://whatever.com/r/werd");
 
@@ -650,18 +708,19 @@
 
   // Only URL-keyed hint included so there are no hints to store within the
   // update data.
-  EXPECT_FALSE(hint_cache()->ProcessAndCacheHints(&hints, update_data.get()));
+  EXPECT_FALSE(hint_cache()->ProcessAndCacheHints(
+      &hints, IsBackedByPersistentStore() ? update_data.get() : nullptr));
 
   EXPECT_TRUE(hint_cache()->GetURLKeyedHint(url));
 }
 
-TEST_F(HintCacheTest, URLKeyedHintExpired) {
+TEST_P(HintCacheTest, URLKeyedHintExpired) {
   const int kMemoryCacheSize = 5;
   CreateAndInitializeHintCache(kMemoryCacheSize);
 
   std::unique_ptr<StoreUpdateData> update_data =
       hint_cache()->CreateUpdateDataForFetchedHints(base::Time());
-  ASSERT_TRUE(update_data);
+  ASSERT_EQ(update_data != nullptr, IsBackedByPersistentStore());
 
   GURL url("https://whatever.com/r/werd");
   int cache_duration_in_secs = 60;
@@ -671,7 +730,8 @@
 
   // Only URL-keyed hint included so there are no hints to store within the
   // update data.
-  EXPECT_FALSE(hint_cache()->ProcessAndCacheHints(&hints, update_data.get()));
+  EXPECT_FALSE(hint_cache()->ProcessAndCacheHints(
+      &hints, IsBackedByPersistentStore() ? update_data.get() : nullptr));
 
   EXPECT_TRUE(hint_cache()->GetURLKeyedHint(url));
 
@@ -679,7 +739,13 @@
   EXPECT_FALSE(hint_cache()->GetURLKeyedHint(url));
 }
 
-TEST_F(HintCacheTest, PurgeExpiredFetchedHints) {
+TEST_P(HintCacheTest, PurgeExpiredFetchedHints) {
+  if (!IsBackedByPersistentStore()) {
+    // Purging expired fetched hints is only really relevant for when we have
+    // a backing store.
+    return;
+  }
+
   const int kMemoryCacheSize = 5;
   CreateAndInitializeHintCache(kMemoryCacheSize);
 
@@ -723,13 +789,13 @@
   EXPECT_TRUE(hint_cache()->HasHint("notpurged.com"));
 }
 
-TEST_F(HintCacheTest, ClearFetchedHints) {
+TEST_P(HintCacheTest, ClearFetchedHints) {
   const int kMemoryCacheSize = 5;
   CreateAndInitializeHintCache(kMemoryCacheSize);
 
   std::unique_ptr<StoreUpdateData> update_data =
       hint_cache()->CreateUpdateDataForFetchedHints(base::Time());
-  ASSERT_TRUE(update_data);
+  ASSERT_EQ(update_data != nullptr, IsBackedByPersistentStore());
 
   GURL url("https://whatever.com/r/werd");
   int cache_duration_in_secs = 60;
@@ -755,7 +821,8 @@
 
   // Only URL-keyed hint included so there are no hints to store within the
   // update data.
-  EXPECT_FALSE(hint_cache()->ProcessAndCacheHints(&hints, update_data.get()));
+  EXPECT_FALSE(hint_cache()->ProcessAndCacheHints(
+      &hints, IsBackedByPersistentStore() ? update_data.get() : nullptr));
 
   EXPECT_TRUE(hint_cache()->GetURLKeyedHint(url));
   EXPECT_TRUE(hint_cache()->GetHostKeyedHintIfLoaded(host));
@@ -765,13 +832,13 @@
   EXPECT_FALSE(hint_cache()->GetHostKeyedHintIfLoaded(host));
 }
 
-TEST_F(HintCacheTest, UnsupportedURLsForURLKeyedHints) {
+TEST_P(HintCacheTest, UnsupportedURLsForURLKeyedHints) {
   const int kMemoryCacheSize = 5;
   CreateAndInitializeHintCache(kMemoryCacheSize);
 
   std::unique_ptr<StoreUpdateData> update_data =
       hint_cache()->CreateUpdateDataForFetchedHints(base::Time());
-  ASSERT_TRUE(update_data);
+  ASSERT_EQ(update_data != nullptr, IsBackedByPersistentStore());
 
   GURL https_url("https://whatever.com/r/werd");
   GURL http_url("http://werd.com/werd/");
@@ -788,7 +855,8 @@
 
   // Only URL-keyed hint included so there are no hints to store within the
   // update data.
-  EXPECT_FALSE(hint_cache()->ProcessAndCacheHints(&hints, update_data.get()));
+  EXPECT_FALSE(hint_cache()->ProcessAndCacheHints(
+      &hints, IsBackedByPersistentStore() ? update_data.get() : nullptr));
 
   EXPECT_TRUE(hint_cache()->GetURLKeyedHint(https_url));
   EXPECT_TRUE(hint_cache()->GetURLKeyedHint(http_url));
@@ -797,13 +865,13 @@
   EXPECT_FALSE(hint_cache()->GetURLKeyedHint(auth_url));
 }
 
-TEST_F(HintCacheTest, URLsWithNoURLKeyedHints) {
+TEST_P(HintCacheTest, URLsWithNoURLKeyedHints) {
   const int kMemoryCacheSize = 5;
   CreateAndInitializeHintCache(kMemoryCacheSize);
 
   std::unique_ptr<StoreUpdateData> update_data =
       hint_cache()->CreateUpdateDataForFetchedHints(base::Time());
-  ASSERT_TRUE(update_data);
+  ASSERT_EQ(update_data != nullptr, IsBackedByPersistentStore());
 
   GURL https_url_without_hint("https://whatever.com/r/nohint");
   GURL https_url_with_hint("https://whatever.com/r/hint");
@@ -817,7 +885,8 @@
 
   // Only URL-keyed hint included so there are no hints to store within the
   // update data.
-  EXPECT_FALSE(hint_cache()->ProcessAndCacheHints(&hints, update_data.get()));
+  EXPECT_FALSE(hint_cache()->ProcessAndCacheHints(
+      &hints, IsBackedByPersistentStore() ? update_data.get() : nullptr));
 
   // Add the url without hint to the url-keyed cache via UpdateFetchedHints.
   std::unique_ptr<proto::GetHintsResponse> get_hints_response =
@@ -842,7 +911,7 @@
   EXPECT_FALSE(hint_cache()->HasURLKeyedEntryForURL(https_url_unseen));
 }
 
-TEST_F(HintCacheTest, ProcessHintsNoUpdateData) {
+TEST_P(HintCacheTest, ProcessHintsNoUpdateData) {
   const int kMemoryCacheSize = 5;
   CreateAndInitializeHintCache(kMemoryCacheSize);
 
@@ -855,10 +924,11 @@
   google::protobuf::RepeatedPtrField<proto::Hint> hints;
   *(hints.Add()) = hint;
 
-  EXPECT_FALSE(hint_cache()->ProcessAndCacheHints(&hints, nullptr));
+  EXPECT_EQ(hint_cache()->ProcessAndCacheHints(&hints, nullptr),
+            !IsBackedByPersistentStore());
 }
 
-TEST_F(HintCacheTest,
+TEST_P(HintCacheTest,
        ProcessHintsWithNoPageHintsOrWhitelistedOptimizationsAndUpdateData) {
   const int kMemoryCacheSize = 5;
   CreateAndInitializeHintCache(kMemoryCacheSize);
@@ -872,12 +942,15 @@
 
   std::unique_ptr<StoreUpdateData> update_data =
       StoreUpdateData::CreateComponentStoreUpdateData(base::Version("1.0.0"));
-  EXPECT_FALSE(hint_cache()->ProcessAndCacheHints(&hints, update_data.get()));
-  // Verify there is 1 store entries: 1 for the metadata entry.
-  EXPECT_EQ(1ul, update_data->TakeUpdateEntries()->size());
+  EXPECT_FALSE(hint_cache()->ProcessAndCacheHints(
+      &hints, IsBackedByPersistentStore() ? update_data.get() : nullptr));
+  if (IsBackedByPersistentStore()) {
+    // Verify there is 1 store entries: 1 for the metadata entry.
+    EXPECT_EQ(1ul, update_data->TakeUpdateEntries()->size());
+  }
 }
 
-TEST_F(HintCacheTest,
+TEST_P(HintCacheTest,
        ProcessHintsWithNoPageHintsButHasWhitelistedOptimizationsAndUpdateData) {
   const int kMemoryCacheSize = 5;
   CreateAndInitializeHintCache(kMemoryCacheSize);
@@ -893,13 +966,16 @@
 
   std::unique_ptr<StoreUpdateData> update_data =
       StoreUpdateData::CreateComponentStoreUpdateData(base::Version("1.0.0"));
-  EXPECT_TRUE(hint_cache()->ProcessAndCacheHints(&hints, update_data.get()));
-  // Verify there is 1 store entries: 1 for the metadata entry plus the 1
-  // added hint entry.
-  EXPECT_EQ(2ul, update_data->TakeUpdateEntries()->size());
+  EXPECT_TRUE(hint_cache()->ProcessAndCacheHints(
+      &hints, IsBackedByPersistentStore() ? update_data.get() : nullptr));
+  if (IsBackedByPersistentStore()) {
+    // Verify there is 1 store entries: 1 for the metadata entry plus the 1
+    // added hint entry.
+    EXPECT_EQ(2ul, update_data->TakeUpdateEntries()->size());
+  }
 }
 
-TEST_F(HintCacheTest, ProcessHintsWithPageHintsAndUpdateData) {
+TEST_P(HintCacheTest, ProcessHintsWithPageHintsAndUpdateData) {
   const int kMemoryCacheSize = 5;
   CreateAndInitializeHintCache(kMemoryCacheSize);
 
@@ -919,10 +995,13 @@
 
   std::unique_ptr<StoreUpdateData> update_data =
       StoreUpdateData::CreateComponentStoreUpdateData(base::Version("1.0.0"));
-  EXPECT_TRUE(hint_cache()->ProcessAndCacheHints(&hints, update_data.get()));
-  // Verify there are 2 store entries: 1 for the metadata entry plus
-  // the 1 added hint entry.
-  EXPECT_EQ(2ul, update_data->TakeUpdateEntries()->size());
+  EXPECT_TRUE(hint_cache()->ProcessAndCacheHints(
+      &hints, IsBackedByPersistentStore() ? update_data.get() : nullptr));
+  if (IsBackedByPersistentStore()) {
+    // Verify there are 2 store entries: 1 for the metadata entry plus
+    // the 1 added hint entry.
+    EXPECT_EQ(2ul, update_data->TakeUpdateEntries()->size());
+  }
 }
 
 }  // namespace
diff --git a/components/optimization_guide/hints_fetcher.cc b/components/optimization_guide/hints_fetcher.cc
index ed199b2a..949baf5 100644
--- a/components/optimization_guide/hints_fetcher.cc
+++ b/components/optimization_guide/hints_fetcher.cc
@@ -126,6 +126,11 @@
 bool HintsFetcher::WasHostCoveredByFetch(PrefService* pref_service,
                                          const std::string& host,
                                          const base::Clock* time_clock) {
+  if (!optimization_guide::features::ShouldPersistHintsToDisk()) {
+    // Don't consult the pref if we aren't even persisting hints to disk.
+    return false;
+  }
+
   DictionaryPrefUpdate hosts_fetched(
       pref_service, prefs::kHintsFetcherHostsSuccessfullyFetched);
   base::Optional<double> value =
@@ -325,6 +330,11 @@
 
 void HintsFetcher::UpdateHostsSuccessfullyFetched(
     base::TimeDelta valid_duration) {
+  if (!optimization_guide::features::ShouldPersistHintsToDisk()) {
+    // Do not persist any state if we aren't persisting hints to disk.
+    return;
+  }
+
   DictionaryPrefUpdate hosts_fetched_list(
       pref_service_, prefs::kHintsFetcherHostsSuccessfullyFetched);
 
@@ -417,7 +427,7 @@
 
     base::Optional<double> value =
         hosts_fetched->FindDoubleKey(HashHostForDictionary(host));
-    if (value) {
+    if (value && optimization_guide::features::ShouldPersistHintsToDisk()) {
       base::Time host_valid_time = base::Time::FromDeltaSinceWindowsEpoch(
           base::TimeDelta::FromSecondsD(*value));
       host_hints_due_for_refresh =
diff --git a/components/optimization_guide/hints_fetcher_unittest.cc b/components/optimization_guide/hints_fetcher_unittest.cc
index 8e7469bb..397c2dd 100644
--- a/components/optimization_guide/hints_fetcher_unittest.cc
+++ b/components/optimization_guide/hints_fetcher_unittest.cc
@@ -34,7 +34,8 @@
 
 constexpr char optimization_guide_service_url[] = "https://hintsserver.com/";
 
-class HintsFetcherTest : public testing::Test {
+class HintsFetcherTest : public testing::Test,
+                         public testing::WithParamInterface<bool> {
  public:
   HintsFetcherTest()
       : task_environment_(base::test::TaskEnvironment::MainThreadType::UI,
@@ -43,8 +44,12 @@
             base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
                 &test_url_loader_factory_)) {
     base::test::ScopedFeatureList scoped_list;
-    scoped_list.InitAndEnableFeatureWithParameters(
-        features::kRemoteOptimizationGuideFetching, {});
+    scoped_list.InitWithFeaturesAndParameters(
+        {{features::kRemoteOptimizationGuideFetching, {}},
+         {features::kOptimizationHints,
+          {{"persist_hints_to_disk",
+            ShouldPersistHintsToDisk() ? "true" : "false"}}}},
+        {});
 
     pref_service_ = std::make_unique<TestingPrefServiceSimple>();
     prefs::RegisterProfilePrefs(pref_service_->registry());
@@ -101,6 +106,8 @@
     hints_fetcher_->SetTimeClockForTesting(clock);
   }
 
+  bool ShouldPersistHintsToDisk() const { return GetParam(); }
+
  protected:
   bool FetchHints(const std::vector<std::string>& hosts,
                   const std::vector<GURL>& urls) {
@@ -166,7 +173,11 @@
   DISALLOW_COPY_AND_ASSIGN(HintsFetcherTest);
 };
 
-TEST_F(HintsFetcherTest,
+INSTANTIATE_TEST_SUITE_P(WithPersistentStore,
+                         HintsFetcherTest,
+                         testing::Values(true, false));
+
+TEST_P(HintsFetcherTest,
        FetchOptimizationGuideServiceHintsLogsHistogramUponExiting) {
   base::HistogramTester histogram_tester;
 
@@ -180,7 +191,7 @@
       1, 1);
 }
 
-TEST_F(HintsFetcherTest, FetchOptimizationGuideServiceHints) {
+TEST_P(HintsFetcherTest, FetchOptimizationGuideServiceHints) {
   base::HistogramTester histogram_tester;
 
   std::string response_content;
@@ -205,7 +216,7 @@
 
 // Tests to ensure that multiple hint fetches by the same object cannot be in
 // progress simultaneously.
-TEST_F(HintsFetcherTest, FetchInProgress) {
+TEST_P(HintsFetcherTest, FetchInProgress) {
   base::SimpleTestClock test_clock;
   SetTimeClockForTesting(&test_clock);
 
@@ -234,7 +245,7 @@
 
 // Tests that the hints are refreshed again for hosts for whom hints were
 // fetched recently.
-TEST_F(HintsFetcherTest, FetchInProgress_HostsHintsRefreshed) {
+TEST_P(HintsFetcherTest, FetchInProgress_HostsHintsRefreshed) {
   base::SimpleTestClock test_clock;
   SetTimeClockForTesting(&test_clock);
 
@@ -298,7 +309,7 @@
 }
 
 // Tests 404 response from request.
-TEST_F(HintsFetcherTest, FetchReturned404) {
+TEST_P(HintsFetcherTest, FetchReturned404) {
   base::HistogramTester histogram_tester;
 
   std::string response_content;
@@ -317,7 +328,7 @@
       HintsFetcherRequestStatus::kResponseError, 1);
 }
 
-TEST_F(HintsFetcherTest, FetchReturnBadResponse) {
+TEST_P(HintsFetcherTest, FetchReturnBadResponse) {
   base::HistogramTester histogram_tester;
 
   std::string response_content = "not proto";
@@ -334,7 +345,7 @@
       HintsFetcherRequestStatus::kResponseError, 1);
 }
 
-TEST_F(HintsFetcherTest, FetchAttemptWhenNetworkOffline) {
+TEST_P(HintsFetcherTest, FetchAttemptWhenNetworkOffline) {
   base::HistogramTester histogram_tester;
 
   SetConnectionOffline();
@@ -362,7 +373,7 @@
       1);
 }
 
-TEST_F(HintsFetcherTest, HintsFetchSuccessfulHostsRecorded) {
+TEST_P(HintsFetcherTest, HintsFetchSuccessfulHostsRecorded) {
   std::vector<std::string> hosts{"host1.com", "host2.com"};
   std::string response_content;
 
@@ -371,6 +382,9 @@
   EXPECT_TRUE(SimulateResponse(response_content, net::HTTP_OK));
   EXPECT_TRUE(hints_fetched());
 
+  if (!ShouldPersistHintsToDisk())
+    return;
+
   const base::DictionaryValue* hosts_fetched = pref_service()->GetDictionary(
       prefs::kHintsFetcherHostsSuccessfullyFetched);
   base::Optional<double> value;
@@ -387,7 +401,7 @@
   }
 }
 
-TEST_F(HintsFetcherTest, HintsFetchFailsHostNotRecorded) {
+TEST_P(HintsFetcherTest, HintsFetchFailsHostNotRecorded) {
   std::vector<std::string> hosts{"host1.com", "host2.com"};
   std::string response_content;
 
@@ -396,6 +410,9 @@
   EXPECT_TRUE(SimulateResponse(response_content, net::HTTP_NOT_FOUND));
   EXPECT_FALSE(hints_fetched());
 
+  if (!ShouldPersistHintsToDisk())
+    return;
+
   const base::DictionaryValue* hosts_fetched = pref_service()->GetDictionary(
       prefs::kHintsFetcherHostsSuccessfullyFetched);
   for (const std::string& host : hosts) {
@@ -403,7 +420,7 @@
   }
 }
 
-TEST_F(HintsFetcherTest, HintsFetchClearHostsSuccessfullyFetched) {
+TEST_P(HintsFetcherTest, HintsFetchClearHostsSuccessfullyFetched) {
   std::vector<std::string> hosts{"host1.com", "host2.com"};
   std::string response_content;
 
@@ -412,6 +429,9 @@
   EXPECT_TRUE(SimulateResponse(response_content, net::HTTP_OK));
   EXPECT_TRUE(hints_fetched());
 
+  if (!ShouldPersistHintsToDisk())
+    return;
+
   const base::DictionaryValue* hosts_fetched = pref_service()->GetDictionary(
       prefs::kHintsFetcherHostsSuccessfullyFetched);
   for (const std::string& host : hosts) {
@@ -426,7 +446,10 @@
   }
 }
 
-TEST_F(HintsFetcherTest, HintsFetcherHostsCovered) {
+TEST_P(HintsFetcherTest, HintsFetcherHostsCovered) {
+  if (!ShouldPersistHintsToDisk())
+    return;
+
   std::vector<std::string> hosts{"host1.com", "host2.com"};
   base::Time host_invalid_time =
       base::Time::Now() + base::TimeDelta().FromHours(1);
@@ -437,7 +460,7 @@
   EXPECT_TRUE(WasHostCoveredByFetch(hosts[1]));
 }
 
-TEST_F(HintsFetcherTest, HintsFetcherCoveredHostExpired) {
+TEST_P(HintsFetcherTest, HintsFetcherCoveredHostExpired) {
   std::string response_content;
   std::vector<std::string> hosts{"host1.com", "host2.com"};
   base::Time host_invalid_time =
@@ -452,6 +475,9 @@
   EXPECT_TRUE(SimulateResponse(response_content, net::HTTP_OK));
   EXPECT_TRUE(hints_fetched());
 
+  if (!ShouldPersistHintsToDisk())
+    return;
+
   // The first pair of hosts should be recorded as failed to be
   // covered by a recent hints fetcher as they have expired.
   EXPECT_FALSE(WasHostCoveredByFetch(hosts[0]));
@@ -469,7 +495,7 @@
   EXPECT_TRUE(WasHostCoveredByFetch(hosts_valid[1]));
 }
 
-TEST_F(HintsFetcherTest, HintsFetcherHostNotCovered) {
+TEST_P(HintsFetcherTest, HintsFetcherHostNotCovered) {
   std::vector<std::string> hosts{"host1.com", "host2.com"};
   base::Time host_invalid_time =
       base::Time::Now() + base::TimeDelta().FromHours(1);
@@ -484,7 +510,10 @@
   EXPECT_FALSE(WasHostCoveredByFetch("newhost.com"));
 }
 
-TEST_F(HintsFetcherTest, HintsFetcherRemoveExpiredOnSuccessfullyFetched) {
+TEST_P(HintsFetcherTest, HintsFetcherRemoveExpiredOnSuccessfullyFetched) {
+  if (!ShouldPersistHintsToDisk())
+    return;
+
   std::string response_content;
   std::vector<std::string> hosts_expired{"host1.com", "host2.com"};
   base::Time host_invalid_time =
@@ -512,7 +541,10 @@
   EXPECT_TRUE(WasHostCoveredByFetch(hosts_valid[1]));
 }
 
-TEST_F(HintsFetcherTest, HintsFetcherSuccessfullyFetchedHostsFull) {
+TEST_P(HintsFetcherTest, HintsFetcherSuccessfullyFetchedHostsFull) {
+  if (!ShouldPersistHintsToDisk())
+    return;
+
   std::string response_content;
   std::vector<std::string> hosts;
   size_t max_hosts =
@@ -541,7 +573,7 @@
   EXPECT_TRUE(WasHostCoveredByFetch(extra_hosts[1]));
 }
 
-TEST_F(HintsFetcherTest, MaxHostsForOptimizationGuideServiceHintsFetch) {
+TEST_P(HintsFetcherTest, MaxHostsForOptimizationGuideServiceHintsFetch) {
   std::string response_content;
   std::vector<std::string> all_hosts;
 
@@ -564,6 +596,9 @@
   EXPECT_TRUE(SimulateResponse(response_content, net::HTTP_OK));
   EXPECT_TRUE(hints_fetched());
 
+  if (!ShouldPersistHintsToDisk())
+    return;
+
   DictionaryPrefUpdate hosts_fetched(
       pref_service(), prefs::kHintsFetcherHostsSuccessfullyFetched);
   EXPECT_EQ(max_hosts_in_fetch_request, hosts_fetched->size());
@@ -575,7 +610,7 @@
   }
 }
 
-TEST_F(HintsFetcherTest, MaxUrlsForOptimizationGuideServiceHintsFetch) {
+TEST_P(HintsFetcherTest, MaxUrlsForOptimizationGuideServiceHintsFetch) {
   base::HistogramTester histogram_tester;
   std::string response_content;
   std::vector<GURL> all_urls;
@@ -612,7 +647,7 @@
   }
 }
 
-TEST_F(HintsFetcherTest, OnlyURLsToFetch) {
+TEST_P(HintsFetcherTest, OnlyURLsToFetch) {
   base::HistogramTester histogram_tester;
   std::string response_content;
 
@@ -631,7 +666,7 @@
       static_cast<int>(HintsFetcherRequestStatus::kSuccess), 1);
 }
 
-TEST_F(HintsFetcherTest, NoHostsOrURLsToFetch) {
+TEST_P(HintsFetcherTest, NoHostsOrURLsToFetch) {
   base::HistogramTester histogram_tester;
   std::string response_content;
 
diff --git a/components/optimization_guide/optimization_guide_features.cc b/components/optimization_guide/optimization_guide_features.cc
index 1595d86b..45567c1 100644
--- a/components/optimization_guide/optimization_guide_features.cc
+++ b/components/optimization_guide/optimization_guide_features.cc
@@ -242,6 +242,12 @@
       kOptimizationTargetPrediction, "max_host_model_features_cache_size", 100);
 }
 
+size_t MaxHostKeyedHintCacheSize() {
+  size_t max_host_keyed_hint_cache_size = GetFieldTrialParamByFeatureAsInt(
+      kOptimizationHints, "max_host_keyed_hint_cache_size", 30);
+  return max_host_keyed_hint_cache_size;
+}
+
 size_t MaxURLKeyedHintCacheSize() {
   size_t max_url_keyed_hint_cache_size = GetFieldTrialParamByFeatureAsInt(
       kOptimizationHints, "max_url_keyed_hint_cache_size", 30);
@@ -250,6 +256,11 @@
   return max_url_keyed_hint_cache_size;
 }
 
+bool ShouldPersistHintsToDisk() {
+  return GetFieldTrialParamByFeatureAsBool(kOptimizationHints,
+                                           "persist_hints_to_disk", true);
+}
+
 bool ShouldOverrideOptimizationTargetDecisionForMetricsPurposes(
     proto::OptimizationTarget optimization_target) {
   if (optimization_target != proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD)
diff --git a/components/optimization_guide/optimization_guide_features.h b/components/optimization_guide/optimization_guide_features.h
index a569117..f93fd6e 100644
--- a/components/optimization_guide/optimization_guide_features.h
+++ b/components/optimization_guide/optimization_guide_features.h
@@ -122,9 +122,17 @@
 size_t MaxHostModelFeaturesCacheSize();
 
 // The maximum number of hints allowed to be maintained in a least-recently-used
-// cache.
+// cache for hosts.
+size_t MaxHostKeyedHintCacheSize();
+
+// The maximum number of hints allowed to be maintained in a least-recently-used
+// cache for URLs.
 size_t MaxURLKeyedHintCacheSize();
 
+// Returns true if hints should be persisted to disk. If this is false, hints
+// will just be stored in-memory and evicted if not recently used.
+bool ShouldPersistHintsToDisk();
+
 // Returns true if the optimization target decision for |optimization_target|
 // should not be propagated to the caller in an effort to fully understand the
 // statistics for the served model and not taint the resulting data.
diff --git a/components/safe_browsing/content/web_ui/safe_browsing_ui.cc b/components/safe_browsing/content/web_ui/safe_browsing_ui.cc
index e49b045..9f570c2 100644
--- a/components/safe_browsing/content/web_ui/safe_browsing_ui.cc
+++ b/components/safe_browsing/content/web_ui/safe_browsing_ui.cc
@@ -278,6 +278,7 @@
 }
 
 void WebUIInfoSingleton::AddToDeepScanRequests(
+    const GURL& tab_url,
     const enterprise_connectors::ContentAnalysisRequest& request) {
   if (!HasListener())
     return;
@@ -289,6 +290,7 @@
         base::Time::Now();
   }
 
+  deep_scan_requests_[request.request_token()].tab_url = tab_url;
   deep_scan_requests_[request.request_token()].content_analysis_request =
       request;
 
@@ -1346,6 +1348,7 @@
 
 #if BUILDFLAG(FULL_SAFE_BROWSING)
 std::string SerializeContentAnalysisRequest(
+    const GURL& tab_url,
     const enterprise_connectors::ContentAnalysisRequest& request) {
   base::DictionaryValue request_dict;
 
@@ -1378,6 +1381,9 @@
     }
     request_dict.SetKey("request_data", std::move(request_data));
   }
+  if (tab_url.is_valid()) {
+    request_dict.SetStringKey("tab_url", tab_url.spec());
+  }
 
   base::ListValue tags;
   for (const std::string& tag : request.tags())
@@ -1618,8 +1624,9 @@
     value.SetStringKey("request",
                        SerializeDeepScanningRequest(data.request.value()));
   } else if (data.content_analysis_request.has_value()) {
-    value.SetStringKey("request", SerializeContentAnalysisRequest(
-                                      data.content_analysis_request.value()));
+    value.SetStringKey(
+        "request", SerializeContentAnalysisRequest(
+                       data.tab_url, data.content_analysis_request.value()));
   }
 
   if (!data.response_time.is_null()) {
diff --git a/components/safe_browsing/content/web_ui/safe_browsing_ui.h b/components/safe_browsing/content/web_ui/safe_browsing_ui.h
index 8e10f470..9f742be6 100644
--- a/components/safe_browsing/content/web_ui/safe_browsing_ui.h
+++ b/components/safe_browsing/content/web_ui/safe_browsing_ui.h
@@ -45,6 +45,7 @@
   base::Optional<DeepScanningClientRequest> request;
   base::Optional<enterprise_connectors::ContentAnalysisRequest>
       content_analysis_request;
+  GURL tab_url;
 
   base::Time response_time;
   std::string response_status;
@@ -333,6 +334,7 @@
   // and response.
   void AddToDeepScanRequests(const DeepScanningClientRequest& request);
   void AddToDeepScanRequests(
+      const GURL& tab_url,
       const enterprise_connectors::ContentAnalysisRequest& request);
 
   // Add the new response to |deep_scan_requests_| and send it to all the open
diff --git a/components/security_interstitials/core/common_string_util.cc b/components/security_interstitials/core/common_string_util.cc
index 13a9d34..4ea21ca 100644
--- a/components/security_interstitials/core/common_string_util.cc
+++ b/components/security_interstitials/core/common_string_util.cc
@@ -67,6 +67,26 @@
   load_time_data->SetString("pem", base::StrCat(encoded_chain));
 }
 
+void PopulateLegacyTLSStrings(base::DictionaryValue* load_time_data,
+                              const base::string16& hostname) {
+  load_time_data->SetString("tabTitle",
+                            l10n_util::GetStringUTF16(IDS_SSL_V2_TITLE));
+  load_time_data->SetString("heading",
+                            l10n_util::GetStringUTF16(IDS_LEGACY_TLS_HEADING));
+  load_time_data->SetString(
+      "primaryButtonText",
+      l10n_util::GetStringUTF16(IDS_SSL_OVERRIDABLE_SAFETY_BUTTON));
+  load_time_data->SetString(
+      "primaryParagraph",
+      l10n_util::GetStringUTF16(IDS_LEGACY_TLS_PRIMARY_PARAGRAPH));
+  load_time_data->SetString(
+      "explanationParagraph",
+      l10n_util::GetStringUTF16(IDS_LEGACY_TLS_EXPLANATION));
+  load_time_data->SetString(
+      "finalParagraph", l10n_util::GetStringFUTF16(
+                            IDS_SSL_OVERRIDABLE_PROCEED_PARAGRAPH, hostname));
+}
+
 }  // namespace common_string_util
 
 }  // namespace security_interstitials
diff --git a/components/security_interstitials/core/common_string_util.h b/components/security_interstitials/core/common_string_util.h
index da10ee7..832a9b7e 100644
--- a/components/security_interstitials/core/common_string_util.h
+++ b/components/security_interstitials/core/common_string_util.h
@@ -32,6 +32,11 @@
 // For determining whether to use the old or new icon sets.
 void PopulateNewIconStrings(base::DictionaryValue* load_time_data);
 
+// Fills in the details for a legacy TLS error. Abstracts the strings for
+// access from ios/.
+void PopulateLegacyTLSStrings(base::DictionaryValue* load_time_data,
+                              const base::string16& hostname);
+
 }  // common_string_util
 
 }  // namespace security_interstitials
diff --git a/components/vector_icons/BUILD.gn b/components/vector_icons/BUILD.gn
index f90cc6a..966bb53 100644
--- a/components/vector_icons/BUILD.gn
+++ b/components/vector_icons/BUILD.gn
@@ -36,6 +36,7 @@
     "folder_managed_touch.icon",
     "folder_open.icon",
     "folder_touch.icon",
+    "font_access.icon",
     "forward_arrow.icon",
     "headset.icon",
     "help.icon",
diff --git a/components/vector_icons/font_access.icon b/components/vector_icons/font_access.icon
new file mode 100644
index 0000000..9de44ba0
--- /dev/null
+++ b/components/vector_icons/font_access.icon
@@ -0,0 +1,39 @@
+// 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.
+
+CANVAS_DIMENSIONS, 24,
+MOVE_TO, 20, 2,
+H_LINE_TO, 4,
+R_CUBIC_TO, -1.1f, 0, -2, 0.9f, -2, 2,
+R_V_LINE_TO, 16,
+R_CUBIC_TO, 0, 1.1f, 0.9f, 2, 2, 2,
+R_H_LINE_TO, 16,
+R_CUBIC_TO, 1.1f, 0, 2, -0.9f, 2, -2,
+V_LINE_TO, 4,
+R_CUBIC_TO, 0, -1.1f, -0.9f, -2, -2, -2,
+CLOSE,
+R_MOVE_TO, 0, 18,
+H_LINE_TO, 4,
+V_LINE_TO, 4,
+R_H_LINE_TO, 16,
+R_V_LINE_TO, 16,
+CLOSE,
+MOVE_TO, 10.69f, 6,
+R_H_LINE_TO, 2.6f,
+R_LINE_TO, 4.51f, 12,
+R_H_LINE_TO, -2.5f,
+R_LINE_TO, -1.01f, -2.87f,
+H_LINE_TO, 9.7f,
+LINE_TO, 8.7f, 18,
+H_LINE_TO, 6.2f,
+R_LINE_TO, 4.49f, -12,
+CLOSE,
+R_MOVE_TO, 2.87f, 7.06f,
+R_LINE_TO, -1.06f, -3.02f,
+R_LINE_TO, -0.43f, -1.44f,
+R_H_LINE_TO, -0.13f,
+R_LINE_TO, -0.44f, 1.44f,
+R_LINE_TO, -1.07f, 3.02f,
+R_H_LINE_TO, 3.13f,
+CLOSE
diff --git a/components/viz/service/display_embedder/skia_output_device_gl.cc b/components/viz/service/display_embedder/skia_output_device_gl.cc
index 8985ec9..2aa2a73 100644
--- a/components/viz/service/display_embedder/skia_output_device_gl.cc
+++ b/components/viz/service/display_embedder/skia_output_device_gl.cc
@@ -15,6 +15,7 @@
 #include "gpu/command_buffer/service/gl_utils.h"
 #include "gpu/command_buffer/service/mailbox_manager.h"
 #include "gpu/command_buffer/service/shared_context_state.h"
+#include "gpu/command_buffer/service/shared_image_factory.h"
 #include "gpu/command_buffer/service/texture_base.h"
 #include "gpu/command_buffer/service/texture_manager.h"
 #include "third_party/skia/include/core/SkSurface.h"
@@ -34,6 +35,7 @@
 
 SkiaOutputDeviceGL::SkiaOutputDeviceGL(
     gpu::MailboxManager* mailbox_manager,
+    gpu::SharedImageRepresentationFactory* representation_factory,
     gpu::SharedContextState* context_state,
     scoped_refptr<gl::GLSurface> gl_surface,
     scoped_refptr<gpu::gles2::FeatureInfo> feature_info,
@@ -42,6 +44,7 @@
     : SkiaOutputDevice(memory_tracker,
                        std::move(did_swap_buffer_complete_callback)),
       mailbox_manager_(mailbox_manager),
+      representation_factory_(representation_factory),
       context_state_(context_state),
       gl_surface_(std::move(gl_surface)),
       supports_async_swap_(gl_surface_->SupportsAsyncSwap()) {
@@ -328,11 +331,26 @@
 
 scoped_refptr<gl::GLImage> SkiaOutputDeviceGL::GetGLImageForMailbox(
     const gpu::Mailbox& mailbox) {
-  // TODO(crbug.com/1005306): Use SharedImageManager to get textures here once
+  // TODO(crbug.com/1005306): Remove ConsumeTexture here once
   // all clients are using SharedImageInterface to create textures.
+  // For example, the legacy mailbox still uses GL textures (no overlay)
+  // and is still used.
   auto* texture_base = mailbox_manager_->ConsumeTexture(mailbox);
-  if (!texture_base)
-    return nullptr;
+  if (!texture_base) {
+    auto overlay = representation_factory_->ProduceOverlay(mailbox);
+    if (!overlay)
+      return nullptr;
+
+    // Return GLImage since the ScopedReadAccess isn't being held by anyone.
+    // TODO(crbug.com/1011555): Have SkiaOutputSurfaceImplOnGpu hold on to the
+    // ScopedReadAccess for overlays like it does for PromiseImage based
+    // resources.
+    std::unique_ptr<gpu::SharedImageRepresentationOverlay::ScopedReadAccess>
+        scoped_overlay_read_access =
+            overlay->BeginScopedReadAccess(/*need_gl_image=*/true);
+    DCHECK(scoped_overlay_read_access);
+    return scoped_overlay_read_access->gl_image();
+  }
 
   if (texture_base->GetType() == gpu::TextureBase::Type::kPassthrough) {
     gpu::gles2::TexturePassthrough* texture =
diff --git a/components/viz/service/display_embedder/skia_output_device_gl.h b/components/viz/service/display_embedder/skia_output_device_gl.h
index c237b00..50a659f7 100644
--- a/components/viz/service/display_embedder/skia_output_device_gl.h
+++ b/components/viz/service/display_embedder/skia_output_device_gl.h
@@ -22,6 +22,7 @@
 namespace gpu {
 class MailboxManager;
 class SharedContextState;
+class SharedImageRepresentationFactory;
 
 namespace gles2 {
 class FeatureInfo;
@@ -34,6 +35,7 @@
  public:
   SkiaOutputDeviceGL(
       gpu::MailboxManager* mailbox_manager,
+      gpu::SharedImageRepresentationFactory* representation_factory,
       gpu::SharedContextState* context_state,
       scoped_refptr<gl::GLSurface> gl_surface,
       scoped_refptr<gpu::gles2::FeatureInfo> feature_info,
@@ -74,6 +76,7 @@
   scoped_refptr<gl::GLImage> GetGLImageForMailbox(const gpu::Mailbox& mailbox);
 
   gpu::MailboxManager* const mailbox_manager_;
+  gpu::SharedImageRepresentationFactory* const representation_factory_;
 
   gpu::SharedContextState* const context_state_;
   scoped_refptr<gl::GLSurface> gl_surface_;
diff --git a/components/viz/service/display_embedder/skia_output_surface_impl_on_gpu.cc b/components/viz/service/display_embedder/skia_output_surface_impl_on_gpu.cc
index fb553c6..49e8487 100644
--- a/components/viz/service/display_embedder/skia_output_surface_impl_on_gpu.cc
+++ b/components/viz/service/display_embedder/skia_output_surface_impl_on_gpu.cc
@@ -1015,7 +1015,8 @@
               GetDidSwapBuffersCompleteCallback());
         } else {
           output_device_ = std::make_unique<SkiaOutputDeviceGL>(
-              dependency_->GetMailboxManager(), context_state_.get(),
+              dependency_->GetMailboxManager(),
+              shared_image_representation_factory_.get(), context_state_.get(),
               gl_surface_, feature_info_, memory_tracker_.get(),
               GetDidSwapBuffersCompleteCallback());
         }
diff --git a/content/browser/BUILD.gn b/content/browser/BUILD.gn
index 2864ee1..f04858c 100644
--- a/content/browser/BUILD.gn
+++ b/content/browser/BUILD.gn
@@ -1879,6 +1879,8 @@
     "utility_process_host.cc",
     "utility_process_host.h",
     "utility_process_host_receiver_bindings.cc",
+    "utility_sandbox_delegate.cc",
+    "utility_sandbox_delegate.h",
     "v8_snapshot_files.cc",
     "v8_snapshot_files.h",
     "video_capture_service.cc",
@@ -2212,6 +2214,7 @@
       "installedapp/installed_app_provider_impl_win.h",
       "renderer_host/virtual_keyboard_controller_win.cc",
       "renderer_host/virtual_keyboard_controller_win.h",
+      "utility_sandbox_delegate_win.cc",
     ]
     defines += [
       # This prevents the inclusion of atlhost.h which paired
@@ -2506,12 +2509,6 @@
       "web_contents/web_contents_view_android.h",
     ]
 
-    jumbo_excluded_sources = [
-      # Files with kJavaLangClass and similar constants:
-      # Bug https://crbug.com/787557.
-      "android/java/java_method.cc",  # and in gin_java_bound_object.cc.
-    ]
-
     set_sources_assignment_filter([])
     sources += [
       "memory/swap_metrics_driver_impl_linux.cc",
diff --git a/content/browser/DEPS b/content/browser/DEPS
index 3b03c6f1..ba84fad 100644
--- a/content/browser/DEPS
+++ b/content/browser/DEPS
@@ -168,7 +168,7 @@
     "+services/network/cookie_manager.h",
     "+third_party/leveldatabase",
   ],
-  "utility_process_host\.cc": [
+  "utility_sandbox_delegate_win\.cc": [
     # TODO(crbug.com/1049894): Remove.
     "+services/network/network_sandbox_win.h",
   ],
diff --git a/content/browser/OWNERS b/content/browser/OWNERS
index 87183d7..e2241f21 100644
--- a/content/browser/OWNERS
+++ b/content/browser/OWNERS
@@ -35,6 +35,12 @@
 # Service sandbox mappings require security review
 per-file service_sandbox_type.h=file://ipc/SECURITY_OWNERS
 
+# Utility sandbox delegate requires security review.
+per-file utility_sandbox_delegate.*=set noparent
+per-file utility_sandbox_delegate.*=file://ipc/SECURITY_OWNERS
+per-file utility_sandbox_delegate_win.*=set noparent
+per-file utility_sandbox_delegate_win.*=file://sandbox/win/OWNERS
+
 # BackForwardCache
 per-file back_forward_cache_browsertest.cc=arthursonzogni@chromium.org
 per-file back_forward_cache_browsertest.cc=altimin@chromium.org
diff --git a/content/browser/accessibility/accessibility_tree_formatter_mac_browsertest.mm b/content/browser/accessibility/accessibility_tree_formatter_mac_browsertest.mm
index ea118c9..2dfbb58 100644
--- a/content/browser/accessibility/accessibility_tree_formatter_mac_browsertest.mm
+++ b/content/browser/accessibility/accessibility_tree_formatter_mac_browsertest.mm
@@ -165,7 +165,7 @@
                     </script>)~~",
                {":3;AXSelectedTextMarkerRange=*"}, R"~~(AXWebArea
 ++AXGroup
-++++AXStaticText AXSelectedTextMarkerRange={anchor: {:3, 0, down}, focus: {:2, -1, down}} AXValue='Paragraph'
+++++AXStaticText AXSelectedTextMarkerRange={anchor: {:2, -1, down}, focus: {:3, 0, down}} AXValue='Paragraph'
 )~~");
 }
 
diff --git a/content/browser/accessibility/browser_accessibility_cocoa.h b/content/browser/accessibility/browser_accessibility_cocoa.h
index d1d406e..cb25390 100644
--- a/content/browser/accessibility/browser_accessibility_cocoa.h
+++ b/content/browser/accessibility/browser_accessibility_cocoa.h
@@ -12,20 +12,25 @@
 #include "base/strings/string16.h"
 #include "content/browser/accessibility/browser_accessibility.h"
 #include "content/browser/accessibility/browser_accessibility_manager.h"
+#include "content/common/content_export.h"
 
 namespace content {
 
 // Used to store changes in edit fields, required by VoiceOver in order to
 // support character echo and other announcements during editing.
-struct AXTextEdit {
-  AXTextEdit() = default;
-  AXTextEdit(base::string16 inserted_text, base::string16 deleted_text)
-      : inserted_text(inserted_text), deleted_text(deleted_text) {}
+struct CONTENT_EXPORT AXTextEdit {
+  AXTextEdit();
+  AXTextEdit(base::string16 inserted_text,
+             base::string16 deleted_text,
+             id edit_text_marker);
+  AXTextEdit(const AXTextEdit& other);
+  ~AXTextEdit();
 
   bool IsEmpty() const { return inserted_text.empty() && deleted_text.empty(); }
 
   base::string16 inserted_text;
   base::string16 deleted_text;
+  base::scoped_nsprotocol<id> edit_text_marker;
 };
 
 // Returns true if the given object is AXTextMarker object.
@@ -35,7 +40,7 @@
 bool IsAXTextMarkerRange(id);
 
 // Returns browser accessibility position for the given AXTextMarker.
-BrowserAccessibilityPosition::AXPositionInstance AXTextMarkerToPosition(id);
+CONTENT_EXPORT BrowserAccessibilityPosition::AXPositionInstance AXTextMarkerToPosition(id);
 
 // Returns browser accessibility range for the given AXTextMarkerRange.
 BrowserAccessibilityPosition::AXRangeType AXTextMarkerRangeToRange(id);
diff --git a/content/browser/accessibility/browser_accessibility_cocoa.mm b/content/browser/accessibility/browser_accessibility_cocoa.mm
index 0104027..cf856f5 100644
--- a/content/browser/accessibility/browser_accessibility_cocoa.mm
+++ b/content/browser/accessibility/browser_accessibility_cocoa.mm
@@ -707,6 +707,20 @@
 
 }  // namespace
 
+namespace content {
+
+AXTextEdit::AXTextEdit() = default;
+AXTextEdit::AXTextEdit(base::string16 inserted_text,
+                       base::string16 deleted_text,
+                       id edit_text_marker)
+    : inserted_text(inserted_text),
+      deleted_text(deleted_text),
+      edit_text_marker(edit_text_marker, base::scoped_policy::RETAIN) {}
+AXTextEdit::AXTextEdit(const AXTextEdit& other) = default;
+AXTextEdit::~AXTextEdit() = default;
+
+}  // namespace content
+
 #if defined(MAC_OS_X_VERSION_10_12) && \
     (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_12)
 #warning NSAccessibilityRequiredAttributeChrome \
@@ -1864,10 +1878,11 @@
     if (size_t{sel_start} == newValue.length() &&
         size_t{sel_end} == newValue.length()) {
       // Don't include oldValue as it would be announced -- very confusing.
-      return content::AXTextEdit(newValue, base::string16());
+      return content::AXTextEdit(newValue, base::string16(), nil);
     }
   }
-  return content::AXTextEdit(insertedText, deletedText);
+  return content::AXTextEdit(insertedText, deletedText,
+                             CreateTextMarker(_owner->CreatePositionAt(i)));
 }
 
 - (BOOL)instanceActive {
@@ -2237,7 +2252,9 @@
 - (id)selectedTextMarkerRange {
   if (![self instanceActive])
     return nil;
-  return CreateTextMarkerRange(GetSelectedRange(*_owner));
+  // Voiceover expects this range to be backwards in order to read the selected
+  // words correctly.
+  return CreateTextMarkerRange(GetSelectedRange(*_owner).AsBackwardRange());
 }
 
 - (NSValue*)size {
diff --git a/content/browser/accessibility/browser_accessibility_cocoa_browsertest.mm b/content/browser/accessibility/browser_accessibility_cocoa_browsertest.mm
index 4148e32..48f8d9d 100644
--- a/content/browser/accessibility/browser_accessibility_cocoa_browsertest.mm
+++ b/content/browser/accessibility/browser_accessibility_cocoa_browsertest.mm
@@ -63,6 +63,43 @@
 }  // namespace
 
 IN_PROC_BROWSER_TEST_F(BrowserAccessibilityCocoaBrowserTest,
+                       AXTextMarkerForTextEdit) {
+  EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL)));
+
+  AccessibilityNotificationWaiter waiter(shell()->web_contents(),
+                                         ui::kAXModeComplete,
+                                         ax::mojom::Event::kLoadComplete);
+  GURL url(R"HTML(data:text/html,
+             <input />)HTML");
+
+  EXPECT_TRUE(NavigateToURL(shell(), url));
+  waiter.WaitForNotification();
+
+  BrowserAccessibility* text_field = FindNode(ax::mojom::Role::kTextField);
+  ASSERT_NE(nullptr, text_field);
+  EXPECT_TRUE(content::ExecuteScript(
+      shell()->web_contents(), "document.querySelector('input').focus()"));
+
+  content::SimulateKeyPress(shell()->web_contents(),
+                            ui::DomKey::FromCharacter('B'), ui::DomCode::US_B,
+                            ui::VKEY_B, false, false, false, false);
+
+  base::scoped_nsobject<BrowserAccessibilityCocoa> cocoa_text_field(
+      [ToBrowserAccessibilityCocoa(text_field) retain]);
+  AccessibilityNotificationWaiter value_waiter(shell()->web_contents(),
+                                               ui::kAXModeComplete,
+                                               ax::mojom::Event::kValueChanged);
+  value_waiter.WaitForNotification();
+  AXTextEdit text_edit = [cocoa_text_field computeTextEdit];
+  EXPECT_NE(text_edit.edit_text_marker, nil);
+
+  EXPECT_EQ(
+      content::AXTextMarkerToPosition(text_edit.edit_text_marker)->ToString(),
+      "TextPosition anchor_id=5 text_offset=1 affinity=downstream "
+      "annotated_text=B<>");
+}
+
+IN_PROC_BROWSER_TEST_F(BrowserAccessibilityCocoaBrowserTest,
                        AXCellForColumnAndRow) {
   EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL)));
 
diff --git a/content/browser/accessibility/browser_accessibility_manager_mac.h b/content/browser/accessibility/browser_accessibility_manager_mac.h
index 8fb447a7..b930963 100644
--- a/content/browser/accessibility/browser_accessibility_manager_mac.h
+++ b/content/browser/accessibility/browser_accessibility_manager_mac.h
@@ -60,7 +60,8 @@
   NSDictionary* GetUserInfoForValueChangedNotification(
       const BrowserAccessibilityCocoa* native_node,
       const base::string16& deleted_text,
-      const base::string16& inserted_text) const;
+      const base::string16& inserted_text,
+      id edit_text_marker) const;
 
   void AnnounceActiveDescendant(BrowserAccessibility* node) const;
 
diff --git a/content/browser/accessibility/browser_accessibility_manager_mac.mm b/content/browser/accessibility/browser_accessibility_manager_mac.mm
index 0f7768b2..bcacfd0 100644
--- a/content/browser/accessibility/browser_accessibility_manager_mac.mm
+++ b/content/browser/accessibility/browser_accessibility_manager_mac.mm
@@ -94,6 +94,8 @@
 NSString* const NSAccessibilityTextChangeElement = @"AXTextChangeElement";
 NSString* const NSAccessibilityTextEditType = @"AXTextEditType";
 NSString* const NSAccessibilityTextChangeValue = @"AXTextChangeValue";
+NSString* const NSAccessibilityChangeValueStartMarker =
+    @"AXTextChangeValueStartMarker";
 NSString* const NSAccessibilityTextChangeValueLength =
     @"AXTextChangeValueLength";
 NSString* const NSAccessibilityTextChangeValues = @"AXTextChangeValues";
@@ -308,16 +310,18 @@
       if (base::mac::IsAtLeastOS10_11() && !text_edits_.empty()) {
         base::string16 deleted_text;
         base::string16 inserted_text;
-        int32_t id = node->GetId();
-        const auto iterator = text_edits_.find(id);
+        int32_t node_id = node->GetId();
+        const auto iterator = text_edits_.find(node_id);
+        id edit_text_marker = nil;
         if (iterator != text_edits_.end()) {
           AXTextEdit text_edit = iterator->second;
           deleted_text = text_edit.deleted_text;
           inserted_text = text_edit.inserted_text;
+          edit_text_marker = text_edit.edit_text_marker;
         }
 
         NSDictionary* user_info = GetUserInfoForValueChangedNotification(
-            native_node, deleted_text, inserted_text);
+            native_node, deleted_text, inserted_text, edit_text_marker);
 
         BrowserAccessibility* root = GetRoot();
         if (!root)
@@ -540,29 +544,42 @@
 BrowserAccessibilityManagerMac::GetUserInfoForValueChangedNotification(
     const BrowserAccessibilityCocoa* native_node,
     const base::string16& deleted_text,
-    const base::string16& inserted_text) const {
+    const base::string16& inserted_text,
+    id edit_text_marker) const {
   DCHECK(native_node);
   if (deleted_text.empty() && inserted_text.empty())
     return nil;
 
   NSMutableArray* changes = [[[NSMutableArray alloc] init] autorelease];
   if (!deleted_text.empty()) {
-    [changes addObject:@{
-      NSAccessibilityTextEditType : @(AXTextEditTypeDelete),
-      NSAccessibilityTextChangeValueLength : @(deleted_text.length()),
-      NSAccessibilityTextChangeValue : base::SysUTF16ToNSString(deleted_text)
-    }];
+    NSMutableDictionary* change =
+        [NSMutableDictionary dictionaryWithDictionary:@{
+          NSAccessibilityTextEditType : @(AXTextEditTypeDelete),
+          NSAccessibilityTextChangeValueLength : @(deleted_text.length()),
+          NSAccessibilityTextChangeValue :
+              base::SysUTF16ToNSString(deleted_text)
+        }];
+    if (edit_text_marker) {
+      change[NSAccessibilityChangeValueStartMarker] = edit_text_marker;
+    }
+    [changes addObject:change];
   }
   if (!inserted_text.empty()) {
     // TODO(nektar): Figure out if this is a paste, insertion or typing.
     // Changes to Blink would be required. A heuristic is currently used.
     auto edit_type = inserted_text.length() > 1 ? @(AXTextEditTypeInsert)
                                                 : @(AXTextEditTypeTyping);
-    [changes addObject:@{
-      NSAccessibilityTextEditType : edit_type,
-      NSAccessibilityTextChangeValueLength : @(inserted_text.length()),
-      NSAccessibilityTextChangeValue : base::SysUTF16ToNSString(inserted_text)
-    }];
+    NSMutableDictionary* change =
+        [NSMutableDictionary dictionaryWithDictionary:@{
+          NSAccessibilityTextEditType : edit_type,
+          NSAccessibilityTextChangeValueLength : @(inserted_text.length()),
+          NSAccessibilityTextChangeValue :
+              base::SysUTF16ToNSString(inserted_text)
+        }];
+    if (edit_text_marker) {
+      change[NSAccessibilityChangeValueStartMarker] = edit_text_marker;
+    }
+    [changes addObject:change];
   }
 
   return @{
diff --git a/content/browser/frame_host/navigation_controller_impl_unittest.cc b/content/browser/frame_host/navigation_controller_impl_unittest.cc
index 7d995dfef..2aa9ab5 100644
--- a/content/browser/frame_host/navigation_controller_impl_unittest.cc
+++ b/content/browser/frame_host/navigation_controller_impl_unittest.cc
@@ -31,6 +31,7 @@
 #include "content/browser/web_contents/web_contents_impl.h"
 #include "content/browser/webui/content_web_ui_controller_factory.h"
 #include "content/browser/webui/web_ui_controller_factory_registry.h"
+#include "content/common/content_navigation_policy.h"
 #include "content/common/frame_messages.h"
 #include "content/common/view_messages.h"
 #include "content/public/browser/render_view_host.h"
@@ -3271,10 +3272,17 @@
   NavigateAndCommit(url1);
   NavigateAndCommit(url2);
 
-  // First two entries should have the same SiteInstance.
   SiteInstance* instance1 = controller.GetEntryAtIndex(0)->site_instance();
   SiteInstance* instance2 = controller.GetEntryAtIndex(1)->site_instance();
-  EXPECT_EQ(instance1, instance2);
+  if (CanSameSiteMainFrameNavigationsChangeSiteInstances()) {
+    // If ProactivelySwapBrowsingInstance is enabled for same-site navigations,
+    // the same-site navigation from |url1| to |url2| should use different
+    // SiteInstances.
+    EXPECT_NE(instance1, instance2);
+  } else {
+    // Otherwise, the first two entries should have the same SiteInstance.
+    EXPECT_EQ(instance1, instance2);
+  }
 
   std::unique_ptr<TestWebContents> other_contents(
       static_cast<TestWebContents*>(CreateTestWebContents().release()));
@@ -3603,10 +3611,17 @@
   NavigateAndCommit(url1);
   NavigateAndCommit(url2);
 
-  // First two entries should have the same SiteInstance.
   SiteInstance* instance1 = controller.GetEntryAtIndex(0)->site_instance();
   SiteInstance* instance2 = controller.GetEntryAtIndex(1)->site_instance();
-  EXPECT_EQ(instance1, instance2);
+  if (CanSameSiteMainFrameNavigationsChangeSiteInstances()) {
+    // If ProactivelySwapBrowsingInstance is enabled for same-site navigations,
+    // the same-site navigation from |url1| to |url2| should use different
+    // SiteInstances.
+    EXPECT_NE(instance1, instance2);
+  } else {
+    // Otherwise, the first two entries should have the same SiteInstance.
+    EXPECT_EQ(instance1, instance2);
+  }
 
   std::unique_ptr<TestWebContents> other_contents(
       static_cast<TestWebContents*>(CreateTestWebContents().release()));
diff --git a/content/browser/frame_host/navigator_unittest.cc b/content/browser/frame_host/navigator_unittest.cc
index f21aa07..8410fd1 100644
--- a/content/browser/frame_host/navigator_unittest.cc
+++ b/content/browser/frame_host/navigator_unittest.cc
@@ -147,12 +147,24 @@
   EXPECT_FALSE(request->common_params().has_user_gesture);
   EXPECT_EQ(kUrl2, request->common_params().url);
   EXPECT_FALSE(request->browser_initiated());
-  EXPECT_FALSE(GetSpeculativeRenderFrameHost(node));
+
+  if (CanSameSiteMainFrameNavigationsChangeRenderFrameHosts()) {
+    // If same-site ProactivelySwapBrowsingInstance or main-frame RenderDocument
+    // is enabled, the RFH should change so we should have a speculative RFH.
+    EXPECT_TRUE(GetSpeculativeRenderFrameHost(node));
+    EXPECT_FALSE(GetSpeculativeRenderFrameHost(node)->is_loading());
+  } else {
+    EXPECT_FALSE(GetSpeculativeRenderFrameHost(node));
+  }
   EXPECT_FALSE(main_test_rfh()->is_loading());
 
   // Have the current RenderFrameHost commit the navigation
   navigation->ReadyToCommit();
-  EXPECT_TRUE(main_test_rfh()->is_loading());
+  if (CanSameSiteMainFrameNavigationsChangeRenderFrameHosts()) {
+    EXPECT_TRUE(GetSpeculativeRenderFrameHost(node)->is_loading());
+  } else {
+    EXPECT_TRUE(main_test_rfh()->is_loading());
+  }
   EXPECT_FALSE(node->navigation_request());
 
   // Commit the navigation.
@@ -1190,6 +1202,14 @@
   contents()->NavigateAndCommit(kUrl1);
   FrameTreeNode* node = main_test_rfh()->frame_tree_node();
 
+  // The test below only makes sense if the same-site navigation below will not
+  // create a speculative RFH, so we need to ensure that we won't trigger a
+  // same-site cross-RFH navigation.
+  // Note: this will not disable RenderDocument.
+  // TODO(crbug.com/936696): Skip this test when main-frame RenderDocument is
+  // enabled.
+  DisableProactiveBrowsingInstanceSwapFor(main_test_rfh());
+
   // Navigate same-site.
   auto navigation =
       NavigationSimulator::CreateBrowserInitiated(kUrl2, contents());
diff --git a/content/browser/frame_host/render_frame_host_impl_unittest.cc b/content/browser/frame_host/render_frame_host_impl_unittest.cc
index 1740444..45aa0d70 100644
--- a/content/browser/frame_host/render_frame_host_impl_unittest.cc
+++ b/content/browser/frame_host/render_frame_host_impl_unittest.cc
@@ -5,6 +5,7 @@
 #include <memory>
 
 #include "content/browser/frame_host/render_frame_host_impl.h"
+#include "content/public/test/test_utils.h"
 #include "content/test/navigation_simulator_impl.h"
 #include "content/test/test_render_view_host.h"
 #include "content/test/test_web_contents.h"
@@ -35,7 +36,13 @@
     simulator->Commit();
   }
   RenderFrameHost* initial_rfh = main_rfh();
-
+  // This test is for a bug that only happens when there is no RFH swap on
+  // same-site navigations, so we should disable same-site proactive
+  // BrowsingInstance for |initial_rfh| before continiung.
+  // Note: this will not disable RenderDocument.
+  // TODO(crbug.com/936696): Skip this test when main-frame RenderDocument is
+  // enabled.
+  DisableProactiveBrowsingInstanceSwapFor(initial_rfh);
   // Verify expected main world origin in a steady state - after a commit it
   // should be the same as the last committed origin.
   EXPECT_EQ(url::Origin::Create(initial_url),
diff --git a/content/browser/frame_host/render_frame_host_manager_browsertest.cc b/content/browser/frame_host/render_frame_host_manager_browsertest.cc
index badd38f..57964d8 100644
--- a/content/browser/frame_host/render_frame_host_manager_browsertest.cc
+++ b/content/browser/frame_host/render_frame_host_manager_browsertest.cc
@@ -6045,6 +6045,78 @@
             site_instance_1->GetProcess());
 }
 
+// Tests that navigations that started but haven't committed yet will be
+// overridden by navigations started later if both navigations created
+// speculative RFHs.
+IN_PROC_BROWSER_TEST_P(ProactivelySwapBrowsingInstancesSameSiteTest,
+                       MultipleNavigationsStarted) {
+  ASSERT_TRUE(embedded_test_server()->Start());
+  GURL a1_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
+  GURL a2_url(embedded_test_server()->GetURL("a.com", "/title2.html"));
+  GURL b1_url(embedded_test_server()->GetURL("b.com", "/title1.html"));
+  GURL b2_url(embedded_test_server()->GetURL("b.com", "/title2.html"));
+  WebContentsImpl* web_contents =
+      static_cast<WebContentsImpl*>(shell()->web_contents());
+
+  // 1) Navigate to A1.
+  EXPECT_TRUE(NavigateToURL(shell(), a1_url));
+  auto* a1_rfh = web_contents->GetMainFrame();
+  FrameTreeNode* node = a1_rfh->frame_tree_node();
+  scoped_refptr<SiteInstanceImpl> a1_site_instance =
+      static_cast<SiteInstanceImpl*>(a1_rfh->GetSiteInstance());
+
+  // 2) Start same-site navigation to A2 without committing.
+  TestNavigationManager navigation_a2(shell()->web_contents(), a2_url);
+  shell()->LoadURL(a2_url);
+  EXPECT_TRUE(navigation_a2.WaitForRequestStart());
+  // Verify that we're now navigating to |a2_url|.
+  EXPECT_EQ(node->navigation_request()->GetURL(), a2_url);
+  // We should have a speculative RFH for this navigation.
+  RenderFrameHostImpl* a2_speculative_rfh =
+      node->render_manager()->speculative_frame_host();
+  EXPECT_TRUE(a2_speculative_rfh);
+  EXPECT_NE(a1_rfh, a2_speculative_rfh);
+  // The speculative RFH should use a different BrowsingInstance than the
+  // current RFH.
+  scoped_refptr<SiteInstanceImpl> a2_site_instance =
+      static_cast<SiteInstanceImpl*>(a2_speculative_rfh->GetSiteInstance());
+  EXPECT_FALSE(a1_site_instance->IsRelatedSiteInstance(a2_site_instance.get()));
+
+  // 3) Start cross-site navigation to B1 without committing.
+  TestNavigationManager navigation_b1(shell()->web_contents(), b1_url);
+  shell()->LoadURL(b1_url);
+  EXPECT_TRUE(navigation_b1.WaitForRequestStart());
+  // Verify that we're now navigating to |b1_url|.
+  EXPECT_EQ(node->navigation_request()->GetURL(), b1_url);
+  // We should have a speculative RFH for this navigation.
+  RenderFrameHostImpl* b1_speculative_rfh =
+      node->render_manager()->speculative_frame_host();
+  EXPECT_TRUE(b1_speculative_rfh);
+  EXPECT_NE(a1_rfh, b1_speculative_rfh);
+  // The speculative RFH should use a different BrowsingInstance than the
+  // current RFH.
+  scoped_refptr<SiteInstanceImpl> b1_site_instance =
+      static_cast<SiteInstanceImpl*>(b1_speculative_rfh->GetSiteInstance());
+  EXPECT_FALSE(a1_site_instance->IsRelatedSiteInstance(b1_site_instance.get()));
+
+  // 4) Start same-site navigation to B2 without committing.
+  TestNavigationManager navigation_b2(shell()->web_contents(), b2_url);
+  shell()->LoadURL(b2_url);
+  EXPECT_TRUE(navigation_b2.WaitForRequestStart());
+  // Verify that we're now navigating to |b2_url|.
+  EXPECT_EQ(node->navigation_request()->GetURL(), b2_url);
+  // We should have a speculative RFH for this navigation.
+  RenderFrameHostImpl* b2_speculative_rfh =
+      node->render_manager()->speculative_frame_host();
+  EXPECT_TRUE(b2_speculative_rfh);
+  EXPECT_NE(a1_rfh, b2_speculative_rfh);
+  // The speculative RFH should use a different BrowsingInstance than the
+  // current RFH.
+  scoped_refptr<SiteInstanceImpl> b2_site_instance =
+      static_cast<SiteInstanceImpl*>(b2_speculative_rfh->GetSiteInstance());
+  EXPECT_FALSE(a1_site_instance->IsRelatedSiteInstance(b2_site_instance.get()));
+}
+
 // Tests history same-site process reuse:
 // 1. Visit A1, A2, B.
 // 2. Go back to A2 (should use new process).
diff --git a/content/browser/media/media_web_contents_observer.cc b/content/browser/media/media_web_contents_observer.cc
index 40ca34d..7522ac159 100644
--- a/content/browser/media/media_web_contents_observer.cc
+++ b/content/browser/media/media_web_contents_observer.cc
@@ -220,6 +220,8 @@
     IPC_MESSAGE_HANDLER(
         MediaPlayerDelegateHostMsg_OnPictureInPictureAvailabilityChanged,
         OnPictureInPictureAvailabilityChanged)
+    IPC_MESSAGE_HANDLER(MediaPlayerDelegateHostMsg_OnAudioOutputSinkChanged,
+                        OnAudioOutputSinkChanged);
     IPC_MESSAGE_HANDLER(MediaPlayerDelegateHostMsg_OnBufferUnderflow,
                         OnBufferUnderflow)
     IPC_MESSAGE_UNHANDLED(handled = false)
@@ -375,6 +377,11 @@
       MediaPlayerId(render_frame_host, delegate_id), available);
 }
 
+void MediaWebContentsObserver::OnAudioOutputSinkChanged(
+    RenderFrameHost* render_frame_host,
+    int delegate_id,
+    std::string hashed_device_id) {}
+
 void MediaWebContentsObserver::OnBufferUnderflow(
     RenderFrameHost* render_frame_host,
     int delegate_id) {
diff --git a/content/browser/media/media_web_contents_observer.h b/content/browser/media/media_web_contents_observer.h
index 39cef801..6c03674c 100644
--- a/content/browser/media/media_web_contents_observer.h
+++ b/content/browser/media/media_web_contents_observer.h
@@ -40,7 +40,7 @@
 
 namespace gfx {
 class Size;
-}  // namespace size
+}  // namespace gfx
 
 namespace content {
 
@@ -148,6 +148,9 @@
   void OnPictureInPictureAvailabilityChanged(RenderFrameHost* render_frame_host,
                                              int delegate_id,
                                              bool available);
+  void OnAudioOutputSinkChanged(RenderFrameHost* render_frame_host,
+                                int delegate_id,
+                                std::string hashed_device_id);
   void OnBufferUnderflow(RenderFrameHost* render_frame_host, int delegate_id);
 
   device::mojom::WakeLock* GetAudioWakeLock();
diff --git a/content/browser/media/session/media_session_controller.cc b/content/browser/media/session/media_session_controller.cc
index e59f84b..042b18fd 100644
--- a/content/browser/media/session/media_session_controller.cc
+++ b/content/browser/media/session/media_session_controller.cc
@@ -4,10 +4,15 @@
 
 #include "content/browser/media/session/media_session_controller.h"
 
+#include "content/browser/frame_host/render_frame_host_impl.h"
+#include "content/browser/media/media_devices_util.h"
+#include "content/browser/media/media_web_contents_observer.h"
 #include "content/browser/media/session/media_session_impl.h"
 #include "content/common/media/media_player_delegate_messages.h"
 #include "content/public/browser/browser_thread.h"
+#include "content/public/browser/media_device_id.h"
 #include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/render_process_host.h"
 #include "content/public/browser/web_contents.h"
 #include "media/base/media_content_type.h"
 
@@ -87,6 +92,26 @@
       id_.render_frame_host->GetRoutingID(), id_.delegate_id));
 }
 
+void MediaSessionController::OnSetAudioSinkId(
+    int player_id,
+    const std::string& raw_device_id) {
+  // The sink id needs to be hashed before it is suitable for use in the
+  // renderer process.
+  auto salt_and_origin = content::GetMediaDeviceSaltAndOrigin(
+      id_.render_frame_host->GetProcess()->GetID(),
+      id_.render_frame_host->GetRoutingID());
+
+  std::string hashed_sink_id = GetHMACForMediaDeviceID(
+      salt_and_origin.device_id_salt, salt_and_origin.origin, raw_device_id);
+
+  // Grant the renderer the permission to use this audio output device.
+  static_cast<RenderFrameHostImpl*>(id_.render_frame_host)
+      ->SetAudioOutputDeviceIdForGlobalMediaControls(hashed_sink_id);
+
+  id_.render_frame_host->Send(new MediaPlayerDelegateMsg_SetAudioSinkId(
+      id_.render_frame_host->GetRoutingID(), id_.delegate_id, hashed_sink_id));
+}
+
 RenderFrameHost* MediaSessionController::render_frame_host() const {
   return id_.render_frame_host;
 }
diff --git a/content/browser/media/session/media_session_controller.h b/content/browser/media/session/media_session_controller.h
index 3be6a819..424633b 100644
--- a/content/browser/media/session/media_session_controller.h
+++ b/content/browser/media/session/media_session_controller.h
@@ -6,6 +6,7 @@
 #define CONTENT_BROWSER_MEDIA_SESSION_MEDIA_SESSION_CONTROLLER_H_
 
 #include "base/compiler_specific.h"
+#include "base/memory/weak_ptr.h"
 #include "base/optional.h"
 #include "base/time/time.h"
 #include "content/browser/media/session/media_session_player_observer.h"
@@ -50,6 +51,8 @@
   void OnSetVolumeMultiplier(int player_id, double volume_multiplier) override;
   void OnEnterPictureInPicture(int player_id) override;
   void OnExitPictureInPicture(int player_id) override;
+  void OnSetAudioSinkId(int player_id,
+                        const std::string& raw_device_id) override;
   RenderFrameHost* render_frame_host() const override;
   base::Optional<media_session::MediaPosition> GetPosition(
       int player_id) const override;
diff --git a/content/browser/media/session/media_session_impl.cc b/content/browser/media/session/media_session_impl.cc
index ac1f222..493f719 100644
--- a/content/browser/media/session/media_session_impl.cc
+++ b/content/browser/media/session/media_session_impl.cc
@@ -26,6 +26,7 @@
 #include "content/public/browser/navigation_handle.h"
 #include "content/public/browser/web_contents.h"
 #include "content/public/common/content_client.h"
+#include "media/audio/audio_device_description.h"
 #include "media/base/media_content_type.h"
 #include "media/base/media_switches.h"
 #include "mojo/public/cpp/bindings/callback_helpers.h"
@@ -1071,7 +1072,13 @@
       normal_players_.begin()->first.player_id);
 }
 
-void MediaSessionImpl::SetAudioSinkId(const base::Optional<std::string>& id) {}
+void MediaSessionImpl::SetAudioSinkId(const base::Optional<std::string>& id) {
+  for (const auto& it : normal_players_) {
+    it.first.observer->OnSetAudioSinkId(
+        it.first.player_id,
+        id.value_or(media::AudioDeviceDescription::kDefaultDeviceId));
+  }
+}
 
 void MediaSessionImpl::GetMediaImageBitmap(
     const media_session::MediaImage& image,
diff --git a/content/browser/media/session/media_session_impl_browsertest.cc b/content/browser/media/session/media_session_impl_browsertest.cc
index 255ea2f..d8bb5076 100644
--- a/content/browser/media/session/media_session_impl_browsertest.cc
+++ b/content/browser/media/session/media_session_impl_browsertest.cc
@@ -40,10 +40,10 @@
 using media_session::mojom::MediaPlaybackState;
 using media_session::mojom::MediaSessionInfo;
 
+using ::testing::_;
 using ::testing::Eq;
 using ::testing::Expectation;
 using ::testing::NiceMock;
-using ::testing::_;
 
 namespace {
 
@@ -56,6 +56,8 @@
 
 constexpr gfx::Size kDefaultFaviconSize = gfx::Size(16, 16);
 
+const std::string kExampleSinkId = "example_device_id";
+
 class MockAudioFocusDelegate : public content::AudioFocusDelegate {
  public:
   MockAudioFocusDelegate(content::MediaSessionImpl* media_session,
@@ -213,6 +215,10 @@
     media_session_->Seek(base::TimeDelta::FromSeconds(-1));
   }
 
+  void UISetAudioSink(const std::string& sink_id) {
+    media_session_->SetAudioSinkId(sink_id);
+  }
+
   void SystemStartDucking() { media_session_->StartDucking(); }
 
   void SystemStopDucking() { media_session_->StopDucking(); }
@@ -2592,6 +2598,17 @@
   }
 }
 
+IN_PROC_BROWSER_TEST_F(MediaSessionImplBrowserTest,
+                       SinkIdChangeNotifiesObservers) {
+  auto player_observer = std::make_unique<MockMediaSessionPlayerObserver>();
+
+  StartNewPlayer(player_observer.get(), media::MediaContentType::Persistent);
+
+  UISetAudioSink(kExampleSinkId);
+  EXPECT_EQ(player_observer->received_set_audio_sink_id_calls(), 1);
+  EXPECT_EQ(player_observer->GetAudioSinkId(0), kExampleSinkId);
+}
+
 class MediaSessionFaviconBrowserTest : public ContentBrowserTest {
  protected:
   MediaSessionFaviconBrowserTest() = default;
diff --git a/content/browser/media/session/media_session_impl_service_routing_unittest.cc b/content/browser/media/session/media_session_impl_service_routing_unittest.cc
index 9e398cab..5ab5476 100644
--- a/content/browser/media/session/media_session_impl_service_routing_unittest.cc
+++ b/content/browser/media/session/media_session_impl_service_routing_unittest.cc
@@ -53,6 +53,8 @@
                void(int player_id, double volume_multiplier));
   MOCK_METHOD1(OnEnterPictureInPicture, void(int player_id));
   MOCK_METHOD1(OnExitPictureInPicture, void(int player_id));
+  MOCK_METHOD2(OnSetAudioSinkId,
+               void(int player_id, const std::string& raw_device_id));
 
   base::Optional<media_session::MediaPosition> GetPosition(
       int player_id) const override {
diff --git a/content/browser/media/session/media_session_impl_uma_unittest.cc b/content/browser/media/session/media_session_impl_uma_unittest.cc
index a46977e..099c5889 100644
--- a/content/browser/media/session/media_session_impl_uma_unittest.cc
+++ b/content/browser/media/session/media_session_impl_uma_unittest.cc
@@ -41,6 +41,8 @@
   }
   void OnEnterPictureInPicture(int player_id) override {}
   void OnExitPictureInPicture(int player_id) override {}
+  void OnSetAudioSinkId(int player_id,
+                        const std::string& raw_device_id) override {}
 
   base::Optional<media_session::MediaPosition> GetPosition(
       int player_id) const override {
diff --git a/content/browser/media/session/media_session_player_observer.h b/content/browser/media/session/media_session_player_observer.h
index 2c65cda..dbd1e5fb 100644
--- a/content/browser/media/session/media_session_player_observer.h
+++ b/content/browser/media/session/media_session_player_observer.h
@@ -44,6 +44,11 @@
   // The given |player_id| has been requested to exit picture-in-picture.
   virtual void OnExitPictureInPicture(int player_id) = 0;
 
+  // The given |player_id| has been requested to route audio output to the
+  // specified audio device.
+  virtual void OnSetAudioSinkId(int player_id,
+                                const std::string& raw_device_id) = 0;
+
   // Returns the position for |player_id|.
   virtual base::Optional<media_session::MediaPosition> GetPosition(
       int player_id) const = 0;
diff --git a/content/browser/media/session/media_session_service_impl_browsertest.cc b/content/browser/media/session/media_session_service_impl_browsertest.cc
index 380ea98..2b2a30b5 100644
--- a/content/browser/media/session/media_session_service_impl_browsertest.cc
+++ b/content/browser/media/session/media_session_service_impl_browsertest.cc
@@ -54,6 +54,8 @@
   }
   void OnEnterPictureInPicture(int player_id) override {}
   void OnExitPictureInPicture(int player_id) override {}
+  void OnSetAudioSinkId(int player_id,
+                        const std::string& raw_device_id) override {}
 
   base::Optional<media_session::MediaPosition> GetPosition(
       int player_id) const override {
diff --git a/content/browser/media/session/mock_media_session_player_observer.cc b/content/browser/media/session/mock_media_session_player_observer.cc
index 48b10d0..1ad3845d 100644
--- a/content/browser/media/session/mock_media_session_player_observer.cc
+++ b/content/browser/media/session/mock_media_session_player_observer.cc
@@ -72,6 +72,16 @@
   players_[player_id].is_in_picture_in_picture_ = false;
 }
 
+void MockMediaSessionPlayerObserver::OnSetAudioSinkId(
+    int player_id,
+    const std::string& raw_device_id) {
+  EXPECT_GE(player_id, 0);
+  EXPECT_EQ(players_.size(), 1u);
+
+  ++received_set_audio_sink_id_calls_;
+  players_[player_id].audio_sink_id_ = raw_device_id;
+}
+
 base::Optional<media_session::MediaPosition>
 MockMediaSessionPlayerObserver::GetPosition(int player_id) const {
   EXPECT_GE(player_id, 0);
@@ -105,6 +115,12 @@
   return players_[player_id].volume_multiplier_;
 }
 
+const std::string& MockMediaSessionPlayerObserver::GetAudioSinkId(
+    size_t player_id) {
+  EXPECT_GT(players_.size(), player_id);
+  return players_[player_id].audio_sink_id_;
+}
+
 void MockMediaSessionPlayerObserver::SetPlaying(size_t player_id,
                                                 bool playing) {
   EXPECT_GT(players_.size(), player_id);
@@ -144,15 +160,23 @@
   return received_exit_picture_in_picture_calls_;
 }
 
+int MockMediaSessionPlayerObserver::received_set_audio_sink_id_calls() const {
+  return received_set_audio_sink_id_calls_;
+}
+
 bool MockMediaSessionPlayerObserver::HasVideo(int player_id) const {
   EXPECT_GE(player_id, 0);
   EXPECT_GT(players_.size(), static_cast<size_t>(player_id));
   return false;
 }
 
-MockMediaSessionPlayerObserver::MockPlayer::MockPlayer(bool is_playing,
-                                                       double volume_multiplier)
-    : is_playing_(is_playing), volume_multiplier_(volume_multiplier) {}
+MockMediaSessionPlayerObserver::MockPlayer::MockPlayer(
+    bool is_playing,
+    double volume_multiplier,
+    const std::string& audio_sink_id)
+    : is_playing_(is_playing),
+      volume_multiplier_(volume_multiplier),
+      audio_sink_id_(audio_sink_id) {}
 
 MockMediaSessionPlayerObserver::MockPlayer::~MockPlayer() = default;
 
diff --git a/content/browser/media/session/mock_media_session_player_observer.h b/content/browser/media/session/mock_media_session_player_observer.h
index 28e9723..e50db8b4 100644
--- a/content/browser/media/session/mock_media_session_player_observer.h
+++ b/content/browser/media/session/mock_media_session_player_observer.h
@@ -30,6 +30,8 @@
   void OnSetVolumeMultiplier(int player_id, double volume_multiplier) override;
   void OnEnterPictureInPicture(int player_id) override;
   void OnExitPictureInPicture(int player_id) override;
+  void OnSetAudioSinkId(int player_id,
+                        const std::string& raw_device_id) override;
   base::Optional<media_session::MediaPosition> GetPosition(
       int player_id) const override;
   bool IsPictureInPictureAvailable(int player_id) const override;
@@ -46,6 +48,9 @@
   // Returns the volume multiplier of |player_id|.
   double GetVolumeMultiplier(size_t player_id);
 
+  // Returns the sink id being used for the audio output of |player_id|
+  const std::string& GetAudioSinkId(size_t player_id);
+
   // Simulate a play state change for |player_id|.
   void SetPlaying(size_t player_id, bool playing);
 
@@ -58,12 +63,15 @@
   int received_seek_backward_calls() const;
   int received_enter_picture_in_picture_calls() const;
   int received_exit_picture_in_picture_calls() const;
+  int received_set_audio_sink_id_calls() const;
 
  private:
   // Internal representation of the players to keep track of their statuses.
   struct MockPlayer {
    public:
-    MockPlayer(bool is_playing = true, double volume_multiplier = 1.0f);
+    explicit MockPlayer(bool is_playing = true,
+                        double volume_multiplier = 1.0f,
+                        const std::string& audio_sink_id = "");
     ~MockPlayer();
     MockPlayer(const MockPlayer&);
 
@@ -71,6 +79,7 @@
     double volume_multiplier_;
     base::Optional<media_session::MediaPosition> position_;
     bool is_in_picture_in_picture_;
+    std::string audio_sink_id_;
   };
 
   // Basic representation of the players. The position in the vector is the
@@ -85,6 +94,7 @@
   int received_seek_backward_calls_ = 0;
   int received_enter_picture_in_picture_calls_ = 0;
   int received_exit_picture_in_picture_calls_ = 0;
+  int received_set_audio_sink_id_calls_ = 0;
 };
 
 }  // namespace content
diff --git a/content/browser/media/session/pepper_player_delegate.cc b/content/browser/media/session/pepper_player_delegate.cc
index 6512ecbc..2e5648c8 100644
--- a/content/browser/media/session/pepper_player_delegate.cc
+++ b/content/browser/media/session/pepper_player_delegate.cc
@@ -72,6 +72,12 @@
   // Pepper player cannot exit picture-in-picture. Do nothing.
 }
 
+void PepperPlayerDelegate::OnSetAudioSinkId(int player_id,
+                                            const std::string& raw_device_id) {
+  // Pepper player cannot change audio sinks. Do nothing.
+  NOTREACHED();
+}
+
 base::Optional<media_session::MediaPosition> PepperPlayerDelegate::GetPosition(
     int player_id) const {
   // Pepper does not support position data.
diff --git a/content/browser/media/session/pepper_player_delegate.h b/content/browser/media/session/pepper_player_delegate.h
index 33a9b2b..9b24467d 100644
--- a/content/browser/media/session/pepper_player_delegate.h
+++ b/content/browser/media/session/pepper_player_delegate.h
@@ -32,6 +32,8 @@
                              double volume_multiplier) override;
   void OnEnterPictureInPicture(int player_id) override;
   void OnExitPictureInPicture(int player_id) override;
+  void OnSetAudioSinkId(int player_id,
+                        const std::string& raw_device_id) override;
   base::Optional<media_session::MediaPosition> GetPosition(
       int player_id) const override;
   bool IsPictureInPictureAvailable(int player_id) const override;
diff --git a/content/browser/utility_process_host.cc b/content/browser/utility_process_host.cc
index 22d001d..088622a 100644
--- a/content/browser/utility_process_host.cc
+++ b/content/browser/utility_process_host.cc
@@ -18,6 +18,7 @@
 #include "components/network_session_configurator/common/network_switches.h"
 #include "content/browser/browser_child_process_host_impl.h"
 #include "content/browser/renderer_host/render_process_host_impl.h"
+#include "content/browser/utility_sandbox_delegate.h"
 #include "content/browser/v8_snapshot_files.h"
 #include "content/common/child_process_host_impl.h"
 #include "content/common/in_process_child_thread_params.h"
@@ -29,10 +30,8 @@
 #include "content/public/common/content_switches.h"
 #include "content/public/common/process_type.h"
 #include "content/public/common/sandboxed_process_launcher_delegate.h"
-#include "content/public/common/zygote/zygote_buildflags.h"
 #include "media/base/media_switches.h"
 #include "media/webrtc/webrtc_switches.h"
-#include "sandbox/policy/features.h"
 #include "sandbox/policy/sandbox_type.h"
 #include "sandbox/policy/switches.h"
 #include "services/network/public/cpp/network_switches.h"
@@ -45,229 +44,8 @@
 #include "components/os_crypt/os_crypt_switches.h"
 #endif
 
-#if defined(OS_WIN)
-#include "sandbox/win/src/sandbox_policy.h"
-#include "sandbox/win/src/sandbox_types.h"
-#include "services/audio/audio_sandbox_win.h"
-#include "services/network/network_sandbox_win.h"
-#endif
-
-#if BUILDFLAG(USE_ZYGOTE_HANDLE)
-#include "content/common/zygote/zygote_handle_impl_linux.h"
-#endif
-
 namespace content {
 
-// NOTE: changes to this class need to be reviewed by the security team.
-class UtilitySandboxedProcessLauncherDelegate
-    : public SandboxedProcessLauncherDelegate {
- public:
-  UtilitySandboxedProcessLauncherDelegate(
-      sandbox::policy::SandboxType sandbox_type,
-      const base::EnvironmentMap& env,
-      const base::CommandLine& cmd_line)
-      :
-#if defined(OS_POSIX)
-        env_(env),
-#endif
-        sandbox_type_(sandbox_type),
-        cmd_line_(cmd_line) {
-#if DCHECK_IS_ON()
-    bool supported_sandbox_type =
-        sandbox_type_ == sandbox::policy::SandboxType::kNoSandbox ||
-#if defined(OS_WIN)
-        sandbox_type_ ==
-            sandbox::policy::SandboxType::kNoSandboxAndElevatedPrivileges ||
-        sandbox_type_ == sandbox::policy::SandboxType::kXrCompositing ||
-        sandbox_type_ == sandbox::policy::SandboxType::kProxyResolver ||
-        sandbox_type_ == sandbox::policy::SandboxType::kPdfConversion ||
-        sandbox_type_ == sandbox::policy::SandboxType::kIconReader ||
-#endif
-        sandbox_type_ == sandbox::policy::SandboxType::kUtility ||
-        sandbox_type_ == sandbox::policy::SandboxType::kNetwork ||
-        sandbox_type_ == sandbox::policy::SandboxType::kCdm ||
-        sandbox_type_ == sandbox::policy::SandboxType::kPrintCompositor ||
-        sandbox_type_ == sandbox::policy::SandboxType::kPpapi ||
-        sandbox_type_ == sandbox::policy::SandboxType::kVideoCapture ||
-#if defined(OS_CHROMEOS)
-        sandbox_type_ == sandbox::policy::SandboxType::kIme ||
-        sandbox_type_ == sandbox::policy::SandboxType::kTts ||
-#endif  // OS_CHROMEOS
-        sandbox_type_ == sandbox::policy::SandboxType::kAudio ||
-#if !defined(OS_MAC)
-        sandbox_type_ == sandbox::policy::SandboxType::kSharingService ||
-#endif
-        sandbox_type_ == sandbox::policy::SandboxType::kSpeechRecognition;
-    DCHECK(supported_sandbox_type);
-#endif  // DCHECK_IS_ON()
-  }
-
-  ~UtilitySandboxedProcessLauncherDelegate() override = default;
-
-#if defined(OS_WIN)
-  bool GetAppContainerId(std::string* appcontainer_id) override {
-    if (sandbox_type_ == sandbox::policy::SandboxType::kXrCompositing &&
-        base::FeatureList::IsEnabled(sandbox::policy::features::kXRSandbox)) {
-      *appcontainer_id = base::WideToUTF8(cmd_line_.GetProgram().value());
-      return true;
-    }
-    return false;
-  }
-
-  bool DisableDefaultPolicy() override {
-    switch (sandbox_type_) {
-      case sandbox::policy::SandboxType::kAudio:
-        // Default policy is disabled for audio process to allow audio drivers
-        // to read device properties (https://crbug.com/883326).
-        return true;
-      case sandbox::policy::SandboxType::kNetwork:
-        // Default policy is disabled for network process to allow incremental
-        // sandbox mitigations to be applied via experiments.
-        return true;
-      case sandbox::policy::SandboxType::kXrCompositing:
-        return base::FeatureList::IsEnabled(
-            sandbox::policy::features::kXRSandbox);
-      default:
-        return false;
-    }
-  }
-
-  bool ShouldLaunchElevated() override {
-    return sandbox_type_ ==
-           sandbox::policy::SandboxType::kNoSandboxAndElevatedPrivileges;
-  }
-
-  bool PreSpawnTarget(sandbox::TargetPolicy* policy) override {
-    if (sandbox_type_ == sandbox::policy::SandboxType::kNetwork)
-      return network::NetworkPreSpawnTarget(policy, cmd_line_);
-
-    if (sandbox_type_ == sandbox::policy::SandboxType::kAudio)
-      return audio::AudioPreSpawnTarget(policy);
-
-    if (sandbox_type_ == sandbox::policy::SandboxType::kProxyResolver) {
-      sandbox::MitigationFlags flags = policy->GetDelayedProcessMitigations();
-      flags |= sandbox::MITIGATION_DYNAMIC_CODE_DISABLE;
-      if (sandbox::SBOX_ALL_OK != policy->SetDelayedProcessMitigations(flags))
-        return false;
-      return true;
-    }
-
-    if (sandbox_type_ == sandbox::policy::SandboxType::kSpeechRecognition) {
-      policy->SetDelayedIntegrityLevel(sandbox::INTEGRITY_LEVEL_LOW);
-      policy->SetIntegrityLevel(sandbox::INTEGRITY_LEVEL_LOW);
-      policy->SetTokenLevel(sandbox::USER_RESTRICTED_SAME_ACCESS,
-                            sandbox::USER_LIMITED);
-    }
-
-    if (sandbox_type_ == sandbox::policy::SandboxType::kIconReader) {
-      policy->SetTokenLevel(sandbox::USER_RESTRICTED_SAME_ACCESS,
-                            sandbox::USER_LOCKDOWN);
-      policy->SetDelayedIntegrityLevel(sandbox::INTEGRITY_LEVEL_UNTRUSTED);
-      policy->SetIntegrityLevel(sandbox::INTEGRITY_LEVEL_LOW);
-      policy->SetLockdownDefaultDacl();
-      policy->SetAlternateDesktop(true);
-
-      sandbox::MitigationFlags flags = policy->GetDelayedProcessMitigations();
-      flags |= sandbox::MITIGATION_DYNAMIC_CODE_DISABLE;
-      if (sandbox::SBOX_ALL_OK != policy->SetDelayedProcessMitigations(flags))
-        return false;
-
-      // Allow file read. These should match IconLoader::GroupForFilepath().
-      policy->AddRule(sandbox::TargetPolicy::SUBSYS_FILES,
-                      sandbox::TargetPolicy::FILES_ALLOW_READONLY,
-                      L"\\??\\*.exe");
-      policy->AddRule(sandbox::TargetPolicy::SUBSYS_FILES,
-                      sandbox::TargetPolicy::FILES_ALLOW_READONLY,
-                      L"\\??\\*.dll");
-      policy->AddRule(sandbox::TargetPolicy::SUBSYS_FILES,
-                      sandbox::TargetPolicy::FILES_ALLOW_READONLY,
-                      L"\\??\\*.ico");
-    }
-
-    if (sandbox_type_ == sandbox::policy::SandboxType::kXrCompositing &&
-        base::FeatureList::IsEnabled(sandbox::policy::features::kXRSandbox)) {
-      // There were issues with some mitigations, causing an inability
-      // to load OpenVR and Oculus APIs.
-      // TODO(https://crbug.com/881919): Try to harden the XR Compositor
-      // sandbox to use mitigations and restrict the token.
-      policy->SetProcessMitigations(0);
-      policy->SetDelayedProcessMitigations(0);
-
-      std::string appcontainer_id;
-      if (!GetAppContainerId(&appcontainer_id)) {
-        return false;
-      }
-      sandbox::ResultCode result =
-          sandbox::policy::SandboxWin::AddAppContainerProfileToPolicy(
-              cmd_line_, sandbox_type_, appcontainer_id, policy);
-      if (result != sandbox::SBOX_ALL_OK) {
-        return false;
-      }
-
-      // Unprotected token/job.
-      policy->SetTokenLevel(sandbox::USER_UNPROTECTED,
-                            sandbox::USER_UNPROTECTED);
-      sandbox::policy::SandboxWin::SetJobLevel(
-          cmd_line_, sandbox::JOB_UNPROTECTED, 0, policy);
-    }
-
-    if (sandbox_type_ == sandbox::policy::SandboxType::kSharingService) {
-      auto result =
-          sandbox::policy::SandboxWin::AddWin32kLockdownPolicy(policy, false);
-      if (result != sandbox::SBOX_ALL_OK)
-        return false;
-
-      auto delayed_flags = policy->GetDelayedProcessMitigations();
-      delayed_flags |= sandbox::MITIGATION_DYNAMIC_CODE_DISABLE;
-      result = policy->SetDelayedProcessMitigations(delayed_flags);
-      if (result != sandbox::SBOX_ALL_OK)
-        return false;
-    }
-
-    return true;
-  }
-#endif  // OS_WIN
-
-#if BUILDFLAG(USE_ZYGOTE_HANDLE)
-  ZygoteHandle GetZygote() override {
-    // If the sandbox has been disabled for a given type, don't use a zygote.
-    if (sandbox::policy::IsUnsandboxedSandboxType(sandbox_type_))
-      return nullptr;
-
-    // Utility processes which need specialized sandboxes fork from the
-    // unsandboxed zygote and then apply their actual sandboxes in the forked
-    // process upon startup.
-    if (sandbox_type_ == sandbox::policy::SandboxType::kNetwork ||
-#if defined(OS_CHROMEOS)
-        sandbox_type_ == sandbox::policy::SandboxType::kIme ||
-        sandbox_type_ == sandbox::policy::SandboxType::kTts ||
-#endif  // OS_CHROMEOS
-        sandbox_type_ == sandbox::policy::SandboxType::kAudio ||
-        sandbox_type_ == sandbox::policy::SandboxType::kSpeechRecognition) {
-      return GetUnsandboxedZygote();
-    }
-
-    // All other types use the pre-sandboxed zygote.
-    return GetGenericZygote();
-  }
-#endif  // BUILDFLAG(USE_ZYGOTE_HANDLE)
-
-#if defined(OS_POSIX)
-  base::EnvironmentMap GetEnvironment() override { return env_; }
-#endif  // OS_POSIX
-
-  sandbox::policy::SandboxType GetSandboxType() override {
-    return sandbox_type_;
-  }
-
- private:
-#if defined(OS_POSIX)
-  base::EnvironmentMap env_;
-#endif  // OS_POSIX
-  sandbox::policy::SandboxType sandbox_type_;
-  base::CommandLine cmd_line_;
-};
-
 UtilityMainThreadFactoryFunction g_utility_main_thread_factory = nullptr;
 
 void UtilityProcessHost::RegisterUtilityMainThreadFactory(
diff --git a/content/browser/utility_sandbox_delegate.cc b/content/browser/utility_sandbox_delegate.cc
new file mode 100644
index 0000000..0d00694
--- /dev/null
+++ b/content/browser/utility_sandbox_delegate.cc
@@ -0,0 +1,98 @@
+// 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/utility_sandbox_delegate.h"
+
+#include "base/check.h"
+#include "build/build_config.h"
+#include "content/public/common/sandboxed_process_launcher_delegate.h"
+#include "content/public/common/zygote/zygote_buildflags.h"
+#include "sandbox/policy/sandbox_type.h"
+
+#if BUILDFLAG(USE_ZYGOTE_HANDLE)
+#include "content/common/zygote/zygote_handle_impl_linux.h"
+#endif
+
+namespace content {
+
+UtilitySandboxedProcessLauncherDelegate::
+    UtilitySandboxedProcessLauncherDelegate(
+        sandbox::policy::SandboxType sandbox_type,
+        const base::EnvironmentMap& env,
+        const base::CommandLine& cmd_line)
+    :
+#if defined(OS_POSIX)
+      env_(env),
+#endif
+      sandbox_type_(sandbox_type),
+      cmd_line_(cmd_line) {
+#if DCHECK_IS_ON()
+  bool supported_sandbox_type =
+      sandbox_type_ == sandbox::policy::SandboxType::kNoSandbox ||
+#if defined(OS_WIN)
+      sandbox_type_ ==
+          sandbox::policy::SandboxType::kNoSandboxAndElevatedPrivileges ||
+      sandbox_type_ == sandbox::policy::SandboxType::kXrCompositing ||
+      sandbox_type_ == sandbox::policy::SandboxType::kProxyResolver ||
+      sandbox_type_ == sandbox::policy::SandboxType::kPdfConversion ||
+      sandbox_type_ == sandbox::policy::SandboxType::kIconReader ||
+#endif
+      sandbox_type_ == sandbox::policy::SandboxType::kUtility ||
+      sandbox_type_ == sandbox::policy::SandboxType::kNetwork ||
+      sandbox_type_ == sandbox::policy::SandboxType::kCdm ||
+      sandbox_type_ == sandbox::policy::SandboxType::kPrintCompositor ||
+      sandbox_type_ == sandbox::policy::SandboxType::kPpapi ||
+      sandbox_type_ == sandbox::policy::SandboxType::kVideoCapture ||
+#if defined(OS_CHROMEOS)
+      sandbox_type_ == sandbox::policy::SandboxType::kIme ||
+      sandbox_type_ == sandbox::policy::SandboxType::kTts ||
+#endif  // OS_CHROMEOS
+      sandbox_type_ == sandbox::policy::SandboxType::kAudio ||
+#if !defined(OS_MAC)
+      sandbox_type_ == sandbox::policy::SandboxType::kSharingService ||
+#endif
+      sandbox_type_ == sandbox::policy::SandboxType::kSpeechRecognition;
+  DCHECK(supported_sandbox_type);
+#endif  // DCHECK_IS_ON()
+}
+
+UtilitySandboxedProcessLauncherDelegate::
+    ~UtilitySandboxedProcessLauncherDelegate() {}
+
+sandbox::policy::SandboxType
+UtilitySandboxedProcessLauncherDelegate::GetSandboxType() {
+  return sandbox_type_;
+}
+
+#if defined(OS_POSIX)
+base::EnvironmentMap UtilitySandboxedProcessLauncherDelegate::GetEnvironment() {
+  return env_;
+}
+#endif  // OS_POSIX
+
+#if BUILDFLAG(USE_ZYGOTE_HANDLE)
+ZygoteHandle UtilitySandboxedProcessLauncherDelegate::GetZygote() {
+  // If the sandbox has been disabled for a given type, don't use a zygote.
+  if (sandbox::policy::IsUnsandboxedSandboxType(sandbox_type_))
+    return nullptr;
+
+  // Utility processes which need specialized sandboxes fork from the
+  // unsandboxed zygote and then apply their actual sandboxes in the forked
+  // process upon startup.
+  if (sandbox_type_ == sandbox::policy::SandboxType::kNetwork ||
+#if defined(OS_CHROMEOS)
+      sandbox_type_ == sandbox::policy::SandboxType::kIme ||
+      sandbox_type_ == sandbox::policy::SandboxType::kTts ||
+#endif  // OS_CHROMEOS
+      sandbox_type_ == sandbox::policy::SandboxType::kAudio ||
+      sandbox_type_ == sandbox::policy::SandboxType::kSpeechRecognition) {
+    return GetUnsandboxedZygote();
+  }
+
+  // All other types use the pre-sandboxed zygote.
+  return GetGenericZygote();
+}
+#endif  // BUILDFLAG(USE_ZYGOTE_HANDLE)
+
+}  // namespace content
diff --git a/content/browser/utility_sandbox_delegate.h b/content/browser/utility_sandbox_delegate.h
new file mode 100644
index 0000000..a9d6f76
--- /dev/null
+++ b/content/browser/utility_sandbox_delegate.h
@@ -0,0 +1,59 @@
+// 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_UTILITY_SANDBOX_DELEGATE_H_
+#define CONTENT_BROWSER_UTILITY_SANDBOX_DELEGATE_H_
+
+#include "base/command_line.h"
+#include "base/environment.h"
+#include "build/build_config.h"
+#include "content/public/common/sandboxed_process_launcher_delegate.h"
+#include "content/public/common/zygote/zygote_buildflags.h"
+#include "sandbox/policy/sandbox_type.h"
+
+#if BUILDFLAG(USE_ZYGOTE_HANDLE)
+#include "content/common/zygote/zygote_handle_impl_linux.h"
+#endif  // BUILDFLAG(USE_ZYGOTE_HANDLE)
+
+#if defined(OS_WIN)
+#include "sandbox/win/src/sandbox_policy.h"
+#endif  // OS_WIN
+
+namespace content {
+class UtilitySandboxedProcessLauncherDelegate
+    : public SandboxedProcessLauncherDelegate {
+ public:
+  UtilitySandboxedProcessLauncherDelegate(
+      sandbox::policy::SandboxType sandbox_type,
+      const base::EnvironmentMap& env,
+      const base::CommandLine& cmd_line);
+  ~UtilitySandboxedProcessLauncherDelegate() override;
+
+  sandbox::policy::SandboxType GetSandboxType() override;
+
+#if defined(OS_WIN)
+  bool GetAppContainerId(std::string* appcontainer_id) override;
+  bool DisableDefaultPolicy() override;
+  bool ShouldLaunchElevated() override;
+  bool PreSpawnTarget(sandbox::TargetPolicy* policy) override;
+#endif  // OS_WIN
+
+#if BUILDFLAG(USE_ZYGOTE_HANDLE)
+  ZygoteHandle GetZygote() override;
+#endif  // BUILDFLAG(USE_ZYGOTE_HANDLE)
+
+#if defined(OS_POSIX)
+  base::EnvironmentMap GetEnvironment() override;
+#endif  // OS_POSIX
+
+ private:
+#if defined(OS_POSIX)
+  base::EnvironmentMap env_;
+#endif  // OS_POSIX
+  sandbox::policy::SandboxType sandbox_type_;
+  base::CommandLine cmd_line_;
+};
+}  // namespace content
+
+#endif  // CONTENT_BROWSER_UTILITY_SANDBOX_DELEGATE_H_
diff --git a/content/browser/utility_sandbox_delegate_win.cc b/content/browser/utility_sandbox_delegate_win.cc
new file mode 100644
index 0000000..8bffc71
--- /dev/null
+++ b/content/browser/utility_sandbox_delegate_win.cc
@@ -0,0 +1,142 @@
+// 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/utility_sandbox_delegate.h"
+
+#include "base/check.h"
+#include "base/feature_list.h"
+#include "base/strings/utf_string_conversions.h"
+#include "content/public/common/sandboxed_process_launcher_delegate.h"
+#include "sandbox/policy/features.h"
+#include "sandbox/policy/sandbox_type.h"
+#include "sandbox/win/src/sandbox_policy.h"
+#include "sandbox/win/src/sandbox_types.h"
+#include "services/audio/audio_sandbox_win.h"
+#include "services/network/network_sandbox_win.h"
+
+namespace content {
+bool UtilitySandboxedProcessLauncherDelegate::GetAppContainerId(
+    std::string* appcontainer_id) {
+  if (sandbox_type_ == sandbox::policy::SandboxType::kXrCompositing &&
+      base::FeatureList::IsEnabled(sandbox::policy::features::kXRSandbox)) {
+    *appcontainer_id = base::WideToUTF8(cmd_line_.GetProgram().value());
+    return true;
+  }
+  return false;
+}
+
+bool UtilitySandboxedProcessLauncherDelegate::DisableDefaultPolicy() {
+  switch (sandbox_type_) {
+    case sandbox::policy::SandboxType::kAudio:
+      // Default policy is disabled for audio process to allow audio drivers
+      // to read device properties (https://crbug.com/883326).
+      return true;
+    case sandbox::policy::SandboxType::kNetwork:
+      // Default policy is disabled for network process to allow incremental
+      // sandbox mitigations to be applied via experiments.
+      return true;
+    case sandbox::policy::SandboxType::kXrCompositing:
+      return base::FeatureList::IsEnabled(
+          sandbox::policy::features::kXRSandbox);
+    default:
+      return false;
+  }
+}
+
+bool UtilitySandboxedProcessLauncherDelegate::ShouldLaunchElevated() {
+  return sandbox_type_ ==
+         sandbox::policy::SandboxType::kNoSandboxAndElevatedPrivileges;
+}
+
+bool UtilitySandboxedProcessLauncherDelegate::PreSpawnTarget(
+    sandbox::TargetPolicy* policy) {
+  if (sandbox_type_ == sandbox::policy::SandboxType::kNetwork)
+    return network::NetworkPreSpawnTarget(policy, cmd_line_);
+
+  if (sandbox_type_ == sandbox::policy::SandboxType::kAudio)
+    return audio::AudioPreSpawnTarget(policy);
+
+  if (sandbox_type_ == sandbox::policy::SandboxType::kProxyResolver) {
+    sandbox::MitigationFlags flags = policy->GetDelayedProcessMitigations();
+    flags |= sandbox::MITIGATION_DYNAMIC_CODE_DISABLE;
+    if (sandbox::SBOX_ALL_OK != policy->SetDelayedProcessMitigations(flags))
+      return false;
+    return true;
+  }
+
+  if (sandbox_type_ == sandbox::policy::SandboxType::kSpeechRecognition) {
+    policy->SetDelayedIntegrityLevel(sandbox::INTEGRITY_LEVEL_LOW);
+    policy->SetIntegrityLevel(sandbox::INTEGRITY_LEVEL_LOW);
+    policy->SetTokenLevel(sandbox::USER_RESTRICTED_SAME_ACCESS,
+                          sandbox::USER_LIMITED);
+  }
+
+  if (sandbox_type_ == sandbox::policy::SandboxType::kIconReader) {
+    policy->SetTokenLevel(sandbox::USER_RESTRICTED_SAME_ACCESS,
+                          sandbox::USER_LOCKDOWN);
+    policy->SetDelayedIntegrityLevel(sandbox::INTEGRITY_LEVEL_UNTRUSTED);
+    policy->SetIntegrityLevel(sandbox::INTEGRITY_LEVEL_LOW);
+    policy->SetLockdownDefaultDacl();
+    policy->SetAlternateDesktop(true);
+
+    sandbox::MitigationFlags flags = policy->GetDelayedProcessMitigations();
+    flags |= sandbox::MITIGATION_DYNAMIC_CODE_DISABLE;
+    if (sandbox::SBOX_ALL_OK != policy->SetDelayedProcessMitigations(flags))
+      return false;
+
+    // Allow file read. These should match IconLoader::GroupForFilepath().
+    policy->AddRule(sandbox::TargetPolicy::SUBSYS_FILES,
+                    sandbox::TargetPolicy::FILES_ALLOW_READONLY,
+                    L"\\??\\*.exe");
+    policy->AddRule(sandbox::TargetPolicy::SUBSYS_FILES,
+                    sandbox::TargetPolicy::FILES_ALLOW_READONLY,
+                    L"\\??\\*.dll");
+    policy->AddRule(sandbox::TargetPolicy::SUBSYS_FILES,
+                    sandbox::TargetPolicy::FILES_ALLOW_READONLY,
+                    L"\\??\\*.ico");
+  }
+
+  if (sandbox_type_ == sandbox::policy::SandboxType::kXrCompositing &&
+      base::FeatureList::IsEnabled(sandbox::policy::features::kXRSandbox)) {
+    // There were issues with some mitigations, causing an inability
+    // to load OpenVR and Oculus APIs.
+    // TODO(https://crbug.com/881919): Try to harden the XR Compositor
+    // sandbox to use mitigations and restrict the token.
+    policy->SetProcessMitigations(0);
+    policy->SetDelayedProcessMitigations(0);
+
+    std::string appcontainer_id;
+    if (!GetAppContainerId(&appcontainer_id)) {
+      return false;
+    }
+    sandbox::ResultCode result =
+        sandbox::policy::SandboxWin::AddAppContainerProfileToPolicy(
+            cmd_line_, sandbox_type_, appcontainer_id, policy);
+    if (result != sandbox::SBOX_ALL_OK) {
+      return false;
+    }
+
+    // Unprotected token/job.
+    policy->SetTokenLevel(sandbox::USER_UNPROTECTED, sandbox::USER_UNPROTECTED);
+    sandbox::policy::SandboxWin::SetJobLevel(
+        cmd_line_, sandbox::JOB_UNPROTECTED, 0, policy);
+  }
+
+  if (sandbox_type_ == sandbox::policy::SandboxType::kSharingService) {
+    auto result =
+        sandbox::policy::SandboxWin::AddWin32kLockdownPolicy(policy, false);
+    if (result != sandbox::SBOX_ALL_OK)
+      return false;
+
+    auto delayed_flags = policy->GetDelayedProcessMitigations();
+    delayed_flags |= sandbox::MITIGATION_DYNAMIC_CODE_DISABLE;
+    result = policy->SetDelayedProcessMitigations(delayed_flags);
+    if (result != sandbox::SBOX_ALL_OK)
+      return false;
+  }
+
+  return true;
+}
+
+}  // namespace content
diff --git a/content/browser/web_contents/web_contents_impl_unittest.cc b/content/browser/web_contents/web_contents_impl_unittest.cc
index b8561753..cc362411 100644
--- a/content/browser/web_contents/web_contents_impl_unittest.cc
+++ b/content/browser/web_contents/web_contents_impl_unittest.cc
@@ -979,20 +979,36 @@
   EXPECT_TRUE(contents()->CrossProcessNavigationPending());
 
   // Suppose the original renderer navigates before the new one is ready.
-  NavigationSimulator::NavigateAndCommitFromDocument(
-      GURL("http://www.google.com/foo"), orig_rfh);
+  const GURL url3("http://www.google.com/foo");
+  NavigationSimulator::NavigateAndCommitFromDocument(url3, orig_rfh);
 
   // Verify that the pending navigation is cancelled.
-  EXPECT_FALSE(orig_rfh->is_waiting_for_beforeunload_completion());
   SiteInstance* instance2 = contents()->GetSiteInstance();
+  if (CanSameSiteMainFrameNavigationsChangeRenderFrameHosts()) {
+    // If same-site ProactivelySwapBrowsingInstance or main-frame RenderDocument
+    // is enabled, the RFH should change.
+    EXPECT_NE(orig_rfh, main_test_rfh());
+  } else {
+    EXPECT_EQ(orig_rfh, main_test_rfh());
+  }
+  if (CanSameSiteMainFrameNavigationsChangeSiteInstances()) {
+    // When ProactivelySwapBrowsingInstance is enabled on same-site navigations,
+    // the SiteInstance will change.
+    EXPECT_NE(instance1, instance2);
+  } else {
+    EXPECT_EQ(instance1, instance2);
+  }
+  EXPECT_FALSE(main_test_rfh()->is_waiting_for_beforeunload_completion());
+  EXPECT_EQ(main_test_rfh()->GetLastCommittedURL(), url3);
   EXPECT_FALSE(contents()->CrossProcessNavigationPending());
-  EXPECT_EQ(orig_rfh, main_test_rfh());
-  EXPECT_EQ(instance1, instance2);
   EXPECT_EQ(nullptr, contents()->GetPendingMainFrame());
 }
 
 // Tests that if we go back twice (same-site then cross-site), and the same-site
-// RFH commits first, the cross-site RFH's navigation is canceled.
+// RFH commits first, the cross-site RFH's navigation is canceled. If the
+// same-site navigation is a cross-RFH navigation, however, the same-site
+// navigation will get canceled instead and we are left with the newer
+// cross-site navigation.
 // TODO(avi,creis): Consider changing this behavior to better match the user's
 // intent.
 TEST_F(WebContentsImplTest, CrossSiteNavigationBackPreempted) {
@@ -1032,8 +1048,20 @@
   SiteInstance* instance3 = contents()->GetSiteInstance();
 
   EXPECT_FALSE(contents()->CrossProcessNavigationPending());
-  EXPECT_EQ(google_rfh, main_test_rfh());
-  EXPECT_EQ(instance2, instance3);
+  if (CanSameSiteMainFrameNavigationsChangeRenderFrameHosts()) {
+    // If same-site ProactivelySwapBrowsingInstance or main-frame RenderDocument
+    // is enabled, the RFH should change.
+    EXPECT_NE(google_rfh, main_test_rfh());
+  } else {
+    EXPECT_EQ(google_rfh, main_test_rfh());
+  }
+  if (CanSameSiteMainFrameNavigationsChangeSiteInstances()) {
+    // When ProactivelySwapBrowsingInstance is enabled on same-site navigations,
+    // the SiteInstance will change.
+    EXPECT_NE(instance2, instance3);
+  } else {
+    EXPECT_EQ(instance2, instance3);
+  }
   EXPECT_FALSE(contents()->GetPendingMainFrame());
   EXPECT_EQ(url3, entry3->GetURL());
   EXPECT_EQ(instance3,
@@ -1041,28 +1069,60 @@
 
   // Go back within the site.
   auto back_navigation1 =
-      NavigationSimulator::CreateHistoryNavigation(-1, contents());
+      NavigationSimulatorImpl::CreateHistoryNavigation(-1, contents());
   back_navigation1->Start();
-  EXPECT_FALSE(contents()->CrossProcessNavigationPending());
+
+  auto* first_pending_rfh = contents()->GetPendingMainFrame();
+  GlobalFrameRoutingId first_pending_rfh_id;
+  if (CanSameSiteMainFrameNavigationsChangeRenderFrameHosts()) {
+    EXPECT_TRUE(contents()->CrossProcessNavigationPending());
+    EXPECT_TRUE(first_pending_rfh);
+    first_pending_rfh_id = first_pending_rfh->GetGlobalFrameRoutingId();
+  } else {
+    EXPECT_FALSE(contents()->CrossProcessNavigationPending());
+    EXPECT_FALSE(first_pending_rfh);
+  }
   EXPECT_EQ(entry2, controller().GetPendingEntry());
 
   // Before that commits, go back again.
   back_navigation1->ReadyToCommit();
   auto back_navigation2 =
-      NavigationSimulator::CreateHistoryNavigation(-1, contents());
+      NavigationSimulatorImpl::CreateHistoryNavigation(-1, contents());
   back_navigation2->Start();
   EXPECT_TRUE(contents()->CrossProcessNavigationPending());
   EXPECT_TRUE(contents()->GetPendingMainFrame());
   EXPECT_EQ(entry1, controller().GetPendingEntry());
+  if (CanSameSiteMainFrameNavigationsChangeRenderFrameHosts()) {
+    // When ProactivelySwapBrowsingInstance or RenderDocument is enabled on
+    // same-site main frame navigation, the first back navigation will create a
+    // speculative RFH even though it's a same-site navigation, and the
+    // speculative RFH will be overwritten by the second back-navigation that
+    // will also create a speculative RFH.
+    EXPECT_NE(first_pending_rfh_id,
+              contents()->GetPendingMainFrame()->GetGlobalFrameRoutingId());
+    // Calling Commit() on the first back navigation below will cause a DCHECK
+    // failure because we've already called DidFinishNavigaition on it, so we
+    // will call it on the second back navigation instead.
+    back_navigation2->Commit();
+  } else {
+    // DidNavigate from the first back. This aborts the second back's
+    // speculative RFH.
+    back_navigation1->Commit();
+  }
 
-  // DidNavigate from the first back. This aborts the second back's pending RFH.
-  back_navigation1->Commit();
-
-  // We should commit this page and forget about the second back.
+  // We have committed this navigation and forgot about the second back if
+  // CanSameSiteMainFrameNavigationsChangeRenderFrameHosts() is false, or the
+  // first back if it's true.
   EXPECT_FALSE(contents()->CrossProcessNavigationPending());
   EXPECT_FALSE(controller().GetPendingEntry());
   EXPECT_EQ(google_rfh, main_test_rfh());
-  EXPECT_EQ(url2, controller().GetLastCommittedEntry()->GetURL());
+  if (CanSameSiteMainFrameNavigationsChangeRenderFrameHosts()) {
+    // We committed the second back navigation and landed on the first page.
+    EXPECT_EQ(url1, controller().GetLastCommittedEntry()->GetURL());
+  } else {
+    // We committed the second back navigation and landed on the second page.
+    EXPECT_EQ(url2, controller().GetLastCommittedEntry()->GetURL());
+  }
 
   // We should not have corrupted the NTP entry.
   EXPECT_EQ(instance3,
@@ -1113,8 +1173,21 @@
   SiteInstance* instance3 = contents()->GetSiteInstance();
 
   EXPECT_FALSE(contents()->CrossProcessNavigationPending());
-  EXPECT_EQ(google_rfh, main_test_rfh());
-  EXPECT_EQ(instance2, instance3);
+  if (CanSameSiteMainFrameNavigationsChangeRenderFrameHosts()) {
+    // If same-site ProactivelySwapBrowsingInstance or main-frame RenderDocument
+    // is enabled, the RFH should change.
+    EXPECT_NE(google_rfh, main_test_rfh());
+    google_rfh = main_test_rfh();
+  } else {
+    EXPECT_EQ(google_rfh, main_test_rfh());
+  }
+  if (CanSameSiteMainFrameNavigationsChangeSiteInstances()) {
+    // When ProactivelySwapBrowsingInstance is enabled on same-site navigations,
+    // the SiteInstance will change.
+    EXPECT_NE(instance2, instance3);
+  } else {
+    EXPECT_EQ(instance2, instance3);
+  }
   EXPECT_FALSE(contents()->GetPendingMainFrame());
   EXPECT_EQ(url3, entry3->GetURL());
   EXPECT_EQ(instance3,
@@ -1124,7 +1197,11 @@
   auto back_navigation1 =
       NavigationSimulator::CreateHistoryNavigation(-1, contents());
   back_navigation1->ReadyToCommit();
-  EXPECT_FALSE(contents()->CrossProcessNavigationPending());
+  if (CanSameSiteMainFrameNavigationsChangeSiteInstances()) {
+    EXPECT_TRUE(contents()->CrossProcessNavigationPending());
+  } else {
+    EXPECT_FALSE(contents()->CrossProcessNavigationPending());
+  }
   EXPECT_EQ(entry2, controller().GetPendingEntry());
 
   // Before that commits, go back again.
@@ -1200,6 +1277,16 @@
   // First, make a non-user initiated same-site navigation.
   const GURL kSameSiteUrl("http://foo/1");
   TestRenderFrameHost* orig_rfh = main_test_rfh();
+  // When ProactivelySwapBrowsingInstance or RenderDocument is enabled on
+  // same-site main frame navigations, the same-site navigation below will
+  // create a speculative RFH that will be overwritten when the cross-site
+  // navigation starts, finishing the same-site navigation, so the scenario in
+  // this test cannot be tested. We should disable same-site proactive
+  // BrowsingInstance for |orig_rfh| before continuing.
+  // Note: this will not disable RenderDocument.
+  // TODO(crbug.com/936696): Skip this test when main-frame RenderDocument is
+  // enabled.
+  DisableProactiveBrowsingInstanceSwapFor(orig_rfh);
   auto same_site_navigation = NavigationSimulator::CreateRendererInitiated(
       kSameSiteUrl, main_test_rfh());
   same_site_navigation->SetHasUserGesture(false);
@@ -1365,7 +1452,13 @@
   // Now, navigate to another page on the same site.
   const GURL url2("http://www.google.com/search?q=kittens");
   NavigationSimulator::NavigateAndCommitFromBrowser(contents(), url2);
-  EXPECT_EQ(orig_rfh, main_test_rfh());
+  if (CanSameSiteMainFrameNavigationsChangeRenderFrameHosts()) {
+    // If ProactivelySwapBrowsingInstance is enabled on same-site navigations,
+    // the same-site navigation above will use a new RFH.
+    EXPECT_NE(orig_rfh, main_test_rfh());
+  } else {
+    EXPECT_EQ(orig_rfh, main_test_rfh());
+  }
 
   // Sanity-check: Confirm we're not starting out in fullscreen mode.
   EXPECT_FALSE(contents()->IsFullscreen());
@@ -1376,8 +1469,8 @@
     main_test_rfh()->frame_tree_node()->UpdateUserActivationState(
         blink::mojom::UserActivationUpdateType::kNotifyActivation,
         blink::mojom::UserActivationNotificationType::kTest);
-    orig_rfh->EnterFullscreen(blink::mojom::FullscreenOptions::New(),
-                              base::BindOnce(&ExpectTrue));
+    main_test_rfh()->EnterFullscreen(blink::mojom::FullscreenOptions::New(),
+                                     base::BindOnce(&ExpectTrue));
     EXPECT_TRUE(contents()->IsFullscreen());
     EXPECT_TRUE(fake_delegate.IsFullscreenForTabOrPending(contents()));
 
@@ -1872,16 +1965,38 @@
   navigation2->Start();
   EXPECT_EQ(1u, instance->GetRelatedActiveContentsCount());
   navigation2->Commit();
-  EXPECT_EQ(1u, instance->GetRelatedActiveContentsCount());
+  if (CanSameSiteMainFrameNavigationsChangeSiteInstances()) {
+    // When ProactivelySwapBrowsingInstance turned on for same-site navigations,
+    // the BrowsingInstance will change on same-site navigations.
+    EXPECT_NE(instance, contents->GetSiteInstance());
+    // Check the previous instance's count.
+    EXPECT_EQ(0u, instance->GetRelatedActiveContentsCount());
+    // Update the current instance.
+    instance = contents->GetSiteInstance();
+    EXPECT_EQ(1u, instance->GetRelatedActiveContentsCount());
+  }
 
   // Navigate to a URL in a different site in the same BrowsingInstance.
   const GURL kUrl2("http://b.com");
-  auto navigation3 =
-      NavigationSimulator::CreateRendererInitiated(kUrl2, main_test_rfh());
+  auto navigation3 = NavigationSimulator::CreateRendererInitiated(
+      kUrl2, contents->GetMainFrame());
   navigation3->ReadyToCommit();
-  EXPECT_FALSE(contents->CrossProcessNavigationPending());
   EXPECT_EQ(1u, instance->GetRelatedActiveContentsCount());
+  if (AreAllSitesIsolatedForTesting() ||
+      CanCrossSiteNavigationsProactivelySwapBrowsingInstances()) {
+    EXPECT_TRUE(contents->CrossProcessNavigationPending());
+  } else {
+    EXPECT_FALSE(contents->CrossProcessNavigationPending());
+  }
   navigation3->Commit();
+  if (CanCrossSiteNavigationsProactivelySwapBrowsingInstances()) {
+    // When ProactivelySwapBrowsingInstance turned on, the BrowsingInstance will
+    // change on cross-site navigations.
+    EXPECT_NE(instance, contents->GetSiteInstance());
+    EXPECT_EQ(0u, instance->GetRelatedActiveContentsCount());
+    // Update the current instance.
+    instance = contents->GetSiteInstance();
+  }
   EXPECT_EQ(1u, instance->GetRelatedActiveContentsCount());
 
   // Navigate to a URL in a different site and different BrowsingInstance, by
@@ -2337,7 +2452,15 @@
       NavigationSimulator::CreateRendererInitiated(kUrl2, main_test_rfh());
   navigation->SetHasUserGesture(false);
   navigation->Commit();
-  EXPECT_EQ(1u, dialog_manager.reset_count());
+  if (CanSameSiteMainFrameNavigationsChangeRenderFrameHosts()) {
+    // If we changed RenderFrameHost on a renderer-initiated navigation above,
+    // we would trigger RenderFrameHostManager::UnloadOldFrame, similar to the
+    // first (user/browser-initiated) navigation, which will trigger dialog
+    // cancellations and increment the reset_count to 2.
+    EXPECT_EQ(2u, dialog_manager.reset_count());
+  } else {
+    EXPECT_EQ(1u, dialog_manager.reset_count());
+  }
 
   contents()->SetJavaScriptDialogManagerForTesting(nullptr);
 }
diff --git a/content/common/media/media_player_delegate_messages.h b/content/common/media/media_player_delegate_messages.h
index c8091a5..fec05a9 100644
--- a/content/common/media/media_player_delegate_messages.h
+++ b/content/common/media/media_player_delegate_messages.h
@@ -74,6 +74,10 @@
 IPC_MESSAGE_ROUTED1(MediaPlayerDelegateMsg_ExitPictureInPicture,
                     int /* delegate_id, distinguishes instances */)
 
+IPC_MESSAGE_ROUTED2(MediaPlayerDelegateMsg_SetAudioSinkId,
+                    int /* delegate_id, distinguishes instances */,
+                    std::string /* sink_id */)
+
 IPC_MESSAGE_ROUTED2(MediaPlayerDelegateMsg_NotifyPowerExperimentState,
                     int /* delegate_id, distinguishes instances */,
                     bool /* is experiment starting (true) or stopping? */)
@@ -120,6 +124,10 @@
     int /* delegate_id, distinguishes instances */,
     bool /* picture-in-picture availability */)
 
+IPC_MESSAGE_ROUTED2(MediaPlayerDelegateHostMsg_OnAudioOutputSinkChanged,
+                    int /* delegate_id, distinguishes instances */,
+                    std::string /* hashed_device_id */)
+
 IPC_MESSAGE_ROUTED1(MediaPlayerDelegateHostMsg_OnBufferUnderflow,
                     int /* delegate_id, distinguishes instances */)
 
diff --git a/content/renderer/accessibility/render_accessibility_impl.cc b/content/renderer/accessibility/render_accessibility_impl.cc
index e1d57b2..cd0c0bd 100644
--- a/content/renderer/accessibility/render_accessibility_impl.cc
+++ b/content/renderer/accessibility/render_accessibility_impl.cc
@@ -222,6 +222,11 @@
   settings->SetAriaModalPrunesAXTree(true);
 #endif
 
+  if (render_frame_->IsMainFrame())
+    event_schedule_mode_ = EventScheduleMode::kDeferEvents;
+  else
+    event_schedule_mode_ = EventScheduleMode::kProcessEventsImmediately;
+
   const WebDocument& document = GetMainDocument();
   if (!document.IsNull()) {
     ax_context_ = std::make_unique<WebAXContext>(document);
diff --git a/content/renderer/accessibility/render_accessibility_impl.h b/content/renderer/accessibility/render_accessibility_impl.h
index c7d3c66..1105b591 100644
--- a/content/renderer/accessibility/render_accessibility_impl.h
+++ b/content/renderer/accessibility/render_accessibility_impl.h
@@ -297,8 +297,8 @@
   // (only when debugging flags are enabled, never under normal circumstances).
   bool has_injected_stylesheet_ = false;
 
-  // We defer events to improve performance durung the initial page load.
-  EventScheduleMode event_schedule_mode_ = EventScheduleMode::kDeferEvents;
+  // We defer events to improve performance during the initial page load.
+  EventScheduleMode event_schedule_mode_;
 
   // Whether we should highlight annotation results visually on the page
   // for debugging.
diff --git a/content/renderer/media/renderer_webmediaplayer_delegate.cc b/content/renderer/media/renderer_webmediaplayer_delegate.cc
index dac5291..e4a67d4 100644
--- a/content/renderer/media/renderer_webmediaplayer_delegate.cc
+++ b/content/renderer/media/renderer_webmediaplayer_delegate.cc
@@ -222,11 +222,17 @@
       routing_id(), delegate_id, available));
 }
 
+void RendererWebMediaPlayerDelegate::DidAudioOutputSinkChange(
+    int delegate_id,
+    const std::string& hashed_device_id) {
+  Send(new MediaPlayerDelegateHostMsg_OnAudioOutputSinkChanged(
+      routing_id(), delegate_id, hashed_device_id));
+}
+
 void RendererWebMediaPlayerDelegate::DidBufferUnderflow(int player_id) {
   Send(new MediaPlayerDelegateHostMsg_OnBufferUnderflow(routing_id(),
                                                         player_id));
 }
-
 void RendererWebMediaPlayerDelegate::WasHidden() {
   RecordAction(base::UserMetricsAction("Media.Hidden"));
 
@@ -268,6 +274,8 @@
                         OnMediaDelegateEnterPictureInPicture)
     IPC_MESSAGE_HANDLER(MediaPlayerDelegateMsg_ExitPictureInPicture,
                         OnMediaDelegateExitPictureInPicture)
+    IPC_MESSAGE_HANDLER(MediaPlayerDelegateMsg_SetAudioSinkId,
+                        OnMediaDelegateSetAudioSink)
     IPC_MESSAGE_HANDLER(MediaPlayerDelegateMsg_NotifyPowerExperimentState,
                         OnMediaDelegatePowerExperimentState)
     IPC_MESSAGE_UNHANDLED(return false)
@@ -397,6 +405,14 @@
     observer->OnExitPictureInPicture();
 }
 
+void RendererWebMediaPlayerDelegate::OnMediaDelegateSetAudioSink(
+    int player_id,
+    std::string sink_id) {
+  Observer* observer = id_map_.Lookup(player_id);
+  if (observer)
+    observer->OnSetAudioSink(sink_id);
+}
+
 void RendererWebMediaPlayerDelegate::OnMediaDelegatePowerExperimentState(
     int player_id,
     bool state) {
diff --git a/content/renderer/media/renderer_webmediaplayer_delegate.h b/content/renderer/media/renderer_webmediaplayer_delegate.h
index 251e7f2..7f4b6b71 100644
--- a/content/renderer/media/renderer_webmediaplayer_delegate.h
+++ b/content/renderer/media/renderer_webmediaplayer_delegate.h
@@ -72,6 +72,8 @@
       const media_session::MediaPosition& position) override;
   void DidPictureInPictureAvailabilityChange(int delegate_id,
                                              bool available) override;
+  void DidAudioOutputSinkChange(int delegate_id,
+                                const std::string& hashed_device_id) override;
   void DidBufferUnderflow(int player_id) override;
 
   // content::RenderFrameObserver overrides.
@@ -105,6 +107,7 @@
   void OnMediaDelegateBecamePersistentVideo(int player_id, bool value);
   void OnMediaDelegateEnterPictureInPicture(int player_id);
   void OnMediaDelegateExitPictureInPicture(int player_id);
+  void OnMediaDelegateSetAudioSink(int player_id, std::string sink_id);
   void OnMediaDelegatePowerExperimentState(int player_id, bool state);
 
   // Schedules UpdateTask() to run soon.
diff --git a/content/renderer/media/renderer_webmediaplayer_delegate_browsertest.cc b/content/renderer/media/renderer_webmediaplayer_delegate_browsertest.cc
index 81488cfa..74268bc 100644
--- a/content/renderer/media/renderer_webmediaplayer_delegate_browsertest.cc
+++ b/content/renderer/media/renderer_webmediaplayer_delegate_browsertest.cc
@@ -52,6 +52,7 @@
   MOCK_METHOD1(OnVolumeMultiplierUpdate, void(double));
   MOCK_METHOD1(OnBecamePersistentVideo, void(bool));
   MOCK_METHOD0(OnPictureInPictureModeEnded, void());
+  MOCK_METHOD1(OnSetAudioSink, void(const std::string&));
 };
 
 class RendererWebMediaPlayerDelegateTest : public content::RenderViewTest {
diff --git a/content/renderer/render_thread_impl.cc b/content/renderer/render_thread_impl.cc
index c0200aa..5530052 100644
--- a/content/renderer/render_thread_impl.cc
+++ b/content/renderer/render_thread_impl.cc
@@ -531,13 +531,14 @@
     const InProcessChildThreadParams& params,
     int32_t client_id,
     std::unique_ptr<blink::scheduler::WebThreadScheduler> scheduler)
-    : ChildThreadImpl(base::DoNothing(),
-                      Options::Builder()
-                          .InBrowserProcess(params)
-                          .ConnectToBrowser(true)
-                          .IPCTaskRunner(scheduler->IPCTaskRunner())
-                          .ExposesInterfacesToBrowser()
-                          .Build()),
+    : ChildThreadImpl(
+          base::DoNothing(),
+          Options::Builder()
+              .InBrowserProcess(params)
+              .ConnectToBrowser(true)
+              .IPCTaskRunner(scheduler->DeprecatedDefaultTaskRunner())
+              .ExposesInterfacesToBrowser()
+              .Build()),
       main_thread_scheduler_(std::move(scheduler)),
       categorized_worker_pool_(new CategorizedWorkerPool()),
       client_id_(client_id) {
@@ -561,12 +562,13 @@
 RenderThreadImpl::RenderThreadImpl(
     base::RepeatingClosure quit_closure,
     std::unique_ptr<blink::scheduler::WebThreadScheduler> scheduler)
-    : ChildThreadImpl(std::move(quit_closure),
-                      Options::Builder()
-                          .ConnectToBrowser(true)
-                          .IPCTaskRunner(scheduler->IPCTaskRunner())
-                          .ExposesInterfacesToBrowser()
-                          .Build()),
+    : ChildThreadImpl(
+          std::move(quit_closure),
+          Options::Builder()
+              .ConnectToBrowser(true)
+              .IPCTaskRunner(scheduler->DeprecatedDefaultTaskRunner())
+              .ExposesInterfacesToBrowser()
+              .Build()),
       main_thread_scheduler_(std::move(scheduler)),
       categorized_worker_pool_(new CategorizedWorkerPool()),
       client_id_(GetClientIdFromCommandLine()) {
@@ -2181,8 +2183,9 @@
 void RenderThreadImpl::OnRendererInterfaceReceiver(
     mojo::PendingAssociatedReceiver<mojom::Renderer> receiver) {
   DCHECK(!renderer_receiver_.is_bound());
-  renderer_receiver_.Bind(std::move(receiver),
-                          GetWebMainThreadScheduler()->IPCTaskRunner());
+  renderer_receiver_.Bind(
+      std::move(receiver),
+      GetWebMainThreadScheduler()->DeprecatedDefaultTaskRunner());
 }
 
 bool RenderThreadImpl::NeedsToRecordFirstActivePaint(
diff --git a/content/renderer/render_view_browsertest.cc b/content/renderer/render_view_browsertest.cc
index e1c6a8b6..8505c8c 100644
--- a/content/renderer/render_view_browsertest.cc
+++ b/content/renderer/render_view_browsertest.cc
@@ -215,7 +215,6 @@
   // public blink API...
   result.name = frame->AssignedName().Utf8();
   result.unique_name = test_render_frame->unique_name();
-  result.frame_policy.sandbox_flags = frame->EffectiveSandboxFlagsForTesting();
   // result.should_enforce_strict_mixed_content_checking is calculated in the
   // browser...
   result.origin = frame->GetSecurityOrigin();
diff --git a/content/test/gpu/gpu_tests/test_expectations/webgl_conformance_expectations.txt b/content/test/gpu/gpu_tests/test_expectations/webgl_conformance_expectations.txt
index 7cc5531..e598907 100644
--- a/content/test/gpu/gpu_tests/test_expectations/webgl_conformance_expectations.txt
+++ b/content/test/gpu/gpu_tests/test_expectations/webgl_conformance_expectations.txt
@@ -151,7 +151,7 @@
 # Vulkan / Passthrough command decoder
 crbug.com/angleproject/3930 [ vulkan passthrough ] conformance/textures/misc/copy-tex-image-and-sub-image-2d.html [ Failure ]
 crbug.com/angleproject/2914 [ vulkan passthrough ] conformance/textures/misc/texture-copying-feedback-loops.html [ Failure ]
-crbug.com/angleproject/4684 [ vulkan passthrough ] conformance/textures/misc/texture-mips.html [ Failure ]
+crbug.com/angleproject/4931 [ win nvidia vulkan passthrough ] conformance/textures/misc/texture-mips.html [ Failure ]
 
 # Win / Intel / Vulkan / Passthrough command decoder
 crbug.com/angleproject/4922 [ win intel vulkan passthrough ] conformance/context/context-attributes-alpha-depth-stencil-antialias.html [ RetryOnFailure ]
@@ -743,10 +743,6 @@
 
 # All platforms, Vulkan backend
 crbug.com/1099961 [ swiftshader passthrough ] conformance/textures/misc/copy-tex-image-and-sub-image-2d.html [ Failure ]
-crbug.com/1099961 [ swiftshader passthrough ] conformance/textures/misc/texture-mips.html [ Failure ]
-crbug.com/1099961 [ swiftshader passthrough ] conformance/textures/misc/texture-size-cube-maps.html [ Failure ]
-crbug.com/1099961 [ swiftshader passthrough ] conformance/textures/misc/texture-size.html [ Failure ]
-crbug.com/1099961 [ swiftshader passthrough ] conformance/textures/misc/texture-sub-image-cube-maps.html [ Failure ]
 
 # Mac. All backends.
 crbug.com/1099960 [ mac swiftshader passthrough ] conformance/context/context-no-alpha-fbo-with-alpha.html [ Failure ]
diff --git a/extensions/browser/extension_function_histogram_value.h b/extensions/browser/extension_function_histogram_value.h
index 82f0b9e..57778a8c 100644
--- a/extensions/browser/extension_function_histogram_value.h
+++ b/extensions/browser/extension_function_histogram_value.h
@@ -1556,6 +1556,7 @@
   AUTOTESTPRIVATE_STOPTHROUGHPUTTRACKERDATACOLLECTION = 1493,
   INPUTMETHODPRIVATE_GETAUTOCORRECTRANGE = 1494,
   FILEMANAGERPRIVATEINTERNAL_SHARESHEETHASTARGETS = 1495,
+  FILEMANAGERPRIVATEINTERNAL_INVOKESHARESHEET = 1496,
   // Last entry: Add new entries above, then run:
   // python tools/metrics/histograms/update_extension_histograms.py
   ENUM_BOUNDARY
diff --git a/extensions/common/BUILD.gn b/extensions/common/BUILD.gn
index 415e8c2..01a6328 100644
--- a/extensions/common/BUILD.gn
+++ b/extensions/common/BUILD.gn
@@ -334,6 +334,7 @@
     deps = [
       "//base",
       "//build:branding_buildflags",
+      "//build:lacros_buildflags",
       "//components/crx_file",
       "//components/nacl/common:buildflags",
       "//components/url_formatter",
diff --git a/extensions/common/api/_permission_features.json b/extensions/common/api/_permission_features.json
index 83e216a2..6991d9e7 100644
--- a/extensions/common/api/_permission_features.json
+++ b/extensions/common/api/_permission_features.json
@@ -498,7 +498,11 @@
       "61FF4757F9420B62B19BA5C96084649339DB31F5",  // http://crbug.com/731941
       "6FB7E1B6C0247B687AC14772E87A117F5F5E4497",  // http://crbug.com/731941
       "9834387FDA1F66A1B5CA06CB442137B556F12F2A",  // http://crbug.com/772346
-      "A9A9FC0228ADF541F0334F22BEFB8F9C245B21D7"   // http://crbug.com/839189
+      "A9A9FC0228ADF541F0334F22BEFB8F9C245B21D7",  // http://crbug.com/839189
+      "F2BCE012B9B7E2D57902B5A4F954EB01A7E548FD",  // http://crbug.com/1105137
+      "D467F51D3846ED6D137F9FD403AE11CE416CD995",  // http://crbug.com/1105137
+      "3823525AD445E0025E449F964C20922996B0F97F",  // http://crbug.com/1105137
+      "827B5D482FADCE120F4694AD0FA0680E3717C6EC"   // http://crbug.com/1105137
     ]
   }],
   "networkingPrivate": {
diff --git a/extensions/common/features/feature.cc b/extensions/common/features/feature.cc
index 36689fd..58996f9 100644
--- a/extensions/common/features/feature.cc
+++ b/extensions/common/features/feature.cc
@@ -11,6 +11,7 @@
 #include "base/strings/string_util.h"
 #include "base/strings/stringprintf.h"
 #include "build/build_config.h"
+#include "build/lacros_buildflags.h"
 #include "extensions/common/extension.h"
 #include "extensions/common/manifest.h"
 
@@ -18,7 +19,12 @@
 
 // static
 Feature::Platform Feature::GetCurrentPlatform() {
-#if defined(OS_CHROMEOS)
+// TODO(https://crbug.com/1052397): For readability, this should become
+// defined(OS_CHROMEOS) && BUILDFLAG(IS_LACROS). The second conditional should
+// be defined(OS_CHROMEOS) && BUILDFLAG(IS_ASH).
+#if BUILDFLAG(IS_LACROS)
+  return LACROS_PLATFORM;
+#elif defined(OS_CHROMEOS) && !BUILDFLAG(IS_LACROS)
   return CHROMEOS_PLATFORM;
 #elif defined(OS_LINUX)
   return LINUX_PLATFORM;
diff --git a/extensions/common/features/feature.h b/extensions/common/features/feature.h
index 2bdc3e2d..ce5d263 100644
--- a/extensions/common/features/feature.h
+++ b/extensions/common/features/feature.h
@@ -46,6 +46,7 @@
   enum Platform {
     UNSPECIFIED_PLATFORM,
     CHROMEOS_PLATFORM,
+    LACROS_PLATFORM,
     LINUX_PLATFORM,
     MACOSX_PLATFORM,
     WIN_PLATFORM
diff --git a/gpu/command_buffer/common/sync_token.cc b/gpu/command_buffer/common/sync_token.cc
index 2ed75f2..0d6e9e6 100644
--- a/gpu/command_buffer/common/sync_token.cc
+++ b/gpu/command_buffer/common/sync_token.cc
@@ -4,6 +4,8 @@
 
 #include "gpu/command_buffer/common/sync_token.h"
 
+#include <sstream>
+
 namespace gpu {
 
 SyncToken::SyncToken()
@@ -21,4 +23,19 @@
 
 SyncToken::SyncToken(const SyncToken& other) = default;
 
+std::string SyncToken::ToDebugString() const {
+  // At the level of the generic command buffer code, the command buffer ID is
+  // an arbitrary 64-bit number. For the debug output, print its upper and lower
+  // 32bit words separately. This ensures more readable output for IDs allocated
+  // by gpu/ipc code which uses these words for channel and route IDs, but it's
+  // still a lossless representation if the IDs don't match this convention.
+  uint64_t id = command_buffer_id().GetUnsafeValue();
+  uint32_t channel_or_high = 0xffffffff & id;
+  uint32_t route_or_low = id >> 32;
+  std::ostringstream stream;
+  stream << static_cast<int>(namespace_id()) << ":" << channel_or_high << ":"
+         << route_or_low << ":" << release_count();
+  return stream.str();
+}
+
 }  // namespace gpu
diff --git a/gpu/command_buffer/common/sync_token.h b/gpu/command_buffer/common/sync_token.h
index a8aa0c6..52d696ca 100644
--- a/gpu/command_buffer/common/sync_token.h
+++ b/gpu/command_buffer/common/sync_token.h
@@ -83,6 +83,8 @@
 
   bool operator!=(const SyncToken& other) const { return !(*this == other); }
 
+  std::string ToDebugString() const;
+
  private:
   bool verified_flush_;
   CommandBufferNamespace namespace_id_;
diff --git a/gpu/command_buffer/service/shared_image_backing_d3d.cc b/gpu/command_buffer/service/shared_image_backing_d3d.cc
index 194259c..05748005 100644
--- a/gpu/command_buffer/service/shared_image_backing_d3d.cc
+++ b/gpu/command_buffer/service/shared_image_backing_d3d.cc
@@ -6,6 +6,7 @@
 
 #include "base/trace_event/memory_dump_manager.h"
 #include "components/viz/common/resources/resource_format_utils.h"
+#include "components/viz/common/resources/resource_sizes.h"
 #include "gpu/command_buffer/common/shared_image_trace_utils.h"
 #include "gpu/command_buffer/service/shared_image_representation_d3d.h"
 #include "gpu/command_buffer/service/shared_image_representation_skia_gl.h"
@@ -45,7 +46,7 @@
     uint32_t usage,
     Microsoft::WRL::ComPtr<IDXGISwapChain1> swap_chain,
     scoped_refptr<gles2::TexturePassthrough> texture,
-    scoped_refptr<gl::GLImageD3D> image,
+    scoped_refptr<gl::GLImage> image,
     size_t buffer_index,
     Microsoft::WRL::ComPtr<ID3D11Texture2D> d3d11_texture,
     base::win::ScopedHandle shared_handle,
@@ -66,7 +67,6 @@
       d3d11_texture_(std::move(d3d11_texture)),
       shared_handle_(std::move(shared_handle)),
       dxgi_keyed_mutex_(std::move(dxgi_keyed_mutex)) {
-  DCHECK(d3d11_texture_);
   DCHECK(texture_);
 }
 
@@ -171,6 +171,10 @@
   return shared_handle_.Get();
 }
 
+gl::GLImage* SharedImageBackingD3D::GetGLImage() const {
+  return image_.get();
+}
+
 bool SharedImageBackingD3D::PresentSwapChain() {
   TRACE_EVENT0("gpu", "SharedImageBackingD3D::PresentSwapChain");
   if (buffer_index_ != 0) {
@@ -195,7 +199,7 @@
 
   api->glBindTextureFn(GL_TEXTURE_2D, texture_->service_id());
   if (!image_->BindTexImage(GL_TEXTURE_2D)) {
-    DLOG(ERROR) << "GLImageD3D::BindTexImage failed";
+    DLOG(ERROR) << "GLImage::BindTexImage failed";
     return false;
   }
 
@@ -223,4 +227,12 @@
       manager, this, tracker);
 }
 
+std::unique_ptr<SharedImageRepresentationOverlay>
+SharedImageBackingD3D::ProduceOverlay(SharedImageManager* manager,
+                                      MemoryTypeTracker* tracker) {
+  TRACE_EVENT0("gpu", "SharedImageBackingD3D::ProduceOverlay");
+  return std::make_unique<SharedImageRepresentationOverlayD3D>(manager, this,
+                                                               tracker);
+}
+
 }  // namespace gpu
diff --git a/gpu/command_buffer/service/shared_image_backing_d3d.h b/gpu/command_buffer/service/shared_image_backing_d3d.h
index a808c3c..50e06454 100644
--- a/gpu/command_buffer/service/shared_image_backing_d3d.h
+++ b/gpu/command_buffer/service/shared_image_backing_d3d.h
@@ -32,7 +32,8 @@
 // Implementation of SharedImageBacking that holds buffer (front buffer/back
 // buffer of swap chain) texture (as gles2::Texture/gles2::TexturePassthrough)
 // and a reference to created swap chain.
-class SharedImageBackingD3D : public ClearTrackingSharedImageBacking {
+class GPU_GLES2_EXPORT SharedImageBackingD3D
+    : public ClearTrackingSharedImageBacking {
  public:
   SharedImageBackingD3D(
       const Mailbox& mailbox,
@@ -44,7 +45,7 @@
       uint32_t usage,
       Microsoft::WRL::ComPtr<IDXGISwapChain1> swap_chain,
       scoped_refptr<gles2::TexturePassthrough> texture,
-      scoped_refptr<gl::GLImageD3D> image,
+      scoped_refptr<gl::GLImage> image,
       size_t buffer_index,
       Microsoft::WRL::ComPtr<ID3D11Texture2D> d3d11_texture,
       base::win::ScopedHandle shared_handle,
@@ -73,6 +74,7 @@
   void EndAccessD3D11();
 
   HANDLE GetSharedHandle() const;
+  gl::GLImage* GetGLImage() const;
 
   bool PresentSwapChain() override;
 
@@ -81,6 +83,10 @@
   ProduceGLTexturePassthrough(SharedImageManager* manager,
                               MemoryTypeTracker* tracker) override;
 
+  std::unique_ptr<SharedImageRepresentationOverlay> ProduceOverlay(
+      SharedImageManager* manager,
+      MemoryTypeTracker* tracker) override;
+
   std::unique_ptr<SharedImageRepresentationSkia> ProduceSkia(
       SharedImageManager* manager,
       MemoryTypeTracker* tracker,
@@ -89,8 +95,10 @@
  private:
   Microsoft::WRL::ComPtr<IDXGISwapChain1> swap_chain_;
   scoped_refptr<gles2::TexturePassthrough> texture_;
-  scoped_refptr<gl::GLImageD3D> image_;
+  scoped_refptr<gl::GLImage> image_;
   const size_t buffer_index_;
+
+  // Texture could be nullptr if an empty backing is needed for testing.
   Microsoft::WRL::ComPtr<ID3D11Texture2D> d3d11_texture_;
 
   // If d3d11_texture_ has a keyed mutex, it will be stored in
diff --git a/gpu/command_buffer/service/shared_image_representation_d3d.cc b/gpu/command_buffer/service/shared_image_representation_d3d.cc
index c400fff..06b3ae18 100644
--- a/gpu/command_buffer/service/shared_image_representation_d3d.cc
+++ b/gpu/command_buffer/service/shared_image_representation_d3d.cc
@@ -133,4 +133,22 @@
 }
 #endif  // BUILDFLAG(USE_DAWN)
 
+SharedImageRepresentationOverlayD3D::SharedImageRepresentationOverlayD3D(
+    SharedImageManager* manager,
+    SharedImageBacking* backing,
+    MemoryTypeTracker* tracker)
+    : SharedImageRepresentationOverlay(manager, backing, tracker) {}
+
+bool SharedImageRepresentationOverlayD3D::BeginReadAccess() {
+  // Note: only the DX11 video decoder uses this overlay and does not need to
+  // synchronize read access from different devices.
+  return true;
+}
+
+void SharedImageRepresentationOverlayD3D::EndReadAccess() {}
+
+gl::GLImage* SharedImageRepresentationOverlayD3D::GetGLImage() {
+  return static_cast<SharedImageBackingD3D*>(backing())->GetGLImage();
+}
+
 }  // namespace gpu
diff --git a/gpu/command_buffer/service/shared_image_representation_d3d.h b/gpu/command_buffer/service/shared_image_representation_d3d.h
index 72e41f1..c73d4c7f 100644
--- a/gpu/command_buffer/service/shared_image_representation_d3d.h
+++ b/gpu/command_buffer/service/shared_image_representation_d3d.h
@@ -20,6 +20,8 @@
 
 namespace gpu {
 
+class SharedImageBackingD3D;
+
 // Representation of a SharedImageBackingD3D as a GL TexturePassthrough.
 class SharedImageRepresentationGLTexturePassthroughD3D
     : public SharedImageRepresentationGLTexturePassthrough {
@@ -65,5 +67,21 @@
 };
 #endif  // BUILDFLAG(USE_DAWN)
 
+// Representation of a SharedImageBackingD3D as an overlay.
+class SharedImageRepresentationOverlayD3D
+    : public SharedImageRepresentationOverlay {
+ public:
+  SharedImageRepresentationOverlayD3D(SharedImageManager* manager,
+                                      SharedImageBacking* backing,
+                                      MemoryTypeTracker* tracker);
+  ~SharedImageRepresentationOverlayD3D() override = default;
+
+ private:
+  bool BeginReadAccess() override;
+  void EndReadAccess() override;
+
+  gl::GLImage* GetGLImage() override;
+};
+
 }  // namespace gpu
 #endif  // GPU_COMMAND_BUFFER_SERVICE_SHARED_IMAGE_REPRESENTATION_D3D_H_
diff --git a/infra/config/generated/luci-notify.cfg b/infra/config/generated/luci-notify.cfg
index 2042535..196a6c9 100644
--- a/infra/config/generated/luci-notify.cfg
+++ b/infra/config/generated/luci-notify.cfg
@@ -102,6 +102,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -120,6 +121,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -146,6 +148,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -164,6 +167,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -208,6 +212,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -226,6 +231,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -251,6 +257,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -313,6 +320,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -338,6 +346,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -356,6 +365,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -407,6 +417,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -432,6 +443,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -463,6 +475,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -494,6 +507,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -520,6 +534,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -546,6 +561,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -741,6 +757,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -766,6 +783,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -790,6 +808,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -814,6 +833,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -839,6 +859,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -864,6 +885,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -889,6 +911,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -914,6 +937,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -939,6 +963,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -963,6 +988,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -981,6 +1007,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -1006,6 +1033,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -1030,6 +1058,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -1055,6 +1084,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -1079,6 +1109,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -1103,6 +1134,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -1127,6 +1159,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -1151,6 +1184,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -1176,6 +1210,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -1201,6 +1236,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -1225,6 +1261,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -1249,6 +1286,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -1325,6 +1363,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -1343,6 +1382,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -1360,6 +1400,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -1378,6 +1419,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -1396,6 +1438,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -1421,6 +1464,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -1446,6 +1490,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -1463,6 +1508,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -1480,6 +1526,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -1497,6 +1544,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -1514,6 +1562,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -1531,6 +1580,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -1548,6 +1598,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -1565,6 +1616,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -1775,6 +1827,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -1793,6 +1846,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -1811,6 +1865,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -1829,6 +1884,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -1846,6 +1902,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -1876,6 +1933,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -1919,6 +1977,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -1937,6 +1996,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -1955,6 +2015,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -1973,6 +2034,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -1991,6 +2053,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -2008,6 +2071,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -2033,6 +2097,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -2050,6 +2115,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -2067,6 +2133,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -2084,6 +2151,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -2102,6 +2170,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -2120,6 +2189,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -2278,6 +2348,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -2296,6 +2367,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -2314,6 +2386,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -2332,6 +2405,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -2350,6 +2424,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -2368,6 +2443,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -2386,6 +2462,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -2404,6 +2481,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -2422,6 +2500,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -2440,6 +2519,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -2556,6 +2636,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -2587,6 +2668,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -2605,6 +2687,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -2636,6 +2719,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -2654,6 +2738,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -2686,6 +2771,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -2704,6 +2790,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -2729,6 +2816,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -2767,6 +2855,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -2785,6 +2874,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -2803,6 +2893,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -2857,6 +2948,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   notifications {
     on_occurrence: FAILURE
@@ -2882,6 +2974,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -2900,6 +2993,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -2918,6 +3012,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -2963,6 +3058,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -2981,6 +3077,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
@@ -2999,6 +3096,7 @@
     email {
       rotation_urls: "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff"
     }
+    template: "tree_closure_email_template"
   }
   builders {
     bucket: "ci"
diff --git a/infra/config/generated/luci-notify/email-templates/tree_closure_email_template.template b/infra/config/generated/luci-notify/email-templates/tree_closure_email_template.template
new file mode 100644
index 0000000..b9a4941
--- /dev/null
+++ b/infra/config/generated/luci-notify/email-templates/tree_closure_email_template.template
@@ -0,0 +1,35 @@
+[Chromium tree closure] Builder "{{ .Build.Builder | formatBuilderID }}"
+
+Builder "{{ .Build.Builder | formatBuilderID }}" failed at {{ .Build.EndTime | time }} on step(s) {{ stepNames .MatchingFailedSteps }}.
+
+<b>This failure is tree closing.</b>
+
+<table>
+  <tr>
+    <td>New status:</td>
+    <td><b>{{ .Build.Status }}</b></td>
+  </tr>
+  <tr>
+    <td>Previous status:</td>
+    <td>{{ .OldStatus }}</td>
+  </tr>
+  <tr>
+    <td>Builder:</td>
+    <td>{{ .Build.Builder | formatBuilderID }}</td>
+  </tr>
+  <tr>
+    <td>Failed steps:</td>
+    <td>{{ stepNames .MatchingFailedSteps }}</td>
+  </tr>
+  <tr>
+    <td>Created at:</td>
+    <td>{{ .Build.CreateTime | time }}</td>
+  </tr>
+  <tr>
+    <td>Finished at:</td>
+    <td>{{ .Build.EndTime | time }}</td>
+  </tr>
+</table>
+
+Build details: {{ buildUrl . }}
+<br/><br/>
diff --git a/infra/config/main.star b/infra/config/main.star
index d2801f4..7788566 100755
--- a/infra/config/main.star
+++ b/infra/config/main.star
@@ -29,6 +29,7 @@
         'luci-logdog.cfg',
         'luci-milo.cfg',
         'luci-notify.cfg',
+        'luci-notify/email-templates/*.template',
         'luci-scheduler.cfg',
         'project.cfg',
         'project.pyl',
diff --git a/infra/config/notifiers.star b/infra/config/notifiers.star
index 6bd2230..44fbf7d0 100644
--- a/infra/config/notifiers.star
+++ b/infra/config/notifiers.star
@@ -71,6 +71,10 @@
     notify_rotation_urls = [
         "https://chrome-ops-rotation-proxy.appspot.com/current/oncallator:chrome-build-sheriff",
     ],
+    template = luci.notifier_template(
+        name = 'tree_closure_email_template',
+        body = io.read_file('templates/tree_closure_email.template'),
+    ),
 )
 
 tree_closure_notifier(
diff --git a/infra/config/templates/tree_closure_email.template b/infra/config/templates/tree_closure_email.template
new file mode 100644
index 0000000..b9a4941
--- /dev/null
+++ b/infra/config/templates/tree_closure_email.template
@@ -0,0 +1,35 @@
+[Chromium tree closure] Builder "{{ .Build.Builder | formatBuilderID }}"
+
+Builder "{{ .Build.Builder | formatBuilderID }}" failed at {{ .Build.EndTime | time }} on step(s) {{ stepNames .MatchingFailedSteps }}.
+
+<b>This failure is tree closing.</b>
+
+<table>
+  <tr>
+    <td>New status:</td>
+    <td><b>{{ .Build.Status }}</b></td>
+  </tr>
+  <tr>
+    <td>Previous status:</td>
+    <td>{{ .OldStatus }}</td>
+  </tr>
+  <tr>
+    <td>Builder:</td>
+    <td>{{ .Build.Builder | formatBuilderID }}</td>
+  </tr>
+  <tr>
+    <td>Failed steps:</td>
+    <td>{{ stepNames .MatchingFailedSteps }}</td>
+  </tr>
+  <tr>
+    <td>Created at:</td>
+    <td>{{ .Build.CreateTime | time }}</td>
+  </tr>
+  <tr>
+    <td>Finished at:</td>
+    <td>{{ .Build.EndTime | time }}</td>
+  </tr>
+</table>
+
+Build details: {{ buildUrl . }}
+<br/><br/>
diff --git a/ios/chrome/browser/flags/about_flags.mm b/ios/chrome/browser/flags/about_flags.mm
index a909e3c..4f89ce7 100644
--- a/ios/chrome/browser/flags/about_flags.mm
+++ b/ios/chrome/browser/flags/about_flags.mm
@@ -680,6 +680,10 @@
     {"scroll-to-text-ios", flag_descriptions::kScrollToTextIOSName,
      flag_descriptions::kScrollToTextIOSDescription, flags_ui::kOsIos,
      FEATURE_VALUE_TYPE(web::features::kScrollToTextIOS)},
+    {"legacy-tls-interstitial",
+     flag_descriptions::kIOSLegacyTLSInterstitialsName,
+     flag_descriptions::kIOSLegacyTLSInterstitialsDescription, flags_ui::kOsIos,
+     FEATURE_VALUE_TYPE(web::features::kIOSLegacyTLSInterstitial)},
 };
 
 bool SkipConditionalFeatureEntry(const flags_ui::FeatureEntry& entry) {
diff --git a/ios/chrome/browser/flags/ios_chrome_flag_descriptions.cc b/ios/chrome/browser/flags/ios_chrome_flag_descriptions.cc
index 3b0fd2b..07af8b9 100644
--- a/ios/chrome/browser/flags/ios_chrome_flag_descriptions.cc
+++ b/ios/chrome/browser/flags/ios_chrome_flag_descriptions.cc
@@ -312,6 +312,12 @@
     "an individual promotion causes that promotion but no other promotions to "
     "occur.";
 
+const char kIOSLegacyTLSInterstitialsName[] = "Show legacy TLS interstitials";
+const char kIOSLegacyTLSInterstitialsDescription[] =
+    "When enabled, an interstitial will be shown on main-frame navigations "
+    "that use legacy TLS connections, and subresources using legacy TLS "
+    "connections will be blocked.";
+
 const char kIOSLookalikeUrlNavigationSuggestionsUIName[] =
     "Lookalike URL Navigation Suggestions UI";
 const char kIOSLookalikeUrlNavigationSuggestionsUIDescription[] =
diff --git a/ios/chrome/browser/flags/ios_chrome_flag_descriptions.h b/ios/chrome/browser/flags/ios_chrome_flag_descriptions.h
index 445fdd68..731f000 100644
--- a/ios/chrome/browser/flags/ios_chrome_flag_descriptions.h
+++ b/ios/chrome/browser/flags/ios_chrome_flag_descriptions.h
@@ -268,6 +268,11 @@
 extern const char kInProductHelpDemoModeName[];
 extern const char kInProductHelpDemoModeDescription[];
 
+// Title and description for the flag to enable interstitials on legacy TLS
+// connections.
+extern const char kIOSLegacyTLSInterstitialsName[];
+extern const char kIOSLegacyTLSInterstitialsDescription[];
+
 // Title and description for the flag to enable interstitials on lookalike
 // URL navigations.
 extern const char kIOSLookalikeUrlNavigationSuggestionsUIName[];
diff --git a/ios/chrome/browser/tabs/BUILD.gn b/ios/chrome/browser/tabs/BUILD.gn
index 5bfa4418..206cd8d 100644
--- a/ios/chrome/browser/tabs/BUILD.gn
+++ b/ios/chrome/browser/tabs/BUILD.gn
@@ -111,6 +111,7 @@
     "//ios/chrome/browser/web_state_list",
     "//ios/chrome/browser/web_state_list/web_usage_enabler",
     "//ios/components/security_interstitials",
+    "//ios/components/security_interstitials/legacy_tls",
     "//ios/components/security_interstitials/lookalikes",
     "//ios/public/provider/chrome/browser",
     "//ios/web/common:features",
diff --git a/ios/chrome/browser/tabs/tab_helper_util.mm b/ios/chrome/browser/tabs/tab_helper_util.mm
index 3c2b174..0a87708 100644
--- a/ios/chrome/browser/tabs/tab_helper_util.mm
+++ b/ios/chrome/browser/tabs/tab_helper_util.mm
@@ -78,6 +78,7 @@
 #import "ios/chrome/browser/web/tab_id_tab_helper.h"
 #import "ios/chrome/browser/web/web_state_delegate_tab_helper.h"
 #import "ios/components/security_interstitials/ios_blocking_page_tab_helper.h"
+#import "ios/components/security_interstitials/legacy_tls/legacy_tls_tab_allow_list.h"
 #import "ios/components/security_interstitials/lookalikes/lookalike_url_container.h"
 #import "ios/components/security_interstitials/lookalikes/lookalike_url_tab_allow_list.h"
 #import "ios/components/security_interstitials/lookalikes/lookalike_url_tab_helper.h"
@@ -197,6 +198,10 @@
     LookalikeUrlContainer::CreateForWebState(web_state);
   }
 
+  if (base::FeatureList::IsEnabled(web::features::kIOSLegacyTLSInterstitial)) {
+    LegacyTLSTabAllowList::CreateForWebState(web_state);
+  }
+
   // TODO(crbug.com/794115): pre-rendered WebState have lots of unnecessary
   // tab helpers for historical reasons. For the moment, AttachTabHelpers
   // allows to inhibit the creation of some of them. Once PreloadController
diff --git a/ios/chrome/browser/web/BUILD.gn b/ios/chrome/browser/web/BUILD.gn
index c7ca81d..c04df53 100644
--- a/ios/chrome/browser/web/BUILD.gn
+++ b/ios/chrome/browser/web/BUILD.gn
@@ -262,8 +262,10 @@
     "//ios/chrome/browser/ui/util",
     "//ios/chrome/browser/web:feature_flags",
     "//ios/components/security_interstitials",
+    "//ios/components/security_interstitials/legacy_tls",
     "//ios/components/security_interstitials/lookalikes",
     "//ios/components/webui:url_constants",
+    "//ios/net",
     "//ios/public/provider/chrome/browser",
     "//ios/public/provider/chrome/browser/voice",
     "//ios/web",
@@ -335,7 +337,9 @@
     "//ios/chrome/browser/web",
     "//ios/chrome/test/fakes",
     "//ios/components/security_interstitials",
+    "//ios/components/security_interstitials/legacy_tls",
     "//ios/components/security_interstitials/lookalikes",
+    "//ios/net",
     "//ios/web/common:features",
     "//ios/web/common:web_view_creation_util",
     "//ios/web/public/test",
@@ -424,6 +428,7 @@
     "forms_egtest.mm",
     "http_auth_egtest.mm",
     "js_print_egtest.mm",
+    "legacy_tls_egtest.mm",
     "lookalike_url_egtest.mm",
     "navigation_egtest.mm",
     "progress_indicator_egtest.mm",
diff --git a/ios/chrome/browser/web/chrome_web_client.h b/ios/chrome/browser/web/chrome_web_client.h
index a716184..86e5182 100644
--- a/ios/chrome/browser/web/chrome_web_client.h
+++ b/ios/chrome/browser/web/chrome_web_client.h
@@ -51,6 +51,8 @@
       bool overridable,
       int64_t navigation_id,
       const base::Callback<void(bool)>& callback) override;
+  bool IsLegacyTLSAllowedForHost(web::WebState* web_state,
+                                 const std::string& hostname) override;
   void PrepareErrorPage(web::WebState* web_state,
                         const GURL& url,
                         NSError* error,
diff --git a/ios/chrome/browser/web/chrome_web_client.mm b/ios/chrome/browser/web/chrome_web_client.mm
index 589a1fd..a45cf93 100644
--- a/ios/chrome/browser/web/chrome_web_client.mm
+++ b/ios/chrome/browser/web/chrome_web_client.mm
@@ -33,11 +33,15 @@
 #import "ios/chrome/browser/web/error_page_util.h"
 #include "ios/chrome/browser/web/features.h"
 #import "ios/components/security_interstitials/ios_blocking_page_tab_helper.h"
+#import "ios/components/security_interstitials/legacy_tls/legacy_tls_blocking_page.h"
+#import "ios/components/security_interstitials/legacy_tls/legacy_tls_controller_client.h"
+#import "ios/components/security_interstitials/legacy_tls/legacy_tls_tab_allow_list.h"
 #import "ios/components/security_interstitials/lookalikes/lookalike_url_blocking_page.h"
 #import "ios/components/security_interstitials/lookalikes/lookalike_url_container.h"
 #import "ios/components/security_interstitials/lookalikes/lookalike_url_controller_client.h"
 #import "ios/components/security_interstitials/lookalikes/lookalike_url_error.h"
 #include "ios/components/webui/web_ui_url_constants.h"
+#import "ios/net/protocol_handler_util.h"
 #include "ios/public/provider/chrome/browser/browser_url_rewriter_provider.h"
 #import "ios/public/provider/chrome/browser/chrome_browser_provider.h"
 #include "ios/public/provider/chrome/browser/chrome_browser_provider.h"
@@ -47,6 +51,7 @@
 #include "ios/web/common/user_agent.h"
 #include "ios/web/public/navigation/browser_url_rewriter.h"
 #include "ios/web/public/navigation/navigation_manager.h"
+#include "net/base/net_errors.h"
 #include "net/http/http_util.h"
 #include "services/metrics/public/cpp/ukm_source_id.h"
 #include "ui/base/l10n/l10n_util.h"
@@ -127,6 +132,23 @@
   return base::SysUTF8ToNSString(error_page_content);
 }
 
+// Returns the legacy TLS error page HTML.
+NSString* GetLegacyTLSErrorPageHTML(web::WebState* web_state,
+                                    int64_t navigation_id) {
+  // Construct the blocking page and associate it with the WebState.
+  std::unique_ptr<security_interstitials::IOSSecurityInterstitialPage> page =
+      std::make_unique<LegacyTLSBlockingPage>(
+          web_state, web_state->GetVisibleURL() /*request_url*/,
+          std::make_unique<LegacyTLSControllerClient>(
+              web_state, web_state->GetVisibleURL(),
+              GetApplicationContext()->GetApplicationLocale()));
+  std::string error_page_content = page->GetHtmlContents();
+  security_interstitials::IOSBlockingPageTabHelper::FromWebState(web_state)
+      ->AssociateBlockingPage(navigation_id, std::move(page));
+
+  return base::SysUTF8ToNSString(error_page_content);
+}
+
 // Returns a string describing the product name and version, of the
 // form "productname/version". Used as part of the user agent string.
 std::string GetMobileProduct() {
@@ -293,6 +315,12 @@
                                      std::move(null_callback));
 }
 
+bool ChromeWebClient::IsLegacyTLSAllowedForHost(web::WebState* web_state,
+                                                const std::string& hostname) {
+  return LegacyTLSTabAllowList::FromWebState(web_state)->IsDomainAllowed(
+      hostname);
+}
+
 void ChromeWebClient::PrepareErrorPage(
     web::WebState* web_state,
     const GURL& url,
@@ -335,6 +363,10 @@
     DCHECK_EQ(kLookalikeUrlErrorCode, final_underlying_error.code);
     std::move(error_html_callback)
         .Run(GetLookalikeUrlErrorPageHtml(web_state, navigation_id));
+  } else if ([final_underlying_error.domain isEqual:net::kNSErrorDomain] &&
+             final_underlying_error.code == net::ERR_SSL_OBSOLETE_VERSION) {
+    std::move(error_html_callback)
+        .Run(GetLegacyTLSErrorPageHTML(web_state, navigation_id));
   } else if (info.has_value()) {
     base::OnceCallback<void(bool)> proceed_callback;
     base::OnceCallback<void(NSString*)> blocking_page_callback =
diff --git a/ios/chrome/browser/web/chrome_web_client_unittest.mm b/ios/chrome/browser/web/chrome_web_client_unittest.mm
index b4c5ee6..e5818188 100644
--- a/ios/chrome/browser/web/chrome_web_client_unittest.mm
+++ b/ios/chrome/browser/web/chrome_web_client_unittest.mm
@@ -33,6 +33,7 @@
 #import "ios/components/security_interstitials/ios_blocking_page_tab_helper.h"
 #import "ios/components/security_interstitials/lookalikes/lookalike_url_container.h"
 #import "ios/components/security_interstitials/lookalikes/lookalike_url_error.h"
+#import "ios/net/protocol_handler_util.h"
 #include "ios/web/common/features.h"
 #import "ios/web/common/web_view_creation_util.h"
 #import "ios/web/public/test/error_test_util.h"
@@ -40,6 +41,7 @@
 #import "ios/web/public/test/fakes/test_web_state.h"
 #import "ios/web/public/test/js_test_util.h"
 #include "ios/web/public/test/scoped_testing_web_client.h"
+#include "net/base/net_errors.h"
 #include "net/http/http_status_code.h"
 #include "net/ssl/ssl_info.h"
 #include "net/test/cert_test_util.h"
@@ -483,6 +485,41 @@
       << base::SysNSStringToUTF8(page);
 }
 
+// Tests PrepareErrorPage for a legacy TLS error, which results in a
+// committed legacy TLS interstitial.
+TEST_F(ChromeWebClientTest, PrepareErrorPageForLegacyTLSError) {
+  web::TestWebState web_state;
+  web_state.SetBrowserState(browser_state());
+  security_interstitials::IOSBlockingPageTabHelper::CreateForWebState(
+      &web_state);
+  auto navigation_manager = std::make_unique<web::TestNavigationManager>();
+  web_state.SetNavigationManager(std::move(navigation_manager));
+
+  NSError* error = [NSError errorWithDomain:net::kNSErrorDomain
+                                       code:net::ERR_SSL_OBSOLETE_VERSION
+                                   userInfo:nil];
+  __block bool callback_called = false;
+  __block NSString* page = nil;
+  base::OnceCallback<void(NSString*)> callback =
+      base::BindOnce(^(NSString* error_html) {
+        callback_called = true;
+        page = error_html;
+      });
+
+  ChromeWebClient web_client;
+  web_client.PrepareErrorPage(&web_state, GURL(kTestUrl), error,
+                              /*is_post=*/false,
+                              /*is_off_the_record=*/false,
+                              /*info=*/base::Optional<net::SSLInfo>(),
+                              /*navigation_id=*/0, std::move(callback));
+
+  EXPECT_TRUE(callback_called);
+  NSString* error_string =
+      l10n_util::GetNSString(IDS_LEGACY_TLS_PRIMARY_PARAGRAPH);
+  EXPECT_TRUE([page containsString:error_string])
+      << base::SysNSStringToUTF8(page);
+}
+
 // Tests the default user agent for different views.
 TEST_F(ChromeWebClientTest, DefaultUserAgent) {
   if (@available(iOS 13, *)) {
diff --git a/ios/chrome/browser/web/legacy_tls_egtest.mm b/ios/chrome/browser/web/legacy_tls_egtest.mm
new file mode 100644
index 0000000..3266d0e
--- /dev/null
+++ b/ios/chrome/browser/web/legacy_tls_egtest.mm
@@ -0,0 +1,190 @@
+// 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 "base/ios/ios_util.h"
+#include "components/strings/grit/components_strings.h"
+#import "ios/chrome/test/earl_grey/chrome_earl_grey.h"
+#import "ios/chrome/test/earl_grey/chrome_test_case.h"
+#import "ios/testing/earl_grey/earl_grey_test.h"
+#include "ios/testing/embedded_test_server_handlers.h"
+#include "ios/web/common/features.h"
+#include "net/ssl/ssl_config.h"
+#include "net/ssl/ssl_server_config.h"
+#include "net/test/embedded_test_server/default_handlers.h"
+#include "ui/base/l10n/l10n_util.h"
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+@interface LegacyTLSTestCase : ChromeTestCase {
+  // A test server that connects via TLS 1.0.
+  std::unique_ptr<net::test_server::EmbeddedTestServer> _legacyTLSServer;
+  // A URL for the legacy TLS test server, which should trigger legacy TLS
+  // interstitials.
+  GURL _legacyTLSURL;
+  // A URL for the normal test server.
+  GURL _safeURL;
+  // Text that is found on the legacy TLS interstitial.
+  std::string _interstitialContent;
+  // Text that is found on |_legacyTLSURL| if the user clicks through.
+  std::string _unsafeContent;
+  // Text that is found on |_safeURL|.
+  std::string _safeContent;
+}
+
+@end
+
+@implementation LegacyTLSTestCase
+
+- (AppLaunchConfiguration)appConfigurationForTestCase {
+  AppLaunchConfiguration config;
+  config.features_enabled.push_back(web::features::kSSLCommittedInterstitials);
+  config.features_enabled.push_back(web::features::kIOSLegacyTLSInterstitial);
+  config.relaunch_policy = NoForceRelaunchAndResetState;
+  return config;
+}
+
+- (void)setUp {
+  [super setUp];
+
+  // Setup server that will negotiate TLS 1.0.
+  _legacyTLSServer = std::make_unique<net::test_server::EmbeddedTestServer>(
+      net::test_server::EmbeddedTestServer::TYPE_HTTPS);
+  net::SSLServerConfig ssl_config;
+  ssl_config.version_min = net::SSL_PROTOCOL_VERSION_TLS1;
+  ssl_config.version_max = net::SSL_PROTOCOL_VERSION_TLS1;
+  _legacyTLSServer->SetSSLConfig(net::EmbeddedTestServer::CERT_OK, ssl_config);
+  RegisterDefaultHandlers(_legacyTLSServer.get());
+  GREYAssertTrue(_legacyTLSServer->Start(),
+                 @"Legacy TLS test server failed to start.");
+
+  _legacyTLSURL = _legacyTLSServer->GetURL("/");
+  _interstitialContent =
+      l10n_util::GetStringUTF8(IDS_LEGACY_TLS_PRIMARY_PARAGRAPH);
+  // The legacy TLS server will cause self-signed cert warnings after we click
+  // through the legacy TLS interstitial, so pull the heading string for that
+  // to match on.
+  _unsafeContent = l10n_util::GetStringUTF8(IDS_SSL_V2_HEADING);
+
+  RegisterDefaultHandlers(self.testServer);
+  GREYAssertTrue(self.testServer->Start(), @"Test server failed to start.");
+  _safeURL = self.testServer->GetURL("/defaultresponse");
+  _safeContent = "Default response";
+}
+
+// On iOS < 14, the legacy TLS interstitial should not be shown.
+- (void)testLegacyTLSInterstitialNotShownOnOldVersions {
+  if (base::ios::IsRunningOnIOS14OrLater()) {
+    return;
+  }
+  [ChromeEarlGrey loadURL:_legacyTLSURL];
+  [ChromeEarlGrey waitForWebStateContainingText:_unsafeContent];
+}
+
+// The remaining tests in this file are only relevant for iOS 14 or later.
+
+// Test that loading a page from a server over TLS 1.0 causes the legacy TLS
+// interstitial to show.
+- (void)testLegacyTLSShowsInterstitial {
+  if (!base::ios::IsRunningOnIOS14OrLater()) {
+    return;
+  }
+  [ChromeEarlGrey loadURL:_legacyTLSURL];
+  [ChromeEarlGrey waitForWebStateContainingText:_interstitialContent];
+}
+
+// Test that going back to safety returns the user to the previous page.
+- (void)testLegacyTLSInterstitialBackToSafety {
+  if (!base::ios::IsRunningOnIOS14OrLater()) {
+    return;
+  }
+  // Load a benign page first.
+  [ChromeEarlGrey loadURL:_safeURL];
+  [ChromeEarlGrey waitForWebStateContainingText:_safeContent];
+
+  // Trigger the legacy TLS interstitial.
+  [ChromeEarlGrey loadURL:_legacyTLSURL];
+  [ChromeEarlGrey waitForWebStateContainingText:_interstitialContent];
+
+  // Tap on the "Back to safety" button and verify that we navigate back to the
+  // previous page.
+  [ChromeEarlGrey tapWebStateElementWithID:@"primary-button"];
+  [ChromeEarlGrey waitForWebStateContainingText:_safeContent];
+}
+
+// Test that clicking through the interstitial works.
+- (void)testLegacyTLSInterstitialProceed {
+  if (!base::ios::IsRunningOnIOS14OrLater()) {
+    return;
+  }
+  // Load a benign page first.
+  [ChromeEarlGrey loadURL:_safeURL];
+  [ChromeEarlGrey waitForWebStateContainingText:_safeContent];
+
+  // Trigger the legacy TLS interstitial.
+  [ChromeEarlGrey loadURL:_legacyTLSURL];
+  [ChromeEarlGrey waitForWebStateContainingText:_interstitialContent];
+
+  // Tap on the "Proceed" link and verify that we go to the unsafe page (in this
+  // case, a cert error interstitial for the test server's self-signed cert).
+  [ChromeEarlGrey tapWebStateElementWithID:@"details-button"];
+  [ChromeEarlGrey tapWebStateElementWithID:@"proceed-link"];
+  [ChromeEarlGrey waitForWebStateContainingText:_unsafeContent];
+}
+
+// Test that clicking through the interstitial is remembered on a reload, and
+// we don't show the interstitial again.
+- (void)testLegacyTLSInterstitialAllowlist {
+  if (!base::ios::IsRunningOnIOS14OrLater()) {
+    return;
+  }
+  // Trigger the legacy TLS interstitial.
+  [ChromeEarlGrey loadURL:_legacyTLSURL];
+  [ChromeEarlGrey waitForWebStateContainingText:_interstitialContent];
+
+  // Tap on the "Proceed" link and verify that we go to the unsafe page.
+  [ChromeEarlGrey tapWebStateElementWithID:@"details-button"];
+  [ChromeEarlGrey tapWebStateElementWithID:@"proceed-link"];
+  [ChromeEarlGrey waitForWebStateContainingText:_unsafeContent];
+
+  // Navigate away to another page.
+  [ChromeEarlGrey loadURL:_safeURL];
+  [ChromeEarlGrey waitForWebStateContainingText:_safeContent];
+
+  // Navigate to the legacy TLS page again. Legacy TLS interstitial should not
+  // be shown.
+  [ChromeEarlGrey loadURL:_legacyTLSURL];
+  [ChromeEarlGrey waitForWebStateContainingText:_unsafeContent];
+}
+
+// Test that the allowlist is cleared after session restart and that we show the
+// legacy TLS interstitial again.
+- (void)testLegacyTLSInterstitialAllowlistClearedOnRestart {
+  if (!base::ios::IsRunningOnIOS14OrLater()) {
+    return;
+  }
+  // Trigger the legacy TLS interstitial.
+  [ChromeEarlGrey loadURL:_legacyTLSURL];
+  [ChromeEarlGrey waitForWebStateContainingText:_interstitialContent];
+
+  // Tap on the "Proceed" link and verify that we go to the unsafe page.
+  [ChromeEarlGrey tapWebStateElementWithID:@"details-button"];
+  [ChromeEarlGrey tapWebStateElementWithID:@"proceed-link"];
+  [ChromeEarlGrey waitForWebStateContainingText:_unsafeContent];
+
+  // Navigate away to another page.
+  [ChromeEarlGrey loadURL:_safeURL];
+  [ChromeEarlGrey waitForWebStateContainingText:_safeContent];
+
+  // Do a session restoration.
+  [ChromeEarlGrey triggerRestoreViaTabGridRemoveAllUndo];
+  [ChromeEarlGrey waitForWebStateContainingText:_safeContent];
+
+  // Navigate to the legacy TLS page again. The interstitial should trigger.
+  [ChromeEarlGrey loadURL:_legacyTLSURL];
+  [ChromeEarlGrey waitForWebStateContainingText:_interstitialContent];
+}
+
+@end
diff --git a/ios/components/security_interstitials/legacy_tls/BUILD.gn b/ios/components/security_interstitials/legacy_tls/BUILD.gn
new file mode 100644
index 0000000..b6c6233
--- /dev/null
+++ b/ios/components/security_interstitials/legacy_tls/BUILD.gn
@@ -0,0 +1,24 @@
+# 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.
+
+source_set("legacy_tls") {
+  configs += [ "//build/config/compiler:enable_arc" ]
+  sources = [
+    "legacy_tls_blocking_page.h",
+    "legacy_tls_blocking_page.mm",
+    "legacy_tls_controller_client.h",
+    "legacy_tls_controller_client.mm",
+    "legacy_tls_tab_allow_list.h",
+    "legacy_tls_tab_allow_list.mm",
+  ]
+  deps = [
+    "//base",
+    "//components/security_interstitials/core",
+    "//components/strings:components_strings_grit",
+    "//ios/components/security_interstitials",
+    "//ios/web/public",
+    "//net",
+    "//ui/base",
+  ]
+}
diff --git a/ios/components/security_interstitials/legacy_tls/DEPS b/ios/components/security_interstitials/legacy_tls/DEPS
new file mode 100644
index 0000000..e972e39
--- /dev/null
+++ b/ios/components/security_interstitials/legacy_tls/DEPS
@@ -0,0 +1,4 @@
+include_rules = [
+  "+ios/net",
+  "+net/base",
+]
diff --git a/ios/components/security_interstitials/legacy_tls/legacy_tls_blocking_page.h b/ios/components/security_interstitials/legacy_tls/legacy_tls_blocking_page.h
new file mode 100644
index 0000000..acd3f5e
--- /dev/null
+++ b/ios/components/security_interstitials/legacy_tls/legacy_tls_blocking_page.h
@@ -0,0 +1,46 @@
+// 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 IOS_COMPONENTS_SECURITY_INTERSTITIALS_LEGACY_TLS_LEGACY_TLS_BLOCKING_PAGE_H_
+#define IOS_COMPONENTS_SECURITY_INTERSTITIALS_LEGACY_TLS_LEGACY_TLS_BLOCKING_PAGE_H_
+
+#include "ios/components/security_interstitials/ios_security_interstitial_page.h"
+#include "ios/components/security_interstitials/legacy_tls/legacy_tls_controller_client.h"
+
+class GURL;
+
+// This class is responsible for showing/hiding the interstitial page that is
+// shown for legacy TLS connections.
+class LegacyTLSBlockingPage
+    : public security_interstitials::IOSSecurityInterstitialPage {
+ public:
+  ~LegacyTLSBlockingPage() override;
+
+  // Creates a legacy TLS blocking page.
+  LegacyTLSBlockingPage(web::WebState* web_state,
+                        const GURL& request_url,
+                        std::unique_ptr<LegacyTLSControllerClient> client);
+
+ protected:
+  // SecurityInterstitialPage implementation:
+  bool ShouldCreateNewNavigation() const override;
+  void PopulateInterstitialStrings(
+      base::DictionaryValue* load_time_data) const override;
+
+ private:
+  void HandleScriptCommand(const base::DictionaryValue& message,
+                           const GURL& origin_url,
+                           bool user_is_interacting,
+                           web::WebFrame* sender_frame) override;
+
+  void AfterShow() override;
+
+  web::WebState* web_state_ = nullptr;
+  const GURL request_url_;
+  std::unique_ptr<LegacyTLSControllerClient> controller_;
+
+  DISALLOW_COPY_AND_ASSIGN(LegacyTLSBlockingPage);
+};
+
+#endif  // IOS_COMPONENTS_SECURITY_INTERSTITIALS_LEGACY_TLS_LEGACY_TLS_BLOCKING_PAGE_H_
diff --git a/ios/components/security_interstitials/legacy_tls/legacy_tls_blocking_page.mm b/ios/components/security_interstitials/legacy_tls/legacy_tls_blocking_page.mm
new file mode 100644
index 0000000..421f9b4
--- /dev/null
+++ b/ios/components/security_interstitials/legacy_tls/legacy_tls_blocking_page.mm
@@ -0,0 +1,100 @@
+// 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.
+
+#import "ios/components/security_interstitials/legacy_tls/legacy_tls_blocking_page.h"
+
+#include <utility>
+
+#include "base/strings/string_number_conversions.h"
+#include "base/values.h"
+#include "components/security_interstitials/core/common_string_util.h"
+#include "components/security_interstitials/core/metrics_helper.h"
+#include "ios/components/security_interstitials/ios_blocking_page_controller_client.h"
+#include "ios/components/security_interstitials/ios_blocking_page_metrics_helper.h"
+#include "net/base/net_errors.h"
+#include "ui/base/l10n/l10n_util.h"
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+LegacyTLSBlockingPage::LegacyTLSBlockingPage(
+    web::WebState* web_state,
+    const GURL& request_url,
+    std::unique_ptr<LegacyTLSControllerClient> client)
+    : security_interstitials::IOSSecurityInterstitialPage(web_state,
+                                                          request_url,
+                                                          client.get()),
+      web_state_(web_state),
+      request_url_(request_url),
+      controller_(std::move(client)) {
+  DCHECK(web_state_);
+
+  // Creating an interstitial without showing it (e.g. from
+  // chrome://interstitials) leaks memory, so don't create it here.
+}
+
+LegacyTLSBlockingPage::~LegacyTLSBlockingPage() = default;
+
+bool LegacyTLSBlockingPage::ShouldCreateNewNavigation() const {
+  return true;
+}
+
+void LegacyTLSBlockingPage::PopulateInterstitialStrings(
+    base::DictionaryValue* load_time_data) const {
+  CHECK(load_time_data);
+
+  // Shared with SSL errors.
+  security_interstitials::common_string_util::PopulateSSLLayoutStrings(
+      net::ERR_SSL_OBSOLETE_VERSION, load_time_data);
+
+  load_time_data->SetBoolean("overridable", true);
+  load_time_data->SetBoolean("hide_primary_button", false);
+  load_time_data->SetBoolean("bad_clock", false);
+  load_time_data->SetString("type", "LEGACY_TLS");
+
+  const base::string16 hostname(
+      security_interstitials::common_string_util::GetFormattedHostName(
+          request_url_));
+  security_interstitials::common_string_util::PopulateLegacyTLSStrings(
+      load_time_data, hostname);
+}
+
+void LegacyTLSBlockingPage::HandleScriptCommand(
+    const base::DictionaryValue& message,
+    const GURL& origin_url,
+    bool user_is_interacting,
+    web::WebFrame* sender_frame) {
+  std::string command_string;
+  if (!message.GetString("command", &command_string)) {
+    LOG(ERROR) << "JS message parameter not found: command";
+    return;
+  }
+
+  // Remove the command prefix so that the string value can be converted to a
+  // SecurityInterstitialCommand enum value.
+  std::size_t delimiter = command_string.find(".");
+  if (delimiter == std::string::npos) {
+    return;
+  }
+
+  // Parse the command int value from the text after the delimiter.
+  int command = 0;
+  if (!base::StringToInt(command_string.substr(delimiter + 1), &command)) {
+    NOTREACHED() << "Command cannot be parsed to an int : " << command_string;
+    return;
+  }
+
+  if (command == security_interstitials::CMD_DONT_PROCEED) {
+    controller_->metrics_helper()->RecordUserDecision(
+        security_interstitials::MetricsHelper::DONT_PROCEED);
+    controller_->GoBack();
+  } else if (command == security_interstitials::CMD_PROCEED) {
+    controller_->metrics_helper()->RecordUserDecision(
+        security_interstitials::MetricsHelper::PROCEED);
+    controller_->Proceed();
+  }
+}
+
+void LegacyTLSBlockingPage::AfterShow() {}
diff --git a/ios/components/security_interstitials/legacy_tls/legacy_tls_controller_client.h b/ios/components/security_interstitials/legacy_tls/legacy_tls_controller_client.h
new file mode 100644
index 0000000..bb9955e0
--- /dev/null
+++ b/ios/components/security_interstitials/legacy_tls/legacy_tls_controller_client.h
@@ -0,0 +1,34 @@
+// 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 IOS_COMPONENTS_SECURITY_INTERSTITIALS_LEGACY_TLS_LEGACY_TLS_CONTROLLER_CLIENT_H_
+#define IOS_COMPONENTS_SECURITY_INTERSTITIALS_LEGACY_TLS_LEGACY_TLS_CONTROLLER_CLIENT_H_
+
+#include "ios/components/security_interstitials/ios_blocking_page_controller_client.h"
+#include "url/gurl.h"
+
+class GURL;
+
+namespace web {
+class WebState;
+}  // namespace web
+
+// Controller client used for legacy TLS blocking pages.
+class LegacyTLSControllerClient
+    : public security_interstitials::IOSBlockingPageControllerClient {
+ public:
+  LegacyTLSControllerClient(web::WebState* web_state,
+                            const GURL& request_url,
+                            const std::string& app_locale);
+  ~LegacyTLSControllerClient() override;
+
+  // security_interstitials::ControllerClient:
+  void Proceed() override;
+
+ private:
+  // The URL of the page causing the insterstitial.
+  const GURL request_url_;
+};
+
+#endif  // IOS_COMPONENTS_SECURITY_INTERSTITIALS_LEGACY_TLS_LEGACY_TLS_CONTROLLER_CLIENT_H_
diff --git a/ios/components/security_interstitials/legacy_tls/legacy_tls_controller_client.mm b/ios/components/security_interstitials/legacy_tls/legacy_tls_controller_client.mm
new file mode 100644
index 0000000..bd69afd
--- /dev/null
+++ b/ios/components/security_interstitials/legacy_tls/legacy_tls_controller_client.mm
@@ -0,0 +1,43 @@
+// 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.
+
+#import "ios/components/security_interstitials/legacy_tls/legacy_tls_controller_client.h"
+
+#include "components/security_interstitials/core/metrics_helper.h"
+#include "ios/components/security_interstitials/ios_blocking_page_metrics_helper.h"
+#include "ios/components/security_interstitials/legacy_tls/legacy_tls_tab_allow_list.h"
+#import "ios/web/public/web_state.h"
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+namespace {
+// Creates a metrics helper for |url|.
+std::unique_ptr<security_interstitials::IOSBlockingPageMetricsHelper>
+CreateMetricsHelper(web::WebState* web_state, const GURL& url) {
+  security_interstitials::MetricsHelper::ReportDetails reporting_info;
+  reporting_info.metric_prefix = "legacy_tls";
+  return std::make_unique<security_interstitials::IOSBlockingPageMetricsHelper>(
+      web_state, url, reporting_info);
+}
+}  // namespace
+
+LegacyTLSControllerClient::LegacyTLSControllerClient(
+    web::WebState* web_state,
+    const GURL& request_url,
+    const std::string& app_locale)
+    : IOSBlockingPageControllerClient(
+          web_state,
+          CreateMetricsHelper(web_state, request_url),
+          app_locale),
+      request_url_(request_url) {}
+
+LegacyTLSControllerClient::~LegacyTLSControllerClient() {}
+
+void LegacyTLSControllerClient::Proceed() {
+  LegacyTLSTabAllowList::FromWebState(web_state())
+      ->AllowDomain(request_url_.host());
+  Reload();
+}
diff --git a/ios/components/security_interstitials/legacy_tls/legacy_tls_tab_allow_list.h b/ios/components/security_interstitials/legacy_tls/legacy_tls_tab_allow_list.h
new file mode 100644
index 0000000..88c47978
--- /dev/null
+++ b/ios/components/security_interstitials/legacy_tls/legacy_tls_tab_allow_list.h
@@ -0,0 +1,38 @@
+// 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 IOS_COMPONENTS_SECURITY_INTERSTITIALS_LEGACY_TLS_LEGACY_TLS_TAB_ALLOW_LIST_H_
+#define IOS_COMPONENTS_SECURITY_INTERSTITIALS_LEGACY_TLS_LEGACY_TLS_TAB_ALLOW_LIST_H_
+
+#include <set>
+#include <string>
+
+#import "ios/web/public/web_state_user_data.h"
+
+// LegacyTLSTabAllowList tracks the allowlist decisions for legacy TLS hosts.
+// Decisions are scoped to the domain.
+class LegacyTLSTabAllowList
+    : public web::WebStateUserData<LegacyTLSTabAllowList> {
+ public:
+  // LegacyTLSTabAllowList is move-only.
+  LegacyTLSTabAllowList(LegacyTLSTabAllowList&& other);
+  LegacyTLSTabAllowList& operator=(LegacyTLSTabAllowList&& other);
+  ~LegacyTLSTabAllowList() override;
+
+  // Returns whether |domain| has been allowlisted.
+  bool IsDomainAllowed(const std::string& domain) const;
+
+  // Allows future navigations to |domain|.
+  void AllowDomain(const std::string& domain);
+
+ private:
+  explicit LegacyTLSTabAllowList(web::WebState* web_state);
+  friend class web::WebStateUserData<LegacyTLSTabAllowList>;
+  WEB_STATE_USER_DATA_KEY_DECL();
+
+  // Set of allowlisted domains.
+  std::set<std::string> allowed_domains_;
+};
+
+#endif  // IOS_COMPONENTS_SECURITY_INTERSTITIALS_LEGACY_TLS_LEGACY_TLS_TAB_ALLOW_LIST_H_
diff --git a/ios/components/security_interstitials/legacy_tls/legacy_tls_tab_allow_list.mm b/ios/components/security_interstitials/legacy_tls/legacy_tls_tab_allow_list.mm
new file mode 100644
index 0000000..55b78fe8
--- /dev/null
+++ b/ios/components/security_interstitials/legacy_tls/legacy_tls_tab_allow_list.mm
@@ -0,0 +1,31 @@
+// 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.
+
+#import "ios/components/security_interstitials/legacy_tls/legacy_tls_tab_allow_list.h"
+
+#import "ios/web/public/web_state.h"
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+WEB_STATE_USER_DATA_KEY_IMPL(LegacyTLSTabAllowList)
+
+LegacyTLSTabAllowList::LegacyTLSTabAllowList(web::WebState* web_state) {}
+
+LegacyTLSTabAllowList::LegacyTLSTabAllowList(LegacyTLSTabAllowList&& other) =
+    default;
+
+LegacyTLSTabAllowList& LegacyTLSTabAllowList::operator=(
+    LegacyTLSTabAllowList&& other) = default;
+
+LegacyTLSTabAllowList::~LegacyTLSTabAllowList() = default;
+
+bool LegacyTLSTabAllowList::IsDomainAllowed(const std::string& domain) const {
+  return allowed_domains_.find(domain) != allowed_domains_.end();
+}
+
+void LegacyTLSTabAllowList::AllowDomain(const std::string& domain) {
+  allowed_domains_.insert(domain);
+}
diff --git a/ios/web/common/features.h b/ios/web/common/features.h
index d7e550d..f26551c 100644
--- a/ios/web/common/features.h
+++ b/ios/web/common/features.h
@@ -63,6 +63,9 @@
 // See also: https://wicg.github.io/scroll-to-text-fragment/
 extern const base::Feature kScrollToTextIOS;
 
+// When enabled, display an interstitial on legacy TLS connections.
+extern const base::Feature kIOSLegacyTLSInterstitial;
+
 // When true, for each navigation, the default user agent is chosen by the
 // WebClient GetDefaultUserAgent() method. If it is false, the mobile version
 // is requested by default.
diff --git a/ios/web/common/features.mm b/ios/web/common/features.mm
index df6b531..9f8e055 100644
--- a/ios/web/common/features.mm
+++ b/ios/web/common/features.mm
@@ -50,6 +50,8 @@
 
 const base::Feature kScrollToTextIOS{"ScrollToTextIOS",
                                      base::FEATURE_DISABLED_BY_DEFAULT};
+const base::Feature kIOSLegacyTLSInterstitial{
+    "IOSLegacyTLSInterstitial", base::FEATURE_DISABLED_BY_DEFAULT};
 
 bool UseWebClientDefaultUserAgent() {
   if (@available(iOS 13, *)) {
diff --git a/ios/web/navigation/BUILD.gn b/ios/web/navigation/BUILD.gn
index 79563e6..2b6252b 100644
--- a/ios/web/navigation/BUILD.gn
+++ b/ios/web/navigation/BUILD.gn
@@ -35,6 +35,7 @@
     "//ios/web/web_state/ui:crw_web_view_navigation_proxy",
     "//ios/web/web_state/ui:web_view_handler",
     "//ios/web/web_view:util",
+    "//net",
     "//ui/base",
     "//url",
   ]
diff --git a/ios/web/navigation/crw_wk_navigation_handler.mm b/ios/web/navigation/crw_wk_navigation_handler.mm
index a906825..6252a077 100644
--- a/ios/web/navigation/crw_wk_navigation_handler.mm
+++ b/ios/web/navigation/crw_wk_navigation_handler.mm
@@ -10,6 +10,7 @@
 #include "base/strings/sys_string_conversions.h"
 #include "base/timer/timer.h"
 #import "ios/net/http_response_headers_util.h"
+#import "ios/net/protocol_handler_util.h"
 #include "ios/web/common/features.h"
 #import "ios/web/common/url_scheme_util.h"
 #import "ios/web/js_messaging/crw_js_injector.h"
@@ -39,6 +40,7 @@
 #import "ios/web/web_view/error_translation_util.h"
 #import "ios/web/web_view/wk_web_view_util.h"
 #import "net/base/mac/url_conversions.h"
+#include "net/base/net_errors.h"
 #include "net/cert/x509_util_ios.h"
 #include "url/gurl.h"
 
@@ -1214,6 +1216,37 @@
              }];
 }
 
+- (void)webView:(WKWebView*)webView
+     authenticationChallenge:(NSURLAuthenticationChallenge*)challenge
+    shouldAllowDeprecatedTLS:(void (^)(BOOL))decisionHandler
+    API_AVAILABLE(ios(14)) {
+  [self didReceiveWKNavigationDelegateCallback];
+  DCHECK(challenge);
+  DCHECK(decisionHandler);
+
+  // If the legacy TLS interstitial is not enabled, don't cause errors.
+  if (!base::FeatureList::IsEnabled(web::features::kIOSLegacyTLSInterstitial)) {
+    decisionHandler(YES);
+    return;
+  }
+
+  if (web::GetWebClient()->IsLegacyTLSAllowedForHost(
+          self.webStateImpl,
+          base::SysNSStringToUTF8(challenge.protectionSpace.host))) {
+    decisionHandler(YES);
+    return;
+  }
+
+  if (self.pendingNavigationInfo) {
+    self.pendingNavigationInfo.cancelled = YES;
+    self.pendingNavigationInfo.cancellationError =
+        [NSError errorWithDomain:net::kNSErrorDomain
+                            code:net::ERR_SSL_OBSOLETE_VERSION
+                        userInfo:nil];
+  }
+  decisionHandler(NO);
+}
+
 - (void)webViewWebContentProcessDidTerminate:(WKWebView*)webView {
   [self didReceiveWKNavigationDelegateCallback];
 
diff --git a/ios/web/navigation/navigation_item_impl.mm b/ios/web/navigation/navigation_item_impl.mm
index 6c1b103..ac70208 100644
--- a/ios/web/navigation/navigation_item_impl.mm
+++ b/ios/web/navigation/navigation_item_impl.mm
@@ -36,6 +36,12 @@
 
 namespace web {
 
+// Value 50 was picked experimentally by examining Chrome for iOS UI. Tab strip
+// on 12.9" iPad Pro trucates the title to less than 50 characters (title that
+// only consists of letters "i"). Tab strip has the biggest surface to fit
+// title.
+const size_t kMaxTitleLength = 50;
+
 // static
 std::unique_ptr<NavigationItem> NavigationItem::Create() {
   return std::unique_ptr<NavigationItem>(new NavigationItemImpl());
@@ -126,7 +132,12 @@
 void NavigationItemImpl::SetTitle(const base::string16& title) {
   if (title_ == title)
     return;
-  title_ = title;
+
+  if (title.size() > kMaxTitleLength) {
+    title_ = gfx::TruncateString(title, kMaxTitleLength, gfx::CHARACTER_BREAK);
+  } else {
+    title_ = title;
+  }
   cached_display_title_.clear();
 }
 
diff --git a/ios/web/navigation/navigation_item_impl_unittest.mm b/ios/web/navigation/navigation_item_impl_unittest.mm
index 57f94ea..66f1a9e 100644
--- a/ios/web/navigation/navigation_item_impl_unittest.mm
+++ b/ios/web/navigation/navigation_item_impl_unittest.mm
@@ -160,6 +160,12 @@
   EXPECT_EQ(original_url, item_->GetURL());
 }
 
+// Tests setting title longer than kMaxTitleLength.
+TEST_F(NavigationItemTest, ExtraLongTitle) {
+  item_->SetTitle(base::UTF8ToUTF16(std::string(kMaxTitleLength + 1, 'i')));
+  EXPECT_EQ(kMaxTitleLength, item_->GetTitle().size());
+}
+
 // Tests NavigationItemImpl::GetDisplayTitleForURL method.
 TEST_F(NavigationItemTest, GetDisplayTitleForURL) {
   base::string16 title;
diff --git a/ios/web/public/navigation/navigation_item.h b/ios/web/public/navigation/navigation_item.h
index 000457d9..b9b6260 100644
--- a/ios/web/public/navigation/navigation_item.h
+++ b/ios/web/public/navigation/navigation_item.h
@@ -23,6 +23,11 @@
 struct Referrer;
 struct SSLStatus;
 
+// User interface limits the length of the title, so placing limit does not
+// have any functional side effects, and allows to use less memory for
+// navigation session.
+extern const size_t kMaxTitleLength;
+
 // A NavigationItem is a data structure that captures all the information
 // required to recreate a browsing state. It represents one point in the
 // chain of navigation managed by a NavigationManager.
diff --git a/ios/web/public/web_client.h b/ios/web/public/web_client.h
index 60dc2fc..6042364 100644
--- a/ios/web/public/web_client.h
+++ b/ios/web/public/web_client.h
@@ -163,6 +163,12 @@
       int64_t navigation_id,
       const base::Callback<void(bool)>& callback);
 
+  // Allows the embedder to specify legacy TLS enforcement on a per-host basis,
+  // for example to allow users to bypass interstitial warnings on affected
+  // hosts.
+  virtual bool IsLegacyTLSAllowedForHost(WebState* web_state,
+                                         const std::string& hostname);
+
   // Calls the given |callback| with the contents of an error page to display
   // when a navigation error occurs. |error| is always a valid pointer. The
   // string passed to |callback| will be nil if no error page should be
diff --git a/ios/web/web_client.mm b/ios/web/web_client.mm
index 812d7812b..f7b923d0 100644
--- a/ios/web/web_client.mm
+++ b/ios/web/web_client.mm
@@ -96,6 +96,11 @@
   callback.Run(false);
 }
 
+bool WebClient::IsLegacyTLSAllowedForHost(WebState* web_state,
+                                          const std::string& hostname) {
+  return false;
+}
+
 void WebClient::PrepareErrorPage(WebState* web_state,
                                  const GURL& url,
                                  NSError* error,
diff --git a/media/blink/webmediaplayer_impl.cc b/media/blink/webmediaplayer_impl.cc
index 980649a8..7c6f63e 100644
--- a/media/blink/webmediaplayer_impl.cc
+++ b/media/blink/webmediaplayer_impl.cc
@@ -1038,9 +1038,11 @@
 
   OutputDeviceStatusCB callback =
       ConvertToOutputDeviceStatusCB(std::move(completion_callback));
+  auto sink_id_utf8 = sink_id.Utf8();
   media_task_runner_->PostTask(
       FROM_HERE, base::BindOnce(&SetSinkIdOnMediaThread, audio_source_provider_,
-                                sink_id.Utf8(), std::move(callback)));
+                                sink_id_utf8, std::move(callback)));
+  delegate_->DidAudioOutputSinkChange(delegate_id_, sink_id_utf8);
 }
 
 STATIC_ASSERT_ENUM(WebMediaPlayer::kPreloadNone, MultibufferDataSource::NONE);
@@ -2563,6 +2565,11 @@
   client_->RequestExitPictureInPicture();
 }
 
+void WebMediaPlayerImpl::OnSetAudioSink(const std::string& sink_id) {
+  SetSinkId(WebString::FromASCII(sink_id),
+            base::DoNothing::Once<base::Optional<blink::WebSetSinkIdError>>());
+}
+
 void WebMediaPlayerImpl::OnVolumeMultiplierUpdate(double multiplier) {
   volume_multiplier_ = multiplier;
   SetVolume(volume_);
diff --git a/media/blink/webmediaplayer_impl.h b/media/blink/webmediaplayer_impl.h
index 3675d8e..1025c7a 100644
--- a/media/blink/webmediaplayer_impl.h
+++ b/media/blink/webmediaplayer_impl.h
@@ -62,12 +62,12 @@
 class WebLocalFrame;
 class WebMediaPlayerClient;
 class WebMediaPlayerEncryptedMediaClient;
-}
+}  // namespace blink
 
 namespace base {
 class SingleThreadTaskRunner;
 class TaskRunner;
-}
+}  // namespace base
 
 namespace cc {
 class VideoLayer;
@@ -77,7 +77,7 @@
 namespace gles2 {
 class GLES2Interface;
 }
-}
+}  // namespace gpu
 
 namespace media {
 class CdmContextRef;
@@ -252,6 +252,7 @@
   void OnSeekBackward(double seconds) override;
   void OnEnterPictureInPicture() override;
   void OnExitPictureInPicture() override;
+  void OnSetAudioSink(const std::string& sink_id) override;
   void OnVolumeMultiplierUpdate(double multiplier) override;
   void OnBecamePersistentVideo(bool value) override;
   void OnPowerExperimentState(bool state) override;
diff --git a/media/blink/webmediaplayer_impl_unittest.cc b/media/blink/webmediaplayer_impl_unittest.cc
index 8f1e29a..a754b25 100644
--- a/media/blink/webmediaplayer_impl_unittest.cc
+++ b/media/blink/webmediaplayer_impl_unittest.cc
@@ -280,6 +280,8 @@
 
   MOCK_METHOD2(DidPictureInPictureAvailabilityChange, void(int, bool));
 
+  MOCK_METHOD2(DidAudioOutputSinkChange, void(int, const std::string&));
+
  private:
   Observer* observer_ = nullptr;
   int player_id_ = 1234;
diff --git a/media/gpu/BUILD.gn b/media/gpu/BUILD.gn
index b614f2e..9ed9a35 100644
--- a/media/gpu/BUILD.gn
+++ b/media/gpu/BUILD.gn
@@ -524,6 +524,10 @@
     ]
     libs = [ "dxguid.lib" ]
   }
+
+  if (is_mac) {
+    deps += [ "//media/gpu/mac:unit_tests" ]
+  }
 }
 
 # TODO(crbug.com/1006266): consider using |use_chromeos_video_acceleration|.
diff --git a/media/gpu/command_buffer_helper.cc b/media/gpu/command_buffer_helper.cc
index 8a445e2..63e0c4d 100644
--- a/media/gpu/command_buffer_helper.cc
+++ b/media/gpu/command_buffer_helper.cc
@@ -14,9 +14,12 @@
 #include "gpu/command_buffer/common/scheduling_priority.h"
 #include "gpu/command_buffer/service/decoder_context.h"
 #include "gpu/command_buffer/service/scheduler.h"
+#include "gpu/command_buffer/service/shared_image_backing.h"
+#include "gpu/command_buffer/service/shared_image_representation.h"
 #include "gpu/command_buffer/service/sync_point_manager.h"
 #include "gpu/ipc/service/command_buffer_stub.h"
 #include "gpu/ipc/service/gpu_channel.h"
+#include "gpu/ipc/service/gpu_channel_manager.h"
 #include "media/gpu/gles2_decoder_helper.h"
 #include "ui/gl/gl_context.h"
 
@@ -45,6 +48,8 @@
 #endif  // defined(OS_MAC)
     );
     decoder_helper_ = GLES2DecoderHelper::Create(stub_->decoder_context());
+    tracker_ =
+        std::make_unique<gpu::MemoryTypeTracker>(stub_->GetMemoryTracker());
   }
 
   gl::GLContext* GetGLContext() override {
@@ -71,6 +76,24 @@
     return decoder_helper_ && decoder_helper_->MakeContextCurrent();
   }
 
+  std::unique_ptr<gpu::SharedImageRepresentationFactoryRef> Register(
+      std::unique_ptr<gpu::SharedImageBacking> backing) override {
+    DVLOG(2) << __func__;
+    DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
+    return stub_->channel()
+        ->gpu_channel_manager()
+        ->shared_image_manager()
+        ->Register(std::move(backing), tracker_.get());
+  }
+
+  gpu::TextureBase* GetTexture(GLuint service_id) const override {
+    DVLOG(2) << __func__ << "(" << service_id << ")";
+    DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
+    DCHECK(stub_->decoder_context()->GetGLContext()->IsCurrent(nullptr));
+    DCHECK(textures_.count(service_id));
+    return textures_.at(service_id)->GetTextureBase();
+  }
+
   GLuint CreateTexture(GLenum target,
                        GLenum internal_format,
                        GLsizei width,
@@ -211,6 +234,8 @@
 
   WillDestroyStubCB will_destroy_stub_cb_;
 
+  std::unique_ptr<gpu::MemoryTypeTracker> tracker_;
+
   THREAD_CHECKER(thread_checker_);
   DISALLOW_COPY_AND_ASSIGN(CommandBufferHelperImpl);
 };
diff --git a/media/gpu/command_buffer_helper.h b/media/gpu/command_buffer_helper.h
index 0b028ce7..ba882cf7 100644
--- a/media/gpu/command_buffer_helper.h
+++ b/media/gpu/command_buffer_helper.h
@@ -18,6 +18,9 @@
 
 namespace gpu {
 class CommandBufferStub;
+class SharedImageBacking;
+class SharedImageRepresentationFactoryRef;
+class TextureBase;
 }  // namespace gpu
 
 namespace gl {
@@ -55,6 +58,12 @@
   // Makes the GL context current.
   virtual bool MakeContextCurrent() = 0;
 
+  // Register a shared image backing
+  virtual std::unique_ptr<gpu::SharedImageRepresentationFactoryRef> Register(
+      std::unique_ptr<gpu::SharedImageBacking> backing) = 0;
+
+  virtual gpu::TextureBase* GetTexture(GLuint service_id) const = 0;
+
   // Creates a texture and returns its |service_id|.
   //
   // See glTexImage2D() for argument definitions.
diff --git a/media/gpu/mac/BUILD.gn b/media/gpu/mac/BUILD.gn
index cc6b29f4..b0f318f 100644
--- a/media/gpu/mac/BUILD.gn
+++ b/media/gpu/mac/BUILD.gn
@@ -18,6 +18,8 @@
   visibility = [ "//media/gpu" ]
 
   sources = [
+    "vt_config_util.h",
+    "vt_config_util.mm",
     "vt_video_decode_accelerator_mac.cc",
     "vt_video_decode_accelerator_mac.h",
     "vt_video_encode_accelerator_mac.cc",
@@ -42,3 +44,16 @@
     "//ui/gl",
   ]
 }
+
+source_set("unit_tests") {
+  testonly = true
+  frameworks = [
+    "CoreFoundation.framework",
+    "CoreMedia.framework",
+  ]
+  deps = [
+    "//media/gpu:test_support",
+    "//testing/gtest",
+  ]
+  sources = [ "vt_config_util_unittest.cc" ]
+}
diff --git a/media/gpu/mac/vt_config_util.h b/media/gpu/mac/vt_config_util.h
new file mode 100644
index 0000000..e640f42
--- /dev/null
+++ b/media/gpu/mac/vt_config_util.h
@@ -0,0 +1,31 @@
+// 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 MEDIA_GPU_MAC_VT_CONFIG_UTIL_H_
+#define MEDIA_GPU_MAC_VT_CONFIG_UTIL_H_
+
+#include <CoreFoundation/CoreFoundation.h>
+#include <CoreMedia/CoreMedia.h>
+
+#include "base/optional.h"
+#include "media/base/hdr_metadata.h"
+#include "media/base/video_codecs.h"
+#include "media/base/video_color_space.h"
+#include "media/gpu/media_gpu_export.h"
+#include "media/video/video_decode_accelerator.h"
+
+namespace media {
+
+MEDIA_GPU_EXPORT CFMutableDictionaryRef
+CreateFormatExtensions(CMVideoCodecType codec_type,
+                       VideoCodecProfile profile,
+                       const VideoColorSpace& color_space,
+                       base::Optional<HDRMetadata> hdr_metadata);
+
+MEDIA_GPU_EXPORT gfx::ColorSpace GetImageBufferColorSpace(
+    CVImageBufferRef image_buffer);
+
+}  // namespace media
+
+#endif  // MEDIA_GPU_MAC_VT_CONFIG_UTIL_H_
diff --git a/media/gpu/mac/vt_config_util.mm b/media/gpu/mac/vt_config_util.mm
new file mode 100644
index 0000000..20b9344
--- /dev/null
+++ b/media/gpu/mac/vt_config_util.mm
@@ -0,0 +1,502 @@
+// 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 "media/gpu/mac/vt_config_util.h"
+
+#import <Foundation/Foundation.h>
+
+#include <simd/simd.h>
+
+#include "base/mac/foundation_util.h"
+#include "base/no_destructor.h"
+
+namespace {
+
+// https://developer.apple.com/documentation/avfoundation/avassettrack/1386694-formatdescriptions?language=objc
+NSString* CMVideoCodecTypeToString(CMVideoCodecType code) {
+  NSString* result = [NSString
+      stringWithFormat:@"%c%c%c%c", (code >> 24) & 0xff, (code >> 16) & 0xff,
+                       (code >> 8) & 0xff, code & 0xff];
+  NSCharacterSet* characterSet = [NSCharacterSet whitespaceCharacterSet];
+  return [result stringByTrimmingCharactersInSet:characterSet];
+}
+
+// Helper functions to convert from CFStringRef kCM* keys to NSString.
+void SetDictionaryValue(NSMutableDictionary<NSString*, id>* dictionary,
+                        CFStringRef key,
+                        id value) {
+  if (value)
+    dictionary[base::mac::CFToNSCast(key)] = value;
+}
+
+void SetDictionaryValue(NSMutableDictionary<NSString*, id>* dictionary,
+                        CFStringRef key,
+                        CFStringRef value) {
+  SetDictionaryValue(dictionary, key, base::mac::CFToNSCast(value));
+}
+
+CFStringRef GetPrimaries(media::VideoColorSpace::PrimaryID primary_id) {
+  switch (primary_id) {
+    case media::VideoColorSpace::PrimaryID::BT709:
+    case media::VideoColorSpace::PrimaryID::UNSPECIFIED:  // Assume BT.709.
+      return kCMFormatDescriptionColorPrimaries_ITU_R_709_2;
+
+    case media::VideoColorSpace::PrimaryID::BT2020:
+      if (@available(macos 10.11, *))
+        return kCMFormatDescriptionColorPrimaries_ITU_R_2020;
+      DLOG(WARNING) << "kCMFormatDescriptionColorPrimaries_ITU_R_2020 "
+                       "unsupported prior to 10.11";
+      return nil;
+
+    case media::VideoColorSpace::PrimaryID::SMPTE170M:
+    case media::VideoColorSpace::PrimaryID::SMPTE240M:
+      return kCMFormatDescriptionColorPrimaries_SMPTE_C;
+
+    case media::VideoColorSpace::PrimaryID::BT470BG:
+      return kCMFormatDescriptionColorPrimaries_EBU_3213;
+
+    case media::VideoColorSpace::PrimaryID::SMPTEST431_2:
+      if (@available(macos 10.11, *))
+        return kCMFormatDescriptionColorPrimaries_DCI_P3;
+      DLOG(WARNING) << "kCMFormatDescriptionColorPrimaries_DCI_P3 unsupported "
+                       "prior to 10.11";
+      return nil;
+
+    case media::VideoColorSpace::PrimaryID::SMPTEST432_1:
+      if (@available(macos 10.11, *))
+        return kCMFormatDescriptionColorPrimaries_P3_D65;
+      DLOG(WARNING) << "kCMFormatDescriptionColorPrimaries_P3_D65 unsupported "
+                       "prior to 10.11";
+      return nil;
+
+    default:
+      DLOG(ERROR) << "Unsupported primary id: " << static_cast<int>(primary_id);
+      return nil;
+  }
+}
+
+CFStringRef GetTransferFunction(
+    media::VideoColorSpace::TransferID transfer_id) {
+  switch (transfer_id) {
+    case media::VideoColorSpace::TransferID::LINEAR:
+      if (@available(macos 10.14, *))
+        return kCMFormatDescriptionTransferFunction_Linear;
+      DLOG(WARNING) << "kCMFormatDescriptionTransferFunction_Linear "
+                       "unsupported prior to 10.14";
+      return nil;
+
+    case media::VideoColorSpace::TransferID::GAMMA22:
+    case media::VideoColorSpace::TransferID::GAMMA28:
+      return kCMFormatDescriptionTransferFunction_UseGamma;
+
+    case media::VideoColorSpace::TransferID::IEC61966_2_1:
+      if (@available(macos 10.13, *))
+        return kCVImageBufferTransferFunction_sRGB;
+      DLOG(WARNING)
+          << "kCVImageBufferTransferFunction_sRGB unsupported prior to 10.13";
+      return nil;
+
+    case media::VideoColorSpace::TransferID::SMPTE170M:
+    case media::VideoColorSpace::TransferID::BT709:
+    case media::VideoColorSpace::TransferID::UNSPECIFIED:  // Assume BT.709.
+      return kCMFormatDescriptionTransferFunction_ITU_R_709_2;
+
+    case media::VideoColorSpace::TransferID::BT2020_10:
+    case media::VideoColorSpace::TransferID::BT2020_12:
+      if (@available(macos 10.11, *))
+        return kCMFormatDescriptionTransferFunction_ITU_R_2020;
+      DLOG(WARNING) << "kCMFormatDescriptionTransferFunction_ITU_R_2020 "
+                       "unsupported prior to 10.11";
+      return nil;
+
+    case media::VideoColorSpace::TransferID::SMPTEST2084:
+      if (@available(macos 10.13, *))
+        return kCMFormatDescriptionTransferFunction_SMPTE_ST_2084_PQ;
+      DLOG(WARNING) << "kCMFormatDescriptionTransferFunction_SMPTE_ST_2084_PQ "
+                       "unsupported prior to 10.13";
+      return nil;
+
+    case media::VideoColorSpace::TransferID::ARIB_STD_B67:
+      if (@available(macos 10.13, *))
+        return kCMFormatDescriptionTransferFunction_ITU_R_2100_HLG;
+      DLOG(WARNING) << "kCMFormatDescriptionTransferFunction_ITU_R_2100_HLG "
+                       "unsupported prior to 10.13";
+      return nil;
+
+    case media::VideoColorSpace::TransferID::SMPTE240M:
+      return kCMFormatDescriptionTransferFunction_SMPTE_240M_1995;
+
+    case media::VideoColorSpace::TransferID::SMPTEST428_1:
+      if (@available(macos 10.12, *))
+        return kCMFormatDescriptionTransferFunction_SMPTE_ST_428_1;
+      DLOG(WARNING) << "kCMFormatDescriptionTransferFunction_SMPTE_ST_428_1 "
+                       "unsupported prior to 10.12";
+      return nil;
+
+    default:
+      DLOG(ERROR) << "Unsupported transfer function: "
+                  << static_cast<int>(transfer_id);
+      return nil;
+  }
+}
+
+CFStringRef GetMatrix(media::VideoColorSpace::MatrixID matrix_id) {
+  switch (matrix_id) {
+    case media::VideoColorSpace::MatrixID::BT709:
+    case media::VideoColorSpace::MatrixID::UNSPECIFIED:  // Assume BT.709.
+      return kCMFormatDescriptionYCbCrMatrix_ITU_R_709_2;
+
+    case media::VideoColorSpace::MatrixID::BT2020_NCL:
+      if (@available(macos 10.11, *))
+        return kCMFormatDescriptionYCbCrMatrix_ITU_R_2020;
+      DLOG(WARNING) << "kCVImageBufferYCbCrMatrix_ITU_R_2020 "
+                       "unsupported prior to 10.11";
+      return nil;
+
+    case media::VideoColorSpace::MatrixID::FCC:
+    case media::VideoColorSpace::MatrixID::SMPTE170M:
+    case media::VideoColorSpace::MatrixID::BT470BG:
+      // The FCC-based coefficients don't exactly match BT.601, but they're
+      // close enough.
+      return kCMFormatDescriptionYCbCrMatrix_ITU_R_601_4;
+
+    case media::VideoColorSpace::MatrixID::SMPTE240M:
+      return kCMFormatDescriptionYCbCrMatrix_SMPTE_240M_1995;
+
+    default:
+      DLOG(ERROR) << "Unsupported matrix id: " << static_cast<int>(matrix_id);
+      return nil;
+  }
+}
+
+void SetContentLightLevelInfo(const media::HDRMetadata& hdr_metadata,
+                              NSMutableDictionary<NSString*, id>* extensions) {
+  if (@available(macos 10.13, *)) {
+    // This is a SMPTEST2086 Content Light Level Information box.
+    struct ContentLightLevelInfoSEI {
+      uint16_t max_content_light_level;
+      uint16_t max_frame_average_light_level;
+    } __attribute__((packed, aligned(2)));
+    static_assert(sizeof(ContentLightLevelInfoSEI) == 4, "Must be 4 bytes");
+
+    // Values are stored in big-endian...
+    ContentLightLevelInfoSEI sei;
+    sei.max_content_light_level =
+        __builtin_bswap16(hdr_metadata.max_content_light_level);
+    sei.max_frame_average_light_level =
+        __builtin_bswap16(hdr_metadata.max_frame_average_light_level);
+
+    NSData* nsdata_sei = [NSData dataWithBytes:&sei length:4];
+    SetDictionaryValue(extensions,
+                       kCMFormatDescriptionExtension_ContentLightLevelInfo,
+                       nsdata_sei);
+  } else {
+    DLOG(WARNING) << "kCMFormatDescriptionExtension_ContentLightLevelInfo "
+                     "unsupported prior to 10.13";
+  }
+}
+
+void SetMasteringMetadata(const media::HDRMetadata& hdr_metadata,
+                          NSMutableDictionary<NSString*, id>* extensions) {
+  if (@available(macos 10.13, *)) {
+    // This is a SMPTEST2086 Mastering Display Color Volume box.
+    struct MasteringDisplayColorVolumeSEI {
+      vector_ushort2 primaries[3];  // GBR
+      vector_ushort2 white_point;
+      uint32_t luminance_max;
+      uint32_t luminance_min;
+    } __attribute__((packed, aligned(4)));
+    static_assert(sizeof(MasteringDisplayColorVolumeSEI) == 24,
+                  "Must be 24 bytes");
+
+    // Make a copy which we can manipulate.
+    auto md = hdr_metadata.mastering_metadata;
+
+    constexpr float kColorCoordinateUpperBound = 50000.0f;
+    md.primary_r.Scale(kColorCoordinateUpperBound);
+    md.primary_g.Scale(kColorCoordinateUpperBound);
+    md.primary_b.Scale(kColorCoordinateUpperBound);
+    md.white_point.Scale(kColorCoordinateUpperBound);
+
+    constexpr float kUnitOfMasteringLuminance = 10000.0f;
+    md.luminance_max *= kUnitOfMasteringLuminance;
+    md.luminance_min *= kUnitOfMasteringLuminance;
+
+    // Values are stored in big-endian...
+    MasteringDisplayColorVolumeSEI sei;
+    sei.primaries[0].x = __builtin_bswap16(md.primary_g.x() + 0.5f);
+    sei.primaries[0].y = __builtin_bswap16(md.primary_g.y() + 0.5f);
+    sei.primaries[1].x = __builtin_bswap16(md.primary_b.x() + 0.5f);
+    sei.primaries[1].y = __builtin_bswap16(md.primary_b.y() + 0.5f);
+    sei.primaries[2].x = __builtin_bswap16(md.primary_r.x() + 0.5f);
+    sei.primaries[2].y = __builtin_bswap16(md.primary_r.y() + 0.5f);
+    sei.white_point.x = __builtin_bswap16(md.white_point.x() + 0.5f);
+    sei.white_point.y = __builtin_bswap16(md.white_point.y() + 0.5f);
+    sei.luminance_max = __builtin_bswap32(md.luminance_max + 0.5f);
+    sei.luminance_min = __builtin_bswap32(md.luminance_min + 0.5f);
+
+    NSData* nsdata_sei = [NSData dataWithBytes:&sei length:24];
+    SetDictionaryValue(
+        extensions, kCMFormatDescriptionExtension_MasteringDisplayColorVolume,
+        nsdata_sei);
+  } else {
+    DLOG(WARNING) << "kCMFormatDescriptionExtension_"
+                     "MasteringDisplayColorVolume unsupported prior to 10.13";
+  }
+}
+
+// Read the value for the key in |key| to CFString and convert it to IdType.
+// Use the list of pairs in |cfstr_id_pairs| to do the conversion (by doing a
+// linear lookup).
+template <typename IdType, typename StringIdPair>
+bool GetImageBufferProperty(CVImageBufferRef image_buffer,
+                            CFStringRef key,
+                            const std::vector<StringIdPair>& cfstr_id_pairs,
+                            IdType* value_as_id) {
+  CFStringRef value_as_string = reinterpret_cast<CFStringRef>(
+      CVBufferGetAttachment(image_buffer, key, nullptr));
+  if (!value_as_string)
+    return false;
+
+  for (const auto& p : cfstr_id_pairs) {
+    if (!CFStringCompare(value_as_string, p.cfstr, 0)) {
+      *value_as_id = p.id;
+      return true;
+    }
+  }
+
+  return false;
+}
+
+gfx::ColorSpace::PrimaryID GetImageBufferPrimary(
+    CVImageBufferRef image_buffer) {
+  struct CVImagePrimary {
+    const CFStringRef cfstr;
+    const gfx::ColorSpace::PrimaryID id;
+  };
+  static const base::NoDestructor<std::vector<CVImagePrimary>>
+      kSupportedPrimaries([] {
+        std::vector<CVImagePrimary> supported_primaries;
+        supported_primaries.push_back({kCVImageBufferColorPrimaries_ITU_R_709_2,
+                                       gfx::ColorSpace::PrimaryID::BT709});
+        supported_primaries.push_back({kCVImageBufferColorPrimaries_EBU_3213,
+                                       gfx::ColorSpace::PrimaryID::BT470BG});
+        supported_primaries.push_back({kCVImageBufferColorPrimaries_SMPTE_C,
+                                       gfx::ColorSpace::PrimaryID::SMPTE240M});
+        if (@available(macos 10.11, *)) {
+          supported_primaries.push_back(
+              {kCVImageBufferColorPrimaries_ITU_R_2020,
+               gfx::ColorSpace::PrimaryID::BT2020});
+        }
+        return supported_primaries;
+      }());
+
+  // The named primaries. Default to BT709.
+  auto primary_id = gfx::ColorSpace::PrimaryID::BT709;
+  if (!GetImageBufferProperty(image_buffer, kCVImageBufferColorPrimariesKey,
+                              *kSupportedPrimaries, &primary_id)) {
+    DLOG(ERROR) << "Failed to find CVImageBufferRef primaries.";
+  }
+  return primary_id;
+}
+
+gfx::ColorSpace::TransferID GetImageBufferTransferFn(
+    CVImageBufferRef image_buffer,
+    double* gamma) {
+  struct CVImageTransferFn {
+    const CFStringRef cfstr;
+    const gfx::ColorSpace::TransferID id;
+  };
+  static const base::NoDestructor<std::vector<CVImageTransferFn>>
+      kSupportedTransferFuncs([] {
+        std::vector<CVImageTransferFn> supported_transfer_funcs;
+        supported_transfer_funcs.push_back(
+            {kCVImageBufferTransferFunction_ITU_R_709_2,
+             gfx::ColorSpace::TransferID::BT709_APPLE});
+        supported_transfer_funcs.push_back(
+            {kCVImageBufferTransferFunction_SMPTE_240M_1995,
+             gfx::ColorSpace::TransferID::SMPTE240M});
+        supported_transfer_funcs.push_back(
+            {kCVImageBufferTransferFunction_UseGamma,
+             gfx::ColorSpace::TransferID::CUSTOM});
+        if (@available(macos 10.11, *)) {
+          supported_transfer_funcs.push_back(
+              {kCVImageBufferTransferFunction_ITU_R_2020,
+               gfx::ColorSpace::TransferID::BT2020_10});
+        }
+        if (@available(macos 10.12, *)) {
+          supported_transfer_funcs.push_back(
+              {kCVImageBufferTransferFunction_SMPTE_ST_428_1,
+               gfx::ColorSpace::TransferID::SMPTEST428_1});
+        }
+        if (@available(macos 10.13, *)) {
+          supported_transfer_funcs.push_back(
+              {kCVImageBufferTransferFunction_SMPTE_ST_2084_PQ,
+               gfx::ColorSpace::TransferID::SMPTEST2084});
+          supported_transfer_funcs.push_back(
+              {kCVImageBufferTransferFunction_ITU_R_2100_HLG,
+               gfx::ColorSpace::TransferID::ARIB_STD_B67});
+          supported_transfer_funcs.push_back(
+              {kCVImageBufferTransferFunction_sRGB,
+               gfx::ColorSpace::TransferID::IEC61966_2_1});
+        }
+        if (@available(macos 10.14, *)) {
+          supported_transfer_funcs.push_back(
+              {kCVImageBufferTransferFunction_Linear,
+               gfx::ColorSpace::TransferID::LINEAR});
+        }
+
+        return supported_transfer_funcs;
+      }());
+
+  // The named transfer function.
+  auto transfer_id = gfx::ColorSpace::TransferID::BT709;
+  if (!GetImageBufferProperty(image_buffer, kCVImageBufferTransferFunctionKey,
+                              *kSupportedTransferFuncs, &transfer_id)) {
+    DLOG(ERROR) << "Failed to find CVImageBufferRef transfer.";
+  }
+
+  if (transfer_id != gfx::ColorSpace::TransferID::CUSTOM)
+    return transfer_id;
+
+  // If we fail to retrieve the gamma parameter, fall back to BT709.
+  constexpr auto kDefaultTransferFn = gfx::ColorSpace::TransferID::BT709;
+  CFNumberRef gamma_number =
+      reinterpret_cast<CFNumberRef>(CVBufferGetAttachment(
+          image_buffer, kCVImageBufferGammaLevelKey, nullptr));
+  if (!gamma_number) {
+    DLOG(ERROR) << "Failed to get CVImageBufferRef gamma level.";
+    return kDefaultTransferFn;
+  }
+
+  // CGFloat is a double on 64-bit systems.
+  CGFloat gamma_double = 0;
+  if (!CFNumberGetValue(gamma_number, kCFNumberCGFloatType, &gamma_double)) {
+    DLOG(ERROR) << "Failed to get CVImageBufferRef gamma level as float.";
+    return kDefaultTransferFn;
+  }
+
+  if (gamma_double == 2.2)
+    return gfx::ColorSpace::TransferID::GAMMA22;
+  if (gamma_double == 2.8)
+    return gfx::ColorSpace::TransferID::GAMMA28;
+
+  *gamma = gamma_double;
+  return transfer_id;
+}
+
+gfx::ColorSpace::MatrixID GetImageBufferMatrix(CVImageBufferRef image_buffer) {
+  struct CVImageMatrix {
+    const CFStringRef cfstr;
+    gfx::ColorSpace::MatrixID id;
+  };
+  static const base::NoDestructor<std::vector<CVImageMatrix>>
+      kSupportedMatrices([] {
+        std::vector<CVImageMatrix> supported_matrices;
+        supported_matrices.push_back({kCVImageBufferYCbCrMatrix_ITU_R_709_2,
+                                      gfx::ColorSpace::MatrixID::BT709});
+        supported_matrices.push_back({kCVImageBufferYCbCrMatrix_ITU_R_601_4,
+                                      gfx::ColorSpace::MatrixID::SMPTE170M});
+        supported_matrices.push_back({kCVImageBufferYCbCrMatrix_SMPTE_240M_1995,
+                                      gfx::ColorSpace::MatrixID::SMPTE240M});
+        if (@available(macos 10.11, *)) {
+          supported_matrices.push_back({kCVImageBufferYCbCrMatrix_ITU_R_2020,
+                                        gfx::ColorSpace::MatrixID::BT2020_NCL});
+        }
+        return supported_matrices;
+      }());
+
+  auto matrix_id = gfx::ColorSpace::MatrixID::INVALID;
+  if (!GetImageBufferProperty(image_buffer, kCVImageBufferYCbCrMatrixKey,
+                              *kSupportedMatrices, &matrix_id)) {
+    DLOG(ERROR) << "Failed to find CVImageBufferRef YUV matrix.";
+  }
+  return matrix_id;
+}
+
+}  // namespace
+
+namespace media {
+
+CFMutableDictionaryRef CreateFormatExtensions(
+    CMVideoCodecType codec_type,
+    VideoCodecProfile profile,
+    const VideoColorSpace& color_space,
+    base::Optional<HDRMetadata> hdr_metadata) {
+  auto* extensions = [[NSMutableDictionary alloc] init];
+  SetDictionaryValue(extensions, kCMFormatDescriptionExtension_FormatName,
+                     CMVideoCodecTypeToString(codec_type));
+
+  // YCbCr without alpha uses 24. See
+  // http://developer.apple.com/qa/qa2001/qa1183.html
+  SetDictionaryValue(extensions, kCMFormatDescriptionExtension_Depth, @24);
+
+  // Set primaries.
+  SetDictionaryValue(extensions, kCMFormatDescriptionExtension_ColorPrimaries,
+                     GetPrimaries(color_space.primaries));
+
+  // Set transfer function.
+  SetDictionaryValue(extensions, kCMFormatDescriptionExtension_TransferFunction,
+                     GetTransferFunction(color_space.transfer));
+  if (color_space.transfer == VideoColorSpace::TransferID::GAMMA22) {
+    SetDictionaryValue(extensions, kCMFormatDescriptionExtension_GammaLevel,
+                       @2.2);
+  } else if (color_space.transfer == VideoColorSpace::TransferID::GAMMA28) {
+    SetDictionaryValue(extensions, kCMFormatDescriptionExtension_GammaLevel,
+                       @2.8);
+  }
+
+  // Set matrix.
+  SetDictionaryValue(extensions, kCMFormatDescriptionExtension_YCbCrMatrix,
+                     GetMatrix(color_space.matrix));
+
+  // Set full range flag.
+  SetDictionaryValue(extensions, kCMFormatDescriptionExtension_FullRangeVideo,
+                     @(color_space.range == gfx::ColorSpace::RangeID::FULL));
+
+  if (hdr_metadata) {
+    SetContentLightLevelInfo(*hdr_metadata, extensions);
+    SetMasteringMetadata(*hdr_metadata, extensions);
+  }
+
+  return base::mac::NSToCFCast(extensions);
+}
+
+gfx::ColorSpace GetImageBufferColorSpace(CVImageBufferRef image_buffer) {
+  double gamma;
+  auto primary_id = GetImageBufferPrimary(image_buffer);
+  auto matrix_id = GetImageBufferMatrix(image_buffer);
+  auto transfer_id = GetImageBufferTransferFn(image_buffer, &gamma);
+
+  // Use a matrix id that is coherent with a primary id. Useful when we fail to
+  // parse the matrix. Previously it was always defaulting to MatrixID::BT709
+  // See http://crbug.com/788236.
+  if (matrix_id == gfx::ColorSpace::MatrixID::INVALID) {
+    if (primary_id == gfx::ColorSpace::PrimaryID::BT470BG)
+      matrix_id = gfx::ColorSpace::MatrixID::BT470BG;
+    else
+      matrix_id = gfx::ColorSpace::MatrixID::BT709;
+  }
+
+  // It is specified to the decoder to use luma=[16,235] chroma=[16,240] via
+  // the kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange.
+  //
+  // TODO(crbug.com/1103432): We'll probably need support for more than limited
+  // range content if we want this to be used for more than video sites.
+  auto range_id = gfx::ColorSpace::RangeID::LIMITED;
+
+  if (transfer_id == gfx::ColorSpace::TransferID::CUSTOM) {
+    // Transfer functions can also be specified as a gamma value.
+    skcms_TransferFunction custom_tr_fn = {2.2f, 1, 0, 1, 0, 0, 0};
+    if (transfer_id == gfx::ColorSpace::TransferID::CUSTOM)
+      custom_tr_fn.g = gamma;
+
+    return gfx::ColorSpace(primary_id, gfx::ColorSpace::TransferID::CUSTOM,
+                           matrix_id, range_id, nullptr, &custom_tr_fn);
+  }
+
+  return gfx::ColorSpace(primary_id, transfer_id, matrix_id, range_id);
+}
+
+}  // namespace media
diff --git a/media/gpu/mac/vt_config_util_unittest.cc b/media/gpu/mac/vt_config_util_unittest.cc
new file mode 100644
index 0000000..9f728315
--- /dev/null
+++ b/media/gpu/mac/vt_config_util_unittest.cc
@@ -0,0 +1,291 @@
+// 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 "media/gpu/mac/vt_config_util.h"
+
+#include "base/containers/span.h"
+#include "base/mac/foundation_util.h"
+#include "base/strings/sys_string_conversions.h"
+#include "media/formats/mp4/box_definitions.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+std::string GetStrValue(CFMutableDictionaryRef dict, CFStringRef key) {
+  return base::SysCFStringRefToUTF8(
+      base::mac::CFCastStrict<CFStringRef>(CFDictionaryGetValue(dict, key)));
+}
+
+CFStringRef GetCFStrValue(CFMutableDictionaryRef dict, CFStringRef key) {
+  return base::mac::CFCastStrict<CFStringRef>(CFDictionaryGetValue(dict, key));
+}
+
+int GetIntValue(CFMutableDictionaryRef dict, CFStringRef key) {
+  CFNumberRef value =
+      base::mac::CFCastStrict<CFNumberRef>(CFDictionaryGetValue(dict, key));
+  int result;
+  return CFNumberGetValue(value, kCFNumberIntType, &result) ? result : -1;
+}
+
+bool GetBoolValue(CFMutableDictionaryRef dict, CFStringRef key) {
+  return CFBooleanGetValue(
+      base::mac::CFCastStrict<CFBooleanRef>(CFDictionaryGetValue(dict, key)));
+}
+
+base::span<const uint8_t> GetDataValue(CFMutableDictionaryRef dict,
+                                       CFStringRef key) {
+  CFDataRef data =
+      base::mac::CFCastStrict<CFDataRef>(CFDictionaryGetValue(dict, key));
+  return data ? base::span<const uint8_t>(
+                    reinterpret_cast<const uint8_t*>(CFDataGetBytePtr(data)),
+                    CFDataGetLength(data))
+              : base::span<const uint8_t>();
+}
+
+base::ScopedCFTypeRef<CVImageBufferRef> CreateCVImageBuffer(
+    media::VideoColorSpace cs) {
+  base::ScopedCFTypeRef<CFMutableDictionaryRef> fmt(
+      CreateFormatExtensions(kCMVideoCodecType_H264, media::H264PROFILE_MAIN,
+                             cs, media::HDRMetadata()));
+
+  base::ScopedCFTypeRef<CVImageBufferRef> image_buffer;
+  OSStatus err =
+      CVPixelBufferCreate(kCFAllocatorDefault, 16, 16,
+                          kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
+                          nullptr, image_buffer.InitializeInto());
+  if (err != noErr) {
+    EXPECT_EQ(err, noErr);
+    return base::ScopedCFTypeRef<CVImageBufferRef>();
+  }
+
+  CVBufferSetAttachments(image_buffer.get(), fmt,
+                         kCVAttachmentMode_ShouldNotPropagate);
+  return image_buffer;
+}
+
+gfx::ColorSpace ToBT709_APPLE(gfx::ColorSpace cs) {
+  return gfx::ColorSpace(cs.GetPrimaryID(),
+                         gfx::ColorSpace::TransferID::BT709_APPLE,
+                         cs.GetMatrixID(), cs.GetRangeID());
+}
+
+void AssertHasEmptyHDRMetadata(CFMutableDictionaryRef fmt) {
+  if (__builtin_available(macos 10.13, *)) {
+    // We constructed with an empty HDRMetadata, so all values should be zero.
+    auto mdcv = GetDataValue(
+        fmt, kCMFormatDescriptionExtension_MasteringDisplayColorVolume);
+    ASSERT_EQ(24u, mdcv.size());
+    for (size_t i = 0; i < mdcv.size(); ++i)
+      EXPECT_EQ(0u, mdcv[i]);
+
+    auto clli =
+        GetDataValue(fmt, kCMFormatDescriptionExtension_ContentLightLevelInfo);
+    ASSERT_EQ(4u, clli.size());
+    for (size_t i = 0; i < clli.size(); ++i)
+      EXPECT_EQ(0u, clli[i]);
+  }
+}
+
+}  // namespace
+
+namespace media {
+
+TEST(VTConfigUtil, CreateFormatExtensions_H264_BT709) {
+  base::ScopedCFTypeRef<CFMutableDictionaryRef> fmt(
+      CreateFormatExtensions(kCMVideoCodecType_H264, H264PROFILE_MAIN,
+                             VideoColorSpace::REC709(), base::nullopt));
+  EXPECT_EQ("avc1", GetStrValue(fmt, kCMFormatDescriptionExtension_FormatName));
+  EXPECT_EQ(24, GetIntValue(fmt, kCMFormatDescriptionExtension_Depth));
+  EXPECT_EQ(kCMFormatDescriptionColorPrimaries_ITU_R_709_2,
+            GetCFStrValue(fmt, kCMFormatDescriptionExtension_ColorPrimaries));
+  EXPECT_EQ(kCMFormatDescriptionTransferFunction_ITU_R_709_2,
+            GetCFStrValue(fmt, kCMFormatDescriptionExtension_TransferFunction));
+  EXPECT_EQ(kCMFormatDescriptionYCbCrMatrix_ITU_R_709_2,
+            GetCFStrValue(fmt, kCMFormatDescriptionExtension_YCbCrMatrix));
+  EXPECT_FALSE(GetBoolValue(fmt, kCMFormatDescriptionExtension_FullRangeVideo));
+
+  if (__builtin_available(macos 10.13, *)) {
+    EXPECT_TRUE(
+        GetDataValue(fmt,
+                     kCMFormatDescriptionExtension_MasteringDisplayColorVolume)
+            .empty());
+    EXPECT_TRUE(
+        GetDataValue(fmt, kCMFormatDescriptionExtension_ContentLightLevelInfo)
+            .empty());
+  }
+}
+
+TEST(VTConfigUtil, CreateFormatExtensions_H264_BT2020_PQ) {
+  base::ScopedCFTypeRef<CFMutableDictionaryRef> fmt(CreateFormatExtensions(
+      kCMVideoCodecType_H264, H264PROFILE_MAIN,
+      VideoColorSpace(VideoColorSpace::PrimaryID::BT2020,
+                      VideoColorSpace::TransferID::SMPTEST2084,
+                      VideoColorSpace::MatrixID::BT2020_NCL,
+                      gfx::ColorSpace::RangeID::FULL),
+      HDRMetadata()));
+  EXPECT_EQ("avc1", GetStrValue(fmt, kCMFormatDescriptionExtension_FormatName));
+  EXPECT_EQ(24, GetIntValue(fmt, kCMFormatDescriptionExtension_Depth));
+
+  if (__builtin_available(macos 10.13, *)) {
+    EXPECT_EQ(kCMFormatDescriptionColorPrimaries_ITU_R_2020,
+              GetCFStrValue(fmt, kCMFormatDescriptionExtension_ColorPrimaries));
+    EXPECT_EQ(
+        kCMFormatDescriptionTransferFunction_SMPTE_ST_2084_PQ,
+        GetCFStrValue(fmt, kCMFormatDescriptionExtension_TransferFunction));
+    EXPECT_EQ(kCMFormatDescriptionYCbCrMatrix_ITU_R_2020,
+              GetCFStrValue(fmt, kCMFormatDescriptionExtension_YCbCrMatrix));
+  }
+  EXPECT_TRUE(GetBoolValue(fmt, kCMFormatDescriptionExtension_FullRangeVideo));
+  AssertHasEmptyHDRMetadata(fmt);
+}
+
+TEST(VTConfigUtil, CreateFormatExtensions_H264_BT2020_HLG) {
+  base::ScopedCFTypeRef<CFMutableDictionaryRef> fmt(CreateFormatExtensions(
+      kCMVideoCodecType_H264, H264PROFILE_MAIN,
+      VideoColorSpace(VideoColorSpace::PrimaryID::BT2020,
+                      VideoColorSpace::TransferID::ARIB_STD_B67,
+                      VideoColorSpace::MatrixID::BT2020_NCL,
+                      gfx::ColorSpace::RangeID::FULL),
+      HDRMetadata()));
+  EXPECT_EQ("avc1", GetStrValue(fmt, kCMFormatDescriptionExtension_FormatName));
+  EXPECT_EQ(24, GetIntValue(fmt, kCMFormatDescriptionExtension_Depth));
+
+  if (__builtin_available(macos 10.13, *)) {
+    EXPECT_EQ(kCMFormatDescriptionColorPrimaries_ITU_R_2020,
+              GetCFStrValue(fmt, kCMFormatDescriptionExtension_ColorPrimaries));
+    EXPECT_EQ(
+        kCMFormatDescriptionTransferFunction_ITU_R_2100_HLG,
+        GetCFStrValue(fmt, kCMFormatDescriptionExtension_TransferFunction));
+    EXPECT_EQ(kCMFormatDescriptionYCbCrMatrix_ITU_R_2020,
+              GetCFStrValue(fmt, kCMFormatDescriptionExtension_YCbCrMatrix));
+  }
+  EXPECT_TRUE(GetBoolValue(fmt, kCMFormatDescriptionExtension_FullRangeVideo));
+  AssertHasEmptyHDRMetadata(fmt);
+}
+
+TEST(VTConfigUtil, CreateFormatExtensions_HDRMetadata) {
+  // Values from real YouTube HDR content.
+  HDRMetadata hdr_meta;
+  hdr_meta.max_content_light_level = 1000;
+  hdr_meta.max_frame_average_light_level = 600;
+  auto& mastering = hdr_meta.mastering_metadata;
+  mastering.luminance_min = 0;
+  mastering.luminance_max = 1000;
+  mastering.primary_r = gfx::PointF(0.68, 0.32);
+  mastering.primary_g = gfx::PointF(0.2649, 0.69);
+  mastering.primary_b = gfx::PointF(0.15, 0.06);
+  mastering.white_point = gfx::PointF(0.3127, 0.3290);
+
+  base::ScopedCFTypeRef<CFMutableDictionaryRef> fmt(CreateFormatExtensions(
+      kCMVideoCodecType_H264, H264PROFILE_MAIN,
+      VideoColorSpace(VideoColorSpace::PrimaryID::BT2020,
+                      VideoColorSpace::TransferID::SMPTEST2084,
+                      VideoColorSpace::MatrixID::BT2020_NCL,
+                      gfx::ColorSpace::RangeID::FULL),
+      hdr_meta));
+  if (__builtin_available(macos 10.13, *)) {
+    {
+      auto mdcv = GetDataValue(
+          fmt, kCMFormatDescriptionExtension_MasteringDisplayColorVolume);
+      ASSERT_EQ(24u, mdcv.size());
+      std::unique_ptr<mp4::BoxReader> box_reader(
+          mp4::BoxReader::ReadConcatentatedBoxes(mdcv.data(), mdcv.size(),
+                                                 nullptr));
+      mp4::MasteringDisplayColorVolume mdcv_box;
+      ASSERT_TRUE(mdcv_box.Parse(box_reader.get()));
+      EXPECT_EQ(mdcv_box.display_primaries_gx, mastering.primary_g.x());
+      EXPECT_EQ(mdcv_box.display_primaries_gy, mastering.primary_g.y());
+      EXPECT_EQ(mdcv_box.display_primaries_bx, mastering.primary_b.x());
+      EXPECT_EQ(mdcv_box.display_primaries_by, mastering.primary_b.y());
+      EXPECT_EQ(mdcv_box.display_primaries_rx, mastering.primary_r.x());
+      EXPECT_EQ(mdcv_box.display_primaries_ry, mastering.primary_r.y());
+      EXPECT_EQ(mdcv_box.white_point_x, mastering.white_point.x());
+      EXPECT_EQ(mdcv_box.white_point_y, mastering.white_point.y());
+      EXPECT_EQ(mdcv_box.max_display_mastering_luminance,
+                mastering.luminance_max);
+      EXPECT_EQ(mdcv_box.min_display_mastering_luminance,
+                mastering.luminance_min);
+    }
+
+    {
+      auto clli = GetDataValue(
+          fmt, kCMFormatDescriptionExtension_ContentLightLevelInfo);
+      ASSERT_EQ(4u, clli.size());
+      std::unique_ptr<mp4::BoxReader> box_reader(
+          mp4::BoxReader::ReadConcatentatedBoxes(clli.data(), clli.size(),
+                                                 nullptr));
+      mp4::ContentLightLevelInformation clli_box;
+      ASSERT_TRUE(clli_box.Parse(box_reader.get()));
+      EXPECT_EQ(clli_box.max_content_light_level,
+                hdr_meta.max_content_light_level);
+      EXPECT_EQ(clli_box.max_pic_average_light_level,
+                hdr_meta.max_frame_average_light_level);
+    }
+  }
+}
+
+TEST(VTConfigUtil, GetImageBufferColorSpace_BT601) {
+  auto cs = VideoColorSpace::REC601();
+  auto image_buffer = CreateCVImageBuffer(cs);
+  ASSERT_TRUE(image_buffer);
+
+  // macOS doesn't have a SMPTE170M transfer function, apps are supposed to use
+  // kCMFormatDescriptionTransferFunction_ITU_R_709_2 instead for SDR content.
+  cs.primaries = VideoColorSpace::PrimaryID::SMPTE240M;
+  auto expected_cs = ToBT709_APPLE(cs.ToGfxColorSpace());
+  EXPECT_EQ(expected_cs, GetImageBufferColorSpace(image_buffer));
+}
+
+TEST(VTConfigUtil, GetImageBufferColorSpace_BT709) {
+  auto cs = VideoColorSpace::REC709();
+  auto image_buffer = CreateCVImageBuffer(cs);
+  ASSERT_TRUE(image_buffer);
+
+  // macOS returns a special BT709_APPLE transfer function since it doesn't use
+  // the same gamma level as is standardized.
+  auto expected_cs = ToBT709_APPLE(cs.ToGfxColorSpace());
+  EXPECT_EQ(expected_cs, GetImageBufferColorSpace(image_buffer));
+}
+
+TEST(VTConfigUtil, GetImageBufferColorSpace_GAMMA22) {
+  auto cs = VideoColorSpace(VideoColorSpace::PrimaryID::SMPTE240M,
+                            VideoColorSpace::TransferID::GAMMA22,
+                            VideoColorSpace::MatrixID::SMPTE240M,
+                            gfx::ColorSpace::RangeID::LIMITED);
+  auto image_buffer = CreateCVImageBuffer(cs);
+  ASSERT_TRUE(image_buffer);
+  EXPECT_EQ(cs.ToGfxColorSpace(), GetImageBufferColorSpace(image_buffer));
+}
+
+TEST(VTConfigUtil, GetImageBufferColorSpace_GAMMA28) {
+  auto cs = VideoColorSpace(VideoColorSpace::PrimaryID::SMPTE240M,
+                            VideoColorSpace::TransferID::GAMMA28,
+                            VideoColorSpace::MatrixID::SMPTE240M,
+                            gfx::ColorSpace::RangeID::LIMITED);
+  auto image_buffer = CreateCVImageBuffer(cs);
+  ASSERT_TRUE(image_buffer);
+  EXPECT_EQ(cs.ToGfxColorSpace(), GetImageBufferColorSpace(image_buffer));
+}
+
+TEST(VTConfigUtil, GetImageBufferColorSpace_BT2020_PQ) {
+  auto cs = VideoColorSpace(VideoColorSpace::PrimaryID::BT2020,
+                            VideoColorSpace::TransferID::SMPTEST2084,
+                            VideoColorSpace::MatrixID::BT2020_NCL,
+                            gfx::ColorSpace::RangeID::LIMITED);
+  auto image_buffer = CreateCVImageBuffer(cs);
+  ASSERT_TRUE(image_buffer);
+  EXPECT_EQ(cs.ToGfxColorSpace(), GetImageBufferColorSpace(image_buffer));
+}
+
+TEST(VTConfigUtil, GetImageBufferColorSpace_BT2020_HLG) {
+  auto cs = VideoColorSpace(VideoColorSpace::PrimaryID::BT2020,
+                            VideoColorSpace::TransferID::ARIB_STD_B67,
+                            VideoColorSpace::MatrixID::BT2020_NCL,
+                            gfx::ColorSpace::RangeID::LIMITED);
+  auto image_buffer = CreateCVImageBuffer(cs);
+  ASSERT_TRUE(image_buffer);
+  EXPECT_EQ(cs.ToGfxColorSpace(), GetImageBufferColorSpace(image_buffer));
+}
+
+}  // namespace media
diff --git a/media/gpu/mac/vt_video_decode_accelerator_mac.cc b/media/gpu/mac/vt_video_decode_accelerator_mac.cc
index fe2a8cb..7d51dbe9 100644
--- a/media/gpu/mac/vt_video_decode_accelerator_mac.cc
+++ b/media/gpu/mac/vt_video_decode_accelerator_mac.cc
@@ -18,6 +18,7 @@
 #include "base/bind.h"
 #include "base/logging.h"
 #include "base/mac/mac_logging.h"
+#include "base/mac/mac_util.h"
 #include "base/memory/ptr_util.h"
 #include "base/metrics/histogram_macros.h"
 #include "base/stl_util.h"
@@ -32,6 +33,8 @@
 #include "base/version.h"
 #include "components/crash/core/common/crash_key.h"
 #include "media/base/limits.h"
+#include "media/base/media_switches.h"
+#include "media/gpu/mac/vt_config_util.h"
 #include "ui/gfx/geometry/rect.h"
 #include "ui/gl/gl_context.h"
 #include "ui/gl/gl_image_io_surface.h"
@@ -110,6 +113,9 @@
 
   // Note that 4:2:0 textures cannot be used directly as RGBA in OpenGL, but are
   // lower power than 4:2:2 when composited directly by CoreAnimation.
+  //
+  // TODO(crbug.com/1103432): Based on other > 8-bit codecs, we'll likely need
+  // to add kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange here.
   int32_t pixel_format = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange;
 #define CFINT(i) CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &i)
   base::ScopedCFTypeRef<CFNumberRef> cf_pixel_format(CFINT(pixel_format));
@@ -297,155 +303,6 @@
   vda->Output(source_frame_refcon, status, image_buffer);
 }
 
-// Read the value for the key in |key| to CFString and convert it to IdType.
-// Use the list of pairs in |cfstr_id_pairs| to do the conversion (by doing a
-// linear lookup).
-template <typename IdType, typename StringIdPair>
-bool GetImageBufferProperty(CVImageBufferRef image_buffer,
-                            CFStringRef key,
-                            const StringIdPair* cfstr_id_pairs,
-                            size_t cfstr_id_pairs_size,
-                            IdType* value_as_id) {
-  CFStringRef value_as_string = reinterpret_cast<CFStringRef>(
-      CVBufferGetAttachment(image_buffer, key, nullptr));
-  if (!value_as_string)
-    return false;
-
-  for (size_t i = 0; i < cfstr_id_pairs_size; ++i) {
-    if (!CFStringCompare(value_as_string, cfstr_id_pairs[i].cfstr, 0)) {
-      *value_as_id = cfstr_id_pairs[i].id;
-      return true;
-    }
-  }
-
-  return false;
-}
-
-// Use a matrix id that is coherent with a primary id. Useful when we fail to
-// parse the matrix. Previously it was always defaulting to MatrixID::BT709
-// See http://crbug.com/788236.
-gfx::ColorSpace::MatrixID GetDefaultMatrixID(
-    const gfx::ColorSpace::PrimaryID primary_id) {
-  gfx::ColorSpace::MatrixID matrix_id = gfx::ColorSpace::MatrixID::BT709;
-
-  switch (primary_id) {
-    case gfx::ColorSpace::PrimaryID::BT709:
-      matrix_id = gfx::ColorSpace::MatrixID::BT709;
-      break;
-    case gfx::ColorSpace::PrimaryID::BT470BG:
-      matrix_id = gfx::ColorSpace::MatrixID::BT470BG;
-      break;
-    default:
-      break;
-  }
-
-  return matrix_id;
-}
-
-gfx::ColorSpace GetImageBufferColorSpace(CVImageBufferRef image_buffer) {
-  // The named primaries. Default to BT709.
-  gfx::ColorSpace::PrimaryID primary_id = gfx::ColorSpace::PrimaryID::BT709;
-  struct {
-    const CFStringRef cfstr;
-    const gfx::ColorSpace::PrimaryID id;
-  } primaries[] = {
-      {
-          kCVImageBufferColorPrimaries_ITU_R_709_2,
-          gfx::ColorSpace::PrimaryID::BT709,
-      },
-      {
-          kCVImageBufferColorPrimaries_EBU_3213,
-          gfx::ColorSpace::PrimaryID::BT470BG,
-      },
-      {
-          kCVImageBufferColorPrimaries_SMPTE_C,
-          gfx::ColorSpace::PrimaryID::SMPTE240M,
-      },
-  };
-  if (!GetImageBufferProperty(image_buffer, kCVImageBufferColorPrimariesKey,
-                              primaries, base::size(primaries), &primary_id)) {
-    DLOG(ERROR) << "Failed to find CVImageBufferRef primaries.";
-  }
-
-  // The named transfer function.
-  gfx::ColorSpace::TransferID transfer_id = gfx::ColorSpace::TransferID::BT709;
-  skcms_TransferFunction custom_tr_fn = {2.2f, 1, 0, 1, 0, 0, 0};
-  struct {
-    const CFStringRef cfstr;
-    gfx::ColorSpace::TransferID id;
-  } transfers[] = {
-      {
-          kCVImageBufferTransferFunction_ITU_R_709_2,
-          gfx::ColorSpace::TransferID::BT709_APPLE,
-      },
-      {
-          kCVImageBufferTransferFunction_SMPTE_240M_1995,
-          gfx::ColorSpace::TransferID::SMPTE240M,
-      },
-      {
-          kCVImageBufferTransferFunction_UseGamma,
-          gfx::ColorSpace::TransferID::CUSTOM,
-      },
-  };
-  if (!GetImageBufferProperty(image_buffer, kCVImageBufferTransferFunctionKey,
-                              transfers, base::size(transfers), &transfer_id)) {
-    DLOG(ERROR) << "Failed to find CVImageBufferRef transfer.";
-  }
-
-  // Transfer functions can also be specified as a gamma value.
-  if (transfer_id == gfx::ColorSpace::TransferID::CUSTOM) {
-    // If we fail to find the custom transfer function parameters, fall back to
-    // BT709.
-    transfer_id = gfx::ColorSpace::TransferID::BT709;
-    CFNumberRef gamma_number =
-        reinterpret_cast<CFNumberRef>(CVBufferGetAttachment(
-            image_buffer, kCVImageBufferGammaLevelKey, nullptr));
-    if (gamma_number) {
-      CGFloat gamma_float = 0;
-      if (CFNumberGetValue(gamma_number, kCFNumberCGFloatType, &gamma_float)) {
-        transfer_id = gfx::ColorSpace::TransferID::CUSTOM;
-        custom_tr_fn.g = gamma_float;
-      } else {
-        DLOG(ERROR) << "Failed to get CVImageBufferRef gamma level as float.";
-      }
-    } else {
-      DLOG(ERROR) << "Failed to get CVImageBufferRef gamma level.";
-    }
-  }
-
-  // Read the RGB to YUV matrix ID.
-  gfx::ColorSpace::MatrixID matrix_id = GetDefaultMatrixID(primary_id);
-  struct {
-    const CFStringRef cfstr;
-    gfx::ColorSpace::MatrixID id;
-  } matrices[] = {{
-                      kCVImageBufferYCbCrMatrix_ITU_R_709_2,
-                      gfx::ColorSpace::MatrixID::BT709,
-                  },
-                  {
-                      kCVImageBufferYCbCrMatrix_ITU_R_601_4,
-                      gfx::ColorSpace::MatrixID::SMPTE170M,
-                  },
-                  {
-                      kCVImageBufferYCbCrMatrix_SMPTE_240M_1995,
-                      gfx::ColorSpace::MatrixID::SMPTE240M,
-                  }};
-  if (!GetImageBufferProperty(image_buffer, kCVImageBufferYCbCrMatrixKey,
-                              matrices, base::size(matrices), &matrix_id)) {
-    DLOG(ERROR) << "Failed to find CVImageBufferRef YUV matrix.";
-  }
-
-  // It is specified to the decoder to use luma=[16,235] chroma=[16,240] via
-  // the kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange.
-  gfx::ColorSpace::RangeID range_id = gfx::ColorSpace::RangeID::LIMITED;
-
-  if (transfer_id == gfx::ColorSpace::TransferID::CUSTOM) {
-    return gfx::ColorSpace(primary_id, gfx::ColorSpace::TransferID::CUSTOM,
-                           matrix_id, range_id, nullptr, &custom_tr_fn);
-  }
-  return gfx::ColorSpace(primary_id, transfer_id, matrix_id, range_id);
-}
-
 }  // namespace
 
 bool InitializeVideoToolbox() {
@@ -615,6 +472,7 @@
   client_ = client;
 
   // Spawn a thread to handle parsing and calling VideoToolbox.
+  // TODO(sandersd): This should probably use a base::ThreadPool thread instead.
   if (!decoder_thread_.Start()) {
     DLOG(ERROR) << "Failed to start decoder thread";
     return false;
diff --git a/media/gpu/test/fake_command_buffer_helper.cc b/media/gpu/test/fake_command_buffer_helper.cc
index 4e7a92cb..2d282ee 100644
--- a/media/gpu/test/fake_command_buffer_helper.cc
+++ b/media/gpu/test/fake_command_buffer_helper.cc
@@ -3,6 +3,8 @@
 // found in the LICENSE file.
 
 #include "media/gpu/test/fake_command_buffer_helper.h"
+#include "gpu/command_buffer/service/shared_image_backing.h"
+#include "gpu/command_buffer/service/shared_image_representation.h"
 
 #include "base/logging.h"
 
@@ -75,6 +77,21 @@
   return is_context_current_;
 }
 
+std::unique_ptr<gpu::SharedImageRepresentationFactoryRef>
+FakeCommandBufferHelper::Register(
+    std::unique_ptr<gpu::SharedImageBacking> backing) {
+  DVLOG(2) << __func__;
+  DCHECK(task_runner_->BelongsToCurrentThread());
+  return nullptr;
+}
+
+gpu::TextureBase* FakeCommandBufferHelper::GetTexture(GLuint service_id) const {
+  DVLOG(2) << __func__ << "(" << service_id << ")";
+  DCHECK(task_runner_->BelongsToCurrentThread());
+  DCHECK(service_ids_.count(service_id));
+  return nullptr;
+}
+
 GLuint FakeCommandBufferHelper::CreateTexture(GLenum target,
                                               GLenum internal_format,
                                               GLsizei width,
diff --git a/media/gpu/test/fake_command_buffer_helper.h b/media/gpu/test/fake_command_buffer_helper.h
index e136974..1e019ce1 100644
--- a/media/gpu/test/fake_command_buffer_helper.h
+++ b/media/gpu/test/fake_command_buffer_helper.h
@@ -41,6 +41,9 @@
   gl::GLContext* GetGLContext() override;
   bool HasStub() override;
   bool MakeContextCurrent() override;
+  std::unique_ptr<gpu::SharedImageRepresentationFactoryRef> Register(
+      std::unique_ptr<gpu::SharedImageBacking> backing) override;
+  gpu::TextureBase* GetTexture(GLuint service_id) const override;
   GLuint CreateTexture(GLenum target,
                        GLenum internal_format,
                        GLsizei width,
diff --git a/media/gpu/windows/d3d11_copying_texture_wrapper.cc b/media/gpu/windows/d3d11_copying_texture_wrapper.cc
index 9157fe8..cbe44cf 100644
--- a/media/gpu/windows/d3d11_copying_texture_wrapper.cc
+++ b/media/gpu/windows/d3d11_copying_texture_wrapper.cc
@@ -30,8 +30,6 @@
 CopyingTexture2DWrapper::~CopyingTexture2DWrapper() = default;
 
 Status CopyingTexture2DWrapper::ProcessTexture(
-    ComD3D11Texture2D texture,
-    size_t array_slice,
     const gfx::ColorSpace& input_color_space,
     MailboxHolderArray* mailbox_dest,
     gfx::ColorSpace* output_color_space) {
@@ -48,11 +46,11 @@
 
   D3D11_VIDEO_PROCESSOR_INPUT_VIEW_DESC input_view_desc = {0};
   input_view_desc.ViewDimension = D3D11_VPIV_DIMENSION_TEXTURE2D;
-  input_view_desc.Texture2D.ArraySlice = array_slice;
+  input_view_desc.Texture2D.ArraySlice = array_slice_;
   input_view_desc.Texture2D.MipSlice = 0;
   ComD3D11VideoProcessorInputView input_view;
   hr = video_processor_->CreateVideoProcessorInputView(
-      texture.Get(), &input_view_desc, &input_view);
+      texture_.Get(), &input_view_desc, &input_view);
   if (!SUCCEEDED(hr)) {
     return Status(StatusCode::kCreateVideoProcessorInputViewFailed)
         .AddCause(HresultToStatus(hr));
@@ -85,19 +83,27 @@
         .AddCause(HresultToStatus(hr));
   }
 
-  return output_texture_wrapper_->ProcessTexture(
-      output_texture_, 0, copy_color_space, mailbox_dest, output_color_space);
+  return output_texture_wrapper_->ProcessTexture(copy_color_space, mailbox_dest,
+                                                 output_color_space);
 }
 
 Status CopyingTexture2DWrapper::Init(
     scoped_refptr<base::SingleThreadTaskRunner> gpu_task_runner,
-    GetCommandBufferHelperCB get_helper_cb) {
+    GetCommandBufferHelperCB get_helper_cb,
+    ComD3D11Texture2D texture,
+    size_t array_slice) {
   auto result = video_processor_->Init(size_.width(), size_.height());
   if (!result.is_ok())
     return std::move(result).AddHere();
 
+  // Remember the texture + array_slice so later, ProcessTexture can still use
+  // it.
+  texture_ = texture;
+  array_slice_ = array_slice;
+
   return output_texture_wrapper_->Init(std::move(gpu_task_runner),
-                                       std::move(get_helper_cb));
+                                       std::move(get_helper_cb),
+                                       output_texture_, /*array_slice=*/0);
 }
 
 void CopyingTexture2DWrapper::SetStreamHDRMetadata(
diff --git a/media/gpu/windows/d3d11_copying_texture_wrapper.h b/media/gpu/windows/d3d11_copying_texture_wrapper.h
index e62d1c7..1583f8b 100644
--- a/media/gpu/windows/d3d11_copying_texture_wrapper.h
+++ b/media/gpu/windows/d3d11_copying_texture_wrapper.h
@@ -31,14 +31,14 @@
                           base::Optional<gfx::ColorSpace> output_color_space);
   ~CopyingTexture2DWrapper() override;
 
-  Status ProcessTexture(ComD3D11Texture2D texture,
-                        size_t array_slice,
-                        const gfx::ColorSpace& input_color_space,
+  Status ProcessTexture(const gfx::ColorSpace& input_color_space,
                         MailboxHolderArray* mailbox_dest,
                         gfx::ColorSpace* output_color_space) override;
 
   Status Init(scoped_refptr<base::SingleThreadTaskRunner> gpu_task_runner,
-              GetCommandBufferHelperCB get_helper_cb) override;
+              GetCommandBufferHelperCB get_helper_cb,
+              ComD3D11Texture2D texture,
+              size_t array_slice) override;
 
   void SetStreamHDRMetadata(const HDRMetadata& stream_metadata) override;
   void SetDisplayHDRMetadata(
@@ -54,6 +54,9 @@
 
   // If set, this is the color space that we last saw in ProcessTexture.
   base::Optional<gfx::ColorSpace> previous_input_color_space_;
+
+  ComD3D11Texture2D texture_;
+  size_t array_slice_ = 0;
 };
 
 }  // namespace media
diff --git a/media/gpu/windows/d3d11_copying_texture_wrapper_unittest.cc b/media/gpu/windows/d3d11_copying_texture_wrapper_unittest.cc
index 9677dcc6..e6058c9 100644
--- a/media/gpu/windows/d3d11_copying_texture_wrapper_unittest.cc
+++ b/media/gpu/windows/d3d11_copying_texture_wrapper_unittest.cc
@@ -86,9 +86,7 @@
  public:
   MockTexture2DWrapper() {}
 
-  Status ProcessTexture(ComD3D11Texture2D texture,
-                        size_t array_slice,
-                        const gfx::ColorSpace& input_color_space,
+  Status ProcessTexture(const gfx::ColorSpace& input_color_space,
                         MailboxHolderArray* mailbox_dest,
                         gfx::ColorSpace* output_color_space) override {
     // Pretend we created an arbitrary color space, so that we're sure that it
@@ -98,7 +96,9 @@
   }
 
   Status Init(scoped_refptr<base::SingleThreadTaskRunner> gpu_task_runner,
-              GetCommandBufferHelperCB get_helper_cb) override {
+              GetCommandBufferHelperCB get_helper_cb,
+              ComD3D11Texture2D in_texture,
+              size_t array_slice) override {
     gpu_task_runner_ = std::move(gpu_task_runner);
     return MockInit();
   }
@@ -222,16 +222,19 @@
   MailboxHolderArray mailboxes;
   gfx::ColorSpace input_color_space = gfx::ColorSpace::CreateSCRGBLinear();
   gfx::ColorSpace output_color_space;
-  EXPECT_EQ(wrapper->Init(gpu_task_runner_, CreateMockHelperCB()).is_ok(),
+  EXPECT_EQ(wrapper
+                ->Init(gpu_task_runner_, CreateMockHelperCB(),
+                       /*texture_d3d=*/nullptr, /*array_slice=*/0)
+                .is_ok(),
             InitSucceeds());
   task_environment_.RunUntilIdle();
   if (GetProcessorProxyInit())
     EXPECT_EQ(texture_wrapper_raw->gpu_task_runner_, gpu_task_runner_);
-  EXPECT_EQ(wrapper
-                ->ProcessTexture(nullptr, 0, input_color_space, &mailboxes,
-                                 &output_color_space)
-                .is_ok(),
-            ProcessTextureSucceeds());
+  EXPECT_EQ(
+      wrapper
+          ->ProcessTexture(input_color_space, &mailboxes, &output_color_space)
+          .is_ok(),
+      ProcessTextureSucceeds());
 
   if (ProcessTextureSucceeds()) {
     // Regardless of what the input space is, the output should be provided by
diff --git a/media/gpu/windows/d3d11_picture_buffer.cc b/media/gpu/windows/d3d11_picture_buffer.cc
index ee56553..aeb0d40 100644
--- a/media/gpu/windows/d3d11_picture_buffer.cc
+++ b/media/gpu/windows/d3d11_picture_buffer.cc
@@ -52,8 +52,9 @@
   view_desc.ViewDimension = D3D11_VDOV_DIMENSION_TEXTURE2D;
   view_desc.Texture2D.ArraySlice = array_slice_;
 
-  Status result = texture_wrapper_->Init(std::move(gpu_task_runner),
-                                         std::move(get_helper_cb));
+  Status result =
+      texture_wrapper_->Init(std::move(gpu_task_runner),
+                             std::move(get_helper_cb), texture_, array_slice_);
   if (!result.is_ok()) {
     MEDIA_LOG(ERROR, media_log) << "Failed to Initialize the wrapper";
     return result;
@@ -75,8 +76,7 @@
     const gfx::ColorSpace& input_color_space,
     MailboxHolderArray* mailbox_dest,
     gfx::ColorSpace* output_color_space) {
-  return texture_wrapper_->ProcessTexture(Texture(), array_slice_,
-                                          input_color_space, mailbox_dest,
+  return texture_wrapper_->ProcessTexture(input_color_space, mailbox_dest,
                                           output_color_space);
 }
 
diff --git a/media/gpu/windows/d3d11_texture_wrapper.cc b/media/gpu/windows/d3d11_texture_wrapper.cc
index 80612a2f..fccbc0a 100644
--- a/media/gpu/windows/d3d11_texture_wrapper.cc
+++ b/media/gpu/windows/d3d11_texture_wrapper.cc
@@ -9,13 +9,38 @@
 #include <utility>
 #include <vector>
 
+#include "components/viz/common/resources/resource_format_utils.h"
+#include "gpu/command_buffer/common/shared_image_usage.h"
 #include "gpu/command_buffer/service/mailbox_manager.h"
+#include "gpu/command_buffer/service/shared_image_backing_d3d.h"
 #include "media/base/bind_to_current_loop.h"
 #include "media/base/win/mf_helpers.h"
+#include "mojo/public/cpp/bindings/callback_helpers.h"
 #include "ui/gl/gl_image.h"
 
 namespace media {
 
+namespace {
+
+base::Optional<viz::ResourceFormat> DXGIFormatToVizFormat(
+    DXGI_FORMAT dxgi_format) {
+  switch (dxgi_format) {
+    case DXGI_FORMAT_NV12:
+      return viz::YUV_420_BIPLANAR;
+    case DXGI_FORMAT_P010:
+      return viz::P010;
+    case DXGI_FORMAT_B8G8R8A8_UNORM:
+      return viz::BGRA_8888;
+    case DXGI_FORMAT_R16G16B16A16_FLOAT:
+      return viz::RGBA_F16;
+    default:
+      NOTREACHED();
+      return {};
+  }
+}
+
+}  // anonymous namespace
+
 // Handy structure so that we can activate / bind one or two textures.
 struct ScopedTextureEverything {
   ScopedTextureEverything(GLenum unit, GLuint service_id)
@@ -63,8 +88,6 @@
 DefaultTexture2DWrapper::~DefaultTexture2DWrapper() = default;
 
 Status DefaultTexture2DWrapper::ProcessTexture(
-    ComD3D11Texture2D texture,
-    size_t array_slice,
     const gfx::ColorSpace& input_color_space,
     MailboxHolderArray* mailbox_dest,
     gfx::ColorSpace* output_color_space) {
@@ -75,14 +98,6 @@
     return Status(StatusCode::kProcessTextureFailed)
         .AddCause(std::move(*received_error_));
 
-  // Temporary check to track down https://crbug.com/1077645
-  CHECK(texture);
-
-  // It's okay to post and forget this call, since it'll be ordered correctly
-  // with respect to any access on the gpu main thread.
-  gpu_resources_.Post(FROM_HERE, &GpuResources::PushNewTexture,
-                      std::move(texture), array_slice);
-
   // TODO(liberato): make sure that |mailbox_holders_| is zero-initialized in
   // case we don't use all the planes.
   for (size_t i = 0; i < VideoFrame::kMaxPlanes; i++)
@@ -96,7 +111,9 @@
 
 Status DefaultTexture2DWrapper::Init(
     scoped_refptr<base::SingleThreadTaskRunner> gpu_task_runner,
-    GetCommandBufferHelperCB get_helper_cb) {
+    GetCommandBufferHelperCB get_helper_cb,
+    ComD3D11Texture2D texture,
+    size_t array_slice) {
   gpu_resources_ = base::SequenceBound<GpuResources>(
       std::move(gpu_task_runner),
       BindToCurrentLoop(base::BindOnce(&DefaultTexture2DWrapper::OnError,
@@ -123,11 +140,18 @@
   // The current implementation is.
   std::vector<gpu::Mailbox> mailboxes;
   for (int texture_idx = 0; texture_idx < textures_per_picture; texture_idx++) {
-    mailboxes.push_back(gpu::Mailbox::Generate());
+    mailboxes.push_back(gpu::Mailbox::GenerateForSharedImage());
     mailbox_holders_[texture_idx] = gpu::MailboxHolder(
         mailboxes[texture_idx], gpu::SyncToken(), GL_TEXTURE_EXTERNAL_OES);
   }
 
+  const base::Optional<viz::ResourceFormat> viz_format =
+      DXGIFormatToVizFormat(dxgi_format_);
+  if (!viz_format.has_value()) {
+    return Status(StatusCode::kUnsupportedTextureFormatForBind)
+        .WithData("dxgi_format", dxgi_format_);
+  }
+
   // Start construction of the GpuResources.
   // We send the texture itself, since we assume that we're using the angle
   // device for decoding.  Sharing seems not to work very well.  Otherwise, we
@@ -135,7 +159,8 @@
   // a handle that we get from |texture| as an IDXGIResource1.
   gpu_resources_.Post(FROM_HERE, &GpuResources::Init, std::move(get_helper_cb),
                       std::move(mailboxes), GL_TEXTURE_EXTERNAL_OES, size_,
-                      textures_per_picture);
+                      textures_per_picture, viz_format.value(), texture,
+                      array_slice);
   return OkStatus();
 }
 
@@ -165,7 +190,10 @@
     const std::vector<gpu::Mailbox> mailboxes,
     GLenum target,
     gfx::Size size,
-    int textures_per_picture) {
+    int textures_per_picture,
+    viz::ResourceFormat format,
+    ComD3D11Texture2D texture,
+    size_t array_slice) {
   helper_ = get_helper_cb.Run();
 
   if (!helper_ || !helper_->MakeContextCurrent()) {
@@ -173,17 +201,6 @@
     return;
   }
 
-  // Create the textures and attach them to the mailboxes.
-  // TODO(liberato): Should we use GL_FLOAT for an fp16 texture?  It doesn't
-  // really seem to matter so far as I can tell.
-  for (int texture_idx = 0; texture_idx < textures_per_picture; texture_idx++) {
-    uint32_t service_id =
-        helper_->CreateTexture(target, GL_RGBA, size.width(), size.height(),
-                               GL_RGBA, GL_UNSIGNED_BYTE);
-    service_ids_.push_back(service_id);
-    helper_->ProduceTexture(mailboxes[texture_idx], service_id);
-  }
-
   // Create the stream for zero-copy use by gl.
   EGLDisplay egl_display = gl::GLSurfaceEGL::GetHardwareDisplay();
   const EGLint stream_attributes[] = {
@@ -205,6 +222,60 @@
   // have a FakeCommandBufferHelper since the service IDs aren't meaningful.
   gl_image_ = base::MakeRefCounted<gl::GLImageDXGI>(size, stream);
 
+  const GLenum internal_format = viz::GLInternalFormat(format);
+  const GLenum data_type = viz::GLDataType(format);
+  const GLenum data_format = viz::GLDataFormat(format);
+
+  // Create the textures and attach them to the mailboxes.
+  // TODO(liberato): Should we use GL_FLOAT for an fp16 texture?  It doesn't
+  // really seem to matter so far as I can tell.
+  for (int texture_idx = 0; texture_idx < textures_per_picture; texture_idx++) {
+    // TODO(crbug.com/1011555): CreateTexture allocates a GL texture, figure out
+    // if this can be removed.
+    const uint32_t service_id =
+        helper_->CreateTexture(target, internal_format, size.width(),
+                               size.height(), data_format, data_type);
+
+    const auto& mailbox = mailboxes[texture_idx];
+
+    // Shared image does not need to store the colorspace since it is already
+    // stored on the VideoFrame which is provided upon presenting the overlay.
+    // To prevent the developer from mistakenly using it, provide the invalid
+    // value from default-construction.
+    const gfx::ColorSpace kInvalidColorSpace;
+
+    // Usage flags to allow the display compositor to draw from it, video to
+    // decode, and allow webgl/canvas access.
+    const uint32_t shared_image_usage =
+        gpu::SHARED_IMAGE_USAGE_VIDEO_DECODE | gpu::SHARED_IMAGE_USAGE_GLES2 |
+        gpu::SHARED_IMAGE_USAGE_RASTER | gpu::SHARED_IMAGE_USAGE_DISPLAY |
+        gpu::SHARED_IMAGE_USAGE_SCANOUT;
+
+    // Create a shared image
+    // TODO(crbug.com/1011555): Need key shared mutex if shared image is ever
+    // used by another device.
+    scoped_refptr<gpu::gles2::TexturePassthrough> gl_texture =
+        gpu::gles2::TexturePassthrough::CheckedCast(
+            helper_->GetTexture(service_id));
+
+    auto shared_image = std::make_unique<gpu::SharedImageBackingD3D>(
+        mailbox, format, size, kInvalidColorSpace, kTopLeft_GrSurfaceOrigin,
+        kPremul_SkAlphaType, shared_image_usage,
+        /*swap_chain=*/nullptr, std::move(gl_texture), gl_image_,
+        /*buffer_index=*/0, texture, base::win::ScopedHandle(),
+        /*dxgi_key_mutex=*/nullptr);
+
+    // Caller is assumed to provide cleared d3d textures.
+    shared_image->SetCleared();
+
+    // Shared images will be destroyed when this wrapper goes away.
+    // Only GpuResource can be used to safely destroy the shared images on the
+    // gpu main thread.
+    shared_images_.push_back(helper_->Register(std::move(shared_image)));
+
+    service_ids_.push_back(service_id);
+  }
+
   // Bind all the textures so that the stream can find them.
   OrderedDestructionList texture_everythings;
   for (int i = 0; i < textures_per_picture; i++)
@@ -259,11 +330,15 @@
     helper_->BindImage(service_ids_[texture_idx], gl_image_.get(),
                        false /* client_managed */);
   }
+
+  // Specify the texture so ProcessTexture knows how to process it using a GL
+  // image.
+  gl_image_->SetTexture(texture, array_slice);
+
+  PushNewTexture();
 }
 
-void DefaultTexture2DWrapper::GpuResources::PushNewTexture(
-    ComD3D11Texture2D texture,
-    size_t array_slice) {
+void DefaultTexture2DWrapper::GpuResources::PushNewTexture() {
   // If init didn't complete, then signal (another) error that will probably be
   // ignored in favor of whatever we signalled earlier.
   if (!gl_image_ || !stream_) {
@@ -271,12 +346,6 @@
     return;
   }
 
-  // Notify |gl_image_| that it has a new texture.  Do this unconditionally, so
-  // hat we can guarantee that the image isn't null.  Nobody expects it to be,
-  // and failures will be noticed only asynchronously.
-  // https://crbug.com/1077645
-  gl_image_->SetTexture(texture, array_slice);
-
   if (!helper_ || !helper_->MakeContextCurrent()) {
     NotifyError(StatusCode::kMakeContextCurrentFailed);
     return;
@@ -285,14 +354,14 @@
   // Notify angle that it has a new texture.
   EGLAttrib frame_attributes[] = {
       EGL_D3D_TEXTURE_SUBRESOURCE_ID_ANGLE,
-      array_slice,
+      gl_image_->level(),
       EGL_NONE,
   };
 
   EGLDisplay egl_display = gl::GLSurfaceEGL::GetHardwareDisplay();
-  if (!eglStreamPostD3DTextureANGLE(egl_display, stream_,
-                                    static_cast<void*>(texture.Get()),
-                                    frame_attributes)) {
+  if (!eglStreamPostD3DTextureANGLE(
+          egl_display, stream_, static_cast<void*>(gl_image_->texture().Get()),
+          frame_attributes)) {
     NotifyError(StatusCode::kPostTextureFailed);
     return;
   }
diff --git a/media/gpu/windows/d3d11_texture_wrapper.h b/media/gpu/windows/d3d11_texture_wrapper.h
index 3878f4fd9..0d2684d2 100644
--- a/media/gpu/windows/d3d11_texture_wrapper.h
+++ b/media/gpu/windows/d3d11_texture_wrapper.h
@@ -47,13 +47,13 @@
   // Initialize the wrapper.
   virtual Status Init(
       scoped_refptr<base::SingleThreadTaskRunner> gpu_task_runner,
-      GetCommandBufferHelperCB get_helper_cb) = 0;
+      GetCommandBufferHelperCB get_helper_cb,
+      ComD3D11Texture2D texture,
+      size_t array_size) = 0;
 
   // Import |texture|, |array_slice| and return the mailbox(es) that can be
   // used to refer to it.
-  virtual Status ProcessTexture(ComD3D11Texture2D texture,
-                                size_t array_slice,
-                                const gfx::ColorSpace& input_color_space,
+  virtual Status ProcessTexture(const gfx::ColorSpace& input_color_space,
                                 MailboxHolderArray* mailbox_dest_out,
                                 gfx::ColorSpace* output_color_space) = 0;
 
@@ -77,11 +77,11 @@
   ~DefaultTexture2DWrapper() override;
 
   Status Init(scoped_refptr<base::SingleThreadTaskRunner> gpu_task_runner,
-              GetCommandBufferHelperCB get_helper_cb) override;
+              GetCommandBufferHelperCB get_helper_cb,
+              ComD3D11Texture2D in_texture,
+              size_t array_slice) override;
 
-  Status ProcessTexture(ComD3D11Texture2D texture,
-                        size_t array_slice,
-                        const gfx::ColorSpace& input_color_space,
+  Status ProcessTexture(const gfx::ColorSpace& input_color_space,
                         MailboxHolderArray* mailbox_dest,
                         gfx::ColorSpace* output_color_space) override;
 
@@ -103,14 +103,18 @@
               const std::vector<gpu::Mailbox> mailboxes,
               GLenum target,
               gfx::Size size,
-              int textures_per_picture);
-
-    // Push a new |texture|, |array_slice| to |gl_image_|.
-    void PushNewTexture(ComD3D11Texture2D texture, size_t array_slice);
+              int textures_per_picture,
+              viz::ResourceFormat format,
+              ComD3D11Texture2D texture,
+              size_t array_slice);
 
     std::vector<uint32_t> service_ids_;
 
    private:
+    // Push a new |texture|, |array_slice| to |gl_image_|.
+    // Both |texture| and |array_slice| were set by Init.
+    void PushNewTexture();
+
     // Notify our wrapper about |status|, if we haven't before.
     void NotifyError(Status status);
 
@@ -121,6 +125,9 @@
     scoped_refptr<gl::GLImageDXGI> gl_image_;
     EGLStreamKHR stream_;
 
+    std::vector<std::unique_ptr<gpu::SharedImageRepresentationFactoryRef>>
+        shared_images_;
+
     DISALLOW_COPY_AND_ASSIGN(GpuResources);
   };
 
diff --git a/media/gpu/windows/d3d11_texture_wrapper_unittest.cc b/media/gpu/windows/d3d11_texture_wrapper_unittest.cc
index fe7b9179..ef9e777 100644
--- a/media/gpu/windows/d3d11_texture_wrapper_unittest.cc
+++ b/media/gpu/windows/d3d11_texture_wrapper_unittest.cc
@@ -90,7 +90,8 @@
   const DXGI_FORMAT dxgi_format = DXGI_FORMAT_NV12;
 
   auto wrapper = std::make_unique<DefaultTexture2DWrapper>(size_, dxgi_format);
-  const Status init_result = wrapper->Init(task_runner_, get_helper_cb_);
+  const Status init_result = wrapper->Init(
+      task_runner_, get_helper_cb_, /*texture_d3d=*/nullptr, /*array_slice=*/0);
   EXPECT_TRUE(init_result.is_ok());
 
   // TODO: verify that ProcessTexture processes both textures.
@@ -101,7 +102,8 @@
   const DXGI_FORMAT dxgi_format = DXGI_FORMAT_B8G8R8A8_UNORM;
 
   auto wrapper = std::make_unique<DefaultTexture2DWrapper>(size_, dxgi_format);
-  const Status init_result = wrapper->Init(task_runner_, get_helper_cb_);
+  const Status init_result = wrapper->Init(
+      task_runner_, get_helper_cb_, /*texture_d3d=*/nullptr, /*array_slice=*/0);
   EXPECT_TRUE(init_result.is_ok());
 }
 
@@ -110,7 +112,8 @@
   const DXGI_FORMAT dxgi_format = DXGI_FORMAT_R16G16B16A16_FLOAT;
 
   auto wrapper = std::make_unique<DefaultTexture2DWrapper>(size_, dxgi_format);
-  const Status init_result = wrapper->Init(task_runner_, get_helper_cb_);
+  const Status init_result = wrapper->Init(
+      task_runner_, get_helper_cb_, /*texture_d3d=*/nullptr, /*array_slice=*/0);
   EXPECT_TRUE(init_result.is_ok());
 }
 
@@ -119,8 +122,19 @@
   const DXGI_FORMAT dxgi_format = DXGI_FORMAT_P010;
 
   auto wrapper = std::make_unique<DefaultTexture2DWrapper>(size_, dxgi_format);
-  const Status init_result = wrapper->Init(task_runner_, get_helper_cb_);
+  const Status init_result = wrapper->Init(
+      task_runner_, get_helper_cb_, /*texture_d3d=*/nullptr, /*array_slice=*/0);
   EXPECT_TRUE(init_result.is_ok());
 }
 
+TEST_F(D3D11TextureWrapperUnittest, UnknownInitFails) {
+  STOP_IF_WIN7();
+  const DXGI_FORMAT dxgi_format = DXGI_FORMAT_UNKNOWN;
+
+  auto wrapper = std::make_unique<DefaultTexture2DWrapper>(size_, dxgi_format);
+  const Status init_result = wrapper->Init(
+      task_runner_, get_helper_cb_, /*texture_d3d=*/nullptr, /*array_slice=*/0);
+  EXPECT_FALSE(init_result.is_ok());
+}
+
 }  // namespace media
\ No newline at end of file
diff --git a/media/remoting/stream_provider.cc b/media/remoting/stream_provider.cc
index d2df3a0..72aeb9b7 100644
--- a/media/remoting/stream_provider.cc
+++ b/media/remoting/stream_provider.cc
@@ -199,12 +199,20 @@
       callback_message.has_audio_decoder_config()) {
     const pb::AudioDecoderConfig audio_message =
         callback_message.audio_decoder_config();
-    UpdateAudioConfig(audio_message);
+    ConvertProtoToAudioDecoderConfig(audio_message, &audio_decoder_config_);
+    if (!audio_decoder_config_.IsValidConfig()) {
+      OnError("Invalid audio config");
+      return;
+    }
   } else if (type_ == DemuxerStream::VIDEO &&
              callback_message.has_video_decoder_config()) {
     const pb::VideoDecoderConfig video_message =
         callback_message.video_decoder_config();
-    UpdateVideoConfig(video_message);
+    ConvertProtoToVideoDecoderConfig(video_message, &video_decoder_config_);
+    if (!video_decoder_config_.IsValidConfig()) {
+      OnError("Invalid video config");
+      return;
+    }
   } else {
     OnError("Config missing");
     return;
@@ -244,8 +252,6 @@
   total_received_frame_count_ = callback_message.count();
 
   if (ToDemuxerStreamStatus(callback_message.status()) == kConfigChanged) {
-    config_changed_ = true;
-
     if (callback_message.has_audio_decoder_config()) {
       const pb::AudioDecoderConfig audio_message =
           callback_message.audio_decoder_config();
@@ -277,14 +283,7 @@
     OnError("Invalid audio config");
     return;
   }
-  if (config_changed_) {
-    DCHECK(audio_decoder_config_.IsValidConfig());
-    DCHECK(!next_audio_decoder_config_.IsValidConfig());
-    next_audio_decoder_config_ = audio_config;
-  } else {
-    DCHECK(!audio_decoder_config_.IsValidConfig());
-    audio_decoder_config_ = audio_config;
-  }
+  next_audio_decoder_config_ = audio_config;
 }
 
 void StreamProvider::MediaStream::UpdateVideoConfig(
@@ -296,14 +295,7 @@
     OnError("Invalid video config");
     return;
   }
-  if (config_changed_) {
-    DCHECK(video_decoder_config_.IsValidConfig());
-    DCHECK(!next_video_decoder_config_.IsValidConfig());
-    next_video_decoder_config_ = video_config;
-  } else {
-    DCHECK(!video_decoder_config_.IsValidConfig());
-    video_decoder_config_ = video_config;
-  }
+  next_video_decoder_config_ = video_config;
 }
 
 void StreamProvider::MediaStream::SendReadUntil() {
@@ -326,7 +318,8 @@
   DCHECK(read_cb);
 
   read_complete_callback_ = std::move(read_cb);
-  if (buffers_.empty() && config_changed_) {
+  if (buffers_.empty() && (next_audio_decoder_config_.IsValidConfig() ||
+                           next_video_decoder_config_.IsValidConfig())) {
     CompleteRead(DemuxerStream::kConfigChanged);
     return;
   }
@@ -345,14 +338,14 @@
 
   switch (status) {
     case DemuxerStream::kConfigChanged:
-      if (type_ == AUDIO) {
-        DCHECK(next_audio_decoder_config_.IsValidConfig());
+      if (next_audio_decoder_config_.IsValidConfig()) {
         audio_decoder_config_ = next_audio_decoder_config_;
-      } else {
-        DCHECK(next_video_decoder_config_.IsValidConfig());
-        video_decoder_config_ = next_video_decoder_config_;
+        next_audio_decoder_config_ = media::AudioDecoderConfig();
       }
-      config_changed_ = false;
+      if (next_video_decoder_config_.IsValidConfig()) {
+        video_decoder_config_ = next_video_decoder_config_;
+        next_video_decoder_config_ = media::VideoDecoderConfig();
+      }
       std::move(read_complete_callback_).Run(status, nullptr);
       return;
     case DemuxerStream::kAborted:
diff --git a/media/remoting/stream_provider.h b/media/remoting/stream_provider.h
index 3b18c47..bd2630b5 100644
--- a/media/remoting/stream_provider.h
+++ b/media/remoting/stream_provider.h
@@ -184,11 +184,6 @@
     // contains how many frames are sent.
     uint32_t total_received_frame_count_ = 0;
 
-    // Indicates whether Audio/VideoDecoderConfig changed and the frames with
-    // the old config are not yet consumed. The new config is stored in the end
-    // of |audio/video_decoder_config_|.
-    bool config_changed_ = false;
-
     // Indicates whether a ReadUntil RPC message was sent without receiving the
     // ReadUntilCallback message yet.
     bool read_until_sent_ = false;
diff --git a/net/http/http_network_transaction_unittest.cc b/net/http/http_network_transaction_unittest.cc
index 26d16a20..b883035 100644
--- a/net/http/http_network_transaction_unittest.cc
+++ b/net/http/http_network_transaction_unittest.cc
@@ -460,7 +460,8 @@
   // other argument should be NULL.
   void PreconnectErrorResendRequestTest(const MockWrite* write_failure,
                                         const MockRead* read_failure,
-                                        bool use_spdy);
+                                        bool use_spdy,
+                                        bool upload = false);
 
   SimpleGetHelperResult SimpleGetHelperForData(
       base::span<StaticSocketDataProvider*> providers) {
@@ -1883,13 +1884,24 @@
 void HttpNetworkTransactionTest::PreconnectErrorResendRequestTest(
     const MockWrite* write_failure,
     const MockRead* read_failure,
-    bool use_spdy) {
+    bool use_spdy,
+    bool chunked_upload) {
+  SpdyTestUtil spdy_util;
   HttpRequestInfo request;
   request.method = "GET";
   request.url = GURL("https://www.foo.com/");
   request.traffic_annotation =
       net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS);
 
+  const char upload_data[] = "foobar";
+  ChunkedUploadDataStream upload_data_stream(0);
+  if (chunked_upload) {
+    request.method = "POST";
+    upload_data_stream.AppendData(upload_data, base::size(upload_data) - 1,
+                                  true);
+    request.upload_data_stream = &upload_data_stream;
+  }
+
   RecordingTestNetLog net_log;
   session_deps_.net_log = &net_log;
   std::unique_ptr<HttpNetworkSession> session(CreateSession(&session_deps_));
@@ -1904,17 +1916,32 @@
   session_deps_.socket_factory->AddSSLSocketDataProvider(&ssl2);
 
   // SPDY versions of the request and response.
-  spdy::SpdySerializedFrame spdy_request(spdy_util_.ConstructSpdyGet(
-      request.url.spec().c_str(), 1, DEFAULT_PRIORITY));
+
+  spdy::SpdyHeaderBlock spdy_post_header_block;
+  spdy_post_header_block[spdy::kHttp2MethodHeader] = "POST";
+  spdy_util.AddUrlToHeaderBlock(request.url.spec(), &spdy_post_header_block);
+  spdy::SpdySerializedFrame spdy_request(
+      chunked_upload
+          ? spdy_util.ConstructSpdyHeaders(1, std::move(spdy_post_header_block),
+                                           DEFAULT_PRIORITY, false)
+          : spdy_util.ConstructSpdyGet(request.url.spec().c_str(), 1,
+                                       DEFAULT_PRIORITY));
+
+  spdy::SpdySerializedFrame spdy_request_body(
+      spdy_util.ConstructSpdyDataFrame(1, "foobar", true));
   spdy::SpdySerializedFrame spdy_response(
-      spdy_util_.ConstructSpdyGetReply(nullptr, 0, 1));
+      spdy_util.ConstructSpdyGetReply(nullptr, 0, 1));
   spdy::SpdySerializedFrame spdy_data(
-      spdy_util_.ConstructSpdyDataFrame(1, "hello", true));
+      spdy_util.ConstructSpdyDataFrame(1, "hello", true));
 
   // HTTP/1.1 versions of the request and response.
-  const char kHttpRequest[] = "GET / HTTP/1.1\r\n"
+  const std::string http_request =
+      std::string(chunked_upload ? "POST" : "GET") +
+      " / HTTP/1.1\r\n"
       "Host: www.foo.com\r\n"
-      "Connection: keep-alive\r\n\r\n";
+      "Connection: keep-alive\r\n" +
+      (chunked_upload ? "Transfer-Encoding: chunked\r\n\r\n" : "\r\n");
+  const char* kHttpRequest = http_request.c_str();
   const char kHttpResponse[] = "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\n";
   const char kHttpData[] = "hello";
 
@@ -1928,8 +1955,14 @@
     ASSERT_TRUE(read_failure);
     if (use_spdy) {
       data1_writes.push_back(CreateMockWrite(spdy_request));
+      if (chunked_upload)
+        data1_writes.push_back(CreateMockWrite(spdy_request_body));
     } else {
       data1_writes.push_back(MockWrite(kHttpRequest));
+      if (chunked_upload) {
+        data1_writes.push_back(MockWrite("6\r\nfoobar\r\n"));
+        data1_writes.push_back(MockWrite("0\r\n\r\n"));
+      }
     }
     data1_reads.push_back(*read_failure);
   }
@@ -1941,19 +1974,25 @@
   std::vector<MockWrite> data2_writes;
 
   if (use_spdy) {
-    data2_writes.push_back(CreateMockWrite(spdy_request, 0, ASYNC));
-
-    data2_reads.push_back(CreateMockRead(spdy_response, 1, ASYNC));
-    data2_reads.push_back(CreateMockRead(spdy_data, 2, ASYNC));
-    data2_reads.push_back(MockRead(ASYNC, OK, 3));
+    int seq = 0;
+    data2_writes.push_back(CreateMockWrite(spdy_request, seq++, ASYNC));
+    if (chunked_upload)
+      data2_writes.push_back(CreateMockWrite(spdy_request_body, seq++, ASYNC));
+    data2_reads.push_back(CreateMockRead(spdy_response, seq++, ASYNC));
+    data2_reads.push_back(CreateMockRead(spdy_data, seq++, ASYNC));
+    data2_reads.push_back(MockRead(ASYNC, OK, seq++));
   } else {
+    int seq = 0;
     data2_writes.push_back(
-        MockWrite(ASYNC, kHttpRequest, strlen(kHttpRequest), 0));
-
+        MockWrite(ASYNC, kHttpRequest, strlen(kHttpRequest), seq++));
+    if (chunked_upload) {
+      data2_writes.push_back(MockWrite(ASYNC, "6\r\nfoobar\r\n", 11, seq++));
+      data2_writes.push_back(MockWrite(ASYNC, "0\r\n\r\n", 5, seq++));
+    }
     data2_reads.push_back(
-        MockRead(ASYNC, kHttpResponse, strlen(kHttpResponse), 1));
-    data2_reads.push_back(MockRead(ASYNC, kHttpData, strlen(kHttpData), 2));
-    data2_reads.push_back(MockRead(ASYNC, OK, 3));
+        MockRead(ASYNC, kHttpResponse, strlen(kHttpResponse), seq++));
+    data2_reads.push_back(MockRead(ASYNC, kHttpData, strlen(kHttpData), seq++));
+    data2_reads.push_back(MockRead(ASYNC, OK, seq++));
   }
   SequencedSocketData data2(data2_reads, data2_writes);
   session_deps_.socket_factory->AddSocketDataProvider(&data2);
@@ -2118,22 +2157,34 @@
 
 TEST_F(HttpNetworkTransactionTest, PreconnectErrorNotConnectedOnWrite) {
   MockWrite write_failure(ASYNC, ERR_SOCKET_NOT_CONNECTED);
-  PreconnectErrorResendRequestTest(&write_failure, nullptr, false);
+  PreconnectErrorResendRequestTest(&write_failure, nullptr,
+                                   false /* use_spdy */);
+  PreconnectErrorResendRequestTest(
+      &write_failure, nullptr, false /* use_spdy */, true /* chunked_upload */);
 }
 
 TEST_F(HttpNetworkTransactionTest, PreconnectErrorReset) {
   MockRead read_failure(ASYNC, ERR_CONNECTION_RESET);
-  PreconnectErrorResendRequestTest(nullptr, &read_failure, false);
+  PreconnectErrorResendRequestTest(nullptr, &read_failure,
+                                   false /* use_spdy */);
+  PreconnectErrorResendRequestTest(nullptr, &read_failure, false /* use_spdy */,
+                                   true /* chunked_upload */);
 }
 
 TEST_F(HttpNetworkTransactionTest, PreconnectErrorEOF) {
   MockRead read_failure(SYNCHRONOUS, OK);  // EOF
-  PreconnectErrorResendRequestTest(nullptr, &read_failure, false);
+  PreconnectErrorResendRequestTest(nullptr, &read_failure,
+                                   false /* use_spdy */);
+  PreconnectErrorResendRequestTest(nullptr, &read_failure, false /* use_spdy */,
+                                   true /* chunked_upload */);
 }
 
 TEST_F(HttpNetworkTransactionTest, PreconnectErrorAsyncEOF) {
   MockRead read_failure(ASYNC, OK);  // EOF
-  PreconnectErrorResendRequestTest(nullptr, &read_failure, false);
+  PreconnectErrorResendRequestTest(nullptr, &read_failure,
+                                   false /* use_spdy */);
+  PreconnectErrorResendRequestTest(nullptr, &read_failure, false /* use_spdy */,
+                                   true /* chunked_upload */);
 }
 
 // Make sure that on a 408 response (Request Timeout), the request is retried,
@@ -2145,27 +2196,39 @@
                         "Content-Length: 6\r\n\r\n"
                         "Pickle");
   KeepAliveConnectionResendRequestTest(nullptr, &read_failure);
-  PreconnectErrorResendRequestTest(nullptr, &read_failure, false);
+  PreconnectErrorResendRequestTest(nullptr, &read_failure,
+                                   false /* use_spdy */);
+  PreconnectErrorResendRequestTest(nullptr, &read_failure, false /* use_spdy */,
+                                   true /* chunked_upload */);
 }
 
 TEST_F(HttpNetworkTransactionTest, SpdyPreconnectErrorNotConnectedOnWrite) {
   MockWrite write_failure(ASYNC, ERR_SOCKET_NOT_CONNECTED);
-  PreconnectErrorResendRequestTest(&write_failure, nullptr, true);
+  PreconnectErrorResendRequestTest(&write_failure, nullptr,
+                                   true /* use_spdy */);
+  PreconnectErrorResendRequestTest(&write_failure, nullptr, true /* use_spdy */,
+                                   true /* chunked_upload */);
 }
 
 TEST_F(HttpNetworkTransactionTest, SpdyPreconnectErrorReset) {
   MockRead read_failure(ASYNC, ERR_CONNECTION_RESET);
-  PreconnectErrorResendRequestTest(nullptr, &read_failure, true);
+  PreconnectErrorResendRequestTest(nullptr, &read_failure, true /* use_spdy */);
+  PreconnectErrorResendRequestTest(nullptr, &read_failure, true /* use_spdy */,
+                                   true /* chunked_upload */);
 }
 
 TEST_F(HttpNetworkTransactionTest, SpdyPreconnectErrorEOF) {
   MockRead read_failure(SYNCHRONOUS, OK);  // EOF
-  PreconnectErrorResendRequestTest(nullptr, &read_failure, true);
+  PreconnectErrorResendRequestTest(nullptr, &read_failure, true /* use_spdy */);
+  PreconnectErrorResendRequestTest(nullptr, &read_failure, true /* use_spdy */,
+                                   true /* chunked_upload */);
 }
 
 TEST_F(HttpNetworkTransactionTest, SpdyPreconnectErrorAsyncEOF) {
   MockRead read_failure(ASYNC, OK);  // EOF
-  PreconnectErrorResendRequestTest(nullptr, &read_failure, true);
+  PreconnectErrorResendRequestTest(nullptr, &read_failure, true /* use_spdy */);
+  PreconnectErrorResendRequestTest(nullptr, &read_failure, true /* use_spdy */,
+                                   true /* chunked_upload */);
 }
 
 TEST_F(HttpNetworkTransactionTest, NonKeepAliveConnectionReset) {
diff --git a/net/quic/quic_flags_list.h b/net/quic/quic_flags_list.h
index e8d0b0f..e2b4b67 100644
--- a/net/quic/quic_flags_list.h
+++ b/net/quic/quic_flags_list.h
@@ -449,3 +449,14 @@
 QUIC_FLAG(bool,
           FLAGS_quic_reloadable_flag_quic_check_encryption_level_in_fast_path,
           true)
+
+// If true, gQUIC will only consult stream_map in
+// QuicSession::GetNumActiveStreams().
+QUIC_FLAG(
+    bool,
+    FLAGS_quic_reloadable_flag_quic_get_stream_information_from_stream_map,
+    false)
+
+// If true, QuicSession does not keep a separate zombie_streams. Instead, all
+// streams are stored in stream_map_.
+QUIC_FLAG(bool, FLAGS_quic_reloadable_flag_quic_remove_zombie_streams, false)
diff --git a/pdf/BUILD.gn b/pdf/BUILD.gn
index 04fffbb..974f69a 100644
--- a/pdf/BUILD.gn
+++ b/pdf/BUILD.gn
@@ -196,6 +196,8 @@
 
     configs += [ ":pdf_common_config" ]
 
+    public_deps = [ "//third_party/abseil-cpp:absl" ]
+
     deps = [
       "//base",
       "//ppapi/cpp:objects",
@@ -214,6 +216,8 @@
 
     configs += [ ":pdf_common_config" ]
 
+    public_deps = [ "//third_party/abseil-cpp:absl" ]
+
     deps = [
       ":features",
       ":internal",
diff --git a/pdf/ppapi_migration/image.cc b/pdf/ppapi_migration/image.cc
index ee74906..f1fc88fa 100644
--- a/pdf/ppapi_migration/image.cc
+++ b/pdf/ppapi_migration/image.cc
@@ -5,13 +5,14 @@
 #include "pdf/ppapi_migration/image.h"
 
 #include "ppapi/cpp/image_data.h"
+#include "third_party/abseil-cpp/absl/types/variant.h"
 #include "third_party/skia/include/core/SkBitmap.h"
 
 namespace chrome_pdf {
 
-Image::Image(const pp::ImageData& pepper_image) : pepper_image_(pepper_image) {}
+Image::Image(const pp::ImageData& pepper_image) : image_(pepper_image) {}
 
-Image::Image(const SkBitmap& skia_image) : skia_image_(skia_image) {}
+Image::Image(const SkBitmap& skia_image) : image_(skia_image) {}
 
 Image::Image(const Image& other) = default;
 
diff --git a/pdf/ppapi_migration/image.h b/pdf/ppapi_migration/image.h
index d9dfec5..35c2ffb 100644
--- a/pdf/ppapi_migration/image.h
+++ b/pdf/ppapi_migration/image.h
@@ -5,8 +5,8 @@
 #ifndef PDF_PPAPI_MIGRATION_IMAGE_H_
 #define PDF_PPAPI_MIGRATION_IMAGE_H_
 
-#include "base/check.h"
 #include "ppapi/cpp/image_data.h"
+#include "third_party/abseil-cpp/absl/types/variant.h"
 #include "third_party/skia/include/core/SkBitmap.h"
 
 namespace chrome_pdf {
@@ -24,18 +24,13 @@
   ~Image();
 
   const pp::ImageData& pepper_image() const {
-    DCHECK(skia_image_.isNull());
-    return pepper_image_;
+    return absl::get<pp::ImageData>(image_);
   }
 
-  const SkBitmap& skia_image() const {
-    DCHECK(pepper_image_.is_null());
-    return skia_image_;
-  }
+  const SkBitmap& skia_image() const { return absl::get<SkBitmap>(image_); }
 
  private:
-  pp::ImageData pepper_image_;
-  SkBitmap skia_image_;
+  absl::variant<pp::ImageData, SkBitmap> image_;
 };
 
 }  // namespace chrome_pdf
diff --git a/services/device/usb/BUILD.gn b/services/device/usb/BUILD.gn
index d2941e4..5b7ea62 100644
--- a/services/device/usb/BUILD.gn
+++ b/services/device/usb/BUILD.gn
@@ -32,6 +32,8 @@
     "usb_device_handle.h",
     "usb_device_handle_android.cc",
     "usb_device_handle_android.h",
+    "usb_device_handle_mac.cc",
+    "usb_device_handle_mac.h",
     "usb_device_handle_win.cc",
     "usb_device_handle_win.h",
     "usb_device_linux.cc",
diff --git a/services/device/usb/usb_device.h b/services/device/usb/usb_device.h
index 6851cb69..15d7430e 100644
--- a/services/device/usb/usb_device.h
+++ b/services/device/usb/usb_device.h
@@ -145,6 +145,7 @@
  private:
   friend class base::RefCountedThreadSafe<UsbDevice>;
   friend class UsbDeviceHandleImpl;
+  friend class UsbDeviceHandleMac;
   friend class UsbDeviceHandleUsbfs;
   friend class UsbDeviceHandleWin;
   friend class UsbServiceAndroid;
diff --git a/services/device/usb/usb_device_handle_mac.cc b/services/device/usb/usb_device_handle_mac.cc
new file mode 100644
index 0000000..2bba7c61
--- /dev/null
+++ b/services/device/usb/usb_device_handle_mac.cc
@@ -0,0 +1,123 @@
+// 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 "services/device/usb/usb_device_handle_mac.h"
+
+#include <IOKit/IOCFPlugIn.h>
+#include <IOKit/IOKitLib.h>
+#include <IOKit/IOReturn.h>
+#include <IOKit/usb/IOUSBLib.h>
+
+#include <utility>
+
+#include "base/mac/scoped_ioplugininterface.h"
+#include "services/device/usb/usb_device_mac.h"
+
+namespace device {
+
+UsbDeviceHandleMac::UsbDeviceHandleMac(
+    scoped_refptr<UsbDeviceMac> device,
+    base::mac::ScopedIOPluginInterface<IOUSBDeviceInterface182>
+        device_interface)
+    : device_interface_(std::move(device_interface)),
+      device_(std::move(device)) {}
+
+UsbDeviceHandleMac::~UsbDeviceHandleMac() {}
+
+scoped_refptr<UsbDevice> UsbDeviceHandleMac::GetDevice() const {
+  return device_;
+}
+
+void UsbDeviceHandleMac::Close() {
+  GetDevice()->HandleClosed(this);
+  device_ = nullptr;
+}
+
+void UsbDeviceHandleMac::SetConfiguration(int configuration_value,
+                                          ResultCallback callback) {
+  NOTIMPLEMENTED();
+  return;
+}
+
+void UsbDeviceHandleMac::ClaimInterface(int interface_number,
+                                        ResultCallback callback) {
+  NOTIMPLEMENTED();
+  return;
+}
+
+void UsbDeviceHandleMac::ReleaseInterface(int interface_number,
+                                          ResultCallback callback) {
+  NOTIMPLEMENTED();
+  return;
+}
+
+void UsbDeviceHandleMac::SetInterfaceAlternateSetting(int interface_number,
+                                                      int alternate_setting,
+                                                      ResultCallback callback) {
+  NOTIMPLEMENTED();
+  return;
+}
+
+void UsbDeviceHandleMac::ResetDevice(ResultCallback callback) {
+  NOTIMPLEMENTED();
+  return;
+}
+
+void UsbDeviceHandleMac::ClearHalt(mojom::UsbTransferDirection direction,
+                                   uint8_t endpoint_number,
+                                   ResultCallback callback) {
+  NOTIMPLEMENTED();
+  return;
+}
+
+void UsbDeviceHandleMac::ControlTransfer(
+    mojom::UsbTransferDirection direction,
+    mojom::UsbControlTransferType request_type,
+    mojom::UsbControlTransferRecipient recipient,
+    uint8_t request,
+    uint16_t value,
+    uint16_t index,
+    scoped_refptr<base::RefCountedBytes> buffer,
+    unsigned int timeout,
+    TransferCallback callback) {
+  NOTIMPLEMENTED();
+  return;
+}
+
+void UsbDeviceHandleMac::IsochronousTransferIn(
+    uint8_t endpoint,
+    const std::vector<uint32_t>& packet_lengths,
+    unsigned int timeout,
+    IsochronousTransferCallback callback) {
+  NOTIMPLEMENTED();
+  return;
+}
+
+void UsbDeviceHandleMac::IsochronousTransferOut(
+    uint8_t endpoint,
+    scoped_refptr<base::RefCountedBytes> buffer,
+    const std::vector<uint32_t>& packet_lengths,
+    unsigned int timeout,
+    IsochronousTransferCallback callback) {
+  NOTIMPLEMENTED();
+  return;
+}
+
+void UsbDeviceHandleMac::GenericTransfer(
+    mojom::UsbTransferDirection direction,
+    uint8_t endpoint_number,
+    scoped_refptr<base::RefCountedBytes> buffer,
+    unsigned int timeout,
+    TransferCallback callback) {
+  NOTIMPLEMENTED();
+  return;
+}
+
+const mojom::UsbInterfaceInfo* UsbDeviceHandleMac::FindInterfaceByEndpoint(
+    uint8_t endpoint_address) {
+  NOTIMPLEMENTED();
+  return nullptr;
+}
+
+}  // namespace device
diff --git a/services/device/usb/usb_device_handle_mac.h b/services/device/usb/usb_device_handle_mac.h
new file mode 100644
index 0000000..02a2b0e
--- /dev/null
+++ b/services/device/usb/usb_device_handle_mac.h
@@ -0,0 +1,85 @@
+// 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 SERVICES_DEVICE_USB_USB_DEVICE_HANDLE_MAC_H_
+#define SERVICES_DEVICE_USB_USB_DEVICE_HANDLE_MAC_H_
+
+#include "services/device/usb/usb_device_handle.h"
+
+#include <IOKit/IOCFPlugIn.h>
+#include <IOKit/IOKitLib.h>
+#include <IOKit/IOReturn.h>
+#include <IOKit/usb/IOUSBLib.h>
+
+#include <vector>
+
+#include "base/mac/scoped_ioplugininterface.h"
+#include "base/memory/ref_counted.h"
+#include "services/device/public/mojom/usb_device.mojom.h"
+
+namespace device {
+
+class UsbDeviceMac;
+
+class UsbDeviceHandleMac : public UsbDeviceHandle {
+ public:
+  UsbDeviceHandleMac(const UsbDeviceHandleMac&) = delete;
+  UsbDeviceHandleMac& operator=(const UsbDeviceHandleMac&) = delete;
+
+  // UsbDeviceHandle implementation:
+  scoped_refptr<UsbDevice> GetDevice() const override;
+  void Close() override;
+  void SetConfiguration(int configuration_value,
+                        ResultCallback callback) override;
+  void ClaimInterface(int interface_number, ResultCallback callback) override;
+  void ReleaseInterface(int interface_number, ResultCallback callback) override;
+  void SetInterfaceAlternateSetting(int interface_number,
+                                    int alternate_setting,
+                                    ResultCallback callback) override;
+  void ResetDevice(ResultCallback callback) override;
+  void ClearHalt(mojom::UsbTransferDirection direction,
+                 uint8_t endpoint_number,
+                 ResultCallback callback) override;
+  void ControlTransfer(mojom::UsbTransferDirection direction,
+                       mojom::UsbControlTransferType request_type,
+                       mojom::UsbControlTransferRecipient recipient,
+                       uint8_t request,
+                       uint16_t value,
+                       uint16_t index,
+                       scoped_refptr<base::RefCountedBytes> buffer,
+                       unsigned int timeout,
+                       TransferCallback callback) override;
+  void IsochronousTransferIn(uint8_t endpoint,
+                             const std::vector<uint32_t>& packet_lengths,
+                             unsigned int timeout,
+                             IsochronousTransferCallback callback) override;
+  void IsochronousTransferOut(uint8_t endpoint,
+                              scoped_refptr<base::RefCountedBytes> buffer,
+                              const std::vector<uint32_t>& packet_lengths,
+                              unsigned int timeout,
+                              IsochronousTransferCallback callback) override;
+  void GenericTransfer(mojom::UsbTransferDirection direction,
+                       uint8_t endpoint_number,
+                       scoped_refptr<base::RefCountedBytes> buffer,
+                       unsigned int timeout,
+                       TransferCallback callback) override;
+  const mojom::UsbInterfaceInfo* FindInterfaceByEndpoint(
+      uint8_t endpoint_address) override;
+
+  UsbDeviceHandleMac(scoped_refptr<UsbDeviceMac> device,
+                     base::mac::ScopedIOPluginInterface<IOUSBDeviceInterface182>
+                         device_interface);
+
+ protected:
+  ~UsbDeviceHandleMac() override;
+  friend class UsbDeviceMac;
+
+ private:
+  base::mac::ScopedIOPluginInterface<IOUSBDeviceInterface182> device_interface_;
+  scoped_refptr<UsbDeviceMac> device_;
+};
+
+}  // namespace device
+
+#endif  // SERVICES_DEVICE_USB_USB_DEVICE_HANDLE_MAC_H_
diff --git a/services/device/usb/usb_device_mac.cc b/services/device/usb/usb_device_mac.cc
index 40667a3f..8371d10 100644
--- a/services/device/usb/usb_device_mac.cc
+++ b/services/device/usb/usb_device_mac.cc
@@ -4,10 +4,21 @@
 
 #include "services/device/usb/usb_device_mac.h"
 
+#include <IOKit/IOCFPlugIn.h>
+#include <IOKit/IOKitLib.h>
+#include <IOKit/IOReturn.h>
+#include <IOKit/usb/IOUSBLib.h>
+
 #include <utility>
 
+#include "base/mac/foundation_util.h"
+#include "base/mac/scoped_ionotificationportref.h"
+#include "base/mac/scoped_ioobject.h"
+#include "base/mac/scoped_ioplugininterface.h"
+#include "components/device_event_log/device_event_log.h"
 #include "services/device/usb/usb_descriptors.h"
 #include "services/device/usb/usb_device_handle.h"
+#include "services/device/usb/usb_device_handle_mac.h"
 
 namespace device {
 
@@ -18,6 +29,61 @@
 UsbDeviceMac::~UsbDeviceMac() = default;
 
 void UsbDeviceMac::Open(OpenCallback callback) {
+  base::ScopedCFTypeRef<CFDictionaryRef> matching_dict(
+      IORegistryEntryIDMatching(entry_id()));
+  if (!matching_dict.get()) {
+    USB_LOG(ERROR) << "Failed to create matching dictionary for ID.";
+    std::move(callback).Run(nullptr);
+    return;
+  }
+
+  // IOServiceGetMatchingService consumes a reference to the matching dictionary
+  // passed to it.
+  base::mac::ScopedIOObject<io_service_t> usb_device(
+      IOServiceGetMatchingService(kIOMasterPortDefault,
+                                  matching_dict.release()));
+  if (!usb_device.get()) {
+    USB_LOG(ERROR) << "IOService not found for ID.";
+    std::move(callback).Run(nullptr);
+    return;
+  }
+
+  base::mac::ScopedIOPluginInterface<IOCFPlugInInterface> plugin_interface;
+  int32_t score;
+  IOReturn kr = IOCreatePlugInInterfaceForService(
+      usb_device, kIOUSBDeviceUserClientTypeID, kIOCFPlugInInterfaceID,
+      plugin_interface.InitializeInto(), &score);
+  if ((kr != kIOReturnSuccess) || !plugin_interface) {
+    USB_LOG(ERROR) << "Unable to create a plug-in: " << std::hex << kr;
+    std::move(callback).Run(nullptr);
+    return;
+  }
+
+  base::mac::ScopedIOPluginInterface<IOUSBDeviceInterface182> device_interface;
+  IOReturn result =
+      (*plugin_interface)
+          ->QueryInterface(
+              plugin_interface.get(),
+              CFUUIDGetUUIDBytes(kIOUSBDeviceInterfaceID),
+              reinterpret_cast<LPVOID*>(device_interface.InitializeInto()));
+  if (result || !device_interface) {
+    USB_LOG(ERROR) << "Couldn’t create a device interface.";
+    std::move(callback).Run(nullptr);
+    return;
+  }
+
+  kr = (*device_interface)->USBDeviceOpen(device_interface);
+  if (kr != kIOReturnSuccess) {
+    USB_LOG(ERROR) << "Failed to open device: " << std::hex << kr;
+    std::move(callback).Run(nullptr);
+    return;
+  }
+
+  auto device_handle = base::MakeRefCounted<UsbDeviceHandleMac>(
+      this, std::move(device_interface));
+
+  handles().push_back(device_handle.get());
+
   std::move(callback).Run(nullptr);
 }
 
diff --git a/services/device/usb/usb_device_mac.h b/services/device/usb/usb_device_mac.h
index 1d45ea56..f27b56b 100644
--- a/services/device/usb/usb_device_mac.h
+++ b/services/device/usb/usb_device_mac.h
@@ -21,8 +21,6 @@
   uint64_t entry_id() const { return entry_id_; }
 
  protected:
-  friend class UsbServiceMac;
-
   ~UsbDeviceMac() override;
 
  private:
diff --git a/services/device/usb/usb_service_mac.cc b/services/device/usb/usb_service_mac.cc
index a968239..4de2e82 100644
--- a/services/device/usb/usb_service_mac.cc
+++ b/services/device/usb/usb_service_mac.cc
@@ -320,6 +320,7 @@
     auto by_guid_it = devices().find(mac_device->guid());
     devices().erase(by_guid_it);
     NotifyDeviceRemoved(mac_device);
+    mac_device->OnDisconnect();
   }
 }
 
diff --git a/services/network/public/cpp/BUILD.gn b/services/network/public/cpp/BUILD.gn
index bef82fa..083f90f 100644
--- a/services/network/public/cpp/BUILD.gn
+++ b/services/network/public/cpp/BUILD.gn
@@ -239,15 +239,6 @@
     "url_request_mojom_traits.cc",
     "url_request_mojom_traits.h",
   ]
-  jumbo_excluded_sources = [
-    # IPC/Params code generators are based on macros and multiple
-    # inclusion of headers using those macros. That is not
-    # compatible with jumbo compiling all source, generators and
-    # users, together, so exclude those files from jumbo compilation.
-    "network_ipc_param_traits.cc",
-    "p2p_param_traits.cc",
-    "url_request_mojom_traits.cc",
-  ]
 
   configs += [ "//build/config/compiler:wexit_time_destructors" ]
 
@@ -258,10 +249,10 @@
     "//services/network/public/mojom:data_pipe_interfaces",
     "//services/network/public/mojom:mutable_network_traffic_annotation_interface",
     "//services/network/public/mojom:trust_tokens_interface",
+    "//third_party/webrtc_overrides:webrtc_component",
     "//url/ipc:url_ipc",
     "//url/mojom:url_mojom_gurl",
     "//url/mojom:url_mojom_origin",
-    "//third_party/webrtc_overrides:webrtc_component",
   ]
   deps = [
     "//base",
diff --git a/testing/buildbot/chromium.chromiumos.json b/testing/buildbot/chromium.chromiumos.json
index e0a7dea..b3b8b9f 100644
--- a/testing/buildbot/chromium.chromiumos.json
+++ b/testing/buildbot/chromium.chromiumos.json
@@ -4181,5 +4181,728 @@
         "test_id_prefix": "ninja://third_party/blink/renderer/platform/wtf:wtf_unittests/"
       }
     ]
+  },
+  "linux-lacros-tester-rel": {
+    "additional_compile_targets": [
+      "chrome",
+      "chrome_sandbox",
+      "linux_symbols",
+      "symupload"
+    ],
+    "gtest_tests": [
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "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/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "accessibility_unittests",
+        "test_id_prefix": "ninja://ui/accessibility:accessibility_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "angle_unittests",
+        "test_id_prefix": "ninja://third_party/angle/src/tests:angle_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "base_unittests",
+        "test_id_prefix": "ninja://base:base_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "base_util_unittests",
+        "test_id_prefix": "ninja://base/util:base_util_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "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/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "blink_fuzzer_unittests",
+        "test_id_prefix": "ninja://third_party/blink/renderer/platform:blink_fuzzer_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "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/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "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/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "name": "webkit_unit_tests",
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "blink_unittests",
+        "test_id_prefix": "ninja://third_party/blink/renderer/controller:blink_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "boringssl_crypto_tests",
+        "test_id_prefix": "ninja://third_party/boringssl:boringssl_crypto_tests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "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": [
+          "--gtest_filter=-*UsingRealWebcam*"
+        ],
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "capture_unittests",
+        "test_id_prefix": "ninja://media/capture:capture_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "cast_unittests",
+        "test_id_prefix": "ninja://media/cast:cast_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "cc_unittests",
+        "test_id_prefix": "ninja://cc:cc_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "chrome_app_unittests",
+        "test_id_prefix": "ninja://chrome/test:chrome_app_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "chromedriver_unittests",
+        "test_id_prefix": "ninja://chrome/test/chromedriver:chromedriver_unittests/"
+      },
+      {
+        "experiment_percentage": 100,
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "content_unittests",
+        "test_id_prefix": "ninja://content/test:content_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "crypto_unittests",
+        "test_id_prefix": "ninja://crypto:crypto_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "device_unittests",
+        "test_id_prefix": "ninja://device:device_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "display_unittests",
+        "test_id_prefix": "ninja://ui/display:display_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "events_unittests",
+        "test_id_prefix": "ninja://ui/events:events_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "filesystem_service_unittests",
+        "test_id_prefix": "ninja://components/services/filesystem:filesystem_service_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "gcm_unit_tests",
+        "test_id_prefix": "ninja://google_apis/gcm:gcm_unit_tests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "gfx_unittests",
+        "test_id_prefix": "ninja://ui/gfx:gfx_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "gin_unittests",
+        "test_id_prefix": "ninja://gin:gin_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "google_apis_unittests",
+        "test_id_prefix": "ninja://google_apis:google_apis_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "gpu_unittests",
+        "test_id_prefix": "ninja://gpu:gpu_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "gwp_asan_unittests",
+        "test_id_prefix": "ninja://components/gwp_asan:gwp_asan_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "ipc_tests",
+        "test_id_prefix": "ninja://ipc:ipc_tests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "jingle_unittests",
+        "test_id_prefix": "ninja://jingle:jingle_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "latency_unittests",
+        "test_id_prefix": "ninja://ui/latency:latency_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "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/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "media_blink_unittests",
+        "test_id_prefix": "ninja://media/blink:media_blink_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "media_unittests",
+        "test_id_prefix": "ninja://media:media_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "midi_unittests",
+        "test_id_prefix": "ninja://media/midi:midi_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "mojo_core_unittests",
+        "test_id_prefix": "ninja://mojo/core:mojo_core_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "mojo_unittests",
+        "test_id_prefix": "ninja://mojo:mojo_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "nacl_helper_nonsfi_unittests",
+        "test_id_prefix": "ninja://components/nacl/loader:nacl_helper_nonsfi_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "nacl_loader_unittests",
+        "test_id_prefix": "ninja://components/nacl/loader:nacl_loader_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "native_theme_unittests",
+        "test_id_prefix": "ninja://ui/native_theme:native_theme_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "net_unittests",
+        "test_id_prefix": "ninja://net:net_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "pdf_unittests",
+        "test_id_prefix": "ninja://pdf:pdf_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "perfetto_unittests",
+        "test_id_prefix": "ninja://third_party/perfetto:perfetto_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "ppapi_unittests",
+        "test_id_prefix": "ninja://ppapi:ppapi_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "printing_unittests",
+        "test_id_prefix": "ninja://printing:printing_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "remoting_unittests",
+        "test_id_prefix": "ninja://remoting:remoting_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "sandbox_linux_unittests",
+        "test_id_prefix": "ninja://sandbox/linux:sandbox_linux_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "service_manager_unittests",
+        "test_id_prefix": "ninja://services/service_manager/tests:service_manager_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "services_unittests",
+        "test_id_prefix": "ninja://services:services_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "shell_dialogs_unittests",
+        "test_id_prefix": "ninja://ui/shell_dialogs:shell_dialogs_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "skia_unittests",
+        "test_id_prefix": "ninja://skia:skia_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "sql_unittests",
+        "test_id_prefix": "ninja://sql:sql_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "storage_unittests",
+        "test_id_prefix": "ninja://storage:storage_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "traffic_annotation_auditor_unittests",
+        "test_id_prefix": "ninja://tools/traffic_annotation/auditor:traffic_annotation_auditor_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "ui_base_unittests",
+        "test_id_prefix": "ninja://ui/base:ui_base_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "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/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
+        },
+        "test": "viz_unittests",
+        "test_id_prefix": "ninja://components/viz:viz_unittests/"
+      },
+      {
+        "merge": {
+          "args": [],
+          "script": "//testing/merge_scripts/standard_gtest_merge.py"
+        },
+        "swarming": {
+          "can_use_on_swarming_builders": true,
+          "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/"
+      }
+    ]
   }
 }
diff --git a/testing/buildbot/chromium.ci.json b/testing/buildbot/chromium.ci.json
index 69993b83..b8735f8 100644
--- a/testing/buildbot/chromium.ci.json
+++ b/testing/buildbot/chromium.ci.json
@@ -255898,8 +255898,7 @@
           ],
           "hard_timeout": 900,
           "io_timeout": 900,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
-          "shards": 2
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "angle_deqp_egl_tests",
         "test_id_prefix": "ninja://third_party/angle/src/tests:angle_deqp_egl_tests/"
@@ -256107,8 +256106,7 @@
           ],
           "hard_timeout": 900,
           "io_timeout": 900,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
-          "shards": 2
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "angle_deqp_egl_tests",
         "test_id_prefix": "ninja://third_party/angle/src/tests:angle_deqp_egl_tests/"
@@ -256316,8 +256314,7 @@
           ],
           "hard_timeout": 900,
           "io_timeout": 900,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
-          "shards": 2
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "angle_deqp_egl_tests",
         "test_id_prefix": "ninja://third_party/angle/src/tests:angle_deqp_egl_tests/"
@@ -256525,8 +256522,7 @@
           ],
           "hard_timeout": 900,
           "io_timeout": 900,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
-          "shards": 2
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "angle_deqp_egl_tests",
         "test_id_prefix": "ninja://third_party/angle/src/tests:angle_deqp_egl_tests/"
@@ -256734,8 +256730,7 @@
           ],
           "hard_timeout": 900,
           "io_timeout": 900,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
-          "shards": 2
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "angle_deqp_egl_tests",
         "test_id_prefix": "ninja://third_party/angle/src/tests:angle_deqp_egl_tests/"
@@ -256943,8 +256938,7 @@
           ],
           "hard_timeout": 900,
           "io_timeout": 900,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
-          "shards": 2
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "angle_deqp_egl_tests",
         "test_id_prefix": "ninja://third_party/angle/src/tests:angle_deqp_egl_tests/"
@@ -266256,8 +266250,7 @@
           ],
           "hard_timeout": 900,
           "io_timeout": 900,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
-          "shards": 2
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "angle_deqp_egl_tests",
         "test_id_prefix": "ninja://third_party/angle/src/tests:angle_deqp_egl_tests/"
@@ -266465,8 +266458,7 @@
           ],
           "hard_timeout": 900,
           "io_timeout": 900,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
-          "shards": 2
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "angle_deqp_egl_tests",
         "test_id_prefix": "ninja://third_party/angle/src/tests:angle_deqp_egl_tests/"
@@ -266674,8 +266666,7 @@
           ],
           "hard_timeout": 900,
           "io_timeout": 900,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
-          "shards": 2
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "angle_deqp_egl_tests",
         "test_id_prefix": "ninja://third_party/angle/src/tests:angle_deqp_egl_tests/"
@@ -266883,8 +266874,7 @@
           ],
           "hard_timeout": 900,
           "io_timeout": 900,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
-          "shards": 2
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "angle_deqp_egl_tests",
         "test_id_prefix": "ninja://third_party/angle/src/tests:angle_deqp_egl_tests/"
@@ -267092,8 +267082,7 @@
           ],
           "hard_timeout": 900,
           "io_timeout": 900,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
-          "shards": 2
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "angle_deqp_egl_tests",
         "test_id_prefix": "ninja://third_party/angle/src/tests:angle_deqp_egl_tests/"
@@ -267301,8 +267290,7 @@
           ],
           "hard_timeout": 900,
           "io_timeout": 900,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
-          "shards": 2
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "angle_deqp_egl_tests",
         "test_id_prefix": "ninja://third_party/angle/src/tests:angle_deqp_egl_tests/"
diff --git a/testing/buildbot/chromium.fyi.json b/testing/buildbot/chromium.fyi.json
index 0687dcc..4f9b558 100644
--- a/testing/buildbot/chromium.fyi.json
+++ b/testing/buildbot/chromium.fyi.json
@@ -61072,729 +61072,6 @@
       }
     ]
   },
-  "linux-lacros-tester-rel": {
-    "additional_compile_targets": [
-      "chrome",
-      "chrome_sandbox",
-      "linux_symbols",
-      "symupload"
-    ],
-    "gtest_tests": [
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "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/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "accessibility_unittests",
-        "test_id_prefix": "ninja://ui/accessibility:accessibility_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "angle_unittests",
-        "test_id_prefix": "ninja://third_party/angle/src/tests:angle_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "base_unittests",
-        "test_id_prefix": "ninja://base:base_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "base_util_unittests",
-        "test_id_prefix": "ninja://base/util:base_util_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "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/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "blink_fuzzer_unittests",
-        "test_id_prefix": "ninja://third_party/blink/renderer/platform:blink_fuzzer_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "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/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "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/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "name": "webkit_unit_tests",
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "blink_unittests",
-        "test_id_prefix": "ninja://third_party/blink/renderer/controller:blink_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "boringssl_crypto_tests",
-        "test_id_prefix": "ninja://third_party/boringssl:boringssl_crypto_tests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "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": [
-          "--gtest_filter=-*UsingRealWebcam*"
-        ],
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "capture_unittests",
-        "test_id_prefix": "ninja://media/capture:capture_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "cast_unittests",
-        "test_id_prefix": "ninja://media/cast:cast_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "cc_unittests",
-        "test_id_prefix": "ninja://cc:cc_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "chrome_app_unittests",
-        "test_id_prefix": "ninja://chrome/test:chrome_app_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "chromedriver_unittests",
-        "test_id_prefix": "ninja://chrome/test/chromedriver:chromedriver_unittests/"
-      },
-      {
-        "experiment_percentage": 100,
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "content_unittests",
-        "test_id_prefix": "ninja://content/test:content_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "crypto_unittests",
-        "test_id_prefix": "ninja://crypto:crypto_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "device_unittests",
-        "test_id_prefix": "ninja://device:device_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "display_unittests",
-        "test_id_prefix": "ninja://ui/display:display_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "events_unittests",
-        "test_id_prefix": "ninja://ui/events:events_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "filesystem_service_unittests",
-        "test_id_prefix": "ninja://components/services/filesystem:filesystem_service_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "gcm_unit_tests",
-        "test_id_prefix": "ninja://google_apis/gcm:gcm_unit_tests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "gfx_unittests",
-        "test_id_prefix": "ninja://ui/gfx:gfx_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "gin_unittests",
-        "test_id_prefix": "ninja://gin:gin_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "google_apis_unittests",
-        "test_id_prefix": "ninja://google_apis:google_apis_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "gpu_unittests",
-        "test_id_prefix": "ninja://gpu:gpu_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "gwp_asan_unittests",
-        "test_id_prefix": "ninja://components/gwp_asan:gwp_asan_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "ipc_tests",
-        "test_id_prefix": "ninja://ipc:ipc_tests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "jingle_unittests",
-        "test_id_prefix": "ninja://jingle:jingle_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "latency_unittests",
-        "test_id_prefix": "ninja://ui/latency:latency_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "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/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "media_blink_unittests",
-        "test_id_prefix": "ninja://media/blink:media_blink_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "media_unittests",
-        "test_id_prefix": "ninja://media:media_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "midi_unittests",
-        "test_id_prefix": "ninja://media/midi:midi_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "mojo_core_unittests",
-        "test_id_prefix": "ninja://mojo/core:mojo_core_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "mojo_unittests",
-        "test_id_prefix": "ninja://mojo:mojo_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "nacl_helper_nonsfi_unittests",
-        "test_id_prefix": "ninja://components/nacl/loader:nacl_helper_nonsfi_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "nacl_loader_unittests",
-        "test_id_prefix": "ninja://components/nacl/loader:nacl_loader_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "native_theme_unittests",
-        "test_id_prefix": "ninja://ui/native_theme:native_theme_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "net_unittests",
-        "test_id_prefix": "ninja://net:net_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "pdf_unittests",
-        "test_id_prefix": "ninja://pdf:pdf_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "perfetto_unittests",
-        "test_id_prefix": "ninja://third_party/perfetto:perfetto_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "ppapi_unittests",
-        "test_id_prefix": "ninja://ppapi:ppapi_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "printing_unittests",
-        "test_id_prefix": "ninja://printing:printing_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "remoting_unittests",
-        "test_id_prefix": "ninja://remoting:remoting_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "sandbox_linux_unittests",
-        "test_id_prefix": "ninja://sandbox/linux:sandbox_linux_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "service_manager_unittests",
-        "test_id_prefix": "ninja://services/service_manager/tests:service_manager_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "services_unittests",
-        "test_id_prefix": "ninja://services:services_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "shell_dialogs_unittests",
-        "test_id_prefix": "ninja://ui/shell_dialogs:shell_dialogs_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "skia_unittests",
-        "test_id_prefix": "ninja://skia:skia_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "sql_unittests",
-        "test_id_prefix": "ninja://sql:sql_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "storage_unittests",
-        "test_id_prefix": "ninja://storage:storage_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "traffic_annotation_auditor_unittests",
-        "test_id_prefix": "ninja://tools/traffic_annotation/auditor:traffic_annotation_auditor_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "ui_base_unittests",
-        "test_id_prefix": "ninja://ui/base:ui_base_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "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/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
-        },
-        "test": "viz_unittests",
-        "test_id_prefix": "ninja://components/viz:viz_unittests/"
-      },
-      {
-        "merge": {
-          "args": [],
-          "script": "//testing/merge_scripts/standard_gtest_merge.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "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/"
-      }
-    ]
-  },
   "linux-perfetto-rel": {
     "additional_compile_targets": [
       "chrome"
diff --git a/testing/buildbot/chromium.swangle.json b/testing/buildbot/chromium.swangle.json
index 07e92f9..30024a9 100644
--- a/testing/buildbot/chromium.swangle.json
+++ b/testing/buildbot/chromium.swangle.json
@@ -87,8 +87,7 @@
           ],
           "hard_timeout": 900,
           "io_timeout": 900,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
-          "shards": 2
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "angle_deqp_egl_tests",
         "test_id_prefix": "ninja://third_party/angle/src/tests:angle_deqp_egl_tests/"
@@ -296,8 +295,7 @@
           ],
           "hard_timeout": 900,
           "io_timeout": 900,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
-          "shards": 2
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "angle_deqp_egl_tests",
         "test_id_prefix": "ninja://third_party/angle/src/tests:angle_deqp_egl_tests/"
@@ -505,8 +503,7 @@
           ],
           "hard_timeout": 900,
           "io_timeout": 900,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
-          "shards": 2
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "angle_deqp_egl_tests",
         "test_id_prefix": "ninja://third_party/angle/src/tests:angle_deqp_egl_tests/"
@@ -714,8 +711,7 @@
           ],
           "hard_timeout": 900,
           "io_timeout": 900,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
-          "shards": 2
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "angle_deqp_egl_tests",
         "test_id_prefix": "ninja://third_party/angle/src/tests:angle_deqp_egl_tests/"
@@ -923,8 +919,7 @@
           ],
           "hard_timeout": 900,
           "io_timeout": 900,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
-          "shards": 2
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "angle_deqp_egl_tests",
         "test_id_prefix": "ninja://third_party/angle/src/tests:angle_deqp_egl_tests/"
@@ -1132,8 +1127,7 @@
           ],
           "hard_timeout": 900,
           "io_timeout": 900,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
-          "shards": 2
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "angle_deqp_egl_tests",
         "test_id_prefix": "ninja://third_party/angle/src/tests:angle_deqp_egl_tests/"
@@ -1473,8 +1467,7 @@
           ],
           "hard_timeout": 900,
           "io_timeout": 900,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
-          "shards": 2
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "angle_deqp_egl_tests",
         "test_id_prefix": "ninja://third_party/angle/src/tests:angle_deqp_egl_tests/"
@@ -1682,8 +1675,7 @@
           ],
           "hard_timeout": 900,
           "io_timeout": 900,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
-          "shards": 2
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "angle_deqp_egl_tests",
         "test_id_prefix": "ninja://third_party/angle/src/tests:angle_deqp_egl_tests/"
@@ -1891,8 +1883,7 @@
           ],
           "hard_timeout": 900,
           "io_timeout": 900,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
-          "shards": 2
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "angle_deqp_egl_tests",
         "test_id_prefix": "ninja://third_party/angle/src/tests:angle_deqp_egl_tests/"
@@ -2100,8 +2091,7 @@
           ],
           "hard_timeout": 900,
           "io_timeout": 900,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
-          "shards": 2
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "angle_deqp_egl_tests",
         "test_id_prefix": "ninja://third_party/angle/src/tests:angle_deqp_egl_tests/"
@@ -2309,8 +2299,7 @@
           ],
           "hard_timeout": 900,
           "io_timeout": 900,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
-          "shards": 2
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "angle_deqp_egl_tests",
         "test_id_prefix": "ninja://third_party/angle/src/tests:angle_deqp_egl_tests/"
@@ -2518,8 +2507,7 @@
           ],
           "hard_timeout": 900,
           "io_timeout": 900,
-          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
-          "shards": 2
+          "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
         },
         "test": "angle_deqp_egl_tests",
         "test_id_prefix": "ninja://third_party/angle/src/tests:angle_deqp_egl_tests/"
diff --git a/testing/buildbot/filters/android.emulator_m.chrome_public_test_apk.filter b/testing/buildbot/filters/android.emulator_m.chrome_public_test_apk.filter
index 7a8a8b0..0065bff 100644
--- a/testing/buildbot/filters/android.emulator_m.chrome_public_test_apk.filter
+++ b/testing/buildbot/filters/android.emulator_m.chrome_public_test_apk.filter
@@ -65,7 +65,11 @@
 -org.chromium.chrome.features.start_surface.StartSurfaceLayoutTest.testIncognitoToggle_thumbnailFetchCount
 -org.chromium.chrome.features.start_surface.StartSurfaceLayoutTest.testThumbnailFetchingResult_defaultAspectRatio
 -org.chromium.chrome.features.start_surface.StartSurfaceLayoutTest.testRecycling_aspectRatioPoint75
+-org.chromium.chrome.features.start_surface.StartSurfaceLayoutTest.testGridToTabToCurrentLiveDetached
 
 # crbug.com/1096295
 -org.chromium.chrome.features.start_surface.StartSurfaceLayoutTest.testActivityCanBeGarbageCollectedAfterFinished
 
+# crbug.com/1112812
+-org.chromium.chrome.browser.autofill_assistant.AutofillAssistantAutostartTest.testAutostart
+-org.chromium.chrome.browser.autofill.AutofillUpstreamTest.testSaveCardInfoBarWithEmptyYear
diff --git a/testing/buildbot/test_suites.pyl b/testing/buildbot/test_suites.pyl
index 5a06a78..4e4a057 100644
--- a/testing/buildbot/test_suites.pyl
+++ b/testing/buildbot/test_suites.pyl
@@ -4198,9 +4198,6 @@
           '--test-launcher-batch-limit=512',
           '--test-launcher-retry-limit=0',
         ],
-        'swarming': {
-          'shards': 2,
-        },
       },
       'angle_deqp_gles2_tests': {
         'args': [
diff --git a/testing/buildbot/waterfalls.pyl b/testing/buildbot/waterfalls.pyl
index 4495481..0f70fb42 100644
--- a/testing/buildbot/waterfalls.pyl
+++ b/testing/buildbot/waterfalls.pyl
@@ -957,6 +957,17 @@
           'gtest_tests': 'linux_chromeos_gtests',
         },
       },
+      'linux-lacros-tester-rel': {
+        'additional_compile_targets': [
+          'chrome',
+          'chrome_sandbox',
+          'linux_symbols',
+          'symupload'
+        ],
+        'test_suites': {
+          'gtest_tests': 'linux_lacros_gtests',
+        },
+      },
     },
   },
   {
@@ -2291,17 +2302,6 @@
           'gtest_tests': 'linux_lacros_gtests',
         },
       },
-      'linux-lacros-tester-rel': {
-        'additional_compile_targets': [
-          'chrome',
-          'chrome_sandbox',
-          'linux_symbols',
-          'symupload'
-        ],
-        'test_suites': {
-          'gtest_tests': 'linux_lacros_gtests',
-        },
-      },
       'linux-perfetto-rel': {
         'additional_compile_targets': [ 'chrome' ],
         'mixins': [
diff --git a/testing/test_env.py b/testing/test_env.py
index 6e39aa4..7199826 100755
--- a/testing/test_env.py
+++ b/testing/test_env.py
@@ -2,6 +2,7 @@
 # Copyright (c) 2012 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.
+# Whitespace change for crbug.com/1112996. TODO: Delete me.
 
 """Sets environment variables needed to run a chromium unit test."""
 
diff --git a/testing/variations/fieldtrial_testing_config.json b/testing/variations/fieldtrial_testing_config.json
index c200eb2..d9a819a 100644
--- a/testing/variations/fieldtrial_testing_config.json
+++ b/testing/variations/fieldtrial_testing_config.json
@@ -2242,6 +2242,25 @@
             ]
         }
     ],
+    "DisableLatencyRecoveryDesktop": [
+        {
+            "platforms": [
+                "chromeos",
+                "linux",
+                "mac",
+                "windows"
+            ],
+            "experiments": [
+                {
+                    "name": "DisabledImplDisabledMain",
+                    "disable_features": [
+                        "ImplLatencyRecovery",
+                        "MainLatencyRecovery"
+                    ]
+                }
+            ]
+        }
+    ],
     "DisableMalwareExtensionsRemotely": [
         {
             "platforms": [
diff --git a/third_party/blink/public/platform/media/webmediaplayer_delegate.h b/third_party/blink/public/platform/media/webmediaplayer_delegate.h
index 3300bea4..fc8c71cb 100644
--- a/third_party/blink/public/platform/media/webmediaplayer_delegate.h
+++ b/third_party/blink/public/platform/media/webmediaplayer_delegate.h
@@ -67,6 +67,7 @@
     virtual void OnSeekBackward(double seconds) = 0;
     virtual void OnEnterPictureInPicture() = 0;
     virtual void OnExitPictureInPicture() = 0;
+    virtual void OnSetAudioSink(const std::string& sink_id) = 0;
 
     // Called to control audio ducking. Output volume should be set to
     // |player_volume| * |multiplier|. The range of |multiplier| is [0, 1],
@@ -130,6 +131,11 @@
   virtual void DidPictureInPictureAvailabilityChange(int delegate_id,
                                                      bool available) = 0;
 
+  // Notify that the audio output sink has changed
+  virtual void DidAudioOutputSinkChange(
+      int delegate_id,
+      const std::string& hashed_device_id) = 0;
+
   // Notify that a buffer underflow event happened for the media player.
   virtual void DidBufferUnderflow(int player_id) = 0;
 
diff --git a/third_party/blink/public/platform/scheduler/test/web_mock_thread_scheduler.h b/third_party/blink/public/platform/scheduler/test/web_mock_thread_scheduler.h
index dd1333c..109b74e 100644
--- a/third_party/blink/public/platform/scheduler/test/web_mock_thread_scheduler.h
+++ b/third_party/blink/public/platform/scheduler/test/web_mock_thread_scheduler.h
@@ -27,6 +27,8 @@
 
   MOCK_METHOD0(DefaultTaskRunner,
                scoped_refptr<base::SingleThreadTaskRunner>());
+  MOCK_METHOD0(DeprecatedDefaultTaskRunner,
+               scoped_refptr<base::SingleThreadTaskRunner>());
   MOCK_METHOD0(CompositorTaskRunner,
                scoped_refptr<base::SingleThreadTaskRunner>());
   MOCK_METHOD0(InputTaskRunner, scoped_refptr<base::SingleThreadTaskRunner>());
diff --git a/third_party/blink/public/platform/scheduler/web_thread_scheduler.h b/third_party/blink/public/platform/scheduler/web_thread_scheduler.h
index 08f8ade..828b8f1 100644
--- a/third_party/blink/public/platform/scheduler/web_thread_scheduler.h
+++ b/third_party/blink/public/platform/scheduler/web_thread_scheduler.h
@@ -82,9 +82,6 @@
 
   virtual scoped_refptr<base::SingleThreadTaskRunner> IPCTaskRunner();
 
-  // Returns the cleanup task runner, which is for cleaning up.
-  virtual scoped_refptr<base::SingleThreadTaskRunner> CleanupTaskRunner();
-
   // Returns a default task runner. This is basically same as the default task
   // runner, but is explicitly allowed to run JavaScript. For the detail, see
   // the comment at blink::ThreadScheduler::DeprecatedDefaultTaskRunner.
diff --git a/third_party/blink/public/platform/task_type.h b/third_party/blink/public/platform/task_type.h
index 9d2ac8d7..6dfc404 100644
--- a/third_party/blink/public/platform/task_type.h
+++ b/third_party/blink/public/platform/task_type.h
@@ -260,7 +260,8 @@
   kMainThreadTaskQueueIdle = 41,
   kMainThreadTaskQueueIPC = 42,
   kMainThreadTaskQueueControl = 43,
-  kMainThreadTaskQueueCleanup = 52,
+  // Removed:
+  // kMainThreadTaskQueueCleanup = 52,
   kMainThreadTaskQueueMemoryPurge = 62,
   kMainThreadTaskQueueNonWaking = 69,
   kCompositorThreadTaskQueueDefault = 45,
diff --git a/third_party/blink/public/web/modules/mediastream/webmediaplayer_ms.h b/third_party/blink/public/web/modules/mediastream/webmediaplayer_ms.h
index a8ca9685..7166dbc4 100644
--- a/third_party/blink/public/web/modules/mediastream/webmediaplayer_ms.h
+++ b/third_party/blink/public/web/modules/mediastream/webmediaplayer_ms.h
@@ -182,6 +182,7 @@
   void OnSeekBackward(double seconds) override;
   void OnEnterPictureInPicture() override;
   void OnExitPictureInPicture() override;
+  void OnSetAudioSink(const std::string& sink_id) override;
   void OnVolumeMultiplierUpdate(double multiplier) override;
   void OnBecamePersistentVideo(bool value) override;
 
diff --git a/third_party/blink/public/web/web_local_frame.h b/third_party/blink/public/web/web_local_frame.h
index 699eb37..a194c36 100644
--- a/third_party/blink/public/web/web_local_frame.h
+++ b/third_party/blink/public/web/web_local_frame.h
@@ -544,15 +544,6 @@
 
   // Iframe sandbox ---------------------------------------------------------
 
-  // TODO(ekaramad): This method is only exposed for testing for certain tests
-  // outside of blink/ that are interested in approximate value of the
-  // FrameReplicationState. This method should be replaced with one in content/
-  // where the notion of FrameReplicationState is relevant to.
-  // Returns the effective sandbox flags which are inherited from their parent
-  // frame.
-  virtual network::mojom::WebSandboxFlags EffectiveSandboxFlagsForTesting()
-      const = 0;
-
   // Returns false if this frame, or any parent frame is sandboxed and does not
   // have the flag "allow-downloads" set.
   virtual bool IsAllowedToDownload() const = 0;
diff --git a/third_party/blink/renderer/core/fetch/BUILD.gn b/third_party/blink/renderer/core/fetch/BUILD.gn
index 5b229766..bd02a17 100644
--- a/third_party/blink/renderer/core/fetch/BUILD.gn
+++ b/third_party/blink/renderer/core/fetch/BUILD.gn
@@ -48,13 +48,5 @@
     "trust_token_to_mojom.h",
   ]
 
-  if (is_win && is_component_build) {
-    # Body.cpp exports a class (CORE_EXPORT) that inherits from
-    # PairIterable<String, String> that is also used as base class by an
-    # imported (CORE_EXPORT) class and that confuses the Windows
-    # linker/compiler. https://crbug.com/739340
-    jumbo_excluded_sources = [ "body.cc" ]
-  }
-
   public_deps = [ "//third_party/blink/renderer/platform" ]
 }
diff --git a/third_party/blink/renderer/core/frame/local_dom_window.cc b/third_party/blink/renderer/core/frame/local_dom_window.cc
index 529612f..a99cecf 100644
--- a/third_party/blink/renderer/core/frame/local_dom_window.cc
+++ b/third_party/blink/renderer/core/frame/local_dom_window.cc
@@ -240,7 +240,8 @@
           MakeGarbageCollected<TextSuggestionController>(*this)),
       isolated_world_csp_map_(
           MakeGarbageCollected<
-              HeapHashMap<int, Member<ContentSecurityPolicy>>>()) {}
+              HeapHashMap<int, Member<ContentSecurityPolicy>>>()),
+      token_(LocalFrameToken(frame.GetFrameToken())) {}
 
 void LocalDOMWindow::BindContentSecurityPolicy() {
   DCHECK(!GetContentSecurityPolicy()->IsBound());
diff --git a/third_party/blink/renderer/core/frame/local_dom_window.h b/third_party/blink/renderer/core/frame/local_dom_window.h
index 19937dd7..2019a18 100644
--- a/third_party/blink/renderer/core/frame/local_dom_window.h
+++ b/third_party/blink/renderer/core/frame/local_dom_window.h
@@ -29,6 +29,7 @@
 
 #include "services/metrics/public/cpp/ukm_recorder.h"
 #include "services/metrics/public/cpp/ukm_source_id.h"
+#include "third_party/blink/public/common/tokens/tokens.h"
 #include "third_party/blink/renderer/bindings/core/v8/script_value.h"
 #include "third_party/blink/renderer/core/core_export.h"
 #include "third_party/blink/renderer/core/dom/document.h"
@@ -104,9 +105,16 @@
 
   static LocalDOMWindow* From(const ScriptState*);
 
-  explicit LocalDOMWindow(LocalFrame&, WindowAgent*);
+  LocalDOMWindow(LocalFrame&, WindowAgent*);
   ~LocalDOMWindow() override;
 
+  // Returns the token identifying the frame that this ExecutionContext was
+  // associated with at the moment of its creation. This remains valid even
+  // after the frame has been destroyed and the ExecutionContext is detached.
+  // This is used as a stable and persistent identifier for attributing detached
+  // context memory usage.
+  const LocalFrameToken& token() const { return token_; }
+
   LocalFrame* GetFrame() const { return To<LocalFrame>(DOMWindow::GetFrame()); }
 
   void Initialize();
@@ -493,6 +501,11 @@
   // document. This helps to count them only once per page load.
   // We don't use std::bitset to avoid to include feature_policy.mojom-blink.h.
   mutable Vector<bool> potentially_violated_features_;
+
+  // Token identifying the LocalFrame that this window was associated with at
+  // creation. Remains valid even after the frame is destroyed and the context
+  // is detached.
+  const LocalFrameToken token_;
 };
 
 template <>
diff --git a/third_party/blink/renderer/core/frame/web_frame_widget_base.cc b/third_party/blink/renderer/core/frame/web_frame_widget_base.cc
index a3b74a6..9d069f8a 100644
--- a/third_party/blink/renderer/core/frame/web_frame_widget_base.cc
+++ b/third_party/blink/renderer/core/frame/web_frame_widget_base.cc
@@ -134,10 +134,11 @@
                                                 std::move(widget_host),
                                                 std::move(widget))),
       client_(&client) {
-  frame_widget_host_.Bind(std::move(frame_widget_host),
-                          ThreadScheduler::Current()->IPCTaskRunner());
+  frame_widget_host_.Bind(
+      std::move(frame_widget_host),
+      ThreadScheduler::Current()->DeprecatedDefaultTaskRunner());
   receiver_.Bind(std::move(frame_widget),
-                 ThreadScheduler::Current()->IPCTaskRunner());
+                 ThreadScheduler::Current()->DeprecatedDefaultTaskRunner());
 }
 
 WebFrameWidgetBase::~WebFrameWidgetBase() = default;
diff --git a/third_party/blink/renderer/core/frame/web_local_frame_impl.cc b/third_party/blink/renderer/core/frame/web_local_frame_impl.cc
index f5d4b39..7783dfe 100644
--- a/third_party/blink/renderer/core/frame/web_local_frame_impl.cc
+++ b/third_party/blink/renderer/core/frame/web_local_frame_impl.cc
@@ -2324,31 +2324,6 @@
   GetFrame()->CopyImageAtViewportPoint(IntPoint(pos_in_viewport));
 }
 
-network::mojom::blink::WebSandboxFlags
-WebLocalFrameImpl::EffectiveSandboxFlagsForTesting() const {
-  if (!GetFrame())
-    return network::mojom::blink::WebSandboxFlags::kNone;
-  network::mojom::blink::WebSandboxFlags flags =
-      GetFrame()->Loader().EffectiveSandboxFlags();
-  if (RuntimeEnabledFeatures::FeaturePolicyForSandboxEnabled()) {
-    // When some of sandbox flags set in the 'sandbox' attribute are implemented
-    // as policies they are removed form the FrameOwner's sandbox flags to avoid
-    // being considered again as part of inherited or CSP sandbox.
-    // Note: if the FrameOwner is remote then the effective flags would miss the
-    // part of sandbox converted to FeaturePolicies. That said, with
-    // FeaturePolicyForSandbox all such flags should be part of the document's
-    // FeaturePolicy. For certain flags such as "downloads", dedicated API
-    // should be used (see IsAllowedToDownload()).
-    auto* local_owner = GetFrame()->DeprecatedLocalOwner();
-    if (local_owner && local_owner->OwnerType() ==
-                           mojom::blink::FrameOwnerElementType::kIframe) {
-      flags |= To<HTMLIFrameElement>(local_owner)
-                   ->sandbox_flags_converted_to_feature_policies();
-    }
-  }
-  return flags;
-}
-
 bool WebLocalFrameImpl::IsAllowedToDownload() const {
   if (!GetFrame())
     return true;
diff --git a/third_party/blink/renderer/core/frame/web_local_frame_impl.h b/third_party/blink/renderer/core/frame/web_local_frame_impl.h
index 94a26b4f..0e18107 100644
--- a/third_party/blink/renderer/core/frame/web_local_frame_impl.h
+++ b/third_party/blink/renderer/core/frame/web_local_frame_impl.h
@@ -248,8 +248,6 @@
       const WebVector<WebString>& words) override;
   void SetContentSettingsClient(WebContentSettingsClient*) override;
   void ReloadImage(const WebNode&) override;
-  network::mojom::blink::WebSandboxFlags EffectiveSandboxFlagsForTesting()
-      const override;
   bool IsAllowedToDownload() const override;
   bool FindForTesting(int identifier,
                       const WebString& search_text,
diff --git a/third_party/blink/renderer/core/html/BUILD.gn b/third_party/blink/renderer/core/html/BUILD.gn
index 77e74b1..e8e37235 100644
--- a/third_party/blink/renderer/core/html/BUILD.gn
+++ b/third_party/blink/renderer/core/html/BUILD.gn
@@ -593,9 +593,6 @@
     "window_name_collection.h",
   ]
 
-  jumbo_excluded_sources =
-      [ "canvas/canvas_rendering_context.cc" ]  # https://crbug.com/716395
-
   deps = [
     "//services/metrics/public/cpp:metrics_cpp",
     "//skia:skcms",
diff --git a/third_party/blink/renderer/core/layout/ng/inline/ng_inline_cursor_test.cc b/third_party/blink/renderer/core/layout/ng/inline/ng_inline_cursor_test.cc
index bef569d..a46f98f 100644
--- a/third_party/blink/renderer/core/layout/ng/inline/ng_inline_cursor_test.cc
+++ b/third_party/blink/renderer/core/layout/ng/inline/ng_inline_cursor_test.cc
@@ -13,8 +13,44 @@
 
 namespace blink {
 
+namespace {
+
 using ::testing::ElementsAre;
 
+String ToDebugString(const NGInlineCursor& cursor) {
+  if (cursor.Current().IsLineBox())
+    return "#linebox";
+
+  if (cursor.Current().IsLayoutGeneratedText()) {
+    StringBuilder result;
+    result.Append("#'");
+    result.Append(cursor.CurrentText());
+    result.Append("'");
+    return result.ToString();
+  }
+
+  if (cursor.Current().IsText())
+    return cursor.CurrentText().ToString().StripWhiteSpace();
+
+  if (const LayoutObject* layout_object = cursor.Current().GetLayoutObject()) {
+    if (const Element* element = DynamicTo<Element>(layout_object->GetNode())) {
+      if (const AtomicString& id = element->GetIdAttribute())
+        return "#" + id;
+    }
+
+    return layout_object->DebugName();
+  }
+
+  return "#null";
+}
+
+Vector<String> LayoutObjectToDebugStringList(NGInlineCursor cursor) {
+  Vector<String> list;
+  for (; cursor; cursor.MoveToNextForSameLayoutObject())
+    list.push_back(ToDebugString(cursor));
+  return list;
+}
+
 class NGInlineCursorTest : public NGLayoutTest,
                            private ScopedLayoutNGFragmentItemForTest,
                            public testing::WithParamInterface<bool> {
@@ -73,35 +109,6 @@
     EXPECT_THAT(backwards, forwards);
   }
 
-  String ToDebugString(const NGInlineCursor& cursor) {
-    if (cursor.Current().IsLineBox())
-      return "#linebox";
-
-    if (cursor.Current().IsLayoutGeneratedText()) {
-      StringBuilder result;
-      result.Append("#'");
-      result.Append(cursor.CurrentText());
-      result.Append("'");
-      return result.ToString();
-    }
-
-    if (cursor.Current().IsText())
-      return cursor.CurrentText().ToString().StripWhiteSpace();
-
-    if (const LayoutObject* layout_object =
-            cursor.Current().GetLayoutObject()) {
-      if (const Element* element =
-              DynamicTo<Element>(layout_object->GetNode())) {
-        if (const AtomicString& id = element->GetIdAttribute())
-          return "#" + id;
-      }
-
-      return layout_object->DebugName();
-    }
-
-    return "#null";
-  }
-
   Vector<String> ToDebugStringListWithBidiLevel(const NGInlineCursor& start) {
     Vector<String> list;
     for (NGInlineCursor cursor(start); cursor; cursor.MoveToNext()) {
@@ -215,12 +222,8 @@
       "</div>");
   NGInlineCursor cursor;
   cursor.MoveToIncludingCulledInline(*GetLayoutObjectByElementId("culled"));
-  Vector<String> list;
-  while (cursor) {
-    list.push_back(ToDebugString(cursor));
-    cursor.MoveToNextForSameLayoutObject();
-  }
-  EXPECT_THAT(list, ElementsAre("abc", "ABC", "", "XYZ", "xyz"));
+  EXPECT_THAT(LayoutObjectToDebugStringList(cursor),
+              ElementsAre("abc", "ABC", "", "XYZ", "xyz"));
 }
 
 // We should not have float:right fragment, because it isn't in-flow in
@@ -233,12 +236,43 @@
       "</div>");
   NGInlineCursor cursor;
   cursor.MoveToIncludingCulledInline(*GetLayoutObjectByElementId("culled"));
-  Vector<String> list;
-  while (cursor) {
-    list.push_back(ToDebugString(cursor));
-    cursor.MoveToNextForSameLayoutObject();
-  }
-  EXPECT_THAT(list, ElementsAre("abc", "xyz"));
+  EXPECT_THAT(LayoutObjectToDebugStringList(cursor), ElementsAre("abc", "xyz"));
+}
+
+TEST_P(NGInlineCursorTest, CulledInlineWithOOF) {
+  SetBodyInnerHTML(R"HTML(
+    <div id=root>
+      <b id=culled>abc<span style="position:absolute"></span>xyz</b>
+    </div>
+  )HTML");
+  NGInlineCursor cursor;
+  cursor.MoveToIncludingCulledInline(*GetLayoutObjectByElementId("culled"));
+  EXPECT_THAT(LayoutObjectToDebugStringList(cursor), ElementsAre("abc", "xyz"));
+}
+
+TEST_P(NGInlineCursorTest, CulledInlineNested) {
+  SetBodyInnerHTML(R"HTML(
+    <div id=root>
+      <b id=culled><span>abc</span> xyz</b>
+    </div>
+  )HTML");
+  NGInlineCursor cursor;
+  cursor.MoveToIncludingCulledInline(*GetLayoutObjectByElementId("culled"));
+  EXPECT_THAT(LayoutObjectToDebugStringList(cursor), ElementsAre("abc", "xyz"));
+}
+
+TEST_P(NGInlineCursorTest, CulledInlineBlockChild) {
+  SetBodyInnerHTML(R"HTML(
+    <div id=root>
+      <b id=culled>
+        <div>block</div>
+        <span>abc</span> xyz
+      </b>
+    </div>
+  )HTML");
+  NGInlineCursor cursor;
+  cursor.MoveToIncludingCulledInline(*GetLayoutObjectByElementId("culled"));
+  EXPECT_THAT(LayoutObjectToDebugStringList(cursor), ElementsAre("#culled"));
 }
 
 TEST_P(NGInlineCursorTest, CulledInlineWithRoot) {
@@ -247,12 +281,8 @@
   )HTML");
   const LayoutObject* layout_inline_a = GetLayoutObjectByElementId("a");
   cursor.MoveToIncludingCulledInline(*layout_inline_a);
-  Vector<String> list;
-  while (cursor) {
-    list.push_back(ToDebugString(cursor));
-    cursor.MoveToNextForSameLayoutObject();
-  }
-  EXPECT_THAT(list, ElementsAre("abc", "", "xyz"));
+  EXPECT_THAT(LayoutObjectToDebugStringList(cursor),
+              ElementsAre("abc", "", "xyz"));
 }
 
 TEST_P(NGInlineCursorTest, CulledInlineWithoutRoot) {
@@ -262,12 +292,8 @@
   const LayoutObject* layout_inline_a = GetLayoutObjectByElementId("a");
   NGInlineCursor cursor;
   cursor.MoveToIncludingCulledInline(*layout_inline_a);
-  Vector<String> list;
-  while (cursor) {
-    list.push_back(ToDebugString(cursor));
-    cursor.MoveToNextForSameLayoutObject();
-  }
-  EXPECT_THAT(list, ElementsAre("abc", "", "xyz"));
+  EXPECT_THAT(LayoutObjectToDebugStringList(cursor),
+              ElementsAre("abc", "", "xyz"));
 }
 
 TEST_P(NGInlineCursorTest, FirstChild) {
@@ -620,12 +646,8 @@
 TEST_P(NGInlineCursorTest, NextForSameLayoutObject) {
   NGInlineCursor cursor = SetupCursor("<pre id=root>abc\ndef\nghi</pre>");
   cursor.MoveTo(*GetLayoutObjectByElementId("root")->SlowFirstChild());
-  Vector<String> list;
-  while (cursor) {
-    list.push_back(ToDebugString(cursor));
-    cursor.MoveToNextForSameLayoutObject();
-  }
-  EXPECT_THAT(list, ElementsAre("abc", "", "def", "", "ghi"));
+  EXPECT_THAT(LayoutObjectToDebugStringList(cursor),
+              ElementsAre("abc", "", "def", "", "ghi"));
 }
 
 // Test |NextForSameLayoutObject| with limit range set.
@@ -660,12 +682,8 @@
   // Now |line2| is limited to the 2nd line. There should be only one framgnet
   // for `<span>` if we search using `line2`.
   LayoutObject* span1 = GetLayoutObjectByElementId("span1");
-  wtf_size_t count = 0;
-  for (line2.MoveTo(*span1); line2; line2.MoveToNextForSameLayoutObject()) {
-    DCHECK_EQ(line2.Current().GetLayoutObject(), span1);
-    ++count;
-  }
-  EXPECT_EQ(count, 1u);
+  line2.MoveTo(*span1);
+  EXPECT_THAT(LayoutObjectToDebugStringList(line2), ElementsAre("#span1"));
 }
 
 TEST_P(NGInlineCursorTest, Sibling) {
@@ -1038,4 +1056,6 @@
               ElementsAre("text3"));
 }
 
+}  // namespace
+
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/layout/ng/inline/ng_inline_layout_algorithm.cc b/third_party/blink/renderer/core/layout/ng/inline/ng_inline_layout_algorithm.cc
index a8688cd..277a5bd 100644
--- a/third_party/blink/renderer/core/layout/ng/inline/ng_inline_layout_algorithm.cc
+++ b/third_party/blink/renderer/core/layout/ng/inline/ng_inline_layout_algorithm.cc
@@ -885,7 +885,7 @@
       unsigned line_text_offset =
           item_result.StartOffset() - line_info->StartOffset();
       DCHECK_EQ(kObjectReplacementCharacter, line_text[line_text_offset]);
-      float space = spacing.ComputeSpacing(line_text_offset, offset);
+      float space = spacing.ComputeSpacing(line_text_offset, 0.0, offset);
       item_result.inline_size += space;
       // |offset| is non-zero only before CJK characters.
       DCHECK_EQ(offset, 0.f);
diff --git a/third_party/blink/renderer/core/layout/ng/inline/ng_inline_node.cc b/third_party/blink/renderer/core/layout/ng/inline/ng_inline_node.cc
index aa09812f..84574b9 100644
--- a/third_party/blink/renderer/core/layout/ng/inline/ng_inline_node.cc
+++ b/third_party/blink/renderer/core/layout/ng/inline/ng_inline_node.cc
@@ -1109,7 +1109,7 @@
     scoped_refptr<ShapeResult> shape_result =
         shaper.Shape(start_item, end_offset);
 
-    if (UNLIKELY(spacing.SetSpacing(font.GetFontDescription())))
+    if (UNLIKELY(spacing.SetSpacing(font)))
       shape_result->ApplySpacing(spacing);
 
     // If the text is from one item, use the ShapeResult as is.
diff --git a/third_party/blink/renderer/core/layout/ng/inline/ng_line_breaker.cc b/third_party/blink/renderer/core/layout/ng/inline/ng_line_breaker.cc
index fd91a99f..f38e03084 100644
--- a/third_party/blink/renderer/core/layout/ng/inline/ng_line_breaker.cc
+++ b/third_party/blink/renderer/core/layout/ng/inline/ng_line_breaker.cc
@@ -2087,7 +2087,7 @@
       DCHECK_EQ(break_iterator_.Locale(), style.LocaleForLineBreakIterator());
     }
     ShapeResultSpacing<String> spacing(spacing_.Text());
-    spacing.SetSpacing(style.GetFontDescription());
+    spacing.SetSpacing(style.GetFont());
     DCHECK_EQ(spacing.LetterSpacing(), spacing_.LetterSpacing());
     DCHECK_EQ(spacing.WordSpacing(), spacing_.WordSpacing());
 #endif
@@ -2136,7 +2136,7 @@
     break_iterator_.SetLocale(style.LocaleForLineBreakIterator());
   }
 
-  spacing_.SetSpacing(style.GetFontDescription());
+  spacing_.SetSpacing(style.GetFont());
 }
 
 void NGLineBreaker::MoveToNextOf(const NGInlineItem& item) {
diff --git a/third_party/blink/renderer/core/layout/ng/ng_fieldset_layout_algorithm.cc b/third_party/blink/renderer/core/layout/ng/ng_fieldset_layout_algorithm.cc
index f57b41b..9e59e6cb 100644
--- a/third_party/blink/renderer/core/layout/ng/ng_fieldset_layout_algorithm.cc
+++ b/third_party/blink/renderer/core/layout/ng/ng_fieldset_layout_algorithm.cc
@@ -19,6 +19,43 @@
 
 namespace blink {
 
+namespace {
+
+enum class LegendBlockAlignment {
+  kStart,
+  kCenter,
+  kEnd,
+};
+
+// This function is very similar to BlockAlignment() in ng_length_utils.cc, but
+// it supports text-align:left/center/right.
+inline LegendBlockAlignment ComputeLegendBlockAlignment(
+    const ComputedStyle& legend_style,
+    const ComputedStyle& fieldset_style) {
+  bool start_auto = legend_style.MarginStartUsing(fieldset_style).IsAuto();
+  bool end_auto = legend_style.MarginEndUsing(fieldset_style).IsAuto();
+  if (start_auto || end_auto) {
+    if (start_auto) {
+      return end_auto ? LegendBlockAlignment::kCenter
+                      : LegendBlockAlignment::kEnd;
+    }
+    return LegendBlockAlignment::kStart;
+  }
+  const bool is_ltr = fieldset_style.IsLeftToRightDirection();
+  switch (legend_style.GetTextAlign()) {
+    case ETextAlign::kLeft:
+      return is_ltr ? LegendBlockAlignment::kStart : LegendBlockAlignment::kEnd;
+    case ETextAlign::kRight:
+      return is_ltr ? LegendBlockAlignment::kEnd : LegendBlockAlignment::kStart;
+    case ETextAlign::kCenter:
+      return LegendBlockAlignment::kCenter;
+    default:
+      return LegendBlockAlignment::kStart;
+  }
+}
+
+}  // namespace
+
 NGFieldsetLayoutAlgorithm::NGFieldsetLayoutAlgorithm(
     const NGLayoutAlgorithmParams& params)
     : NGLayoutAlgorithm(params),
@@ -329,7 +366,7 @@
   LayoutUnit legend_inline_start = ComputeLegendInlineOffset(
       legend.Style(),
       NGFragment(writing_mode_, result->PhysicalFragment()).InlineSize(),
-      legend_margins, BorderScrollbarPadding().inline_start,
+      legend_margins, Style(), BorderScrollbarPadding().inline_start,
       ChildAvailableSize().inline_size);
 
   // NOTE: For painting purposes, this must be kept in sync with:
@@ -344,22 +381,21 @@
     const ComputedStyle& legend_style,
     LayoutUnit legend_border_box_inline_size,
     const NGBoxStrut& legend_margins,
+    const ComputedStyle& fieldset_style,
     LayoutUnit fieldset_border_padding_inline_start,
     LayoutUnit fieldset_content_inline_size) {
-  const ETextAlign align = legend_style.GetTextAlign();
-  const ETextAlign align_end = legend_style.IsLeftToRightDirection()
-                                   ? ETextAlign::kRight
-                                   : ETextAlign::kLeft;
-  LayoutUnit legend_inline_start = fieldset_border_padding_inline_start;
-  if (align == ETextAlign::kCenter) {
-    legend_inline_start +=
-        (fieldset_content_inline_size - legend_border_box_inline_size) / 2;
-  } else if (align == align_end) {
-    legend_inline_start += fieldset_content_inline_size -
-                           legend_border_box_inline_size -
-                           legend_margins.inline_end;
-  } else {
-    legend_inline_start += legend_margins.inline_start;
+  LayoutUnit legend_inline_start =
+      fieldset_border_padding_inline_start + legend_margins.inline_start;
+  // The following logic is very similar to ResolveInlineMargins(), but it uses
+  // ComputeLegendBlockAlignment().
+  const LayoutUnit available_space =
+      fieldset_content_inline_size - legend_border_box_inline_size;
+  if (available_space > LayoutUnit()) {
+    auto alignment = ComputeLegendBlockAlignment(legend_style, fieldset_style);
+    if (alignment == LegendBlockAlignment::kCenter)
+      legend_inline_start += available_space / 2;
+    else if (alignment == LegendBlockAlignment::kEnd)
+      legend_inline_start += available_space - legend_margins.inline_end;
   }
   return legend_inline_start;
 }
diff --git a/third_party/blink/renderer/core/layout/ng/ng_fieldset_layout_algorithm.h b/third_party/blink/renderer/core/layout/ng/ng_fieldset_layout_algorithm.h
index ac41aa7c..76c61a3 100644
--- a/third_party/blink/renderer/core/layout/ng/ng_fieldset_layout_algorithm.h
+++ b/third_party/blink/renderer/core/layout/ng/ng_fieldset_layout_algorithm.h
@@ -31,6 +31,7 @@
       const ComputedStyle& legend_style,
       LayoutUnit legend_border_box_inline_size,
       const NGBoxStrut& legend_margins,
+      const ComputedStyle& fieldset_style,
       LayoutUnit fieldset_border_padding_inline_start,
       LayoutUnit fieldset_content_inline_size);
 
diff --git a/third_party/blink/renderer/core/loader/document_loader.cc b/third_party/blink/renderer/core/loader/document_loader.cc
index 3139b88..4a711f3 100644
--- a/third_party/blink/renderer/core/loader/document_loader.cc
+++ b/third_party/blink/renderer/core/loader/document_loader.cc
@@ -1501,6 +1501,30 @@
   }
 }
 
+network::mojom::blink::WebSandboxFlags DocumentLoader::CalculateSandboxFlags() {
+  auto sandbox_flags = GetFrameLoader().GetForcedSandboxFlags() |
+                       content_security_policy_->GetSandboxMask() |
+                       frame_policy_.sandbox_flags;
+  if (archive_) {
+    // The URL of a Document loaded from a MHTML archive is controlled by
+    // the Content-Location header. This would allow UXSS, since
+    // Content-Location can be arbitrarily controlled to control the
+    // Document's URL and origin. Instead, force a Document loaded from a
+    // MHTML archive to be sandboxed, providing exceptions only for creating
+    // new windows.
+    DCHECK(commit_reason_ == CommitReason::kRegular ||
+           commit_reason_ == CommitReason::kInitialization);
+    sandbox_flags |= (network::mojom::blink::WebSandboxFlags::kAll &
+                      ~(network::mojom::blink::WebSandboxFlags::kPopups |
+                        network::mojom::blink::WebSandboxFlags::
+                            kPropagatesToAuxiliaryBrowsingContexts));
+  } else if (commit_reason_ == CommitReason::kXSLT) {
+    // An XSLT document inherits sandbox flags from the document that create it.
+    sandbox_flags |= frame_->DomWindow()->GetSandboxFlags();
+  }
+  return sandbox_flags;
+}
+
 scoped_refptr<SecurityOrigin> DocumentLoader::CalculateOrigin(
     Document* owner_document,
     network::mojom::blink::WebSandboxFlags sandbox_flags) {
@@ -1635,31 +1659,7 @@
             .BoolValue());
   }
 
-  // Make the snapshot value of sandbox flags from the beginning of navigation
-  // available in frame loader, so that the value could be further used to
-  // initialize sandbox flags in security context. crbug.com/1026627
-  GetFrameLoader().SetFrameOwnerSandboxFlags(frame_policy_.sandbox_flags);
-
-  network::mojom::blink::WebSandboxFlags sandbox_flags =
-      GetFrameLoader().EffectiveSandboxFlags() |
-      content_security_policy_->GetSandboxMask();
-  if (archive_) {
-    // The URL of a Document loaded from a MHTML archive is controlled by
-    // the Content-Location header. This would allow UXSS, since
-    // Content-Location can be arbitrarily controlled to control the
-    // Document's URL and origin. Instead, force a Document loaded from a
-    // MHTML archive to be sandboxed, providing exceptions only for creating
-    // new windows.
-    DCHECK(commit_reason_ == CommitReason::kRegular ||
-           commit_reason_ == CommitReason::kInitialization);
-    sandbox_flags |= (network::mojom::blink::WebSandboxFlags::kAll &
-                      ~(network::mojom::blink::WebSandboxFlags::kPopups |
-                        network::mojom::blink::WebSandboxFlags::
-                            kPropagatesToAuxiliaryBrowsingContexts));
-  } else if (commit_reason_ == CommitReason::kXSLT) {
-    sandbox_flags |= frame_->DomWindow()->GetSandboxFlags();
-  }
-
+  auto sandbox_flags = CalculateSandboxFlags();
   auto security_origin = CalculateOrigin(owner_document, sandbox_flags);
 
   GlobalObjectReusePolicy global_object_reuse_policy =
diff --git a/third_party/blink/renderer/core/loader/document_loader.h b/third_party/blink/renderer/core/loader/document_loader.h
index e682741..5ba686a 100644
--- a/third_party/blink/renderer/core/loader/document_loader.h
+++ b/third_party/blink/renderer/core/loader/document_loader.h
@@ -339,6 +339,7 @@
       mojom::MHTMLLoadResult::kSuccess;
 
  private:
+  network::mojom::blink::WebSandboxFlags CalculateSandboxFlags();
   scoped_refptr<SecurityOrigin> CalculateOrigin(
       Document* owner_document,
       network::mojom::blink::WebSandboxFlags);
diff --git a/third_party/blink/renderer/core/loader/frame_loader.cc b/third_party/blink/renderer/core/loader/frame_loader.cc
index ee724b0..b9b4ead3 100644
--- a/third_party/blink/renderer/core/loader/frame_loader.cc
+++ b/third_party/blink/renderer/core/loader/frame_loader.cc
@@ -214,7 +214,14 @@
 FrameLoader::FrameLoader(LocalFrame* frame)
     : frame_(frame),
       progress_tracker_(MakeGarbageCollected<ProgressTracker>(frame)),
-      forced_sandbox_flags_(network::mojom::blink::WebSandboxFlags::kNone),
+      // Frames need to inherit the sandbox flags of their parent frame.
+      // These can be fixed at construction time, because the only actions that
+      // trigger a sandbox flags change in the parent will necessarily detach
+      // this frame.
+      forced_sandbox_flags_(
+          frame_->Tree().Parent()
+              ? frame_->Tree().Parent()->GetSecurityContext()->GetSandboxFlags()
+              : network::mojom::blink::WebSandboxFlags::kNone),
       dispatching_did_clear_window_object_in_main_world_(false),
       detached_(false),
       virtual_time_pauser_(
@@ -1494,11 +1501,6 @@
   forced_sandbox_flags_ |= flags;
 }
 
-void FrameLoader::SetFrameOwnerSandboxFlags(
-    network::mojom::blink::WebSandboxFlags flags) {
-  frame_owner_sandbox_flags_ = flags;
-}
-
 void FrameLoader::DispatchDidClearDocumentOfWindowObject() {
   if (state_machine_.CreatingInitialEmptyDocument())
     return;
@@ -1532,32 +1534,11 @@
   Client()->DispatchDidClearWindowObjectInMainWorld();
 }
 
-network::mojom::blink::WebSandboxFlags FrameLoader::EffectiveSandboxFlags()
-    const {
-  network::mojom::blink::WebSandboxFlags flags = forced_sandbox_flags_;
-  if (frame_->Owner()) {
-    // Cannot use flags in frame_owner->GetFramePolicy().sandbox_flags, because
-    // frame_owner's frame policy is volatile and can be changed by javascript
-    // before navigation commits. Uses a snapshot
-    // value(frame_owner_sandbox_flags_) which is set in
-    // DocumentInit::WithFramePolicy instead. crbug.com/1026627
-    DCHECK(frame_owner_sandbox_flags_.has_value());
-    flags |= frame_owner_sandbox_flags_.value();
-  }
-  // Frames need to inherit the sandbox flags of their parent frame.
-  if (Frame* parent_frame = frame_->Tree().Parent())
-    flags |= parent_frame->GetSecurityContext()->GetSandboxFlags();
-  return flags;
-}
-
 network::mojom::blink::WebSandboxFlags
 FrameLoader::PendingEffectiveSandboxFlags() const {
   network::mojom::blink::WebSandboxFlags flags = forced_sandbox_flags_;
   if (FrameOwner* frame_owner = frame_->Owner())
     flags |= frame_owner->GetFramePolicy().sandbox_flags;
-  // Frames need to inherit the sandbox flags of their parent frame.
-  if (Frame* parent_frame = frame_->Tree().Parent())
-    flags |= parent_frame->GetSecurityContext()->GetSandboxFlags();
   return flags;
 }
 
diff --git a/third_party/blink/renderer/core/loader/frame_loader.h b/third_party/blink/renderer/core/loader/frame_loader.h
index d73aa96..484feb4 100644
--- a/third_party/blink/renderer/core/loader/frame_loader.h
+++ b/third_party/blink/renderer/core/loader/frame_loader.h
@@ -148,16 +148,9 @@
   // sandbox attribute of any parent frames.
   void ForceSandboxFlags(network::mojom::blink::WebSandboxFlags flags);
 
-  // Set frame_owner's effective sandbox flags, which are sandbox flags value
-  // at the beginning of navigation.
-  void SetFrameOwnerSandboxFlags(network::mojom::blink::WebSandboxFlags flags);
-
-  // Includes the collection of forced, inherited, and FrameOwner's sandbox
-  // flags, where the FrameOwner's flag is snapshotted from the last committed
-  // navigation. Note: with FeaturePolicyForSandbox the frame owner's sandbox
-  // flags only includes the flags which are *not* implemented as feature
-  // policies already present in the FrameOwner's ContainerPolicy.
-  network::mojom::blink::WebSandboxFlags EffectiveSandboxFlags() const;
+  network::mojom::blink::WebSandboxFlags GetForcedSandboxFlags() const {
+    return forced_sandbox_flags_;
+  }
 
   // Includes the collection of forced, inherited, and FrameOwner's sandbox
   // flags. Note: with FeaturePolicyForSandbox the frame owner's sandbox flags
@@ -310,14 +303,6 @@
   std::unique_ptr<ClientNavigationState> client_navigation_;
 
   network::mojom::blink::WebSandboxFlags forced_sandbox_flags_;
-  // A snapshot value of frame_owner's sandbox flags states at the beginning of
-  // navigation. For main frame which does not have a frame owner, the value is
-  // base::nullopt.
-  // The snapshot value is needed because of potential racing conditions on
-  // sandbox attribute on iframe element.
-  // crbug.com/1026627
-  base::Optional<network::mojom::blink::WebSandboxFlags>
-      frame_owner_sandbox_flags_ = base::nullopt;
 
   bool dispatching_did_clear_window_object_in_main_world_;
   bool detached_;
diff --git a/third_party/blink/renderer/core/paint/ng/ng_fieldset_painter.cc b/third_party/blink/renderer/core/paint/ng/ng_fieldset_painter.cc
index c1c55ee..c23d9e82 100644
--- a/third_party/blink/renderer/core/paint/ng/ng_fieldset_painter.cc
+++ b/third_party/blink/renderer/core/paint/ng/ng_fieldset_painter.cc
@@ -86,7 +86,7 @@
         NGFieldsetLayoutAlgorithm::ComputeLegendInlineOffset(
             (*legend)->Style(),
             legend_border_box.size.ConvertToLogical(writing_mode).inline_size,
-            legend_margins, border_padding.inline_start,
+            legend_margins, style, border_padding.inline_start,
             fieldset_content_inline_size),
         legend_margins.block_start};
     legend_border_box.offset = offset.ConvertToPhysical(
diff --git a/third_party/blink/renderer/core/script/BUILD.gn b/third_party/blink/renderer/core/script/BUILD.gn
index 3b08884..7007485 100644
--- a/third_party/blink/renderer/core/script/BUILD.gn
+++ b/third_party/blink/renderer/core/script/BUILD.gn
@@ -67,6 +67,4 @@
   ]
 
   deps = [ "//third_party/blink/public:resources" ]
-
-  jumbo_excluded_sources = [ "modulator.cc" ]  # https://crbug.com/716395
 }
diff --git a/third_party/blink/renderer/modules/mediastream/webmediaplayer_ms.cc b/third_party/blink/renderer/modules/mediastream/webmediaplayer_ms.cc
index 96b32f75..efd9d8a 100644
--- a/third_party/blink/renderer/modules/mediastream/webmediaplayer_ms.cc
+++ b/third_party/blink/renderer/modules/mediastream/webmediaplayer_ms.cc
@@ -11,6 +11,7 @@
 
 #include "base/bind.h"
 #include "base/callback.h"
+#include "base/optional.h"
 #include "base/threading/thread_task_runner_handle.h"
 #include "build/build_config.h"
 #include "cc/layers/video_frame_provider_client_impl.h"
@@ -805,7 +806,9 @@
   media::OutputDeviceStatusCB callback =
       ConvertToOutputDeviceStatusCB(std::move(completion_callback));
   if (audio_renderer_) {
-    audio_renderer_->SwitchOutputDevice(sink_id.Utf8(), std::move(callback));
+    auto sink_id_utf8 = sink_id.Utf8();
+    audio_renderer_->SwitchOutputDevice(sink_id_utf8, std::move(callback));
+    delegate_->DidAudioOutputSinkChange(delegate_id_, sink_id_utf8);
   } else {
     std::move(callback).Run(media::OUTPUT_DEVICE_STATUS_ERROR_INTERNAL);
     SendLogMessage(String::Format(
@@ -1081,6 +1084,11 @@
   client_->RequestExitPictureInPicture();
 }
 
+void WebMediaPlayerMS::OnSetAudioSink(const std::string& sink_id) {
+  SetSinkId(WebString::FromASCII(sink_id),
+            base::DoNothing::Once<base::Optional<blink::WebSetSinkIdError>>());
+}
+
 void WebMediaPlayerMS::OnVolumeMultiplierUpdate(double multiplier) {
   // TODO(perkj, magjed): See TODO in OnPlay().
 }
diff --git a/third_party/blink/renderer/modules/mediastream/webmediaplayer_ms_test.cc b/third_party/blink/renderer/modules/mediastream/webmediaplayer_ms_test.cc
index e514cfc..03e2af5 100644
--- a/third_party/blink/renderer/modules/mediastream/webmediaplayer_ms_test.cc
+++ b/third_party/blink/renderer/modules/mediastream/webmediaplayer_ms_test.cc
@@ -183,6 +183,11 @@
     EXPECT_EQ(delegate_id_, delegate_id);
   }
 
+  void DidAudioOutputSinkChange(int delegate_id,
+                                const std::string& hashed_device_id) override {
+    EXPECT_EQ(delegate_id_, delegate_id);
+  }
+
  private:
   int delegate_id_ = 1234;
   Observer* observer_ = nullptr;
@@ -609,7 +614,6 @@
   void RequestExitPictureInPicture() override {}
   Features GetFeatures() override { return Features(); }
 
-
   // Implementation of cc::VideoFrameProvider::Client
   void StopUsingProvider() override;
   void StartRendering() override;
diff --git a/third_party/blink/renderer/modules/storage/cached_storage_area.cc b/third_party/blink/renderer/modules/storage/cached_storage_area.cc
index 1497131..e8d5a4d 100644
--- a/third_party/blink/renderer/modules/storage/cached_storage_area.cc
+++ b/third_party/blink/renderer/modules/storage/cached_storage_area.cc
@@ -181,12 +181,12 @@
 CachedStorageArea::CachedStorageArea(
     AreaType type,
     scoped_refptr<const SecurityOrigin> origin,
-    scoped_refptr<base::SingleThreadTaskRunner> ipc_runner,
+    scoped_refptr<base::SingleThreadTaskRunner> task_runner,
     StorageNamespace* storage_namespace)
     : type_(type),
       origin_(std::move(origin)),
       storage_namespace_(storage_namespace),
-      ipc_task_runner_(std::move(ipc_runner)),
+      task_runner_(std::move(task_runner)),
       areas_(MakeGarbageCollected<HeapHashMap<WeakMember<Source>, String>>()) {
   BindStorageArea();
   base::trace_event::MemoryDumpManager::GetInstance()->RegisterDumpProvider(
@@ -204,17 +204,16 @@
   // Some tests may not provide a StorageNamespace.
   DCHECK(!remote_area_);
   if (new_area) {
-    remote_area_.Bind(std::move(new_area), ipc_task_runner_);
+    remote_area_.Bind(std::move(new_area), task_runner_);
   } else if (storage_namespace_) {
     storage_namespace_->BindStorageArea(
-        origin_, remote_area_.BindNewPipeAndPassReceiver(ipc_task_runner_));
+        origin_, remote_area_.BindNewPipeAndPassReceiver(task_runner_));
   } else {
     return;
   }
 
   receiver_.reset();
-  remote_area_->AddObserver(
-      receiver_.BindNewPipeAndPassRemote(ipc_task_runner_));
+  remote_area_->AddObserver(receiver_.BindNewPipeAndPassRemote(task_runner_));
 }
 
 void CachedStorageArea::ResetConnection(
diff --git a/third_party/blink/renderer/modules/storage/cached_storage_area.h b/third_party/blink/renderer/modules/storage/cached_storage_area.h
index b4e046bd..28c9c3d0 100644
--- a/third_party/blink/renderer/modules/storage/cached_storage_area.h
+++ b/third_party/blink/renderer/modules/storage/cached_storage_area.h
@@ -177,7 +177,7 @@
   const AreaType type_;
   const scoped_refptr<const SecurityOrigin> origin_;
   const WeakPersistent<StorageNamespace> storage_namespace_;
-  const scoped_refptr<base::SingleThreadTaskRunner> ipc_task_runner_;
+  const scoped_refptr<base::SingleThreadTaskRunner> task_runner_;
 
   std::unique_ptr<StorageAreaMap> map_;
 
diff --git a/third_party/blink/renderer/modules/storage/storage_controller.cc b/third_party/blink/renderer/modules/storage/storage_controller.cc
index 995be27..6c1590b 100644
--- a/third_party/blink/renderer/modules/storage/storage_controller.cc
+++ b/third_party/blink/renderer/modules/storage/storage_controller.cc
@@ -42,12 +42,13 @@
 
 // static
 StorageController* StorageController::GetInstance() {
-  DEFINE_STATIC_LOCAL(StorageController, gCachedStorageAreaController,
-                      (GetDomStorageConnection(),
-                       Thread::MainThread()->Scheduler()->IPCTaskRunner(),
-                       base::SysInfo::IsLowEndDevice()
-                           ? kStorageControllerTotalCacheLimitInBytesLowEnd
-                           : kStorageControllerTotalCacheLimitInBytes));
+  DEFINE_STATIC_LOCAL(
+      StorageController, gCachedStorageAreaController,
+      (GetDomStorageConnection(),
+       Thread::MainThread()->Scheduler()->DeprecatedDefaultTaskRunner(),
+       base::SysInfo::IsLowEndDevice()
+           ? kStorageControllerTotalCacheLimitInBytesLowEnd
+           : kStorageControllerTotalCacheLimitInBytes));
   return &gCachedStorageAreaController;
 }
 
@@ -63,9 +64,9 @@
 
 StorageController::StorageController(
     DomStorageConnection connection,
-    scoped_refptr<base::SingleThreadTaskRunner> ipc_runner,
+    scoped_refptr<base::SingleThreadTaskRunner> task_runner,
     size_t total_cache_limit)
-    : ipc_runner_(std::move(ipc_runner)),
+    : task_runner_(std::move(task_runner)),
       namespaces_(MakeGarbageCollected<
                   HeapHashMap<String, WeakMember<StorageNamespace>>>()),
       total_cache_limit_(total_cache_limit),
diff --git a/third_party/blink/renderer/modules/storage/storage_controller.h b/third_party/blink/renderer/modules/storage/storage_controller.h
index 3deb45c0..554f6cb 100644
--- a/third_party/blink/renderer/modules/storage/storage_controller.h
+++ b/third_party/blink/renderer/modules/storage/storage_controller.h
@@ -62,7 +62,7 @@
     mojo::PendingReceiver<mojom::blink::DomStorageClient> client_receiver;
   };
   StorageController(DomStorageConnection connection,
-                    scoped_refptr<base::SingleThreadTaskRunner> ipc_runner,
+                    scoped_refptr<base::SingleThreadTaskRunner> task_runner,
                     size_t total_cache_limit);
 
   // Creates a MakeGarbageCollected<StorageNamespace> for Session storage, and
@@ -92,8 +92,8 @@
     return dom_storage_remote_.get();
   }
 
-  scoped_refptr<base::SingleThreadTaskRunner> IPCTaskRunner() {
-    return ipc_runner_;
+  scoped_refptr<base::SingleThreadTaskRunner> TaskRunner() {
+    return task_runner_;
   }
 
  private:
@@ -102,7 +102,7 @@
   // mojom::blink::DomStorageClient:
   void ResetStorageAreaAndNamespaceConnections() override;
 
-  scoped_refptr<base::SingleThreadTaskRunner> ipc_runner_;
+  scoped_refptr<base::SingleThreadTaskRunner> task_runner_;
   Persistent<HeapHashMap<String, WeakMember<StorageNamespace>>> namespaces_;
   Persistent<StorageNamespace> local_storage_namespace_;
   size_t total_cache_limit_;
diff --git a/third_party/blink/renderer/modules/storage/storage_namespace.cc b/third_party/blink/renderer/modules/storage/storage_namespace.cc
index 0f53d5f..bb5c9a0 100644
--- a/third_party/blink/renderer/modules/storage/storage_namespace.cc
+++ b/third_party/blink/renderer/modules/storage/storage_namespace.cc
@@ -101,7 +101,7 @@
   result = base::MakeRefCounted<CachedStorageArea>(
       IsSessionStorage() ? CachedStorageArea::AreaType::kSessionStorage
                          : CachedStorageArea::AreaType::kLocalStorage,
-      origin, controller_->IPCTaskRunner(), this);
+      origin, controller_->TaskRunner(), this);
   cached_areas_.insert(std::move(origin), result);
   return result;
 }
@@ -212,7 +212,7 @@
     return;
   controller_->dom_storage()->BindSessionStorageNamespace(
       namespace_id_,
-      namespace_.BindNewPipeAndPassReceiver(controller_->IPCTaskRunner()));
+      namespace_.BindNewPipeAndPassReceiver(controller_->TaskRunner()));
 }
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/modules/webaudio/periodic_wave.cc b/third_party/blink/renderer/modules/webaudio/periodic_wave.cc
index 2ea0d53..9efa189d 100644
--- a/third_party/blink/renderer/modules/webaudio/periodic_wave.cc
+++ b/third_party/blink/renderer/modules/webaudio/periodic_wave.cc
@@ -36,6 +36,7 @@
 #include "third_party/blink/renderer/modules/webaudio/periodic_wave.h"
 #include "third_party/blink/renderer/platform/audio/fft_frame.h"
 #include "third_party/blink/renderer/platform/audio/vector_math.h"
+#include "third_party/blink/renderer/platform/bindings/exception_messages.h"
 #include "third_party/blink/renderer/platform/bindings/exception_state.h"
 
 #if defined(CPU_ARM_NEON)
@@ -70,6 +71,22 @@
     return nullptr;
   }
 
+  if (real.size() < 2) {
+    exception_state.ThrowDOMException(
+        DOMExceptionCode::kIndexSizeError,
+        ExceptionMessages::IndexExceedsMinimumBound("length of the real array",
+                                                    real.size(), 2u));
+    return nullptr;
+  }
+
+  if (imag.size() < 2) {
+    exception_state.ThrowDOMException(
+        DOMExceptionCode::kIndexSizeError,
+        ExceptionMessages::IndexExceedsMinimumBound("length of the imag array",
+                                                    imag.size(), 2u));
+    return nullptr;
+  }
+
   PeriodicWave* periodic_wave =
       MakeGarbageCollected<PeriodicWave>(context.sampleRate());
   periodic_wave->CreateBandLimitedTables(real.data(), imag.data(), real.size(),
diff --git a/third_party/blink/renderer/modules/webgpu/gpu_shader_module.cc b/third_party/blink/renderer/modules/webgpu/gpu_shader_module.cc
index 8e40d48..2733049 100644
--- a/third_party/blink/renderer/modules/webgpu/gpu_shader_module.cc
+++ b/third_party/blink/renderer/modules/webgpu/gpu_shader_module.cc
@@ -21,13 +21,14 @@
   WGPUShaderModuleDescriptor dawn_desc = {};
   WGPUShaderModuleWGSLDescriptor wgsl_desc = {};
   WGPUShaderModuleSPIRVDescriptor spirv_desc = {};
+  std::string wgsl_code;
 
   auto wgsl_or_spirv = webgpu_desc->code();
   if (wgsl_or_spirv.IsUSVString()) {
-    std::string code = wgsl_or_spirv.GetAsUSVString().Utf8();
+    wgsl_code = wgsl_or_spirv.GetAsUSVString().Utf8();
 
     wgsl_desc.chain.sType = WGPUSType_ShaderModuleWGSLDescriptor;
-    wgsl_desc.source = code.c_str();
+    wgsl_desc.source = wgsl_code.c_str();
     dawn_desc.nextInChain = reinterpret_cast<WGPUChainedStruct*>(&wgsl_desc);
   } else {
     DCHECK(wgsl_or_spirv.IsUint32Array());
@@ -48,8 +49,10 @@
     dawn_desc.nextInChain = reinterpret_cast<WGPUChainedStruct*>(&spirv_desc);
   }
 
+  std::string label;
   if (webgpu_desc->hasLabel()) {
-    dawn_desc.label = webgpu_desc->label().Utf8().data();
+    label = webgpu_desc->label().Utf8();
+    dawn_desc.label = label.c_str();
   }
 
   return MakeGarbageCollected<GPUShaderModule>(
diff --git a/third_party/blink/renderer/platform/BUILD.gn b/third_party/blink/renderer/platform/BUILD.gn
index 557c4ab..e0653b5 100644
--- a/third_party/blink/renderer/platform/BUILD.gn
+++ b/third_party/blink/renderer/platform/BUILD.gn
@@ -1533,27 +1533,6 @@
     ]
   }
 
-  if (is_win) {
-    jumbo_excluded_sources = [
-      # https://crbug.com/775979 - Uses libjpeg_turbo which uses a
-      # "boolean" typedef which is different (int) from the Windows
-      # standard "boolean" typedef (unsigned char), resulting in
-      # compilation errors when both are joined in a translation unit.
-      "image-decoders/jpeg/jpeg_image_decoder.cc",
-    ]
-
-    if (is_component_build) {
-      # https://crbug.com/764823 - Mixing certain //url/ headers and
-      # using url::RawCanonOutputT<char> in one translation unit breaks
-      # the Windows component build. These files use RawCanonOutput.
-      jumbo_excluded_sources += [
-        "link_hash.cc",
-        "weborigin/kurl.cc",
-        "weborigin/origin_access_entry.cc",
-        "weborigin/security_origin.cc",
-      ]
-    }
-  }
   configs += [
     ":blink_platform_pch",
     ":blink_platform_config",
diff --git a/third_party/blink/renderer/platform/fonts/font.h b/third_party/blink/renderer/platform/fonts/font.h
index 8864a71..3e0aeec6 100644
--- a/third_party/blink/renderer/platform/fonts/font.h
+++ b/third_party/blink/renderer/platform/fonts/font.h
@@ -241,6 +241,13 @@
     return EnsureFontFallbackList()->ShouldSkipDrawing();
   }
 
+  // Returns true if any of the matched @font-face rules has set a
+  // letter-spacing-override value.
+  bool HasLetterSpacingOverride() const {
+    return font_fallback_list_ &&
+           font_fallback_list_->HasLetterSpacingOverride();
+  }
+
  private:
   // TODO(xiaochengh): The function not only initializes null FontFallbackList,
   // but also syncs invalid FontFallbackList. Rename it for better readability.
diff --git a/third_party/blink/renderer/platform/fonts/font_data.h b/third_party/blink/renderer/platform/fonts/font_data.h
index d79e7fd..579b4fd7 100644
--- a/third_party/blink/renderer/platform/fonts/font_data.h
+++ b/third_party/blink/renderer/platform/fonts/font_data.h
@@ -52,6 +52,7 @@
   virtual bool IsLoadingFallback() const = 0;
   virtual bool IsSegmented() const = 0;
   virtual bool ShouldSkipDrawing() const = 0;
+  virtual bool HasLetterSpacingOverride() const = 0;
 
  private:
   DISALLOW_COPY_AND_ASSIGN(FontData);
diff --git a/third_party/blink/renderer/platform/fonts/font_fallback_list.cc b/third_party/blink/renderer/platform/fonts/font_fallback_list.cc
index 6cb1dd0..8512b83 100644
--- a/third_party/blink/renderer/platform/fonts/font_fallback_list.cc
+++ b/third_party/blink/renderer/platform/fonts/font_fallback_list.cc
@@ -47,6 +47,7 @@
       generation_(FontCache::GetFontCache()->Generation()),
       has_loading_fallback_(false),
       has_custom_font_(false),
+      has_letter_spacing_override_(false),
       can_shape_word_by_word_(false),
       can_shape_word_by_word_computed_(false),
       is_invalid_(false) {}
@@ -61,6 +62,7 @@
   family_index_ = 0;
   has_loading_fallback_ = false;
   has_custom_font_ = false;
+  has_letter_spacing_override_ = false;
   can_shape_word_by_word_ = false;
   can_shape_word_by_word_computed_ = false;
   font_selector_version_ = font_selector_ ? font_selector_->Version() : 0;
@@ -280,6 +282,8 @@
       has_loading_fallback_ = true;
     if (result->IsCustomFont())
       has_custom_font_ = true;
+    if (result->HasLetterSpacingOverride())
+      has_letter_spacing_override_ = true;
   }
   return result.get();
 }
diff --git a/third_party/blink/renderer/platform/fonts/font_fallback_list.h b/third_party/blink/renderer/platform/fonts/font_fallback_list.h
index ebda8686..d2a56aff 100644
--- a/third_party/blink/renderer/platform/fonts/font_fallback_list.h
+++ b/third_party/blink/renderer/platform/fonts/font_fallback_list.h
@@ -111,6 +111,7 @@
 
   bool HasLoadingFallback() const { return has_loading_fallback_; }
   bool HasCustomFont() const { return has_custom_font_; }
+  bool HasLetterSpacingOverride() const { return has_letter_spacing_override_; }
 
  private:
   explicit FontFallbackList(FontSelector* font_selector);
@@ -132,6 +133,7 @@
   uint16_t generation_;
   bool has_loading_fallback_ : 1;
   bool has_custom_font_ : 1;
+  bool has_letter_spacing_override_ : 1;
   bool can_shape_word_by_word_ : 1;
   bool can_shape_word_by_word_computed_ : 1;
   bool is_invalid_ : 1;
diff --git a/third_party/blink/renderer/platform/fonts/segmented_font_data.cc b/third_party/blink/renderer/platform/fonts/segmented_font_data.cc
index f68af86..a9a0bd1 100644
--- a/third_party/blink/renderer/platform/fonts/segmented_font_data.cc
+++ b/third_party/blink/renderer/platform/fonts/segmented_font_data.cc
@@ -86,4 +86,13 @@
   return false;
 }
 
+bool SegmentedFontData::HasLetterSpacingOverride() const {
+  auto* end = faces_.end();
+  for (auto* it = faces_.begin(); it != end; ++it) {
+    if ((*it)->FontData()->HasLetterSpacingOverride())
+      return true;
+  }
+  return false;
+}
+
 }  // namespace blink
diff --git a/third_party/blink/renderer/platform/fonts/segmented_font_data.h b/third_party/blink/renderer/platform/fonts/segmented_font_data.h
index 17cc3ed..0b237e5 100644
--- a/third_party/blink/renderer/platform/fonts/segmented_font_data.h
+++ b/third_party/blink/renderer/platform/fonts/segmented_font_data.h
@@ -58,6 +58,7 @@
   bool IsLoadingFallback() const override;
   bool IsSegmented() const override;
   bool ShouldSkipDrawing() const override;
+  bool HasLetterSpacingOverride() const override;
 
   Vector<scoped_refptr<FontDataForRangeSet>, 1> faces_;
 };
diff --git a/third_party/blink/renderer/platform/fonts/shaping/caching_word_shape_iterator.h b/third_party/blink/renderer/platform/fonts/shaping/caching_word_shape_iterator.h
index af68756..b0c2647 100644
--- a/third_party/blink/renderer/platform/fonts/shaping/caching_word_shape_iterator.h
+++ b/third_party/blink/renderer/platform/fonts/shaping/caching_word_shape_iterator.h
@@ -58,7 +58,7 @@
 
     // SVG sets SpacingDisabled because it handles spacing by themselves.
     if (!run.SpacingDisabled())
-      spacing_.SetSpacingAndExpansion(font->GetFontDescription());
+      spacing_.SetSpacingAndExpansion(*font);
   }
 
   bool Next(scoped_refptr<const ShapeResult>* word_result) {
diff --git a/third_party/blink/renderer/platform/fonts/shaping/harfbuzz_shaper_test.cc b/third_party/blink/renderer/platform/fonts/shaping/harfbuzz_shaper_test.cc
index 1bbc062..f220ea8 100644
--- a/third_party/blink/renderer/platform/fonts/shaping/harfbuzz_shaper_test.cc
+++ b/third_party/blink/renderer/platform/fonts/shaping/harfbuzz_shaper_test.cc
@@ -714,7 +714,7 @@
   ShapeResultSpacing<String> spacing(string);
   FontDescription font_description;
   font_description.SetLetterSpacing(-5);
-  spacing.SetSpacing(font_description);
+  spacing.SetSpacing(Font(font_description));
   result->ApplySpacing(spacing);
 
   EXPECT_EQ(5 * 5, width - result->Width());
@@ -729,7 +729,7 @@
   ShapeResultSpacing<String> spacing(string);
   FontDescription font_description;
   font_description.SetLetterSpacing(-char_width);
-  spacing.SetSpacing(font_description);
+  spacing.SetSpacing(Font(font_description));
   result->ApplySpacing(spacing);
 
   // EXPECT_EQ(0.0f, result->Width());
@@ -744,7 +744,7 @@
   ShapeResultSpacing<String> spacing(string);
   FontDescription font_description;
   font_description.SetLetterSpacing(-2 * char_width);
-  spacing.SetSpacing(font_description);
+  spacing.SetSpacing(Font(font_description));
   result->ApplySpacing(spacing);
 
   // CSS does not allow negative width, it should be clampled to 0.
diff --git a/third_party/blink/renderer/platform/fonts/shaping/shape_result.cc b/third_party/blink/renderer/platform/fonts/shaping/shape_result.cc
index 7b1c08b..972fdc4a 100644
--- a/third_party/blink/renderer/platform/fonts/shaping/shape_result.cc
+++ b/third_party/blink/renderer/platform/fonts/shaping/shape_result.cc
@@ -854,7 +854,8 @@
       }
 
       space = spacing.ComputeSpacing(
-          run_start_index + glyph_data.character_index, offset);
+          run_start_index + glyph_data.character_index,
+          run->font_data_->GetLetterSpacingOverride(), offset);
       glyph_data.advance += space;
       total_space_for_run += space;
 
diff --git a/third_party/blink/renderer/platform/fonts/shaping/shape_result_spacing.cc b/third_party/blink/renderer/platform/fonts/shaping/shape_result_spacing.cc
index ad24510..349ae05 100644
--- a/third_party/blink/renderer/platform/fonts/shaping/shape_result_spacing.cc
+++ b/third_party/blink/renderer/platform/fonts/shaping/shape_result_spacing.cc
@@ -4,15 +4,17 @@
 
 #include "third_party/blink/renderer/platform/fonts/shaping/shape_result_spacing.h"
 
+#include "third_party/blink/renderer/platform/fonts/font.h"
 #include "third_party/blink/renderer/platform/fonts/font_description.h"
 #include "third_party/blink/renderer/platform/text/text_run.h"
 
 namespace blink {
 
 template <typename TextContainerType>
-bool ShapeResultSpacing<TextContainerType>::SetSpacing(
-    const FontDescription& font_description) {
-  if (!font_description.LetterSpacing() && !font_description.WordSpacing()) {
+bool ShapeResultSpacing<TextContainerType>::SetSpacing(const Font& font) {
+  const FontDescription& font_description = font.GetFontDescription();
+  if (!font_description.LetterSpacing() && !font_description.WordSpacing() &&
+      !font.HasLetterSpacingOverride()) {
     has_spacing_ = false;
     return false;
   }
@@ -41,18 +43,19 @@
 
 template <typename TextContainerType>
 void ShapeResultSpacing<TextContainerType>::SetSpacingAndExpansion(
-    const FontDescription& font_description) {
+    const Font& font) {
   // Available only for TextRun since it has expansion data.
   NOTREACHED();
 }
 
 template <>
-void ShapeResultSpacing<TextRun>::SetSpacingAndExpansion(
-    const FontDescription& font_description) {
+void ShapeResultSpacing<TextRun>::SetSpacingAndExpansion(const Font& font) {
+  const FontDescription& font_description = font.GetFontDescription();
   letter_spacing_ = font_description.LetterSpacing();
   word_spacing_ = font_description.WordSpacing();
   expansion_ = text_.Expansion();
-  has_spacing_ = letter_spacing_ || word_spacing_ || expansion_;
+  has_spacing_ = letter_spacing_ || word_spacing_ || expansion_ ||
+                 font.HasLetterSpacingOverride();
   if (!has_spacing_)
     return;
 
@@ -118,8 +121,10 @@
 }
 
 template <typename TextContainerType>
-float ShapeResultSpacing<TextContainerType>::ComputeSpacing(unsigned index,
-                                                            float& offset) {
+float ShapeResultSpacing<TextContainerType>::ComputeSpacing(
+    unsigned index,
+    float letter_spacing_override,
+    float& offset) {
   DCHECK(has_spacing_);
   UChar32 character = text_[index];
   bool treat_as_space =
@@ -131,8 +136,10 @@
     character = kSpaceCharacter;
 
   float spacing = 0;
-  if (letter_spacing_ && !Character::TreatAsZeroWidthSpace(character))
-    spacing += letter_spacing_;
+
+  bool has_letter_spacing = letter_spacing_ || letter_spacing_override;
+  if (has_letter_spacing && !Character::TreatAsZeroWidthSpace(character))
+    spacing += letter_spacing_ + letter_spacing_override;
 
   if (treat_as_space && (index || character == kNoBreakSpaceCharacter))
     spacing += word_spacing_;
diff --git a/third_party/blink/renderer/platform/fonts/shaping/shape_result_spacing.h b/third_party/blink/renderer/platform/fonts/shaping/shape_result_spacing.h
index a3cbc71..582c803 100644
--- a/third_party/blink/renderer/platform/fonts/shaping/shape_result_spacing.h
+++ b/third_party/blink/renderer/platform/fonts/shaping/shape_result_spacing.h
@@ -11,7 +11,7 @@
 
 namespace blink {
 
-class FontDescription;
+class Font;
 
 // A context object to apply letter-spacing, word-spacing, and justification to
 // ShapeResult.
@@ -42,8 +42,10 @@
     return expansion_opportunity_count_;
   }
 
-  // Set letter-spacing and word-spacing.
-  bool SetSpacing(const FontDescription&);
+  // Set letter-spacing, word-spacing, and letter-spacing-override. Uses a Font
+  // argument instead of FontDescription as letter-spacing-override is retrieved
+  // from CSS @font-face, not from style like word-spacing and letter-spacing.
+  bool SetSpacing(const Font&);
 
   // Set the expansion for the justification.
   void SetExpansion(float expansion,
@@ -52,15 +54,17 @@
                     bool allows_leading_expansion = false,
                     bool allows_trailing_expansion = false);
 
-  // Set letter-spacing, word-spacing, and justification.
-  // Available only for TextRun.
-  void SetSpacingAndExpansion(const FontDescription&);
+  // Set letter-spacing, word-spacing, letter-spacing-override and
+  // justification. Available only for TextRun.
+  void SetSpacingAndExpansion(const Font&);
 
   // Compute the sum of all spacings for the specified |index|.
   // The |index| is for the |TextContainerType| given in the constructor.
   // For justification, this function must be called incrementally since it
   // keeps states and counts consumed justification opportunities.
-  float ComputeSpacing(unsigned index, float& offset);
+  float ComputeSpacing(unsigned index,
+                       float letter_spacing_override,
+                       float& offset);
 
  private:
   bool IsAfterExpansion() const { return is_after_expansion_; }
@@ -88,8 +92,7 @@
 // Forward declare so no implicit instantiations happen before the
 // first explicit instantiation (which would be a C++ violation).
 template <>
-void ShapeResultSpacing<TextRun>::SetSpacingAndExpansion(
-    const FontDescription&);
+void ShapeResultSpacing<TextRun>::SetSpacingAndExpansion(const Font&);
 }  // namespace blink
 
 #endif
diff --git a/third_party/blink/renderer/platform/fonts/simple_font_data.cc b/third_party/blink/renderer/platform/fonts/simple_font_data.cc
index 46cda19..5b4749c 100644
--- a/third_party/blink/renderer/platform/fonts/simple_font_data.cc
+++ b/third_party/blink/renderer/platform/fonts/simple_font_data.cc
@@ -173,6 +173,11 @@
   DCHECK(face);
   if (int units_per_em = face->getUnitsPerEm())
     font_metrics_.SetUnitsPerEm(units_per_em);
+
+  if (metrics_override.letter_spacing_override) {
+    letter_spacing_override_ =
+        *metrics_override.letter_spacing_override * platform_data_.size();
+  }
 }
 
 void SimpleFontData::PlatformGlyphInit() {
diff --git a/third_party/blink/renderer/platform/fonts/simple_font_data.h b/third_party/blink/renderer/platform/fonts/simple_font_data.h
index cf2e191..07cf76b 100644
--- a/third_party/blink/renderer/platform/fonts/simple_font_data.h
+++ b/third_party/blink/renderer/platform/fonts/simple_font_data.h
@@ -153,6 +153,14 @@
     return visual_overflow_inflation_for_descent_;
   }
 
+  bool HasLetterSpacingOverride() const override {
+    return letter_spacing_override_.has_value();
+  }
+
+  float GetLetterSpacingOverride() const {
+    return letter_spacing_override_.value_or(0);
+  }
+
  protected:
   SimpleFontData(
       const FontPlatformData&,
@@ -203,6 +211,10 @@
   unsigned visual_overflow_inflation_for_ascent_;
   unsigned visual_overflow_inflation_for_descent_;
 
+  // The additional spacing between letters as defined by the
+  // letter-spacing-override value in @font-face.
+  base::Optional<float> letter_spacing_override_;
+
   mutable LayoutUnit em_height_ascent_;
   mutable LayoutUnit em_height_descent_;
 
diff --git a/third_party/blink/renderer/platform/graphics/gpu/xr_webgl_drawing_buffer.cc b/third_party/blink/renderer/platform/graphics/gpu/xr_webgl_drawing_buffer.cc
index 8714be68..7771ffdb 100644
--- a/third_party/blink/renderer/platform/graphics/gpu/xr_webgl_drawing_buffer.cc
+++ b/third_party/blink/renderer/platform/graphics/gpu/xr_webgl_drawing_buffer.cc
@@ -216,8 +216,6 @@
 
 void XRWebGLDrawingBuffer::UseSharedBuffer(
     const gpu::MailboxHolder& buffer_mailbox_holder) {
-  DVLOG(3) << __FUNCTION__;
-
   gpu::gles2::GLES2Interface* gl = drawing_buffer_->ContextGL();
 
   // Ensure that the mailbox holder is ready to use, the following actions need
@@ -228,6 +226,10 @@
   // recovery for cases where these assumptions may not be accurate.
   DCHECK(buffer_mailbox_holder.sync_token.HasData());
   DCHECK(!buffer_mailbox_holder.mailbox.IsZero());
+  DVLOG(3) << __func__
+           << ": mailbox=" << buffer_mailbox_holder.mailbox.ToDebugString()
+           << ", SyncToken="
+           << buffer_mailbox_holder.sync_token.ToDebugString();
   gl->WaitSyncTokenCHROMIUM(buffer_mailbox_holder.sync_token.GetConstData());
 
   // Create a texture backed by the shared buffer image.
diff --git a/third_party/blink/renderer/platform/scheduler/common/dummy_schedulers.cc b/third_party/blink/renderer/platform/scheduler/common/dummy_schedulers.cc
index 8f5bde3..38188f7 100644
--- a/third_party/blink/renderer/platform/scheduler/common/dummy_schedulers.cc
+++ b/third_party/blink/renderer/platform/scheduler/common/dummy_schedulers.cc
@@ -243,11 +243,6 @@
     return base::ThreadTaskRunnerHandle::Get();
   }
 
-  scoped_refptr<base::SingleThreadTaskRunner> CleanupTaskRunner() override {
-    DCHECK(WTF::IsMainThread());
-    return base::ThreadTaskRunnerHandle::Get();
-  }
-
   scoped_refptr<base::SingleThreadTaskRunner> V8TaskRunner() override {
     DCHECK(WTF::IsMainThread());
     return base::ThreadTaskRunnerHandle::Get();
diff --git a/third_party/blink/renderer/platform/scheduler/common/web_thread_scheduler.cc b/third_party/blink/renderer/platform/scheduler/common/web_thread_scheduler.cc
index 641f84c..d6f37ee 100644
--- a/third_party/blink/renderer/platform/scheduler/common/web_thread_scheduler.cc
+++ b/third_party/blink/renderer/platform/scheduler/common/web_thread_scheduler.cc
@@ -79,12 +79,6 @@
 }
 
 scoped_refptr<base::SingleThreadTaskRunner>
-WebThreadScheduler::CleanupTaskRunner() {
-  NOTREACHED();
-  return nullptr;
-}
-
-scoped_refptr<base::SingleThreadTaskRunner>
 WebThreadScheduler::DeprecatedDefaultTaskRunner() {
   NOTREACHED();
   return nullptr;
diff --git a/third_party/blink/renderer/platform/scheduler/main_thread/frame_scheduler_impl.cc b/third_party/blink/renderer/platform/scheduler/main_thread/frame_scheduler_impl.cc
index 537aebf..d95f96ca 100644
--- a/third_party/blink/renderer/platform/scheduler/main_thread/frame_scheduler_impl.cc
+++ b/third_party/blink/renderer/platform/scheduler/main_thread/frame_scheduler_impl.cc
@@ -473,7 +473,6 @@
     case TaskType::kMainThreadTaskQueueIdle:
     case TaskType::kMainThreadTaskQueueIPC:
     case TaskType::kMainThreadTaskQueueControl:
-    case TaskType::kMainThreadTaskQueueCleanup:
     case TaskType::kMainThreadTaskQueueMemoryPurge:
     case TaskType::kCompositorThreadTaskQueueDefault:
     case TaskType::kCompositorThreadTaskQueueInput:
diff --git a/third_party/blink/renderer/platform/scheduler/main_thread/main_thread_scheduler_impl.cc b/third_party/blink/renderer/platform/scheduler/main_thread/main_thread_scheduler_impl.cc
index c3cb873..a3188441 100644
--- a/third_party/blink/renderer/platform/scheduler/main_thread/main_thread_scheduler_impl.cc
+++ b/third_party/blink/renderer/platform/scheduler/main_thread/main_thread_scheduler_impl.cc
@@ -234,8 +234,6 @@
       MainThreadTaskQueue::QueueType::kV8));
   ipc_task_queue_ = NewTaskQueue(MainThreadTaskQueue::QueueCreationParams(
       MainThreadTaskQueue::QueueType::kIPC));
-  cleanup_task_queue_ = NewTaskQueue(MainThreadTaskQueue::QueueCreationParams(
-      MainThreadTaskQueue::QueueType::kCleanup));
   non_waking_task_queue_ =
       NewTaskQueue(MainThreadTaskQueue::QueueCreationParams(
                        MainThreadTaskQueue::QueueType::kNonWaking)
@@ -249,8 +247,6 @@
       TaskType::kMainThreadTaskQueueControl);
   ipc_task_runner_ =
       ipc_task_queue_->CreateTaskRunner(TaskType::kMainThreadTaskQueueIPC);
-  cleanup_task_runner_ = cleanup_task_queue_->CreateTaskRunner(
-      TaskType::kMainThreadTaskQueueCleanup);
   non_waking_task_runner_ = non_waking_task_queue_->CreateTaskRunner(
       TaskType::kMainThreadTaskQueueNonWaking);
 
@@ -660,11 +656,6 @@
 }
 
 scoped_refptr<base::SingleThreadTaskRunner>
-MainThreadSchedulerImpl::CleanupTaskRunner() {
-  return cleanup_task_runner_;
-}
-
-scoped_refptr<base::SingleThreadTaskRunner>
 MainThreadSchedulerImpl::DeprecatedDefaultTaskRunner() {
   return helper_.DeprecatedDefaultTaskRunner();
 }
diff --git a/third_party/blink/renderer/platform/scheduler/main_thread/main_thread_scheduler_impl.h b/third_party/blink/renderer/platform/scheduler/main_thread/main_thread_scheduler_impl.h
index dc65f93..b2b7914a 100644
--- a/third_party/blink/renderer/platform/scheduler/main_thread/main_thread_scheduler_impl.h
+++ b/third_party/blink/renderer/platform/scheduler/main_thread/main_thread_scheduler_impl.h
@@ -161,7 +161,6 @@
   std::unique_ptr<WebWidgetScheduler> CreateWidgetScheduler() override;
   // Note: this is also shared by the ThreadScheduler interface.
   scoped_refptr<base::SingleThreadTaskRunner> IPCTaskRunner() override;
-  scoped_refptr<base::SingleThreadTaskRunner> CleanupTaskRunner() override;
   scoped_refptr<base::SingleThreadTaskRunner> NonWakingTaskRunner() override;
   scoped_refptr<base::SingleThreadTaskRunner> DeprecatedDefaultTaskRunner()
       override;
@@ -839,7 +838,6 @@
 
   scoped_refptr<MainThreadTaskQueue> v8_task_queue_;
   scoped_refptr<MainThreadTaskQueue> ipc_task_queue_;
-  scoped_refptr<MainThreadTaskQueue> cleanup_task_queue_;
   scoped_refptr<MainThreadTaskQueue> memory_purge_task_queue_;
   scoped_refptr<MainThreadTaskQueue> non_waking_task_queue_;
 
@@ -847,7 +845,6 @@
   scoped_refptr<base::SingleThreadTaskRunner> compositor_task_runner_;
   scoped_refptr<base::SingleThreadTaskRunner> control_task_runner_;
   scoped_refptr<base::SingleThreadTaskRunner> ipc_task_runner_;
-  scoped_refptr<base::SingleThreadTaskRunner> cleanup_task_runner_;
   scoped_refptr<base::SingleThreadTaskRunner> non_waking_task_runner_;
 
   MemoryPurgeManager memory_purge_manager_;
diff --git a/third_party/blink/renderer/platform/scheduler/main_thread/main_thread_task_queue.cc b/third_party/blink/renderer/platform/scheduler/main_thread/main_thread_task_queue.cc
index 4c8ed578..2c8614e 100644
--- a/third_party/blink/renderer/platform/scheduler/main_thread/main_thread_task_queue.cc
+++ b/third_party/blink/renderer/platform/scheduler/main_thread/main_thread_task_queue.cc
@@ -55,8 +55,6 @@
       return "input_tq";
     case MainThreadTaskQueue::QueueType::kDetached:
       return "detached_tq";
-    case MainThreadTaskQueue::QueueType::kCleanup:
-      return "cleanup_tq";
     case MainThreadTaskQueue::QueueType::kOther:
       return "other_tq";
     case MainThreadTaskQueue::QueueType::kWebScheduling:
@@ -93,7 +91,6 @@
     case MainThreadTaskQueue::QueueType::kIPC:
     case MainThreadTaskQueue::QueueType::kInput:
     case MainThreadTaskQueue::QueueType::kDetached:
-    case MainThreadTaskQueue::QueueType::kCleanup:
     case MainThreadTaskQueue::QueueType::kNonWaking:
     case MainThreadTaskQueue::QueueType::kOther:
       return false;
@@ -115,7 +112,6 @@
     case QueueType::kV8:
     case QueueType::kIPC:
     case QueueType::kNonWaking:
-    case QueueType::kCleanup:
       return QueueClass::kNone;
     case QueueType::kFrameLoading:
     case QueueType::kFrameLoadingControl:
diff --git a/third_party/blink/renderer/platform/scheduler/main_thread/main_thread_task_queue.h b/third_party/blink/renderer/platform/scheduler/main_thread/main_thread_task_queue.h
index 5689e50..1b6b45a 100644
--- a/third_party/blink/renderer/platform/scheduler/main_thread/main_thread_task_queue.h
+++ b/third_party/blink/renderer/platform/scheduler/main_thread/main_thread_task_queue.h
@@ -71,8 +71,7 @@
     // TODO(altimin): Move to the top when histogram is renumbered.
     kDetached = 19,
 
-    kCleanup = 20,
-
+    // 20 : kCleanup, obsolete.
     // 21 : kWebSchedulingUserInteraction, obsolete.
     // 22 : kWebSchedulingBestEffort, obsolete.
 
diff --git a/third_party/blink/renderer/platform/scheduler/main_thread/task_type_names.cc b/third_party/blink/renderer/platform/scheduler/main_thread/task_type_names.cc
index f462aab..c62b378 100644
--- a/third_party/blink/renderer/platform/scheduler/main_thread/task_type_names.cc
+++ b/third_party/blink/renderer/platform/scheduler/main_thread/task_type_names.cc
@@ -105,8 +105,6 @@
       return "MainThreadTaskQueueIPC";
     case TaskType::kMainThreadTaskQueueControl:
       return "MainThreadTaskQueueControl";
-    case TaskType::kMainThreadTaskQueueCleanup:
-      return "MainThreadTaskQueueCleanup";
     case TaskType::kMainThreadTaskQueueMemoryPurge:
       return "MainThreadTaskQueueMemoryPurge";
     case TaskType::kMainThreadTaskQueueNonWaking:
diff --git a/third_party/blink/renderer/platform/scheduler/test/renderer_scheduler_test_support.cc b/third_party/blink/renderer/platform/scheduler/test/renderer_scheduler_test_support.cc
index 95619cc..d99f517 100644
--- a/third_party/blink/renderer/platform/scheduler/test/renderer_scheduler_test_support.cc
+++ b/third_party/blink/renderer/platform/scheduler/test/renderer_scheduler_test_support.cc
@@ -32,10 +32,6 @@
     return base::ThreadTaskRunnerHandle::Get();
   }
 
-  scoped_refptr<base::SingleThreadTaskRunner> CleanupTaskRunner() override {
-    return base::ThreadTaskRunnerHandle::Get();
-  }
-
   std::unique_ptr<Thread> CreateMainThread() override {
     return simple_thread_scheduler_->CreateMainThread();
   }
diff --git a/third_party/blink/renderer/platform/scheduler/worker/worker_scheduler.cc b/third_party/blink/renderer/platform/scheduler/worker/worker_scheduler.cc
index 04fa08a..b3274af 100644
--- a/third_party/blink/renderer/platform/scheduler/worker/worker_scheduler.cc
+++ b/third_party/blink/renderer/platform/scheduler/worker/worker_scheduler.cc
@@ -196,7 +196,6 @@
     case TaskType::kMainThreadTaskQueueIdle:
     case TaskType::kMainThreadTaskQueueIPC:
     case TaskType::kMainThreadTaskQueueControl:
-    case TaskType::kMainThreadTaskQueueCleanup:
     case TaskType::kMainThreadTaskQueueMemoryPurge:
     case TaskType::kMainThreadTaskQueueNonWaking:
     case TaskType::kCompositorThreadTaskQueueDefault:
diff --git a/third_party/blink/web_tests/TestExpectations b/third_party/blink/web_tests/TestExpectations
index 23a00432..1d5a611 100644
--- a/third_party/blink/web_tests/TestExpectations
+++ b/third_party/blink/web_tests/TestExpectations
@@ -1307,9 +1307,6 @@
 
 # Fieldset in NG
 #
-crbug.com/875235 virtual/layout_ng_fieldset/external/wpt/html/rendering/non-replaced-elements/the-fieldset-and-legend-elements/legend-align.html [ Failure ]
-crbug.com/875235 virtual/layout_ng_fieldset/external/wpt/html/rendering/non-replaced-elements/the-fieldset-and-legend-elements/legend-auto-margins.html [ Failure ]
-crbug.com/875235 virtual/layout_ng_fieldset/external/wpt/html/rendering/non-replaced-elements/the-fieldset-and-legend-elements/legend-margin-inline.html [ Failure ]
 
 ### virtual/layout_ng_fieldset/fast/forms/fieldset/
 crbug.com/875235 virtual/layout_ng_fieldset/fast/forms/fieldset/legend-small-after-margin-before-border-horizontal-mode.html [ Failure ]
@@ -6851,9 +6848,6 @@
 
 crbug.com/1111410 http/tests/serviceworker/service-worker-gc.html [ Pass Failure ]
 
-# Sheriff 2020-08-01
-crbug.com/1112111 fast/forms/month/month-picker-appearance-step.html [ Pass Failure ]
-
 # Sheriff 2020-08-03
 crbug.com/1108423 external/wpt/referrer-policy/gen/worker-classic.http-rp/unset/worker-classic.http.html [ Pass Timeout ]
 crbug.com/1106429 virtual/percent-based-scrolling/max-percent-delta-page-zoom.html [ Pass Failure ]
@@ -6867,3 +6861,6 @@
 crbug.com/1112771 external/wpt/css/css-grid/layout-algorithm/grid-flex-track-intrinsic-sizes-001.html [ Failure ]
 crbug.com/1112771 external/wpt/webhid/idlharness.https.window.html [ Failure ]
 crbug.com/1112771 external/wpt/css/css-grid/grid-definition/grid-inline-template-columns-rows-resolved-values-001.tentative.html [ Failure ]
+
+# Sheriff 2020-08-05
+crbug.com/1113050 fast/borders/border-radius-mask-video-ratio.html [ Pass Failure ]
diff --git a/third_party/blink/web_tests/external/wpt/webaudio/the-audio-api/the-periodicwave-interface/periodicWave-expected.txt b/third_party/blink/web_tests/external/wpt/webaudio/the-audio-api/the-periodicwave-interface/periodicWave-expected.txt
deleted file mode 100644
index 8d26f7e..0000000
--- a/third_party/blink/web_tests/external/wpt/webaudio/the-audio-api/the-periodicwave-interface/periodicWave-expected.txt
+++ /dev/null
@@ -1,34 +0,0 @@
-This is a testharness.js-based test.
-PASS # AUDIT TASK RUNNER STARTED.
-PASS Executing "create with factory method"
-PASS Executing "different length with factory method"
-PASS Executing "too small with factory method"
-PASS Executing "create with constructor"
-PASS Executing "different length with constructor"
-PASS Executing "too small with constructor"
-PASS Executing "output test"
-PASS Audit report
-PASS > [create with factory method] 
-PASS   context.createPeriodicWave(new Float32Array(4096), new Float32Array(4096)) did not throw an exception.
-PASS < [create with factory method] All assertions passed. (total 1 assertions)
-PASS > [different length with factory method] 
-PASS   context.createPeriodicWave(new Float32Array(512), new Float32Array(4)) threw IndexSizeError: "Failed to execute 'createPeriodicWave' on 'BaseAudioContext': length of real array (512) and length of imaginary array (4) must match.".
-PASS < [different length with factory method] All assertions passed. (total 1 assertions)
-PASS > [too small with factory method] 
-FAIL X context.createPeriodicWave(new Float32Array(1), new Float32Array(1)) did not throw an exception. assert_true: expected true got false
-FAIL < [too small with factory method] 1 out of 1 assertions were failed. assert_true: expected true got false
-PASS > [create with constructor] 
-PASS   new PeriodicWave(context, { real : new Float32Array(4096), imag : new Float32Array(4096) }) did not throw an exception.
-PASS < [create with constructor] All assertions passed. (total 1 assertions)
-PASS > [different length with constructor] 
-PASS   new PeriodicWave(context, { real : new Float32Array(4096), imag : new Float32Array(4) }) threw IndexSizeError: "Failed to construct 'PeriodicWave': length of real array (4096) and length of imaginary array (4) must match.".
-PASS < [different length with constructor] All assertions passed. (total 1 assertions)
-PASS > [too small with constructor] 
-FAIL X new PeriodicWave(context, { real : new Float32Array(1), imag : new Float32Array(1) }) did not throw an exception. assert_true: expected true got false
-FAIL < [too small with constructor] 1 out of 1 assertions were failed. assert_true: expected true got false
-PASS > [output test] 
-PASS   rendering PeriodicWave is identical to the array AudioBuffer.
-PASS < [output test] All assertions passed. (total 1 assertions)
-FAIL # AUDIT TASK RUNNER FINISHED: 2 out of 7 tasks were failed. assert_true: expected true got false
-Harness: the test ran to completion.
-
diff --git a/third_party/blink/web_tests/fast/forms/month/month-picker-appearance-step.html b/third_party/blink/web_tests/fast/forms/month/month-picker-appearance-step.html
index 055551c2..fc7291b 100644
--- a/third_party/blink/web_tests/fast/forms/month/month-picker-appearance-step.html
+++ b/third_party/blink/web_tests/fast/forms/month/month-picker-appearance-step.html
@@ -3,7 +3,7 @@
 testRunner.waitUntilDone();
 </script>
 <script src="../../../fast/forms/resources/picker-common.js"></script>
-<input type=month id=month value="2019-08" step="3">
+<input type="month" id="month" value="2019-08" step="3" max="2020-01">
 <script>
 openPicker(document.getElementById('month'), () => testRunner.notifyDone());
 </script>
diff --git a/third_party/blink/web_tests/platform/linux/fast/forms/month/month-picker-appearance-step-expected.png b/third_party/blink/web_tests/platform/linux/fast/forms/month/month-picker-appearance-step-expected.png
index 009b9c3..c53df59 100644
--- a/third_party/blink/web_tests/platform/linux/fast/forms/month/month-picker-appearance-step-expected.png
+++ b/third_party/blink/web_tests/platform/linux/fast/forms/month/month-picker-appearance-step-expected.png
Binary files differ
diff --git a/third_party/blink/web_tests/platform/linux/virtual/layout_ng_fieldset/external/wpt/html/rendering/non-replaced-elements/the-fieldset-and-legend-elements/legend-align-expected.txt b/third_party/blink/web_tests/platform/linux/virtual/layout_ng_fieldset/external/wpt/html/rendering/non-replaced-elements/the-fieldset-and-legend-elements/legend-align-expected.txt
new file mode 100644
index 0000000..2c1519b
--- /dev/null
+++ b/third_party/blink/web_tests/platform/linux/virtual/layout_ng_fieldset/external/wpt/html/rendering/non-replaced-elements/the-fieldset-and-legend-elements/legend-align-expected.txt
@@ -0,0 +1,13 @@
+This is a testharness.js-based test.
+PASS div[align=left] legend
+PASS div[align=center] legend
+PASS div[align=right] legend
+PASS div[align=justify] legend
+FAIL div[style="text-align: center"] legend assert_equals: expected legend[align=left] expected 24 but got 394
+FAIL div[style="text-align: center"][align=center] legend assert_equals: expected legend[align=left] expected 24 but got 394
+PASS legend[style="margin: 0 auto"]
+PASS legend[style="margin: 0 0 0 auto"]
+PASS fieldset[dir=rtl] legend
+FAIL fieldset[dir=rtl] legend[style="text-align: left"] assert_equals: expected legend[align=right] expected 764 but got 24
+Harness: the test ran to completion.
+
diff --git a/third_party/blink/web_tests/platform/mac-mac10.10/fast/forms/month/month-picker-appearance-step-expected.png b/third_party/blink/web_tests/platform/mac-mac10.10/fast/forms/month/month-picker-appearance-step-expected.png
index 33a7cba..35f5be9a 100644
--- a/third_party/blink/web_tests/platform/mac-mac10.10/fast/forms/month/month-picker-appearance-step-expected.png
+++ b/third_party/blink/web_tests/platform/mac-mac10.10/fast/forms/month/month-picker-appearance-step-expected.png
Binary files differ
diff --git a/third_party/blink/web_tests/platform/mac-retina/fast/forms/month/month-picker-appearance-step-expected.png b/third_party/blink/web_tests/platform/mac-retina/fast/forms/month/month-picker-appearance-step-expected.png
new file mode 100644
index 0000000..cc91228
--- /dev/null
+++ b/third_party/blink/web_tests/platform/mac-retina/fast/forms/month/month-picker-appearance-step-expected.png
Binary files differ
diff --git a/third_party/blink/web_tests/platform/mac/fast/forms/month/month-picker-appearance-step-expected.png b/third_party/blink/web_tests/platform/mac/fast/forms/month/month-picker-appearance-step-expected.png
index cc91228..9426e5de 100644
--- a/third_party/blink/web_tests/platform/mac/fast/forms/month/month-picker-appearance-step-expected.png
+++ b/third_party/blink/web_tests/platform/mac/fast/forms/month/month-picker-appearance-step-expected.png
Binary files differ
diff --git a/third_party/blink/web_tests/platform/mac/virtual/layout_ng_fieldset/external/wpt/html/rendering/non-replaced-elements/the-fieldset-and-legend-elements/legend-align-expected.txt b/third_party/blink/web_tests/platform/mac/virtual/layout_ng_fieldset/external/wpt/html/rendering/non-replaced-elements/the-fieldset-and-legend-elements/legend-align-expected.txt
new file mode 100644
index 0000000..2c1519b
--- /dev/null
+++ b/third_party/blink/web_tests/platform/mac/virtual/layout_ng_fieldset/external/wpt/html/rendering/non-replaced-elements/the-fieldset-and-legend-elements/legend-align-expected.txt
@@ -0,0 +1,13 @@
+This is a testharness.js-based test.
+PASS div[align=left] legend
+PASS div[align=center] legend
+PASS div[align=right] legend
+PASS div[align=justify] legend
+FAIL div[style="text-align: center"] legend assert_equals: expected legend[align=left] expected 24 but got 394
+FAIL div[style="text-align: center"][align=center] legend assert_equals: expected legend[align=left] expected 24 but got 394
+PASS legend[style="margin: 0 auto"]
+PASS legend[style="margin: 0 0 0 auto"]
+PASS fieldset[dir=rtl] legend
+FAIL fieldset[dir=rtl] legend[style="text-align: left"] assert_equals: expected legend[align=right] expected 764 but got 24
+Harness: the test ran to completion.
+
diff --git a/third_party/blink/web_tests/platform/win/fast/forms/month/month-picker-appearance-step-expected.png b/third_party/blink/web_tests/platform/win/fast/forms/month/month-picker-appearance-step-expected.png
index a9ced9f9..6a21cf5f 100644
--- a/third_party/blink/web_tests/platform/win/fast/forms/month/month-picker-appearance-step-expected.png
+++ b/third_party/blink/web_tests/platform/win/fast/forms/month/month-picker-appearance-step-expected.png
Binary files differ
diff --git a/third_party/blink/web_tests/platform/win/virtual/layout_ng_fieldset/external/wpt/html/rendering/non-replaced-elements/the-fieldset-and-legend-elements/legend-align-expected.txt b/third_party/blink/web_tests/platform/win/virtual/layout_ng_fieldset/external/wpt/html/rendering/non-replaced-elements/the-fieldset-and-legend-elements/legend-align-expected.txt
new file mode 100644
index 0000000..7493d20
--- /dev/null
+++ b/third_party/blink/web_tests/platform/win/virtual/layout_ng_fieldset/external/wpt/html/rendering/non-replaced-elements/the-fieldset-and-legend-elements/legend-align-expected.txt
@@ -0,0 +1,13 @@
+This is a testharness.js-based test.
+PASS div[align=left] legend
+PASS div[align=center] legend
+PASS div[align=right] legend
+PASS div[align=justify] legend
+FAIL div[style="text-align: center"] legend assert_equals: expected legend[align=left] expected 24 but got 395
+FAIL div[style="text-align: center"][align=center] legend assert_equals: expected legend[align=left] expected 24 but got 395
+PASS legend[style="margin: 0 auto"]
+PASS legend[style="margin: 0 0 0 auto"]
+PASS fieldset[dir=rtl] legend
+FAIL fieldset[dir=rtl] legend[style="text-align: left"] assert_equals: expected legend[align=right] expected 765 but got 24
+Harness: the test ran to completion.
+
diff --git a/third_party/blink/web_tests/platform/win7/fast/forms/month/month-picker-appearance-step-expected.png b/third_party/blink/web_tests/platform/win7/fast/forms/month/month-picker-appearance-step-expected.png
index 2a2d0954a..6d25f2a 100644
--- a/third_party/blink/web_tests/platform/win7/fast/forms/month/month-picker-appearance-step-expected.png
+++ b/third_party/blink/web_tests/platform/win7/fast/forms/month/month-picker-appearance-step-expected.png
Binary files differ
diff --git a/third_party/blink/web_tests/webaudio/internals/audiocontext-close.html b/third_party/blink/web_tests/webaudio/internals/audiocontext-close.html
index a495261e..91b6449 100644
--- a/third_party/blink/web_tests/webaudio/internals/audiocontext-close.html
+++ b/third_party/blink/web_tests/webaudio/internals/audiocontext-close.html
@@ -17,7 +17,7 @@
       let osc;
       let gain;
       let offlinePromise;
-      let wave = new Float32Array(1);
+      let wave = new Float32Array(5);
 
       let audit = Audit.createTaskRunner();
 
diff --git a/third_party/blink/web_tests/wpt_internal/css/css-fonts/letter-spacing-override-ref.html b/third_party/blink/web_tests/wpt_internal/css/css-fonts/letter-spacing-override-ref.html
new file mode 100644
index 0000000..6fdd7f1a
--- /dev/null
+++ b/third_party/blink/web_tests/wpt_internal/css/css-fonts/letter-spacing-override-ref.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" href="mailto:xiaochengh@chromium.org">
+<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/4792">
+<link rel="assert" title="Tests that letter-spacing-override sets letter spacing to characters rendered with the font face only">
+<title>Tests the letter-spacing-override descriptor of @font-face</title>
+<style>
+@font-face {
+  font-family: custom-font;
+  src: local(Ahem), url(/fonts/Ahem.ttf);
+  unicode-range: U+0-7F; /* ASCII only */
+}
+
+.target {
+  font: 20px custom-font, sans-serif;
+}
+
+.letter-spacing {
+  letter-spacing: 1em;
+}
+
+</style>
+
+<p>letter-spacing-override should affect Ahem characters only.</p>
+
+<div class="target">
+  <span class="letter-spacing">XXX</span>一二三<span class="letter-spacing">XXX</span>
+</div>
+
+<p>letter-spacing-override: 0 should be the same as no override.</p>
+
+<div class="target">
+  XXX一二三XXX
+</div>
diff --git a/third_party/blink/web_tests/wpt_internal/css/css-fonts/letter-spacing-override.html b/third_party/blink/web_tests/wpt_internal/css/css-fonts/letter-spacing-override.html
new file mode 100644
index 0000000..b6bd045
--- /dev/null
+++ b/third_party/blink/web_tests/wpt_internal/css/css-fonts/letter-spacing-override.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" href="mailto:xiaochengh@chromium.org">
+<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/4792">
+<link rel="match" href="letter-spacing-override-ref.html">
+<link rel="assert" title="Tests that letter-spacing-override sets letter spacing to characters rendered with the font face only">
+<title>Tests the letter-spacing-override descriptor of @font-face</title>
+<style>
+@font-face {
+  font-family: custom-font;
+  src: local(Ahem), url(/fonts/Ahem.ttf);
+  letter-spacing-override: 1.0;
+  unicode-range: U+0-7F; /* ASCII only */
+}
+
+.target {
+  font: 20px custom-font, sans-serif;
+}
+
+@font-face {
+  font-family: reference-font;
+  src: local(Ahem), url(/fonts/Ahem.ttf);
+  letter-spacing-override: 0;
+  unicode-range: U+0-7F; /* ASCII only */
+}
+
+.reference {
+  font: 20px reference-font, sans-serif;
+}
+
+</style>
+
+<p>letter-spacing-override should affect Ahem characters only.</p>
+
+<div class="target">
+  XXX一二三XXX
+</div>
+
+<p>letter-spacing-override: 0 should be the same as no override.</p>
+
+<div class="reference">
+  XXX一二三XXX
+</div>
diff --git a/third_party/closure_compiler/externs/file_manager_private.js b/third_party/closure_compiler/externs/file_manager_private.js
index 8e040f3..299b523 100644
--- a/third_party/closure_compiler/externs/file_manager_private.js
+++ b/third_party/closure_compiler/externs/file_manager_private.js
@@ -1056,6 +1056,14 @@
  */
 chrome.fileManagerPrivate.sharesheetHasTargets = function(entries, callback) {};
 
+/**
+ * Invoke Sharesheet for selected files. If not possible, then returns
+ * an error via chrome.runtime.lastError. |entries| Array of selected entries.
+ * @param {!Array<!Entry>} entries
+ * @param {function()} callback
+ */
+chrome.fileManagerPrivate.invokeSharesheet = function(entries, callback) {};
+
 /** @type {!ChromeEvent} */
 chrome.fileManagerPrivate.onMountCompleted;
 
diff --git a/tools/generate_stubs/generate_stubs.py b/tools/generate_stubs/generate_stubs.py
index 27a11ed..9cf65c7 100755
--- a/tools/generate_stubs/generate_stubs.py
+++ b/tools/generate_stubs/generate_stubs.py
@@ -311,8 +311,8 @@
         module_opened = true;
         opened_libraries[cur_module] = handle;
       } else {
-        %(logging_function)s << "dlopen(" << dso_path->c_str() << ") failed, "
-                << "dlerror() says:\\n" << dlerror();
+        %(logging_function)s << "dlopen(" << dso_path->c_str() << ") failed.";
+        %(logging_function)s << "dlerror() says:\\n" << dlerror();
       }
     }
 
diff --git a/tools/json_schema_compiler/cpp_bundle_generator.py b/tools/json_schema_compiler/cpp_bundle_generator.py
index 3c7ad43b..4e4fbf3 100644
--- a/tools/json_schema_compiler/cpp_bundle_generator.py
+++ b/tools/json_schema_compiler/cpp_bundle_generator.py
@@ -143,7 +143,13 @@
     ifdefs = []
     for platform in model_object.platforms:
       if platform == Platforms.CHROMEOS:
-        ifdefs.append('defined(OS_CHROMEOS)')
+        # TODO(https://crbug.com/1052397): For readability, this should become
+        # defined(OS_CHROMEOS) && BUILDFLAG(IS_ASH).
+        ifdefs.append('(defined(OS_CHROMEOS) && !BUILDFLAG(IS_LACROS))')
+      elif platform == Platforms.LACROS:
+        # TODO(https://crbug.com/1052397): For readability, this should become
+        # defined(OS_CHROMEOS) && BUILDFLAG(IS_LACROS).
+        ifdefs.append('BUILDFLAG(IS_LACROS)')
       elif platform == Platforms.LINUX:
         ifdefs.append('(defined(OS_LINUX) && !defined(OS_CHROMEOS))')
       elif platform == Platforms.MAC:
@@ -252,6 +258,9 @@
         os.path.join(self._bundle._impl_dir,
                      'generated_api_registration.h')))
     c.Append()
+    c.Append('#include "build/build_config.h"')
+    c.Append('#include "build/lacros_buildflags.h"')
+    c.Append()
     for namespace in self._bundle._model.namespaces.values():
       namespace_name = namespace.unix_name.replace("experimental_", "")
       implementation_header = namespace.compiler_options.get(
diff --git a/tools/json_schema_compiler/cpp_bundle_generator_test.py b/tools/json_schema_compiler/cpp_bundle_generator_test.py
index 2241ecd..0994d06 100755
--- a/tools/json_schema_compiler/cpp_bundle_generator_test.py
+++ b/tools/json_schema_compiler/cpp_bundle_generator_test.py
@@ -36,15 +36,15 @@
         'test/function_platform_all.json')
     self.assertEquals(
         'defined(OS_WIN) || (defined(OS_LINUX) && !defined(OS_CHROMEOS)) || '
-        'defined(OS_CHROMEOS)',
+        '(defined(OS_CHROMEOS) && !BUILDFLAG(IS_LACROS))',
         _getPlatformIfdefs(cpp_bundle_generator, model))
 
   def testIfDefsForChromeOS(self):
     cpp_bundle_generator, model = _createCppBundleGenerator(
         'test/function_platform_chromeos.json')
-    self.assertEquals(
-        'defined(OS_CHROMEOS)',
-        _getPlatformIfdefs(cpp_bundle_generator, model))
+    self.assertEquals('(defined(OS_CHROMEOS) && !BUILDFLAG(IS_LACROS))',
+                      _getPlatformIfdefs(cpp_bundle_generator, model))
+
 
 if __name__ == '__main__':
   unittest.main()
diff --git a/tools/json_schema_compiler/feature_compiler.py b/tools/json_schema_compiler/feature_compiler.py
index fbdeaf6..acb4e45 100644
--- a/tools/json_schema_compiler/feature_compiler.py
+++ b/tools/json_schema_compiler/feature_compiler.py
@@ -240,6 +240,7 @@
         list: {
             'enum_map': {
                 'chromeos': 'Feature::CHROMEOS_PLATFORM',
+                'lacros': 'Feature::LACROS_PLATFORM',
                 'linux': 'Feature::LINUX_PLATFORM',
                 'mac': 'Feature::MACOSX_PLATFORM',
                 'win': 'Feature::WIN_PLATFORM',
diff --git a/tools/json_schema_compiler/model.py b/tools/json_schema_compiler/model.py
index cafd0e1d..49f2855 100644
--- a/tools/json_schema_compiler/model.py
+++ b/tools/json_schema_compiler/model.py
@@ -605,6 +605,7 @@
   """
   CHROMEOS = _PlatformInfo("chromeos")
   CHROMEOS_TOUCH = _PlatformInfo("chromeos_touch")
+  LACROS = _PlatformInfo("lacros")
   LINUX = _PlatformInfo("linux")
   MAC = _PlatformInfo("mac")
   WIN = _PlatformInfo("win")
diff --git a/tools/mb/mb_config.pyl b/tools/mb/mb_config.pyl
index 2f4569c6..1bb02656 100644
--- a/tools/mb/mb_config.pyl
+++ b/tools/mb/mb_config.pyl
@@ -135,6 +135,7 @@
       'linux-chromeos-rel': 'chromeos_with_codecs_release_bot',
       'linux-chromeos-dbg': 'chromeos_with_codecs_debug_bot',
       'linux-lacros-builder-rel': 'lacros_on_linux_release_bot',
+      'linux-lacros-compile-rel': 'lacros_on_linux_release_bot',
       'linux-lacros-tester-rel': 'lacros_on_linux_release_bot',
     },
 
@@ -888,7 +889,6 @@
       'linux-clang-tidy-rel': 'release_trybot',
       'linux-dcheck-off-rel': 'release_trybot_dcheck_off',
       'linux-gcc-rel': 'release_bot_x86_minimal_symbols_no_clang_cxx11',
-      'linux-lacros-compile-rel': 'lacros_on_linux_release_bot',
       'linux-lacros-fyi-rel': 'lacros_on_linux_release_bot',
       'linux-libfuzzer-asan-rel': 'libfuzzer_asan_release_trybot',
       'linux-ozone-rel': 'ozone_linux_release_trybot',
@@ -2854,7 +2854,7 @@
     },
 
     'official_optimize': {
-      'gn_args': 'is_official_build=true is_debug=false',
+      'gn_args': 'is_official_build=true',
     },
 
     'official_optimize_goma': {
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index 84b1147..7247cf7f 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -24126,6 +24126,7 @@
       label="AUTOTESTPRIVATE_STOPTHROUGHPUTTRACKERDATACOLLECTION"/>
   <int value="1494" label="INPUTMETHODPRIVATE_GETAUTOCORRECTRANGE"/>
   <int value="1495" label="FILEMANAGERPRIVATEINTERNAL_SHARESHEETHASTARGETS"/>
+  <int value="1496" label="FILEMANAGERPRIVATEINTERNAL_INVOKESHARESHEET"/>
 </enum>
 
 <enum name="ExtensionIconState">
@@ -39509,6 +39510,12 @@
   <int value="3" label="Holdback"/>
 </enum>
 
+<enum name="LiteVideoThrottleResult">
+  <int value="0" label="None"/>
+  <int value="1" label="Throttled without stopping"/>
+  <int value="2" label="Throttled but stopped due to rebuffer"/>
+</enum>
+
 <enum name="LiveCaptionsSessionEvent">
   <int value="0" label="Stream started"/>
   <int value="1" label="Stream finished"/>
@@ -39928,6 +39935,7 @@
   <int value="-2020721975" label="smart-virtual-keyboard"/>
   <int value="-2020024440" label="scroll-end-effect"/>
   <int value="-2017953534" label="enable-hosted-app-shim-creation"/>
+  <int value="-2017778637" label="PrintSaveToDrive:disabled"/>
   <int value="-2015293660" label="AccessibilityExposeDisplayNone:disabled"/>
   <int value="-2013551096" label="ViewsSimplifiedFullscreenUI:disabled"/>
   <int value="-2013124655" label="EnableEphemeralFlashPermission:disabled"/>
@@ -41427,6 +41435,7 @@
   <int value="-470247915"
       label="AutofillUpstreamEditableExpirationDate:enabled"/>
   <int value="-468697885" label="ArcInputMethod:enabled"/>
+  <int value="-467792766" label="ReengagementNotification:enabled"/>
   <int value="-466704882" label="webview-log-js-console-messages"/>
   <int value="-465381408" label="Sharesheet:disabled"/>
   <int value="-462554210"
@@ -42006,6 +42015,7 @@
   <int value="173288154" label="PrintPdfAsImage:enabled"/>
   <int value="173339199" label="SmsReceiverCrossDevice:disabled"/>
   <int value="174759256" label="LockScreenMediaControls:enabled"/>
+  <int value="174917935" label="ReengagementNotification:disabled"/>
   <int value="178337215" label="enable-md-history"/>
   <int value="178693406" label="LockScreenMediaControls:disabled"/>
   <int value="180074362" label="memory-pressure-thresholds"/>
@@ -42391,6 +42401,7 @@
   <int value="606723570" label="SharingUseDeviceInfo:disabled"/>
   <int value="606834606" label="force-color-profile"/>
   <int value="606969417" label="DiscoverApp:enabled"/>
+  <int value="608761130" label="PrintSaveToDrive:enabled"/>
   <int value="609112512" label="touch-selection-strategy"/>
   <int value="609580715" label="ArcCupsApi:disabled"/>
   <int value="610545308" label="enable-potentially-annoying-security-features"/>
diff --git a/tools/metrics/histograms/histograms.xml b/tools/metrics/histograms/histograms.xml
index f7cfe42..b3e1b0ea 100644
--- a/tools/metrics/histograms/histograms.xml
+++ b/tools/metrics/histograms/histograms.xml
@@ -4056,7 +4056,7 @@
 </histogram>
 
 <histogram name="Android.Omnibox.InvalidMatch" enum="MatchResult"
-    expires_after="2020-08-30">
+    expires_after="2021-02-27">
   <owner>ender@chromium.org</owner>
   <owner>tedchoc@chromium.org</owner>
   <owner>mpearson@chromium.org</owner>
@@ -116687,6 +116687,9 @@
 
 <histogram name="Omnibox.HistoryQuickHistoryIDSetFromWords" units="ms"
     expires_after="M86">
+  <obsolete>
+    Removed in August 2020.
+  </obsolete>
   <owner>mpearson@chromium.org</owner>
   <owner>jdonnelly@chromium.org</owner>
   <summary>
diff --git a/tools/metrics/ukm/ukm.xml b/tools/metrics/ukm/ukm.xml
index e4b994d7..b15bc23 100644
--- a/tools/metrics/ukm/ukm.xml
+++ b/tools/metrics/ukm/ukm.xml
@@ -5343,6 +5343,12 @@
       requests throttled.
     </summary>
   </metric>
+  <metric name="ThrottlingResult" enum="LiteVideoThrottleResult">
+    <summary>
+      The result for whether media requests associated with this navigation were
+      successfully throttled or stopped due to an opt-out event.
+    </summary>
+  </metric>
   <metric name="ThrottlingStartDecision" enum="LiteVideoDecision">
     <summary>
       The decision for whether media requests associated with this frame will be
diff --git a/tools/perf/core/perfetto_binary_roller/binary_deps.json b/tools/perf/core/perfetto_binary_roller/binary_deps.json
index 4454f1e..57980c6c 100644
--- a/tools/perf/core/perfetto_binary_roller/binary_deps.json
+++ b/tools/perf/core/perfetto_binary_roller/binary_deps.json
@@ -1,12 +1,12 @@
 {
     "trace_processor_shell": {
         "win": {
-            "hash": "c3f650eb5d76f8320c288295bcb161c6cfb2c66e",
-            "remote_path": "perfetto_binaries/trace_processor_shell/win/46b0d8780f6664c080dbec70c09bdd207cff12e9/trace_processor_shell.exe"
+            "hash": "80b5e25a6e577a2be1fc35689406db65b821a568",
+            "remote_path": "perfetto_binaries/trace_processor_shell/win/b156b016f22bbd24370ff2dc1bcc0a7b3edf7ee4/trace_processor_shell.exe"
         },
         "mac": {
-            "hash": "a0bf2c8901f2ad304e539e0d2543dd652461e536",
-            "remote_path": "perfetto_binaries/trace_processor_shell/mac/7176f54720e6f5ff087e1fe6407191b5692a7aa2/trace_processor_shell"
+            "hash": "f2ababab87ba7a0db590374ad04a1d08325604aa",
+            "remote_path": "perfetto_binaries/trace_processor_shell/mac/b156b016f22bbd24370ff2dc1bcc0a7b3edf7ee4/trace_processor_shell"
         },
         "linux": {
             "hash": "1f57c64fd764e42c7fa3bd16d9313ca091d7358b",
diff --git a/ui/accelerated_widget_mac/ca_renderer_layer_tree.mm b/ui/accelerated_widget_mac/ca_renderer_layer_tree.mm
index f6600264..e67729e 100644
--- a/ui/accelerated_widget_mac/ca_renderer_layer_tree.mm
+++ b/ui/accelerated_widget_mac/ca_renderer_layer_tree.mm
@@ -447,6 +447,7 @@
         if (has_hdr_color_space)
           type = CALayerType::kHDRCopier;
         break;
+      // TODO(crbug.com/1103432): We'll likely need YpCbCr10 here for HDR.
       case kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange:
         // Only allow 4:2:0 frames which fill the layer's contents to be
         // promoted to AV layers.
diff --git a/ui/accessibility/ax_range.h b/ui/accessibility/ax_range.h
index 62db9d3..2d263e1c 100644
--- a/ui/accessibility/ax_range.h
+++ b/ui/accessibility/ax_range.h
@@ -131,6 +131,12 @@
                : AXRange(anchor_->Clone(), focus_->Clone());
   }
 
+  AXRange AsBackwardRange() const {
+    return (CompareEndpoints(anchor(), focus()).value_or(0) < 0)
+               ? AXRange(focus_->Clone(), anchor_->Clone())
+               : AXRange(anchor_->Clone(), focus_->Clone());
+  }
+
   bool IsCollapsed() const { return !IsNull() && *anchor_ == *focus_; }
 
   // We define a "leaf text range" as an AXRange whose endpoints are leaf text
diff --git a/ui/base/ime/win/BUILD.gn b/ui/base/ime/win/BUILD.gn
index 5a7c0c7d..8121777 100644
--- a/ui/base/ime/win/BUILD.gn
+++ b/ui/base/ime/win/BUILD.gn
@@ -45,11 +45,4 @@
   libs = [ "imm32.lib" ]
 
   ldflags = [ "/DELAYLOAD:imm32.dll" ]
-
-  jumbo_excluded_sources = [
-    # tsf_text_store.cc needs INITGUID to be defined before
-    # including any header to properly generate GUID objects. That
-    # is not guaranteed when included in a jumbo build.
-    "tsf_text_store.cc",
-  ]
 }
diff --git a/ui/file_manager/file_manager/foreground/js/file_manager_commands.js b/ui/file_manager/file_manager/foreground/js/file_manager_commands.js
index 7cfb2a1..d5301e82 100644
--- a/ui/file_manager/file_manager/foreground/js/file_manager_commands.js
+++ b/ui/file_manager/file_manager/foreground/js/file_manager_commands.js
@@ -1574,7 +1574,13 @@
  */
 CommandHandler.COMMANDS_['invoke-sharesheet'] = new class extends Command {
   execute(event, fileManager) {
-    // TODO(crbug.com/1097623): Implement this.
+    const entries = fileManager.selectionHandler.selection.entries;
+    chrome.fileManagerPrivate.invokeSharesheet(entries, () => {
+      if (chrome.runtime.lastError) {
+        console.error(chrome.runtime.lastError.message);
+        return;
+      }
+    });
   }
 
   /** @override */
diff --git a/ui/file_manager/file_manager/foreground/js/ui/directory_tree.js b/ui/file_manager/file_manager/foreground/js/ui/directory_tree.js
index 7067e53..a04ab700 100644
--- a/ui/file_manager/file_manager/foreground/js/ui/directory_tree.js
+++ b/ui/file_manager/file_manager/foreground/js/ui/directory_tree.js
@@ -64,10 +64,10 @@
  *
  * @param {!DirectoryEntry|!FilesAppDirEntry} entry The entry to be searched
  *     for. Can be a fake.
- * @return {boolean} True if the parent item is found.
+ * @return {!Promise<boolean>} True if the parent item is found.
  * @this {(DirectoryItem|VolumeItem|DirectoryTree)}
  */
-DirectoryItemTreeBaseMethods.searchAndSelectByEntry = function(entry) {
+DirectoryItemTreeBaseMethods.searchAndSelectByEntry = async function(entry) {
   for (let i = 0; i < this.items.length; i++) {
     const item = this.items[i];
     if (!item.entry) {
@@ -78,18 +78,18 @@
     // When we looking for an item in team drives, recursively search inside the
     // "Google Drive" root item.
     if (util.isSharedDriveEntry(entry) && item instanceof DriveVolumeItem) {
-      item.selectByEntry(entry);
+      await item.selectByEntry(entry);
       return true;
     }
 
     if (util.isComputersEntry(entry) && item instanceof DriveVolumeItem) {
-      item.selectByEntry(entry);
+      await item.selectByEntry(entry);
       return true;
     }
 
     if (util.isDescendantEntry(item.entry, entry) ||
         util.isSameEntry(item.entry, entry)) {
-      item.selectByEntry(entry);
+      await item.selectByEntry(entry);
       return true;
     }
   }
@@ -508,10 +508,10 @@
    *
    * @param {!DirectoryEntry|!FilesAppDirEntry} entry The entry to be searched
    *     for. Can be a fake.
-   * @return {boolean} True if the parent item is found.
+   * @return {!Promise<boolean>} True if the parent item is found.
    */
-  searchAndSelectByEntry(entry) {
-    return DirectoryItemTreeBaseMethods.searchAndSelectByEntry.call(
+  async searchAndSelectByEntry(entry) {
+    return await DirectoryItemTreeBaseMethods.searchAndSelectByEntry.call(
         this, entry);
   }
 
@@ -746,20 +746,22 @@
    * Select the item corresponding to the given {@code entry}.
    * @param {!DirectoryEntry|!FilesAppDirEntry} entry The entry to be selected.
    *     Can be a fake.
+   * @return {!Promise<void>}
    */
-  selectByEntry(entry) {
+  async selectByEntry(entry) {
     if (util.isSameEntry(entry, this.entry)) {
       this.selected = true;
       return;
     }
 
-    if (this.searchAndSelectByEntry(entry)) {
+    if (await this.searchAndSelectByEntry(entry)) {
       return;
     }
 
     // If the entry doesn't exist, updates sub directories and tries again.
-    this.updateSubDirectories(
-        false /* recursive */, this.searchAndSelectByEntry.bind(this, entry));
+    await new Promise(
+        this.updateSubDirectories.bind(this, false /* recursive */));
+    await this.searchAndSelectByEntry(entry);
   }
 
   /**
@@ -1562,11 +1564,12 @@
    * Select the item corresponding to the given entry.
    * @param {!DirectoryEntry|!FilesAppDirEntry} entry The directory entry to be
    *     selected. Can be a fake.
+   * @return {!Promise<void>}
    * @override
    */
-  selectByEntry(entry) {
+  async selectByEntry(entry) {
     // Find the item to be selected among children.
-    this.searchAndSelectByEntry(entry);
+    await this.searchAndSelectByEntry(entry);
   }
 
   /**
@@ -2129,9 +2132,9 @@
    *
    * @param {!DirectoryEntry|!FilesAppDirEntry} entry The entry to be searched
    *     for. Can be a fake.
-   * @return {boolean} True if the parent item is found.
+   * @return {!Promise<boolean>} True if the parent item is found.
    */
-  searchAndSelectByEntry(entry) {
+  async searchAndSelectByEntry(entry) {
     // If the |entry| is same as one of volumes or shortcuts, select it.
     for (let i = 0; i < this.items.length; i++) {
       // Skips the Drive root volume. For Drive entries, one of children of
@@ -2142,13 +2145,14 @@
       }
 
       if (util.isSameEntry(item.entry, entry)) {
-        item.selectByEntry(entry);
+        await item.selectByEntry(entry);
         return true;
       }
     }
     // Otherwise, search whole tree.
     const found =
-        DirectoryItemTreeBaseMethods.searchAndSelectByEntry.call(this, entry);
+        await DirectoryItemTreeBaseMethods.searchAndSelectByEntry.call(
+            this, entry);
     return found;
   }
 
@@ -2189,13 +2193,14 @@
    * Select the item corresponding to the given entry.
    * @param {!DirectoryEntry|!FilesAppDirEntry} entry The directory entry to be
    *     selected. Can be a fake.
+   * @return {!Promise<void>}
    */
-  selectByEntry(entry) {
+  async selectByEntry(entry) {
     if (this.selectedItem && util.isSameEntry(entry, this.selectedItem.entry)) {
       return;
     }
 
-    if (this.searchAndSelectByEntry(entry)) {
+    if (await this.searchAndSelectByEntry(entry)) {
       return;
     }
 
@@ -2205,11 +2210,11 @@
     if (!volumeInfo) {
       return;
     }
-    volumeInfo.resolveDisplayRoot(() => {
+    volumeInfo.resolveDisplayRoot(async () => {
       if (this.sequence_ !== currentSequence) {
         return;
       }
-      if (!this.searchAndSelectByEntry(entry)) {
+      if (!(await this.searchAndSelectByEntry(entry))) {
         this.selectedItem = null;
       }
     });
@@ -2335,8 +2340,8 @@
    * @param {!Event} event Event.
    * @private
    */
-  onCurrentDirectoryChanged_(event) {
-    this.selectByEntry(event.newDirEntry);
+  async onCurrentDirectoryChanged_(event) {
+    await this.selectByEntry(event.newDirEntry);
 
     const selectedItem = this.selectedItem;
 
diff --git a/ui/file_manager/integration_tests/file_manager/recents.js b/ui/file_manager/integration_tests/file_manager/recents.js
index 935aefe..1a6eb08 100644
--- a/ui/file_manager/integration_tests/file_manager/recents.js
+++ b/ui/file_manager/integration_tests/file_manager/recents.js
@@ -159,6 +159,31 @@
   await verifyRecents(appId, RECENT_ENTRY_SET.concat(RECENT_ENTRY_SET));
 };
 
+testcase.recentsNested = async () => {
+  // Populate downloads with nested folder structure. |desktop| is added to
+  // ensure Recents has different files to Downloads/A/B/C
+  const appId = await setupAndWaitUntilReady(
+      RootPath.DOWNLOADS,
+      NESTED_ENTRY_SET.concat([ENTRIES.deeplyBurriedSmallJpeg]), []);
+
+  // Verifies file list in Recents.
+  await verifyRecents(appId, [ENTRIES.deeplyBurriedSmallJpeg]);
+
+  // Tests that selecting "Go to file location" for a file navigates to
+  // Downloads/A/B/C since the file in Recents is from Downloads/A/B/C.
+  await goToFileLocation(appId, ENTRIES.deeplyBurriedSmallJpeg.nameText);
+  await remoteCall.waitForElement(appId, `[scan-completed="C"]`);
+  await remoteCall.waitForFiles(
+      appId, TestEntryInfo.getExpectedRows([ENTRIES.deeplyBurriedSmallJpeg]));
+  await verifyBreadcrumbsPath(appId, '/My files/Downloads/A/B/C');
+
+  // Check: The directory should be highlighted in the directory tree.
+  await remoteCall.waitForElement(
+      appId,
+      '.tree-item[full-path-for-testing="/Downloads/A/B/C"] > ' +
+          '.tree-row[selected][active]');
+};
+
 testcase.recentAudioDownloads = async () => {
   const appId = await setupAndWaitUntilReady(
       RootPath.DOWNLOADS, BASIC_LOCAL_ENTRY_SET, []);
@@ -217,4 +242,4 @@
   // from Drive should be shown too.
   await verifyRecentVideos(
       appId, [RECENTLY_MODIFIED_VIDEO, RECENTLY_MODIFIED_VIDEO]);
-};
\ No newline at end of file
+};
diff --git a/ui/gfx/render_text_unittest.cc b/ui/gfx/render_text_unittest.cc
index 0ff5f56..c405f94 100644
--- a/ui/gfx/render_text_unittest.cc
+++ b/ui/gfx/render_text_unittest.cc
@@ -38,6 +38,7 @@
 #include "ui/gfx/font.h"
 #include "ui/gfx/font_names_testing.h"
 #include "ui/gfx/geometry/point.h"
+#include "ui/gfx/geometry/point_conversions.h"
 #include "ui/gfx/geometry/point_f.h"
 #include "ui/gfx/range/range.h"
 #include "ui/gfx/range/range_f.h"
@@ -3567,12 +3568,11 @@
     SCOPED_TRACE(base::StringPrintf("Testing case[%" PRIuS "]", i));
     render_text->SetText(UTF8ToUTF16(kTestStrings[i]));
     for (size_t j = 0; j < render_text->text().length(); ++j) {
-      const Range range(render_text->GetCursorSpan(Range(j, j + 1)).Round());
+      gfx::RangeF cursor_span = render_text->GetCursorSpan(Range(j, j + 1));
       // Test a point just inside the leading edge of the glyph bounds.
-      int x = range.is_reversed() ? range.GetMax() - 1 : range.GetMin() + 1;
-      EXPECT_EQ(
-          j, render_text->FindCursorPosition(Point(x, GetCursorYForTesting()))
-                 .caret_pos());
+      float x = cursor_span.start() + (cursor_span.is_reversed() ? -1 : 1);
+      Point point = gfx::ToCeiledPoint(PointF(x, GetCursorYForTesting()));
+      EXPECT_EQ(j, render_text->FindCursorPosition(point).caret_pos());
     }
   }
 }
@@ -5994,8 +5994,9 @@
 
     for (size_t j = 0; j < 4; ++j) {
       SCOPED_TRACE(base::StringPrintf("Case %" PRIuS ", char %" PRIuS, i, j));
-      EXPECT_EQ(cases[i].bounds[j],
-                run.GetGraphemeBounds(render_text, j).Round());
+      RangeF bounds_f = run.GetGraphemeBounds(render_text, j);
+      Range bounds(std::round(bounds_f.start()), std::round(bounds_f.end()));
+      EXPECT_EQ(cases[i].bounds[j], bounds);
     }
   }
 }
@@ -6328,13 +6329,13 @@
 
   run.range = Range(3, 8);
   run.shape.glyph_count = 0;
-  EXPECT_EQ(Range(0, 0), run.CharRangeToGlyphRange(Range(4, 5)));
-  EXPECT_EQ(Range(0, 0), run.GetGraphemeBounds(render_text, 4).Round());
+  EXPECT_EQ(Range(), run.CharRangeToGlyphRange(Range(4, 5)));
+  EXPECT_EQ(RangeF(), run.GetGraphemeBounds(render_text, 4));
   Range chars;
   Range glyphs;
   run.GetClusterAt(4, &chars, &glyphs);
   EXPECT_EQ(Range(3, 8), chars);
-  EXPECT_EQ(Range(0, 0), glyphs);
+  EXPECT_EQ(Range(), glyphs);
 }
 
 // Ensure the line breaker doesn't compute the word's width bigger than the
diff --git a/weblayer/browser/java/org/chromium/weblayer_private/WebLayerImpl.java b/weblayer/browser/java/org/chromium/weblayer_private/WebLayerImpl.java
index df97a99..ad8b5eb 100644
--- a/weblayer/browser/java/org/chromium/weblayer_private/WebLayerImpl.java
+++ b/weblayer/browser/java/org/chromium/weblayer_private/WebLayerImpl.java
@@ -693,7 +693,9 @@
     }
 
     private static void notifyWebViewRunningInProcess(ClassLoader webViewClassLoader) {
-        try {
+        // TODO(crbug.com/1112001): Investigate why loading classes causes strict mode
+        // violations in some situations.
+        try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) {
             Class<?> webViewChromiumFactoryProviderClass =
                     Class.forName("com.android.webview.chromium.WebViewChromiumFactoryProvider",
                             true, webViewClassLoader);