diff --git a/DEPS b/DEPS
index 25f7563..417ffde 100644
--- a/DEPS
+++ b/DEPS
@@ -208,7 +208,7 @@
   # 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': '929acfd81ac536f1b7d5699427a01e1238efabd4',
+  'v8_revision': 'f53ac97298868f535ef84b516d80ebb6d41c67eb',
   # 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.
@@ -216,11 +216,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': '79e39478028c56b4be4e4549f8c9e53381ae48ce',
+  'angle_revision': 'c99e405cc2b3f1c358bf2e594ba8c9d4658bc82c',
   # 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': '69b79eec6533f113920a5dd863ee2dea7d0c64e7',
+  'swiftshader_revision': 'a0aeb64e01d75b257495eda180bea9758d072005',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling PDFium
   # and whatever else without interference from each other.
@@ -283,7 +283,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': '92feb60a1acd5fb8a9be021111df077d3d29bc2d',
+  'devtools_frontend_revision': '695ff14eb349e747ed16e3ca89265f41b883936d',
   # 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.
@@ -323,7 +323,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling feed
   # and whatever else without interference from each other.
-  'dawn_revision': '54586e61210cfdfc9835f81b57c794f4ac19356b',
+  'dawn_revision': 'cc84ee24fc89229d956a6cd1a8ed0f7fd7528c1b',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling feed
   # and whatever else without interference from each other.
@@ -379,8 +379,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling feed
   # and whatever else without interference from each other.
-  # TODO(crbug.com/1166332) rename to clang_format_revision.
-  'clang_fmt_revision':    '99803d74e35962f63a775f29477882afd4d57d94',
+  'clang_format_revision':    '99803d74e35962f63a775f29477882afd4d57d94',
 
   # If you change this, also update the libc++ revision in
   # //buildtools/deps_revisions.gni.
@@ -411,7 +410,7 @@
   'src/buildtools/clang_format/script':
     Var('chromium_git') +
     '/external/github.com/llvm/llvm-project/clang/tools/clang-format.git@' +
-    Var('clang_fmt_revision'),
+    Var('clang_format_revision'),
   'src/buildtools/linux64': {
     'packages': [
       {
@@ -910,7 +909,7 @@
   # Tools used when building Chrome for Chrome OS. This affects both the Simple
   # Chrome workflow, as well as the chromeos-chrome ebuild.
   'src/third_party/chromite': {
-      'url': Var('chromium_git') + '/chromiumos/chromite.git' + '@' + '16ecc391ff9a68eef9375cb5d7f7afca3c2b6aac',
+      'url': Var('chromium_git') + '/chromiumos/chromite.git' + '@' + 'd93177cb824290cc917dd4309319995bdd34cf3a',
       'condition': 'checkout_chromeos',
   },
 
@@ -930,7 +929,7 @@
   },
 
   'src/third_party/depot_tools':
-    Var('chromium_git') + '/chromium/tools/depot_tools.git' + '@' + '593a6b575b137c42c91ef2439dbf6c526e5c5980',
+    Var('chromium_git') + '/chromium/tools/depot_tools.git' + '@' + '36de4be91ef66852c49aa5fd9e0cc31d5ec05ae9',
 
   'src/third_party/devtools-frontend/src':
     Var('chromium_git') + '/devtools/devtools-frontend' + '@' + Var('devtools_frontend_revision'),
@@ -1302,7 +1301,7 @@
   },
 
   'src/third_party/perfetto':
-    Var('android_git') + '/platform/external/perfetto.git' + '@' + '6ecb51ac72ea4de3663b4442ad3e72b64f660af1',
+    Var('android_git') + '/platform/external/perfetto.git' + '@' + '3d4f2c26f0d21ae10266f3be3b15c8b7fcf0a6ed',
 
   'src/third_party/perl': {
       'url': Var('chromium_git') + '/chromium/deps/perl.git' + '@' + '6f3e5028eb65d0b4c5fdd792106ac4c84eee1eb3',
@@ -1538,7 +1537,7 @@
     Var('chromium_git') + '/external/github.com/gpuweb/cts.git' + '@' + '66460536ee975a3e98931b7b40a661a63fd9cd57',
 
   'src/third_party/webrtc':
-    Var('webrtc_git') + '/src.git' + '@' + '6097b0fac0946a29d59a9266ea656c39b3fd7336',
+    Var('webrtc_git') + '/src.git' + '@' + 'ab6335041130fdc0365a604f49668eac9194f9d6',
 
   'src/third_party/libgifcodec':
      Var('skia_git') + '/libgifcodec' + '@'+  Var('libgifcodec_revision'),
@@ -1586,7 +1585,7 @@
       'packages': [
         {
           'package': 'skia/tools/goldctl/windows-amd64',
-          'version': '5QaZrGtCcd9HFM-qeg4vP-CrEpvhqlZtkfwAtVBee2oC',
+          'version': 'wr5p_MyXcHVxA-eHznijCeFaqR2HUo02Hw70e0CWCIoC',
         },
       ],
       'dep_type': 'cipd',
@@ -1596,7 +1595,7 @@
       'packages': [
         {
           'package': 'skia/tools/goldctl/mac-amd64',
-          'version': 'F9PFp7kNc3dn_X7b1PYmPsEb3VMHDY-8pXiZ7WNi09MC',
+          'version': 'TPtcrf_XH3mB9pdK4W3N0QPZunsqJ08T7U7fWPvMnRAC',
         },
       ],
       'dep_type': 'cipd',
@@ -1610,7 +1609,7 @@
     Var('chromium_git') + '/v8/v8.git' + '@' +  Var('v8_revision'),
 
   'src-internal': {
-    'url': 'https://chrome-internal.googlesource.com/chrome/src-internal.git@e7c96aaf8edb25f561c2c8fc45bee7e2893a867b',
+    'url': 'https://chrome-internal.googlesource.com/chrome/src-internal.git@19b198ff86078b2069ba923779293444b57856a2',
     'condition': 'checkout_src_internal',
   },
 
diff --git a/android_webview/browser/BUILD.gn b/android_webview/browser/BUILD.gn
index 41991c8..5710717 100644
--- a/android_webview/browser/BUILD.gn
+++ b/android_webview/browser/BUILD.gn
@@ -100,6 +100,10 @@
     "aw_web_contents_view_delegate.h",
     "aw_web_ui_controller_factory.cc",
     "aw_web_ui_controller_factory.h",
+    "component_updater/loader_policies/origin_trials_component_loader_policy.cc",
+    "component_updater/loader_policies/origin_trials_component_loader_policy.h",
+    "component_updater/origin_trials_component_loader.cc",
+    "component_updater/origin_trials_component_loader.h",
     "component_updater/registration.cc",
     "component_updater/registration.h",
     "component_updater/trust_token_key_commitments_component_loader.cc",
@@ -199,9 +203,11 @@
     "//components/cdm/browser",
     "//components/component_updater/android:embedded_component_loader",
     "//components/component_updater/android:loader_policies",
+    "//components/component_updater/installer_policies",
     "//components/content_capture/android",
     "//components/content_capture/browser",
     "//components/embedder_support/android:util",
+    "//components/embedder_support/origin_trials",
     "//components/favicon_base:favicon_base",
     "//components/flags_ui",
     "//components/keyed_service/content",
diff --git a/android_webview/browser/DEPS b/android_webview/browser/DEPS
index 528342ad..1c00556 100644
--- a/android_webview/browser/DEPS
+++ b/android_webview/browser/DEPS
@@ -14,10 +14,12 @@
   "+components/autofill/core/common",
   "+components/cdm/browser",
   "+components/component_updater/android",
+  "+components/component_updater/installer_policies",
   "+components/crash/content/browser",
   "+components/crash/core",
   "+components/download/public/common",
   "+components/embedder_support/android/metrics",
+  "+components/embedder_support/origin_trials",
   "+components/favicon_base",
   "+components/flags_ui",
   "+components/heap_profiling",
diff --git a/android_webview/browser/aw_feature_list_creator.cc b/android_webview/browser/aw_feature_list_creator.cc
index c749c87a..8a330880 100644
--- a/android_webview/browser/aw_feature_list_creator.cc
+++ b/android_webview/browser/aw_feature_list_creator.cc
@@ -29,6 +29,7 @@
 #include "cc/base/switches.h"
 #include "components/autofill/core/common/autofill_prefs.h"
 #include "components/embedder_support/android/metrics/android_metrics_service_client.h"
+#include "components/embedder_support/origin_trials/origin_trial_prefs.h"
 #include "components/metrics/metrics_pref_names.h"
 #include "components/metrics/persistent_histograms.h"
 #include "components/policy/core/browser/configuration_policy_pref_store.h"
@@ -122,6 +123,7 @@
   AwMetricsServiceClient::RegisterPrefs(pref_registry.get());
   variations::VariationsService::RegisterPrefs(pref_registry.get());
 
+  embedder_support::OriginTrialPrefs::RegisterPrefs(pref_registry.get());
   AwBrowserProcess::RegisterNetworkContextLocalStatePrefs(pref_registry.get());
 
   PrefServiceFactory pref_service_factory;
diff --git a/android_webview/browser/component_updater/loader_policies/origin_trials_component_loader_policy.cc b/android_webview/browser/component_updater/loader_policies/origin_trials_component_loader_policy.cc
new file mode 100644
index 0000000..c917039f
--- /dev/null
+++ b/android_webview/browser/component_updater/loader_policies/origin_trials_component_loader_policy.cc
@@ -0,0 +1,49 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "android_webview/browser/component_updater/loader_policies/origin_trials_component_loader_policy.h"
+
+#include <stdint.h>
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "android_webview/browser/aw_browser_process.h"
+#include "base/containers/flat_map.h"
+#include "base/values.h"
+#include "base/version.h"
+#include "components/component_updater/installer_policies/origin_trials_component_installer.h"
+#include "components/embedder_support/origin_trials/component_updater_utils.h"
+
+namespace android_webview {
+
+OriginTrialsComponentLoaderPolicy::OriginTrialsComponentLoaderPolicy() =
+    default;
+
+OriginTrialsComponentLoaderPolicy::~OriginTrialsComponentLoaderPolicy() =
+    default;
+
+void OriginTrialsComponentLoaderPolicy::ComponentLoaded(
+    const base::Version& version,
+    const base::flat_map<std::string, int>& fd_map,
+    std::unique_ptr<base::DictionaryValue> manifest) {
+  // Read the configuration from the manifest and set values in browser
+  // local_state. These will be used on the next browser restart.
+  // If an individual configuration value is missing, treat as a reset to the
+  // browser defaults.
+  embedder_support::ReadOriginTrialsConfigAndPopulateLocalState(
+      android_webview::AwBrowserProcess::GetInstance()->local_state(),
+      std::move(manifest));
+}
+
+void OriginTrialsComponentLoaderPolicy::ComponentLoadFailed() {}
+
+void OriginTrialsComponentLoaderPolicy::GetHash(
+    std::vector<uint8_t>* hash) const {
+  component_updater::OriginTrialsComponentInstallerPolicy::GetComponentHash(
+      hash);
+}
+
+}  // namespace android_webview
diff --git a/android_webview/browser/component_updater/loader_policies/origin_trials_component_loader_policy.h b/android_webview/browser/component_updater/loader_policies/origin_trials_component_loader_policy.h
new file mode 100644
index 0000000..f17f313
--- /dev/null
+++ b/android_webview/browser/component_updater/loader_policies/origin_trials_component_loader_policy.h
@@ -0,0 +1,49 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef ANDROID_WEBVIEW_BROWSER_COMPONENT_UPDATER_LOADER_POLICIES_ORIGIN_TRIALS_COMPONENT_LOADER_POLICY_H_
+#define ANDROID_WEBVIEW_BROWSER_COMPONENT_UPDATER_LOADER_POLICIES_ORIGIN_TRIALS_COMPONENT_LOADER_POLICY_H_
+
+#include <stdint.h>
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "base/containers/flat_map.h"
+#include "components/component_updater/android/component_loader_policy.h"
+
+namespace base {
+class DictionaryValue;
+class Version;
+}  // namespace base
+
+namespace android_webview {
+
+// OriginTrialsComponentLoaderPolicy defines a loader responsible
+// for receiving origin trials config.
+class OriginTrialsComponentLoaderPolicy
+    : public component_updater::ComponentLoaderPolicy {
+ public:
+  OriginTrialsComponentLoaderPolicy();
+  ~OriginTrialsComponentLoaderPolicy() override;
+
+  OriginTrialsComponentLoaderPolicy(const OriginTrialsComponentLoaderPolicy&) =
+      delete;
+  OriginTrialsComponentLoaderPolicy& operator=(
+      const OriginTrialsComponentLoaderPolicy&) = delete;
+
+ private:
+  // The following methods override ComponentLoaderPolicy.
+  void ComponentLoaded(
+      const base::Version& version,
+      const base::flat_map<std::string, int>& fd_map,
+      std::unique_ptr<base::DictionaryValue> manifest) override;
+  void ComponentLoadFailed() override;
+  void GetHash(std::vector<uint8_t>* hash) const override;
+};
+
+}  // namespace android_webview
+
+#endif  // ANDROID_WEBVIEW_BROWSER_COMPONENT_UPDATER_LOADER_POLICIES_ORIGIN_TRIALS_COMPONENT_LOADER_POLICY_H_
\ No newline at end of file
diff --git a/android_webview/browser/component_updater/origin_trials_component_loader.cc b/android_webview/browser/component_updater/origin_trials_component_loader.cc
new file mode 100644
index 0000000..428d672
--- /dev/null
+++ b/android_webview/browser/component_updater/origin_trials_component_loader.cc
@@ -0,0 +1,25 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "android_webview/browser/component_updater/origin_trials_component_loader.h"
+
+#include <memory>
+
+#include "android_webview/browser/component_updater/loader_policies/origin_trials_component_loader_policy.h"
+#include "android_webview/common/aw_features.h"
+#include "base/feature_list.h"
+#include "components/component_updater/android/component_loader_policy.h"
+#include "components/component_updater/android/component_loader_policy_forward.h"
+
+namespace android_webview {
+
+void LoadOriginTrialsComponent(
+    component_updater::ComponentLoaderPolicyVector& policies) {
+  if (!base::FeatureList::IsEnabled(features::kWebViewOriginTrials))
+    return;
+
+  policies.push_back(std::make_unique<OriginTrialsComponentLoaderPolicy>());
+}
+
+}  // namespace android_webview
diff --git a/android_webview/browser/component_updater/origin_trials_component_loader.h b/android_webview/browser/component_updater/origin_trials_component_loader.h
new file mode 100644
index 0000000..b3c13bb3
--- /dev/null
+++ b/android_webview/browser/component_updater/origin_trials_component_loader.h
@@ -0,0 +1,23 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef ANDROID_WEBVIEW_BROWSER_COMPONENT_UPDATER_ORIGIN_TRIALS_COMPONENT_LOADER_H_
+#define ANDROID_WEBVIEW_BROWSER_COMPONENT_UPDATER_ORIGIN_TRIALS_COMPONENT_LOADER_H_
+
+#include "components/component_updater/android/component_loader_policy_forward.h"
+
+namespace component_updater {
+class ComponentLoaderPolicy;
+}  // namespace component_updater
+
+namespace android_webview {
+
+// Adds origin trials ComponentLoaderPolicy to `policies`, if Origin Trials
+// support is enabled.
+void LoadOriginTrialsComponent(
+    component_updater::ComponentLoaderPolicyVector& policies);
+
+}  // namespace android_webview
+
+#endif  // ANDROID_WEBVIEW_BROWSER_COMPONENT_UPDATER_ORIGIN_TRIALS_COMPONENT_LOADER_H_
diff --git a/android_webview/browser/component_updater/registration.cc b/android_webview/browser/component_updater/registration.cc
index 54fc5a7..574702eb 100644
--- a/android_webview/browser/component_updater/registration.cc
+++ b/android_webview/browser/component_updater/registration.cc
@@ -4,13 +4,15 @@
 
 #include "android_webview/browser/component_updater/registration.h"
 
+#include "android_webview/browser/component_updater/origin_trials_component_loader.h"
 #include "android_webview/browser/component_updater/trust_token_key_commitments_component_loader.h"
 
 namespace android_webview {
 
 component_updater::ComponentLoaderPolicyVector GetComponentLoaderPolicies() {
   component_updater::ComponentLoaderPolicyVector policies;
-  LoadTrustTokenKeyCommitmentsComponent(&policies);
+  LoadTrustTokenKeyCommitmentsComponent(policies);
+  LoadOriginTrialsComponent(policies);
   return policies;
 }
 
diff --git a/android_webview/browser/component_updater/trust_token_key_commitments_component_loader.cc b/android_webview/browser/component_updater/trust_token_key_commitments_component_loader.cc
index 3eedac6..48547c6 100644
--- a/android_webview/browser/component_updater/trust_token_key_commitments_component_loader.cc
+++ b/android_webview/browser/component_updater/trust_token_key_commitments_component_loader.cc
@@ -21,7 +21,7 @@
 // Add trust tokens ComponentLoaderPolicy to the given policies vector, if Trust
 // Tokens is enabled.
 void LoadTrustTokenKeyCommitmentsComponent(
-    ComponentLoaderPolicyVector* policies) {
+    ComponentLoaderPolicyVector& policies) {
   if (!base::FeatureList::IsEnabled(network::features::kTrustTokens))
     return;
 
@@ -29,7 +29,7 @@
       << "Registering Trust Token Key Commitments component for loading in "
          "embedded WebView.";
 
-  policies->push_back(
+  policies.push_back(
       std::make_unique<
           component_updater::TrustTokenKeyCommitmentsComponentLoaderPolicy>(
           /* on_commitments_ready = */ base::BindRepeating(
diff --git a/android_webview/browser/component_updater/trust_token_key_commitments_component_loader.h b/android_webview/browser/component_updater/trust_token_key_commitments_component_loader.h
index ccff9b8..2928bee 100644
--- a/android_webview/browser/component_updater/trust_token_key_commitments_component_loader.h
+++ b/android_webview/browser/component_updater/trust_token_key_commitments_component_loader.h
@@ -17,7 +17,7 @@
     std::vector<std::unique_ptr<component_updater::ComponentLoaderPolicy>>;
 
 void LoadTrustTokenKeyCommitmentsComponent(
-    ComponentLoaderPolicyVector* policies);
+    ComponentLoaderPolicyVector& policies);
 
 }  // namespace android_webview
 
diff --git a/ash/accessibility/point_scan_controller.cc b/ash/accessibility/point_scan_controller.cc
index 424dd508..9fd563b 100644
--- a/ash/accessibility/point_scan_controller.cc
+++ b/ash/accessibility/point_scan_controller.cc
@@ -11,7 +11,7 @@
 namespace {
 
 constexpr int kDefaultRangeWidthDips = 150;
-constexpr float kDefaultRangeHeightDips = 120;
+constexpr int kDefaultRangeHeightDips = 120;
 constexpr float kLineScanSlowDownFactor = 0.5f;
 
 }  // namespace
diff --git a/ash/accessibility/point_scan_layer_animation_info.cc b/ash/accessibility/point_scan_layer_animation_info.cc
index 613be15e..1b146ad4 100644
--- a/ash/accessibility/point_scan_layer_animation_info.cc
+++ b/ash/accessibility/point_scan_layer_animation_info.cc
@@ -4,11 +4,16 @@
 
 #include "ash/accessibility/point_scan_layer_animation_info.h"
 
+namespace {
+constexpr base::TimeDelta kLingerDelay = base::TimeDelta::FromMilliseconds(250);
+}
+
 namespace ash {
 
 void PointScanLayerAnimationInfo::Clear() {
   start_time = base::TimeTicks();
   change_time = base::TimeTicks();
+  linger_until = base::TimeTicks();
   offset = 0;
   offset_bound = 0;
   offset_start = 0;
@@ -19,19 +24,27 @@
   if (timestamp < animation_info->start_time)
     timestamp = animation_info->start_time;
 
-  float change_delta = (timestamp - animation_info->start_time).InSecondsF();
+  if (timestamp < animation_info->linger_until)
+    return;
+
+  base::TimeTicks change_from_time =
+      std::max(animation_info->linger_until, animation_info->start_time);
+  float change_delta = (timestamp - change_from_time).InSecondsF();
+  if (change_from_time == base::TimeTicks())
+    change_delta = 0;
+  float offset_delta = animation_info->offset_bound *
+                       (change_delta / animation_info->animation_rate);
+  animation_info->offset += offset_delta;
 
   if (animation_info->offset > animation_info->offset_bound) {
     animation_info->offset = animation_info->offset_bound;
     animation_info->animation_rate *= -1;
+    animation_info->linger_until = timestamp + kLingerDelay;
   } else if (animation_info->offset < animation_info->offset_start) {
     animation_info->offset = animation_info->offset_start;
     animation_info->animation_rate *= -1;
+    animation_info->linger_until = timestamp + kLingerDelay;
   }
-
-  float offset_delta = animation_info->offset_bound *
-                       (change_delta / animation_info->animation_rate);
-  animation_info->offset += offset_delta;
 }
 
 }  // namespace ash
diff --git a/ash/accessibility/point_scan_layer_animation_info.h b/ash/accessibility/point_scan_layer_animation_info.h
index c9fb4ce..e96ee68 100644
--- a/ash/accessibility/point_scan_layer_animation_info.h
+++ b/ash/accessibility/point_scan_layer_animation_info.h
@@ -12,6 +12,7 @@
 struct PointScanLayerAnimationInfo {
   base::TimeTicks start_time;
   base::TimeTicks change_time;
+  base::TimeTicks linger_until;
   float offset = 0;
   float offset_bound = 0;
   float offset_start = 0;
diff --git a/ash/fast_ink/fast_ink_pointer_controller.cc b/ash/fast_ink/fast_ink_pointer_controller.cc
index 8bc44ba2..e32e4b8 100644
--- a/ash/fast_ink/fast_ink_pointer_controller.cc
+++ b/ash/fast_ink/fast_ink_pointer_controller.cc
@@ -4,10 +4,12 @@
 
 #include "ash/fast_ink/fast_ink_pointer_controller.h"
 
+#include "ash/public/cpp/stylus_utils.h"
 #include "ui/aura/window.h"
 #include "ui/display/screen.h"
 #include "ui/events/base_event_utils.h"
 #include "ui/views/widget/widget.h"
+#include "ui/wm/core/coordinate_conversion.h"
 
 namespace fast_ink {
 namespace {
@@ -18,9 +20,14 @@
 
 }  // namespace
 
+// TODO(llin): Update |enabled_for_mouse_event_| based on whether the user has
+// been interacted with a stylus.
 FastInkPointerController::FastInkPointerController()
     : presentation_delay_(
-          base::TimeDelta::FromMilliseconds(kPresentationDelayMs)) {}
+          base::TimeDelta::FromMilliseconds(kPresentationDelayMs)),
+      enabled_for_mouse_event_(!ash::stylus_utils::HasStylusInput()) {
+  input_device_event_observation_.Observe(ui::DeviceDataManager::GetInstance());
+}
 
 FastInkPointerController::~FastInkPointerController() {}
 
@@ -32,51 +39,142 @@
   // while it is being animated away.
 }
 
-bool FastInkPointerController::CanStartNewGesture(ui::TouchEvent* event) {
-  // 1) The stylus is pressed
-  // 2) The stylus is moving, but the pointer session has not started yet
-  // (most likely because the preceding press event was consumed by another
-  // handler).
-  return (event->type() == ui::ET_TOUCH_PRESSED ||
-          (event->type() == ui::ET_TOUCH_MOVED && !GetPointerView()));
+void FastInkPointerController::AddExcludedWindow(aura::Window* window) {
+  DCHECK(window);
+  excluded_windows_.Add(window);
 }
 
-void FastInkPointerController::OnTouchEvent(ui::TouchEvent* event) {
-  if (!enabled_)
-    return;
+bool FastInkPointerController::CanStartNewGesture(ui::LocatedEvent* event) {
+  if (IsPointerInExcludedWindows(event))
+    return false;
 
-  if (event->pointer_details().pointer_type != ui::EventPointerType::kPen)
-    return;
+  // 1) The stylus/finger is pressed.
+  // 2) The stylus/finger is moving, but the pointer session has not started yet
+  // (most likely because the preceding press event was consumed by another
+  // handler).
+  bool can_start_on_touch_event =
+      event->type() == ui::ET_TOUCH_PRESSED ||
+      (event->type() == ui::ET_TOUCH_MOVED && !GetPointerView());
+  if (can_start_on_touch_event)
+    return true;
 
-  if (event->type() != ui::ET_TOUCH_MOVED &&
-      event->type() != ui::ET_TOUCH_PRESSED &&
-      event->type() != ui::ET_TOUCH_RELEASED)
-    return;
+  // 1) The mouse is pressed.
+  // 2) The mouse is moving, but the pointer session has not started yet
+  // (most likely because the preceding press event was consumed by another
+  // handler).
+  bool can_start_on_mouse_event =
+      event->type() == ui::ET_MOUSE_PRESSED ||
+      (event->type() == ui::ET_MOUSE_MOVED && !GetPointerView());
+  if (can_start_on_mouse_event)
+    return true;
 
-  // Find the root window that the event was captured on. We never need to
-  // switch between different root windows because it is not physically possible
-  // to seamlessly drag a stylus between two displays like it is with a mouse.
+  return false;
+}
+
+bool FastInkPointerController::ShouldProcessEvent(ui::LocatedEvent* event) {
+  return event->type() == ui::ET_TOUCH_RELEASED ||
+         event->type() == ui::ET_TOUCH_MOVED ||
+         event->type() == ui::ET_TOUCH_PRESSED ||
+         event->type() == ui::ET_MOUSE_PRESSED ||
+         event->type() == ui::ET_MOUSE_RELEASED ||
+         event->type() == ui::ET_MOUSE_MOVED;
+}
+
+bool FastInkPointerController::IsPointerInExcludedWindows(
+    ui::LocatedEvent* event) {
+  gfx::Point screen_location = event->location();
+  aura::Window* event_target = static_cast<aura::Window*>(event->target());
+  wm::ConvertPointToScreen(event_target, &screen_location);
+
+  for (const auto* excluded_window : excluded_windows_.windows()) {
+    if (excluded_window->GetBoundsInScreen().Contains(screen_location)) {
+      return true;
+    }
+  }
+
+  return false;
+}
+
+bool FastInkPointerController::MaybeCreatePointerView(
+    ui::LocatedEvent* event,
+    bool can_start_new_gesture) {
   aura::Window* root_window =
       static_cast<aura::Window*>(event->target())->GetRootWindow();
-
-  if (CanStartNewGesture(event)) {
+  if (can_start_new_gesture) {
     DestroyPointerView();
     CreatePointerView(presentation_delay_, root_window);
   } else {
     views::View* pointer_view = GetPointerView();
     if (!pointer_view)
-      return;
+      return false;
     views::Widget* widget = pointer_view->GetWidget();
     if (widget->IsClosed() ||
         widget->GetNativeWindow()->GetRootWindow() != root_window) {
       // The pointer widget is no longer valid, end the current pointer session.
       DestroyPointerView();
-      return;
+      return false;
     }
   }
 
-  UpdatePointerView(event);
-  event->StopPropagation();
+  return true;
+}
+
+void FastInkPointerController::OnTouchEvent(ui::TouchEvent* event) {
+  const int touch_id = event->pointer_details().id;
+  // Keep track of touch point count.
+  if (event->type() == ui::ET_TOUCH_PRESSED)
+    touch_ids_.insert(touch_id);
+  if (event->type() == ui::ET_TOUCH_RELEASED ||
+      event->type() == ui::ET_TOUCH_CANCELLED) {
+    auto iter = touch_ids_.find(touch_id);
+
+    // Can happen if this object is constructed while fingers were down.
+    if (iter == touch_ids_.end())
+      return;
+
+    touch_ids_.erase(touch_id);
+  }
+
+  if (!enabled_)
+    return;
+
+  // Disable on touch events if the device has stylus.
+  if (ash::stylus_utils::HasStylusInput() &&
+      event->pointer_details().pointer_type != ui::EventPointerType::kPen) {
+    return;
+  }
+
+  // Disable for multiple fingers touch.
+  if (touch_ids_.size() > 1) {
+    DestroyPointerView();
+    return;
+  }
+
+  if (!ShouldProcessEvent(event))
+    return;
+
+  // Update pointer view and stop event propagation if pointer view is
+  // available.
+  if (MaybeCreatePointerView(event, CanStartNewGesture(event))) {
+    UpdatePointerView(event);
+    event->StopPropagation();
+  }
+}
+
+void FastInkPointerController::OnMouseEvent(ui::MouseEvent* event) {
+  if (!enabled_ || !enabled_for_mouse_event_ || !ShouldProcessEvent(event))
+    return;
+
+  // Update pointer view and stop event propagation if pointer view is
+  // available.
+  if (MaybeCreatePointerView(event, CanStartNewGesture(event))) {
+    UpdatePointerView(event);
+    event->StopPropagation();
+  }
+}
+
+void FastInkPointerController::OnDeviceListsComplete() {
+  enabled_for_mouse_event_ = !ash::stylus_utils::HasStylusInput();
 }
 
 }  // namespace fast_ink
diff --git a/ash/fast_ink/fast_ink_pointer_controller.h b/ash/fast_ink/fast_ink_pointer_controller.h
index 017269f..3d7a0b6 100644
--- a/ash/fast_ink/fast_ink_pointer_controller.h
+++ b/ash/fast_ink/fast_ink_pointer_controller.h
@@ -6,22 +6,31 @@
 #define ASH_FAST_INK_FAST_INK_POINTER_CONTROLLER_H_
 
 #include "base/macros.h"
+#include "base/scoped_observation.h"
 #include "base/time/time.h"
+#include "ui/aura/window_tracker.h"
+#include "ui/events/devices/device_data_manager.h"
+#include "ui/events/devices/input_device_event_observer.h"
 #include "ui/events/event_handler.h"
 
 namespace aura {
 class Window;
-}
+}  // namespace aura
+
+namespace ui {
+class LocatedEvent;
+}  // namespace ui
 
 namespace views {
 class View;
-}
+}  // namespace views
 
 namespace fast_ink {
 
 // Base class for a fast ink based pointer controller. Enables/disables
 // the pointer, receives points and passes them off to be rendered.
-class FastInkPointerController : public ui::EventHandler {
+class FastInkPointerController : public ui::EventHandler,
+                                 public ui::InputDeviceEventObserver {
  public:
   FastInkPointerController();
   ~FastInkPointerController() override;
@@ -32,13 +41,34 @@
   // the pointer.
   virtual void SetEnabled(bool enabled);
 
+  // Add window that should be excluded from handling events.
+  void AddExcludedWindow(aura::Window* window);
+
  protected:
   // Whether the controller is ready to start handling a new gesture.
-  virtual bool CanStartNewGesture(ui::TouchEvent* event);
+  virtual bool CanStartNewGesture(ui::LocatedEvent* event);
+  // Whether the event should be processed and stop propagation.
+  virtual bool ShouldProcessEvent(ui::LocatedEvent* event);
+
+  bool enabled_for_mouse_event() const { return enabled_for_mouse_event_; }
+
+  // Return true if the location of the event is in one of the excluded windows.
+  bool IsPointerInExcludedWindows(ui::LocatedEvent* event);
 
  private:
+  // Creates new pointer view if `can_start_new_gesture` is true. Otherwise, try
+  // to re-use existing one. Ends the current pointer session if the pointer
+  // widget is no longer valid. Returns true if there is a pointer view
+  // available.
+  bool MaybeCreatePointerView(ui::LocatedEvent* event,
+                              bool can_start_new_gesture);
+
   // ui::EventHandler:
   void OnTouchEvent(ui::TouchEvent* event) override;
+  void OnMouseEvent(ui::MouseEvent* event) override;
+
+  // ui::InputDeviceEventObserver:
+  void OnDeviceListsComplete() override;
 
   // Returns the pointer view.
   virtual views::View* GetPointerView() const = 0;
@@ -49,6 +79,7 @@
 
   // Updates the pointer view.
   virtual void UpdatePointerView(ui::TouchEvent* event) = 0;
+  virtual void UpdatePointerView(ui::MouseEvent* event) {}
 
   // Destroys the pointer view if it exists.
   virtual void DestroyPointerView() = 0;
@@ -57,6 +88,18 @@
   const base::TimeDelta presentation_delay_;
 
   bool enabled_ = false;
+  // True if enabled for mouse event.
+  bool enabled_for_mouse_event_ = false;
+
+  // Set of touch ids.
+  std::set<int> touch_ids_;
+
+  // If the pointer event is in the bound of any of the |excluded_windows_|.
+  // Skip processing the event.
+  aura::WindowTracker excluded_windows_;
+
+  base::ScopedObservation<ui::DeviceDataManager, ui::InputDeviceEventObserver>
+      input_device_event_observation_{this};
 
   DISALLOW_COPY_AND_ASSIGN(FastInkPointerController);
 };
diff --git a/ash/fast_ink/laser/laser_pointer_controller.cc b/ash/fast_ink/laser/laser_pointer_controller.cc
index 58e9a3c9..bfc84e8 100644
--- a/ash/fast_ink/laser/laser_pointer_controller.cc
+++ b/ash/fast_ink/laser/laser_pointer_controller.cc
@@ -77,6 +77,14 @@
 
 void LaserPointerController::UpdatePointerView(ui::TouchEvent* event) {
   LaserPointerView* laser_pointer_view = GetLaserPointerView();
+
+  if (IsPointerInExcludedWindows(event)) {
+    // Destroy the |LaserPointerView| since the pointer is in the bound of
+    // excluded windows.
+    DestroyPointerView();
+    return;
+  }
+
   laser_pointer_view->AddNewPoint(event->root_location_f(),
                                   event->time_stamp());
   if (event->type() == ui::ET_TOUCH_RELEASED) {
@@ -85,17 +93,49 @@
   }
 }
 
+void LaserPointerController::UpdatePointerView(ui::MouseEvent* event) {
+  LaserPointerView* laser_pointer_view = GetLaserPointerView();
+  if (event->type() == ui::ET_MOUSE_MOVED) {
+    if (IsPointerInExcludedWindows(event)) {
+      // Destroy the |LaserPointerView| since the cursor is in the bound of
+      // excluded windows.
+      DestroyPointerView();
+      return;
+    }
+
+    // TODO(llin): Hide the mouse cursor.
+  }
+
+  laser_pointer_view->AddNewPoint(event->root_location_f(),
+                                  event->time_stamp());
+  if (event->type() == ui::ET_MOUSE_RELEASED) {
+    laser_pointer_view->FadeOut(base::BindOnce(
+        &LaserPointerController::DestroyPointerView, base::Unretained(this)));
+  }
+}
+
 void LaserPointerController::DestroyPointerView() {
   laser_pointer_view_widget_.reset();
 }
 
-bool LaserPointerController::CanStartNewGesture(ui::TouchEvent* event) {
+bool LaserPointerController::CanStartNewGesture(ui::LocatedEvent* event) {
   // Ignore events over the palette.
+  // TODO(llin): Register palette as a excluded window instead.
   if (palette_utils::PaletteContainsPointInScreen(event->root_location()))
     return false;
   return FastInkPointerController::CanStartNewGesture(event);
 }
 
+bool LaserPointerController::ShouldProcessEvent(ui::LocatedEvent* event) {
+  // Allow clicking when laser pointer is enabled.
+  if (event->type() == ui::ET_MOUSE_PRESSED ||
+      event->type() == ui::ET_MOUSE_RELEASED) {
+    return false;
+  }
+
+  return FastInkPointerController::ShouldProcessEvent(event);
+}
+
 void LaserPointerController::NotifyStateChanged(bool enabled) {
   for (LaserPointerObserver& observer : observers_)
     observer.OnLaserPointerStateChanged(enabled);
diff --git a/ash/fast_ink/laser/laser_pointer_controller.h b/ash/fast_ink/laser/laser_pointer_controller.h
index 9068b48..79388f1 100644
--- a/ash/fast_ink/laser/laser_pointer_controller.h
+++ b/ash/fast_ink/laser/laser_pointer_controller.h
@@ -47,8 +47,10 @@
   void CreatePointerView(base::TimeDelta presentation_delay,
                          aura::Window* root_window) override;
   void UpdatePointerView(ui::TouchEvent* event) override;
+  void UpdatePointerView(ui::MouseEvent* event) override;
   void DestroyPointerView() override;
-  bool CanStartNewGesture(ui::TouchEvent* event) override;
+  bool CanStartNewGesture(ui::LocatedEvent* event) override;
+  bool ShouldProcessEvent(ui::LocatedEvent* event) override;
 
   void NotifyStateChanged(bool enabled);
 
diff --git a/ash/fast_ink/laser/laser_pointer_controller_unittest.cc b/ash/fast_ink/laser/laser_pointer_controller_unittest.cc
index d81e625..9f53207 100644
--- a/ash/fast_ink/laser/laser_pointer_controller_unittest.cc
+++ b/ash/fast_ink/laser/laser_pointer_controller_unittest.cc
@@ -6,6 +6,7 @@
 
 #include "ash/fast_ink/laser/laser_pointer_controller_test_api.h"
 #include "ash/fast_ink/laser/laser_pointer_view.h"
+#include "ash/public/cpp/stylus_utils.h"
 #include "ash/shell.h"
 #include "ash/test/ash_test_base.h"
 #include "ui/events/test/event_generator.h"
@@ -54,6 +55,65 @@
   }
 
  protected:
+  void VerifyLaserPointerRendererTouchEvent() {
+    ui::test::EventGenerator* event_generator = GetEventGenerator();
+
+    // When disabled the laser pointer should not be showing.
+    event_generator->MoveTouch(gfx::Point(1, 1));
+    EXPECT_FALSE(controller_test_api_->IsShowingLaserPointer());
+
+    // Verify that by enabling the mode, the laser pointer should still not be
+    // showing.
+    controller_test_api_->SetEnabled(true);
+    EXPECT_FALSE(controller_test_api_->IsShowingLaserPointer());
+
+    // Verify moving the finger 4 times will not display the laser pointer.
+    event_generator->MoveTouch(gfx::Point(2, 2));
+    event_generator->MoveTouch(gfx::Point(3, 3));
+    event_generator->MoveTouch(gfx::Point(4, 4));
+    event_generator->MoveTouch(gfx::Point(5, 5));
+    EXPECT_FALSE(controller_test_api_->IsShowingLaserPointer());
+
+    // Verify pressing the finger will show the laser pointer and add a point
+    // but will not activate fading out.
+    event_generator->PressTouch();
+    EXPECT_TRUE(controller_test_api_->IsShowingLaserPointer());
+    EXPECT_FALSE(controller_test_api_->IsFadingAway());
+    EXPECT_EQ(1, controller_test_api_->laser_points().GetNumberOfPoints());
+
+    // Verify dragging the finger 2 times will add 2 more points.
+    event_generator->MoveTouch(gfx::Point(6, 6));
+    event_generator->MoveTouch(gfx::Point(7, 7));
+    EXPECT_EQ(3, controller_test_api_->laser_points().GetNumberOfPoints());
+
+    // Verify releasing the finger still shows the laser pointer, which is
+    // fading away.
+    event_generator->ReleaseTouch();
+    EXPECT_TRUE(controller_test_api_->IsShowingLaserPointer());
+    EXPECT_TRUE(controller_test_api_->IsFadingAway());
+
+    // Verify that disabling the mode does not display the laser pointer.
+    controller_test_api_->SetEnabled(false);
+    EXPECT_FALSE(controller_test_api_->IsShowingLaserPointer());
+    EXPECT_FALSE(controller_test_api_->IsFadingAway());
+
+    // Verify that disabling the mode while laser pointer is displayed does not
+    // display the laser pointer.
+    controller_test_api_->SetEnabled(true);
+    event_generator->PressTouch();
+    event_generator->MoveTouch(gfx::Point(6, 6));
+    EXPECT_TRUE(controller_test_api_->IsShowingLaserPointer());
+    controller_test_api_->SetEnabled(false);
+    EXPECT_FALSE(controller_test_api_->IsShowingLaserPointer());
+
+    // Verify that the laser pointer does not add points while disabled.
+    event_generator->PressTouch();
+    event_generator->MoveTouch(gfx::Point(8, 8));
+    event_generator->ReleaseTouch();
+    event_generator->MoveTouch(gfx::Point(9, 9));
+    EXPECT_FALSE(controller_test_api_->IsShowingLaserPointer());
+  }
+
   TestLaserPointerObserver* observer() { return observer_.get(); }
 
   std::unique_ptr<LaserPointerController> controller_;
@@ -66,64 +126,10 @@
 // Test to ensure the class responsible for drawing the laser pointer receives
 // points from stylus movements as expected.
 TEST_F(LaserPointerControllerTest, LaserPointerRenderer) {
-  // The laser pointer mode only works with stylus.
+  stylus_utils::SetHasStylusInputForTesting();
   ui::test::EventGenerator* event_generator = GetEventGenerator();
   event_generator->EnterPenPointerMode();
-
-  // When disabled the laser pointer should not be showing.
-  event_generator->MoveTouch(gfx::Point(1, 1));
-  EXPECT_FALSE(controller_test_api_->IsShowingLaserPointer());
-
-  // Verify that by enabling the mode, the laser pointer should still not be
-  // showing.
-  controller_test_api_->SetEnabled(true);
-  EXPECT_FALSE(controller_test_api_->IsShowingLaserPointer());
-
-  // Verify moving the stylus 4 times will not display the laser pointer.
-  event_generator->MoveTouch(gfx::Point(2, 2));
-  event_generator->MoveTouch(gfx::Point(3, 3));
-  event_generator->MoveTouch(gfx::Point(4, 4));
-  event_generator->MoveTouch(gfx::Point(5, 5));
-  EXPECT_FALSE(controller_test_api_->IsShowingLaserPointer());
-
-  // Verify pressing the stylus will show the laser pointer and add a point but
-  // will not activate fading out.
-  event_generator->PressTouch();
-  EXPECT_TRUE(controller_test_api_->IsShowingLaserPointer());
-  EXPECT_FALSE(controller_test_api_->IsFadingAway());
-  EXPECT_EQ(1, controller_test_api_->laser_points().GetNumberOfPoints());
-
-  // Verify dragging the stylus 2 times will add 2 more points.
-  event_generator->MoveTouch(gfx::Point(6, 6));
-  event_generator->MoveTouch(gfx::Point(7, 7));
-  EXPECT_EQ(3, controller_test_api_->laser_points().GetNumberOfPoints());
-
-  // Verify releasing the stylus still shows the laser pointer, which is fading
-  // away.
-  event_generator->ReleaseTouch();
-  EXPECT_TRUE(controller_test_api_->IsShowingLaserPointer());
-  EXPECT_TRUE(controller_test_api_->IsFadingAway());
-
-  // Verify that disabling the mode does not display the laser pointer.
-  controller_test_api_->SetEnabled(false);
-  EXPECT_FALSE(controller_test_api_->IsShowingLaserPointer());
-  EXPECT_FALSE(controller_test_api_->IsFadingAway());
-
-  // Verify that disabling the mode while laser pointer is displayed does not
-  // display the laser pointer.
-  controller_test_api_->SetEnabled(true);
-  event_generator->PressTouch();
-  event_generator->MoveTouch(gfx::Point(6, 6));
-  EXPECT_TRUE(controller_test_api_->IsShowingLaserPointer());
-  controller_test_api_->SetEnabled(false);
-  EXPECT_FALSE(controller_test_api_->IsShowingLaserPointer());
-
-  // Verify that the laser pointer does not add points while disabled.
-  event_generator->PressTouch();
-  event_generator->MoveTouch(gfx::Point(8, 8));
-  event_generator->ReleaseTouch();
-  event_generator->MoveTouch(gfx::Point(9, 9));
-  EXPECT_FALSE(controller_test_api_->IsShowingLaserPointer());
+  VerifyLaserPointerRendererTouchEvent();
 
   // Verify that the laser pointer does not get shown if points are not coming
   // from the stylus, even when enabled.
@@ -148,6 +154,73 @@
   static_cast<ui::EventHandler*>(controller_.get())->OnTouchEvent(&touch);
 }
 
+// Test to ensure the class responsible for drawing the laser pointer receives
+// points from mouse movements as expected.
+TEST_F(LaserPointerControllerTest, LaserPointerRendererTouchEvent) {
+  stylus_utils::SetNoStylusInputForTesting();
+  VerifyLaserPointerRendererTouchEvent();
+
+  // Make sure that event can be sent after the pointer widget is destroyed
+  // by release. This can happen if the touch event causes the deletion of
+  // the pointer event in an earlier event handler.
+  ui::PointerDetails pointer_details;
+
+  ui::TouchEvent touch(ui::ET_TOUCH_MOVED, gfx::PointF(), gfx::PointF(),
+                       base::TimeTicks(), pointer_details, 0);
+  ui::Event::DispatcherApi api(&touch);
+  api.set_target(Shell::GetPrimaryRootWindow());
+  static_cast<ui::EventHandler*>(controller_.get())->OnTouchEvent(&touch);
+}
+
+// Test to ensure the class responsible for drawing the laser pointer receives
+// points from mouse movements as expected.
+TEST_F(LaserPointerControllerTest, LaserPointerRendererMouseEvent) {
+  stylus_utils::SetNoStylusInputForTesting();
+
+  ui::test::EventGenerator* event_generator = GetEventGenerator();
+
+  // When disabled the laser pointer should not be showing.
+  event_generator->MoveMouseTo(gfx::Point(1, 1));
+  EXPECT_FALSE(controller_test_api_->IsShowingLaserPointer());
+
+  // Verify that by enabling the mode, the laser pointer should still not be
+  // showing.
+  controller_test_api_->SetEnabled(true);
+  EXPECT_FALSE(controller_test_api_->IsShowingLaserPointer());
+
+  // Verify moving the cursor 4 times will display the laser pointer.
+  event_generator->MoveMouseTo(gfx::Point(2, 2));
+  event_generator->MoveMouseTo(gfx::Point(3, 3));
+  event_generator->MoveMouseTo(gfx::Point(4, 4));
+  event_generator->MoveMouseTo(gfx::Point(5, 5));
+  EXPECT_TRUE(controller_test_api_->IsShowingLaserPointer());
+  EXPECT_FALSE(controller_test_api_->IsFadingAway());
+  EXPECT_EQ(4, controller_test_api_->laser_points().GetNumberOfPoints());
+
+  // Verify moving the cursor 2 times will add 2 more points.
+  event_generator->MoveMouseTo(gfx::Point(6, 6));
+  event_generator->MoveMouseTo(gfx::Point(7, 7));
+  EXPECT_EQ(6, controller_test_api_->laser_points().GetNumberOfPoints());
+
+  // Verify that disabling the mode does not display the laser pointer.
+  controller_test_api_->SetEnabled(false);
+  EXPECT_FALSE(controller_test_api_->IsShowingLaserPointer());
+  EXPECT_FALSE(controller_test_api_->IsFadingAway());
+
+  // Verify that disabling the mode while laser pointer is displayed does not
+  // display the laser pointer.
+  controller_test_api_->SetEnabled(true);
+  event_generator->MoveMouseTo(gfx::Point(6, 6));
+  EXPECT_TRUE(controller_test_api_->IsShowingLaserPointer());
+  controller_test_api_->SetEnabled(false);
+  EXPECT_FALSE(controller_test_api_->IsShowingLaserPointer());
+
+  // Verify that the laser pointer does not add points while disabled.
+  event_generator->MoveMouseTo(gfx::Point(8, 8));
+  event_generator->MoveMouseTo(gfx::Point(9, 9));
+  EXPECT_FALSE(controller_test_api_->IsShowingLaserPointer());
+}
+
 // Test to ensure the class responsible for drawing the laser pointer handles
 // prediction as expected when it receives points from stylus movements.
 TEST_F(LaserPointerControllerTest, LaserPointerPrediction) {
diff --git a/ash/highlighter/highlighter_controller.cc b/ash/highlighter/highlighter_controller.cc
index f2d78dd..bffce0b5 100644
--- a/ash/highlighter/highlighter_controller.cc
+++ b/ash/highlighter/highlighter_controller.cc
@@ -232,7 +232,7 @@
   DestroyResultView();
 }
 
-bool HighlighterController::CanStartNewGesture(ui::TouchEvent* event) {
+bool HighlighterController::CanStartNewGesture(ui::LocatedEvent* event) {
   // Ignore events over the palette.
   if (palette_utils::PaletteContainsPointInScreen(event->root_location()))
     return false;
diff --git a/ash/highlighter/highlighter_controller.h b/ash/highlighter/highlighter_controller.h
index 7bf9347..3e3f46c 100644
--- a/ash/highlighter/highlighter_controller.h
+++ b/ash/highlighter/highlighter_controller.h
@@ -90,7 +90,7 @@
                          aura::Window* root_window) override;
   void UpdatePointerView(ui::TouchEvent* event) override;
   void DestroyPointerView() override;
-  bool CanStartNewGesture(ui::TouchEvent* event) override;
+  bool CanStartNewGesture(ui::LocatedEvent* event) override;
 
   // Performs gesture recognition, initiates appropriate visual effects,
   // notifies the observer if necessary.
diff --git a/ash/highlighter/highlighter_controller_unittest.cc b/ash/highlighter/highlighter_controller_unittest.cc
index afb84bd..3e1d2f1 100644
--- a/ash/highlighter/highlighter_controller_unittest.cc
+++ b/ash/highlighter/highlighter_controller_unittest.cc
@@ -9,6 +9,7 @@
 #include "ash/assistant/test/assistant_ash_test_base.h"
 #include "ash/fast_ink/fast_ink_points.h"
 #include "ash/highlighter/highlighter_controller_test_api.h"
+#include "ash/public/cpp/stylus_utils.h"
 #include "ash/shell.h"
 #include "ash/system/palette/mock_palette_tool_delegate.h"
 #include "ash/system/palette/palette_tool.h"
@@ -120,6 +121,8 @@
 // Test to ensure the class responsible for drawing the highlighter pointer
 // receives points from stylus movements as expected.
 TEST_F(HighlighterControllerTest, HighlighterRenderer) {
+  ash::stylus_utils::SetHasStylusInputForTesting();
+
   // The highlighter pointer mode only works with stylus.
   ui::test::EventGenerator* event_generator = GetEventGenerator();
   event_generator->EnterPenPointerMode();
diff --git a/ash/projector/projector_ui_controller.cc b/ash/projector/projector_ui_controller.cc
index ee62479d..168a880 100644
--- a/ash/projector/projector_ui_controller.cc
+++ b/ash/projector/projector_ui_controller.cc
@@ -10,6 +10,7 @@
 #include "ash/shell.h"
 #include "ash/strings/grit/ash_strings.h"
 #include "ash/system/toast/toast_manager_impl.h"
+#include "ui/aura/window.h"
 #include "ui/base/l10n/l10n_util.h"
 #include "ui/views/widget/widget.h"
 
@@ -32,6 +33,11 @@
   Shell::Get()->toast_manager()->Show(toast);
 }
 
+void AddExcludedWindowToFastInkController(aura::Window* window) {
+  DCHECK(window);
+  Shell::Get()->laser_pointer_controller()->AddExcludedWindow(window);
+}
+
 }  // namespace
 
 ProjectorUiController::ProjectorUiController(
@@ -44,6 +50,8 @@
   if (!projector_bar_widget_) {
     // Create the toolbar.
     projector_bar_widget_ = ProjectorBarView::Create(projector_controller_);
+    AddExcludedWindowToFastInkController(
+        projector_bar_widget_->GetNativeWindow());
   }
 
   projector_bar_widget_->ShowInactive();
diff --git a/ash/public/cpp/external_arc/message_center/arc_notification_content_view.cc b/ash/public/cpp/external_arc/message_center/arc_notification_content_view.cc
index 0136db0..63aa89981 100644
--- a/ash/public/cpp/external_arc/message_center/arc_notification_content_view.cc
+++ b/ash/public/cpp/external_arc/message_center/arc_notification_content_view.cc
@@ -555,7 +555,8 @@
   auto mask_painter =
       std::make_unique<message_center::NotificationBackgroundPainter>(
           top_radius_, bottom_radius_,
-          message_center::kNotificationBackgroundColor);
+          GetNativeTheme()->GetSystemColor(
+              ui::NativeTheme::kColorId_NotificationBackground));
   // Set insets to round visible notification corners. https://crbug.com/866777
   mask_painter->set_insets(new_insets);
 
@@ -723,6 +724,13 @@
   notification_view->OnContentBlurred();
 }
 
+void ArcNotificationContentView::OnThemeChanged() {
+  View::OnThemeChanged();
+  // OnThemeChanged may be called before container is set.
+  if (GetWidget() && GetNativeViewContainer())
+    UpdateMask(true);
+}
+
 void ArcNotificationContentView::OnRemoteInputActivationChanged(
     bool activated) {
   // Remove the focus from the currently focused view-control in the message
diff --git a/ash/public/cpp/external_arc/message_center/arc_notification_content_view.h b/ash/public/cpp/external_arc/message_center/arc_notification_content_view.h
index 18b8f97..95340f84 100644
--- a/ash/public/cpp/external_arc/message_center/arc_notification_content_view.h
+++ b/ash/public/cpp/external_arc/message_center/arc_notification_content_view.h
@@ -55,7 +55,6 @@
   ArcNotificationContentView& operator=(const ArcNotificationContentView&) = delete;
   ~ArcNotificationContentView() override;
 
-
   void Update(const message_center::Notification& notification);
   message_center::NotificationControlButtonsView* GetControlButtonsView();
   void UpdateControlButtonsVisibility();
@@ -105,6 +104,7 @@
   void OnMouseExited(const ui::MouseEvent& event) override;
   void OnFocus() override;
   void OnBlur() override;
+  void OnThemeChanged() override;
   views::FocusTraversable* GetFocusTraversable() override;
   void GetAccessibleNodeData(ui::AXNodeData* node_data) override;
   void OnAccessibilityEvent(ax::mojom::Event event) override;
diff --git a/ash/public/cpp/external_arc/message_center/arc_notification_view.cc b/ash/public/cpp/external_arc/message_center/arc_notification_view.cc
index e1274fb14..af5c67cc 100644
--- a/ash/public/cpp/external_arc/message_center/arc_notification_view.cc
+++ b/ash/public/cpp/external_arc/message_center/arc_notification_view.cc
@@ -56,9 +56,6 @@
         content_view_->background()->get_color());
   }
 
-  focus_painter_ = views::Painter::CreateSolidFocusPainter(
-      message_center::kFocusBorderColor, gfx::Insets(0, 1, 3, 2));
-
   UpdateCornerRadius(message_center::kNotificationCornerRadius,
                      message_center::kNotificationCornerRadius);
 }
@@ -162,6 +159,14 @@
     return item_->OpenSnooze();
 }
 
+void ArcNotificationView::OnThemeChanged() {
+  message_center::MessageView::OnThemeChanged();
+  focus_painter_ = views::Painter::CreateSolidFocusPainter(
+      GetNativeTheme()->GetSystemColor(
+          ui::NativeTheme::kColorId_FocusedBorderColor),
+      gfx::Insets(0, 1, 3, 2));
+}
+
 void ArcNotificationView::OnContainerAnimationEnded() {
   content_view_->OnContainerAnimationEnded();
 }
diff --git a/ash/public/cpp/external_arc/message_center/arc_notification_view.h b/ash/public/cpp/external_arc/message_center/arc_notification_view.h
index 19720f14..84e643f8 100644
--- a/ash/public/cpp/external_arc/message_center/arc_notification_view.h
+++ b/ash/public/cpp/external_arc/message_center/arc_notification_view.h
@@ -58,6 +58,7 @@
   void OnContainerAnimationEnded() override;
   void OnSettingsButtonPressed(const ui::Event& event) override;
   void OnSnoozeButtonPressed(const ui::Event& event) override;
+  void OnThemeChanged() override;
   void UpdateCornerRadius(int top_radius, int bottom_radius) override;
 
   // views::SlideOutControllerDelegate:
diff --git a/ash/public/cpp/stylus_utils.cc b/ash/public/cpp/stylus_utils.cc
index cfd9010..2a5fe39 100644
--- a/ash/public/cpp/stylus_utils.cc
+++ b/ash/public/cpp/stylus_utils.cc
@@ -60,5 +60,9 @@
   g_has_stylus_input_for_testing = true;
 }
 
+void SetNoStylusInputForTesting() {
+  g_has_stylus_input_for_testing = false;
+}
+
 }  // namespace stylus_utils
 }  // namespace ash
diff --git a/ash/public/cpp/stylus_utils.h b/ash/public/cpp/stylus_utils.h
index b3a92aa5..6d39c61 100644
--- a/ash/public/cpp/stylus_utils.h
+++ b/ash/public/cpp/stylus_utils.h
@@ -24,9 +24,12 @@
 // Returns true if the device has an internal stylus.
 ASH_PUBLIC_EXPORT bool HasInternalStylus();
 
-// Forcibly say the device is has stylus input for testing purposes.
+// Forcibly say the device has stylus input for testing purposes.
 ASH_PUBLIC_EXPORT void SetHasStylusInputForTesting();
 
+// Forcibly say the device doesn't have stylus input for testing purposes.
+ASH_PUBLIC_EXPORT void SetNoStylusInputForTesting();
+
 }  // namespace stylus_utils
 }  // namespace ash
 
diff --git a/ash/system/message_center/stacked_notification_bar.cc b/ash/system/message_center/stacked_notification_bar.cc
index 70b3295..09e3b70 100644
--- a/ash/system/message_center/stacked_notification_bar.cc
+++ b/ash/system/message_center/stacked_notification_bar.cc
@@ -124,12 +124,18 @@
   void OnThemeChanged() override {
     views::ImageView::OnThemeChanged();
 
+    auto* theme = GetNativeTheme();
+
     auto* notification =
         message_center::MessageCenter::Get()->FindVisibleNotificationById(id_);
     SkColor accent_color = GetNativeTheme()->GetSystemColor(
         ui::NativeTheme::kColorId_NotificationDefaultAccentColor);
     gfx::Image masked_small_icon = notification->GenerateMaskedSmallIcon(
-        kStackedNotificationIconSize, accent_color);
+        kStackedNotificationIconSize, accent_color,
+        theme->GetSystemColor(
+            ui::NativeTheme::kColorId_MessageCenterSmallImageMaskBackground),
+        theme->GetSystemColor(
+            ui::NativeTheme::kColorId_MessageCenterSmallImageMaskForeground));
 
     if (masked_small_icon.IsEmpty()) {
       SetImage(gfx::CreateVectorIcon(message_center::kProductIcon,
diff --git a/ash/system/unified/notification_icons_controller.cc b/ash/system/unified/notification_icons_controller.cc
index cd49564..bc69d455 100644
--- a/ash/system/unified/notification_icons_controller.cc
+++ b/ash/system/unified/notification_icons_controller.cc
@@ -79,10 +79,15 @@
     message_center::Notification* notification) {
   notification_id_ = notification->id();
 
+  auto* theme = GetNativeTheme();
   gfx::Image masked_small_icon = notification->GenerateMaskedSmallIcon(
       kUnifiedTrayIconSize,
       AshColorProvider::Get()->GetContentLayerColor(
-          AshColorProvider::ContentLayerType::kIconColorPrimary));
+          AshColorProvider::ContentLayerType::kIconColorPrimary),
+      theme->GetSystemColor(
+          ui::NativeTheme::kColorId_MessageCenterSmallImageMaskBackground),
+      theme->GetSystemColor(
+          ui::NativeTheme::kColorId_MessageCenterSmallImageMaskForeground));
   if (!masked_small_icon.IsEmpty()) {
     image_view()->SetImage(masked_small_icon.AsImageSkia());
   } else {
diff --git a/ash/wm/desks/desk_animation_impl_unittest.cc b/ash/wm/desks/desk_animation_impl_unittest.cc
index 398d30d2..de78d325 100644
--- a/ash/wm/desks/desk_animation_impl_unittest.cc
+++ b/ash/wm/desks/desk_animation_impl_unittest.cc
@@ -15,6 +15,19 @@
 
 namespace ash {
 
+namespace {
+
+void WaitEndingScreenshotTaken(DeskActivationAnimation* animation) {
+  base::RunLoop run_loop;
+  auto* desk_switch_animator =
+      animation->GetDeskSwitchAnimatorAtIndexForTesting(0);
+  RootWindowDeskSwitchAnimatorTestApi(desk_switch_animator)
+      .SetOnEndingScreenshotTakenCallback(run_loop.QuitClosure());
+  run_loop.Run();
+}
+
+}  // namespace
+
 using DeskActivationAnimationTest = AshTestBase;
 
 // Tests that there is no crash when ending a swipe animation before the
@@ -86,32 +99,26 @@
   desks_controller->NewDesk(DesksCreationRemovalSource::kButton);
   desks_controller->NewDesk(DesksCreationRemovalSource::kButton);
 
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(features::kEnhancedDeskAnimations);
+
   DeskActivationAnimation animation(desks_controller, 0, 1,
                                     DesksSwitchSource::kDeskSwitchTouchpad,
                                     /*update_window_activation=*/false);
   animation.set_skip_notify_controller_on_animation_finished_for_testing(true);
   animation.Launch();
 
-  auto wait_ending_screenshot_taken = [](DeskActivationAnimation* animation) {
-    base::RunLoop run_loop;
-    auto* desk_switch_animator =
-        animation->GetDeskSwitchAnimatorAtIndexForTesting(0);
-    RootWindowDeskSwitchAnimatorTestApi(desk_switch_animator)
-        .SetOnEndingScreenshotTakenCallback(run_loop.QuitClosure());
-    run_loop.Run();
-  };
-
-  wait_ending_screenshot_taken(&animation);
+  WaitEndingScreenshotTaken(&animation);
   EXPECT_EQ(0, animation.visible_desk_changes());
 
   // Swipe enough so that our third and fourth desk screenshots are taken, and
   // then swipe so that the fourth desk is fully shown. There should be 3
   // visible desk changes in total.
   animation.UpdateSwipeAnimation(-kTouchpadSwipeLengthForDeskChange);
-  wait_ending_screenshot_taken(&animation);
+  WaitEndingScreenshotTaken(&animation);
 
   animation.UpdateSwipeAnimation(-kTouchpadSwipeLengthForDeskChange);
-  wait_ending_screenshot_taken(&animation);
+  WaitEndingScreenshotTaken(&animation);
 
   animation.UpdateSwipeAnimation(-3 * kTouchpadSwipeLengthForDeskChange);
   EXPECT_EQ(3, animation.visible_desk_changes());
@@ -131,4 +138,24 @@
   EXPECT_EQ(7, animation.visible_desk_changes());
 }
 
+// Tests that closing windows during a desk animation does not cause a crash.
+TEST_F(DeskActivationAnimationTest, CloseWindowDuringAnimation) {
+  auto* desks_controller = DesksController::Get();
+  desks_controller->NewDesk(DesksCreationRemovalSource::kButton);
+
+  std::unique_ptr<aura::Window> window = CreateAppWindow(gfx::Rect(250, 100));
+
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(features::kEnhancedDeskAnimations);
+
+  DeskActivationAnimation animation(desks_controller, 0, 1,
+                                    DesksSwitchSource::kDeskSwitchTouchpad,
+                                    /*update_window_activation=*/false);
+  animation.set_skip_notify_controller_on_animation_finished_for_testing(true);
+  animation.Launch();
+
+  window.reset();
+  WaitEndingScreenshotTaken(&animation);
+}
+
 }  // namespace ash
diff --git a/ash/wm/overview/overview_controller_unittest.cc b/ash/wm/overview/overview_controller_unittest.cc
index d8fe0f69..fdb6943 100644
--- a/ash/wm/overview/overview_controller_unittest.cc
+++ b/ash/wm/overview/overview_controller_unittest.cc
@@ -620,6 +620,34 @@
   EXPECT_TRUE(WindowState::Get(window.get())->IsMinimized());
 }
 
+// Tests that overview animations continue even if a window gets destroyed
+// during the animation.
+TEST_F(OverviewControllerTest, CloseWindowDuringAnimation) {
+  // Create two windows. They should both be visible so that they both get
+  // animated.
+  std::unique_ptr<aura::Window> window1 = CreateAppWindow(gfx::Rect(250, 100));
+  std::unique_ptr<aura::Window> window2 =
+      CreateAppWindow(gfx::Rect(250, 250, 250, 100));
+
+  ui::ScopedAnimationDurationScaleMode non_zero(
+      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
+  Shell::Get()->overview_controller()->StartOverview();
+
+  // Destroy a window during the enter animation.
+  window1.reset();
+  ShellTestApi().WaitForOverviewAnimationState(
+      OverviewAnimationState::kEnterAnimationComplete);
+  ASSERT_TRUE(Shell::Get()->overview_controller()->InOverviewSession());
+
+  Shell::Get()->overview_controller()->EndOverview();
+
+  // Destroy a window during the exit animation.
+  window2.reset();
+  ShellTestApi().WaitForOverviewAnimationState(
+      OverviewAnimationState::kExitAnimationComplete);
+  EXPECT_FALSE(Shell::Get()->overview_controller()->InOverviewSession());
+}
+
 // A subclass of DeskSwitchAnimationWaiter that additionally attempts to start
 // overview after the desk animation screenshots have been taken. Using the
 // regular DeskSwitchAnimatorWaiter and attempting to start overview before
diff --git a/ash/wm/tablet_mode/tablet_mode_controller_unittest.cc b/ash/wm/tablet_mode/tablet_mode_controller_unittest.cc
index a5c0ce2d..4d993c0 100644
--- a/ash/wm/tablet_mode/tablet_mode_controller_unittest.cc
+++ b/ash/wm/tablet_mode/tablet_mode_controller_unittest.cc
@@ -1632,6 +1632,31 @@
   histogram_tester.ExpectTotalCount(kExitHistogram, 0);
 }
 
+// Tests that closing a window during the tablet mode enter animation does not
+// cause a crash.
+TEST_F(TabletModeControllerTest, CloseWindowDuringEnterAnimation) {
+  std::unique_ptr<aura::Window> window = CreateAppWindow(gfx::Rect(250, 100));
+
+  ui::ScopedAnimationDurationScaleMode test_duration_mode(
+      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
+
+  tablet_mode_controller()->SetEnabledForTest(true);
+  window.reset();
+}
+
+// Tests that closing a window during the tablet mode exit animation does not
+// cause a crash.
+TEST_F(TabletModeControllerTest, CloseWindowDuringExitAnimation) {
+  std::unique_ptr<aura::Window> window = CreateAppWindow(gfx::Rect(250, 100));
+  tablet_mode_controller()->SetEnabledForTest(true);
+
+  ui::ScopedAnimationDurationScaleMode test_duration_mode(
+      ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
+
+  tablet_mode_controller()->SetEnabledForTest(false);
+  window.reset();
+}
+
 class TabletModeControllerOnDeviceTest : public TabletModeControllerTest {
  public:
   TabletModeControllerOnDeviceTest() = default;
diff --git a/base/BUILD.gn b/base/BUILD.gn
index 092acd0..5288a27 100644
--- a/base/BUILD.gn
+++ b/base/BUILD.gn
@@ -598,8 +598,6 @@
     "strings/escape.h",
     "strings/latin1_string_conversions.cc",
     "strings/latin1_string_conversions.h",
-    "strings/nullable_string16.cc",
-    "strings/nullable_string16.h",
     "strings/pattern.cc",
     "strings/pattern.h",
     "strings/safe_sprintf.cc",
@@ -3009,7 +3007,6 @@
       "strings/char_traits_unittest.cc",
       "strings/escape_unittest.cc",
       "strings/no_trigraphs_unittest.cc",
-      "strings/nullable_string16_unittest.cc",
       "strings/pattern_unittest.cc",
       "strings/safe_sprintf_unittest.cc",
       "strings/strcat_unittest.cc",
diff --git a/base/allocator/partition_allocator/pcscan.cc b/base/allocator/partition_allocator/pcscan.cc
index 21388347..43d20dc 100644
--- a/base/allocator/partition_allocator/pcscan.cc
+++ b/base/allocator/partition_allocator/pcscan.cc
@@ -200,7 +200,10 @@
   V(Sweep)                               \
   V(Overall)
 
-#define FOR_ALL_PCSCAN_MUTATOR_SCOPES(V) V(Scan)
+#define FOR_ALL_PCSCAN_MUTATOR_SCOPES(V) \
+  V(Clear)                               \
+  V(Scan)                                \
+  V(Overall)
 
 class StatsCollector final {
  public:
@@ -208,12 +211,14 @@
 #define DECLARE_ENUM(name) k##name,
     FOR_ALL_PCSCAN_SCANNER_SCOPES(DECLARE_ENUM)
 #undef DECLARE_ENUM
+        kNumIds,
   };
 
   enum class MutatorId {
 #define DECLARE_ENUM(name) k##name,
     FOR_ALL_PCSCAN_MUTATOR_SCOPES(DECLARE_ENUM)
 #undef DECLARE_ENUM
+        kNumIds,
   };
 
   enum class Context {
@@ -255,12 +260,22 @@
           return "PCScan.Scanner.Sweep";
         case ScannerId::kOverall:
           return "PCScan.Scanner";
+        case ScannerId::kNumIds:
+          __builtin_unreachable();
       }
     }
 
     static constexpr const char* ToTracingString(MutatorId id) {
-      PA_DCHECK(id == MutatorId::kScan);
-      return "PCScan.Mutator.Sweep";
+      switch (id) {
+        case MutatorId::kClear:
+          return "PCScan.Mutator.Clear";
+        case MutatorId::kScan:
+          return "PCScan.Mutator.Scan";
+        case MutatorId::kOverall:
+          return "PCScan.Mutator";
+        case MutatorId::kNumIds:
+          __builtin_unreachable();
+      }
     }
 
     StatsCollector& stats_;
@@ -278,28 +293,48 @@
   StatsCollector& operator=(const StatsCollector&) = delete;
 
   void IncreaseScopeTime(ScannerId type, base::TimeDelta duration) {
-    scanner_scopes_[static_cast<size_t>(type)] += duration;
+    const int64_t ms = duration.InMicroseconds();
+    PA_DCHECK(ms <= std::numeric_limits<uint32_t>::max());
+    scanner_scopes_[static_cast<size_t>(type)].fetch_add(
+        ms, std::memory_order_relaxed);
   }
 
   void IncreaseScopeTime(MutatorId type, base::TimeDelta duration) {
-    mutator_scopes_[static_cast<size_t>(type)] += duration;
+    const int64_t ms = duration.InMicroseconds();
+    PA_DCHECK(ms <= std::numeric_limits<uint32_t>::max());
+    mutator_scopes_[static_cast<size_t>(type)].fetch_add(
+        ms, std::memory_order_relaxed);
   }
 
+  void IncreaseSurvivedQuarantineSize(size_t size) {
+    survived_quarantine_size_.fetch_add(size, std::memory_order_relaxed);
+  }
+  size_t survived_quarantine_size() const {
+    return survived_quarantine_size_.load(std::memory_order_relaxed);
+  }
+
+  void IncreaseSweptSize(size_t size) { swept_size_ += size; }
+  size_t swept_size() const { return swept_size_; }
+
   void UpdateHistograms() {
     if (!process_name_) {
       // Don't update histograms if |process_name_| is not set.
       return;
     }
-#define UPDATE_UMA(name)                       \
-  UMA_HISTOGRAM_TIMES(                         \
-      ToUMAString(ScannerId::k##name).c_str(), \
-      scanner_scopes_[static_cast<size_t>(ScannerId::k##name)]);
+#define UPDATE_UMA(name)                                                 \
+  UMA_HISTOGRAM_TIMES(                                                   \
+      ToUMAString(ScannerId::k##name).c_str(),                           \
+      base::TimeDelta::FromMicroseconds(                                 \
+          scanner_scopes_[static_cast<size_t>(ScannerId::k##name)].load( \
+              std::memory_order_relaxed)));
     FOR_ALL_PCSCAN_SCANNER_SCOPES(UPDATE_UMA)
 #undef UPDATE_UMA
-#define UPDATE_UMA(name)                       \
-  UMA_HISTOGRAM_TIMES(                         \
-      ToUMAString(MutatorId::k##name).c_str(), \
-      mutator_scopes_[static_cast<size_t>(MutatorId::k##name)]);
+#define UPDATE_UMA(name)                                                 \
+  UMA_HISTOGRAM_TIMES(                                                   \
+      ToUMAString(MutatorId::k##name).c_str(),                           \
+      base::TimeDelta::FromMicroseconds(                                 \
+          mutator_scopes_[static_cast<size_t>(MutatorId::k##name)].load( \
+              std::memory_order_relaxed)));
     FOR_ALL_PCSCAN_MUTATOR_SCOPES(UPDATE_UMA)
 #undef UPDATE_UMA
   }
@@ -320,24 +355,57 @@
         return "PA.PCScan." + process_name + ".Scanner.Sweep";
       case ScannerId::kOverall:
         return "PA.PCScan." + process_name + ".Scanner";
+      case ScannerId::kNumIds:
+        __builtin_unreachable();
     }
   }
 
   MetadataString ToUMAString(MutatorId id) const {
     PA_DCHECK(process_name_);
-    PA_DCHECK(id == MutatorId::kScan);
-    return "PA.PCScan." + MetadataString(process_name_) + ".Mutator.Scan";
+    const MetadataString process_name = process_name_;
+    switch (id) {
+      case MutatorId::kClear:
+        return "PA.PCScan." + process_name + ".Mutator.Clear";
+      case MutatorId::kScan:
+        return "PA.PCScan." + process_name + ".Mutator.Scan";
+      case MutatorId::kOverall:
+        return "PA.PCScan." + process_name + ".Mutator";
+      case MutatorId::kNumIds:
+        __builtin_unreachable();
+    }
   }
 
+  std::array<std::atomic<uint32_t>, static_cast<size_t>(ScannerId::kNumIds)>
+      scanner_scopes_;
+  std::array<std::atomic<uint32_t>, static_cast<size_t>(MutatorId::kNumIds)>
+      mutator_scopes_;
+  std::atomic<size_t> survived_quarantine_size_{0u};
+  size_t swept_size_ = 0u;
   const char* process_name_ = nullptr;
-  std::array<base::TimeDelta, 4> scanner_scopes_;
-  std::array<base::TimeDelta, 1> mutator_scopes_;
 };
 
 #undef FOR_ALL_PCSCAN_MUTATOR_SCOPES
 #undef FOR_ALL_PCSCAN_SCANNER_SCOPES
 
-// Internal singleton that keeps cold data.
+enum class SimdSupport : uint8_t {
+  kUnvectorized,
+  kSSE3,
+  kAVX2,
+  // TODO(bikineev): Add support for Neon.
+};
+
+SimdSupport DetectSimdSupport() {
+  base::CPU cpu;
+  if (cpu.has_avx2())
+    return SimdSupport::kAVX2;
+  if (cpu.has_sse3())
+    return SimdSupport::kSSE3;
+  return SimdSupport::kUnvectorized;
+}
+
+// Internal PCScan singleton. The separation between frontend and backend is
+// needed to keep access to the hot data (quarantine) in the frontend fast,
+// whereas the backend can hold cold data.
 class PCScanInternal final {
  public:
   using Root = PCScan::Root;
@@ -373,8 +441,8 @@
   static PCScanInternal& Instance() {
     // Since the data that PCScanInternal holds is cold, it's fine to have the
     // runtime check for thread-safe local static initialization.
-    static PCScanInternal instance;
-    return instance;
+    static base::NoDestructor<PCScanInternal> instance;
+    return *instance;
   }
 
   PCScanInternal(const PCScanInternal&) = delete;
@@ -395,14 +463,19 @@
   // Get size of all committed pages from scannable and nonscannable roots.
   size_t CalculateTotalHeapSize() const;
 
+  SimdSupport simd_support() const { return simd_support_; }
+
   void ClearRootsForTesting();  // IN-TEST
 
  private:
+  friend base::NoDestructor<PCScanInternal>;
+
   PCScanInternal();
 
   Roots scannable_roots_{};
   Roots nonscannable_roots_{};
   const char* process_name_ = nullptr;
+  const SimdSupport simd_support_;
 };
 
 void PCScanInternal::Roots::Add(Root* root) {
@@ -418,7 +491,7 @@
   current_ = 0;
 }
 
-PCScanInternal::PCScanInternal() {
+PCScanInternal::PCScanInternal() : simd_support_(DetectSimdSupport()) {
 #if defined(PA_HAS_64_BITS_POINTERS)
   if (features::IsPartitionAllocGigaCageEnabled()) {
     PartitionAddressSpace::Init();
@@ -493,8 +566,8 @@
   struct ScanArea {
     ScanArea(uintptr_t* begin, uintptr_t* end) : begin(begin), end(end) {}
 
-    uintptr_t* begin;
-    uintptr_t* end;
+    uintptr_t* begin = nullptr;
+    uintptr_t* end = nullptr;
   };
   using ScanAreas = std::vector<ScanArea, MetadataAllocator<ScanArea>>;
 
@@ -548,8 +621,8 @@
     typename Root::ScopedGuard guard(root->lock_);
 
     // Take a snapshot of all super pages and scannable slot spans.
-    // TODO(bikineev): Consider making current_extent lock-free and moving it to
-    // the concurrent thread.
+    // TODO(bikineev): Consider making current_extent lock-free and moving it
+    // to the concurrent thread.
     for (auto* super_page_extent = root->first_extent; super_page_extent;
          super_page_extent = super_page_extent->next) {
       for (char* super_page = super_page_extent->super_page_base;
@@ -607,7 +680,8 @@
 }  // namespace
 
 // This class is responsible for performing the entire PCScan task.
-class PCScan::PCScanTask final {
+// TODO(bikineev): Move PCScan algorithm out of PCScanTask.
+class PCScanTask final {
  public:
   static void* operator new(size_t size) {
     return PCScanMetadataAllocator().AllocFlagsNoHooks(0, size);
@@ -623,12 +697,14 @@
   PCScanTask(PCScanTask&&) noexcept = delete;
   PCScanTask& operator=(PCScanTask&&) noexcept = delete;
 
-  // Execute PCScan. Must be executed only once.
-  void RunOnce() &&;
+  // Execute PCScan from the scanner thread. Must be called only once from the
+  // scanner thread.
+  void RunFromScanner();
 
  private:
   class ScanLoop;
 
+  using Root = PCScan::Root;
   using SlotSpan = SlotSpanMetadata<ThreadSafe>;
 
   struct GigaCageLookupPolicy {
@@ -669,15 +745,17 @@
 
   // Scans all registeres partitions and marks reachable quarantined objects.
   // Returns the size of marked objects.
-  size_t ScanPartitions();
+  void ScanPartitions();
 
-  // Clear quarantined objects and filter out super pages that don't contain
-  // quarantine.
-  void ClearQuarantinedObjectsAndFilterSuperPages();
+  // Clear quarantined objects and prepare card table for fast lookup
+  void ClearQuarantinedObjectsAndPrepareCardTable();
 
   // Sweeps (frees) unreachable quarantined entries. Returns the size of swept
   // objects.
-  size_t SweepQuarantine();
+  void SweepQuarantine();
+
+  // Finishes the scanner (updates limits, UMA, etc).
+  void FinishScanner();
 
   // Cache the pcscan epoch to avoid the compiler loading the atomic
   // QuarantineData::epoch_ on each access.
@@ -688,8 +766,8 @@
 };
 
 template <typename LookupPolicy>
-ALWAYS_INLINE QuarantineBitmap*
-PCScan::PCScanTask::TryFindScannerBitmapForPointer(uintptr_t maybe_ptr) const {
+ALWAYS_INLINE QuarantineBitmap* PCScanTask::TryFindScannerBitmapForPointer(
+    uintptr_t maybe_ptr) const {
   // First, check if |maybe_ptr| points to a valid super page or a quarantined
   // card.
   LookupPolicy lookup{snapshot_};
@@ -717,7 +795,7 @@
 // entries in the scanner bitmap correspond to unreachable objects.
 template <typename LookupPolicy>
 ALWAYS_INLINE size_t
-PCScan::PCScanTask::TryMarkObjectInNormalBucketPool(uintptr_t maybe_ptr) const {
+PCScanTask::TryMarkObjectInNormalBucketPool(uintptr_t maybe_ptr) const {
   using AccessType = QuarantineBitmap::AccessType;
   // Check if maybe_ptr points somewhere to the heap.
   auto* scanner_bitmap =
@@ -755,22 +833,17 @@
   return target_slot_span->bucket->slot_size;
 }
 
-void PCScan::PCScanTask::ClearQuarantinedObjectsAndFilterSuperPages() {
+void PCScanTask::ClearQuarantinedObjectsAndPrepareCardTable() {
   using AccessType = QuarantineBitmap::AccessType;
 
-  StatsCollector::ScannerScope clear_scope(stats_,
-                                           StatsCollector::ScannerId::kClear);
-
   const bool giga_cage_enabled = features::IsPartitionAllocGigaCageEnabled();
-  PCScanSnapshot::SuperPages filtered_super_pages;
   for (auto super_page : snapshot_.quarantinable_super_pages()) {
     auto* bitmap = QuarantineBitmapFromPointer(
         QuarantineBitmapType::kScanner, pcscan_epoch_,
         reinterpret_cast<char*>(super_page));
     auto* root = Root::FromSuperPage(reinterpret_cast<char*>(super_page));
-    bool visited = false;
     bitmap->template Iterate<AccessType::kNonAtomic>(
-        [root, giga_cage_enabled, &visited](uintptr_t ptr) {
+        [root, giga_cage_enabled](uintptr_t ptr) {
           auto* object = reinterpret_cast<void*>(ptr);
           auto* slot_span = SlotSpan::FromSlotInnerPtr(object);
           // Use zero as a zapping value to speed up the fast bailout check in
@@ -785,21 +858,13 @@
 #else
           (void)giga_cage_enabled;
 #endif
-          visited = true;
         });
-    if (visited) {
-      // Filter out super pages that don't contain quarantined objects to bail
-      // out earlier in the fast path (and avoid expensive cache-misses while
-      // checking the quarantine bit).
-      filtered_super_pages.insert(super_page);
-    }
   }
-  snapshot_.quarantinable_super_pages() = std::move(filtered_super_pages);
 }
 
 // Class used to perform actual scanning. Dispatches at runtime based on
 // supported SIMD extensions.
-class PCScan::PCScanTask::ScanLoop final {
+class PCScanTask::ScanLoop final {
  public:
   explicit ScanLoop(const PCScanTask& pcscan_task)
       : scan_function_(GetScanFunction()),
@@ -832,16 +897,16 @@
     if (UNLIKELY(!features::IsPartitionAllocGigaCageEnabled())) {
       return &ScanLoop::RunUnvectorizedNoGigaCage;
     }
-    // We define vectorized versions of the scanning loop only for 64bit since
-    // they require support of the 64bit GigaCage, and only for x86 because
-    // a special instruction set is required.
+// We allow vectorization only for 64bit since they require support of the
+// 64bit GigaCage, and only for x86 because a special instruction set is
+// required.
 #if defined(ARCH_CPU_X86_64)
-    base::CPU cpu;
-    if (cpu.has_avx2())
+    const SimdSupport simd = PCScanInternal::Instance().simd_support();
+    if (simd == SimdSupport::kAVX2)
       return &ScanLoop::RunAVX2;
-    if (cpu.has_sse3())
+    if (simd == SimdSupport::kSSE3)
       return &ScanLoop::RunSSE3;
-#endif  // defined(ARCH_CPU_X86_64)
+#endif
     return &ScanLoop::RunUnvectorized;
   }
 
@@ -993,16 +1058,16 @@
 #endif
 };
 
-size_t PCScan::PCScanTask::ScanPartitions() {
-  StatsCollector::ScannerScope scan_scope(stats_,
-                                          StatsCollector::ScannerId::kScan);
+PCScanTask::PCScanTask(PCScan& pcscan)
+    : pcscan_epoch_(pcscan.quarantine_data_.epoch()),
+      stats_(PCScanInternal::Instance().process_name()),
+      pcscan_(pcscan) {}
 
+void PCScanTask::ScanPartitions() {
   const ScanLoop scan_loop(*this);
-
-  size_t new_quarantine_size = 0;
-
   // For scanning large areas, it's worthwhile checking whether the range that
   // is scanned contains quarantined objects.
+  size_t quarantine_size = 0;
   for (auto scan_area : snapshot_.large_scan_areas()) {
     // The bitmap is (a) always guaranteed to exist and (b) the same for all
     // objects in a given slot span.
@@ -1022,25 +1087,23 @@
       uintptr_t* current_slot_end =
           current_slot + (scan_area.slot_size / sizeof(uintptr_t));
       PA_DCHECK(current_slot_end <= scan_area.end);
-      new_quarantine_size += scan_loop.Run(current_slot, current_slot_end);
+      quarantine_size += scan_loop.Run(current_slot, current_slot_end);
     }
   }
+  // Scan areas with regular size slots.
   for (auto scan_area : snapshot_.scan_areas()) {
-    new_quarantine_size += scan_loop.Run(scan_area.begin, scan_area.end);
+    quarantine_size += scan_loop.Run(scan_area.begin, scan_area.end);
   }
-  return new_quarantine_size;
+  stats_.IncreaseSurvivedQuarantineSize(quarantine_size);
 }
 
-size_t PCScan::PCScanTask::SweepQuarantine() {
+void PCScanTask::SweepQuarantine() {
   using AccessType = QuarantineBitmap::AccessType;
 
-  StatsCollector::ScannerScope sweep_scope(stats_,
-                                           StatsCollector::ScannerId::kSweep);
+  const bool giga_cage_enabled = features::IsPartitionAllocGigaCageEnabled();
   size_t swept_bytes = 0;
 
-  const bool giga_cage_enabled = features::IsPartitionAllocGigaCageEnabled();
-
-  for (auto super_page : snapshot_.quarantinable_super_pages()) {
+  for (uintptr_t super_page : snapshot_.quarantinable_super_pages()) {
     auto* bitmap = QuarantineBitmapFromPointer(
         QuarantineBitmapType::kScanner, pcscan_epoch_,
         reinterpret_cast<char*>(super_page));
@@ -1067,46 +1130,54 @@
         });
   }
 
-  return swept_bytes;
+  stats_.IncreaseSweptSize(swept_bytes);
 }
 
-PCScan::PCScanTask::PCScanTask(PCScan& pcscan)
-    : pcscan_epoch_(pcscan.quarantine_data_.epoch()),
-      stats_(PCScanInternal::Instance().process_name()),
-      pcscan_(pcscan) {}
-
-void PCScan::PCScanTask::RunOnce() && {
-  size_t new_quarantine_size = 0;
-  size_t swept_bytes = 0;
-
-  // Take snapshot of partition-alloc heap.
-  snapshot_.Take(pcscan_epoch_);
-
-  {
-    StatsCollector::ScannerScope overall_scope(
-        stats_, StatsCollector::ScannerId::kOverall);
-
-    // Clear all quarantined objects and filter out super pages that
-    // don't contain quarantined objects.
-    ClearQuarantinedObjectsAndFilterSuperPages();
-
-    // Mark and sweep the quarantine list.
-    new_quarantine_size = ScanPartitions();
-    swept_bytes = SweepQuarantine();
-  }
-
+void PCScanTask::FinishScanner() {
   stats_.UpdateHistograms();
-  LogStats(swept_bytes, pcscan_.quarantine_data_.last_size(),
-           new_quarantine_size);
+  LogStats(stats_.swept_size(), pcscan_.quarantine_data_.last_size(),
+           stats_.survived_quarantine_size());
 
   const size_t total_pa_heap_size =
       PCScanInternal::Instance().CalculateTotalHeapSize();
 
-  pcscan_.quarantine_data_.Account(new_quarantine_size);
+  pcscan_.quarantine_data_.Account(stats_.survived_quarantine_size());
   pcscan_.quarantine_data_.GrowLimitIfNeeded(total_pa_heap_size);
 
   // Check that concurrent task can't be scheduled twice.
-  PA_CHECK(pcscan_.in_progress_.exchange(false, std::memory_order_acq_rel));
+  PA_CHECK(pcscan_.state_.exchange(PCScan::State::kNotRunning,
+                                   std::memory_order_acq_rel) ==
+           PCScan::State::kSweepingAndFinishing);
+}
+
+void PCScanTask::RunFromScanner() {
+  {
+    StatsCollector::ScannerScope overall_scope(
+        stats_, StatsCollector::ScannerId::kOverall);
+    // Take snapshot of partition-alloc heap.
+    snapshot_.Take(pcscan_epoch_);
+    {
+      // Clear all quarantined objects and prepare the card table.
+      StatsCollector::ScannerScope clear_scope(
+          stats_, StatsCollector::ScannerId::kClear);
+      ClearQuarantinedObjectsAndPrepareCardTable();
+    }
+    {
+      // Scan heap for dangling references.
+      StatsCollector::ScannerScope scan_scope(stats_,
+                                              StatsCollector::ScannerId::kScan);
+      ScanPartitions();
+    }
+    pcscan_.state_.store(PCScan::State::kSweepingAndFinishing,
+                         std::memory_order_relaxed);
+    {
+      // Sweep unreachable quarantined objects.
+      StatsCollector::ScannerScope sweep_scope(
+          stats_, StatsCollector::ScannerId::kSweep);
+      SweepQuarantine();
+    }
+  }
+  FinishScanner();
 }
 
 class PCScan::PCScanThread final {
@@ -1149,7 +1220,7 @@
         condvar_.wait(lock, [this] { return posted_task_.get(); });
         std::swap(current_task, posted_task_);
       }
-      std::move(*current_task).RunOnce();
+      std::move(*current_task).RunFromScanner();
     }
   }
 
@@ -1188,23 +1259,26 @@
                   [](Root* root) { return root->IsQuarantineEnabled(); }));
 #endif
 
-  if (in_progress_.exchange(true, std::memory_order_acq_rel)) {
+  if (state_.exchange(State::kScheduled, std::memory_order_acq_rel) !=
+      State::kNotRunning) {
     // Bail out if PCScan is already in progress.
     return;
   }
 
   quarantine_data_.ResetAndAdvanceEpoch();
 
-  // Initialize PCScan task.
+  // Create PCScan task.
   auto task = std::make_unique<PCScanTask>(*this);
 
+  state_.store(State::kScanning, std::memory_order_release);
+
   // Post PCScan task.
   if (LIKELY(invocation_mode == InvocationMode::kNonBlocking)) {
     PCScanThread::Instance().PostTask(std::move(task));
   } else {
     PA_DCHECK(InvocationMode::kBlocking == invocation_mode ||
               InvocationMode::kForcedBlocking == invocation_mode);
-    std::move(*task).RunOnce();
+    std::move(*task).RunFromScanner();
   }
 }
 
diff --git a/base/allocator/partition_allocator/pcscan.h b/base/allocator/partition_allocator/pcscan.h
index 816643f..1a4a813 100644
--- a/base/allocator/partition_allocator/pcscan.h
+++ b/base/allocator/partition_allocator/pcscan.h
@@ -26,6 +26,8 @@
 namespace base {
 namespace internal {
 
+class PCScanTask;
+
 [[noreturn]] BASE_EXPORT NOINLINE NOT_TAIL_CALLED void DoubleFreeAttempt();
 
 // PCScan (Probabilistic Conservative Scanning) is the algorithm that eliminates
@@ -74,9 +76,7 @@
   void PerformScanIfNeeded(InvocationMode invocation_mode);
 
   // Checks if there is a PCScan task currently in progress.
-  ALWAYS_INLINE bool IsInProgress() const {
-    return in_progress_.load(std::memory_order_relaxed);
-  }
+  ALWAYS_INLINE bool IsInProgress() const;
 
   // Sets process name (used for histograms). |name| must be a string literal.
   void SetProcessName(const char* name);
@@ -84,8 +84,8 @@
   void ClearRootsForTesting();
 
  private:
-  class PCScanTask;
   class PCScanThread;
+  friend class PCScanTask;
   friend class PCScanTest;
 
   class QuarantineData final {
@@ -117,6 +117,17 @@
     size_t last_size_ = 0;
   };
 
+  enum class State : uint8_t {
+    // PCScan task is not scheduled.
+    kNotRunning,
+    // PCScan task is being started and about to be scheduled.
+    kScheduled,
+    // PCScan task is scheduled and can be scanning (or clearing).
+    kScanning,
+    // PCScan task is sweeping or finalizing.
+    kSweepingAndFinishing
+  };
+
   inline constexpr PCScan();
 
   // Performs scanning unconditionally.
@@ -126,7 +137,7 @@
   static PCScan instance_ PA_CONSTINIT;
 
   QuarantineData quarantine_data_{};
-  std::atomic<bool> in_progress_{false};
+  std::atomic<State> state_{State::kNotRunning};
 };
 
 // To please Chromium's clang plugin.
@@ -140,6 +151,10 @@
 // To please Chromium's clang plugin.
 constexpr PCScan::PCScan() = default;
 
+ALWAYS_INLINE bool PCScan::IsInProgress() const {
+  return state_.load(std::memory_order_relaxed) != State::kNotRunning;
+}
+
 ALWAYS_INLINE void PCScan::MoveToQuarantine(void* ptr, size_t slot_size) {
   auto* quarantine = QuarantineBitmapFromPointer(QuarantineBitmapType::kMutator,
                                                  quarantine_data_.epoch(), ptr);
@@ -151,7 +166,7 @@
   const bool is_limit_reached = quarantine_data_.Account(slot_size);
   if (UNLIKELY(is_limit_reached)) {
     // Perform a quick check if another scan is already in progress.
-    if (in_progress_.load(std::memory_order_relaxed))
+    if (IsInProgress())
       return;
     // Avoid blocking the current thread for regular scans.
     PerformScan(InvocationMode::kNonBlocking);
diff --git a/base/strings/nullable_string16.cc b/base/strings/nullable_string16.cc
deleted file mode 100644
index 118e817..0000000
--- a/base/strings/nullable_string16.cc
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright (c) 2013 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "base/strings/nullable_string16.h"
-
-#include <ostream>
-#include <utility>
-
-namespace base {
-NullableString16::NullableString16() = default;
-NullableString16::NullableString16(const NullableString16& other) = default;
-NullableString16::NullableString16(NullableString16&& other) = default;
-
-NullableString16::NullableString16(const std::u16string& string, bool is_null) {
-  if (!is_null)
-    string_.emplace(string);
-}
-
-NullableString16::NullableString16(Optional<std::u16string> optional_string16)
-    : string_(std::move(optional_string16)) {}
-
-NullableString16::~NullableString16() = default;
-NullableString16& NullableString16::operator=(const NullableString16& other) =
-    default;
-NullableString16& NullableString16::operator=(NullableString16&& other) =
-    default;
-
-std::ostream& operator<<(std::ostream& out, const NullableString16& value) {
-  return value.is_null() ? out << "(null)" : out << value.string();
-}
-
-}  // namespace base
diff --git a/base/strings/nullable_string16.h b/base/strings/nullable_string16.h
deleted file mode 100644
index fdce54b4..0000000
--- a/base/strings/nullable_string16.h
+++ /dev/null
@@ -1,57 +0,0 @@
-// Copyright (c) 2010 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 BASE_STRINGS_NULLABLE_STRING16_H_
-#define BASE_STRINGS_NULLABLE_STRING16_H_
-
-#include <iosfwd>
-#include <string>
-
-#include "base/base_export.h"
-#include "base/optional.h"
-#include "base/strings/string_util.h"
-
-namespace base {
-
-// This class is a simple wrapper for std::u16string which also contains a null
-// state.  This should be used only where the difference between null and
-// empty is meaningful.
-class BASE_EXPORT NullableString16 {
- public:
-  NullableString16();
-  NullableString16(const NullableString16& other);
-  NullableString16(NullableString16&& other);
-  NullableString16(const std::u16string& string, bool is_null);
-  explicit NullableString16(Optional<std::u16string> optional_string16);
-  ~NullableString16();
-
-  NullableString16& operator=(const NullableString16& other);
-  NullableString16& operator=(NullableString16&& other);
-
-  const std::u16string& string() const {
-    return string_ ? *string_ : EmptyString16();
-  }
-  bool is_null() const { return !string_; }
-  const Optional<std::u16string>& as_optional_string16() const {
-    return string_;
-  }
-
- private:
-  Optional<std::u16string> string_;
-};
-
-inline bool operator==(const NullableString16& a, const NullableString16& b) {
-  return a.as_optional_string16() == b.as_optional_string16();
-}
-
-inline bool operator!=(const NullableString16& a, const NullableString16& b) {
-  return !(a == b);
-}
-
-BASE_EXPORT std::ostream& operator<<(std::ostream& out,
-                                     const NullableString16& value);
-
-}  // namespace base
-
-#endif  // BASE_STRINGS_NULLABLE_STRING16_H_
diff --git a/base/strings/nullable_string16_unittest.cc b/base/strings/nullable_string16_unittest.cc
deleted file mode 100644
index 3654e38..0000000
--- a/base/strings/nullable_string16_unittest.cc
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright 2013 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "base/strings/nullable_string16.h"
-#include "base/strings/utf_string_conversions.h"
-#include "testing/gtest/include/gtest/gtest.h"
-
-namespace base {
-
-TEST(NullableString16Test, DefaultConstructor) {
-  NullableString16 s;
-  EXPECT_TRUE(s.is_null());
-  EXPECT_EQ(std::u16string(), s.string());
-}
-
-TEST(NullableString16Test, Equals) {
-  NullableString16 a(ASCIIToUTF16("hello"), false);
-  NullableString16 b(ASCIIToUTF16("hello"), false);
-  EXPECT_EQ(a, b);
-}
-
-TEST(NullableString16Test, NotEquals) {
-  NullableString16 a(ASCIIToUTF16("hello"), false);
-  NullableString16 b(ASCIIToUTF16("world"), false);
-  EXPECT_NE(a, b);
-}
-
-TEST(NullableString16Test, NotEqualsNull) {
-  NullableString16 a(ASCIIToUTF16("hello"), false);
-  NullableString16 b;
-  EXPECT_NE(a, b);
-}
-
-}  // namespace base
diff --git a/base/threading/thread_restrictions.h b/base/threading/thread_restrictions.h
index aa30785..dc5dacc0 100644
--- a/base/threading/thread_restrictions.h
+++ b/base/threading/thread_restrictions.h
@@ -123,6 +123,7 @@
 class RTCVideoEncoder;
 class SourceStream;
 class VideoFrameResourceProvider;
+class WebRtcVideoFrameAdapter;
 class LegacyWebRtcVideoFrameAdapter;
 class WorkerThread;
 namespace scheduler {
@@ -537,6 +538,7 @@
   friend class base::StackSamplingProfiler;
   friend class blink::RTCVideoDecoderAdapter;
   friend class blink::RTCVideoEncoder;
+  friend class blink::WebRtcVideoFrameAdapter;
   friend class blink::LegacyWebRtcVideoFrameAdapter;
   friend class cc::TileTaskManagerImpl;
   friend class content::CategorizedWorkerPool;
diff --git a/cc/base/devtools_instrumentation.cc b/cc/base/devtools_instrumentation.cc
index b65c154..c3cc859 100644
--- a/cc/base/devtools_instrumentation.cc
+++ b/cc/base/devtools_instrumentation.cc
@@ -4,6 +4,8 @@
 
 #include "cc/base/devtools_instrumentation.h"
 
+#include <string>
+
 namespace cc {
 namespace devtools_instrumentation {
 namespace {
@@ -36,6 +38,7 @@
 const char kLayerId[] = "layerId";
 const char kLayerTreeId[] = "layerTreeId";
 const char kPixelRefId[] = "pixelRefId";
+const char kPresentationTimestamp[] = "presentationTimestamp";
 
 const char kImageUploadTask[] = "ImageUploadTask";
 const char kImageDecodeTask[] = "ImageDecodeTask";
diff --git a/cc/base/devtools_instrumentation.h b/cc/base/devtools_instrumentation.h
index 02f0254..b27c77c 100644
--- a/cc/base/devtools_instrumentation.h
+++ b/cc/base/devtools_instrumentation.h
@@ -8,6 +8,7 @@
 #include <stdint.h>
 
 #include <memory>
+#include <utility>
 
 #include "base/metrics/histogram_functions.h"
 #include "base/metrics/histogram_macros.h"
@@ -32,6 +33,7 @@
 CC_BASE_EXPORT extern const char kLayerId[];
 CC_BASE_EXPORT extern const char kLayerTreeId[];
 CC_BASE_EXPORT extern const char kPixelRefId[];
+CC_BASE_EXPORT extern const char kPresentationTimestamp[];
 
 CC_BASE_EXPORT extern const char kImageDecodeTask[];
 CC_BASE_EXPORT extern const char kBeginFrame[];
@@ -181,10 +183,31 @@
                        internal::kLayerTreeId, layer_tree_host_id);
 }
 
-inline void CC_BASE_EXPORT DidDrawFrame(int layer_tree_host_id) {
-  TRACE_EVENT_INSTANT1(internal::CategoryName::kTimelineFrame,
-                       internal::kDrawFrame, TRACE_EVENT_SCOPE_THREAD,
-                       internal::kLayerTreeId, layer_tree_host_id);
+constexpr uint64_t GetUniqueIDFromLayerTreeHostIdAndFrameToken(
+    int layer_tree_host_id,
+    uint32_t frame_token) {
+  return static_cast<uint64_t>(layer_tree_host_id) << 32 |
+         static_cast<uint64_t>(frame_token);
+}
+
+inline void CC_BASE_EXPORT DidDrawFrame(int layer_tree_host_id,
+                                        uint32_t frame_token) {
+  TRACE_EVENT_NESTABLE_ASYNC_BEGIN1(internal::CategoryName::kTimelineFrame,
+                                    internal::kDrawFrame,
+                                    GetUniqueIDFromLayerTreeHostIdAndFrameToken(
+                                        layer_tree_host_id, frame_token),
+                                    internal::kLayerTreeId, layer_tree_host_id);
+}
+
+inline void CC_BASE_EXPORT
+DidPresentFrame(int layer_tree_host_id,
+                uint32_t frame_token,
+                base::TimeTicks presentation_timestamp) {
+  TRACE_EVENT_NESTABLE_ASYNC_END1(
+      internal::CategoryName::kTimelineFrame, internal::kDrawFrame,
+      GetUniqueIDFromLayerTreeHostIdAndFrameToken(layer_tree_host_id,
+                                                  frame_token),
+      internal::kPresentationTimestamp, presentation_timestamp);
 }
 
 inline void CC_BASE_EXPORT DidRequestMainThreadFrame(int layer_tree_host_id) {
diff --git a/cc/metrics/compositor_frame_reporter.cc b/cc/metrics/compositor_frame_reporter.cc
index fcad7a8c..93be01c4 100644
--- a/cc/metrics/compositor_frame_reporter.cc
+++ b/cc/metrics/compositor_frame_reporter.cc
@@ -46,6 +46,12 @@
 constexpr int kFrameSequenceTrackerTypeCount =
     static_cast<int>(FrameSequenceTrackerType::kMaxType) + 1;
 
+// Maximum number of partial update dependents a reporter can own. When a
+// reporter with too many dependents is terminated, it will terminate all its
+// dependents which will block the pipeline for a long time. Too many dependents
+// also means too much memory usage.
+constexpr size_t kMaxOwnedPartialUpdateDependents = 300u;
+
 // Names for the viz breakdowns that are shown in trace as substages under
 // PipelineReporter -> SubmitCompositorFrameToPresentationCompositorFrame or
 // EventLatency -> SubmitCompositorFrameToPresentationCompositorFrame.
@@ -576,7 +582,7 @@
 
   // Set up the new reporter so that it depends on |this| for partial update
   // information.
-  new_reporter->SetPartialUpdateDecider(weak_factory_.GetWeakPtr());
+  new_reporter->SetPartialUpdateDecider(this);
 
   return new_reporter;
 }
@@ -1255,10 +1261,6 @@
   return false;
 }
 
-base::WeakPtr<CompositorFrameReporter> CompositorFrameReporter::GetWeakPtr() {
-  return weak_factory_.GetWeakPtr();
-}
-
 void CompositorFrameReporter::AdoptReporter(
     std::unique_ptr<CompositorFrameReporter> reporter) {
   // If |this| reporter is dependent on another reporter to decide about partial
@@ -1270,32 +1272,36 @@
 }
 
 void CompositorFrameReporter::SetPartialUpdateDecider(
-    base::WeakPtr<CompositorFrameReporter> decider) {
+    CompositorFrameReporter* decider) {
   DCHECK(decider);
-  has_partial_update_ = true;
-  partial_update_decider_ = decider;
-  decider->partial_update_dependents_.push(GetWeakPtr());
   DCHECK(partial_update_dependents_.empty());
+  has_partial_update_ = true;
+  partial_update_decider_ = decider->GetWeakPtr();
+  decider->partial_update_dependents_.push(GetWeakPtr());
 }
 
 void CompositorFrameReporter::DiscardOldPartialUpdateReporters() {
   DCHECK_LE(owned_partial_update_dependents_.size(),
             partial_update_dependents_.size());
-  while (owned_partial_update_dependents_.size() > 300u) {
+  // Remove old owned partial update dependents if there are too many.
+  while (owned_partial_update_dependents_.size() >
+         kMaxOwnedPartialUpdateDependents) {
     auto& dependent = owned_partial_update_dependents_.front();
     dependent->set_has_partial_update(false);
-    partial_update_dependents_.pop();
     owned_partial_update_dependents_.pop();
     discarded_partial_update_dependents_count_++;
   }
+
+  // Remove dependent reporters from the front of `partial_update_dependents_`
+  // queue if they are already destroyed.
+  while (!partial_update_dependents_.empty() &&
+         !partial_update_dependents_.front()) {
+    partial_update_dependents_.pop();
+  }
 }
 
-bool CompositorFrameReporter::MightHavePartialUpdate() const {
-  return !!partial_update_decider_;
-}
-
-size_t CompositorFrameReporter::GetPartialUpdateDependentsCount() const {
-  return partial_update_dependents_.size();
+base::WeakPtr<CompositorFrameReporter> CompositorFrameReporter::GetWeakPtr() {
+  return weak_factory_.GetWeakPtr();
 }
 
 }  // namespace cc
diff --git a/cc/metrics/compositor_frame_reporter.h b/cc/metrics/compositor_frame_reporter.h
index 0e9f000..67c2614 100644
--- a/cc/metrics/compositor_frame_reporter.h
+++ b/cc/metrics/compositor_frame_reporter.h
@@ -256,7 +256,7 @@
   void SetVizBreakdown(const viz::FrameTimingDetails& viz_breakdown);
   void SetEventsMetrics(EventMetrics::List events_metrics);
 
-  int StageHistorySizeForTesting() { return stage_history_.size(); }
+  int stage_history_size_for_testing() const { return stage_history_.size(); }
 
   void OnFinishImplFrame(base::TimeTicks timestamp);
   void OnAbortBeginMainFrame(base::TimeTicks timestamp);
@@ -288,10 +288,15 @@
     tick_clock_ = tick_clock;
   }
 
-  void SetPartialUpdateDecider(base::WeakPtr<CompositorFrameReporter> decider);
+  void SetPartialUpdateDecider(CompositorFrameReporter* decider);
 
-  bool MightHavePartialUpdate() const;
-  size_t GetPartialUpdateDependentsCount() const;
+  size_t partial_update_dependents_size_for_testing() const {
+    return partial_update_dependents_.size();
+  }
+
+  size_t owned_partial_update_dependents_size_for_testing() const {
+    return owned_partial_update_dependents_.size();
+  }
 
   const viz::BeginFrameId& frame_id() const { return args_.frame_id; }
 
@@ -303,12 +308,10 @@
   // If this is a cloned reporter, then this returns a weak-ptr to the original
   // reporter this was cloned from (using |CopyReporterAtBeginImplStage()|).
 
-  base::WeakPtr<CompositorFrameReporter> partial_update_decider() {
-    return partial_update_decider_;
+  CompositorFrameReporter* partial_update_decider() const {
+    return partial_update_decider_.get();
   }
 
-  base::WeakPtr<CompositorFrameReporter> GetWeakPtr();
-
  protected:
   void set_has_partial_update(bool has_partial_update) {
     has_partial_update_ = has_partial_update;
@@ -354,6 +357,8 @@
 
   bool IsDroppedFrameAffectingSmoothness() const;
 
+  base::WeakPtr<CompositorFrameReporter> GetWeakPtr();
+
   const bool should_report_metrics_;
   const viz::BeginFrameArgs args_;
 
diff --git a/cc/metrics/compositor_frame_reporter_unittest.cc b/cc/metrics/compositor_frame_reporter_unittest.cc
index e7a38ba..a7b9cdc 100644
--- a/cc/metrics/compositor_frame_reporter_unittest.cc
+++ b/cc/metrics/compositor_frame_reporter_unittest.cc
@@ -30,16 +30,7 @@
 
 class CompositorFrameReporterTest : public testing::Test {
  public:
-  CompositorFrameReporterTest()
-      : pipeline_reporter_(std::make_unique<CompositorFrameReporter>(
-            CompositorFrameReporter::ActiveTrackers(),
-            viz::BeginFrameArgs(),
-            nullptr,
-            /*should_report_metrics=*/true,
-            CompositorFrameReporter::SmoothThread::kSmoothBoth,
-            /*layer_tree_host_id=*/1,
-            &dropped_frame_counter_)) {
-    pipeline_reporter_->set_tick_clock(&test_tick_clock_);
+  CompositorFrameReporterTest() : pipeline_reporter_(CreatePipelineReporter()) {
     AdvanceNowByMs(1);
     dropped_frame_counter_.set_total_counter(&total_frame_counter_);
   }
@@ -115,6 +106,17 @@
     return event_times;
   }
 
+  std::unique_ptr<CompositorFrameReporter> CreatePipelineReporter() {
+    auto reporter = std::make_unique<CompositorFrameReporter>(
+        CompositorFrameReporter::ActiveTrackers(), viz::BeginFrameArgs(),
+        /*latency_ukm_reporter=*/nullptr,
+        /*should_report_metrics=*/true,
+        CompositorFrameReporter::SmoothThread::kSmoothBoth,
+        /*layer_tree_host_id=*/1, &dropped_frame_counter_);
+    reporter->set_tick_clock(&test_tick_clock_);
+    return reporter;
+  }
+
   // This should be defined before |pipeline_reporter_| so it is created before
   // and destroyed after that.
   base::SimpleTestTickClock test_tick_clock_;
@@ -130,30 +132,30 @@
   pipeline_reporter_->StartStage(
       CompositorFrameReporter::StageType::kBeginImplFrameToSendBeginMainFrame,
       Now());
-  EXPECT_EQ(0, pipeline_reporter_->StageHistorySizeForTesting());
+  EXPECT_EQ(0, pipeline_reporter_->stage_history_size_for_testing());
 
   AdvanceNowByMs(3);
   pipeline_reporter_->StartStage(
       CompositorFrameReporter::StageType::kSendBeginMainFrameToCommit, Now());
-  EXPECT_EQ(1, pipeline_reporter_->StageHistorySizeForTesting());
+  EXPECT_EQ(1, pipeline_reporter_->stage_history_size_for_testing());
 
   AdvanceNowByMs(3);
   pipeline_reporter_->StartStage(
       CompositorFrameReporter::StageType::kEndActivateToSubmitCompositorFrame,
       Now());
-  EXPECT_EQ(2, pipeline_reporter_->StageHistorySizeForTesting());
+  EXPECT_EQ(2, pipeline_reporter_->stage_history_size_for_testing());
 
   AdvanceNowByMs(3);
   pipeline_reporter_->StartStage(
       CompositorFrameReporter::StageType::
           kSubmitCompositorFrameToPresentationCompositorFrame,
       Now());
-  EXPECT_EQ(3, pipeline_reporter_->StageHistorySizeForTesting());
+  EXPECT_EQ(3, pipeline_reporter_->stage_history_size_for_testing());
 
   AdvanceNowByMs(3);
   pipeline_reporter_->TerminateFrame(
       CompositorFrameReporter::FrameTerminationStatus::kPresentedFrame, Now());
-  EXPECT_EQ(4, pipeline_reporter_->StageHistorySizeForTesting());
+  EXPECT_EQ(4, pipeline_reporter_->stage_history_size_for_testing());
 
   pipeline_reporter_ = nullptr;
   histogram_tester.ExpectTotalCount(
@@ -175,18 +177,18 @@
 
   pipeline_reporter_->StartStage(CompositorFrameReporter::StageType::kCommit,
                                  Now());
-  EXPECT_EQ(0, pipeline_reporter_->StageHistorySizeForTesting());
+  EXPECT_EQ(0, pipeline_reporter_->stage_history_size_for_testing());
 
   AdvanceNowByMs(3);
   pipeline_reporter_->StartStage(
       CompositorFrameReporter::StageType::kEndCommitToActivation, Now());
-  EXPECT_EQ(1, pipeline_reporter_->StageHistorySizeForTesting());
+  EXPECT_EQ(1, pipeline_reporter_->stage_history_size_for_testing());
 
   AdvanceNowByMs(2);
   pipeline_reporter_->TerminateFrame(
       CompositorFrameReporter::FrameTerminationStatus::kReplacedByNewReporter,
       Now());
-  EXPECT_EQ(2, pipeline_reporter_->StageHistorySizeForTesting());
+  EXPECT_EQ(2, pipeline_reporter_->stage_history_size_for_testing());
 
   pipeline_reporter_ = nullptr;
   histogram_tester.ExpectTotalCount("CompositorLatency.Commit", 0);
@@ -199,18 +201,18 @@
 
   pipeline_reporter_->StartStage(
       CompositorFrameReporter::StageType::kActivation, Now());
-  EXPECT_EQ(0, pipeline_reporter_->StageHistorySizeForTesting());
+  EXPECT_EQ(0, pipeline_reporter_->stage_history_size_for_testing());
 
   AdvanceNowByMs(3);
   pipeline_reporter_->StartStage(
       CompositorFrameReporter::StageType::kEndActivateToSubmitCompositorFrame,
       Now());
-  EXPECT_EQ(1, pipeline_reporter_->StageHistorySizeForTesting());
+  EXPECT_EQ(1, pipeline_reporter_->stage_history_size_for_testing());
 
   AdvanceNowByMs(2);
   pipeline_reporter_->TerminateFrame(
       CompositorFrameReporter::FrameTerminationStatus::kPresentedFrame, Now());
-  EXPECT_EQ(2, pipeline_reporter_->StageHistorySizeForTesting());
+  EXPECT_EQ(2, pipeline_reporter_->stage_history_size_for_testing());
 
   pipeline_reporter_ = nullptr;
   histogram_tester.ExpectTotalCount("CompositorLatency.Activation", 1);
@@ -235,18 +237,18 @@
 
   pipeline_reporter_->StartStage(
       CompositorFrameReporter::StageType::kSendBeginMainFrameToCommit, Now());
-  EXPECT_EQ(0, pipeline_reporter_->StageHistorySizeForTesting());
+  EXPECT_EQ(0, pipeline_reporter_->stage_history_size_for_testing());
 
   AdvanceNowByMs(3);
   pipeline_reporter_->StartStage(CompositorFrameReporter::StageType::kCommit,
                                  Now());
-  EXPECT_EQ(1, pipeline_reporter_->StageHistorySizeForTesting());
+  EXPECT_EQ(1, pipeline_reporter_->stage_history_size_for_testing());
 
   AdvanceNowByMs(2);
   pipeline_reporter_->TerminateFrame(
       CompositorFrameReporter::FrameTerminationStatus::kDidNotPresentFrame,
       Now());
-  EXPECT_EQ(2, pipeline_reporter_->StageHistorySizeForTesting());
+  EXPECT_EQ(2, pipeline_reporter_->stage_history_size_for_testing());
 
   pipeline_reporter_ = nullptr;
   histogram_tester.ExpectTotalCount(
@@ -480,5 +482,122 @@
               IsEmpty());
 }
 
+// Verifies that partial update dependent queues are working as expected when
+// they reach their maximum capacity.
+TEST_F(CompositorFrameReporterTest, PartialUpdateDependentQueues) {
+  // This constant should match the constant with the same name in
+  // compositor_frame_reporter.cc.
+  const size_t kMaxOwnedPartialUpdateDependents = 300u;
+
+  // The first three dependent reporters for the front of the queue.
+  std::unique_ptr<CompositorFrameReporter> deps[] = {
+      CreatePipelineReporter(),
+      CreatePipelineReporter(),
+      CreatePipelineReporter(),
+  };
+
+  // Set `deps[0]` as a dependent of the main reporter and adopt it at the same
+  // time. This should enqueue it in both non-owned and owned dependents queues.
+  deps[0]->SetPartialUpdateDecider(pipeline_reporter_.get());
+  pipeline_reporter_->AdoptReporter(std::move(deps[0]));
+  DCHECK_EQ(1u,
+            pipeline_reporter_->partial_update_dependents_size_for_testing());
+  DCHECK_EQ(
+      1u,
+      pipeline_reporter_->owned_partial_update_dependents_size_for_testing());
+
+  // Set `deps[1]` as a dependent of the main reporter, but don't adopt it yet.
+  // This should enqueue it in non-owned dependents queue only.
+  deps[1]->SetPartialUpdateDecider(pipeline_reporter_.get());
+  DCHECK_EQ(2u,
+            pipeline_reporter_->partial_update_dependents_size_for_testing());
+  DCHECK_EQ(
+      1u,
+      pipeline_reporter_->owned_partial_update_dependents_size_for_testing());
+
+  // Set `deps[2]` as a dependent of the main reporter and adopt it at the same
+  // time. This should enqueue it in both non-owned and owned dependents queues.
+  deps[2]->SetPartialUpdateDecider(pipeline_reporter_.get());
+  pipeline_reporter_->AdoptReporter(std::move(deps[2]));
+  DCHECK_EQ(3u,
+            pipeline_reporter_->partial_update_dependents_size_for_testing());
+  DCHECK_EQ(
+      2u,
+      pipeline_reporter_->owned_partial_update_dependents_size_for_testing());
+
+  // Now adopt `deps[1]` to enqueue it in the owned dependents queue.
+  pipeline_reporter_->AdoptReporter(std::move(deps[1]));
+  DCHECK_EQ(3u,
+            pipeline_reporter_->partial_update_dependents_size_for_testing());
+  DCHECK_EQ(
+      3u,
+      pipeline_reporter_->owned_partial_update_dependents_size_for_testing());
+
+  // Fill the queues with more dependent reporters until the capacity is
+  // reached. After this, the queues should look like this (assuming n equals
+  // `kMaxOwnedPartialUpdateDependents`):
+  //   Partial Update Dependents:       [0, 1, 2, 3, 4, ..., n-1]
+  //   Owned Partial Update Dependents: [0, 2, 1, 3, 4, ..., n-1]
+  while (
+      pipeline_reporter_->owned_partial_update_dependents_size_for_testing() <
+      kMaxOwnedPartialUpdateDependents) {
+    std::unique_ptr<CompositorFrameReporter> dependent =
+        CreatePipelineReporter();
+    dependent->SetPartialUpdateDecider(pipeline_reporter_.get());
+    pipeline_reporter_->AdoptReporter(std::move(dependent));
+  }
+  DCHECK_EQ(kMaxOwnedPartialUpdateDependents,
+            pipeline_reporter_->partial_update_dependents_size_for_testing());
+  DCHECK_EQ(
+      kMaxOwnedPartialUpdateDependents,
+      pipeline_reporter_->owned_partial_update_dependents_size_for_testing());
+
+  // Enqueue a new dependent reporter. This should pop `deps[0]` from the front
+  // of the owned dependents queue and destroy it. Since the same one is in
+  // front of the non-owned dependents queue, it will be popped out of that
+  // queue, too. The queues will look like this:
+  //   Partial Update Dependents:       [1, 2, 3, 4, ..., n]
+  //   Owned Partial Update Dependents: [2, 1, 3, 4, ..., n]
+  auto new_dep = CreatePipelineReporter();
+  new_dep->SetPartialUpdateDecider(pipeline_reporter_.get());
+  pipeline_reporter_->AdoptReporter(std::move(new_dep));
+  DCHECK_EQ(kMaxOwnedPartialUpdateDependents,
+            pipeline_reporter_->partial_update_dependents_size_for_testing());
+  DCHECK_EQ(
+      kMaxOwnedPartialUpdateDependents,
+      pipeline_reporter_->owned_partial_update_dependents_size_for_testing());
+
+  // Enqueue another new dependent reporter. This should pop `deps[2]` from the
+  // front of the owned dependents queue and destroy it. Since another reporter
+  // is in front of the non-owned dependents queue it won't be popped out of
+  // that queue. The queues will look like this:
+  //   Partial Update Dependents:       [2, 3, 4, ..., n+1]
+  //   Owned Partial Update Dependents: [2, nullptr, 3, 4, ..., n+1]
+  new_dep = CreatePipelineReporter();
+  new_dep->SetPartialUpdateDecider(pipeline_reporter_.get());
+  pipeline_reporter_->AdoptReporter(std::move(new_dep));
+  DCHECK_EQ(kMaxOwnedPartialUpdateDependents + 1,
+            pipeline_reporter_->partial_update_dependents_size_for_testing());
+  DCHECK_EQ(
+      kMaxOwnedPartialUpdateDependents,
+      pipeline_reporter_->owned_partial_update_dependents_size_for_testing());
+
+  // Enqueue yet another new dependent reporter. This should pop `deps[1]` from
+  // the front of the owned dependents queue and destroy it. Since the same one
+  // is in front of the non-owned dependents queue followed by `deps[2]` which
+  // was destroyed in the previous step, they will be popped out of that queue,
+  // too. The queues will look like this:
+  //   Partial Update Dependents:       [3, 4, ..., n+2]
+  //   Owned Partial Update Dependents: [3, 4, ..., n+2]
+  new_dep = CreatePipelineReporter();
+  new_dep->SetPartialUpdateDecider(pipeline_reporter_.get());
+  pipeline_reporter_->AdoptReporter(std::move(new_dep));
+  DCHECK_EQ(kMaxOwnedPartialUpdateDependents,
+            pipeline_reporter_->partial_update_dependents_size_for_testing());
+  DCHECK_EQ(
+      kMaxOwnedPartialUpdateDependents,
+      pipeline_reporter_->owned_partial_update_dependents_size_for_testing());
+}
+
 }  // namespace
 }  // namespace cc
diff --git a/cc/metrics/compositor_frame_reporting_controller.cc b/cc/metrics/compositor_frame_reporting_controller.cc
index dec573d1..d6103ee 100644
--- a/cc/metrics/compositor_frame_reporting_controller.cc
+++ b/cc/metrics/compositor_frame_reporting_controller.cc
@@ -222,8 +222,8 @@
     AdvanceReporterStage(PipelineStage::kBeginImplFrame,
                          PipelineStage::kActivate);
     impl_reporter = std::move(reporters_[PipelineStage::kActivate]);
-    auto partial_update_decider =
-        HasOutstandingUpdatesFromMain(current_frame_id);
+    CompositorFrameReporter* partial_update_decider =
+        GetOutstandingUpdatesFromMain(current_frame_id);
     if (partial_update_decider)
       impl_reporter->SetPartialUpdateDecider(partial_update_decider);
   } else if (CanSubmitMainFrame(current_frame_id)) {
@@ -333,8 +333,8 @@
   } else {
     // The stage_reporter in this case was waiting for main, so needs to
     // be adopted by the reporter which is waiting on Main thread's work
-    auto partial_update_decider =
-        HasOutstandingUpdatesFromMain(stage_reporter->frame_id());
+    CompositorFrameReporter* partial_update_decider =
+        GetOutstandingUpdatesFromMain(stage_reporter->frame_id());
     if (partial_update_decider) {
       stage_reporter->SetPartialUpdateDecider(partial_update_decider);
       stage_reporter->OnDidNotProduceFrame(FrameSkippedReason::kWaitingOnMain);
@@ -383,11 +383,11 @@
     // the original reporter, so that the cloned reporter stays alive until the
     // original reporter is terminated, and the cloned reporter's 'partial
     // update' flag can be unset if necessary.
-    if (reporter->MightHavePartialUpdate()) {
-      auto orig_reporter = reporter->partial_update_decider();
-      if (orig_reporter)
-        orig_reporter->AdoptReporter(std::move(reporter));
+    if (CompositorFrameReporter* orig_reporter =
+            reporter->partial_update_decider()) {
+      orig_reporter->AdoptReporter(std::move(reporter));
     }
+
     submitted_compositor_frames_.erase(submitted_frame);
   }
 }
@@ -532,8 +532,8 @@
   return smooth_thread_history_.lower_bound(timestamp)->second;
 }
 
-base::WeakPtr<CompositorFrameReporter>
-CompositorFrameReportingController::HasOutstandingUpdatesFromMain(
+CompositorFrameReporter*
+CompositorFrameReportingController::GetOutstandingUpdatesFromMain(
     const viz::BeginFrameId& id) const {
   // Any unterminated reporter in the 'main frame', or 'commit' stages, then
   // that indicates some pending updates from the main thread.
@@ -541,17 +541,17 @@
     const auto& reporter = reporters_[PipelineStage::kBeginMainFrame];
     if (reporter && reporter->frame_id() < id &&
         !reporter->did_abort_main_frame()) {
-      return reporter->GetWeakPtr();
+      return reporter.get();
     }
   }
   {
     const auto& reporter = reporters_[PipelineStage::kCommit];
     if (reporter && reporter->frame_id() < id) {
       DCHECK(!reporter->did_abort_main_frame());
-      return reporter->GetWeakPtr();
+      return reporter.get();
     }
   }
-  return {};
+  return nullptr;
 }
 
 void CompositorFrameReportingController::CreateReportersForDroppedFrames(
diff --git a/cc/metrics/compositor_frame_reporting_controller.h b/cc/metrics/compositor_frame_reporting_controller.h
index 9054c0a..46eb58c 100644
--- a/cc/metrics/compositor_frame_reporting_controller.h
+++ b/cc/metrics/compositor_frame_reporting_controller.h
@@ -126,9 +126,9 @@
       base::TimeTicks timestamp) const;
 
   // Checks whether there are reporters containing updates from the main
-  // thread, and returns a weak-ptr to that reporter (if any). Otherwise returns
-  // null.
-  base::WeakPtr<CompositorFrameReporter> HasOutstandingUpdatesFromMain(
+  // thread, and returns a pointer to that reporter (if any). Otherwise returns
+  // nullptr.
+  CompositorFrameReporter* GetOutstandingUpdatesFromMain(
       const viz::BeginFrameId& id) const;
 
   // If the display-compositor skips over some frames (e.g. when the gpu is
diff --git a/cc/metrics/compositor_frame_reporting_controller_unittest.cc b/cc/metrics/compositor_frame_reporting_controller_unittest.cc
index 8f13ac70..c2d5297e 100644
--- a/cc/metrics/compositor_frame_reporting_controller_unittest.cc
+++ b/cc/metrics/compositor_frame_reporting_controller_unittest.cc
@@ -66,7 +66,8 @@
     };
     for (auto stage : kStages) {
       auto& reporter = reporters()[stage];
-      if (reporter && reporter->GetPartialUpdateDependentsCount() > 0) {
+      if (reporter &&
+          reporter->partial_update_dependents_size_for_testing() > 0) {
         ++count;
       }
     }
@@ -84,7 +85,7 @@
     for (auto stage : kStages) {
       auto& reporter = reporters()[stage];
       if (reporter)
-        count += reporter->GetPartialUpdateDependentsCount();
+        count += reporter->partial_update_dependents_size_for_testing();
     }
     return count;
   }
diff --git a/cc/trees/layer_tree_host_impl.cc b/cc/trees/layer_tree_host_impl.cc
index 6423a98..b184cb2 100644
--- a/cc/trees/layer_tree_host_impl.cc
+++ b/cc/trees/layer_tree_host_impl.cc
@@ -2035,6 +2035,8 @@
 void LayerTreeHostImpl::DidPresentCompositorFrame(
     uint32_t frame_token,
     const viz::FrameTimingDetails& details) {
+  devtools_instrumentation::DidPresentFrame(
+      id_, frame_token, details.presentation_feedback.timestamp);
   PresentationTimeCallbackBuffer::PendingCallbacks activated_callbacks =
       presentation_time_callbacks_.PopPendingCallbacks(frame_token);
 
@@ -2425,7 +2427,7 @@
   active_tree_->ResetAllChangeTracking();
 
   active_tree_->set_has_ever_been_drawn(true);
-  devtools_instrumentation::DidDrawFrame(id_);
+  devtools_instrumentation::DidDrawFrame(id_, frame_token);
   benchmark_instrumentation::IssueImplThreadRenderingStatsEvent(
       rendering_stats_instrumentation_->TakeImplThreadRenderingStats());
 
diff --git a/chrome/android/BUILD.gn b/chrome/android/BUILD.gn
index ae5d38a2..b6dd28cf 100644
--- a/chrome/android/BUILD.gn
+++ b/chrome/android/BUILD.gn
@@ -1243,7 +1243,7 @@
     "//components/media_router/browser/android:java",
     "//components/media_router/browser/android:test_support_java",
     "//components/messages/android:java",
-    "//components/messages/android:javatests",
+    "//components/messages/android/internal:javatests",
     "//components/metrics:metrics_java",
     "//components/minidump_uploader:minidump_uploader_java",
     "//components/minidump_uploader:minidump_uploader_javatests",
diff --git a/chrome/android/chrome_java_resources.gni b/chrome/android/chrome_java_resources.gni
index 0830596..a62e8cc 100644
--- a/chrome/android/chrome_java_resources.gni
+++ b/chrome/android/chrome_java_resources.gni
@@ -703,7 +703,6 @@
   "java/res/layout/bookmark_widget.xml",
   "java/res/layout/bookmark_widget_icons_only.xml",
   "java/res/layout/bookmark_widget_item.xml",
-  "java/res/layout/chip_view_menu_item.xml",
   "java/res/layout/clear_browsing_data_button.xml",
   "java/res/layout/clear_browsing_data_tabs.xml",
   "java/res/layout/clear_browsing_important_dialog_listview.xml",
diff --git a/chrome/android/chrome_java_sources.gni b/chrome/android/chrome_java_sources.gni
index 2d0ead4..0701085 100644
--- a/chrome/android/chrome_java_sources.gni
+++ b/chrome/android/chrome_java_sources.gni
@@ -68,7 +68,6 @@
   "java/src/org/chromium/chrome/browser/announcement/AnnouncementNotificationManager.java",
   "java/src/org/chromium/chrome/browser/app/ChromeActivity.java",
   "java/src/org/chromium/chrome/browser/app/appmenu/AppMenuPropertiesDelegateImpl.java",
-  "java/src/org/chromium/chrome/browser/app/appmenu/ChipViewMenuItemViewBinder.java",
   "java/src/org/chromium/chrome/browser/app/appmenu/DividerLineMenuItemViewBinder.java",
   "java/src/org/chromium/chrome/browser/app/appmenu/IncognitoMenuItemViewBinder.java",
   "java/src/org/chromium/chrome/browser/app/appmenu/ManagedByMenuItemViewBinder.java",
diff --git a/chrome/android/chrome_test_java_sources.gni b/chrome/android/chrome_test_java_sources.gni
index daebb01..58a93508 100644
--- a/chrome/android/chrome_test_java_sources.gni
+++ b/chrome/android/chrome_test_java_sources.gni
@@ -593,6 +593,7 @@
   "javatests/src/org/chromium/chrome/browser/toolbar/ToolbarSecurityIconTest.java",
   "javatests/src/org/chromium/chrome/browser/toolbar/ToolbarTest.java",
   "javatests/src/org/chromium/chrome/browser/toolbar/VoiceToolbarButtonControllerTest.java",
+  "javatests/src/org/chromium/chrome/browser/toolbar/adaptive/OptionalNewTabButtonControllerTabletTest.java",
   "javatests/src/org/chromium/chrome/browser/toolbar/adaptive/OptionalNewTabButtonControllerTest.java",
   "javatests/src/org/chromium/chrome/browser/toolbar/top/AdaptiveToolbarTest.java",
   "javatests/src/org/chromium/chrome/browser/toolbar/top/BrandColorTest.java",
diff --git a/chrome/android/features/start_surface/internal/java/src/org/chromium/chrome/features/start_surface/ExploreSurfaceCoordinator.java b/chrome/android/features/start_surface/internal/java/src/org/chromium/chrome/features/start_surface/ExploreSurfaceCoordinator.java
index 41dd9e1..10558b8 100644
--- a/chrome/android/features/start_surface/internal/java/src/org/chromium/chrome/features/start_surface/ExploreSurfaceCoordinator.java
+++ b/chrome/android/features/start_surface/internal/java/src/org/chromium/chrome/features/start_surface/ExploreSurfaceCoordinator.java
@@ -12,7 +12,6 @@
 import org.chromium.base.supplier.Supplier;
 import org.chromium.chrome.browser.app.ChromeActivity;
 import org.chromium.chrome.browser.feed.FeedSurfaceCoordinator;
-import org.chromium.chrome.browser.feed.FeedV1ActionOptions;
 import org.chromium.chrome.browser.feed.StreamLifecycleManager;
 import org.chromium.chrome.browser.feed.shared.FeedSurfaceDelegate;
 import org.chromium.chrome.browser.feed.shared.stream.Stream;
@@ -111,17 +110,10 @@
             }
         }
 
-        FeedV1ActionOptions feedActionOptions = new FeedV1ActionOptions();
-        feedActionOptions.inhibitDownload = true;
-        feedActionOptions.inhibitOpenInIncognito = true;
-        feedActionOptions.inhibitOpenInNewTab = true;
-        feedActionOptions.inhibitLearnMore = true;
-
         FeedSurfaceCoordinator feedSurfaceCoordinator = new FeedSurfaceCoordinator(mActivity,
-                mActivity.getSnackbarManager(), mActivity.getTabModelSelector(),
-                mActivity.getWindowAndroid(), null, null, sectionHeaderView, feedActionOptions,
-                isInNightMode, this, mExploreSurfaceNavigationDelegate, profile, isPlaceholderShown,
-                bottomSheetController, mActivity.getShareDelegateSupplier(),
+                mActivity.getSnackbarManager(), mActivity.getWindowAndroid(), null, null,
+                sectionHeaderView, isInNightMode, this, mExploreSurfaceNavigationDelegate, profile,
+                isPlaceholderShown, bottomSheetController, mActivity.getShareDelegateSupplier(),
                 scrollableContainerDelegate);
         feedSurfaceCoordinator.getView().setId(R.id.start_surface_explore_view);
         return feedSurfaceCoordinator;
diff --git a/chrome/android/features/start_surface/internal/java/src/org/chromium/chrome/features/start_surface/FeedLoadingLayout.java b/chrome/android/features/start_surface/internal/java/src/org/chromium/chrome/features/start_surface/FeedLoadingLayout.java
index ce823bb..4e73eb1 100644
--- a/chrome/android/features/start_surface/internal/java/src/org/chromium/chrome/features/start_surface/FeedLoadingLayout.java
+++ b/chrome/android/features/start_surface/internal/java/src/org/chromium/chrome/features/start_surface/FeedLoadingLayout.java
@@ -18,7 +18,6 @@
 import android.widget.LinearLayout;
 
 import org.chromium.chrome.browser.feed.FeedSurfaceCoordinator;
-import org.chromium.chrome.browser.feed.shared.FeedFeatures;
 import org.chromium.chrome.start_surface.R;
 import org.chromium.components.browser_ui.widget.displaystyle.UiConfig;
 import org.chromium.components.browser_ui.widget.displaystyle.ViewResizer;
@@ -73,21 +72,10 @@
     private void setHeader() {
         LinearLayout headerView = findViewById(R.id.feed_placeholder_header);
         ViewGroup.LayoutParams lp = headerView.getLayoutParams();
-        // FeedFeatures.cachedIsReportingUserActions uses CachedFeatureFlags for checking feature
-        // states, but these same features are checked directly with ChromeFeatureList in other
-        // places. Using the cached check here is deliberate for pre-native usage. This
-        // inconsistency is fine because the check here is for the Feed header blank size, the
-        // mismatch is bearable and only once for every change.
-        if (FeedFeatures.cachedIsReportingUserActions()) {
             // Header blank size should be consistent with
             // R.layout.new_tab_page_snippets_expandable_header_with_menu.
             lp.height =
                     getResources().getDimensionPixelSize(R.dimen.snippets_article_header_menu_size);
-        } else {
-            // Header blank size should be consistent with R.layout.ss_feed_header.
-            lp.height =
-                    getResources().getDimensionPixelSize(R.dimen.snippets_article_header_height);
-        }
         headerView.setLayoutParams(lp);
     }
 
@@ -99,9 +87,8 @@
 
         LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
                 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
-        int contentPadding = FeedFeatures.cachedIsV2Enabled() ? mResources.getDimensionPixelSize(
-                                     R.dimen.content_suggestions_card_modern_padding_v2)
-                                                              : 0;
+        int contentPadding =
+                mResources.getDimensionPixelSize(R.dimen.content_suggestions_card_modern_padding);
         lp.setMargins(contentPadding, 0, contentPadding, dpToPx(CARD_MARGIN_DP));
 
         // Set the First placeholder container - an image-right card.
@@ -235,12 +222,9 @@
      * is resized by {@link ViewResizer} in {@link FeedSurfaceCoordinator}
      */
     private void setPadding() {
-        int defaultPadding = mResources.getDimensionPixelSize(FeedFeatures.cachedIsV2Enabled()
-                        ? R.dimen.content_suggestions_card_modern_margin_v2
-                        : R.dimen.content_suggestions_card_modern_margin);
-        int widePadding = mResources.getDimensionPixelSize(FeedFeatures.cachedIsV2Enabled()
-                        ? R.dimen.ntp_wide_card_lateral_margins_v2
-                        : R.dimen.ntp_wide_card_lateral_margins);
+        int defaultPadding =
+                mResources.getDimensionPixelSize(R.dimen.content_suggestions_card_modern_margin);
+        int widePadding = mResources.getDimensionPixelSize(R.dimen.ntp_wide_card_lateral_margins);
 
         ViewResizer.createAndAttach(this, mUiConfig, defaultPadding, widePadding);
     }
diff --git a/chrome/android/features/start_surface/internal/javatests/src/org/chromium/chrome/features/start_surface/InstantStartTest.java b/chrome/android/features/start_surface/internal/javatests/src/org/chromium/chrome/features/start_surface/InstantStartTest.java
index 8dcc584..1e0517ba 100644
--- a/chrome/android/features/start_surface/internal/javatests/src/org/chromium/chrome/features/start_surface/InstantStartTest.java
+++ b/chrome/android/features/start_surface/internal/javatests/src/org/chromium/chrome/features/start_surface/InstantStartTest.java
@@ -78,10 +78,6 @@
 import org.chromium.base.StreamUtil;
 import org.chromium.base.SysUtils;
 import org.chromium.base.library_loader.LibraryLoader;
-import org.chromium.base.test.params.ParameterAnnotations;
-import org.chromium.base.test.params.ParameterProvider;
-import org.chromium.base.test.params.ParameterSet;
-import org.chromium.base.test.params.ParameterizedRunner;
 import org.chromium.base.test.util.CommandLineFlags;
 import org.chromium.base.test.util.Criteria;
 import org.chromium.base.test.util.CriteriaHelper;
@@ -97,7 +93,6 @@
 import org.chromium.chrome.browser.compositor.layouts.StaticLayout;
 import org.chromium.chrome.browser.compositor.layouts.content.TabContentManager;
 import org.chromium.chrome.browser.device.DeviceClassManager;
-import org.chromium.chrome.browser.feed.FeedV2;
 import org.chromium.chrome.browser.flags.CachedFeatureFlags;
 import org.chromium.chrome.browser.flags.ChromeFeatureList;
 import org.chromium.chrome.browser.flags.ChromeSwitches;
@@ -128,7 +123,7 @@
 import org.chromium.chrome.browser.toolbar.top.TopToolbarCoordinator;
 import org.chromium.chrome.browser.util.ChromeAccessibilityUtil;
 import org.chromium.chrome.test.ChromeActivityTestRule;
-import org.chromium.chrome.test.ChromeJUnit4RunnerDelegate;
+import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
 import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
 import org.chromium.chrome.test.R;
 import org.chromium.chrome.test.util.ActivityUtils;
@@ -161,8 +156,7 @@
 /**
  * Integration tests of Instant Start which requires 2-stage initialization for Clank startup.
  */
-@RunWith(ParameterizedRunner.class)
-@ParameterAnnotations.UseRunnerDelegate(ChromeJUnit4RunnerDelegate.class)
+@RunWith(ChromeJUnit4ClassRunner.class)
 // clang-format off
 @CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
 @EnableFeatures({ChromeFeatureList.TAB_GRID_LAYOUT_ANDROID,
@@ -198,24 +192,6 @@
     }
 
     /**
-     * Parameter set controlling whether Feed v2 is enabled.
-     */
-    public static class FeedParams implements ParameterProvider {
-        @Override
-        public List<ParameterSet> getParameters() {
-            List<ParameterSet> feedParams = new ArrayList<ParameterSet>();
-            if (FeedV2.IS_AVAILABLE) {
-                feedParams.add(new ParameterSet().value(true).name("FeedV2"));
-            }
-            return feedParams;
-        }
-    }
-
-    private void setFeedVersion(boolean isFeedV2) {
-        CachedFeatureFlags.setForTesting(ChromeFeatureList.INTEREST_FEED_V2, isFeedV2);
-    }
-
-    /**
      * Only launch Chrome without waiting for a current tab.
      * This test could not use {@link ChromeActivityTestRule#startMainActivityFromLauncher()}
      * because of its {@link org.chromium.chrome.browser.tab.Tab} dependency.
@@ -774,10 +750,9 @@
             "/exclude_mv_tiles/true" +
             "/hide_switch_when_no_incognito_tabs/true" +
             "/show_last_active_tab_only/true"})
-    @ParameterAnnotations.UseMethodParameter(FeedParams.class)
-    public void renderSingleAsHomepage_SingleTabNoMVTiles(boolean isFeedV2) throws IOException {
+    public void renderSingleAsHomepage_SingleTabNoMVTiles()
+        throws IOException {
         // clang-format on
-        setFeedVersion(isFeedV2);
 
         createTabStateFile(new int[] {0});
         createThumbnailBitmapAndWriteToFile(0);
@@ -806,15 +781,13 @@
     @SmallTest
     @Restriction({UiRestriction.RESTRICTION_TYPE_PHONE})
     @EnableFeatures({ChromeFeatureList.TAB_SWITCHER_ON_RETURN + "<Study,",
-            ChromeFeatureList.START_SURFACE_ANDROID + "<Study", ChromeFeatureList.INTEREST_FEED_V2})
+            ChromeFeatureList.START_SURFACE_ANDROID + "<Study"})
     // clang-format off
     @CommandLineFlags.Add({ChromeSwitches.DISABLE_NATIVE_INITIALIZATION,
             "force-fieldtrials=Study/Group",
             IMMEDIATE_RETURN_PARAMS + "/start_surface_variation/single"})
-    @ParameterAnnotations.UseMethodParameter(FeedParams.class)
-    public void testFeedPlaceholderFromColdStart(boolean isFeedV2) {
+    public void testFeedPlaceholderFromColdStart() {
         // clang-format on
-        setFeedVersion(isFeedV2);
         startMainActivityFromLauncher();
         Assert.assertFalse(mActivityTestRule.getActivity().isTablet());
         Assert.assertTrue(CachedFeatureFlags.isEnabled(ChromeFeatureList.INSTANT_START));
@@ -838,14 +811,12 @@
     @SmallTest
     @Restriction({UiRestriction.RESTRICTION_TYPE_PHONE})
     @EnableFeatures({ChromeFeatureList.TAB_SWITCHER_ON_RETURN + "<Study,",
-            ChromeFeatureList.START_SURFACE_ANDROID + "<Study", ChromeFeatureList.INTEREST_FEED_V2})
+            ChromeFeatureList.START_SURFACE_ANDROID + "<Study"})
     // clang-format off
     @CommandLineFlags.Add({"force-fieldtrials=Study/Group",
             IMMEDIATE_RETURN_PARAMS + "/start_surface_variation/single"})
-    @ParameterAnnotations.UseMethodParameter(FeedParams.class)
-    public void testCachedFeedVisibility(boolean isFeedV2) {
+    public void testCachedFeedVisibility() {
         // clang-format on
-        setFeedVersion(isFeedV2);
         startMainActivityFromLauncher();
         mActivityTestRule.waitForActivityNativeInitializationComplete();
         // FEED_ARTICLES_LIST_VISIBLE should equal to ARTICLES_LIST_VISIBLE.
@@ -879,15 +850,13 @@
     @SmallTest
     @Restriction({UiRestriction.RESTRICTION_TYPE_PHONE})
     @EnableFeatures({ChromeFeatureList.TAB_SWITCHER_ON_RETURN + "<Study,",
-            ChromeFeatureList.START_SURFACE_ANDROID + "<Study", ChromeFeatureList.INTEREST_FEED_V2})
+            ChromeFeatureList.START_SURFACE_ANDROID + "<Study"})
     // clang-format off
     @CommandLineFlags.Add({ChromeSwitches.DISABLE_NATIVE_INITIALIZATION,
             "force-fieldtrials=Study/Group",
             IMMEDIATE_RETURN_PARAMS + "/start_surface_variation/single"})
-    @ParameterAnnotations.UseMethodParameter(FeedParams.class)
-    public void testHidePlaceholder(boolean isFeedV2) {
+    public void testHidePlaceholder() {
         // clang-format on
-        setFeedVersion(isFeedV2);
         StartSurfaceConfiguration.setFeedVisibilityForTesting(false);
         startMainActivityFromLauncher();
 
@@ -899,15 +868,13 @@
     @SmallTest
     @Restriction({UiRestriction.RESTRICTION_TYPE_PHONE})
     @EnableFeatures({ChromeFeatureList.TAB_SWITCHER_ON_RETURN + "<Study,",
-            ChromeFeatureList.START_SURFACE_ANDROID + "<Study", ChromeFeatureList.INTEREST_FEED_V2})
+            ChromeFeatureList.START_SURFACE_ANDROID + "<Study"})
     // clang-format off
     @CommandLineFlags.Add({ChromeSwitches.DISABLE_NATIVE_INITIALIZATION,
             "force-fieldtrials=Study/Group",
             IMMEDIATE_RETURN_PARAMS + "/start_surface_variation/single"})
-    @ParameterAnnotations.UseMethodParameter(FeedParams.class)
-    public void testShowPlaceholder(boolean isFeedV2) {
+    public void testShowPlaceholder() {
         // clang-format on
-        setFeedVersion(isFeedV2);
         StartSurfaceConfiguration.setFeedVisibilityForTesting(true);
         startMainActivityFromLauncher();
 
@@ -1137,11 +1104,8 @@
     @CommandLineFlags.Add({ChromeSwitches.DISABLE_NATIVE_INITIALIZATION,
             "force-fieldtrials=Study/Group",
             IMMEDIATE_RETURN_PARAMS + "/start_surface_variation/single"})
-    @ParameterAnnotations.UseMethodParameter(FeedParams.class)
-    public void renderSingleAsHomepage_Landscape(boolean isFeedV2) throws IOException {
+    public void renderSingleAsHomepage_Landscape() throws IOException {
         // clang-format on
-        setFeedVersion(isFeedV2);
-
         createTabStateFile(new int[] {0, 1, 2});
 
         startMainActivityFromLauncher();
diff --git a/chrome/android/features/start_surface/internal/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceLayoutTest.java b/chrome/android/features/start_surface/internal/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceLayoutTest.java
index d8ded71f..61d4812 100644
--- a/chrome/android/features/start_surface/internal/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceLayoutTest.java
+++ b/chrome/android/features/start_surface/internal/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceLayoutTest.java
@@ -94,7 +94,6 @@
 import org.chromium.chrome.browser.compositor.layouts.Layout;
 import org.chromium.chrome.browser.compositor.layouts.StaticLayout;
 import org.chromium.chrome.browser.compositor.layouts.content.TabContentManager;
-import org.chromium.chrome.browser.feed.shared.FeedFeatures;
 import org.chromium.chrome.browser.flags.CachedFeatureFlags;
 import org.chromium.chrome.browser.flags.ChromeFeatureList;
 import org.chromium.chrome.browser.flags.ChromeSwitches;
@@ -1892,6 +1891,7 @@
 
     @Test
     @MediumTest
+    @DisabledTest(message = "crbug.com/1187320 This doesn't work with FeedV2.")
     public void testActivityCanBeGarbageCollectedAfterFinished() throws Exception {
         prepareTabs(1, 0, "about:blank");
 
@@ -1906,12 +1906,9 @@
         mTabListDelegate = null;
         mActivityTestRule.setActivity(activity);
 
-        // TODO(crbug.com/1129187): Looks like this doesn't work with FeedV2.
-        if (!FeedFeatures.isV2Enabled()) {
-            // A longer timeout is needed. Achieve that by using the CriteriaHelper.pollUiThread.
-            CriteriaHelper.pollUiThread(
-                    () -> GarbageCollectionTestUtils.canBeGarbageCollected(activityRef));
-        }
+        // A longer timeout is needed. Achieve that by using the CriteriaHelper.pollUiThread.
+        CriteriaHelper.pollUiThread(
+                () -> GarbageCollectionTestUtils.canBeGarbageCollected(activityRef));
     }
 
     /**
diff --git a/chrome/android/features/start_surface/internal/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceTest.java b/chrome/android/features/start_surface/internal/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceTest.java
index 04f9503..964a755 100644
--- a/chrome/android/features/start_surface/internal/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceTest.java
+++ b/chrome/android/features/start_surface/internal/javatests/src/org/chromium/chrome/features/start_surface/StartSurfaceTest.java
@@ -1106,7 +1106,7 @@
                                 SingleTabSwitcherMediator.SINGLE_TAB_TITLE_AVAILABLE_TIME_UMA,
                                 isInstantStart)));
 
-        // TODO(crbug.com/1129187): Looks like this doesn't work with FeedV2.
+        // TODO(crbug.com/1187320): Looks like this doesn't work with FeedV2.
         if (!(FeedFeatures.isV2Enabled() && mImmediateReturn)) {
             Assert.assertEquals(expectedRecordCount,
                     RecordHistogram.getHistogramTotalCountForTesting(
@@ -1114,7 +1114,7 @@
                                     FeedSurfaceMediator.FEED_CONTENT_FIRST_LOADED_TIME_MS_UMA,
                                     isInstantStart)));
         }
-        // TODO(crbug.com/1129187): Looks like this doesn't work with FeedV2.
+        // TODO(crbug.com/1187320): Looks like this doesn't work with FeedV2.
         if (!(FeedFeatures.isV2Enabled() && mImmediateReturn && mUseInstantStart)) {
             Assert.assertEquals(expectedRecordCount,
                     RecordHistogram.getHistogramTotalCountForTesting(
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/TasksView.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/TasksView.java
index 00677dd4..cae9d6d 100644
--- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/TasksView.java
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/TasksView.java
@@ -30,7 +30,6 @@
 import org.chromium.base.ApiCompatibilityUtils;
 import org.chromium.base.MathUtils;
 import org.chromium.chrome.browser.feed.FeedSurfaceCoordinator;
-import org.chromium.chrome.browser.feed.shared.FeedFeatures;
 import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
 import org.chromium.chrome.browser.ntp.IncognitoDescriptionView;
 import org.chromium.chrome.browser.ntp.search.SearchBoxCoordinator;
@@ -100,7 +99,7 @@
     public void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
         mUiConfig.updateDisplayStyle();
-        alignHeaderForFeedV2();
+        alignHeaderForFeed();
     }
 
     private void adjustScrollMode(AppBarLayout.LayoutParams layoutParams) {
@@ -132,21 +131,13 @@
         // ExploreSurfaceCoordinator.
         TextView titleDescription = (TextView) findViewById(R.id.tab_switcher_title_description);
         TextView moreTabs = (TextView) findViewById(R.id.more_tabs);
-        if (FeedFeatures.cachedIsReportingUserActions()) {
-            ApiCompatibilityUtils.setTextAppearance(
-                    titleDescription, R.style.TextAppearance_TextSmall_Secondary);
-            ApiCompatibilityUtils.setTextAppearance(
-                    moreTabs, R.style.TextAppearance_TextSmall_Blue);
-            ViewCompat.setPaddingRelative(titleDescription,
-                    mContext.getResources().getDimensionPixelSize(R.dimen.card_padding),
-                    titleDescription.getPaddingTop(), titleDescription.getPaddingEnd(),
-                    titleDescription.getPaddingBottom());
-        } else {
-            ApiCompatibilityUtils.setTextAppearance(
-                    titleDescription, R.style.TextAppearance_TextMediumThick_Primary);
-            ApiCompatibilityUtils.setTextAppearance(
-                    moreTabs, R.style.TextAppearance_TextMedium_Blue);
-        }
+        ApiCompatibilityUtils.setTextAppearance(
+                titleDescription, R.style.TextAppearance_TextSmall_Secondary);
+        ApiCompatibilityUtils.setTextAppearance(moreTabs, R.style.TextAppearance_TextSmall_Blue);
+        ViewCompat.setPaddingRelative(titleDescription,
+                mContext.getResources().getDimensionPixelSize(R.dimen.card_padding),
+                titleDescription.getPaddingTop(), titleDescription.getPaddingEnd(),
+                titleDescription.getPaddingBottom());
     }
 
     ViewGroup getCarouselTabSwitcherContainer() {
@@ -463,25 +454,20 @@
      */
     private void setHeaderPadding() {
         int defaultPadding = 0;
-        int widePadding = getResources().getDimensionPixelSize(FeedFeatures.cachedIsV2Enabled()
-                        ? R.dimen.ntp_wide_card_lateral_margins_v2
-                        : R.dimen.ntp_wide_card_lateral_margins);
+        int widePadding =
+                getResources().getDimensionPixelSize(R.dimen.ntp_wide_card_lateral_margins);
 
         ViewResizer.createAndAttach(mHeaderView, mUiConfig, defaultPadding, widePadding);
-        alignHeaderForFeedV2();
+        alignHeaderForFeed();
     }
 
     /**
-     * Feed v2 has extra content padding, we need to align the header with it. However, the padding
+     * Feed has extra content padding, we need to align the header with it. However, the padding
      * of the header is already bound with ViewResizer in setHeaderPadding(), so we update the left
      * & right margins of MV tiles container and carousel tab switcher container.
      */
-    private void alignHeaderForFeedV2() {
-        if (!FeedFeatures.cachedIsV2Enabled()) {
-            return;
-        }
-
-        MarginLayoutParams MVParams =
+    private void alignHeaderForFeed() {
+        MarginLayoutParams mostVisitedLayoutParams =
                 (MarginLayoutParams) mHeaderView.findViewById(R.id.mv_tiles_container)
                         .getLayoutParams();
 
@@ -489,15 +475,15 @@
                 (MarginLayoutParams) mCarouselTabSwitcherContainer.getLayoutParams();
 
         int margin = getResources().getDimensionPixelSize(
-                R.dimen.content_suggestions_card_modern_padding_v2);
+                R.dimen.content_suggestions_card_modern_padding);
         if (getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) {
-            MVParams.leftMargin = margin;
-            MVParams.rightMargin = margin;
+            mostVisitedLayoutParams.leftMargin = margin;
+            mostVisitedLayoutParams.rightMargin = margin;
             carouselTabSwitcherParams.leftMargin = margin;
             carouselTabSwitcherParams.rightMargin = margin;
         } else {
-            MVParams.leftMargin = 0;
-            MVParams.rightMargin = 0;
+            mostVisitedLayoutParams.leftMargin = 0;
+            mostVisitedLayoutParams.rightMargin = 0;
             carouselTabSwitcherParams.leftMargin =
                     getResources().getDimensionPixelSize(R.dimen.tab_carousel_start_margin);
             carouselTabSwitcherParams.rightMargin = 0;
diff --git a/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/DEPS b/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/DEPS
deleted file mode 100644
index 28f8ef6..0000000
--- a/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/DEPS
+++ /dev/null
@@ -1,9 +0,0 @@
-include_rules = [
-  "-chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/v2",
-]
-
-specific_include_rules = {
-    "FeedV2\.java": [
-        "+chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/v2",
-    ],
-}
diff --git a/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedSurfaceCoordinator.java b/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedSurfaceCoordinator.java
index 0c73af5..fe0f6d63 100644
--- a/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedSurfaceCoordinator.java
+++ b/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedSurfaceCoordinator.java
@@ -26,12 +26,12 @@
 import org.chromium.base.supplier.Supplier;
 import org.chromium.chrome.R;
 import org.chromium.chrome.browser.feature_engagement.TrackerFactory;
-import org.chromium.chrome.browser.feed.shared.FeedFeatures;
 import org.chromium.chrome.browser.feed.shared.FeedSurfaceDelegate;
 import org.chromium.chrome.browser.feed.shared.FeedSurfaceProvider;
 import org.chromium.chrome.browser.feed.shared.stream.Header;
 import org.chromium.chrome.browser.feed.shared.stream.NonDismissibleHeader;
 import org.chromium.chrome.browser.feed.shared.stream.Stream;
+import org.chromium.chrome.browser.feed.v2.FeedStream;
 import org.chromium.chrome.browser.flags.ChromeFeatureList;
 import org.chromium.chrome.browser.native_page.ContextMenuManager;
 import org.chromium.chrome.browser.native_page.NativePageNavigationDelegate;
@@ -47,7 +47,6 @@
 import org.chromium.chrome.browser.share.ShareDelegate;
 import org.chromium.chrome.browser.signin.services.IdentityServicesProvider;
 import org.chromium.chrome.browser.signin.ui.PersonalizedSigninPromoView;
-import org.chromium.chrome.browser.tabmodel.TabModelSelector;
 import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
 import org.chromium.chrome.browser.user_education.UserEducationHelper;
 import org.chromium.chrome.features.start_surface.StartSurfaceConfiguration;
@@ -75,34 +74,6 @@
     @VisibleForTesting
     public static final String FEED_STREAM_CREATED_TIME_MS_UMA = "FeedStreamCreatedTime";
 
-    /**
-     * Provides an interface that can be implemented by both Feed v1 and v2, allowing us to compile
-     * without Feed v1. Once v1 is removed, this interface should be removed.
-     */
-    public interface StreamWrapper {
-        int defaultMarginPixels(Activity activity);
-        int wideMarginPixels(Activity activity);
-
-        /**
-         * Creates the stream. Only called once unless doneWithStream is called.
-         */
-        Stream createStream(Profile profile, Activity activity, boolean showDarkBackground,
-                SnackbarManager snackbarManager,
-                NativePageNavigationDelegate pageNavigationDelegate, UiConfig uiConfig,
-                boolean placeholderShown, BottomSheetController bottomSheetController,
-                WindowAndroid windowAndroid, FeedV1ActionOptions v1ActionOptions,
-                Supplier<ShareDelegate> shareDelegateSupplier);
-
-        /**
-         * Called after the stream returned by createStream() is no longer needed.
-         */
-        void doneWithStream();
-        boolean isPlaceholderShown();
-        void addScrollListener();
-    }
-
-    StreamWrapper mStreamWrapper;
-
     protected final Activity mActivity;
     private final SnackbarManager mSnackbarManager;
     @Nullable
@@ -114,13 +85,11 @@
     private final int mWideMarginPixels;
     private final FeedSurfaceMediator mMediator;
     private final BottomSheetController mBottomSheetController;
-    private final FeedV1ActionOptions mV1ActionOptions;
     private final WindowAndroid mWindowAndroid;
     private final Supplier<ShareDelegate> mShareSupplier;
 
     private UiConfig mUiConfig;
     private FrameLayout mRootView;
-    private ContextMenuManager mContextMenuManager;
     private Tracker mTracker;
     private long mStreamCreatedTimeMs;
 
@@ -147,16 +116,11 @@
     private @Nullable ScrollView mScrollViewForPolicy;
     private @Nullable ViewResizer mScrollViewResizer;
 
-    // Used for the feed header menu.
-    private UserEducationHelper mUserEducationHelper;
-
     // Used to handle things related to the main scrollable container of NTP surface.
     private @Nullable ScrollableContainerDelegate mScrollableContainerDelegate;
 
     private @Nullable HeaderIphScrollListener mHeaderIphScrollListener;
 
-    private final Handler mHandler = new Handler();
-
     private class SignInPromoHeader implements Header {
         @Override
         public View getView() {
@@ -268,32 +232,27 @@
      * Constructs a new FeedSurfaceCoordinator.
      * @param activity The containing {@link Activity}.
      * @param snackbarManager The {@link SnackbarManager} displaying Snackbar UI.
-     * @param tabModelSelector {@link TabModelSelector} object.
      * @param windowAndroid The window of the page.
      * @param snapScrollHelper The {@link SnapScrollHelper} for the New Tab Page.
      * @param ntpHeader The extra header on top of the feeds for the New Tab Page.
      * @param sectionHeaderView The {@link SectionHeaderView} for the feed.
-     * @param actionOptions Configures feed v1 actions.
      * @param showDarkBackground Whether is shown on dark background.
      * @param delegate The constructing {@link FeedSurfaceDelegate}.
      * @param pageNavigationDelegate The {@link NativePageNavigationDelegate}
      *                               that handles page navigation.
      * @param profile The current user profile.
      * @param isPlaceholderShownInitially Whether the placeholder is shown initially.
-     * @param bottomSheetController The bottom sheet controller, used in v2.
+     * @param bottomSheetController The bottom sheet controller.
      * @param shareDelegateSupplier The supplier for the share delegate used to share articles.
      */
     public FeedSurfaceCoordinator(Activity activity, SnackbarManager snackbarManager,
-            TabModelSelector tabModelSelector, WindowAndroid windowAndroid,
-            @Nullable SnapScrollHelper snapScrollHelper, @Nullable View ntpHeader,
-            @Nullable SectionHeaderView sectionHeaderView, FeedV1ActionOptions actionOptions,
+            WindowAndroid windowAndroid, @Nullable SnapScrollHelper snapScrollHelper,
+            @Nullable View ntpHeader, @Nullable SectionHeaderView sectionHeaderView,
             boolean showDarkBackground, FeedSurfaceDelegate delegate,
             @Nullable NativePageNavigationDelegate pageNavigationDelegate, Profile profile,
             boolean isPlaceholderShownInitially, BottomSheetController bottomSheetController,
             Supplier<ShareDelegate> shareDelegateSupplier,
             @Nullable ScrollableContainerDelegate externalScrollableContainerDelegate) {
-        mStreamWrapper = FeedV2.createStreamWrapper();
-
         mActivity = activity;
         mSnackbarManager = snackbarManager;
         mNtpHeader = ntpHeader;
@@ -303,20 +262,20 @@
         mPageNavigationDelegate = pageNavigationDelegate;
         mBottomSheetController = bottomSheetController;
         mProfile = profile;
-        mV1ActionOptions = actionOptions;
         mWindowAndroid = windowAndroid;
         mShareSupplier = shareDelegateSupplier;
         mScrollableContainerDelegate = externalScrollableContainerDelegate;
 
         Resources resources = mActivity.getResources();
-        mDefaultMarginPixels = mStreamWrapper.defaultMarginPixels(activity);
-        mWideMarginPixels = mStreamWrapper.wideMarginPixels(activity);
+        mDefaultMarginPixels = mActivity.getResources().getDimensionPixelSize(
+                R.dimen.content_suggestions_card_modern_margin);
+        mWideMarginPixels = mActivity.getResources().getDimensionPixelSize(
+                R.dimen.ntp_wide_card_lateral_margins);
 
         mRootView = new RootView(mActivity);
         mRootView.setPadding(0, resources.getDimensionPixelOffset(R.dimen.tab_strip_height), 0, 0);
         mUiConfig = new UiConfig(mRootView);
 
-        mTracker = TrackerFactory.getTrackerForProfile(profile);
 
         if (isEnhancedProtectionPromoEnabled()) {
             mEnhancedProtectionPromoController =
@@ -340,8 +299,6 @@
         // Mediator should be created before any Stream changes.
         mMediator = new FeedSurfaceMediator(
                 this, snapScrollHelper, mPageNavigationDelegate, mSectionHeaderModel);
-
-        mUserEducationHelper = new UserEducationHelper(mActivity, mHandler);
     }
 
     @Override
@@ -350,7 +307,6 @@
         mMediator.destroy();
         if (mStreamLifecycleManager != null) mStreamLifecycleManager.destroy();
         mStreamLifecycleManager = null;
-        mStreamWrapper.doneWithStream();
         if (mEnhancedProtectionPromoController != null) {
             mEnhancedProtectionPromoController.destroy();
         }
@@ -407,7 +363,7 @@
 
     /** @return Whether the placeholder is shown. */
     public boolean isPlaceholderShown() {
-        return mStreamWrapper.isPlaceholderShown();
+        return mStream.isPlaceholderShown();
     }
 
     /**
@@ -422,9 +378,9 @@
         }
 
         mStreamCreatedTimeMs = SystemClock.elapsedRealtime();
-        mStream = mStreamWrapper.createStream(mProfile, mActivity, mShowDarkBackground,
-                mSnackbarManager, mPageNavigationDelegate, mUiConfig, mIsPlaceholderShownInitially,
-                mBottomSheetController, mWindowAndroid, mV1ActionOptions, mShareSupplier);
+        mStream = new FeedStream(mActivity, mShowDarkBackground, mSnackbarManager,
+                mPageNavigationDelegate, mBottomSheetController, mIsPlaceholderShownInitially,
+                mWindowAndroid, mShareSupplier);
 
         mStreamLifecycleManager = mDelegate.createStreamLifecycleManager(mStream, mActivity);
 
@@ -453,8 +409,6 @@
             mStream.setHeaderViews(Arrays.asList(new NonDismissibleHeader(mSectionHeaderView)));
         }
 
-        mStreamWrapper.addScrollListener();
-
         // Work around https://crbug.com/943873 where default focus highlight shows up after
         // toggling dark mode.
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -498,7 +452,6 @@
                 mEnhancedProtectionPromoController.destroy();
                 mEnhancedProtectionPromoController = null;
             }
-            mStreamWrapper.doneWithStream();
         }
 
         mScrollViewForPolicy = new PolicyScrollView(mActivity);
@@ -542,13 +495,6 @@
             mSigninPromoView = (PersonalizedSigninPromoView) inflater.inflate(
                     R.layout.personalized_signin_promo_view_modern_content_suggestions, mRootView,
                     false);
-
-            // If the placeholder is shown in Feed v1, delay to show the sign-in view until the
-            // articles are shown. Feed v2's articles don't have fade-in animations, so sign-in view
-            // is already shown together with v2 articles.
-            if (isPlaceholderShown() && !FeedFeatures.isV2Enabled()) {
-                mSigninPromoView.setVisibility(View.INVISIBLE);
-            }
         }
         return mSigninPromoView;
     }
@@ -600,14 +546,6 @@
                 mStreamCreatedTimeMs - activityCreationTimeMs, mIsPlaceholderShownInitially);
     }
 
-    Tracker getFeatureEngagementTracker() {
-        return mTracker;
-    }
-
-    UserEducationHelper getUserEducationHelper() {
-        return mUserEducationHelper;
-    }
-
     EnhancedProtectionPromoController getEnhancedProtectionPromoController() {
         return mEnhancedProtectionPromoController;
     }
@@ -661,11 +599,12 @@
         HeaderIphScrollListener.Delegate delegate = new HeaderIphScrollListener.Delegate() {
             @Override
             public Tracker getFeatureEngagementTracker() {
-                return mTracker;
+                return TrackerFactory.getTrackerForProfile(mProfile);
             }
             @Override
             public void showMenuIph() {
-                mSectionHeaderView.showMenuIph(mUserEducationHelper);
+                UserEducationHelper helper = new UserEducationHelper(mActivity, new Handler());
+                mSectionHeaderView.showMenuIph(helper);
             }
             @Override
             public boolean isFeedExpanded() {
diff --git a/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedSurfaceMediator.java b/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedSurfaceMediator.java
index c29639aa..fe24fb4 100644
--- a/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedSurfaceMediator.java
+++ b/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedSurfaceMediator.java
@@ -221,24 +221,17 @@
                 mStreamContentChanged = true;
                 if (mSnapScrollHelper != null) mSnapScrollHelper.resetSearchBoxOnScroll(true);
 
-                // Feed v2's background is set to be transparent in {@link
+                // Feed's background is set to be transparent in {@link
                 // FeedSurfaceCoordinator#createStream} to show the Feed placeholder. When first
                 // batch of articles are about to show, set recyclerView back to non-transparent.
-                // Feed v2 doesn't call onAddFinished(), so we hide placeholder here.
-                if (FeedFeatures.isV2Enabled() && mCoordinator.isPlaceholderShown()) {
+                if (mCoordinator.isPlaceholderShown()) {
                     stream.hidePlaceholder();
                 }
             }
 
             @Override
             public void onAddFinished() {
-                // Feed v1's background is set to be transparent in {@link
-                // FeedSurfaceCoordinator#createStream} to show the Feed placeholder. After first
-                // batch of articles finish fade-in animation, set recyclerView back to
-                // non-transparent.
-                if (!FeedFeatures.isV2Enabled() && mCoordinator.isPlaceholderShown()) {
-                    stream.hidePlaceholder();
-                }
+                // TODO(crbug.com/1187320): Remove this method altogether when bug fixed.
                 if (mContentFirstAvailableTimeMs == 0) {
                     mContentFirstAvailableTimeMs = SystemClock.elapsedRealtime();
                     if (mHasPendingUmaRecording) {
@@ -248,16 +241,6 @@
                 }
                 mIsLoadingFeed = false;
             }
-
-            @Override
-            public void onAddStarting() {
-                // Feed v1's sign-in view is set to be invisible in {@link
-                // FeedSurfaceCoordinator#getSigninPromoView} if the Feed placeholder is shown. Set
-                // sign-in box visible back when Feed articles are about to show.
-                if (!FeedFeatures.isV2Enabled() && mCoordinator.isPlaceholderShown()) {
-                    mCoordinator.fadeInSigninView();
-                }
-            }
         };
         stream.addOnContentChangedListener(mStreamContentChangedListener);
 
@@ -272,7 +255,7 @@
             mFeedMenuModel = buildMenuItems();
 
             PropertyModel interestFeedHeader = SectionHeaderProperties.createSectionHeader(
-                    getSectionHeaderText(suggestionsVisible));
+                    getInterestFeedHeaderText(suggestionsVisible));
             mSectionHeaderModel.get(SectionHeaderListProperties.SECTION_HEADERS_KEY)
                     .add(interestFeedHeader);
 
@@ -393,7 +376,7 @@
             mSectionHeaderModel.get(SectionHeaderListProperties.SECTION_HEADERS_KEY)
                     .get(INTEREST_FEED_HEADER_POSITION)
                     .set(SectionHeaderProperties.HEADER_TEXT_KEY,
-                            getSectionHeaderText(suggestionsVisible));
+                            getInterestFeedHeaderText(suggestionsVisible));
         }
 
         // Update toggleswitch item, which is last item in list.
@@ -425,8 +408,8 @@
         SuggestionsMetrics.recordArticlesListVisible();
     }
 
-    /** Returns the section header text based on the selected default search engine */
-    private String getSectionHeaderText(boolean isExpanded) {
+    /** Returns the interest feed header text based on the selected default search engine */
+    private String getInterestFeedHeaderText(boolean isExpanded) {
         Resources res = mCoordinator.getSectionHeaderView().getResources();
         final boolean isDefaultSearchEngineGoogle =
                 TemplateUrlServiceFactory.get().isDefaultSearchEngineGoogle();
diff --git a/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedV1ActionOptions.java b/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedV1ActionOptions.java
deleted file mode 100644
index acd81df..0000000
--- a/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedV1ActionOptions.java
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright 2020 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-package org.chromium.chrome.browser.feed;
-
-/**
- * Options for handling actions in Feed V1.
- * TODO(crbug.com/1165828): V1 was removed, these do nothing.
- */
-public class FeedV1ActionOptions {
-    public boolean inhibitDownload;
-    public boolean inhibitOpenInIncognito;
-    public boolean inhibitOpenInNewTab;
-    public boolean inhibitLearnMore;
-}
diff --git a/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedV2.java b/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedV2.java
deleted file mode 100644
index 208a77b..0000000
--- a/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedV2.java
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright 2020 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-package org.chromium.chrome.browser.feed;
-
-import org.chromium.chrome.browser.feed.v2.FeedStreamSurface;
-import org.chromium.chrome.browser.feed.v2.FeedStreamWrapper;
-
-/**
- * Provides access to FeedV2 with a similar interface as FeedV1.
- */
-public class FeedV2 {
-    // Whether FeedV2 is compiled in.
-    public static final boolean IS_AVAILABLE = FeedV2BuildFlag.IS_AVAILABLE;
-
-    public static void startup() {
-        FeedStreamSurface.startup();
-    }
-
-    public static FeedSurfaceCoordinator.StreamWrapper createStreamWrapper() {
-        return new FeedStreamWrapper();
-    }
-}
diff --git a/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedV2BuildFlag.java b/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedV2BuildFlag.java
deleted file mode 100644
index 7dad06f..0000000
--- a/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedV2BuildFlag.java
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright 2020 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-package org.chromium.chrome.browser.feed;
-
-/**
- * Provides the value of the feed_v2_enabled build flag.
- */
-class FeedV2BuildFlag {
-    public static final boolean IS_AVAILABLE = true;
-}
diff --git a/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/shared/FeedFeatures.java b/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/shared/FeedFeatures.java
index 380c417..2d549d9 100644
--- a/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/shared/FeedFeatures.java
+++ b/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/shared/FeedFeatures.java
@@ -5,7 +5,6 @@
 package org.chromium.chrome.browser.feed.shared;
 
 import org.chromium.base.Log;
-import org.chromium.chrome.browser.flags.CachedFeatureFlags;
 import org.chromium.chrome.browser.flags.ChromeFeatureList;
 import org.chromium.chrome.browser.preferences.Pref;
 import org.chromium.chrome.browser.preferences.PrefChangeRegistrar;
@@ -26,32 +25,11 @@
 
     private static PrefChangeRegistrar sPrefChangeRegistrar;
 
-    /**
-     * @return Whether implicit Feed user actions are being reported based on feature states. Can be
-     *         used for both Feed v1 and v2.
-     */
-    public static boolean isReportingUserActions() {
-        return isV2Enabled()
-                || ChromeFeatureList.isEnabled(ChromeFeatureList.REPORT_FEED_USER_ACTIONS);
-    }
-
-    /**
-     * Identical to {@link isReportingUserActions} but uses {@link CachedFeatureFlags} for checking
-     * feature states.
-     */
-    public static boolean cachedIsReportingUserActions() {
-        return cachedIsV2Enabled()
-                || CachedFeatureFlags.isEnabled(ChromeFeatureList.REPORT_FEED_USER_ACTIONS);
-    }
-
+    /** TODO(crbug.com/1187320): Remove when tests are fixed. */
     public static boolean isV2Enabled() {
         return true;
     }
 
-    public static boolean cachedIsV2Enabled() {
-        return true;
-    }
-
     /**
      * @return Whether the feed is allowed to be used. The feed is disabled if supervised user or
      * enterprise policy has once been added within the current session. The value returned by
diff --git a/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/shared/stream/Stream.java b/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/shared/stream/Stream.java
index 05a78ba..fa45708 100644
--- a/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/shared/stream/Stream.java
+++ b/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/shared/stream/Stream.java
@@ -179,11 +179,5 @@
          * {@link androidx.recyclerview.widget.SimpleItemAnimator#onAddFinished} event is received.
          */
         default void onAddFinished(){};
-
-        /**
-         * Called by Stream when an
-         * {@link androidx.recyclerview.widget.SimpleItemAnimator#onAddStarting} event is received.
-         */
-        default void onAddStarting(){};
     }
 }
diff --git a/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/v2/FeedStreamWrapper.java b/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/v2/FeedStreamWrapper.java
deleted file mode 100644
index 8a7d324..0000000
--- a/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/v2/FeedStreamWrapper.java
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright 2020 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-package org.chromium.chrome.browser.feed.v2;
-
-import android.app.Activity;
-
-import org.chromium.base.supplier.Supplier;
-import org.chromium.chrome.R;
-import org.chromium.chrome.browser.feed.FeedSurfaceCoordinator;
-import org.chromium.chrome.browser.feed.FeedV1ActionOptions;
-import org.chromium.chrome.browser.feed.shared.stream.Stream;
-import org.chromium.chrome.browser.native_page.NativePageNavigationDelegate;
-import org.chromium.chrome.browser.profiles.Profile;
-import org.chromium.chrome.browser.share.ShareDelegate;
-import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
-import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
-import org.chromium.components.browser_ui.widget.displaystyle.UiConfig;
-import org.chromium.ui.base.WindowAndroid;
-
-/**
- * Wraps management of the Feed V2 stream.
- */
-public class FeedStreamWrapper implements FeedSurfaceCoordinator.StreamWrapper {
-    private Stream mStream;
-    @Override
-    public int defaultMarginPixels(Activity activity) {
-        return activity.getResources().getDimensionPixelSize(
-                R.dimen.content_suggestions_card_modern_margin_v2);
-    }
-
-    @Override
-    public int wideMarginPixels(Activity activity) {
-        return activity.getResources().getDimensionPixelSize(
-                R.dimen.ntp_wide_card_lateral_margins_v2);
-    }
-
-    @Override
-    public Stream createStream(Profile profile, Activity activity, boolean showDarkBackground,
-            SnackbarManager snackbarManager, NativePageNavigationDelegate pageNavigationDelegate,
-            UiConfig uiConfig, boolean placeholderShown,
-            BottomSheetController bottomSheetController, WindowAndroid windowAndroid,
-            FeedV1ActionOptions v1ActionOptions, Supplier<ShareDelegate> shareDelegateSupplier) {
-        mStream = new FeedStream(activity, showDarkBackground, snackbarManager,
-                pageNavigationDelegate, bottomSheetController, placeholderShown, windowAndroid,
-                shareDelegateSupplier);
-        return mStream;
-    }
-
-    @Override
-    public boolean isPlaceholderShown() {
-        return mStream.isPlaceholderShown();
-    }
-
-    @Override
-    public void doneWithStream() {}
-
-    @Override
-    public void addScrollListener() {}
-}
diff --git a/chrome/android/feed/feed_java_sources.gni b/chrome/android/feed/feed_java_sources.gni
index bf843b3..fe7dfcd 100644
--- a/chrome/android/feed/feed_java_sources.gni
+++ b/chrome/android/feed/feed_java_sources.gni
@@ -23,8 +23,6 @@
   "//chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedSurfaceCoordinator.java",
   "//chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedSurfaceMediator.java",
   "//chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedUma.java",
-  "//chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedV1ActionOptions.java",
-  "//chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedV2.java",
   "//chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/HeaderIphScrollListener.java",
   "//chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/NtpStreamLifecycleManager.java",
   "//chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/StreamLifecycleManager.java",
@@ -45,50 +43,33 @@
   "//chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/v2/FeedSliceViewTracker.java",
   "//chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/v2/FeedStream.java",
   "//chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/v2/FeedStreamSurface.java",
-  "//chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/v2/FeedStreamWrapper.java",
   "//chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/v2/NativeViewListRenderer.java",
 ]
 
-if (enable_feed_v2) {
-  feed_java_sources += [ "//chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/FeedV2BuildFlag.java" ]
-} else {
-  feed_java_sources += [ "//chrome/android/feed/dummy/java/src/org/chromium/chrome/browser/feed/FeedV2BuildFlag.java" ]
-}
-
 feed_srcjar_deps = []
 
-feed_junit_test_java_sources = []
+feed_junit_test_java_sources = [
+  "junit/src/org/chromium/chrome/browser/feed/v2/FakeLinearLayoutManager.java",
+  "junit/src/org/chromium/chrome/browser/feed/v2/FeedListContentManagerTest.java",
+  "junit/src/org/chromium/chrome/browser/feed/v2/FeedProcessScopeDependencyProviderTest.java",
+  "junit/src/org/chromium/chrome/browser/feed/v2/FeedSliceViewTrackerTest.java",
+  "junit/src/org/chromium/chrome/browser/feed/v2/FeedStreamSurfaceTest.java",
+  "junit/src/org/chromium/chrome/browser/feed/v2/FeedStreamTest.java",
+  "junit/src/org/chromium/chrome/browser/feed/v2/NativeViewListRendererTest.java",
+]
 
-if (enable_feed_v2) {
-  feed_junit_test_java_sources += [
-    "junit/src/org/chromium/chrome/browser/feed/v2/FakeLinearLayoutManager.java",
-    "junit/src/org/chromium/chrome/browser/feed/v2/FeedListContentManagerTest.java",
-    "junit/src/org/chromium/chrome/browser/feed/v2/FeedProcessScopeDependencyProviderTest.java",
-    "junit/src/org/chromium/chrome/browser/feed/v2/FeedSliceViewTrackerTest.java",
-    "junit/src/org/chromium/chrome/browser/feed/v2/FeedStreamSurfaceTest.java",
-    "junit/src/org/chromium/chrome/browser/feed/v2/FeedStreamTest.java",
-    "junit/src/org/chromium/chrome/browser/feed/v2/NativeViewListRendererTest.java",
-  ]
-}
+feed_test_java_sources = [
+  "//chrome/android/feed/core/javatests/src/org/chromium/chrome/browser/feed/HeaderIphScrollListenerTest.java",
+  "//chrome/android/feed/core/javatests/src/org/chromium/chrome/browser/feed/v2/FeedProcessScopeDependencyProviderNativeTest.java",
+  "//chrome/android/feed/core/javatests/src/org/chromium/chrome/browser/feed/v2/FeedV2NewTabPageTest.java",
+  "//chrome/android/feed/core/javatests/src/org/chromium/chrome/browser/feed/v2/FeedV2TestHelper.java",
+  "//chrome/android/feed/core/javatests/src/org/chromium/chrome/browser/feed/v2/TestFeedServer.java",
+]
 
-feed_test_java_sources = [ "//chrome/android/feed/core/javatests/src/org/chromium/chrome/browser/feed/HeaderIphScrollListenerTest.java" ]
-
-if (enable_feed_v2) {
-  feed_test_java_sources += [
-    "//chrome/android/feed/core/javatests/src/org/chromium/chrome/browser/feed/v2/FeedProcessScopeDependencyProviderNativeTest.java",
-    "//chrome/android/feed/core/javatests/src/org/chromium/chrome/browser/feed/v2/FeedV2NewTabPageTest.java",
-    "//chrome/android/feed/core/javatests/src/org/chromium/chrome/browser/feed/v2/FeedV2TestHelper.java",
-    "//chrome/android/feed/core/javatests/src/org/chromium/chrome/browser/feed/v2/TestFeedServer.java",
-  ]
-}
-
-feed_test_deps = []
-if (enable_feed_v2) {
-  feed_test_deps += feed_deps + [
-                      "//chrome/browser/privacy:java",
-                      "//chrome/browser/user_education:java",
-                      "//chrome/browser/xsurface:java",
-                      "//third_party/android_deps:guava_android_java",
-                      "//third_party/google-truth:google_truth_java",
-                    ]
-}
+feed_test_deps = feed_deps + [
+                   "//chrome/browser/privacy:java",
+                   "//chrome/browser/user_education:java",
+                   "//chrome/browser/xsurface:java",
+                   "//third_party/android_deps:guava_android_java",
+                   "//third_party/google-truth:google_truth_java",
+                 ]
diff --git a/chrome/android/java/res/layout/chip_view_menu_item.xml b/chrome/android/java/res/layout/chip_view_menu_item.xml
deleted file mode 100644
index dab94325..0000000
--- a/chrome/android/java/res/layout/chip_view_menu_item.xml
+++ /dev/null
@@ -1,33 +0,0 @@
-<?xml version="1.0" encoding="utf-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.
--->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:layout_width="match_parent"
-    android:layout_height="?android:attr/listPreferredItemHeightSmall"
-    android:gravity="center_vertical"
-    android:orientation="horizontal" >
-
-    <org.chromium.components.browser_ui.widget.text.TextViewWithCompoundDrawables
-        android:id="@+id/title"
-        style="@style/AppMenuItemTextViewWithCompoundDrawables"
-        android:layout_weight="1"
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        android:paddingStart="16dp"
-        android:background="?attr/listChoiceBackgroundIndicator"/>
-
-    <org.chromium.ui.widget.ChipView
-        android:id="@+id/chip_view"
-        android:layout_weight="0"
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        android:layout_gravity="end"
-        android:layout_marginEnd="16dp"
-        style="@style/MenuChip"
-        android:visibility="gone" />
-
-</LinearLayout>
\ No newline at end of file
diff --git a/chrome/android/java/res/menu/main_menu.xml b/chrome/android/java/res/menu/main_menu.xml
index b955ddd..357b392 100644
--- a/chrome/android/java/res/menu/main_menu.xml
+++ b/chrome/android/java/res/menu/main_menu.xml
@@ -52,28 +52,12 @@
         <item android:id="@+id/open_history_menu_id"
             android:title="@string/menu_history"
             android:icon="@drawable/ic_history_googblue_24dp" />
-        <item android:id="@+id/downloads_row_menu_id"
-            android:title="@null">
-          <menu>
-                <item android:id="@+id/downloads_menu_id"
-                    android:title="@string/menu_downloads"
-                    android:icon="@drawable/infobar_download_complete" />
-                <item android:id="@+id/offline_page_chip_id"
-                    android:title="@string/menu_download"
-                    android:icon="@drawable/ic_file_download_white_24dp" />
-          </menu>
-        </item>
-        <item android:id="@+id/all_bookmarks_row_menu_id"
-            android:title="@null">
-          <menu>
-                <item android:id="@+id/all_bookmarks_menu_id"
-                    android:title="@string/menu_bookmarks"
-                    android:icon="@drawable/btn_star_filled" />
-                <item android:id="@+id/bookmark_this_page_chip_id"
-                    android:title="@string/menu_bookmark"
-                    android:icon="@drawable/btn_star" />
-          </menu>
-        </item>
+        <item android:id="@+id/downloads_menu_id"
+            android:title="@string/menu_downloads"
+            android:icon="@drawable/infobar_download_complete" />
+        <item android:id="@+id/all_bookmarks_menu_id"
+            android:title="@string/menu_bookmarks"
+            android:icon="@drawable/btn_star_filled" />
         <item android:id="@+id/recent_tabs_menu_id"
             android:title="@string/menu_recent_tabs"
             android:icon="@drawable/devices_black_24dp" />
diff --git a/chrome/android/java/res/values/colors.xml b/chrome/android/java/res/values/colors.xml
index 499a312..226b4ef 100644
--- a/chrome/android/java/res/values/colors.xml
+++ b/chrome/android/java/res/values/colors.xml
@@ -108,6 +108,9 @@
     <color name="offline_indicator_offline_color">@android:color/black</color>
     <color name="offline_indicator_back_online_color">@color/default_bg_color_blue</color>
 
+    <!-- Material colorSurface  See See https://crbug.com/1186712 -->
+    <color name="material_color_surface">@color/default_bg_color_elev_4</color>
+
     <!-- Other colors -->
     <color name="media_viewer_bg">#000000</color>
     <color name="image_viewer_bg">#0E0E0E</color>
diff --git a/chrome/android/java/res/values/dimens.xml b/chrome/android/java/res/values/dimens.xml
index 4912c3c..26f588f 100644
--- a/chrome/android/java/res/values/dimens.xml
+++ b/chrome/android/java/res/values/dimens.xml
@@ -295,8 +295,7 @@
          to both the top and bottom bounds to bring the 48dp modern_toolbar_background_size to 56dp
          (matches toolbar_height_no_shadow). -->
     <dimen name="ntp_search_box_bounds_vertical_inset_modern">-4dp</dimen>
-    <dimen name="ntp_wide_card_lateral_margins">48dp</dimen>
-    <dimen name="ntp_wide_card_lateral_margins_v2">36dp</dimen>
+    <dimen name="ntp_wide_card_lateral_margins">36dp</dimen>
     <dimen name="snippets_article_header_height">40dp</dimen>
     <dimen name="snippets_article_header_menu_size">48dp</dimen>
     <dimen name="feed_v2_header_menu_width">18dp</dimen>
@@ -305,9 +304,9 @@
     <!-- This should match |ntp_header_lateral_margins_v2|. -->
     <dimen name="ntp_header_lateral_margins_v2">16dp</dimen>
     <!-- This is in sp because we want the icon to scale with the TextView it sits alongside. -->
-    <dimen name="content_suggestions_card_modern_margin">12dp</dimen>
-    <dimen name="content_suggestions_card_modern_margin_v2">0dp</dimen>
-    <dimen name="content_suggestions_card_modern_padding_v2">16dp</dimen>
+    <dimen name="content_suggestions_card_modern_margin">0dp</dimen>
+    <dimen name="content_suggestions_card_modern_padding">16dp</dimen>
+    <dimen name="content_suggestions_card_bottom_margin">12dp</dimen>
     <dimen name="md_incognito_ntp_line_spacing">6sp</dimen>
     <dimen name="md_incognito_ntp_padding_left">16dp</dimen>
     <dimen name="cryptid_height_in_logo_wrapper">60dp</dimen>
diff --git a/chrome/android/java/res/values/styles.xml b/chrome/android/java/res/values/styles.xml
index 961a3b16..9e555df 100644
--- a/chrome/android/java/res/values/styles.xml
+++ b/chrome/android/java/res/values/styles.xml
@@ -66,7 +66,10 @@
          navigation bar colors from being applied -->
     <style name="Theme.Chromium.SearchActivity" parent="Base.Theme.Chromium.WithWindowAnimation" />
 
-    <style name="Base.Theme.Chromium.TabbedMode" parent="Theme.Chromium.WithWindowAnimation" />
+    <style name="Base.Theme.Chromium.TabbedMode" parent="Theme.Chromium.WithWindowAnimation">
+        <!-- Attributes for material design component. See https://crbug.com/1186712 -->
+        <item name="colorSurface">@color/material_color_surface</item>
+    </style>
     <style name="Theme.Chromium.TabbedMode" parent="Base.Theme.Chromium.TabbedMode" />
 
     <!-- Web app themes -->
@@ -548,7 +551,7 @@
 
     <!-- Content and Site Suggestions -->
     <style name="SuggestionCardModern" parent="Card">
-        <item name="android:layout_marginBottom">@dimen/content_suggestions_card_modern_margin</item>
+        <item name="android:layout_marginBottom">@dimen/content_suggestions_card_bottom_margin</item>
     </style>
 
     <!-- Password manager settings page -->
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 3af5ba67..581b085 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ChromeTabbedActivity.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ChromeTabbedActivity.java
@@ -86,7 +86,7 @@
 import org.chromium.chrome.browser.download.DownloadOpenSource;
 import org.chromium.chrome.browser.download.DownloadUtils;
 import org.chromium.chrome.browser.feature_engagement.TrackerFactory;
-import org.chromium.chrome.browser.feed.FeedV2;
+import org.chromium.chrome.browser.feed.v2.FeedStreamSurface;
 import org.chromium.chrome.browser.firstrun.FirstRunSignInProcessor;
 import org.chromium.chrome.browser.flags.ActivityType;
 import org.chromium.chrome.browser.flags.ChromeFeatureList;
@@ -770,7 +770,7 @@
     private void maybeGetFeedAppLifecycleAndMaybeCreatePageViewObserver() {
         try (TraceEvent e = TraceEvent.scoped("ChromeTabbedActivity."
                      + "maybeGetFeedAppLifecycleAndMaybeCreatePageViewObserver")) {
-            FeedV2.startup();
+            FeedStreamSurface.startup();
 
             if (UsageStatsService.isEnabled()) {
                 UsageStatsService.getInstance().createPageViewObserver(
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/app/ChromeActivity.java b/chrome/android/java/src/org/chromium/chrome/browser/app/ChromeActivity.java
index 5c0257a..ae46341 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/app/ChromeActivity.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/app/ChromeActivity.java
@@ -2264,13 +2264,13 @@
             return false;
         }
 
-        if (id == R.id.bookmark_this_page_id || id == R.id.bookmark_this_page_chip_id) {
+        if (id == R.id.bookmark_this_page_id) {
             addOrEditBookmark(currentTab);
             RecordUserAction.record("MobileMenuAddToBookmarks");
             return true;
         }
 
-        if (id == R.id.offline_page_id || id == R.id.offline_page_chip_id) {
+        if (id == R.id.offline_page_id) {
             DownloadUtils.downloadOfflinePage(this, currentTab);
             RecordUserAction.record("MobileMenuDownloadPage");
             return true;
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/app/appmenu/AppMenuPropertiesDelegateImpl.java b/chrome/android/java/src/org/chromium/chrome/browser/app/appmenu/AppMenuPropertiesDelegateImpl.java
index 3e639eb9..ceb95ead 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/app/appmenu/AppMenuPropertiesDelegateImpl.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/app/appmenu/AppMenuPropertiesDelegateImpl.java
@@ -11,7 +11,6 @@
 import android.os.Bundle;
 import android.os.SystemClock;
 import android.text.TextUtils;
-import android.util.Pair;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.SubMenu;
@@ -42,7 +41,6 @@
 import org.chromium.chrome.browser.flags.CachedFeatureFlags;
 import org.chromium.chrome.browser.flags.ChromeFeatureList;
 import org.chromium.chrome.browser.flags.ChromeSwitches;
-import org.chromium.chrome.browser.flags.StringCachedFieldTrialParameter;
 import org.chromium.chrome.browser.image_descriptions.ImageDescriptionsController;
 import org.chromium.chrome.browser.incognito.IncognitoUtils;
 import org.chromium.chrome.browser.multiwindow.MultiWindowModeStateDispatcher;
@@ -69,23 +67,13 @@
 import org.chromium.ui.base.DeviceFormFactor;
 
 import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
 import java.util.List;
-import java.util.Map;
-import java.util.Set;
 
 /**
  * Base implementation of {@link AppMenuPropertiesDelegate} that handles hiding and showing menu
  * items based on activity state.
  */
 public class AppMenuPropertiesDelegateImpl implements AppMenuPropertiesDelegate {
-    public static final StringCachedFieldTrialParameter THREE_BUTTON_ACTION_BAR_VARIATION =
-            new StringCachedFieldTrialParameter(
-                    ChromeFeatureList.TABBED_APP_OVERFLOW_MENU_THREE_BUTTON_ACTIONBAR,
-                    "three_button_action_bar", "");
-
     private static Boolean sItemBookmarkedForTesting;
 
     protected MenuItem mReloadMenuItem;
@@ -105,13 +93,6 @@
     // Keeps track of which menu item was shown when installable app is detected.
     private int mAddAppTitleShown;
 
-    // The keys of the Map are menuitem ids, the first elements in the Pair are menuitem ids,
-    // and the second elements in the Pair are AppMenuSimilarSelectionType. If users first
-    // selected the menuitems in the Pair.first, and then selected a menuitem which is the key
-    // if the Map, then users' selection match the pattern Pair.second.
-    private static final Map<Integer, Pair<Set<Integer>, Integer>> sSimilarSelectedMenuItemMap =
-            createSimilarSelectedMap();
-
     @VisibleForTesting
     @IntDef({MenuGroup.INVALID, MenuGroup.PAGE_MENU, MenuGroup.OVERVIEW_MODE_MENU,
             MenuGroup.START_SURFACE_MODE_MENU, MenuGroup.TABLET_EMPTY_MODE_MENU})
@@ -123,33 +104,6 @@
         int TABLET_EMPTY_MODE_MENU = 3;
     }
 
-    @IntDef({ThreeButtonActionBarType.DISABLED, ThreeButtonActionBarType.ACTION_CHIP_VIEW,
-            ThreeButtonActionBarType.DESTINATION_CHIP_VIEW,
-            ThreeButtonActionBarType.DEPRECATED_ADD_TO_OPTION})
-    @interface ThreeButtonActionBarType {
-        int DISABLED = 0;
-        int ACTION_CHIP_VIEW = 1;
-        int DESTINATION_CHIP_VIEW = 2;
-        int DEPRECATED_ADD_TO_OPTION = 3;
-    }
-
-    /**
-     * Keep this list sync with AppMenuSimilarSelectionType in enums.xml.
-     */
-    @IntDef({AppMenuSimilarSelectionType.NO_MATCH,
-            AppMenuSimilarSelectionType.BOOKMARK_PAGE_THEN_ALL_BOOKMARKS,
-            AppMenuSimilarSelectionType.ALL_BOOKMARKS_THEN_BOOKMARK_PAGE,
-            AppMenuSimilarSelectionType.DOWNLOAD_PAGE_THEN_ALL_DOWNLOADS,
-            AppMenuSimilarSelectionType.ALL_DOWNLOADS_THEN_DOWNLOAD_PAGE})
-    @interface AppMenuSimilarSelectionType {
-        int NO_MATCH = -1;
-        int BOOKMARK_PAGE_THEN_ALL_BOOKMARKS = 0;
-        int ALL_BOOKMARKS_THEN_BOOKMARK_PAGE = 1;
-        int DOWNLOAD_PAGE_THEN_ALL_DOWNLOADS = 2;
-        int ALL_DOWNLOADS_THEN_DOWNLOAD_PAGE = 3;
-        int NUM_ENTRIES = 4;
-    }
-
     protected @Nullable OverviewModeBehavior mOverviewModeBehavior;
     protected BookmarkBridge mBookmarkBridge;
     protected Runnable mAppMenuInvalidator;
@@ -214,7 +168,6 @@
         customViewBinders.add(new ManagedByMenuItemViewBinder());
         customViewBinders.add(new IncognitoMenuItemViewBinder());
         customViewBinders.add(new DividerLineMenuItemViewBinder());
-        customViewBinders.add(new ChipViewMenuItemViewBinder(getThreeButtonActionBarType()));
         return customViewBinders;
     }
 
@@ -309,26 +262,12 @@
             loadingStateChanged(currentTab.isLoading());
 
             MenuItem bookmarkMenuItem = actionBar.findItem(R.id.bookmark_this_page_id);
-            if (shouldShowThreeButtonActionBar()) {
-                actionBar.removeItem(R.id.bookmark_this_page_id);
-            } else {
-                updateBookmarkMenuItem(bookmarkMenuItem, currentTab);
-            }
+            updateBookmarkMenuItem(bookmarkMenuItem, currentTab);
 
             MenuItem offlineMenuItem = actionBar.findItem(R.id.offline_page_id);
-            if (offlineMenuItem != null) {
-                if (shouldShowThreeButtonActionBar()) {
-                    actionBar.removeItem(R.id.offline_page_id);
-                } else {
-                    offlineMenuItem.setEnabled(shouldEnableDownloadPage(currentTab));
-                }
-            }
+            offlineMenuItem.setEnabled(shouldEnableDownloadPage(currentTab));
 
-            if (shouldShowThreeButtonActionBar()) {
-                assert actionBar.size() == 3;
-            } else {
-                assert actionBar.size() == 5;
-            }
+            assert actionBar.size() == 5;
         }
 
         mUpdateMenuItemVisible = shouldShowUpdateMenuItem();
@@ -340,41 +279,6 @@
 
         menu.findItem(R.id.move_to_other_window_menu_id).setVisible(shouldShowMoveToOtherWindow());
 
-        if (shouldShowThreeButtonActionBar()) {
-            @ThreeButtonActionBarType
-            int threeButtonActionBarType = getThreeButtonActionBarType();
-
-            MenuItem downloadMenuItem =
-                    menu.findItem(R.id.downloads_row_menu_id).getSubMenu().getItem(1);
-            assert downloadMenuItem.getItemId() == R.id.offline_page_chip_id;
-            downloadMenuItem.setEnabled(shouldEnableDownloadPage(currentTab));
-
-            MenuItem bookmarkMenuItem =
-                    menu.findItem(R.id.all_bookmarks_row_menu_id).getSubMenu().getItem(1);
-            assert bookmarkMenuItem.getItemId() == R.id.bookmark_this_page_chip_id;
-            updateBookmarkMenuItem(bookmarkMenuItem, currentTab);
-
-            // Update titles for ChipView menu items.
-            if (threeButtonActionBarType == ThreeButtonActionBarType.ACTION_CHIP_VIEW) {
-                downloadMenuItem.setTitle(R.string.add);
-                if (bookmarkMenuItem.isChecked()) {
-                    bookmarkMenuItem.setTitle(R.string.bookmark_item_edit);
-                } else {
-                    bookmarkMenuItem.setTitle(R.string.add);
-                }
-            } else if (threeButtonActionBarType == ThreeButtonActionBarType.DESTINATION_CHIP_VIEW) {
-                MenuItem allDownloadMenuItem =
-                        menu.findItem(R.id.downloads_row_menu_id).getSubMenu().getItem(0);
-                assert allDownloadMenuItem.getItemId() == R.id.downloads_menu_id;
-                allDownloadMenuItem.setTitle(R.string.all);
-
-                MenuItem allBookmarkMenuItem =
-                        menu.findItem(R.id.all_bookmarks_row_menu_id).getSubMenu().getItem(0);
-                assert allBookmarkMenuItem.getItemId() == R.id.all_bookmarks_menu_id;
-                allBookmarkMenuItem.setTitle(R.string.all);
-            }
-        }
-
         // Don't allow either "chrome://" pages or interstitial pages to be shared.
         menu.findItem(R.id.share_row_menu_id).setVisible(mShareUtils.shouldEnableShare(currentTab));
 
@@ -456,13 +360,6 @@
                         && item.getItemId() != R.id.update_menu_id) {
                     item.setIcon(null);
                 }
-                // Remove icons for menu items that have submenus.
-                if (item.getItemId() == R.id.downloads_row_menu_id
-                        || item.getItemId() == R.id.all_bookmarks_row_menu_id) {
-                    for (int j = 0; j < item.getSubMenu().size(); ++j) {
-                        item.getSubMenu().getItem(j).setIcon(null);
-                    }
-                }
             }
 
             if (item.getItemId() == R.id.new_incognito_tab_menu_id && item.isVisible()) {
@@ -841,94 +738,4 @@
     static void setPageBookmarkedForTesting(Boolean bookmarked) {
         sItemBookmarkedForTesting = bookmarked;
     }
-
-    private static boolean shouldShowThreeButtonActionBar() {
-        return CachedFeatureFlags.isEnabled(
-                ChromeFeatureList.TABBED_APP_OVERFLOW_MENU_THREE_BUTTON_ACTIONBAR);
-    }
-
-    /**
-     * @return The type of three button action bar should be shown.
-     */
-    private static @ThreeButtonActionBarType int getThreeButtonActionBarType() {
-        if (shouldShowThreeButtonActionBar()) {
-            if (THREE_BUTTON_ACTION_BAR_VARIATION.getValue().equals("action_chip_view")) {
-                return ThreeButtonActionBarType.ACTION_CHIP_VIEW;
-            } else if (THREE_BUTTON_ACTION_BAR_VARIATION.getValue().equals(
-                               "destination_chip_view")) {
-                return ThreeButtonActionBarType.DESTINATION_CHIP_VIEW;
-            }
-        }
-        return ThreeButtonActionBarType.DISABLED;
-    }
-
-    /**
-     * @return The "download" menu items id in the app menu.
-     */
-    public static int getOfflinePageId() {
-        @ThreeButtonActionBarType
-        int type = getThreeButtonActionBarType();
-        if (type == ThreeButtonActionBarType.ACTION_CHIP_VIEW
-                || type == ThreeButtonActionBarType.DESTINATION_CHIP_VIEW) {
-            return R.id.offline_page_chip_id;
-        }
-        return R.id.offline_page_id;
-    }
-
-    @Override
-    public boolean recordAppMenuSimilarSelectionIfNeeded(
-            int previousMenuItemId, int currentMenuItemId) {
-        @AppMenuSimilarSelectionType
-        int pattern = findSimilarSelectionPattern(previousMenuItemId, currentMenuItemId);
-        if (pattern == AppMenuSimilarSelectionType.NO_MATCH) {
-            return false;
-        }
-
-        RecordHistogram.recordEnumeratedHistogram("Mobile.AppMenu.SimilarSelection", pattern,
-                AppMenuSimilarSelectionType.NUM_ENTRIES);
-        return true;
-    }
-
-    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
-    public @AppMenuSimilarSelectionType int findSimilarSelectionPattern(
-            int previousMenuItemId, int currentMenuItemId) {
-        Pair<Set<Integer>, Integer> menuItemToSelectType =
-                sSimilarSelectedMenuItemMap.get(currentMenuItemId);
-        if (menuItemToSelectType != null
-                && menuItemToSelectType.first.contains(previousMenuItemId)) {
-            return menuItemToSelectType.second;
-        }
-
-        return AppMenuSimilarSelectionType.NO_MATCH;
-    }
-
-    private static Map<Integer, Pair<Set<Integer>, Integer>> createSimilarSelectedMap() {
-        Map<Integer, Pair<Set<Integer>, Integer>> map = new LinkedHashMap<>();
-        map.put(R.id.all_bookmarks_menu_id,
-                new Pair<Set<Integer>, Integer>(
-                        new HashSet<>(Arrays.asList(
-                                R.id.bookmark_this_page_id, R.id.bookmark_this_page_chip_id)),
-                        AppMenuSimilarSelectionType.BOOKMARK_PAGE_THEN_ALL_BOOKMARKS));
-        map.put(R.id.bookmark_this_page_id,
-                new Pair<Set<Integer>, Integer>(
-                        new HashSet<>(Arrays.asList(R.id.all_bookmarks_menu_id)),
-                        AppMenuSimilarSelectionType.ALL_BOOKMARKS_THEN_BOOKMARK_PAGE));
-        map.put(R.id.bookmark_this_page_chip_id,
-                new Pair<Set<Integer>, Integer>(
-                        new HashSet<>(Arrays.asList(R.id.all_bookmarks_menu_id)),
-                        AppMenuSimilarSelectionType.ALL_BOOKMARKS_THEN_BOOKMARK_PAGE));
-        map.put(R.id.downloads_menu_id,
-                new Pair<Set<Integer>, Integer>(new HashSet<>(Arrays.asList(R.id.offline_page_id,
-                                                        R.id.offline_page_chip_id)),
-                        AppMenuSimilarSelectionType.DOWNLOAD_PAGE_THEN_ALL_DOWNLOADS));
-        map.put(R.id.offline_page_id,
-                new Pair<Set<Integer>, Integer>(
-                        new HashSet<>(Arrays.asList(R.id.downloads_menu_id)),
-                        AppMenuSimilarSelectionType.ALL_DOWNLOADS_THEN_DOWNLOAD_PAGE));
-        map.put(R.id.offline_page_chip_id,
-                new Pair<Set<Integer>, Integer>(
-                        new HashSet<>(Arrays.asList(R.id.downloads_menu_id)),
-                        AppMenuSimilarSelectionType.ALL_DOWNLOADS_THEN_DOWNLOAD_PAGE));
-        return map;
-    }
 }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/app/appmenu/ChipViewMenuItemViewBinder.java b/chrome/android/java/src/org/chromium/chrome/browser/app/appmenu/ChipViewMenuItemViewBinder.java
deleted file mode 100644
index e4c5c1dc..0000000
--- a/chrome/android/java/src/org/chromium/chrome/browser/app/appmenu/ChipViewMenuItemViewBinder.java
+++ /dev/null
@@ -1,141 +0,0 @@
-// Copyright 2020 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-package org.chromium.chrome.browser.app.appmenu;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.view.LayoutInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.annotation.ColorRes;
-import androidx.annotation.Nullable;
-import androidx.appcompat.content.res.AppCompatResources;
-
-import org.chromium.chrome.R;
-import org.chromium.chrome.browser.app.appmenu.AppMenuPropertiesDelegateImpl.ThreeButtonActionBarType;
-import org.chromium.chrome.browser.ui.appmenu.AppMenuClickHandler;
-import org.chromium.chrome.browser.ui.appmenu.CustomViewBinder;
-import org.chromium.components.browser_ui.widget.highlight.ViewHighlighter;
-import org.chromium.components.browser_ui.widget.text.TextViewWithCompoundDrawables;
-import org.chromium.ui.widget.ChipView;
-
-/**
- * A custom view binder used to bind a menu item with an optional chip shown after the main menu
- * item text.
- */
-class ChipViewMenuItemViewBinder implements CustomViewBinder {
-    private static final int CHIP_VIEW_ITEM_VIEW_TYPE = 0;
-    private final @ThreeButtonActionBarType int mThreeButtonActionBarType;
-
-    ChipViewMenuItemViewBinder(@ThreeButtonActionBarType int threeButtonActionBarType) {
-        mThreeButtonActionBarType = threeButtonActionBarType;
-    }
-
-    @Override
-    public int getViewTypeCount() {
-        return 1;
-    }
-
-    @Override
-    public int getItemViewType(int id) {
-        return (id == R.id.downloads_row_menu_id || id == R.id.all_bookmarks_row_menu_id)
-                ? CHIP_VIEW_ITEM_VIEW_TYPE
-                : CustomViewBinder.NOT_HANDLED;
-    }
-
-    @Override
-    public View getView(MenuItem item, @Nullable View convertView, ViewGroup parent,
-            LayoutInflater inflater, AppMenuClickHandler appMenuClickHandler,
-            @Nullable Integer highlightedItemId) {
-        assert item.getItemId() == R.id.downloads_row_menu_id
-                || item.getItemId() == R.id.all_bookmarks_row_menu_id;
-
-        final MenuItem destinationItem = item.getSubMenu().getItem(0);
-        final MenuItem actionItem = item.getSubMenu().getItem(1);
-
-        // By default, when no experiments are enabled, the menu item should go to the destination
-        // (downloads manager or bookmarks manager).
-        MenuItem mainMenuItem = destinationItem;
-        MenuItem chipViewMenuItem = null;
-
-        if (mThreeButtonActionBarType == ThreeButtonActionBarType.ACTION_CHIP_VIEW) {
-            // If the action chip view variant is enabled, the chip view will be the add bookmark or
-            // add download action.
-            chipViewMenuItem = actionItem;
-        } else if (mThreeButtonActionBarType == ThreeButtonActionBarType.DESTINATION_CHIP_VIEW) {
-            // Else, if the destination chip view variant is enabled, the chip view will be the
-            // destination and the menu item text will be the action.
-            mainMenuItem = actionItem;
-            chipViewMenuItem = destinationItem;
-        } // else the menu item will not have chip view as other normal menu items.
-
-        ChipViewMenuItemViewHolder holder;
-        if (convertView == null || !(convertView.getTag() instanceof ChipViewMenuItemViewHolder)) {
-            holder = new ChipViewMenuItemViewHolder();
-            convertView = inflater.inflate(R.layout.chip_view_menu_item, parent, false);
-            holder.title = convertView.findViewById(R.id.title);
-            holder.chipView = convertView.findViewById(R.id.chip_view);
-            convertView.setTag(holder);
-        } else {
-            holder = (ChipViewMenuItemViewHolder) convertView.getTag();
-        }
-
-        holder.title.setCompoundDrawablesRelative(mainMenuItem.getIcon(), null, null, null);
-        holder.title.setText(mainMenuItem.getTitle());
-        holder.title.setEnabled(mainMenuItem.isEnabled());
-        final MenuItem finalMainMenuItem = mainMenuItem;
-        holder.title.setOnClickListener(v -> appMenuClickHandler.onItemClick(finalMainMenuItem));
-        @ColorRes
-        int theme = mainMenuItem.isChecked() ? R.color.blue_mode_tint
-                                             : R.color.default_icon_color_secondary_tint_list;
-        holder.title.setDrawableTintColor(
-                AppCompatResources.getColorStateList(convertView.getContext(), theme));
-
-        if (chipViewMenuItem != null) {
-            holder.chipView.setVisibility(View.VISIBLE);
-            holder.chipView.getPrimaryTextView().setText(chipViewMenuItem.getTitle());
-            holder.chipView.setEnabled(chipViewMenuItem.isEnabled());
-            final MenuItem finalChipViewMenuItem = chipViewMenuItem;
-            holder.chipView.setOnClickListener(
-                    v -> appMenuClickHandler.onItemClick(finalChipViewMenuItem));
-
-            if (highlightedItemId != null && chipViewMenuItem.getItemId() == highlightedItemId) {
-                ViewHighlighter.turnOnRectangularHighlight(
-                        holder.chipView, holder.chipView.getCornerRadius());
-            } else {
-                ViewHighlighter.turnOffHighlight(holder.chipView);
-            }
-        }
-
-        if (highlightedItemId != null && mainMenuItem.getItemId() == highlightedItemId) {
-            ViewHighlighter.turnOnRectangularHighlight(holder.title);
-        } else {
-            ViewHighlighter.turnOffHighlight(holder.title);
-        }
-
-        convertView.setEnabled(false);
-
-        return convertView;
-    }
-
-    @Override
-    public boolean supportsEnterAnimation(int id) {
-        return true;
-    }
-
-    @Override
-    public int getPixelHeight(Context context) {
-        TypedArray a = context.obtainStyledAttributes(
-                new int[] {android.R.attr.listPreferredItemHeightSmall});
-        return a.getDimensionPixelSize(0, 0);
-    }
-
-    private static class ChipViewMenuItemViewHolder {
-        public TextViewWithCompoundDrawables title;
-        public ChipView chipView;
-    }
-}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/app/flags/ChromeCachedFlags.java b/chrome/android/java/src/org/chromium/chrome/browser/app/flags/ChromeCachedFlags.java
index 1d37106..1f81a422 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/app/flags/ChromeCachedFlags.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/app/flags/ChromeCachedFlags.java
@@ -7,7 +7,6 @@
 import android.text.TextUtils;
 
 import org.chromium.base.annotations.RemovableInRelease;
-import org.chromium.chrome.browser.app.appmenu.AppMenuPropertiesDelegateImpl;
 import org.chromium.chrome.browser.compositor.layouts.content.TabContentManager;
 import org.chromium.chrome.browser.firstrun.FirstRunUtils;
 import org.chromium.chrome.browser.flags.CachedFeatureFlags;
@@ -81,7 +80,6 @@
                 ChromeFeatureList.TAB_GROUPS_ANDROID,
                 ChromeFeatureList.TAB_GROUPS_CONTINUATION_ANDROID,
                 ChromeFeatureList.TAB_TO_GTS_ANIMATION,
-                ChromeFeatureList.TABBED_APP_OVERFLOW_MENU_THREE_BUTTON_ACTIONBAR,
                 ChromeFeatureList.THEME_REFACTOR_ANDROID,
                 ChromeFeatureList.TOOLBAR_USE_HARDWARE_BITMAP_DRAW,
                 ChromeFeatureList.USE_CHIME_ANDROID_SDK,
@@ -94,7 +92,6 @@
         // clang-format off
         List<CachedFieldTrialParameter> fieldTrialsToCache = Arrays.asList(
                 AdaptiveToolbarFeatures.MODE_PARAM,
-                AppMenuPropertiesDelegateImpl.THREE_BUTTON_ACTION_BAR_VARIATION,
                 ConditionalTabStripUtils.CONDITIONAL_TAB_STRIP_INFOBAR_LIMIT,
                 ConditionalTabStripUtils.CONDITIONAL_TAB_STRIP_INFOBAR_PERIOD,
                 ConditionalTabStripUtils.CONDITIONAL_TAB_STRIP_SESSION_TIME_MS,
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/contextmenu/ChromeContextMenuPopulator.java b/chrome/android/java/src/org/chromium/chrome/browser/contextmenu/ChromeContextMenuPopulator.java
index 04b2d87..8f3e0f4 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/contextmenu/ChromeContextMenuPopulator.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/contextmenu/ChromeContextMenuPopulator.java
@@ -90,7 +90,6 @@
     private final Supplier<ShareDelegate> mShareDelegateSupplier;
     private final ExternalAuthUtils mExternalAuthUtils;
     private final ContextMenuParams mParams;
-    private boolean mEnableLensWithSearchByImageText;
     private boolean mIsLensIntentInProgress;
     private @Nullable UkmRecorder.Bridge mUkmRecorderBridge;
     private ContextMenuNativeDelegate mNativeDelegate;
@@ -452,12 +451,7 @@
                             getSearchByImageMenuItemsToShowAndRecordMetrics(
                                     mParams.getPageUrl(), mItemDelegate.isIncognito());
                     if (imageSearchMenuItemsToShow.get(LENS_SEARCH_MENU_ITEM_KEY)) {
-                        if (LensUtils.useLensWithSearchByImageText()) {
-                            mEnableLensWithSearchByImageText = true;
-                            imageGroup.add(createListItem(Item.SEARCH_BY_IMAGE));
-                        } else {
-                            imageGroup.add(createListItem(Item.SEARCH_WITH_GOOGLE_LENS, true));
-                        }
+                        imageGroup.add(createListItem(Item.SEARCH_WITH_GOOGLE_LENS, true));
                         maybeRecordUkmLensShown();
                     } else if (imageSearchMenuItemsToShow.get(SEARCH_BY_IMAGE_MENU_ITEM_KEY)) {
                         imageGroup.add(createListItem(Item.SEARCH_BY_IMAGE));
@@ -690,14 +684,8 @@
             prefManager.writeBoolean(
                     ChromePreferenceKeys.CONTEXT_MENU_SEARCH_WITH_GOOGLE_LENS_CLICKED, true);
         } else if (itemId == R.id.contextmenu_search_by_image) {
-            if (mEnableLensWithSearchByImageText) {
-                recordContextMenuSelection(ContextMenuUma.Action.SEARCH_WITH_GOOGLE_LENS);
-                searchWithGoogleLens(LensEntryPoint.CONTEXT_MENU_SEARCH_MENU_ITEM,
-                        /*requiresConfirmation=*/false);
-            } else {
-                recordContextMenuSelection(ContextMenuUma.Action.SEARCH_BY_IMAGE);
-                mNativeDelegate.searchForImage();
-            }
+            recordContextMenuSelection(ContextMenuUma.Action.SEARCH_BY_IMAGE);
+            mNativeDelegate.searchForImage();
         } else if (itemId == R.id.contextmenu_shop_similar_products) {
             recordContextMenuSelection(ContextMenuUma.Action.SHOP_SIMILAR_PRODUCTS);
             searchWithGoogleLens(
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/messages/MessageContainerCoordinator.java b/chrome/android/java/src/org/chromium/chrome/browser/messages/MessageContainerCoordinator.java
index 97c6f45e..0e1a62f3 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/messages/MessageContainerCoordinator.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/messages/MessageContainerCoordinator.java
@@ -64,9 +64,8 @@
     public int getMessageMaxTranslation() {
         // The max translation is message height + message shadow + controls height (adjusted for
         // Message container offsets)
-        final int messageHeightWithShadow = mContainer.findViewById(R.id.message_banner).getHeight()
-                + mContainer.getResources().getDimensionPixelOffset(
-                        R.dimen.message_shadow_top_margin);
+        final int messageHeightWithShadow =
+                mContainer.getMessageBannerHeight() + mContainer.getMessageShadowTopMargin();
         return messageHeightWithShadow + getContainerTopOffset();
     }
 
@@ -87,6 +86,6 @@
         final Resources res = mContainer.getResources();
         return mControlsManager.getContentOffset()
                 - res.getDimensionPixelOffset(R.dimen.message_bubble_inset)
-                - res.getDimensionPixelOffset(R.dimen.message_shadow_top_margin);
+                - mContainer.getMessageShadowTopMargin();
     }
 }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ntp/NewTabPage.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/NewTabPage.java
index e415c07..a001c91 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ntp/NewTabPage.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/NewTabPage.java
@@ -35,7 +35,6 @@
 import org.chromium.chrome.browser.download.DownloadManagerService;
 import org.chromium.chrome.browser.feature_engagement.TrackerFactory;
 import org.chromium.chrome.browser.feed.FeedSurfaceCoordinator;
-import org.chromium.chrome.browser.feed.FeedV1ActionOptions;
 import org.chromium.chrome.browser.feed.NtpStreamLifecycleManager;
 import org.chromium.chrome.browser.feed.StreamLifecycleManager;
 import org.chromium.chrome.browser.feed.shared.FeedSurfaceDelegate;
@@ -55,7 +54,6 @@
 import org.chromium.chrome.browser.query_tiles.QueryTileSection.QueryInfo;
 import org.chromium.chrome.browser.search_engines.TemplateUrlServiceFactory;
 import org.chromium.chrome.browser.share.ShareDelegate;
-import org.chromium.chrome.browser.suggestions.SuggestionsDependencyFactory;
 import org.chromium.chrome.browser.suggestions.SuggestionsMetrics;
 import org.chromium.chrome.browser.suggestions.SuggestionsNavigationDelegate;
 import org.chromium.chrome.browser.suggestions.SuggestionsUiDelegateImpl;
@@ -297,8 +295,6 @@
         mNewTabPageUma = uma;
         Profile profile = Profile.fromWebContents(mTab.getWebContents());
 
-        SuggestionsDependencyFactory depsFactory = SuggestionsDependencyFactory.getInstance();
-
         SuggestionsNavigationDelegate navigationDelegate = new SuggestionsNavigationDelegate(
                 activity, profile, nativePageHost, tabModelSelector, mTab);
         mNewTabPageManager = new NewTabPageManagerImpl(
@@ -349,8 +345,8 @@
         mActivityLifecycleDispatcher.register(mLifecycleObserver);
 
         updateSearchProviderHasLogo();
-        initializeMainView(activity, windowAndroid, snackbarManager, tabModelSelector, uma,
-                isInNightMode, bottomSheetController, shareDelegateSupplier);
+        initializeMainView(activity, windowAndroid, snackbarManager, uma, isInNightMode,
+                bottomSheetController, shareDelegateSupplier);
 
         mBrowserControlsStateProvider = browserControlsStateProvider;
         getView().addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@@ -395,15 +391,14 @@
      * @param activity The activity used to initialize the view.
      * @param windowAndroid Provides the current active tab.
      * @param snackbarManager {@link SnackbarManager} object.
-     * @param tabModelSelector {@link TabModelSelector} object.
      * @param uma {@link NewTabPageUma} object recording user metrics.
      * @param isInNightMode {@code true} if the night mode setting is on.
      * @param bottomSheetController The controller for bottom sheets.  Used by the feed.
      * @param shareDelegateSupplier Supplies a delegate used to open SharingHub.
      */
     protected void initializeMainView(Activity activity, WindowAndroid windowAndroid,
-            SnackbarManager snackbarManager, TabModelSelector tabModelSelector, NewTabPageUma uma,
-            boolean isInNightMode, BottomSheetController bottomSheetController,
+            SnackbarManager snackbarManager, NewTabPageUma uma, boolean isInNightMode,
+            BottomSheetController bottomSheetController,
             ObservableSupplier<ShareDelegate> shareDelegateSupplier) {
         Profile profile = Profile.fromWebContents(mTab.getWebContents());
 
@@ -419,13 +414,12 @@
                     R.layout.new_tab_page_feed_v2_expandable_header, null, false);
         }
 
-        mFeedSurfaceProvider =
-                new FeedSurfaceCoordinator(activity, snackbarManager, tabModelSelector,
-                        windowAndroid, new SnapScrollHelper(mNewTabPageManager, mNewTabPageLayout),
-                        mNewTabPageLayout, sectionHeaderView, new FeedV1ActionOptions(),
-                        isInNightMode, this, mNewTabPageManager.getNavigationDelegate(), profile,
-                        /* isPlaceholderShownInitially= */ false, bottomSheetController,
-                        shareDelegateSupplier, /* externalScrollableContainerDelegate= */ null);
+        mFeedSurfaceProvider = new FeedSurfaceCoordinator(activity, snackbarManager, windowAndroid,
+                new SnapScrollHelper(mNewTabPageManager, mNewTabPageLayout), mNewTabPageLayout,
+                sectionHeaderView, isInNightMode, this, mNewTabPageManager.getNavigationDelegate(),
+                profile,
+                /* isPlaceholderShownInitially= */ false, bottomSheetController,
+                shareDelegateSupplier, /* externalScrollableContainerDelegate= */ null);
 
         // Record the timestamp at which the new tab page's construction started.
         uma.trackTimeToFirstDraw(mFeedSurfaceProvider.getView(), mConstructedTimeNs);
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/omnibox/voice/VoiceRecognitionHandler.java b/chrome/android/java/src/org/chromium/chrome/browser/omnibox/voice/VoiceRecognitionHandler.java
index 7a97c475..3aa795f4 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/omnibox/voice/VoiceRecognitionHandler.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/omnibox/voice/VoiceRecognitionHandler.java
@@ -156,6 +156,23 @@
     private final SettingsLauncher mSettingsLauncher;
 
     /**
+     * AudioPermissionState defined in tools/metrics/histograms/enums.xml.
+     *
+     * Do not reorder or remove items, only add new items before NUM_ENTRIES.
+     */
+    @IntDef({AudioPermissionState.GRANTED, AudioPermissionState.DENIED_CAN_ASK_AGAIN,
+            AudioPermissionState.DENIED_CANNOT_ASK_AGAIN})
+    public @interface AudioPermissionState {
+        // Permissions have been granted and won't be requested this time.
+        int GRANTED = 0;
+        int DENIED_CAN_ASK_AGAIN = 1;
+        int DENIED_CANNOT_ASK_AGAIN = 2;
+
+        // Be sure to also update enums.xml when updating these values.
+        int NUM_ENTRIES = 3;
+    }
+
+    /**
      * VoiceInteractionEventSource defined in tools/metrics/histograms/enums.xml.
      *
      * Do not reorder or remove items, only add new items before NUM_ENTRIES.
@@ -728,24 +745,33 @@
      */
     private boolean ensureAudioPermissionGranted(
             Activity activity, WindowAndroid windowAndroid, @VoiceInteractionSource int source) {
-        if (windowAndroid.hasPermission(Manifest.permission.RECORD_AUDIO)) return true;
-
+        if (windowAndroid.hasPermission(Manifest.permission.RECORD_AUDIO)) {
+            recordAudioPermissionStateEvent(AudioPermissionState.GRANTED);
+            return true;
+        }
         // If we don't have permission and also can't ask, then there's no more work left other
         // than telling the delegate to update the mic state.
         if (!windowAndroid.canRequestPermission(Manifest.permission.RECORD_AUDIO)) {
+            recordAudioPermissionStateEvent(AudioPermissionState.DENIED_CANNOT_ASK_AGAIN);
             notifyVoiceAvailabilityImpacted();
             return false;
         }
 
         PermissionCallback callback = (permissions, grantResults) -> {
             if (grantResults.length != 1) {
+                recordAudioPermissionStateEvent(AudioPermissionState.DENIED_CAN_ASK_AGAIN);
                 return;
             }
 
             if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+                // Don't record granted permission here, it will get logged from
+                // within startSystemForVoiceSearch call.
                 startSystemForVoiceSearch(activity, windowAndroid, source);
             } else if (!windowAndroid.canRequestPermission(Manifest.permission.RECORD_AUDIO)) {
+                recordAudioPermissionStateEvent(AudioPermissionState.DENIED_CANNOT_ASK_AGAIN);
                 notifyVoiceAvailabilityImpacted();
+            } else {
+                recordAudioPermissionStateEvent(AudioPermissionState.DENIED_CAN_ASK_AGAIN);
             }
         };
         windowAndroid.requestPermissions(new String[] {Manifest.permission.RECORD_AUDIO}, callback);
@@ -1195,6 +1221,16 @@
                 "VoiceInteraction.AssistantIntent.TranslateExtrasAttached", result);
     }
 
+    /**
+     * Records audio permissions state when a system voice recognition is requested.
+     * @param permissionsState The current RECORD_AUDIO permission state.
+     */
+    @VisibleForTesting
+    protected void recordAudioPermissionStateEvent(@AudioPermissionState int permissionsState) {
+        RecordHistogram.recordEnumeratedHistogram("VoiceInteraction.AudioPermissionEvent",
+                permissionsState, AudioPermissionState.NUM_ENTRIES);
+    }
+
     /** Allows for overriding the TranslateBridgeWrapper for test purposes. */
     @VisibleForTesting
     protected void setTranslateBridgeWrapper(TranslateBridgeWrapper wrapper) {
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/read_later/ReadLaterIPHController.java b/chrome/android/java/src/org/chromium/chrome/browser/read_later/ReadLaterIPHController.java
index ea3c7eb..1ad3c191 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/read_later/ReadLaterIPHController.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/read_later/ReadLaterIPHController.java
@@ -43,10 +43,6 @@
      */
     public void onCopyContextMenuItemClicked() {
         if (!ChromeFeatureList.isEnabled(ChromeFeatureList.READ_LATER)) return;
-        if (ChromeFeatureList.isEnabled(
-                    ChromeFeatureList.TABBED_APP_OVERFLOW_MENU_THREE_BUTTON_ACTIONBAR)) {
-            return;
-        }
         mUserEducationHelper.requestShowIPH(
                 new IPHCommandBuilder(mToolbarMenuButton.getContext().getResources(),
                         FeatureConstants.READ_LATER_APP_MENU_BOOKMARK_THIS_PAGE_FEATURE,
@@ -68,10 +64,6 @@
 
     private void showReadLaterAppMenuBookmarksIPH() {
         if (!ChromeFeatureList.isEnabled(ChromeFeatureList.READ_LATER)) return;
-        if (ChromeFeatureList.isEnabled(
-                    ChromeFeatureList.TABBED_APP_OVERFLOW_MENU_THREE_BUTTON_ACTIONBAR)) {
-            return;
-        }
         mUserEducationHelper.requestShowIPH(
                 new IPHCommandBuilder(mToolbarMenuButton.getContext().getResources(),
                         FeatureConstants.READ_LATER_APP_MENU_BOOKMARKS_FEATURE,
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/share/LensUtils.java b/chrome/android/java/src/org/chromium/chrome/browser/share/LensUtils.java
index ca72df5c..469fe5d7 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/share/LensUtils.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/share/LensUtils.java
@@ -52,8 +52,6 @@
             "minAgsaVersionForDirectIntent";
     private static final String MIN_AGSA_VERSION_DIRECT_INTENT_SDK_FEATURE_PARAM_NAME =
             "minAgsaVersionForDirectIntentSdk";
-    private static final String USE_SEARCH_BY_IMAGE_TEXT_FEATURE_PARAM_NAME =
-            "useSearchByImageText";
     private static final String LENS_SHOPPING_ALLOWLIST_ENTRIES_FEATURE_PARAM_NAME =
             "allowlistEntries";
     private static final String LENS_SHOPPING_URL_PATTERNS_FEATURE_PARAM_NAME =
@@ -503,15 +501,6 @@
     }
 
     /**
-     * Whether to display the lens menu item with the search by image text
-     */
-    public static boolean useLensWithSearchByImageText() {
-        return ChromeFeatureList.getFieldTrialParamByFeatureAsBoolean(
-                ChromeFeatureList.CONTEXT_MENU_SEARCH_WITH_GOOGLE_LENS,
-                USE_SEARCH_BY_IMAGE_TEXT_FEATURE_PARAM_NAME, false);
-    }
-
-    /**
      * Whether to display the lens menu item shop similar products. only one of the
      * 3 params should be set to true: useLensWithShopSimilarProducts,
      * useLensWithShopImageWithGoogleLens and useLensWithShopImageWithGoogleLens.
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/toolbar/ToolbarButtonInProductHelpController.java b/chrome/android/java/src/org/chromium/chrome/browser/toolbar/ToolbarButtonInProductHelpController.java
index 63be0dd..aa80a0d 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/toolbar/ToolbarButtonInProductHelpController.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/toolbar/ToolbarButtonInProductHelpController.java
@@ -14,7 +14,6 @@
 import org.chromium.base.supplier.Supplier;
 import org.chromium.base.task.PostTask;
 import org.chromium.chrome.R;
-import org.chromium.chrome.browser.app.appmenu.AppMenuPropertiesDelegateImpl;
 import org.chromium.chrome.browser.datareduction.DataReductionSavingsMilestonePromo;
 import org.chromium.chrome.browser.download.DownloadUtils;
 import org.chromium.chrome.browser.feature_engagement.ScreenshotMonitor;
@@ -291,10 +290,7 @@
                 new IPHCommandBuilder(mActivity.getResources(), featureName,
                         R.string.iph_download_page_for_offline_usage_text,
                         R.string.iph_download_page_for_offline_usage_accessibility_text)
-                        .setOnShowCallback(
-                                ()
-                                        -> turnOnHighlightForMenuItem(
-                                                AppMenuPropertiesDelegateImpl.getOfflinePageId()))
+                        .setOnShowCallback(() -> turnOnHighlightForMenuItem(R.id.offline_page_id))
                         .setOnDismissCallback(this::turnOffHighlightForMenuItem)
                         .setAnchorView(mMenuButtonAnchorView)
                         .build());
@@ -336,7 +332,7 @@
         }
 
         Integer menuItemId = DownloadUtils.isAllowedToDownloadPage(mCurrentTabSupplier.get())
-                ? AppMenuPropertiesDelegateImpl.getOfflinePageId()
+                ? R.id.offline_page_id
                 : R.id.downloads_menu_id;
 
         mUserEducationHelper.requestShowIPH(
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/app/appmenu/TabbedAppMenuTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/app/appmenu/TabbedAppMenuTest.java
index a91d8a5c..9653d50 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/app/appmenu/TabbedAppMenuTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/app/appmenu/TabbedAppMenuTest.java
@@ -10,7 +10,6 @@
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
-import android.widget.LinearLayout;
 import android.widget.ListView;
 
 import androidx.test.filters.SmallTest;
@@ -36,7 +35,6 @@
 import org.chromium.chrome.browser.ChromeTabbedActivity;
 import org.chromium.chrome.browser.compositor.layouts.EmptyOverviewModeObserver;
 import org.chromium.chrome.browser.compositor.layouts.OverviewModeBehavior;
-import org.chromium.chrome.browser.flags.ChromeFeatureList;
 import org.chromium.chrome.browser.flags.ChromeSwitches;
 import org.chromium.chrome.browser.ui.appmenu.AppMenuHandler;
 import org.chromium.chrome.browser.ui.appmenu.AppMenuTestSupport;
@@ -45,8 +43,6 @@
 import org.chromium.chrome.test.util.ActivityUtils;
 import org.chromium.chrome.test.util.ChromeRenderTestRule;
 import org.chromium.chrome.test.util.ChromeTabUtils;
-import org.chromium.chrome.test.util.browser.Features.DisableFeatures;
-import org.chromium.chrome.test.util.browser.Features.EnableFeatures;
 import org.chromium.content_public.browser.UiThreadTaskTraits;
 import org.chromium.content_public.browser.test.util.TestThreadUtils;
 import org.chromium.ui.test.util.UiRestriction;
@@ -243,7 +239,6 @@
     @Test
     @SmallTest
     @Feature({"Browser", "Main", "Bookmark", "RenderTest"})
-    @DisableFeatures({ChromeFeatureList.TABBED_APP_OVERFLOW_MENU_THREE_BUTTON_ACTIONBAR})
     @Restriction(UiRestriction.RESTRICTION_TYPE_PHONE)
     public void testBookmarkMenuItem() throws IOException {
         MenuItem bookmarkStar =
@@ -275,92 +270,6 @@
     @Test
     @SmallTest
     @Feature({"Browser", "Main", "RenderTest"})
-    @Restriction(UiRestriction.RESTRICTION_TYPE_PHONE)
-    @EnableFeatures({ChromeFeatureList.TABBED_APP_OVERFLOW_MENU_THREE_BUTTON_ACTIONBAR + "<Study"})
-    @CommandLineFlags.Add({"force-fieldtrials=Study/Group",
-            "force-fieldtrial-params=Study.Group:three_button_action_bar/action_chip_view"})
-    public void
-    testActionChipViewMenuItem() throws IOException {
-        LinearLayout actionBar = (LinearLayout) getListView().getChildAt(0);
-        Assert.assertEquals(3, actionBar.getChildCount());
-        mRenderTestRule.render(
-                getListView().getChildAt(0), "tinted_rounded_corner_icon_row_three_buttons");
-
-        int downloadRowIndex = findIndexOfMenuItemById(R.id.downloads_row_menu_id);
-        Assert.assertNotEquals("No download row found.", -1, downloadRowIndex);
-        mRenderTestRule.render(getListView().getChildAt(downloadRowIndex),
-                "download_row_rounded_action_chip_view");
-
-        MenuItem bookmarkRow = AppMenuTestSupport.getMenu(mActivityTestRule.getAppMenuCoordinator())
-                                       .findItem(R.id.all_bookmarks_row_menu_id);
-        MenuItem bookmarkMenuItem = bookmarkRow.getSubMenu().getItem(1);
-        Assert.assertFalse("Bookmark item should not be checked.", bookmarkMenuItem.isChecked());
-        int bookmarkRowIndex = findIndexOfMenuItemById(R.id.all_bookmarks_row_menu_id);
-        Assert.assertTrue("No bookmark row found.", bookmarkRowIndex != -1);
-        mRenderTestRule.render(getListView().getChildAt(bookmarkRowIndex),
-                "bookmark_row_rounded_action_chip_view");
-
-        TestThreadUtils.runOnUiThreadBlocking(() -> mAppMenuHandler.hideAppMenu());
-        AppMenuPropertiesDelegateImpl.setPageBookmarkedForTesting(true);
-        showAppMenuAndAssertMenuShown();
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
-
-        bookmarkRow = AppMenuTestSupport.getMenu(mActivityTestRule.getAppMenuCoordinator())
-                              .findItem(R.id.all_bookmarks_row_menu_id);
-        bookmarkMenuItem = bookmarkRow.getSubMenu().getItem(1);
-        Assert.assertTrue("Bookmark item should be checked.", bookmarkMenuItem.isChecked());
-        mRenderTestRule.render(getListView().getChildAt(bookmarkRowIndex),
-                "bookmark_row_rounded_action_chip_view_bookmarked");
-
-        AppMenuPropertiesDelegateImpl.setPageBookmarkedForTesting(null);
-    }
-
-    @Test
-    @SmallTest
-    @Feature({"Browser", "Main", "RenderTest"})
-    @Restriction(UiRestriction.RESTRICTION_TYPE_PHONE)
-    @EnableFeatures({ChromeFeatureList.TABBED_APP_OVERFLOW_MENU_THREE_BUTTON_ACTIONBAR + "<Study"})
-    @CommandLineFlags.Add({"force-fieldtrials=Study/Group",
-            "force-fieldtrial-params=Study.Group:three_button_action_bar/destination_chip_view"})
-    public void
-    testDestinationChipViewMenuItem() throws IOException {
-        LinearLayout actionBar = (LinearLayout) getListView().getChildAt(0);
-        Assert.assertEquals(3, actionBar.getChildCount());
-        mRenderTestRule.render(
-                getListView().getChildAt(0), "tinted_rounded_corner_icon_row_three_buttons");
-
-        int downloadRowIndex = findIndexOfMenuItemById(R.id.downloads_row_menu_id);
-        Assert.assertNotEquals("No download row found.", -1, downloadRowIndex);
-        mRenderTestRule.render(getListView().getChildAt(downloadRowIndex),
-                "download_row_rounded_destination_chip_view");
-
-        MenuItem bookmarkRow = AppMenuTestSupport.getMenu(mActivityTestRule.getAppMenuCoordinator())
-                                       .findItem(R.id.all_bookmarks_row_menu_id);
-        MenuItem bookmarkMenuItem = bookmarkRow.getSubMenu().getItem(1);
-        Assert.assertFalse("Bookmark item should not be checked.", bookmarkMenuItem.isChecked());
-        int bookmarkRowIndex = findIndexOfMenuItemById(R.id.all_bookmarks_row_menu_id);
-        Assert.assertTrue("No bookmark row found.", bookmarkRowIndex != -1);
-        mRenderTestRule.render(getListView().getChildAt(bookmarkRowIndex),
-                "bookmark_row_rounded_destination_chip_view");
-
-        TestThreadUtils.runOnUiThreadBlocking(() -> mAppMenuHandler.hideAppMenu());
-        AppMenuPropertiesDelegateImpl.setPageBookmarkedForTesting(true);
-        showAppMenuAndAssertMenuShown();
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
-
-        bookmarkRow = AppMenuTestSupport.getMenu(mActivityTestRule.getAppMenuCoordinator())
-                              .findItem(R.id.all_bookmarks_row_menu_id);
-        bookmarkMenuItem = bookmarkRow.getSubMenu().getItem(1);
-        Assert.assertTrue("Bookmark item should be checked.", bookmarkMenuItem.isChecked());
-        mRenderTestRule.render(getListView().getChildAt(bookmarkRowIndex),
-                "bookmark_row_rounded_destination_chip_view_bookmarked");
-
-        AppMenuPropertiesDelegateImpl.setPageBookmarkedForTesting(null);
-    }
-
-    @Test
-    @SmallTest
-    @Feature({"Browser", "Main", "RenderTest"})
     public void testDividerLineMenuItem() throws IOException {
         int firstDividerLineIndex = findIndexOfMenuItemById(R.id.divider_line_id);
         Assert.assertTrue("No divider line found.", firstDividerLineIndex != -1);
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/bookmarks/BookmarkTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/bookmarks/BookmarkTest.java
index 45c7657..e22a7dc 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/bookmarks/BookmarkTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/bookmarks/BookmarkTest.java
@@ -1825,9 +1825,18 @@
                         LaunchCauseMetrics.LAUNCH_CAUSE_HISTOGRAM));
     }
 
+    /**
+     * Test that we record Bookmarks.OpenBookmarkManager.PerProfileType when
+     * R.id.all_bookmarks_menu_id is clicked in regular mode.
+     *
+     * Please note that this test doesn't run for tablet because of the way bookmark manager is
+     * opened for tablets via openBookmarkManager test method which circumvents the click of
+     * R.id.all_bookmarks_menu_id, this doesn't happen in actual case and the metric indeed gets
+     * recorded in tablets.
+     */
     @Test
     @MediumTest
-    @DisabledTest(message = "crbug.com/1187193")
+    @Restriction({UiRestriction.RESTRICTION_TYPE_PHONE})
     public void testRecordsHistogramWhenBookmarkManagerOpened_InRegular() throws Throwable {
         Assert.assertEquals(0,
                 RecordHistogram.getHistogramTotalCountForTesting(
@@ -1847,9 +1856,18 @@
                         BrowserProfileType.REGULAR));
     }
 
+    /**
+     * Test that we record Bookmarks.OpenBookmarkManager.PerProfileType when
+     * R.id.all_bookmarks_menu_id is clicked in Incognito mode.
+     *
+     * Please note that this test doesn't run for tablet because of the way bookmark manager is
+     * opened for tablets via openBookmarkManager test method which circumvents the click of
+     * R.id.all_bookmarks_menu_id. This doesn't happen in actual case and the metric indeed gets
+     * recorded in tablets.
+     */
     @Test
     @MediumTest
-    @DisabledTest(message = "crbug.com/1187193")
+    @Restriction({UiRestriction.RESTRICTION_TYPE_PHONE})
     public void testRecordsHistogramWhenBookmarkManagerOpened_InIncognito() throws Throwable {
         Assert.assertEquals(0,
                 RecordHistogram.getHistogramTotalCountForTesting(
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/browserservices/TrustedWebActivityClientTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/browserservices/TrustedWebActivityClientTest.java
index 0bf558d..2b1f9dd 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/browserservices/TrustedWebActivityClientTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/browserservices/TrustedWebActivityClientTest.java
@@ -38,7 +38,7 @@
 import org.chromium.chrome.browser.notifications.NotificationUmaTracker;
 import org.chromium.chrome.browser.notifications.StandardNotificationBuilder;
 import org.chromium.chrome.test.util.browser.Features;
-import org.chromium.chrome.test.util.browser.Features.DisableFeatures;
+import org.chromium.chrome.test.util.browser.Features.EnableFeatures;
 import org.chromium.components.embedder_support.util.Origin;
 import org.chromium.content_public.browser.UiThreadTaskTraits;
 
@@ -64,7 +64,7 @@
  * 4. This sends a Message to ResponseHandler in this class.
  */
 @RunWith(BaseJUnit4ClassRunner.class)
-@DisableFeatures(ChromeFeatureList.USE_NOTIFICATION_COMPAT_BUILDER)
+@EnableFeatures(ChromeFeatureList.USE_NOTIFICATION_COMPAT_BUILDER)
 public class TrustedWebActivityClientTest {
     private static final Uri SCOPE = Uri.parse("https://www.example.com/notifications");
     private static final Origin ORIGIN = Origin.create(SCOPE);
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/contextmenu/RevampedContextMenuTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/contextmenu/RevampedContextMenuTest.java
index 437bf88..cd307c3c 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/contextmenu/RevampedContextMenuTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/contextmenu/RevampedContextMenuTest.java
@@ -329,27 +329,6 @@
     @Test
     @MediumTest
     @Feature({"Browser"})
-    @CommandLineFlags.Add({"enable-features="
-                    + ChromeFeatureList.CONTEXT_MENU_SEARCH_WITH_GOOGLE_LENS + "<FakeStudyName",
-            "force-fieldtrials=FakeStudyName/Enabled",
-            "force-fieldtrial-params=FakeStudyName.Enabled:useSearchByImageText/true"})
-    public void
-    testSearchWithGoogleLensWithSearchByImageTextFiresIntent() throws Throwable {
-        Tab tab = mDownloadTestRule.getActivity().getActivityTab();
-
-        LensUtils.setFakePassableLensEnvironmentForTesting(true);
-        ShareHelper.setIgnoreActivityNotFoundExceptionForTesting(true);
-        hardcodeTestImageForSharing(TEST_JPG_IMAGE_FILE_EXTENSION);
-
-        RevampedContextMenuUtils.selectContextMenuItemWithExpectedIntent(
-                InstrumentationRegistry.getInstrumentation(), mDownloadTestRule.getActivity(), tab,
-                "testImage", R.id.contextmenu_search_by_image,
-                "com.google.android.googlequicksearchbox");
-    }
-
-    @Test
-    @MediumTest
-    @Feature({"Browser"})
     public void testLongPressOnImage() throws TimeoutException {
         checkOpenImageInNewTab("testImage", "/chrome/test/data/android/contextmenu/test_image.png");
     }
@@ -940,28 +919,6 @@
     @Test
     @SmallTest
     @Feature({"Browser", "ContextMenu"})
-    @CommandLineFlags.Add({"enable-features="
-                    + ChromeFeatureList.CONTEXT_MENU_SEARCH_WITH_GOOGLE_LENS + "<FakeStudyName",
-            "force-fieldtrials=FakeStudyName/Enabled",
-            "force-fieldtrial-params=FakeStudyName.Enabled:useSearchByImageText/true"})
-    public void
-    testContextMenuRetrievesImageOptionsLensEnabledSearchByImageText() throws TimeoutException {
-        Tab tab = mDownloadTestRule.getActivity().getActivityTab();
-        RevampedContextMenuCoordinator menu =
-                RevampedContextMenuUtils.openContextMenu(tab, "testImage");
-
-        Integer[] expectedItems = {R.id.contextmenu_save_image,
-                R.id.contextmenu_open_image_in_new_tab, R.id.contextmenu_search_by_image,
-                R.id.contextmenu_share_image, R.id.contextmenu_copy_image};
-        Integer[] featureItems = {R.id.contextmenu_open_image_in_ephemeral_tab};
-        expectedItems =
-                addItemsIf(EphemeralTabCoordinator.isSupported(), expectedItems, featureItems);
-        assertMenuItemsAreEqual(menu, expectedItems);
-    }
-
-    @Test
-    @SmallTest
-    @Feature({"Browser", "ContextMenu"})
     @Policies.Add({ @Policies.Item(key = "DefaultSearchProviderEnabled", string = "false") })
     public void testContextMenuRetrievesImageOptions_NoDefaultSearchEngine()
             throws TimeoutException {
@@ -1049,35 +1006,6 @@
     @Test
     @SmallTest
     @Feature({"Browser", "ContextMenu"})
-    @CommandLineFlags.Add({"enable-features="
-                    + ChromeFeatureList.CONTEXT_MENU_SEARCH_WITH_GOOGLE_LENS + "<FakeStudyName",
-            "force-fieldtrials=FakeStudyName/Enabled",
-            "force-fieldtrial-params=FakeStudyName.Enabled:useSearchByImageText/true"})
-    public void
-    testContextMenuRetrievesImageLinkOptionsSearchLensEnabledSearchByImageText()
-            throws TimeoutException {
-        LensUtils.setFakePassableLensEnvironmentForTesting(true);
-
-        Tab tab = mDownloadTestRule.getActivity().getActivityTab();
-        RevampedContextMenuCoordinator menu =
-                RevampedContextMenuUtils.openContextMenu(tab, "testImageLink");
-
-        Integer[] expectedItems = {R.id.contextmenu_open_in_new_tab,
-                R.id.contextmenu_open_in_incognito_tab, R.id.contextmenu_copy_link_address,
-                R.id.contextmenu_save_link_as, R.id.contextmenu_save_image,
-                R.id.contextmenu_open_image_in_new_tab, R.id.contextmenu_search_by_image,
-                R.id.contextmenu_share_image, R.id.contextmenu_share_link,
-                R.id.contextmenu_copy_image};
-        Integer[] featureItems = {R.id.contextmenu_open_in_ephemeral_tab,
-                R.id.contextmenu_open_image_in_ephemeral_tab};
-        expectedItems =
-                addItemsIf(EphemeralTabCoordinator.isSupported(), expectedItems, featureItems);
-        assertMenuItemsAreEqual(menu, expectedItems);
-    }
-
-    @Test
-    @SmallTest
-    @Feature({"Browser", "ContextMenu"})
     @CommandLineFlags.
     Add({"enable-features=" + ChromeFeatureList.CONTEXT_MENU_GOOGLE_LENS_CHIP + "<FakeStudyName",
             "force-fieldtrials=FakeStudyName/Enabled",
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/notifications/CustomNotificationBuilderTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/notifications/CustomNotificationBuilderTest.java
index fb20a734..903bf77 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/notifications/CustomNotificationBuilderTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/notifications/CustomNotificationBuilderTest.java
@@ -39,7 +39,7 @@
 import org.chromium.chrome.browser.flags.ChromeFeatureList;
 import org.chromium.chrome.browser.notifications.channels.ChromeChannelDefinitions;
 import org.chromium.chrome.test.util.browser.Features;
-import org.chromium.chrome.test.util.browser.Features.DisableFeatures;
+import org.chromium.chrome.test.util.browser.Features.EnableFeatures;
 import org.chromium.components.browser_ui.notifications.NotificationMetadata;
 import org.chromium.components.browser_ui.notifications.PendingIntentProvider;
 import org.chromium.content_public.browser.test.NativeLibraryTestUtils;
@@ -51,7 +51,7 @@
  * Instrumentation unit tests for CustomNotificationBuilder.
  */
 @RunWith(BaseJUnit4ClassRunner.class)
-@DisableFeatures(ChromeFeatureList.USE_NOTIFICATION_COMPAT_BUILDER)
+@EnableFeatures(ChromeFeatureList.USE_NOTIFICATION_COMPAT_BUILDER)
 public class CustomNotificationBuilderTest {
     private static final String NOTIFICATION_TAG = "TestNotificationTag";
     private static final int NOTIFICATION_ID = 99;
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/notifications/StandardNotificationBuilderTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/notifications/StandardNotificationBuilderTest.java
index 19ef0a5..1ba7b2a 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/notifications/StandardNotificationBuilderTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/notifications/StandardNotificationBuilderTest.java
@@ -35,7 +35,7 @@
 import org.chromium.chrome.browser.flags.ChromeFeatureList;
 import org.chromium.chrome.browser.notifications.channels.ChromeChannelDefinitions;
 import org.chromium.chrome.test.util.browser.Features;
-import org.chromium.chrome.test.util.browser.Features.DisableFeatures;
+import org.chromium.chrome.test.util.browser.Features.EnableFeatures;
 import org.chromium.components.browser_ui.notifications.NotificationMetadata;
 import org.chromium.components.browser_ui.notifications.PendingIntentProvider;
 import org.chromium.components.browser_ui.widget.RoundedIconGenerator;
@@ -51,7 +51,7 @@
  */
 @RunWith(BaseJUnit4ClassRunner.class)
 @Batch(Batch.UNIT_TESTS)
-@DisableFeatures(ChromeFeatureList.USE_NOTIFICATION_COMPAT_BUILDER)
+@EnableFeatures(ChromeFeatureList.USE_NOTIFICATION_COMPAT_BUILDER)
 public class StandardNotificationBuilderTest {
     private static final String NOTIFICATION_TAG = "TestNotificationTag";
     private static final int NOTIFICATION_ID = 99;
@@ -141,9 +141,17 @@
         Assert.assertNotNull(
                 NotificationTestUtil.getLargeIconFromNotification(context, notification));
 
-        Assert.assertEquals(Notification.DEFAULT_ALL, notification.defaults);
-        Assert.assertEquals(1, notification.vibrate.length);
-        Assert.assertEquals(100L, notification.vibrate[0]);
+        // On Android O+ the defaults are ignored as vibrate and silent moved to the notification
+        // channel.
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
+                && NotificationBuilderBase.shouldUseCompat()) {
+            Assert.assertEquals(0, notification.defaults);
+        } else {
+            Assert.assertEquals(Notification.DEFAULT_ALL, notification.defaults);
+            Assert.assertEquals(1, notification.vibrate.length);
+            Assert.assertEquals(100L, notification.vibrate[0]);
+        }
+
         Notification.Action[] actions = NotificationTestUtil.getActions(notification);
         Assert.assertEquals(3, actions.length);
         Assert.assertEquals("button 1", NotificationTestUtil.getActionTitle(actions[0]));
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/omnibox/voice/VoiceRecognitionHandlerTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/omnibox/voice/VoiceRecognitionHandlerTest.java
index f07b48a..3785cbc 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/omnibox/voice/VoiceRecognitionHandlerTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/omnibox/voice/VoiceRecognitionHandlerTest.java
@@ -50,6 +50,7 @@
 import org.chromium.chrome.browser.omnibox.suggestions.AutocompleteDelegate;
 import org.chromium.chrome.browser.omnibox.suggestions.OmniboxSuggestionsDropdownEmbedder;
 import org.chromium.chrome.browser.omnibox.voice.VoiceRecognitionHandler.AssistantActionPerformed;
+import org.chromium.chrome.browser.omnibox.voice.VoiceRecognitionHandler.AudioPermissionState;
 import org.chromium.chrome.browser.omnibox.voice.VoiceRecognitionHandler.TranslateBridgeWrapper;
 import org.chromium.chrome.browser.omnibox.voice.VoiceRecognitionHandler.VoiceIntentTarget;
 import org.chromium.chrome.browser.omnibox.voice.VoiceRecognitionHandler.VoiceInteractionSource;
@@ -1481,6 +1482,43 @@
 
     @Test
     @SmallTest
+    public void testRecordAudioState_deniedCannotAsk() {
+        mPermissionDelegate.setHasPermission(false);
+        mPermissionDelegate.setCanRequestPermission(false);
+        TestThreadUtils.runOnUiThreadBlocking(
+                () -> { mHandler.startVoiceRecognition(VoiceInteractionSource.OMNIBOX); });
+        Assert.assertEquals(1,
+                RecordHistogram.getHistogramValueCountForTesting(
+                        "VoiceInteraction.AudioPermissionEvent",
+                        AudioPermissionState.DENIED_CANNOT_ASK_AGAIN));
+    }
+
+    @Test
+    @SmallTest
+    public void testRecordAudioState_deniedCanAsk() {
+        mPermissionDelegate.setCanRequestPermission(true);
+        mPermissionDelegate.setPermissionResults(PackageManager.PERMISSION_DENIED);
+        TestThreadUtils.runOnUiThreadBlocking(
+                () -> { mHandler.startVoiceRecognition(VoiceInteractionSource.OMNIBOX); });
+        Assert.assertEquals(1,
+                RecordHistogram.getHistogramValueCountForTesting(
+                        "VoiceInteraction.AudioPermissionEvent",
+                        AudioPermissionState.DENIED_CAN_ASK_AGAIN));
+    }
+
+    @Test
+    @SmallTest
+    public void testRecordAudioState_granted() {
+        mPermissionDelegate.setHasPermission(true);
+        TestThreadUtils.runOnUiThreadBlocking(
+                () -> { mHandler.startVoiceRecognition(VoiceInteractionSource.OMNIBOX); });
+        Assert.assertEquals(1,
+                RecordHistogram.getHistogramValueCountForTesting(
+                        "VoiceInteraction.AudioPermissionEvent", AudioPermissionState.GRANTED));
+    }
+
+    @Test
+    @SmallTest
     public void testCallback_CalledTwice() {
         startVoiceRecognition(VoiceInteractionSource.NTP);
         Assert.assertEquals(-1, mHandler.getVoiceSearchUnexpectedResultSource());
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/read_later/ReadLaterIphTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/read_later/ReadLaterIphTest.java
index 72f0e79b..4f11fc0 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/read_later/ReadLaterIphTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/read_later/ReadLaterIphTest.java
@@ -107,13 +107,8 @@
         RevampedContextMenuUtils.selectContextMenuItem(InstrumentationRegistry.getInstrumentation(),
                 activity, tab, CONTEXT_MENU_LINK_DOM_ID, R.id.contextmenu_copy_link_address);
 
-        boolean threeButtonActionBarEnabled = ChromeFeatureList.isEnabled(
-                ChromeFeatureList.TABBED_APP_OVERFLOW_MENU_THREE_BUTTON_ACTIONBAR);
-        onView(withId(R.id.menu_button_wrapper))
-                .check(matches(withHighlight(!threeButtonActionBarEnabled)));
-        if (!threeButtonActionBarEnabled) {
-            waitForHelpBubble(withText(R.string.reading_list_save_pages_for_later));
-        }
+        onView(withId(R.id.menu_button_wrapper)).check(matches(withHighlight(true)));
+        waitForHelpBubble(withText(R.string.reading_list_save_pages_for_later));
     }
 
     private ViewInteraction waitForHelpBubble(Matcher<View> matcher) {
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/toolbar/adaptive/OptionalNewTabButtonControllerTabletTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/toolbar/adaptive/OptionalNewTabButtonControllerTabletTest.java
new file mode 100644
index 0000000..b3e81d4
--- /dev/null
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/toolbar/adaptive/OptionalNewTabButtonControllerTabletTest.java
@@ -0,0 +1,74 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.toolbar.adaptive;
+
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+
+import static org.chromium.chrome.test.util.ViewUtils.waitForView;
+
+import androidx.test.filters.MediumTest;
+
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.chromium.base.test.util.Batch;
+import org.chromium.base.test.util.CommandLineFlags;
+import org.chromium.base.test.util.DisableIf;
+import org.chromium.chrome.R;
+import org.chromium.chrome.browser.flags.ChromeFeatureList;
+import org.chromium.chrome.browser.flags.ChromeSwitches;
+import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
+import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
+import org.chromium.chrome.test.batch.BlankCTATabInitialStateRule;
+import org.chromium.chrome.test.util.ViewUtils;
+import org.chromium.chrome.test.util.browser.Features.EnableFeatures;
+import org.chromium.ui.test.util.UiDisableIf;
+
+/**
+ * Tests {@link OptionalNewTabButtonController} on tablet. Phone functionality is tested by {@link
+ * OptionalNewTabButtonControllerTest}.
+ */
+@RunWith(ChromeJUnit4ClassRunner.class)
+@Batch(Batch.PER_CLASS)
+@EnableFeatures({ChromeFeatureList.ADAPTIVE_BUTTON_IN_TOP_TOOLBAR})
+@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE,
+        "enable-features=" + ChromeFeatureList.ADAPTIVE_BUTTON_IN_TOP_TOOLBAR + "<Study",
+        "force-fieldtrials=Study/Group", "force-fieldtrial-params=Study.Group:mode/always-new-tab"})
+@DisableIf.Device(type = {UiDisableIf.PHONE})
+public class OptionalNewTabButtonControllerTabletTest {
+    private static final String TEST_PAGE = "/chrome/test/data/android/navigate/simple.html";
+
+    @ClassRule
+    public static final ChromeTabbedActivityTestRule sActivityTestRule =
+            new ChromeTabbedActivityTestRule();
+
+    @Rule
+    public final BlankCTATabInitialStateRule mInitialStateRule =
+            new BlankCTATabInitialStateRule(sActivityTestRule, /*clearAllTabState=*/false);
+
+    private String mTestPageUrl;
+    private String mButtonDescription;
+
+    @Before
+    public void setUp() {
+        mTestPageUrl = sActivityTestRule.getTestServer().getURL(TEST_PAGE);
+        mButtonDescription =
+                sActivityTestRule.getActivity().getResources().getString(R.string.button_new_tab);
+    }
+
+    @Test
+    @MediumTest
+    public void testButton_hiddenOnTablet() {
+        sActivityTestRule.loadUrl(mTestPageUrl, /*secondsToWait=*/10);
+
+        onView(isRoot()).check(waitForView(
+                withId(R.id.optional_toolbar_button), ViewUtils.VIEW_GONE | ViewUtils.VIEW_NULL));
+    }
+}
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/toolbar/adaptive/OptionalNewTabButtonControllerTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/toolbar/adaptive/OptionalNewTabButtonControllerTest.java
index fc529fae..807f83eb 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/toolbar/adaptive/OptionalNewTabButtonControllerTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/toolbar/adaptive/OptionalNewTabButtonControllerTest.java
@@ -45,14 +45,16 @@
 import org.chromium.content_public.browser.test.util.TestThreadUtils;
 import org.chromium.ui.test.util.UiDisableIf;
 
-/** Tests {@link OptionalNewTabButtonController}. */
+/**
+ * Tests {@link OptionalNewTabButtonController}. See also {@link
+ * OptionalNewTabButtonControllerTabletTest}.
+ */
 @RunWith(ChromeJUnit4ClassRunner.class)
 @Batch(Batch.PER_CLASS)
 @EnableFeatures({ChromeFeatureList.ADAPTIVE_BUTTON_IN_TOP_TOOLBAR})
 @CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE,
         "enable-features=" + ChromeFeatureList.ADAPTIVE_BUTTON_IN_TOP_TOOLBAR + "<Study",
         "force-fieldtrials=Study/Group", "force-fieldtrial-params=Study.Group:mode/always-new-tab"})
-// See: https://crbug.com/1187559
 @DisableIf.Device(type = {UiDisableIf.TABLET})
 public class OptionalNewTabButtonControllerTest {
     private static final String TEST_PAGE = "/chrome/test/data/android/navigate/simple.html";
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/vr/README.md b/chrome/android/javatests/src/org/chromium/chrome/browser/vr/README.md
index 19a5348..5f219d0 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/vr/README.md
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/vr/README.md
@@ -15,7 +15,9 @@
 
 If you are reproducing an issue with the AR tests, run
 `export DOWNLOAD_VR_TEST_APKS=1 && gclient runhooks` in order to get the
-playback datasets that are necessary.
+playback datasets that are necessary. This requires authentication, run
+`gsutil.py config` [documentation](https://chromium.googlesource.com/chromiumos/docs/+/master/gsutil.md) 
+to set this up if necessary.
 
 **NOTE** The message "Main  Unable to find package info for org.chromium.chrome"
          is usually displayed when the test package is being installed and does
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/vr/WebXrArAnchorsTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/vr/WebXrArAnchorsTest.java
index 8cb33e3..131984a 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/vr/WebXrArAnchorsTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/vr/WebXrArAnchorsTest.java
@@ -23,6 +23,7 @@
 import org.chromium.base.test.params.ParameterSet;
 import org.chromium.base.test.params.ParameterizedRunner;
 import org.chromium.base.test.util.CommandLineFlags;
+import org.chromium.base.test.util.DisabledTest;
 import org.chromium.base.test.util.MinAndroidSdkLevel;
 import org.chromium.chrome.browser.flags.ChromeSwitches;
 import org.chromium.chrome.browser.vr.rules.ArPlaybackFile;
@@ -69,6 +70,7 @@
     @MediumTest
     @XrActivityRestriction({XrActivityRestriction.SupportedActivity.ALL})
     @ArPlaybackFile("chrome/test/data/xr/ar_playback_datasets/floor_session_12s_30fps.mp4")
+    @DisabledTest(message = "crbug.com/1188722")
     public void testHitTestAnchorSucceedsWithPlane() {
         mWebXrArTestFramework.loadFileAndAwaitInitialization(
                 "webxr_test_basic_anchors_hittest", PAGE_LOAD_TIMEOUT_S);
@@ -84,6 +86,7 @@
     @MediumTest
     @XrActivityRestriction({XrActivityRestriction.SupportedActivity.ALL})
     @ArPlaybackFile("chrome/test/data/xr/ar_playback_datasets/floor_session_12s_30fps.mp4")
+    @DisabledTest(message = "crbug.com/1188722")
     public void testFreeFloatingAnchorSucceeds() {
         mWebXrArTestFramework.loadFileAndAwaitInitialization(
                 "webxr_test_basic_anchors_freefloating", PAGE_LOAD_TIMEOUT_S);
@@ -101,6 +104,7 @@
     @XrActivityRestriction({XrActivityRestriction.SupportedActivity.ALL})
     @ArPlaybackFile(
             "chrome/test/data/xr/ar_playback_datasets/floor_session_with_tracking_loss_37s_30fps.mp4")
+    @DisabledTest(message = "crbug.com/1188722")
     public void
     testAnchorStates() {
         mWebXrArTestFramework.loadFileAndAwaitInitialization(
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/app/appmenu/AppMenuPropertiesDelegateUnitTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/app/appmenu/AppMenuPropertiesDelegateUnitTest.java
index 99cfdbd..c9d3979 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/app/appmenu/AppMenuPropertiesDelegateUnitTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/app/appmenu/AppMenuPropertiesDelegateUnitTest.java
@@ -37,14 +37,11 @@
 import org.chromium.base.test.util.JniMocker;
 import org.chromium.chrome.R;
 import org.chromium.chrome.browser.ActivityTabProvider;
-import org.chromium.chrome.browser.app.appmenu.AppMenuPropertiesDelegateImpl.AppMenuSimilarSelectionType;
 import org.chromium.chrome.browser.app.appmenu.AppMenuPropertiesDelegateImpl.MenuGroup;
 import org.chromium.chrome.browser.bookmarks.BookmarkBridge;
 import org.chromium.chrome.browser.compositor.layouts.OverviewModeBehavior;
 import org.chromium.chrome.browser.device.DeviceConditions;
 import org.chromium.chrome.browser.device.ShadowDeviceConditions;
-import org.chromium.chrome.browser.flags.CachedFeatureFlags;
-import org.chromium.chrome.browser.flags.ChromeFeatureList;
 import org.chromium.chrome.browser.multiwindow.MultiWindowModeStateDispatcher;
 import org.chromium.chrome.browser.omaha.UpdateMenuItemHelper;
 import org.chromium.chrome.browser.preferences.Pref;
@@ -234,10 +231,9 @@
 
         Integer[] expectedItems = {R.id.icon_row_menu_id, R.id.new_tab_menu_id,
                 R.id.new_incognito_tab_menu_id, R.id.divider_line_id, R.id.open_history_menu_id,
-                R.id.downloads_row_menu_id, R.id.all_bookmarks_row_menu_id,
-                R.id.recent_tabs_menu_id, R.id.divider_line_id,
-                R.id.request_desktop_site_row_menu_id, R.id.divider_line_id, R.id.preferences_id,
-                R.id.help_id};
+                R.id.downloads_menu_id, R.id.all_bookmarks_menu_id, R.id.recent_tabs_menu_id,
+                R.id.divider_line_id, R.id.request_desktop_site_row_menu_id, R.id.divider_line_id,
+                R.id.preferences_id, R.id.help_id};
         assertMenuItemsAreEqual(menu, expectedItems);
     }
 
@@ -255,15 +251,15 @@
 
         Integer[] expectedItems = {R.id.icon_row_menu_id, R.id.new_tab_menu_id,
                 R.id.new_incognito_tab_menu_id, R.id.divider_line_id, R.id.open_history_menu_id,
-                R.id.downloads_row_menu_id, R.id.all_bookmarks_row_menu_id,
-                R.id.recent_tabs_menu_id, R.id.divider_line_id, R.id.share_row_menu_id,
-                R.id.find_in_page_id, R.id.translate_id, R.id.add_to_homescreen_id,
-                R.id.request_desktop_site_row_menu_id, R.id.divider_line_id, R.id.preferences_id,
-                R.id.help_id};
+                R.id.downloads_menu_id, R.id.all_bookmarks_menu_id, R.id.recent_tabs_menu_id,
+                R.id.divider_line_id, R.id.share_row_menu_id, R.id.find_in_page_id,
+                R.id.translate_id, R.id.add_to_homescreen_id, R.id.request_desktop_site_row_menu_id,
+                R.id.divider_line_id, R.id.preferences_id, R.id.help_id};
         Integer[] expectedTitles = {0, R.string.menu_new_tab, R.string.menu_new_incognito_tab, 0,
-                R.string.menu_history, 0, 0, R.string.menu_recent_tabs, 0, 0,
-                R.string.menu_find_in_page, R.string.menu_translate,
-                R.string.menu_add_to_homescreen, 0, 0, R.string.menu_settings, R.string.menu_help};
+                R.string.menu_history, R.string.menu_downloads, R.string.menu_bookmarks,
+                R.string.menu_recent_tabs, 0, 0, R.string.menu_find_in_page,
+                R.string.menu_translate, R.string.menu_add_to_homescreen, 0, 0,
+                R.string.menu_settings, R.string.menu_help};
         Integer[] expectedActionBarItems = {R.id.forward_menu_id, R.id.bookmark_this_page_id,
                 R.id.offline_page_id, R.id.info_menu_id, R.id.reload_menu_id};
         assertMenuItemsAreEqual(menu, expectedItems);
@@ -288,17 +284,17 @@
         mAppMenuPropertiesDelegate.prepareMenu(menu, null);
 
         Integer[] expectedItems = {R.id.icon_row_menu_id, R.id.new_tab_menu_id,
-                R.id.new_incognito_tab_menu_id, R.id.divider_line_id,
-                R.id.all_bookmarks_row_menu_id, R.id.open_history_menu_id,
-                R.id.downloads_row_menu_id, R.id.recent_tabs_menu_id, R.id.divider_line_id,
-                R.id.translate_id, R.id.share_row_menu_id, R.id.find_in_page_id,
-                R.id.add_to_homescreen_id, R.id.request_desktop_site_row_menu_id,
-                R.id.divider_line_id, R.id.preferences_id, R.id.help_id};
+                R.id.new_incognito_tab_menu_id, R.id.divider_line_id, R.id.open_history_menu_id,
+                R.id.downloads_menu_id, R.id.all_bookmarks_menu_id, R.id.recent_tabs_menu_id,
+                R.id.divider_line_id, R.id.translate_id, R.id.share_row_menu_id,
+                R.id.find_in_page_id, R.id.add_to_homescreen_id,
+                R.id.request_desktop_site_row_menu_id, R.id.divider_line_id, R.id.preferences_id,
+                R.id.help_id};
         Integer[] expectedTitles = {0, R.string.menu_new_tab, R.string.menu_new_incognito_tab, 0,
-                R.string.menu_history, 0, 0, R.string.menu_recent_tabs, 0, 0,
-                R.string.menu_find_in_page, R.string.menu_translate,
-                R.string.menu_add_to_homescreen_install, 0, 0, R.string.menu_settings,
-                R.string.menu_help};
+                R.string.menu_history, R.string.menu_downloads, R.string.menu_bookmarks,
+                R.string.menu_recent_tabs, 0, 0, R.string.menu_find_in_page,
+                R.string.menu_translate, R.string.menu_add_to_homescreen_install, 0, 0,
+                R.string.menu_settings, R.string.menu_help};
         Integer[] expectedActionBarItems = {R.id.forward_menu_id, R.id.bookmark_this_page_id,
                 R.id.offline_page_id, R.id.info_menu_id, R.id.reload_menu_id};
         assertMenuItemsAreEqual(menu, expectedItems);
@@ -321,11 +317,10 @@
 
         Integer[] expectedItems = {R.id.icon_row_menu_id, R.id.new_tab_menu_id,
                 R.id.new_incognito_tab_menu_id, R.id.divider_line_id, R.id.open_history_menu_id,
-                R.id.downloads_row_menu_id, R.id.all_bookmarks_row_menu_id,
-                R.id.recent_tabs_menu_id, R.id.divider_line_id, R.id.share_row_menu_id,
-                R.id.find_in_page_id, R.id.translate_id, R.id.add_to_homescreen_id,
-                R.id.request_desktop_site_row_menu_id, R.id.divider_line_id, R.id.preferences_id,
-                R.id.help_id, R.id.managed_by_menu_id};
+                R.id.downloads_menu_id, R.id.all_bookmarks_menu_id, R.id.recent_tabs_menu_id,
+                R.id.divider_line_id, R.id.share_row_menu_id, R.id.find_in_page_id,
+                R.id.translate_id, R.id.add_to_homescreen_id, R.id.request_desktop_site_row_menu_id,
+                R.id.divider_line_id, R.id.preferences_id, R.id.help_id, R.id.managed_by_menu_id};
         assertMenuItemsAreEqual(menu, expectedItems);
     }
 
@@ -359,69 +354,15 @@
         mAppMenuPropertiesDelegate.prepareMenu(menu, null);
 
         Integer[] expectedItems = {R.id.update_menu_id, R.id.new_tab_menu_id,
-                R.id.new_incognito_tab_menu_id, R.id.recent_tabs_menu_id, R.id.open_history_menu_id,
-                R.id.translate_id, R.id.find_in_page_id, R.id.add_to_homescreen_id,
-                R.id.reader_mode_prefs_id, R.id.preferences_id, R.id.help_id};
+                R.id.new_incognito_tab_menu_id, R.id.open_history_menu_id, R.id.downloads_menu_id,
+                R.id.all_bookmarks_menu_id, R.id.recent_tabs_menu_id, R.id.translate_id,
+                R.id.find_in_page_id, R.id.add_to_homescreen_id, R.id.reader_mode_prefs_id,
+                R.id.preferences_id, R.id.help_id};
         assertMenuItemsHaveIcons(menu, expectedItems);
     }
 
     @Test
     @Config(qualifiers = "sw320dp")
-    public void testPageMenuItems_Phone_RegularPage_regroup() {
-        setUpMocksForPageMenu();
-        setMenuOptions(false /*isNativePage*/, true /*showTranslate*/, true /*showUpdate*/,
-                true /*showMoveToOtherWindow*/, false /*showReaderModePrefs*/,
-                true /*showAddToHomeScreen*/, true /*showPaintPreview*/);
-
-        Assert.assertEquals(MenuGroup.PAGE_MENU, mAppMenuPropertiesDelegate.getMenuGroup());
-        Menu menu = createTestMenu();
-        mAppMenuPropertiesDelegate.prepareMenu(menu, null);
-
-        Integer[] expectedItems = {R.id.icon_row_menu_id, R.id.update_menu_id,
-                R.id.move_to_other_window_menu_id, R.id.new_tab_menu_id,
-                R.id.new_incognito_tab_menu_id, R.id.divider_line_id, R.id.open_history_menu_id,
-                R.id.downloads_row_menu_id, R.id.all_bookmarks_row_menu_id,
-                R.id.recent_tabs_menu_id, R.id.divider_line_id, R.id.share_row_menu_id,
-                R.id.paint_preview_show_id, R.id.find_in_page_id, R.id.translate_id,
-                R.id.add_to_homescreen_id, R.id.request_desktop_site_row_menu_id,
-                R.id.divider_line_id, R.id.preferences_id, R.id.help_id};
-        Integer[] expectedActionBarItems = {R.id.forward_menu_id, R.id.bookmark_this_page_id,
-                R.id.offline_page_id, R.id.info_menu_id, R.id.reload_menu_id};
-        assertMenuItemsAreEqual(menu, expectedItems);
-        assertActionBarItemsAreEqual(menu, expectedActionBarItems);
-    }
-
-    @Test
-    @Config(qualifiers = "sw320dp")
-    public void testPageMenuItems_Phone_RegularPage_threebutton_actionbar() {
-        CachedFeatureFlags.setForTesting(
-                ChromeFeatureList.TABBED_APP_OVERFLOW_MENU_THREE_BUTTON_ACTIONBAR, true);
-        AppMenuPropertiesDelegateImpl.THREE_BUTTON_ACTION_BAR_VARIATION.setForTesting(
-                "action_chip_view");
-        setUpMocksForPageMenu();
-        setMenuOptions(false /*isNativePage*/, true /*showTranslate*/, true /*showUpdate*/,
-                true /*showMoveToOtherWindow*/, false /*showReaderModePrefs*/,
-                true /*showAddToHomeScreen*/, true /*showPaintPreview*/);
-
-        Assert.assertEquals(MenuGroup.PAGE_MENU, mAppMenuPropertiesDelegate.getMenuGroup());
-        Menu menu = createTestMenu();
-        mAppMenuPropertiesDelegate.prepareMenu(menu, null);
-
-        Integer[] expectedItems = {R.id.icon_row_menu_id, R.id.update_menu_id, R.id.new_tab_menu_id,
-                R.id.new_incognito_tab_menu_id, R.id.move_to_other_window_menu_id,
-                R.id.divider_line_id, R.id.open_history_menu_id, R.id.downloads_row_menu_id,
-                R.id.all_bookmarks_row_menu_id, R.id.recent_tabs_menu_id, R.id.divider_line_id,
-                R.id.share_row_menu_id, R.id.paint_preview_show_id, R.id.find_in_page_id,
-                R.id.translate_id, R.id.add_to_homescreen_id, R.id.request_desktop_site_row_menu_id,
-                R.id.divider_line_id, R.id.preferences_id, R.id.help_id};
-        Integer[] expectedActionBarItems = {
-                R.id.forward_menu_id, R.id.info_menu_id, R.id.reload_menu_id};
-        assertMenuItemsAreEqual(menu, expectedItems);
-        assertActionBarItemsAreEqual(menu, expectedActionBarItems);
-    }
-
-    @Test
-    @Config(qualifiers = "sw320dp")
     public void testOverviewMenuItems_Phone() {
         setUpMocksForOverviewMenu();
         when(mIncognitoTabModel.getCount()).thenReturn(0);
@@ -490,9 +431,9 @@
 
         Integer[] expectedItems = {R.id.icon_row_menu_id, R.id.new_tab_menu_id,
                 R.id.new_incognito_tab_menu_id, R.id.divider_line_id, R.id.open_history_menu_id,
-                R.id.all_bookmarks_row_menu_id, R.id.downloads_row_menu_id,
-                R.id.recent_tabs_menu_id, R.id.divider_line_id, R.id.share_row_menu_id,
-                R.id.get_image_descriptions_id, R.id.find_in_page_id, R.id.add_to_homescreen_id,
+                R.id.downloads_menu_id, R.id.all_bookmarks_menu_id, R.id.recent_tabs_menu_id,
+                R.id.divider_line_id, R.id.share_row_menu_id, R.id.get_image_descriptions_id,
+                R.id.find_in_page_id, R.id.add_to_homescreen_id,
                 R.id.request_desktop_site_row_menu_id, R.id.divider_line_id, R.id.preferences_id,
                 R.id.help_id};
 
@@ -522,78 +463,6 @@
                 "Get image descriptions", menu.findItem(R.id.get_image_descriptions_id).getTitle());
     }
 
-    @Test
-    public void testMenuItems_AppMenuSimilarSelectionChecker() {
-        Assert.assertEquals("No match for bookmark page then all bookmarks",
-                AppMenuSimilarSelectionType.BOOKMARK_PAGE_THEN_ALL_BOOKMARKS,
-                mAppMenuPropertiesDelegate.findSimilarSelectionPattern(
-                        R.id.bookmark_this_page_id, R.id.all_bookmarks_menu_id));
-        Assert.assertEquals("No match for bookmark page then all bookmarks",
-                AppMenuSimilarSelectionType.BOOKMARK_PAGE_THEN_ALL_BOOKMARKS,
-                mAppMenuPropertiesDelegate.findSimilarSelectionPattern(
-                        R.id.bookmark_this_page_chip_id, R.id.all_bookmarks_menu_id));
-        Assert.assertTrue("Should return true for bookmark page then all bookmarks",
-                mAppMenuPropertiesDelegate.recordAppMenuSimilarSelectionIfNeeded(
-                        R.id.bookmark_this_page_id, R.id.all_bookmarks_menu_id));
-        Assert.assertTrue("Should return true for bookmark page then all bookmarks",
-                mAppMenuPropertiesDelegate.recordAppMenuSimilarSelectionIfNeeded(
-                        R.id.bookmark_this_page_chip_id, R.id.all_bookmarks_menu_id));
-
-        Assert.assertEquals("No match for all bookmarks then bookmark page",
-                AppMenuSimilarSelectionType.ALL_BOOKMARKS_THEN_BOOKMARK_PAGE,
-                mAppMenuPropertiesDelegate.findSimilarSelectionPattern(
-                        R.id.all_bookmarks_menu_id, R.id.bookmark_this_page_id));
-        Assert.assertEquals("No match for all bookmarks then bookmark page",
-                AppMenuSimilarSelectionType.ALL_BOOKMARKS_THEN_BOOKMARK_PAGE,
-                mAppMenuPropertiesDelegate.findSimilarSelectionPattern(
-                        R.id.all_bookmarks_menu_id, R.id.bookmark_this_page_chip_id));
-        Assert.assertTrue("Should return true for all bookmarks then bookmark page",
-                mAppMenuPropertiesDelegate.recordAppMenuSimilarSelectionIfNeeded(
-                        R.id.all_bookmarks_menu_id, R.id.bookmark_this_page_id));
-        Assert.assertTrue("Should return true for all bookmarks then bookmark page",
-                mAppMenuPropertiesDelegate.recordAppMenuSimilarSelectionIfNeeded(
-                        R.id.all_bookmarks_menu_id, R.id.bookmark_this_page_chip_id));
-
-        Assert.assertEquals("No match for download page then all downloads",
-                AppMenuSimilarSelectionType.DOWNLOAD_PAGE_THEN_ALL_DOWNLOADS,
-                mAppMenuPropertiesDelegate.findSimilarSelectionPattern(
-                        R.id.offline_page_id, R.id.downloads_menu_id));
-        Assert.assertEquals("No match for download page then all downloads",
-                AppMenuSimilarSelectionType.DOWNLOAD_PAGE_THEN_ALL_DOWNLOADS,
-                mAppMenuPropertiesDelegate.findSimilarSelectionPattern(
-                        R.id.offline_page_chip_id, R.id.downloads_menu_id));
-        Assert.assertTrue("Should return true for download page then all downloads",
-                mAppMenuPropertiesDelegate.recordAppMenuSimilarSelectionIfNeeded(
-                        R.id.offline_page_id, R.id.downloads_menu_id));
-        Assert.assertTrue("Should return true for download page then all downloads",
-                mAppMenuPropertiesDelegate.recordAppMenuSimilarSelectionIfNeeded(
-                        R.id.offline_page_chip_id, R.id.downloads_menu_id));
-
-        Assert.assertEquals("No match for all downloads then download page",
-                AppMenuSimilarSelectionType.ALL_DOWNLOADS_THEN_DOWNLOAD_PAGE,
-                mAppMenuPropertiesDelegate.findSimilarSelectionPattern(
-                        R.id.downloads_menu_id, R.id.offline_page_id));
-        Assert.assertEquals("No match for all downloads then download page",
-                AppMenuSimilarSelectionType.ALL_DOWNLOADS_THEN_DOWNLOAD_PAGE,
-                mAppMenuPropertiesDelegate.findSimilarSelectionPattern(
-                        R.id.downloads_menu_id, R.id.offline_page_chip_id));
-        Assert.assertTrue("Should return true for all downloads then download page",
-                mAppMenuPropertiesDelegate.recordAppMenuSimilarSelectionIfNeeded(
-                        R.id.downloads_menu_id, R.id.offline_page_id));
-        Assert.assertTrue("Should return true for all downloads then download page",
-                mAppMenuPropertiesDelegate.recordAppMenuSimilarSelectionIfNeeded(
-                        R.id.downloads_menu_id, R.id.offline_page_chip_id));
-
-        Assert.assertEquals("Should no match for all downloads then all bookmarks",
-                AppMenuSimilarSelectionType.NO_MATCH,
-                mAppMenuPropertiesDelegate.findSimilarSelectionPattern(
-                        R.id.downloads_menu_id, R.id.all_bookmarks_menu_id));
-        Assert.assertEquals("Should no match for new tab then find in page",
-                AppMenuSimilarSelectionType.NO_MATCH,
-                mAppMenuPropertiesDelegate.findSimilarSelectionPattern(
-                        R.id.new_tab_menu_id, R.id.find_in_page_id));
-    }
-
     private void setUpMocksForPageMenu() {
         when(mActivityTabProvider.get()).thenReturn(mTab);
         when(mOverviewModeBehavior.overviewVisible()).thenReturn(false);
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/feed/v2/FeedStreamSurfaceTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/feed/v2/FeedStreamSurfaceTest.java
index d470d17..2d74f0df 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/feed/v2/FeedStreamSurfaceTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/feed/v2/FeedStreamSurfaceTest.java
@@ -88,9 +88,6 @@
         }
 
         @Override
-        public void onAddStarting() {}
-
-        @Override
         public void onAddFinished() {}
     }
 
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/tabbed_mode/TabbedAppMenuPropertiesDelegateUnitTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/tabbed_mode/TabbedAppMenuPropertiesDelegateUnitTest.java
index dcc345b..db6215c30 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/tabbed_mode/TabbedAppMenuPropertiesDelegateUnitTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/tabbed_mode/TabbedAppMenuPropertiesDelegateUnitTest.java
@@ -184,9 +184,9 @@
 
         Integer[] expectedItems = {R.id.icon_row_menu_id, R.id.new_tab_menu_id,
                 R.id.new_incognito_tab_menu_id, R.id.divider_line_id, R.id.open_history_menu_id,
-                R.id.downloads_row_menu_id, R.id.all_bookmarks_row_menu_id,
-                R.id.recent_tabs_menu_id, R.id.divider_line_id, R.id.translate_id,
-                R.id.share_row_menu_id, R.id.find_in_page_id, R.id.add_to_homescreen_id,
+                R.id.downloads_menu_id, R.id.all_bookmarks_menu_id, R.id.recent_tabs_menu_id,
+                R.id.divider_line_id, R.id.translate_id, R.id.share_row_menu_id,
+                R.id.find_in_page_id, R.id.add_to_homescreen_id,
                 R.id.request_desktop_site_row_menu_id, R.id.divider_line_id, R.id.preferences_id,
                 R.id.help_id, R.id.managed_by_menu_id};
         assertMenuItemsAreEqual(menu, expectedItems);
diff --git a/chrome/android/profiles/newest.txt b/chrome/android/profiles/newest.txt
index 9b161f15..28d8a3a 100644
--- a/chrome/android/profiles/newest.txt
+++ b/chrome/android/profiles/newest.txt
@@ -1 +1 @@
-chromeos-chrome-amd64-91.0.4435.0_rc-r1-merged.afdo.bz2
+chromeos-chrome-amd64-91.0.4448.0_rc-r1-merged.afdo.bz2
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
index a7e1d41..2688160 100644
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -1336,8 +1336,6 @@
     "prefs/chrome_pref_service_factory.h",
     "prefs/incognito_mode_prefs.cc",
     "prefs/incognito_mode_prefs.h",
-    "prefs/origin_trial_prefs.cc",
-    "prefs/origin_trial_prefs.h",
     "prefs/pref_metrics_service.cc",
     "prefs/pref_metrics_service.h",
     "prefs/pref_service_incognito_allowlist.cc",
@@ -2035,6 +2033,7 @@
     "//components/download/public/background_service:public",
     "//components/embedder_support",
     "//components/embedder_support:browser_util",
+    "//components/embedder_support/origin_trials",
     "//components/encrypted_messages",
     "//components/enterprise",
     "//components/enterprise/common/proto:connectors_proto",
@@ -4000,8 +3999,6 @@
                                                               # to extensions
                                                               # section ?
       "speech/extension_api/tts_extension_api_constants.h",
-      "speech/on_device_speech_recognizer.cc",
-      "speech/on_device_speech_recognizer.h",
       "speech/speech_recognition_client_browser_interface.cc",
       "speech/speech_recognition_client_browser_interface.h",
       "speech/speech_recognition_client_browser_interface_factory.cc",
@@ -4209,6 +4206,8 @@
         "speech/cros_speech_recognition_service.h",
         "speech/cros_speech_recognition_service_factory.cc",
         "speech/cros_speech_recognition_service_factory.h",
+        "speech/on_device_speech_recognizer.cc",
+        "speech/on_device_speech_recognizer.h",
       ]
 
       deps += [ "//chrome/services/speech:lib" ]
@@ -4631,11 +4630,15 @@
       "lacros/account_manager_facade_factory_lacros.cc",
       "lacros/account_manager_util.cc",
       "lacros/account_manager_util.h",
+      "lacros/automation_manager_lacros.cc",
+      "lacros/automation_manager_lacros.h",
       "lacros/cert_db_initializer.h",
       "lacros/cert_db_initializer_factory.cc",
       "lacros/cert_db_initializer_factory.h",
       "lacros/cert_db_initializer_impl.cc",
       "lacros/cert_db_initializer_impl.h",
+      "lacros/chrome_browser_main_extra_parts_lacros.cc",
+      "lacros/chrome_browser_main_extra_parts_lacros.h",
       "lacros/client_cert_store_lacros.cc",
       "lacros/client_cert_store_lacros.h",
       "lacros/crosapi_pref_observer.cc",
diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
index 6f216ba..3299fbbce 100644
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -2070,22 +2070,6 @@
          base::size(kPhotoPickerVideoSupportEnabledWithAnimatedThumbnails),
          nullptr}};
 
-const FeatureEntry::FeatureParam
-    kTabbedAppOverflowMenuThreeButtonActionbarAction[] = {
-        {"three_button_action_bar", "action_chip_view"}};
-const FeatureEntry::FeatureParam
-    kTabbedAppOverflowMenuThreeButtonActionbarDestination[] = {
-        {"three_button_action_bar", "destination_chip_view"}};
-const FeatureEntry::FeatureVariation
-    kTabbedAppOverflowMenuThreeButtonActionbarVariations[] = {
-        {"(three button with action chip view)",
-         kTabbedAppOverflowMenuThreeButtonActionbarAction,
-         base::size(kTabbedAppOverflowMenuThreeButtonActionbarAction), nullptr},
-        {"(three button with destination chip view)",
-         kTabbedAppOverflowMenuThreeButtonActionbarDestination,
-         base::size(kTabbedAppOverflowMenuThreeButtonActionbarDestination),
-         nullptr}};
-
 // Request Desktop Site on Tablet by default variations.
 const FeatureEntry::FeatureParam kRequestDesktopSiteForTablets768[] = {
     {"screen_width_dp", "768"},
@@ -4471,14 +4455,6 @@
 #endif  // OS_ANDROID
 
 #if defined(OS_ANDROID)
-    {"tabbed-app-overflow-menu-three-button-actionbar",
-     flag_descriptions::kTabbedAppOverflowMenuThreeButtonActionbarName,
-     flag_descriptions::kTabbedAppOverflowMenuThreeButtonActionbarDescription,
-     kOsAndroid,
-     FEATURE_WITH_PARAMS_VALUE_TYPE(
-         chrome::android::kTabbedAppOverflowMenuThreeButtonActionbar,
-         kTabbedAppOverflowMenuThreeButtonActionbarVariations,
-         "AndroidAppMenuUiReworkPhase4And5")},
     {"request-desktop-site-for-tablets",
      flag_descriptions::kRequestDesktopSiteForTabletsName,
      flag_descriptions::kRequestDesktopSiteForTabletsDescription, kOsAndroid,
diff --git a/chrome/browser/accessibility/caption_controller_browsertest.cc b/chrome/browser/accessibility/caption_controller_browsertest.cc
index 239686a0..c36aaef 100644
--- a/chrome/browser/accessibility/caption_controller_browsertest.cc
+++ b/chrome/browser/accessibility/caption_controller_browsertest.cc
@@ -6,6 +6,7 @@
 
 #include "base/files/file_path.h"
 #include "base/ranges/ranges.h"
+#include "base/test/scoped_feature_list.h"
 #include "build/build_config.h"
 #include "build/chromeos_buildflags.h"
 #include "chrome/browser/accessibility/caption_controller_factory.h"
@@ -13,8 +14,10 @@
 #include "chrome/browser/browser_process.h"
 #include "chrome/browser/lifetime/application_lifetime.h"
 #include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/profiles/profile_keep_alive_types.h"
 #include "chrome/browser/profiles/profile_manager.h"
 #include "chrome/browser/profiles/profile_window.h"
+#include "chrome/browser/profiles/scoped_profile_keep_alive.h"
 #include "chrome/browser/ui/browser.h"
 #include "chrome/browser/ui/browser_window.h"
 #include "chrome/browser/ui/caption_bubble_controller.h"
@@ -320,6 +323,7 @@
     //
     // TODO(crbug.com/88586): Remove the other branch once
     // DestroyProfileOnBrowserClose becomes the default.
+    base::RunLoop().RunUntilIdle();
     std::vector<Profile*> loaded_profiles =
         g_browser_process->profile_manager()->GetLoadedProfiles();
     auto it = base::ranges::find(loaded_profiles, profile);
@@ -591,6 +595,13 @@
   CaptionController* controller1 = GetControllerForProfile(profile1);
   CaptionController* controller2 = GetControllerForProfile(profile2);
 
+  // TODO(crbug.com/88586): Remove this test when the
+  // DestroyProfileOnBrowserClose flag is removed.
+  ScopedProfileKeepAlive profile1_keep_alive(
+      profile1, ProfileKeepAliveOrigin::kBrowserWindow);
+  ScopedProfileKeepAlive profile2_keep_alive(
+      profile2, ProfileKeepAliveOrigin::kBrowserWindow);
+
   // Enable live caption on both profiles.
   SetLiveCaptionEnabled(true);
   SetLiveCaptionEnabledForProfile(true, profile2);
diff --git a/chrome/browser/android/explore_sites/explore_sites_fetcher.cc b/chrome/browser/android/explore_sites/explore_sites_fetcher.cc
index 55c56a9..45b6e6b 100644
--- a/chrome/browser/android/explore_sites/explore_sites_fetcher.cc
+++ b/chrome/browser/android/explore_sites/explore_sites_fetcher.cc
@@ -120,7 +120,8 @@
       callback_(std::move(callback)),
       url_loader_factory_(loader_factory) {
   base::Version version = version_info::GetVersion();
-  std::string channel_name = chrome::GetChannelName();
+  std::string channel_name =
+      chrome::GetChannelName(chrome::WithExtendedStable(true));
   client_version_ = base::StringPrintf("%d.%d.%d.%s.chrome",
                                        version.components()[0],  // Major
                                        version.components()[2],  // Build
diff --git a/chrome/browser/ash/crosapi/automation_ash.cc b/chrome/browser/ash/crosapi/automation_ash.cc
new file mode 100644
index 0000000..d393f05
--- /dev/null
+++ b/chrome/browser/ash/crosapi/automation_ash.cc
@@ -0,0 +1,132 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ash/crosapi/automation_ash.h"
+
+#include "base/bind.h"
+#include "base/pickle.h"
+#include "chrome/browser/ash/crosapi/window_util.h"
+#include "chrome/common/channel_info.h"
+#include "components/exo/shell_surface_base.h"
+#include "components/version_info/channel.h"
+#include "extensions/browser/api/automation_internal/automation_event_router.h"
+#include "extensions/common/extension_messages.h"
+#include "ui/accessibility/ax_tree_id_registry.h"
+#include "ui/views/widget/widget.h"
+#include "ui/views/widget/widget_delegate.h"
+
+namespace crosapi {
+
+AutomationAsh::AutomationAsh() {
+  // TODO(https://crbug.com/1185764): Add a hook to listen for all actions.
+}
+
+AutomationAsh::~AutomationAsh() = default;
+
+void AutomationAsh::BindReceiver(
+    mojo::PendingReceiver<mojom::Automation> pending_receiver) {
+  receivers_.Add(this, std::move(pending_receiver));
+}
+
+void AutomationAsh::EnableDesktop() {
+  desktop_enabled_ = true;
+  for (auto& pair : automation_clients_) {
+    pair.second->Enable();
+  }
+}
+
+void AutomationAsh::EnableTree(const ui::AXTreeID& tree_id) {
+  if (!tree_id.token().has_value())
+    return;
+
+  for (auto& pair : automation_clients_) {
+    pair.second->EnableTree(tree_id.token().value());
+  }
+}
+
+void AutomationAsh::RegisterAutomationClient(
+    mojo::PendingRemote<mojom::AutomationClient> client,
+    const base::UnguessableToken& token) {
+  mojo::Remote<mojom::AutomationClient> remote(std::move(client));
+  remote.set_disconnect_handler(base::BindOnce(
+      &AutomationAsh::ClientDisconnected, weak_factory_.GetWeakPtr(), token));
+  automation_clients_[token] = std::move(remote);
+
+  if (desktop_enabled_) {
+    automation_clients_[token]->Enable();
+  }
+}
+
+void AutomationAsh::ReceiveEventPrototype(
+    const std::string& event_bundle_string,
+    bool root,
+    const base::UnguessableToken& token,
+    const std::string& window_id) {
+  // This prototype method is only implemented on developer builds of Chrome. We
+  // check for this by checking that the build of Chrome is unbranded.
+  if (chrome::GetChannel() != version_info::Channel::UNKNOWN)
+    return;
+
+  auto it = automation_clients_.find(token);
+  if (it == automation_clients_.end()) {
+    LOG(ERROR) << "Received automation event for an unregistered client. "
+                  "Ignoring the event.";
+    return;
+  }
+
+  base::Pickle pickle(event_bundle_string.data(), event_bundle_string.size());
+  base::PickleIterator iterator(pickle);
+  ExtensionMsg_AccessibilityEventBundleParams event_bundle;
+  bool success =
+      IPC::ParamTraits<ExtensionMsg_AccessibilityEventBundleParams>::Read(
+          &pickle, &iterator, &event_bundle);
+  if (!success) {
+    LOG(ERROR) << "ExtensionMsg_AccessibilityEventBundleParams deserialization "
+                  "failure";
+    return;
+  }
+
+  if (root) {
+    // TODO(https://crbug.com/1185764): This is fine for prototyping but we'll
+    // likely want a specific binding for a Lacros AutomationManagerAura to push
+    // its ax tree id along with the window id, and ax root node id
+    aura::Window* window = crosapi::GetShellSurfaceWindow(window_id);
+    if (window) {
+      views::Widget* widget = views::Widget::GetWidgetForNativeWindow(window);
+      if (widget) {
+        static_cast<exo::ShellSurfaceBase*>(widget->widget_delegate())
+            ->SetChildAxTreeId(event_bundle.tree_id);
+      }
+    }
+  }
+
+  // TODO(https://crbug.com/1185764): Forward the event to
+  // AutomationEventRouter.
+}
+
+void AutomationAsh::ForwardActionPrototype(
+    const ui::AXTreeID& tree_id,
+    int32_t automation_node_id,
+    const std::string& action_type,
+    int32_t request_id,
+    const base::DictionaryValue& optional_args) {
+  // This prototype method is only implemented on developer builds of Chrome. We
+  // check for this by checking that the build of Chrome is unbranded.
+  if (chrome::GetChannel() != version_info::Channel::UNKNOWN)
+    return;
+
+  if (!tree_id.token().has_value())
+    return;
+  for (auto& pair : automation_clients_) {
+    pair.second->PerformActionPrototype(tree_id.token().value(),
+                                        automation_node_id, action_type,
+                                        request_id, optional_args.Clone());
+  }
+}
+
+void AutomationAsh::ClientDisconnected(const base::UnguessableToken& token) {
+  automation_clients_.erase(token);
+}
+
+}  // namespace crosapi
diff --git a/chrome/browser/ash/crosapi/automation_ash.h b/chrome/browser/ash/crosapi/automation_ash.h
new file mode 100644
index 0000000..476d8bae
--- /dev/null
+++ b/chrome/browser/ash/crosapi/automation_ash.h
@@ -0,0 +1,68 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_ASH_CROSAPI_AUTOMATION_ASH_H_
+#define CHROME_BROWSER_ASH_CROSAPI_AUTOMATION_ASH_H_
+
+#include "base/memory/weak_ptr.h"
+#include "base/unguessable_token.h"
+#include "chromeos/crosapi/mojom/automation.mojom.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "mojo/public/cpp/bindings/pending_remote.h"
+#include "mojo/public/cpp/bindings/receiver_set.h"
+#include "ui/accessibility/ax_tree_id.h"
+
+namespace crosapi {
+
+// Implements the crosapi interface for automation. Lives in Ash-Chrome on
+// the UI thread.
+class AutomationAsh : public mojom::Automation {
+ public:
+  AutomationAsh();
+  AutomationAsh(const AutomationAsh&) = delete;
+  AutomationAsh& operator=(const AutomationAsh&) = delete;
+  ~AutomationAsh() override;
+
+  void BindReceiver(mojo::PendingReceiver<mojom::Automation> receiver);
+
+  // Called by ash's internal a11y implementation. Data is forwarded to Lacros.
+  void EnableDesktop();
+  void EnableTree(const ui::AXTreeID& tree_id);
+
+  // crosapi::mojom::Automation:
+  void RegisterAutomationClient(
+      mojo::PendingRemote<mojom::AutomationClient> client,
+      const base::UnguessableToken& token) override;
+  void ReceiveEventPrototype(const std::string& event_bundle,
+                             bool root,
+                             const base::UnguessableToken& token,
+                             const std::string& window_id) override;
+
+  // Forwards an action to all crosapi clients. This has no effect on production
+  // builds of chrome. It exists for prototyping for developers.
+  void ForwardActionPrototype(const ui::AXTreeID& tree_id,
+                              int32_t automation_node_id,
+                              const std::string& action_type,
+                              int32_t request_id,
+                              const base::DictionaryValue& optional_args);
+
+ private:
+  // Called when an AutomationClient is disconnected.
+  void ClientDisconnected(const base::UnguessableToken& token);
+
+  bool desktop_enabled_ = false;
+
+  // Any number of crosapi clients can connect to this class.
+  mojo::ReceiverSet<mojom::Automation> receivers_;
+
+  // This map maintains a list of all known automation clients.
+  std::map<base::UnguessableToken, mojo::Remote<mojom::AutomationClient>>
+      automation_clients_;
+
+  base::WeakPtrFactory<AutomationAsh> weak_factory_{this};
+};
+
+}  // namespace crosapi
+
+#endif  // CHROME_BROWSER_ASH_CROSAPI_AUTOMATION_ASH_H_
diff --git a/chrome/browser/ash/crosapi/browser_util.cc b/chrome/browser/ash/crosapi/browser_util.cc
index 6251c44..2678e694 100644
--- a/chrome/browser/ash/crosapi/browser_util.cc
+++ b/chrome/browser/ash/crosapi/browser_util.cc
@@ -193,10 +193,11 @@
 
 base::flat_map<base::Token, uint32_t> GetInterfaceVersions() {
   static_assert(
-      crosapi::mojom::Crosapi::Version_ == 17,
+      crosapi::mojom::Crosapi::Version_ == 18,
       "if you add a new crosapi, please add it to the version map here");
   InterfaceVersions versions;
   AddVersion<chromeos::sensors::mojom::SensorHalClient>(&versions);
+  AddVersion<crosapi::mojom::Automation>(&versions);
   AddVersion<crosapi::mojom::AccountManager>(&versions);
   AddVersion<crosapi::mojom::BrowserServiceHost>(&versions);
   AddVersion<crosapi::mojom::CertDatabase>(&versions);
diff --git a/chrome/browser/ash/crosapi/crosapi_ash.cc b/chrome/browser/ash/crosapi/crosapi_ash.cc
index 0d897ad..c0cdc0c 100644
--- a/chrome/browser/ash/crosapi/crosapi_ash.cc
+++ b/chrome/browser/ash/crosapi/crosapi_ash.cc
@@ -11,6 +11,7 @@
 #include "ash/components/account_manager/account_manager.h"
 #include "ash/components/account_manager/account_manager_ash.h"
 #include "ash/components/account_manager/account_manager_factory.h"
+#include "chrome/browser/ash/crosapi/automation_ash.h"
 #include "chrome/browser/ash/crosapi/browser_manager.h"
 #include "chrome/browser/ash/crosapi/browser_service_host_ash.h"
 #include "chrome/browser/ash/crosapi/cert_database_ash.h"
@@ -48,7 +49,8 @@
 namespace crosapi {
 
 CrosapiAsh::CrosapiAsh()
-    : browser_service_host_ash_(std::make_unique<BrowserServiceHostAsh>()),
+    : automation_ash_(std::make_unique<AutomationAsh>()),
+      browser_service_host_ash_(std::make_unique<BrowserServiceHostAsh>()),
       cert_database_ash_(std::make_unique<CertDatabaseAsh>()),
       clipboard_ash_(std::make_unique<ClipboardAsh>()),
       device_attributes_ash_(std::make_unique<DeviceAttributesAsh>()),
@@ -87,6 +89,11 @@
     disconnect_handler_map_.emplace(id, std::move(disconnect_handler));
 }
 
+void CrosapiAsh::BindAutomation(
+    mojo::PendingReceiver<mojom::Automation> receiver) {
+  automation_ash_->BindReceiver(std::move(receiver));
+}
+
 void CrosapiAsh::BindAccountManager(
     mojo::PendingReceiver<mojom::AccountManager> receiver) {
   // Assumptions:
diff --git a/chrome/browser/ash/crosapi/crosapi_ash.h b/chrome/browser/ash/crosapi/crosapi_ash.h
index ac15958..1cccbfc1a 100644
--- a/chrome/browser/ash/crosapi/crosapi_ash.h
+++ b/chrome/browser/ash/crosapi/crosapi_ash.h
@@ -17,6 +17,7 @@
 
 namespace crosapi {
 
+class AutomationAsh;
 class BrowserServiceHostAsh;
 class CertDatabaseAsh;
 class ClipboardAsh;
@@ -47,6 +48,8 @@
                     base::OnceClosure disconnect_handler);
 
   // crosapi::mojom::Crosapi:
+  void BindAutomation(
+      mojo::PendingReceiver<mojom::Automation> receiver) override;
   void BindAccountManager(
       mojo::PendingReceiver<mojom::AccountManager> receiver) override;
   void BindBrowserServiceHost(
@@ -100,10 +103,13 @@
     return browser_service_host_ash_.get();
   }
 
+  AutomationAsh* automation_ash() { return automation_ash_.get(); }
+
  private:
   // Called when a connection is lost.
   void OnDisconnected();
 
+  std::unique_ptr<AutomationAsh> automation_ash_;
   std::unique_ptr<BrowserServiceHostAsh> browser_service_host_ash_;
   std::unique_ptr<CertDatabaseAsh> cert_database_ash_;
   std::unique_ptr<ClipboardAsh> clipboard_ash_;
diff --git a/chrome/browser/ash/crosapi/message_center_ash.cc b/chrome/browser/ash/crosapi/message_center_ash.cc
index beccb8e5..c744b26 100644
--- a/chrome/browser/ash/crosapi/message_center_ash.cc
+++ b/chrome/browser/ash/crosapi/message_center_ash.cc
@@ -57,8 +57,13 @@
   rich_data.timestamp = notification->timestamp;
   if (!notification->image.isNull())
     rich_data.image = gfx::Image(notification->image);
-  if (!notification->badge.isNull())
+  if (!notification->badge.isNull()) {
     rich_data.small_image = gfx::Image(notification->badge);
+    if (notification->badge_needs_additional_masking_has_value) {
+      rich_data.small_image_needs_additional_masking =
+          notification->badge_needs_additional_masking;
+    }
+  }
   for (const auto& mojo_item : notification->items) {
     mc::NotificationItem item;
     item.title = mojo_item->title;
diff --git a/chrome/browser/ash/login/session/user_session_initializer.cc b/chrome/browser/ash/login/session/user_session_initializer.cc
index 87ea1b05..bee81773 100644
--- a/chrome/browser/ash/login/session/user_session_initializer.cc
+++ b/chrome/browser/ash/login/session/user_session_initializer.cc
@@ -15,6 +15,7 @@
 #include "chrome/browser/ash/plugin_vm/plugin_vm_manager.h"
 #include "chrome/browser/ash/plugin_vm/plugin_vm_manager_factory.h"
 #include "chrome/browser/ash/profiles/profile_helper.h"
+#include "chrome/browser/ash/settings/cros_settings.h"
 #include "chrome/browser/browser_process.h"
 #include "chrome/browser/browser_process_platform_part_chromeos.h"
 #include "chrome/browser/chromeos/arc/session/arc_service_launcher.h"
@@ -38,6 +39,7 @@
 #include "chrome/browser/ui/ash/holding_space/holding_space_keyed_service_factory.h"
 #include "chrome/browser/ui/ash/media_client_impl.h"
 #include "chrome/common/pref_names.h"
+#include "chromeos/dbus/pciguard/pciguard_client.h"
 #include "chromeos/network/network_cert_loader.h"
 #include "chromeos/tpm/install_attributes.h"
 #include "components/prefs/pref_service.h"
@@ -229,6 +231,14 @@
       plugin_vm_manager->OnPrimaryUserSessionStarted();
 
     VmCameraMicManager::Get()->OnPrimaryUserSessionStarted(primary_profile_);
+
+    bool pcie_tunneling_allowed = false;
+    CrosSettings::Get()->GetBoolean(
+        chromeos::kDevicePeripheralDataAccessEnabled, &pcie_tunneling_allowed);
+    // Pciguard can only be set by non-guest, primary users. By default,
+    // Pciguard is turned on.
+    PciguardClient::Get()->SendExternalPciDevicesPermissionState(
+        pcie_tunneling_allowed);
   }
 }
 
diff --git a/chrome/browser/ash/login/ui/captive_portal_dialog_delegate.cc b/chrome/browser/ash/login/ui/captive_portal_dialog_delegate.cc
index 31364de..6299bcce 100644
--- a/chrome/browser/ash/login/ui/captive_portal_dialog_delegate.cc
+++ b/chrome/browser/ash/login/ui/captive_portal_dialog_delegate.cc
@@ -10,6 +10,7 @@
 #include "chrome/browser/ui/ash/ash_util.h"
 #include "chrome/browser/ui/webui/chrome_web_contents_handler.h"
 #include "components/web_modal/web_contents_modal_dialog_manager.h"
+#include "content/public/browser/web_contents_observer.h"
 #include "ui/aura/window.h"
 #include "ui/display/display.h"
 #include "ui/display/screen.h"
@@ -18,10 +19,35 @@
 
 namespace chromeos {
 
+// Cleans up the delegate for a WebContentsModalDialogManager on destruction, or
+// on WebContents destruction, whichever comes first.
+class CaptivePortalDialogDelegate::ModalDialogManagerCleanup
+    : public content::WebContentsObserver {
+ public:
+  // This constructor automatically observes |web_contents| for its lifetime.
+  explicit ModalDialogManagerCleanup(content::WebContents* web_contents)
+      : content::WebContentsObserver(web_contents) {}
+  ModalDialogManagerCleanup(const ModalDialogManagerCleanup&) = delete;
+  ModalDialogManagerCleanup& operator=(const ModalDialogManagerCleanup&) =
+      delete;
+  ~ModalDialogManagerCleanup() override { ResetDelegate(); }
+
+  // content::WebContentsObserver:
+  void WebContentsDestroyed() override { ResetDelegate(); }
+
+  void ResetDelegate() {
+    if (!web_contents())
+      return;
+    web_modal::WebContentsModalDialogManager::FromWebContents(web_contents())
+        ->SetDelegate(nullptr);
+  }
+};
+
 CaptivePortalDialogDelegate::CaptivePortalDialogDelegate(
     views::WebDialogView* host_dialog_view)
     : host_view_(host_dialog_view),
       web_contents_(host_dialog_view->web_contents()) {
+  DCHECK(web_contents_);
   view_ =
       new views::WebDialogView(ProfileHelper::GetSigninProfile(), this,
                                std::make_unique<ChromeWebContentsHandler>());
@@ -46,10 +72,13 @@
   web_modal::WebContentsModalDialogManager::CreateForWebContents(web_contents_);
   web_modal::WebContentsModalDialogManager::FromWebContents(web_contents_)
       ->SetDelegate(this);
+  modal_dialog_manager_cleanup_ =
+      std::make_unique<ModalDialogManagerCleanup>(web_contents_);
 }
 
 CaptivePortalDialogDelegate::~CaptivePortalDialogDelegate() {
-  // TODO(jamescook): Clean up modal dialog delegate and observers.
+  for (auto& observer : modal_dialog_host_observer_list_)
+    observer.OnHostDestroying();
 }
 
 void CaptivePortalDialogDelegate::Show() {
@@ -133,12 +162,12 @@
 
 void CaptivePortalDialogDelegate::AddObserver(
     web_modal::ModalDialogHostObserver* observer) {
-  // TODO(jamescook): Store observers in a list.
+  modal_dialog_host_observer_list_.AddObserver(observer);
 }
 
 void CaptivePortalDialogDelegate::RemoveObserver(
     web_modal::ModalDialogHostObserver* observer) {
-  // TODO(jamescook): Store observers in a list.
+  modal_dialog_host_observer_list_.RemoveObserver(observer);
 }
 
 base::WeakPtr<CaptivePortalDialogDelegate>
diff --git a/chrome/browser/ash/login/ui/captive_portal_dialog_delegate.h b/chrome/browser/ash/login/ui/captive_portal_dialog_delegate.h
index 51c04223..4804c90 100644
--- a/chrome/browser/ash/login/ui/captive_portal_dialog_delegate.h
+++ b/chrome/browser/ash/login/ui/captive_portal_dialog_delegate.h
@@ -5,7 +5,10 @@
 #ifndef CHROME_BROWSER_ASH_LOGIN_UI_CAPTIVE_PORTAL_DIALOG_DELEGATE_H_
 #define CHROME_BROWSER_ASH_LOGIN_UI_CAPTIVE_PORTAL_DIALOG_DELEGATE_H_
 
+#include <memory>
+
 #include "base/memory/weak_ptr.h"
+#include "base/observer_list.h"
 #include "chrome/browser/ui/chrome_web_modal_dialog_manager_delegate.h"
 #include "components/web_modal/web_contents_modal_dialog_host.h"
 #include "ui/web_dialogs/web_dialog_delegate.h"
@@ -66,12 +69,20 @@
 
   base::WeakPtr<CaptivePortalDialogDelegate> GetWeakPtr();
 
+  views::Widget* widget_for_test() { return widget_; }
+  content::WebContents* web_contents_for_test() { return web_contents_; }
+
  private:
   views::Widget* widget_ = nullptr;
   views::WebDialogView* view_ = nullptr;
   views::WebDialogView* host_view_ = nullptr;
   content::WebContents* web_contents_ = nullptr;
 
+  class ModalDialogManagerCleanup;
+  std::unique_ptr<ModalDialogManagerCleanup> modal_dialog_manager_cleanup_;
+  base::ObserverList<web_modal::ModalDialogHostObserver>::Unchecked
+      modal_dialog_host_observer_list_;
+
   base::WeakPtrFactory<CaptivePortalDialogDelegate> weak_ptr_factory_{this};
 };
 
diff --git a/chrome/browser/ash/login/ui/captive_portal_dialog_delegate_browsertest.cc b/chrome/browser/ash/login/ui/captive_portal_dialog_delegate_browsertest.cc
new file mode 100644
index 0000000..eb250e1
--- /dev/null
+++ b/chrome/browser/ash/login/ui/captive_portal_dialog_delegate_browsertest.cc
@@ -0,0 +1,82 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ash/login/ui/captive_portal_dialog_delegate.h"
+
+#include "chrome/browser/ash/login/login_manager_test.h"
+#include "chrome/browser/ash/login/test/login_manager_mixin.h"
+#include "chrome/browser/ash/login/ui/login_display_host.h"
+#include "chrome/browser/ash/login/ui/login_display_host_mojo.h"
+#include "components/constrained_window/constrained_window_views.h"
+#include "content/public/test/browser_test.h"
+#include "ui/views/accessibility/view_accessibility.h"
+#include "ui/views/test/widget_test.h"
+#include "ui/views/view.h"
+#include "ui/views/widget/widget.h"
+#include "ui/views/window/dialog_delegate.h"
+
+namespace chromeos {
+namespace {
+
+// A simulated modal dialog. Taking focus seems important to repro the crash,
+// but I'm not sure why.
+class ChildModalDialogDelegate : public views::DialogDelegateView {
+ public:
+  ChildModalDialogDelegate() {
+    // Our views::Widget will delete us.
+    DCHECK(owned_by_widget());
+    SetModalType(ui::MODAL_TYPE_CHILD);
+    SetFocusBehavior(FocusBehavior::ALWAYS);
+    // Dialogs that take focus must have a name to pass accessibility checks.
+    GetViewAccessibility().OverrideName("Test dialog");
+  }
+  ChildModalDialogDelegate(const ChildModalDialogDelegate&) = delete;
+  ChildModalDialogDelegate& operator=(const ChildModalDialogDelegate&) = delete;
+  ~ChildModalDialogDelegate() override = default;
+};
+
+class CaptivePortalDialogDelegateTest : public LoginManagerTest {
+ public:
+  // Simulate a login screen with an existing user.
+  CaptivePortalDialogDelegateTest() { login_mixin_.AppendRegularUsers(1); }
+  CaptivePortalDialogDelegateTest(const CaptivePortalDialogDelegateTest&) =
+      delete;
+  CaptivePortalDialogDelegateTest& operator=(
+      const CaptivePortalDialogDelegateTest&) = delete;
+  ~CaptivePortalDialogDelegateTest() override = default;
+
+  LoginManagerMixin login_mixin_{&mixin_host_};
+};
+
+// Regression test for use-after-free and crash. https://1170577
+IN_PROC_BROWSER_TEST_F(CaptivePortalDialogDelegateTest,
+                       ShowModalDialogDoesNotCrash) {
+  // Show the captive portal dialog.
+  LoginDisplayHostMojo* login_display_host =
+      static_cast<LoginDisplayHostMojo*>(LoginDisplayHost::default_host());
+  OobeUIDialogDelegate* oobe_ui_dialog = login_display_host->dialog_for_test();
+  CaptivePortalDialogDelegate* portal_dialog =
+      oobe_ui_dialog->captive_portal_delegate_for_test();
+  views::test::WidgetVisibleWaiter show_waiter(
+      portal_dialog->widget_for_test());
+  portal_dialog->Show();
+  show_waiter.Wait();
+
+  // Show a child modal dialog, similar to an http auth modal dialog.
+  content::WebContents* web_contents = portal_dialog->web_contents_for_test();
+  // The ChildModalDialogDelegate is owned by the views system.
+  constrained_window::ShowWebModalDialogViews(new ChildModalDialogDelegate,
+                                              web_contents);
+
+  // Close the parent dialog.
+  views::test::WidgetDestroyedWaiter destroyed_waiter(
+      portal_dialog->widget_for_test());
+  portal_dialog->Close();
+  destroyed_waiter.Wait();
+
+  // No crash.
+}
+
+}  // namespace
+}  // namespace chromeos
diff --git a/chrome/browser/ash/login/ui/login_display_host_mojo.h b/chrome/browser/ash/login/ui/login_display_host_mojo.h
index 26f927b..a47518b 100644
--- a/chrome/browser/ash/login/ui/login_display_host_mojo.h
+++ b/chrome/browser/ash/login/ui/login_display_host_mojo.h
@@ -137,6 +137,8 @@
   // messages on the Views and WebUI side. Consider removing.
   bool IsOobeUIDialogVisible() const;
 
+  OobeUIDialogDelegate* dialog_for_test() { return dialog_; }
+
  private:
   void LoadOobeDialog();
 
diff --git a/chrome/browser/ash/login/ui/oobe_ui_dialog_delegate.h b/chrome/browser/ash/login/ui/oobe_ui_dialog_delegate.h
index 1580ffd..39ed4b9 100644
--- a/chrome/browser/ash/login/ui/oobe_ui_dialog_delegate.h
+++ b/chrome/browser/ash/login/ui/oobe_ui_dialog_delegate.h
@@ -84,6 +84,10 @@
 
   views::View* GetWebDialogView();
 
+  CaptivePortalDialogDelegate* captive_portal_delegate_for_test() {
+    return captive_portal_delegate_.get();
+  }
+
  private:
   // ui::WebDialogDelegate:
   ui::ModalType GetDialogModalType() const override;
diff --git a/chrome/browser/background/background_contents_service.cc b/chrome/browser/background/background_contents_service.cc
index 19eb67e..3b5224e 100644
--- a/chrome/browser/background/background_contents_service.cc
+++ b/chrome/browser/background/background_contents_service.cc
@@ -652,7 +652,7 @@
   DCHECK(IsTracked(background_contents));
   const std::string& appid = GetParentApplicationId(background_contents);
   DictionaryPrefUpdate update(prefs_, prefs::kRegisteredBackgroundContents);
-  update.Get()->RemoveWithoutPathExpansion(appid, nullptr);
+  update.Get()->RemoveKey(appid);
 }
 
 void BackgroundContentsService::ShutdownAssociatedBackgroundContents(
diff --git a/chrome/browser/chrome_content_browser_client.cc b/chrome/browser/chrome_content_browser_client.cc
index e266c95..c526332 100644
--- a/chrome/browser/chrome_content_browser_client.cc
+++ b/chrome/browser/chrome_content_browser_client.cc
@@ -659,6 +659,7 @@
 
 #if BUILDFLAG(IS_CHROMEOS_LACROS)
 #include "chrome/browser/chrome_browser_main_parts_lacros.h"
+#include "chrome/browser/lacros/chrome_browser_main_extra_parts_lacros.h"
 #include "chrome/browser/ui/views/chrome_browser_main_extra_parts_views_lacros.h"
 #include "chromeos/lacros/lacros_chrome_service_impl.h"
 #include "ui/base/ui_base_switches.h"
@@ -1400,6 +1401,10 @@
   main_parts->AddParts(std::make_unique<ChromeBrowserMainExtraPartsAsh>());
 #endif
 
+#if BUILDFLAG(IS_CHROMEOS_LACROS)
+  main_parts->AddParts(std::make_unique<ChromeBrowserMainExtraPartsLacros>());
+#endif
+
 #if defined(USE_X11) || defined(USE_OZONE)
   main_parts->AddParts(std::make_unique<ChromeBrowserMainExtraPartsOzone>());
 #endif
@@ -5433,9 +5438,8 @@
                                       recv_bytes, sent_bytes);
   }
 #if !defined(OS_ANDROID)
-  task_manager::TaskManagerInterface::GetTaskManager()
-      ->UpdateAccumulatedStatsNetworkForRoute(process_id, routing_id,
-                                              recv_bytes, sent_bytes);
+  task_manager::TaskManagerInterface::UpdateAccumulatedStatsNetworkForRoute(
+      process_id, routing_id, recv_bytes, sent_bytes);
 #endif
 }
 
diff --git a/chrome/browser/chromeos/BUILD.gn b/chrome/browser/chromeos/BUILD.gn
index b25b1bbe..9765280 100644
--- a/chrome/browser/chromeos/BUILD.gn
+++ b/chrome/browser/chromeos/BUILD.gn
@@ -660,6 +660,8 @@
     "../ash/certificate_provider/sign_requests.h",
     "../ash/certificate_provider/thread_safe_certificate_map.cc",
     "../ash/certificate_provider/thread_safe_certificate_map.h",
+    "../ash/crosapi/automation_ash.cc",
+    "../ash/crosapi/automation_ash.h",
     "../ash/crosapi/browser_loader.cc",
     "../ash/crosapi/browser_loader.h",
     "../ash/crosapi/browser_manager.cc",
diff --git a/chrome/browser/chromeos/arc/intent_helper/arc_settings_service.cc b/chrome/browser/chromeos/arc/intent_helper/arc_settings_service.cc
index 7684c93..d4614a5 100644
--- a/chrome/browser/chromeos/arc/intent_helper/arc_settings_service.cc
+++ b/chrome/browser/chromeos/arc/intent_helper/arc_settings_service.cc
@@ -49,6 +49,7 @@
 #include "components/proxy_config/pref_proxy_config_tracker_impl.h"
 #include "components/proxy_config/proxy_config_dictionary.h"
 #include "components/proxy_config/proxy_config_pref_names.h"
+#include "components/proxy_config/proxy_prefs.h"
 #include "net/base/url_util.h"
 #include "net/proxy_resolution/proxy_bypass_rules.h"
 #include "net/proxy_resolution/proxy_config.h"
@@ -94,6 +95,13 @@
   return !host->empty() && *port;
 }
 
+bool IsProxyAutoDetectionConfigured(const base::Value* proxy_config_dict) {
+  ProxyConfigDictionary dict(proxy_config_dict->Clone());
+  ProxyPrefs::ProxyMode mode;
+  dict.GetMode(&mode);
+  return mode == ProxyPrefs::MODE_AUTO_DETECT;
+}
+
 }  // namespace
 
 namespace arc {
@@ -233,6 +241,9 @@
   std::string default_network_name_;
   // Proxy configuration of the default network.
   base::Value default_proxy_config_;
+  // The PAC URL associated with `default_network_name_`, received via the DHCP
+  // discovery method.
+  GURL dhcp_wpad_url_;
 
   DISALLOW_COPY_AND_ASSIGN(ArcSettingsServiceImpl);
 };
@@ -303,15 +314,35 @@
 }
 
 // This function is called when the default network changes or when any of its
-// properties change.
+// properties change. If the proxy configuration of the default network has
+// changed, this method will call `SyncProxySettings` which syncs the proxy
+// settings with ARC. Proxy changes on the default network are triggered by:
+// - a user changing the proxy in the Network Settings UI;
+// - ONC policy changes;
+// - DHCP settings the WPAD URL via  option 252.
 void ArcSettingsServiceImpl::DefaultNetworkChanged(
     const chromeos::NetworkState* network) {
-  bool sync_proxy = false;
-  // kProxy pref has more priority than the default network update.
-  // If a default network is changed to the network with ONC policy with proxy
-  // settings, it should be translated here.
-  if (!network || IsPrefProxyConfigApplied())
+  if (!network)
     return;
+
+  bool dhcp_wpad_url_changed =
+      dhcp_wpad_url_ != network->GetWebProxyAutoDiscoveryUrl();
+  dhcp_wpad_url_ = network->GetWebProxyAutoDiscoveryUrl();
+
+  if (IsPrefProxyConfigApplied()) {
+    //  Normally, we would ignore proxy changes coming from the default
+    //  network because the kProxy pref has priority. If the proxy is
+    //  configured to use the Web Proxy Auto-Discovery (WPAD) Protocol via the
+    //  DHCP discovery method, the PAC URL will be propagated to Chrome via the
+    //  default network properties.
+    if (dhcp_wpad_url_changed && IsProxyAutoDetectionConfigured(GetPrefs()->Get(
+                                     proxy_config::prefs::kProxy))) {
+      SyncProxySettings();
+    }
+    return;
+  }
+
+  bool sync_proxy = false;
   // Trigger a proxy settings sync to ARC if the default network changes.
   if (default_network_name_ != network->name()) {
     default_network_name_ = network->name();
@@ -325,6 +356,14 @@
     default_proxy_config_ = network->proxy_config().Clone();
     sync_proxy = true;
   }
+
+  // Check if proxy auto detection is enabled. If yes, and the PAC URL set via
+  // DHCP has changed, propagate the change to ARC.
+  if (!default_proxy_config_.is_none() && dhcp_wpad_url_changed &&
+      IsProxyAutoDetectionConfigured(&default_proxy_config_)) {
+    sync_proxy = true;
+  }
+
   if (!sync_proxy)
     return;
 
@@ -522,9 +561,16 @@
     case ProxyPrefs::MODE_SYSTEM:
       VLOG(1) << "The system mode is not translated.";
       return;
-    case ProxyPrefs::MODE_AUTO_DETECT:
-      extras.SetString("pacUrl", "http://wpad/wpad.dat");
+    case ProxyPrefs::MODE_AUTO_DETECT: {
+      // WPAD with DHCP has a higher priority than DNS.
+      if (dhcp_wpad_url_.is_valid()) {
+        extras.SetString("pacUrl", dhcp_wpad_url_.spec());
+      } else {
+        // Fallback to WPAD via DNS.
+        extras.SetString("pacUrl", "http://wpad/wpad.dat");
+      }
       break;
+    }
     case ProxyPrefs::MODE_PAC_SCRIPT: {
       std::string pac_url;
       if (!proxy_config_dict->GetPacUrl(&pac_url)) {
diff --git a/chrome/browser/chromeos/arc/intent_helper/arc_settings_service_browsertest.cc b/chrome/browser/chromeos/arc/intent_helper/arc_settings_service_browsertest.cc
index d8b6129b..14d0743 100644
--- a/chrome/browser/chromeos/arc/intent_helper/arc_settings_service_browsertest.cc
+++ b/chrome/browser/chromeos/arc/intent_helper/arc_settings_service_browsertest.cc
@@ -21,6 +21,7 @@
 #include "chrome/common/pref_names.h"
 #include "chrome/test/base/in_process_browser_test.h"
 #include "chromeos/dbus/dbus_thread_manager.h"
+#include "chromeos/dbus/shill/shill_ipconfig_client.h"
 #include "chromeos/dbus/shill/shill_profile_client.h"
 #include "chromeos/dbus/shill/shill_service_client.h"
 #include "chromeos/network/network_handler.h"
@@ -856,4 +857,72 @@
             proxy_sync_count);
 }
 
+IN_PROC_BROWSER_TEST_F(ArcSettingsServiceTest, WebProxyAutoDiscovery) {
+  fake_intent_helper_instance_->clear_broadcasts();
+
+  // Set the proxy config to use auto-discovery. There's no PAC URL set via DHCP
+  // so the URL "http://wpad/wpad.dat" set via DNS will be propagated to ARC.
+  base::Value proxy_config_wpad(base::Value::Type::DICTIONARY);
+  proxy_config_wpad.SetKey("mode",
+                           base::Value(ProxyPrefs::kAutoDetectProxyModeName));
+  browser()->profile()->GetPrefs()->Set(proxy_config::prefs::kProxy,
+                                        proxy_config_wpad);
+
+  RunUntilIdle();
+  const char kWebProxyAutodetectionUrl[] = "www.proxyurl.com:443";
+
+  chromeos::ShillIPConfigClient::TestInterface* ip_config_client =
+      chromeos::DBusThreadManager::Get()
+          ->GetShillIPConfigClient()
+          ->GetTestInterface();
+
+  // Set the WPAD DHCP URL. This should now have precedence over the PAC URL set
+  // via DNS.
+  base::Value wpad_config(base::Value::Type::DICTIONARY);
+  wpad_config.SetKey(shill::kWebProxyAutoDiscoveryUrlProperty,
+                     base::Value(kWebProxyAutodetectionUrl));
+  const std::string kIPConfigPath = "test_ip_config";
+  ip_config_client->AddIPConfig(kIPConfigPath, wpad_config);
+
+  chromeos::ShillServiceClient::TestInterface* service_test =
+      chromeos::DBusThreadManager::Get()
+          ->GetShillServiceClient()
+          ->GetTestInterface();
+
+  service_test->SetServiceProperty(kDefaultServicePath,
+                                   shill::kIPConfigProperty,
+                                   base::Value(kIPConfigPath));
+  RunUntilIdle();
+
+  // Remove the proxy.
+  base::Value proxy_config_direct(base::Value::Type::DICTIONARY);
+  proxy_config_direct.SetKey("mode",
+                             base::Value(ProxyPrefs::kDirectProxyModeName));
+  browser()->profile()->GetPrefs()->Set(proxy_config::prefs::kProxy,
+                                        proxy_config_direct);
+
+  RunUntilIdle();
+  base::Value expected_proxy_config_wpad_dns(base::Value::Type::DICTIONARY);
+  expected_proxy_config_wpad_dns.SetKey(
+      "mode", base::Value(ProxyPrefs::kAutoDetectProxyModeName));
+  expected_proxy_config_wpad_dns.SetKey("pacUrl",
+                                        base::Value("http://wpad/wpad.dat"));
+
+  base::Value expected_proxy_config_wpad_dhcp(base::Value::Type::DICTIONARY);
+  expected_proxy_config_wpad_dhcp.SetKey(
+      "mode", base::Value(ProxyPrefs::kAutoDetectProxyModeName));
+  expected_proxy_config_wpad_dhcp.SetKey(
+      "pacUrl", base::Value(kWebProxyAutodetectionUrl));
+
+  base::Value expected_proxy_config_direct(base::Value::Type::DICTIONARY);
+  expected_proxy_config_direct.SetKey(
+      "mode", base::Value(ProxyPrefs::kDirectProxyModeName));
+
+  EXPECT_EQ(CountProxyBroadcasts(fake_intent_helper_instance_->broadcasts(),
+                                 {&expected_proxy_config_wpad_dns,
+                                  &expected_proxy_config_wpad_dhcp,
+                                  &expected_proxy_config_direct}),
+            3);
+}
+
 }  // namespace arc
diff --git a/chrome/browser/chromeos/chrome_browser_main_chromeos.cc b/chrome/browser/chromeos/chrome_browser_main_chromeos.cc
index fd0ee2f..d0595b9 100644
--- a/chrome/browser/chromeos/chrome_browser_main_chromeos.cc
+++ b/chrome/browser/chromeos/chrome_browser_main_chromeos.cc
@@ -161,7 +161,6 @@
 #include "chromeos/cryptohome/system_salt_getter.h"
 #include "chromeos/dbus/constants/cryptohome_key_delegate_constants.h"
 #include "chromeos/dbus/dbus_thread_manager.h"
-#include "chromeos/dbus/pciguard/pciguard_client.h"
 #include "chromeos/dbus/power/power_manager_client.h"
 #include "chromeos/dbus/power/power_policy_controller.h"
 #include "chromeos/dbus/services/cros_dbus_service.h"
@@ -1115,14 +1114,6 @@
   bool pcie_tunneling_allowed = false;
   CrosSettings::Get()->GetBoolean(chromeos::kDevicePeripheralDataAccessEnabled,
                                   &pcie_tunneling_allowed);
-
-  // External PCI devices are only allowed in non-guest, primary users.
-  if (!user_manager::UserManager::Get()->IsLoggedInAsGuest() &&
-      ProfileHelper::IsPrimaryProfile(profile())) {
-    PciguardClient::Get()->SendExternalPciDevicesPermissionState(
-        pcie_tunneling_allowed);
-  }
-
   if (chromeos::features::IsPciguardUiEnabled()) {
     ash::PciePeripheralManager::Initialize(
         user_manager::UserManager::Get()->IsLoggedInAsGuest(),
diff --git a/chrome/browser/chromeos/platform_keys/extension_platform_keys_service.cc b/chrome/browser/chromeos/platform_keys/extension_platform_keys_service.cc
index adac86b7..12fd2e3 100644
--- a/chrome/browser/chromeos/platform_keys/extension_platform_keys_service.cc
+++ b/chrome/browser/chromeos/platform_keys/extension_platform_keys_service.cc
@@ -28,7 +28,6 @@
 #include "chrome/browser/profiles/profile.h"
 #include "content/public/browser/browser_thread.h"
 #include "extensions/browser/extension_registry.h"
-#include "extensions/browser/state_store.h"
 #include "extensions/common/extension.h"
 #include "extensions/common/features/behavior_feature.h"
 #include "extensions/common/features/feature.h"
@@ -763,11 +762,7 @@
 ExtensionPlatformKeysService::SelectDelegate::~SelectDelegate() {}
 
 ExtensionPlatformKeysService::ExtensionPlatformKeysService(
-    bool profile_is_managed,
-    PrefService* profile_prefs,
-    policy::PolicyService* profile_policies,
-    content::BrowserContext* browser_context,
-    extensions::StateStore* state_store)
+    content::BrowserContext* browser_context)
     : browser_context_(browser_context),
       platform_keys_service_(
           platform_keys::PlatformKeysServiceFactory::GetForBrowserContext(
@@ -777,7 +772,6 @@
               GetForBrowserContext(browser_context)) {
   DCHECK(platform_keys_service_);
   DCHECK(browser_context);
-  DCHECK(state_store);
 }
 
 ExtensionPlatformKeysService::~ExtensionPlatformKeysService() {}
diff --git a/chrome/browser/chromeos/platform_keys/extension_platform_keys_service.h b/chrome/browser/chromeos/platform_keys/extension_platform_keys_service.h
index 3718c2ce..e9ae589 100644
--- a/chrome/browser/chromeos/platform_keys/extension_platform_keys_service.h
+++ b/chrome/browser/chromeos/platform_keys/extension_platform_keys_service.h
@@ -18,26 +18,16 @@
 #include "chrome/browser/chromeos/platform_keys/platform_keys_service.h"
 #include "components/keyed_service/core/keyed_service.h"
 
-class PrefService;
-
 namespace content {
 class BrowserContext;
 class WebContents;
 }  // namespace content
 
-namespace extensions {
-class StateStore;
-}
-
 namespace net {
 class X509Certificate;
 typedef std::vector<scoped_refptr<X509Certificate>> CertificateList;
 }  // namespace net
 
-namespace policy {
-class PolicyService;
-}
-
 namespace chromeos {
 
 class ExtensionPlatformKeysService : public KeyedService {
@@ -72,17 +62,9 @@
     DISALLOW_ASSIGN(SelectDelegate);
   };
 
-  // Stores registration information in |state_store|, i.e. for each extension
-  // the list of public keys that are valid to be used for signing. See
-  // |ExtensionKeyPermissionsService| for more details.
-  // |browser_context| and |state_store| must not be null and outlive this
-  // object.
+  // |browser_context| must not be null and must outlive this object.
   explicit ExtensionPlatformKeysService(
-      bool profile_is_managed,
-      PrefService* profile_prefs,
-      policy::PolicyService* profile_policies,
-      content::BrowserContext* browser_context,
-      extensions::StateStore* state_store);
+      content::BrowserContext* browser_context);
 
   ~ExtensionPlatformKeysService() override;
 
diff --git a/chrome/browser/chromeos/platform_keys/extension_platform_keys_service_factory.cc b/chrome/browser/chromeos/platform_keys/extension_platform_keys_service_factory.cc
index a63e44ee..125e5b4 100644
--- a/chrome/browser/chromeos/platform_keys/extension_platform_keys_service_factory.cc
+++ b/chrome/browser/chromeos/platform_keys/extension_platform_keys_service_factory.cc
@@ -17,10 +17,7 @@
 #include "chrome/browser/chromeos/platform_keys/extension_platform_keys_service.h"
 #include "chrome/browser/chromeos/platform_keys/key_permissions/key_permissions_service_factory.h"
 #include "chrome/browser/chromeos/platform_keys/platform_keys_service_factory.h"
-#include "chrome/browser/extensions/extension_system_factory.h"
-#include "chrome/browser/policy/profile_policy_connector.h"
 #include "chrome/browser/profiles/incognito_helpers.h"
-#include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/platform_keys_certificate_selector_chromeos.h"
 #include "components/keyed_service/content/browser_context_dependency_manager.h"
 #include "extensions/browser/extension_registry.h"
@@ -91,7 +88,6 @@
     : BrowserContextKeyedServiceFactory(
           "ExtensionPlatformKeysService",
           BrowserContextDependencyManager::GetInstance()) {
-  DependsOn(extensions::ExtensionSystemFactory::GetInstance());
   DependsOn(chromeos::platform_keys::PlatformKeysServiceFactory::GetInstance());
   DependsOn(
       chromeos::platform_keys::KeyPermissionsServiceFactory::GetInstance());
@@ -107,18 +103,8 @@
 
 KeyedService* ExtensionPlatformKeysServiceFactory::BuildServiceInstanceFor(
     content::BrowserContext* context) const {
-  extensions::StateStore* const store =
-      extensions::ExtensionSystem::Get(context)->state_store();
-
-  policy::ProfilePolicyConnector* const policy_connector =
-      Profile::FromBrowserContext(context)->GetProfilePolicyConnector();
-
-  Profile* const profile = Profile::FromBrowserContext(context);
-
   ExtensionPlatformKeysService* const service =
-      new ExtensionPlatformKeysService(
-          policy_connector->IsManaged(), profile->GetPrefs(),
-          policy_connector->policy_service(), context, store);
+      new ExtensionPlatformKeysService(context);
 
   service->SetSelectDelegate(std::make_unique<DefaultSelectDelegate>());
   return service;
diff --git a/chrome/browser/component_updater/chrome_origin_trials_component_installer.cc b/chrome/browser/component_updater/chrome_origin_trials_component_installer.cc
index 15dfd84..bfcb1c2 100644
--- a/chrome/browser/component_updater/chrome_origin_trials_component_installer.cc
+++ b/chrome/browser/component_updater/chrome_origin_trials_component_installer.cc
@@ -4,24 +4,16 @@
 
 #include "chrome/browser/component_updater/chrome_origin_trials_component_installer.h"
 
+#include <cstdint>
+#include <utility>
+#include <vector>
+
+#include "base/callback.h"
 #include "chrome/browser/browser_process.h"
-#include "chrome/common/chrome_switches.h"
-#include "components/embedder_support/origin_trials/pref_names.h"
-#include "components/prefs/pref_service.h"
-#include "components/prefs/scoped_user_pref_update.h"
+#include "components/embedder_support/origin_trials/component_updater_utils.h"
 
 namespace component_updater {
 
-namespace {
-
-static const char kManifestPublicKeyPath[] = "origin-trials.public-key";
-static const char kManifestDisabledFeaturesPath[] =
-    "origin-trials.disabled-features";
-static const char kManifestDisabledTokenSignaturesPath[] =
-    "origin-trials.disabled-tokens.signatures";
-
-}  // namespace
-
 void ChromeOriginTrialsComponentInstallerPolicy::ComponentReady(
     const base::Version& version,
     const base::FilePath& install_dir,
@@ -30,36 +22,8 @@
   // local_state. These will be used on the next browser restart.
   // If an individual configuration value is missing, treat as a reset to the
   // browser defaults.
-  PrefService* local_state = g_browser_process->local_state();
-  std::string override_public_key;
-  if (manifest->GetString(kManifestPublicKeyPath, &override_public_key)) {
-    local_state->Set(embedder_support::prefs::kOriginTrialPublicKey,
-                     base::Value(override_public_key));
-  } else {
-    local_state->ClearPref(embedder_support::prefs::kOriginTrialPublicKey);
-  }
-  base::ListValue* override_disabled_feature_list = nullptr;
-  const bool manifest_has_disabled_features = manifest->GetList(
-      kManifestDisabledFeaturesPath, &override_disabled_feature_list);
-  if (manifest_has_disabled_features &&
-      !override_disabled_feature_list->empty()) {
-    ListPrefUpdate update(
-        local_state, embedder_support::prefs::kOriginTrialDisabledFeatures);
-    update->Swap(override_disabled_feature_list);
-  } else {
-    local_state->ClearPref(
-        embedder_support::prefs::kOriginTrialDisabledFeatures);
-  }
-  base::ListValue* disabled_tokens_list = nullptr;
-  const bool manifest_has_disabled_tokens = manifest->GetList(
-      kManifestDisabledTokenSignaturesPath, &disabled_tokens_list);
-  if (manifest_has_disabled_tokens && !disabled_tokens_list->empty()) {
-    ListPrefUpdate update(local_state,
-                          embedder_support::prefs::kOriginTrialDisabledTokens);
-    update->Swap(disabled_tokens_list);
-  } else {
-    local_state->ClearPref(embedder_support::prefs::kOriginTrialDisabledTokens);
-  }
+  embedder_support::ReadOriginTrialsConfigAndPopulateLocalState(
+      g_browser_process->local_state(), std::move(manifest));
 }
 
 void RegisterOriginTrialsComponent(ComponentUpdateService* updater_service) {
diff --git a/chrome/browser/component_updater/origin_trials_component_installer_unittest.cc b/chrome/browser/component_updater/origin_trials_component_installer_unittest.cc
index 6ac2bf3..259423b 100644
--- a/chrome/browser/component_updater/origin_trials_component_installer_unittest.cc
+++ b/chrome/browser/component_updater/origin_trials_component_installer_unittest.cc
@@ -4,10 +4,8 @@
 
 #include "chrome/browser/component_updater/chrome_origin_trials_component_installer.h"
 
-#include <string>
 #include <utility>
 
-#include "base/files/file_path.h"
 #include "base/files/scoped_temp_dir.h"
 #include "base/values.h"
 #include "base/version.h"
@@ -26,31 +24,8 @@
 // will be generated server-side, so any changes need to be intentional and
 // coordinated.
 static const char kManifestOriginTrialsKey[] = "origin-trials";
-static const char kManifestPublicKeyPath[] = "origin-trials.public-key";
-static const char kManifestDisabledFeaturesPath[] =
-    "origin-trials.disabled-features";
-static const char kManifestDisabledTokensPath[] =
-    "origin-trials.disabled-tokens";
-static const char kManifestDisabledTokenSignaturesPath[] =
-    "origin-trials.disabled-tokens.signatures";
-
 static const char kTestUpdateVersion[] = "1.0";
 static const char kExistingPublicKey[] = "existing public key";
-static const char kNewPublicKey[] = "new public key";
-static const char kExistingDisabledFeature[] = "already disabled";
-static const std::vector<std::string> kExistingDisabledFeatures = {
-    kExistingDisabledFeature};
-static const char kNewDisabledFeature1[] = "newly disabled 1";
-static const char kNewDisabledFeature2[] = "newly disabled 2";
-static const std::vector<std::string> kNewDisabledFeatures = {
-    kNewDisabledFeature1, kNewDisabledFeature2};
-static const char kExistingDisabledToken[] = "already disabled token";
-static const std::vector<std::string> kExistingDisabledTokens = {
-    kExistingDisabledToken};
-static const char kNewDisabledToken1[] = "newly disabled token 1";
-static const char kNewDisabledToken2[] = "newly disabled token 2";
-static const std::vector<std::string> kNewDisabledTokens = {kNewDisabledToken1,
-                                                            kNewDisabledToken2};
 
 }  // namespace
 
@@ -80,70 +55,6 @@
                             std::move(manifest));
   }
 
-  void AddDisabledFeaturesToPrefs(const std::vector<std::string>& features) {
-    base::ListValue disabled_feature_list;
-    disabled_feature_list.AppendStrings(features);
-    ListPrefUpdate update(
-        local_state(), embedder_support::prefs::kOriginTrialDisabledFeatures);
-    update->Swap(&disabled_feature_list);
-  }
-
-  void CheckDisabledFeaturesPrefs(const std::vector<std::string>& features) {
-    ASSERT_FALSE(features.empty());
-
-    ASSERT_TRUE(local_state()->HasPrefPath(
-        embedder_support::prefs::kOriginTrialDisabledFeatures));
-
-    const base::ListValue* disabled_feature_list = local_state()->GetList(
-        embedder_support::prefs::kOriginTrialDisabledFeatures);
-    ASSERT_TRUE(disabled_feature_list);
-
-    ASSERT_EQ(features.size(), disabled_feature_list->GetSize());
-
-    std::string disabled_feature;
-    for (size_t i = 0; i < features.size(); ++i) {
-      const bool found = disabled_feature_list->GetString(i, &disabled_feature);
-      EXPECT_TRUE(found) << "Entry not found or not a string at index " << i;
-      if (!found) {
-        continue;
-      }
-      EXPECT_EQ(features[i], disabled_feature)
-          << "Feature lists differ at index " << i;
-    }
-  }
-
-  void AddDisabledTokensToPrefs(const std::vector<std::string>& tokens) {
-    base::ListValue disabled_token_list;
-    disabled_token_list.AppendStrings(tokens);
-    ListPrefUpdate update(local_state(),
-                          embedder_support::prefs::kOriginTrialDisabledTokens);
-    update->Swap(&disabled_token_list);
-  }
-
-  void CheckDisabledTokensPrefs(const std::vector<std::string>& tokens) {
-    ASSERT_FALSE(tokens.empty());
-
-    ASSERT_TRUE(local_state()->HasPrefPath(
-        embedder_support::prefs::kOriginTrialDisabledTokens));
-
-    const base::ListValue* disabled_token_list = local_state()->GetList(
-        embedder_support::prefs::kOriginTrialDisabledTokens);
-    ASSERT_TRUE(disabled_token_list);
-
-    ASSERT_EQ(tokens.size(), disabled_token_list->GetSize());
-
-    std::string disabled_token;
-    for (size_t i = 0; i < tokens.size(); ++i) {
-      const bool found = disabled_token_list->GetString(i, &disabled_token);
-      EXPECT_TRUE(found) << "Entry not found or not a string at index " << i;
-      if (!found) {
-        continue;
-      }
-      EXPECT_EQ(tokens[i], disabled_token)
-          << "Token lists differ at index " << i;
-    }
-  }
-
   PrefService* local_state() { return g_browser_process->local_state(); }
 
  protected:
@@ -170,158 +81,4 @@
       embedder_support::prefs::kOriginTrialPublicKey));
 }
 
-TEST_F(OriginTrialsComponentInstallerTest, PublicKeySetWhenOverrideExists) {
-  ASSERT_FALSE(local_state()->HasPrefPath(
-      embedder_support::prefs::kOriginTrialPublicKey));
-
-  auto manifest = std::make_unique<base::DictionaryValue>();
-  manifest->SetString(kManifestPublicKeyPath, kNewPublicKey);
-  LoadUpdates(std::move(manifest));
-
-  EXPECT_EQ(kNewPublicKey, local_state()->GetString(
-                               embedder_support::prefs::kOriginTrialPublicKey));
-}
-
-TEST_F(OriginTrialsComponentInstallerTest,
-       DisabledFeaturesResetToDefaultWhenListMissing) {
-  AddDisabledFeaturesToPrefs(kExistingDisabledFeatures);
-  ASSERT_TRUE(local_state()->HasPrefPath(
-      embedder_support::prefs::kOriginTrialDisabledFeatures));
-
-  // Load with empty section in manifest
-  LoadUpdates(nullptr);
-
-  EXPECT_FALSE(local_state()->HasPrefPath(
-      embedder_support::prefs::kOriginTrialDisabledFeatures));
-}
-
-TEST_F(OriginTrialsComponentInstallerTest,
-       DisabledFeaturesResetToDefaultWhenListEmpty) {
-  AddDisabledFeaturesToPrefs(kExistingDisabledFeatures);
-  ASSERT_TRUE(local_state()->HasPrefPath(
-      embedder_support::prefs::kOriginTrialDisabledFeatures));
-
-  auto manifest = std::make_unique<base::DictionaryValue>();
-  auto disabled_feature_list = std::make_unique<base::ListValue>();
-  manifest->Set(kManifestDisabledFeaturesPath,
-                std::move(disabled_feature_list));
-
-  LoadUpdates(std::move(manifest));
-
-  EXPECT_FALSE(local_state()->HasPrefPath(
-      embedder_support::prefs::kOriginTrialDisabledFeatures));
-}
-
-TEST_F(OriginTrialsComponentInstallerTest, DisabledFeaturesSetWhenListExists) {
-  ASSERT_FALSE(local_state()->HasPrefPath(
-      embedder_support::prefs::kOriginTrialDisabledFeatures));
-
-  auto manifest = std::make_unique<base::DictionaryValue>();
-  auto disabled_feature_list = std::make_unique<base::ListValue>();
-  disabled_feature_list->AppendString(kNewDisabledFeature1);
-  manifest->Set(kManifestDisabledFeaturesPath,
-                std::move(disabled_feature_list));
-
-  LoadUpdates(std::move(manifest));
-
-  std::vector<std::string> features = {kNewDisabledFeature1};
-  CheckDisabledFeaturesPrefs(features);
-}
-
-TEST_F(OriginTrialsComponentInstallerTest,
-       DisabledFeaturesReplacedWhenListExists) {
-  AddDisabledFeaturesToPrefs(kExistingDisabledFeatures);
-  ASSERT_TRUE(local_state()->HasPrefPath(
-      embedder_support::prefs::kOriginTrialDisabledFeatures));
-
-  auto manifest = std::make_unique<base::DictionaryValue>();
-  auto disabled_feature_list = std::make_unique<base::ListValue>();
-  disabled_feature_list->AppendStrings(kNewDisabledFeatures);
-  manifest->Set(kManifestDisabledFeaturesPath,
-                std::move(disabled_feature_list));
-
-  LoadUpdates(std::move(manifest));
-
-  CheckDisabledFeaturesPrefs(kNewDisabledFeatures);
-}
-
-TEST_F(OriginTrialsComponentInstallerTest,
-       DisabledTokensResetToDefaultWhenListMissing) {
-  AddDisabledTokensToPrefs(kExistingDisabledTokens);
-  ASSERT_TRUE(local_state()->HasPrefPath(
-      embedder_support::prefs::kOriginTrialDisabledTokens));
-
-  // Load with empty section in manifest
-  LoadUpdates(nullptr);
-
-  EXPECT_FALSE(local_state()->HasPrefPath(
-      embedder_support::prefs::kOriginTrialDisabledTokens));
-}
-
-TEST_F(OriginTrialsComponentInstallerTest,
-       DisabledTokensResetToDefaultWhenKeyExistsAndListMissing) {
-  AddDisabledTokensToPrefs(kExistingDisabledTokens);
-  ASSERT_TRUE(local_state()->HasPrefPath(
-      embedder_support::prefs::kOriginTrialDisabledTokens));
-
-  // Load with disabled tokens key in manifest, but no list values
-  auto manifest = std::make_unique<base::DictionaryValue>();
-  manifest->Set(kManifestDisabledTokensPath, std::make_unique<base::Value>());
-
-  LoadUpdates(std::move(manifest));
-
-  EXPECT_FALSE(local_state()->HasPrefPath(
-      embedder_support::prefs::kOriginTrialDisabledTokens));
-}
-
-TEST_F(OriginTrialsComponentInstallerTest,
-       DisabledTokensResetToDefaultWhenListEmpty) {
-  AddDisabledTokensToPrefs(kExistingDisabledTokens);
-  ASSERT_TRUE(local_state()->HasPrefPath(
-      embedder_support::prefs::kOriginTrialDisabledTokens));
-
-  auto manifest = std::make_unique<base::DictionaryValue>();
-  auto disabled_token_list = std::make_unique<base::ListValue>();
-  manifest->Set(kManifestDisabledTokenSignaturesPath,
-                std::move(disabled_token_list));
-
-  LoadUpdates(std::move(manifest));
-
-  EXPECT_FALSE(local_state()->HasPrefPath(
-      embedder_support::prefs::kOriginTrialDisabledTokens));
-}
-
-TEST_F(OriginTrialsComponentInstallerTest, DisabledTokensSetWhenListExists) {
-  ASSERT_FALSE(local_state()->HasPrefPath(
-      embedder_support::prefs::kOriginTrialDisabledTokens));
-
-  auto manifest = std::make_unique<base::DictionaryValue>();
-  auto disabled_token_list = std::make_unique<base::ListValue>();
-  disabled_token_list->AppendString(kNewDisabledToken1);
-  manifest->Set(kManifestDisabledTokenSignaturesPath,
-                std::move(disabled_token_list));
-
-  LoadUpdates(std::move(manifest));
-
-  std::vector<std::string> tokens = {kNewDisabledToken1};
-  CheckDisabledTokensPrefs(tokens);
-}
-
-TEST_F(OriginTrialsComponentInstallerTest,
-       DisabledTokensReplacedWhenListExists) {
-  AddDisabledTokensToPrefs(kExistingDisabledTokens);
-  ASSERT_TRUE(local_state()->HasPrefPath(
-      embedder_support::prefs::kOriginTrialDisabledTokens));
-
-  auto manifest = std::make_unique<base::DictionaryValue>();
-  auto disabled_token_list = std::make_unique<base::ListValue>();
-  disabled_token_list->AppendStrings(kNewDisabledTokens);
-  manifest->Set(kManifestDisabledTokenSignaturesPath,
-                std::move(disabled_token_list));
-
-  LoadUpdates(std::move(manifest));
-
-  CheckDisabledTokensPrefs(kNewDisabledTokens);
-}
-
 }  // namespace component_updater
diff --git a/chrome/browser/extensions/BUILD.gn b/chrome/browser/extensions/BUILD.gn
index 12fd069..ca86ca1 100644
--- a/chrome/browser/extensions/BUILD.gn
+++ b/chrome/browser/extensions/BUILD.gn
@@ -957,17 +957,11 @@
     sources += [
       "api/enterprise_device_attributes/enterprise_device_attributes_api.h",
       "api/enterprise_platform_keys/enterprise_platform_keys_api.h",
-      "api/messaging/native_message_built_in_host.cc",
-      "api/messaging/native_message_built_in_host.h",
-      "api/messaging/native_message_echo_host.cc",
-      "api/messaging/native_message_echo_host.h",
       "api/platform_keys/platform_keys_api.h",
     ]
     deps += [
       "//chromeos/crosapi/cpp",
       "//chromeos/crosapi/mojom",
-      "//remoting/host",
-      "//remoting/host/it2me:chrome_os_host",
     ]
     if (is_chromeos_lacros) {
       sources += [
@@ -975,23 +969,11 @@
         "api/enterprise_device_attributes/enterprise_device_attributes_api_lacros.h",
         "api/enterprise_platform_keys/enterprise_platform_keys_api_lacros.cc",
         "api/enterprise_platform_keys/enterprise_platform_keys_api_lacros.h",
-        "api/messaging/native_message_host_lacros.cc",
         "api/platform_keys/platform_keys_api_lacros.cc",
         "api/platform_keys/platform_keys_api_lacros.h",
       ]
       deps += [ "//chromeos/lacros" ]
-    } else {
-      sources += [ "api/messaging/native_message_host_chromeos.cc" ]
     }
-  } else {
-    sources += [
-      "api/messaging/native_message_process_host.cc",
-      "api/messaging/native_message_process_host.h",
-      "api/messaging/native_messaging_launch_from_native.cc",
-      "api/messaging/native_messaging_launch_from_native.h",
-      "api/messaging/native_process_launcher.cc",
-      "api/messaging/native_process_launcher.h",
-    ]
   }
 
   if (is_chromeos_ash) {
@@ -1024,6 +1006,7 @@
       "api/input_ime/input_ime_event_router_base.h",
       "api/media_perception_private/media_perception_api_delegate_chromeos.cc",
       "api/media_perception_private/media_perception_api_delegate_chromeos.h",
+      "api/messaging/native_message_host_chromeos.cc",
       "api/networking_cast_private/chrome_networking_cast_private_delegate.cc",
       "api/networking_cast_private/chrome_networking_cast_private_delegate.h",
       "api/networking_cast_private/networking_cast_private_api.cc",
@@ -1111,6 +1094,9 @@
       "//components/user_manager",
       "//media/capture:capture_lib",
       "//media/capture/video/chromeos/mojom:cros_camera",
+      "//remoting/base",
+      "//remoting/host",
+      "//remoting/host/it2me:chrome_os_host",
       "//third_party/protobuf:protobuf_lite",
       "//ui/base/ime/chromeos",
       "//ui/chromeos",
@@ -1139,6 +1125,12 @@
       "api/enterprise_reporting_private/keychain_data_helper_mac.h",
       "api/enterprise_reporting_private/keychain_data_helper_mac.mm",
       "api/image_writer_private/operation_nonchromeos.cc",
+      "api/messaging/native_message_process_host.cc",
+      "api/messaging/native_message_process_host.h",
+      "api/messaging/native_messaging_launch_from_native.cc",
+      "api/messaging/native_messaging_launch_from_native.h",
+      "api/messaging/native_process_launcher.cc",
+      "api/messaging/native_process_launcher.h",
       "api/tabs/tabs_util.cc",
       "chrome_kiosk_delegate.cc",
       "default_apps.cc",
diff --git a/chrome/browser/extensions/api/DEPS b/chrome/browser/extensions/api/DEPS
index e66c2600..88f2d8a 100644
--- a/chrome/browser/extensions/api/DEPS
+++ b/chrome/browser/extensions/api/DEPS
@@ -3,6 +3,7 @@
   "+services/device/public",
 
    # Enable remote assistance on Chrome OS
+  "+remoting/base",
   "+remoting/host",
 ]
 
diff --git a/chrome/browser/extensions/api/automation_internal/chrome_automation_internal_api_delegate.cc b/chrome/browser/extensions/api/automation_internal/chrome_automation_internal_api_delegate.cc
index d75371b..a992b6bb 100644
--- a/chrome/browser/extensions/api/automation_internal/chrome_automation_internal_api_delegate.cc
+++ b/chrome/browser/extensions/api/automation_internal/chrome_automation_internal_api_delegate.cc
@@ -26,6 +26,9 @@
 #endif
 
 #if BUILDFLAG(IS_CHROMEOS_ASH)
+#include "chrome/browser/ash/crosapi/automation_ash.h"
+#include "chrome/browser/ash/crosapi/crosapi_ash.h"
+#include "chrome/browser/ash/crosapi/crosapi_manager.h"
 #include "chrome/browser/chromeos/arc/accessibility/arc_accessibility_helper_bridge.h"
 #endif
 
@@ -82,6 +85,13 @@
 bool ChromeAutomationInternalApiDelegate::EnableTree(
     const ui::AXTreeID& tree_id) {
 #if BUILDFLAG(IS_CHROMEOS_ASH)
+  // CrosapiManager may not be initialized on unit testing.
+  // Propagate the EnableTree signal to crosapi clients.
+  if (crosapi::CrosapiManager::IsInitialized()) {
+    crosapi::CrosapiManager::Get()->crosapi_ash()->automation_ash()->EnableTree(
+        tree_id);
+  }
+
   arc::ArcAccessibilityHelperBridge* bridge =
       arc::ArcAccessibilityHelperBridge::GetForBrowserContext(
           GetActiveUserContext());
@@ -92,6 +102,17 @@
 }
 
 void ChromeAutomationInternalApiDelegate::EnableDesktop() {
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+  // CrosapiManager may not be initialized on unit testing.
+  // Propagate the EnableDesktop signal to crosapi clients.
+  if (crosapi::CrosapiManager::IsInitialized()) {
+    crosapi::CrosapiManager::Get()
+        ->crosapi_ash()
+        ->automation_ash()
+        ->EnableDesktop();
+  }
+#endif
+
 #if defined(USE_AURA)
   AutomationManagerAura::GetInstance()->Enable();
 #else
diff --git a/chrome/browser/extensions/api/messaging/native_message_built_in_host.cc b/chrome/browser/extensions/api/messaging/native_message_built_in_host.cc
deleted file mode 100644
index 97d7e38..0000000
--- a/chrome/browser/extensions/api/messaging/native_message_built_in_host.cc
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright 2021 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "chrome/browser/extensions/api/messaging/native_message_built_in_host.h"
-
-#include <string>
-
-#include "content/public/browser/browser_context.h"
-#include "extensions/browser/api/messaging/native_message_host.h"
-#include "extensions/common/constants.h"
-#include "extensions/common/url_pattern.h"
-#include "ui/gfx/native_widget_types.h"
-#include "url/gurl.h"
-
-namespace extensions {
-
-namespace {
-
-bool MatchesSecurityOrigin(const NativeMessageBuiltInHost& host,
-                           const std::string& extension_id) {
-  GURL origin(std::string(kExtensionScheme) + "://" + extension_id);
-  for (size_t i = 0; i < host.allowed_origins_count; i++) {
-    URLPattern allowed_origin(URLPattern::SCHEME_ALL, host.allowed_origins[i]);
-    if (allowed_origin.MatchesSecurityOrigin(origin)) {
-      return true;
-    }
-  }
-  return false;
-}
-
-}  // namespace
-
-std::unique_ptr<NativeMessageHost> NativeMessageHost::Create(
-    content::BrowserContext* browser_context,
-    gfx::NativeView native_view,
-    const std::string& source_extension_id,
-    const std::string& native_host_name,
-    bool allow_user_level,
-    std::string* error) {
-  for (size_t i = 0; i < kBuiltInHostsCount; i++) {
-    const auto& host = kBuiltInHosts[i];
-    if (host.name == native_host_name) {
-      if (MatchesSecurityOrigin(host, source_extension_id)) {
-        return (*host.create_function)(browser_context);
-      }
-      *error = kForbiddenError;
-      return nullptr;
-    }
-  }
-  *error = kNotFoundError;
-  return nullptr;
-}
-
-}  // namespace extensions
diff --git a/chrome/browser/extensions/api/messaging/native_message_built_in_host.h b/chrome/browser/extensions/api/messaging/native_message_built_in_host.h
deleted file mode 100644
index 71da8c6..0000000
--- a/chrome/browser/extensions/api/messaging/native_message_built_in_host.h
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright 2021 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef CHROME_BROWSER_EXTENSIONS_API_MESSAGING_NATIVE_MESSAGE_BUILT_IN_HOST_H_
-#define CHROME_BROWSER_EXTENSIONS_API_MESSAGING_NATIVE_MESSAGE_BUILT_IN_HOST_H_
-
-#include <memory>
-
-#include <stddef.h>
-
-namespace content {
-class BrowserContext;
-}
-
-namespace extensions {
-
-class NativeMessageHost;
-
-struct NativeMessageBuiltInHost {
-  // Unique name to identify the built-in host.
-  const char* const name;
-
-  // The extension origins allowed to create the built-in host.
-  const char* const* const allowed_origins;
-
-  // The count of |allowed_origins|.
-  size_t allowed_origins_count;
-
-  // The factory function used to create new instances of this host.
-  std::unique_ptr<NativeMessageHost> (*create_function)(
-      content::BrowserContext*);
-};
-
-// The set of built-in hosts that can be instantiated. These are defined in the
-// platform-specific impl files.
-extern const NativeMessageBuiltInHost kBuiltInHosts[];
-
-// The count of built-in hosts defined in |kBuiltInHosts|.
-extern const size_t kBuiltInHostsCount;
-
-}  // namespace extensions
-
-#endif  // CHROME_BROWSER_EXTENSIONS_API_MESSAGING_NATIVE_MESSAGE_BUILT_IN_HOST_H_
diff --git a/chrome/browser/extensions/api/messaging/native_message_echo_host.cc b/chrome/browser/extensions/api/messaging/native_message_echo_host.cc
deleted file mode 100644
index 93151b21..0000000
--- a/chrome/browser/extensions/api/messaging/native_message_echo_host.cc
+++ /dev/null
@@ -1,75 +0,0 @@
-// Copyright 2021 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "chrome/browser/extensions/api/messaging/native_message_echo_host.h"
-
-#include <utility>
-
-#include "base/json/json_reader.h"
-#include "base/json/json_writer.h"
-#include "base/optional.h"
-#include "base/single_thread_task_runner.h"
-#include "base/stl_util.h"
-#include "base/threading/thread_task_runner_handle.h"
-#include "base/values.h"
-#include "content/public/browser/browser_context.h"
-
-namespace extensions {
-
-// static
-// Must match ScopedTestNativeMessagingHost::kHostName.
-const char* const NativeMessageEchoHost::kHostName =
-    "com.google.chrome.test.echo";
-
-// static
-// Must match ScopedTestNativeMessagingHost::kExtensionId.
-const char* const NativeMessageEchoHost::kOrigins[] = {
-    "chrome-extension://knldjmfmopnpolahpmmgbagdohdnhkik/"};
-
-// static
-const int NativeMessageEchoHost::kOriginCount = base::size(kOrigins);
-
-// static
-std::unique_ptr<NativeMessageHost> NativeMessageEchoHost::Create(
-    content::BrowserContext* browser_context) {
-  return std::make_unique<NativeMessageEchoHost>();
-}
-
-NativeMessageEchoHost::NativeMessageEchoHost() = default;
-NativeMessageEchoHost::~NativeMessageEchoHost() = default;
-
-void NativeMessageEchoHost::Start(Client* client) {
-  client_ = client;
-}
-
-void NativeMessageEchoHost::OnMessage(const std::string& request_string) {
-  base::Optional<base::Value> request_value =
-      base::JSONReader::Read(request_string);
-  if (!request_value.has_value()) {
-    client_->CloseChannel(kHostInputOutputError);
-  } else if (request_string.find("stopHostTest") != std::string::npos) {
-    client_->CloseChannel(kNativeHostExited);
-  } else if (request_string.find("bigMessageTest") != std::string::npos) {
-    client_->CloseChannel(kHostInputOutputError);
-  } else {
-    ProcessEcho(base::Value::AsDictionaryValue(request_value.value()));
-  }
-}
-
-scoped_refptr<base::SingleThreadTaskRunner> NativeMessageEchoHost::task_runner()
-    const {
-  return base::ThreadTaskRunnerHandle::Get();
-}
-
-void NativeMessageEchoHost::ProcessEcho(const base::DictionaryValue& request) {
-  base::DictionaryValue response;
-  response.SetInteger("id", ++message_number_);
-  response.Set("echo", request.CreateDeepCopy());
-  response.SetString("caller_url", kOrigins[0]);
-  std::string response_string;
-  base::JSONWriter::Write(response, &response_string);
-  client_->PostMessageFromNativeHost(response_string);
-}
-
-}  // namespace extensions
diff --git a/chrome/browser/extensions/api/messaging/native_message_echo_host.h b/chrome/browser/extensions/api/messaging/native_message_echo_host.h
deleted file mode 100644
index 7c3e3ea2..0000000
--- a/chrome/browser/extensions/api/messaging/native_message_echo_host.h
+++ /dev/null
@@ -1,60 +0,0 @@
-// Copyright 2021 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef CHROME_BROWSER_EXTENSIONS_API_MESSAGING_NATIVE_MESSAGE_ECHO_HOST_H_
-#define CHROME_BROWSER_EXTENSIONS_API_MESSAGING_NATIVE_MESSAGE_ECHO_HOST_H_
-
-#include <memory>
-#include <string>
-
-#include "extensions/browser/api/messaging/native_message_host.h"
-
-namespace base {
-class DictionaryValue;
-class SingleThreadTaskRunner;
-}  // namespace base
-
-namespace {
-class BrowserContext;
-}
-
-namespace extensions {
-
-// A test NativeMessageHost used in ExtensionApiTest::NativeMessagingBasic.
-// See //chrome/browser/extensions/api/messaging/native_messaging_apitest.cc
-// The behavior in this implementation must match the expectations defined in
-// //chrome/test/data/native_messaging/native_hosts/echo.py as that script is
-// used to drive the tests.
-class NativeMessageEchoHost : public NativeMessageHost {
- public:
-  static const char* const kHostName;
-  static const char* const kOrigins[];
-  static const int kOriginCount;
-
-  static std::unique_ptr<NativeMessageHost> Create(
-      content::BrowserContext* browser_context);
-
-  NativeMessageEchoHost();
-  NativeMessageEchoHost(const NativeMessageEchoHost&) = delete;
-  NativeMessageEchoHost& operator=(const NativeMessageEchoHost&) = delete;
-  ~NativeMessageEchoHost() override;
-
-  // NativeMessageHost implementation.
-  void Start(Client* client) override;
-  void OnMessage(const std::string& request_string) override;
-  scoped_refptr<base::SingleThreadTaskRunner> task_runner() const override;
-
- private:
-  void ProcessEcho(const base::DictionaryValue& request);
-
-  // Counter used to ensure message uniqueness for testing.
-  int message_number_ = 0;
-
-  // |client_| must outlive this test instance.
-  Client* client_ = nullptr;
-};
-
-}  // namespace extensions
-
-#endif  // CHROME_BROWSER_EXTENSIONS_API_MESSAGING_NATIVE_MESSAGE_ECHO_HOST_H_
diff --git a/chrome/browser/extensions/api/messaging/native_message_host_chromeos.cc b/chrome/browser/extensions/api/messaging/native_message_host_chromeos.cc
index 712e167..0459cad 100644
--- a/chrome/browser/extensions/api/messaging/native_message_host_chromeos.cc
+++ b/chrome/browser/extensions/api/messaging/native_message_host_chromeos.cc
@@ -8,23 +8,93 @@
 #include <string>
 #include <utility>
 
+#include "base/bind.h"
+#include "base/callback_helpers.h"
+#include "base/json/json_reader.h"
+#include "base/json/json_writer.h"
+#include "base/location.h"
+#include "base/single_thread_task_runner.h"
 #include "base/stl_util.h"
+#include "base/threading/thread_task_runner_handle.h"
+#include "base/values.h"
 #include "chrome/browser/browser_process.h"
 #include "chrome/browser/chromeos/arc/extensions/arc_support_message_host.h"
 #include "chrome/browser/chromeos/drive/drivefs_native_message_host.h"
 #include "chrome/browser/chromeos/wilco_dtc_supportd/wilco_dtc_supportd_messaging.h"
-#include "chrome/browser/extensions/api/messaging/native_message_built_in_host.h"
-#include "chrome/browser/extensions/api/messaging/native_message_echo_host.h"
-#include "content/public/browser/browser_context.h"
 #include "content/public/browser/browser_task_traits.h"
 #include "content/public/browser/browser_thread.h"
-#include "remoting/host/it2me/it2me_native_messaging_host_allowed_origins.h"
+#include "extensions/common/constants.h"
+#include "extensions/common/url_pattern.h"
 #include "remoting/host/it2me/it2me_native_messaging_host_chromeos.h"
+#include "ui/gfx/native_widget_types.h"
+#include "url/gurl.h"
 
 namespace extensions {
 
 namespace {
 
+// A simple NativeMessageHost that mimics the implementation of
+// chrome/test/data/native_messaging/native_hosts/echo.py. It is currently
+// used for testing by ExtensionApiTest::NativeMessagingBasic.
+
+const char* const kEchoHostOrigins[] = {
+    // ScopedTestNativeMessagingHost::kExtensionId
+    "chrome-extension://knldjmfmopnpolahpmmgbagdohdnhkik/"};
+
+class EchoHost : public NativeMessageHost {
+ public:
+  static std::unique_ptr<NativeMessageHost> Create(
+      content::BrowserContext* browser_context) {
+    return std::make_unique<EchoHost>();
+  }
+
+  EchoHost() = default;
+
+  void Start(Client* client) override { client_ = client; }
+
+  void OnMessage(const std::string& request_string) override {
+    std::unique_ptr<base::Value> request_value =
+        base::JSONReader::ReadDeprecated(request_string);
+    std::unique_ptr<base::DictionaryValue> request(
+        static_cast<base::DictionaryValue*>(request_value.release()));
+    if (request_string.find("stopHostTest") != std::string::npos) {
+      client_->CloseChannel(kNativeHostExited);
+    } else if (request_string.find("bigMessageTest") != std::string::npos) {
+      client_->CloseChannel(kHostInputOutputError);
+    } else {
+      ProcessEcho(*request);
+    }
+  }
+
+  scoped_refptr<base::SingleThreadTaskRunner> task_runner() const override {
+    return base::ThreadTaskRunnerHandle::Get();
+  }
+
+ private:
+  void ProcessEcho(const base::DictionaryValue& request) {
+    base::DictionaryValue response;
+    response.SetInteger("id", ++message_number_);
+    response.Set("echo", request.CreateDeepCopy());
+    response.SetString("caller_url", kEchoHostOrigins[0]);
+    std::string response_string;
+    base::JSONWriter::Write(response, &response_string);
+    client_->PostMessageFromNativeHost(response_string);
+  }
+
+  int message_number_ = 0;
+  Client* client_ = nullptr;
+
+  DISALLOW_COPY_AND_ASSIGN(EchoHost);
+};
+
+struct BuiltInHost {
+  const char* const name;
+  const char* const* const allowed_origins;
+  int allowed_origins_count;
+  std::unique_ptr<NativeMessageHost> (*create_function)(
+      content::BrowserContext*);
+};
+
 std::unique_ptr<NativeMessageHost> CreateIt2MeHost(
     content::BrowserContext* browser_context) {
   return remoting::CreateIt2MeNativeMessagingHostForChromeOS(
@@ -32,26 +102,67 @@
       g_browser_process->policy_service());
 }
 
+// If you modify the list of allowed_origins, don't forget to update
+// remoting/host/it2me/com.google.chrome.remote_assistance.json.jinja2
+// to keep the two lists in sync.
+// TODO(kelvinp): Load the native messaging manifest as a resource file into
+// chrome and fetch the list of allowed_origins from the manifest (see
+// crbug/424743).
+const char* const kRemotingIt2MeOrigins[] = {
+    "chrome-extension://inomeogfingihgjfjlpeplalcfajhgai/",
+    "chrome-extension://hpodccmdligbeohchckkeajbfohibipg/"};
+
+bool MatchesSecurityOrigin(const BuiltInHost& host,
+                           const std::string& extension_id) {
+  GURL origin(std::string(kExtensionScheme) + "://" + extension_id);
+  for (int i = 0; i < host.allowed_origins_count; i++) {
+    URLPattern allowed_origin(URLPattern::SCHEME_ALL, host.allowed_origins[i]);
+    if (allowed_origin.MatchesSecurityOrigin(origin)) {
+      return true;
+    }
+  }
+  return false;
+}
+
 }  // namespace
 
-const NativeMessageBuiltInHost kBuiltInHosts[] = {
-    {NativeMessageEchoHost::kHostName, NativeMessageEchoHost::kOrigins,
-     NativeMessageEchoHost::kOriginCount, &NativeMessageEchoHost::Create},
-    {remoting::kIt2MeNativeMessageHostName, remoting::kIt2MeOrigins,
-     remoting::kIt2MeOriginsSize, &CreateIt2MeHost},
-    {arc::ArcSupportMessageHost::kHostName,
-     arc::ArcSupportMessageHost::kHostOrigin, 1,
-     &arc::ArcSupportMessageHost::Create},
-    {chromeos::kWilcoDtcSupportdUiMessageHost,
-     chromeos::kWilcoDtcSupportdHostOrigins,
-     chromeos::kWilcoDtcSupportdHostOriginsSize,
-     &chromeos::CreateExtensionOwnedWilcoDtcSupportdMessageHost},
-    {drive::kDriveFsNativeMessageHostName,
-     drive::kDriveFsNativeMessageHostOrigins,
-     drive::kDriveFsNativeMessageHostOriginsSize,
-     &drive::CreateDriveFsNativeMessageHost},
-};
+std::unique_ptr<NativeMessageHost> NativeMessageHost::Create(
+    content::BrowserContext* browser_context,
+    gfx::NativeView native_view,
+    const std::string& source_extension_id,
+    const std::string& native_host_name,
+    bool allow_user_level,
+    std::string* error) {
+  static const BuiltInHost kBuiltInHosts[] = {
+      // ScopedTestNativeMessagingHost::kHostName
+      {"com.google.chrome.test.echo", kEchoHostOrigins,
+       base::size(kEchoHostOrigins), &EchoHost::Create},
+      {"com.google.chrome.remote_assistance", kRemotingIt2MeOrigins,
+       base::size(kRemotingIt2MeOrigins), &CreateIt2MeHost},
+      {arc::ArcSupportMessageHost::kHostName,
+       arc::ArcSupportMessageHost::kHostOrigin, 1,
+       &arc::ArcSupportMessageHost::Create},
+      {chromeos::kWilcoDtcSupportdUiMessageHost,
+       chromeos::kWilcoDtcSupportdHostOrigins,
+       chromeos::kWilcoDtcSupportdHostOriginsSize,
+       &chromeos::CreateExtensionOwnedWilcoDtcSupportdMessageHost},
+      {drive::kDriveFsNativeMessageHostName,
+       drive::kDriveFsNativeMessageHostOrigins,
+       drive::kDriveFsNativeMessageHostOriginsSize,
+       &drive::CreateDriveFsNativeMessageHost},
+  };
 
-const size_t kBuiltInHostsCount = base::size(kBuiltInHosts);
+  for (const BuiltInHost& host : kBuiltInHosts) {
+    if (host.name == native_host_name) {
+      if (MatchesSecurityOrigin(host, source_extension_id)) {
+        return (*host.create_function)(browser_context);
+      }
+      *error = kForbiddenError;
+      return nullptr;
+    }
+  }
+  *error = kNotFoundError;
+  return nullptr;
+}
 
 }  // namespace extensions
diff --git a/chrome/browser/extensions/api/messaging/native_message_host_lacros.cc b/chrome/browser/extensions/api/messaging/native_message_host_lacros.cc
deleted file mode 100644
index a2b5097..0000000
--- a/chrome/browser/extensions/api/messaging/native_message_host_lacros.cc
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright 2021 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "extensions/browser/api/messaging/native_message_host.h"
-
-#include <memory>
-#include <string>
-
-#include "base/stl_util.h"
-#include "chrome/browser/extensions/api/messaging/native_message_built_in_host.h"
-#include "chrome/browser/extensions/api/messaging/native_message_echo_host.h"
-#include "content/public/browser/browser_context.h"
-#include "content/public/browser/browser_task_traits.h"
-#include "content/public/browser/browser_thread.h"
-#include "remoting/host/it2me/it2me_native_messaging_host_allowed_origins.h"
-#include "remoting/host/it2me/it2me_native_messaging_host_lacros.h"
-
-namespace extensions {
-
-namespace {
-
-std::unique_ptr<NativeMessageHost> CreateIt2MeHost(
-    content::BrowserContext* browser_context) {
-  return remoting::CreateIt2MeNativeMessagingHostForLacros(
-      content::GetIOThreadTaskRunner({}), content::GetUIThreadTaskRunner({}));
-}
-
-}  // namespace
-
-const NativeMessageBuiltInHost kBuiltInHosts[] = {
-    {NativeMessageEchoHost::kHostName, NativeMessageEchoHost::kOrigins,
-     NativeMessageEchoHost::kOriginCount, &NativeMessageEchoHost::Create},
-    {remoting::kIt2MeNativeMessageHostName, remoting::kIt2MeOrigins,
-     remoting::kIt2MeOriginsSize, &CreateIt2MeHost},
-};
-
-const size_t kBuiltInHostsCount = base::size(kBuiltInHosts);
-
-}  // namespace extensions
diff --git a/chrome/browser/extensions/api/messaging/native_messaging_apitest.cc b/chrome/browser/extensions/api/messaging/native_messaging_apitest.cc
index 8134e92..d40535f 100644
--- a/chrome/browser/extensions/api/messaging/native_messaging_apitest.cc
+++ b/chrome/browser/extensions/api/messaging/native_messaging_apitest.cc
@@ -83,7 +83,7 @@
   ASSERT_TRUE(RunTest("native_messaging_connect")) << message_;
 }
 
-#if !BUILDFLAG(IS_CHROMEOS_ASH) && !BUILDFLAG(IS_CHROMEOS_LACROS)
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
 
 class TestProcessManagerObserver : public ProcessManagerObserver {
  public:
@@ -437,7 +437,7 @@
   ASSERT_NO_FATAL_FAILURE(TestKeepAliveStateObserver().WaitForNoKeepAlive());
 }
 
-#endif  // !BUILDFLAG(IS_CHROMEOS_ASH) && !BUILDFLAG(IS_CHROMEOS_LACROS)
+#endif  // !BUILDFLAG(IS_CHROMEOS_ASH)
 
 }  // namespace
 }  // namespace extensions
diff --git a/chrome/browser/extensions/api/notifications/extension_notification_handler.cc b/chrome/browser/extensions/api/notifications/extension_notification_handler.cc
index 90e763b4..f4f4142 100644
--- a/chrome/browser/extensions/api/notifications/extension_notification_handler.cc
+++ b/chrome/browser/extensions/api/notifications/extension_notification_handler.cc
@@ -7,7 +7,6 @@
 #include "base/callback.h"
 #include "base/check_op.h"
 #include "base/metrics/histogram_macros.h"
-#include "base/strings/nullable_string16.h"
 #include "base/strings/string_piece.h"
 #include "chrome/browser/extensions/api/notifications/extension_notification_display_helper.h"
 #include "chrome/browser/extensions/api/notifications/extension_notification_display_helper_factory.h"
diff --git a/chrome/browser/extensions/api/notifications/notifications_api.cc b/chrome/browser/extensions/api/notifications/notifications_api.cc
index a8b745f..e21637a 100644
--- a/chrome/browser/extensions/api/notifications/notifications_api.cc
+++ b/chrome/browser/extensions/api/notifications/notifications_api.cc
@@ -104,33 +104,6 @@
   return scoped_id.substr(index_of_separator);
 }
 
-const gfx::ImageSkia CreateSolidColorImage(int width,
-                                           int height,
-                                           SkColor color) {
-  SkBitmap bitmap;
-  bitmap.allocN32Pixels(width, height);
-  bitmap.eraseColor(color);
-  return gfx::ImageSkia::CreateFrom1xBitmap(bitmap);
-}
-
-// Take the alpha channel of small_image, mask it with the foreground,
-// then add the masked foreground on top of the background
-const gfx::Image GetMaskedSmallImage(const gfx::ImageSkia& small_image) {
-  int width = small_image.width();
-  int height = small_image.height();
-
-  // Background color grey
-  const gfx::ImageSkia background = CreateSolidColorImage(
-      width, height, message_center::kSmallImageMaskBackgroundColor);
-  // Foreground color white
-  const gfx::ImageSkia foreground = CreateSolidColorImage(
-      width, height, message_center::kSmallImageMaskForegroundColor);
-  const gfx::ImageSkia masked_small_image =
-      gfx::ImageSkiaOperations::CreateMaskedImage(foreground, small_image);
-  return gfx::Image(gfx::ImageSkiaOperations::CreateSuperimposedImage(
-      background, masked_small_image));
-}
-
 // Converts the |notification_bitmap| (in RGBA format) to the |*return_image|
 // (which is in ARGB format).
 bool NotificationBitmapToGfxImage(
@@ -275,8 +248,8 @@
       *error = kUnableToDecodeIconError;
       return false;
     }
-    optional_fields.small_image =
-        GetMaskedSmallImage(small_icon_mask.AsImageSkia());
+    optional_fields.small_image = small_icon_mask;
+    optional_fields.small_image_needs_additional_masking = true;
   }
 
   if (options->priority.get())
@@ -430,8 +403,8 @@
       *error = kUnableToDecodeIconError;
       return false;
     }
-    notification->set_small_image(
-        GetMaskedSmallImage(app_icon_mask.AsImageSkia()));
+    notification->set_small_image(app_icon_mask);
+    notification->set_small_image_needs_additional_masking(true);
   }
 
   if (options->priority)
diff --git a/chrome/browser/extensions/api/notifications/notifications_apitest.cc b/chrome/browser/extensions/api/notifications/notifications_apitest.cc
index 523d702..32a5223 100644
--- a/chrome/browser/extensions/api/notifications/notifications_apitest.cc
+++ b/chrome/browser/extensions/api/notifications/notifications_apitest.cc
@@ -556,3 +556,18 @@
             notification->fullscreen_visibility());
 }
 #endif  // !defined(OS_MAC)
+
+IN_PROC_BROWSER_TEST_F(NotificationsApiTest, TestSmallImage) {
+  ExtensionTestMessageListener notification_created_listener("created", false);
+  const Extension* extension = LoadAppWithWindowState(
+      "notifications/api/basic_app", WindowState::NORMAL);
+  ASSERT_TRUE(extension) << message_;
+  ASSERT_TRUE(notification_created_listener.WaitUntilSatisfied());
+
+  message_center::Notification* notification =
+      GetNotificationForExtension(extension);
+  ASSERT_TRUE(notification);
+
+  EXPECT_FALSE(notification->small_image().IsEmpty());
+  EXPECT_TRUE(notification->small_image_needs_additional_masking());
+}
diff --git a/chrome/browser/extensions/extension_allowlist.cc b/chrome/browser/extensions/extension_allowlist.cc
index fdb9b5b6..e8a3ace8 100644
--- a/chrome/browser/extensions/extension_allowlist.cc
+++ b/chrome/browser/extensions/extension_allowlist.cc
@@ -20,7 +20,11 @@
     : profile_(profile),
       extension_prefs_(extension_prefs),
       extension_service_(extension_service),
-      registry_(ExtensionRegistry::Get(profile)) {}
+      registry_(ExtensionRegistry::Get(profile)) {
+  extension_prefs_observation_.Observe(extension_prefs);
+}
+
+ExtensionAllowlist::~ExtensionAllowlist() = default;
 
 void ExtensionAllowlist::Init() {
   SetAllowlistEnforcedField();
@@ -55,33 +59,46 @@
     return;
   }
 
-  bool allowlisted = allowlist_value->GetBool();
+  AllowlistState allowlist_state = allowlist_value->GetBool()
+                                       ? ALLOWLIST_ALLOWLISTED
+                                       : ALLOWLIST_NOT_ALLOWLISTED;
+
+  if (allowlist_state ==
+      extension_prefs_->GetExtensionAllowlistState(extension_id)) {
+    // Do nothing if the state didn't change.
+    return;
+  }
 
   // TODO(jeffcyr): Add an observer when allowlist state change, otherwise the
   // 'chrome://extensions' warning may not be refreshed.
 
   // Set the allowlist state even if there is no enforcement. This will allow
   // immediate enforcement when it is activated.
-  extension_prefs_->SetExtensionAllowlistState(
-      extension_id,
-      allowlisted ? ALLOWLIST_ALLOWLISTED : ALLOWLIST_NOT_ALLOWLISTED);
+  extension_prefs_->SetExtensionAllowlistState(extension_id, allowlist_state);
 
-  if (!is_allowlist_enforced_) {
-    DCHECK(!extension_prefs_->HasDisableReason(
-        extension_id, disable_reason::DISABLE_NOT_ALLOWLISTED));
-    return;
-  }
+  if (allowlist_state == ALLOWLIST_ALLOWLISTED) {
+    // The extension is now allowlisted, remove the disable reason if present
+    // and ask for a user acknowledge if the extension was re-enabled in the
+    // process.
 
-  if (allowlisted) {
+    if (!extension_prefs_->HasDisableReason(
+            extension_id, disable_reason::DISABLE_NOT_ALLOWLISTED)) {
+      // Nothing to do if the extension was not already disabled by allowlist
+      // enforcement.
+      return;
+    }
+
     extension_service_->RemoveDisableReasonAndMaybeEnable(
         extension_id, disable_reason::DISABLE_NOT_ALLOWLISTED);
-  } else {
-    // TODO(jeffcyr): User re-enable is broken, if the user manually re-enabled
-    // the extension, it is currently disabled again on the next update check or
-    // restart. This will be fixed before launch.
 
-    extension_service_->DisableExtension(
-        extension_id, disable_reason::DISABLE_NOT_ALLOWLISTED);
+    if (registry_->enabled_extensions().Contains(extension_id)) {
+      // Inform the user if the extension is now enabled.
+      extension_prefs_->SetExtensionAllowlistAcknowledgeState(
+          extension_id, ALLOWLIST_ACKNOWLEDGE_NEEDED);
+    }
+  } else if (is_allowlist_enforced_) {
+    // The extension is no longer allowlisted, try to apply enforcement.
+    ApplyEnforcement(extension_id);
   }
 }
 
@@ -99,6 +116,44 @@
       safe_browsing::IsEnhancedProtectionEnabled(*profile_->GetPrefs());
 }
 
+// `ApplyEnforcement` can be called when an extension becomes not allowlisted or
+// when the allowlist enforcement is activated (for already not allowlisted
+// extensions).
+void ExtensionAllowlist::ApplyEnforcement(const std::string& extension_id) {
+  DCHECK(is_allowlist_enforced_);
+  DCHECK_EQ(extension_prefs_->GetExtensionAllowlistState(extension_id),
+            ALLOWLIST_NOT_ALLOWLISTED);
+
+  // Early exit if the enforcement is already done.
+  if (extension_prefs_->HasDisableReason(
+          extension_id, disable_reason::DISABLE_NOT_ALLOWLISTED)) {
+    return;
+  }
+
+  // Do not re-enforce if the extension was explicitly enabled by the user.
+  if (extension_prefs_->GetExtensionAllowlistAcknowledgeState(extension_id) ==
+      ALLOWLIST_ACKNOWLEDGE_ENABLED_BY_USER) {
+    return;
+  }
+
+  bool was_enabled = registry_->enabled_extensions().Contains(extension_id);
+  extension_service_->DisableExtension(extension_id,
+                                       disable_reason::DISABLE_NOT_ALLOWLISTED);
+
+  // The user should acknowledge the disable action if the extension was
+  // previously enabled and the disable reason could be added (it can be denied
+  // by policy).
+  if (was_enabled &&
+      extension_prefs_->HasDisableReason(
+          extension_id, disable_reason::DISABLE_NOT_ALLOWLISTED)) {
+    extension_prefs_->SetExtensionAllowlistAcknowledgeState(
+        extension_id, ALLOWLIST_ACKNOWLEDGE_NEEDED);
+  } else {
+    extension_prefs_->SetExtensionAllowlistAcknowledgeState(
+        extension_id, ALLOWLIST_ACKNOWLEDGE_NONE);
+  }
+}
+
 void ExtensionAllowlist::ActivateAllowlistEnforcement() {
   DCHECK(is_allowlist_enforced_);
 
@@ -107,8 +162,7 @@
   for (const auto& extension : *all_extensions) {
     if (extension_prefs_->GetExtensionAllowlistState(extension->id()) ==
         ALLOWLIST_NOT_ALLOWLISTED) {
-      extension_service_->DisableExtension(
-          extension->id(), disable_reason::DISABLE_NOT_ALLOWLISTED);
+      ApplyEnforcement(extension->id());
     }
   }
 }
@@ -118,16 +172,24 @@
 
   std::unique_ptr<ExtensionSet> all_extensions =
       registry_->GenerateInstalledExtensionsSet();
+
+  // Find all extensions disabled by allowlist enforcement, remove the disable
+  // reason and reset the acknowledge state.
   for (const auto& extension : *all_extensions) {
-    extension_service_->RemoveDisableReasonAndMaybeEnable(
-        extension->id(), disable_reason::DISABLE_NOT_ALLOWLISTED);
+    if (extension_prefs_->HasDisableReason(
+            extension->id(), disable_reason::DISABLE_NOT_ALLOWLISTED)) {
+      extension_service_->RemoveDisableReasonAndMaybeEnable(
+          extension->id(), disable_reason::DISABLE_NOT_ALLOWLISTED);
+      extension_prefs_->SetExtensionAllowlistAcknowledgeState(
+          extension->id(), ALLOWLIST_ACKNOWLEDGE_NONE);
+    }
   }
 }
 
 void ExtensionAllowlist::OnSafeBrowsingEnhancedChanged() {
   bool old_value = is_allowlist_enforced_;
 
-  // Note that |is_allowlist_enforced_| could remain |false| even if the ESB
+  // Note that `is_allowlist_enforced_` could remain `false` even if the ESB
   // setting was turned on if the feature flag is disabled.
   SetAllowlistEnforcedField();
 
@@ -141,4 +203,24 @@
   }
 }
 
+// ExtensionPrefsObserver::OnExtensionStateChanged override
+void ExtensionAllowlist::OnExtensionStateChanged(
+    const std::string& extension_id,
+    bool is_now_enabled) {
+  if (!is_now_enabled)
+    return;  // We only care if the extension is now enabled.
+
+  if (!is_allowlist_enforced_)
+    return;  // We only care if allowlist if being enforced.
+
+  if (extension_prefs_->GetExtensionAllowlistState(extension_id) ==
+      ALLOWLIST_NOT_ALLOWLISTED) {
+    // The extension was enabled even though it's not on the allowlist. Consider
+    // this an acknowledgement from the user, and ensure we don't disable the
+    // extension again.
+    extension_prefs_->SetExtensionAllowlistAcknowledgeState(
+        extension_id, ALLOWLIST_ACKNOWLEDGE_ENABLED_BY_USER);
+  }
+}
+
 }  // namespace extensions
diff --git a/chrome/browser/extensions/extension_allowlist.h b/chrome/browser/extensions/extension_allowlist.h
index 05685321..afb4cb5 100644
--- a/chrome/browser/extensions/extension_allowlist.h
+++ b/chrome/browser/extensions/extension_allowlist.h
@@ -5,7 +5,10 @@
 #ifndef CHROME_BROWSER_EXTENSIONS_EXTENSION_ALLOWLIST_H_
 #define CHROME_BROWSER_EXTENSIONS_EXTENSION_ALLOWLIST_H_
 
+#include "base/scoped_observation.h"
 #include "components/prefs/pref_change_registrar.h"
+#include "extensions/browser/extension_prefs.h"
+#include "extensions/browser/extension_prefs_observer.h"
 
 class Profile;
 
@@ -14,19 +17,18 @@
 }  // namespace base
 
 namespace extensions {
-class ExtensionPrefs;
 class ExtensionRegistry;
 class ExtensionService;
 
 // Manages the Safe Browsing CRX Allowlist.
-class ExtensionAllowlist {
+class ExtensionAllowlist : private ExtensionPrefsObserver {
  public:
   ExtensionAllowlist(Profile* profile,
                      ExtensionPrefs* extension_prefs,
                      ExtensionService* extension_service);
   ExtensionAllowlist(const ExtensionAllowlist&) = delete;
   ExtensionAllowlist& operator=(const ExtensionAllowlist&) = delete;
-  ~ExtensionAllowlist() = default;
+  ~ExtensionAllowlist();
 
   void Init();
 
@@ -45,16 +47,27 @@
   // Set if the allowlist should be enforced or not.
   void SetAllowlistEnforcedField();
 
+  // Apply the allowlist enforcement by disabling a not allowlisted extension if
+  // allowed by policy.
+  void ApplyEnforcement(const std::string& extension_id);
+
   // Blocklist all extensions with allowlist state `ALLOWLIST_NOT_ALLOWLISTED`.
   void ActivateAllowlistEnforcement();
 
   // Unblocklist all extensions with allowlist state
-  // |ALLOWLIST_NOT_ALLOWLISTED|.
+  // `ALLOWLIST_NOT_ALLOWLISTED`.
   void DeactivateAllowlistEnforcement();
 
   // Called when the 'Enhanced Safe Browsing' setting changes.
   void OnSafeBrowsingEnhancedChanged();
 
+  // ExtensionPrefsObserver:
+  // Observes extension state changes to set
+  // `ALLOWLIST_ACKNOWLEDGE_ENABLED_BY_USER` when a not allowlisted extension is
+  // re-enabled by the user.
+  void OnExtensionStateChanged(const std::string& extension_id,
+                               bool is_now_enabled) override;
+
   Profile* profile_ = nullptr;
   ExtensionPrefs* extension_prefs_ = nullptr;
   ExtensionService* extension_service_ = nullptr;
@@ -65,6 +78,9 @@
 
   // Used to subscribe to profile preferences updates.
   PrefChangeRegistrar pref_change_registrar_;
+
+  base::ScopedObservation<ExtensionPrefs, ExtensionPrefsObserver>
+      extension_prefs_observation_{this};
 };
 
 }  // namespace extensions
diff --git a/chrome/browser/extensions/extension_allowlist_unittest.cc b/chrome/browser/extensions/extension_allowlist_unittest.cc
index 0bb59d0f..987a2836 100644
--- a/chrome/browser/extensions/extension_allowlist_unittest.cc
+++ b/chrome/browser/extensions/extension_allowlist_unittest.cc
@@ -19,6 +19,7 @@
 // Extension ids used during testing.
 constexpr char kExtensionId1[] = "behllobkkfkfnphdnhnkndlbkcpglgmj";
 constexpr char kExtensionId2[] = "hpiknbiabeeppbpihjehijgoemciehgk";
+constexpr char kExtensionId3[] = "bjafgdebaacbbbecmhlhpofkepfkgcpa";
 
 }  // namespace
 
@@ -26,15 +27,7 @@
 //
 // Features EnforceSafeBrowsingExtensionAllowlist and
 // DisableMalwareExtensionsRemotely are enabled.
-class ExtensionAllowlistUnitTest : public ExtensionServiceTestBase {
- public:
-  ExtensionAllowlistUnitTest() {
-    feature_list_.InitWithFeatures(
-        {extensions_features::kEnforceSafeBrowsingExtensionAllowlist,
-         extensions_features::kDisableMalwareExtensionsRemotely},
-        {});
-  }
-
+class ExtensionAllowlistUnitTestBase : public ExtensionServiceTestBase {
  protected:
   // Creates a test extension service with 3 installed extensions.
   void CreateExtensionService(bool enhanced_protection_enabled) {
@@ -77,6 +70,16 @@
   ExtensionPrefs* extension_prefs_;
 };
 
+class ExtensionAllowlistUnitTest : public ExtensionAllowlistUnitTestBase {
+ public:
+  ExtensionAllowlistUnitTest() {
+    feature_list_.InitWithFeatures(
+        {extensions_features::kEnforceSafeBrowsingExtensionAllowlist,
+         extensions_features::kDisableMalwareExtensionsRemotely},
+        {});
+  }
+};
+
 TEST_F(ExtensionAllowlistUnitTest, AllowlistEnforcement) {
   // Created with 3 installed extensions.
   CreateExtensionService(/*enhanced_protection_enabled=*/true);
@@ -409,11 +412,192 @@
             extension_prefs()->GetDisableReasons(kExtensionId2));
 }
 
-TEST_F(ExtensionAllowlistUnitTest, NoEnforcementWhenFeatureDisabled) {
-  base::test::ScopedFeatureList feature_list;
-  feature_list.InitAndDisableFeature(
-      extensions_features::kEnforceSafeBrowsingExtensionAllowlist);
+TEST_F(ExtensionAllowlistUnitTest, AcknowledgeNeededOnEnforcement) {
+  CreateExtensionService(/*enhanced_protection_enabled=*/true);
 
+  service()->Init();
+  EXPECT_TRUE(IsEnabled(kExtensionId1));
+  EXPECT_EQ(
+      ALLOWLIST_ACKNOWLEDGE_NONE,
+      extension_prefs()->GetExtensionAllowlistAcknowledgeState(kExtensionId1));
+
+  // Make the extension not allowlisted.
+  PerformActionBasedOnOmahaAttributes(kExtensionId1,
+                                      /*is_malware=*/false,
+                                      /*is_allowlisted=*/false);
+
+  // Expect the acknowledge state to change appropriately.
+  EXPECT_TRUE(IsDisabled(kExtensionId1));
+  EXPECT_EQ(
+      ALLOWLIST_ACKNOWLEDGE_NEEDED,
+      extension_prefs()->GetExtensionAllowlistAcknowledgeState(kExtensionId1));
+}
+
+TEST_F(ExtensionAllowlistUnitTest, AcknowledgeNotNeededIfAlreadyDisabled) {
+  CreateExtensionService(/*enhanced_protection_enabled=*/true);
+
+  service()->Init();
+  service()->DisableExtension(kExtensionId1,
+                              disable_reason::DISABLE_USER_ACTION);
+  EXPECT_TRUE(IsDisabled(kExtensionId1));
+  EXPECT_EQ(
+      ALLOWLIST_ACKNOWLEDGE_NONE,
+      extension_prefs()->GetExtensionAllowlistAcknowledgeState(kExtensionId1));
+
+  // Make the extension not allowlisted.
+  PerformActionBasedOnOmahaAttributes(kExtensionId1,
+                                      /*is_malware=*/false,
+                                      /*is_allowlisted=*/false);
+
+  // There is no need for acknowledge if the extension was already disabled.
+  EXPECT_TRUE(IsDisabled(kExtensionId1));
+  EXPECT_EQ(disable_reason::DISABLE_NOT_ALLOWLISTED |
+                disable_reason::DISABLE_USER_ACTION,
+            extension_prefs()->GetDisableReasons(kExtensionId1));
+  EXPECT_EQ(
+      ALLOWLIST_ACKNOWLEDGE_NONE,
+      extension_prefs()->GetExtensionAllowlistAcknowledgeState(kExtensionId1));
+}
+
+TEST_F(ExtensionAllowlistUnitTest,
+       AcknowledgeStateIsSetWhenExtensionIsReenabled) {
+  CreateExtensionService(/*enhanced_protection_enabled=*/true);
+
+  EXPECT_EQ(
+      ALLOWLIST_ACKNOWLEDGE_NONE,
+      extension_prefs()->GetExtensionAllowlistAcknowledgeState(kExtensionId1));
+
+  // Start with a not allowlisted extension.
+  extension_prefs()->SetExtensionAllowlistState(kExtensionId1,
+                                                ALLOWLIST_NOT_ALLOWLISTED);
+
+  // The enforcement on init should disable the extension.
+  service()->Init();
+  EXPECT_TRUE(IsDisabled(kExtensionId1));
+  EXPECT_EQ(
+      ALLOWLIST_ACKNOWLEDGE_NEEDED,
+      extension_prefs()->GetExtensionAllowlistAcknowledgeState(kExtensionId1));
+
+  // Re-enable the extension.
+  service()->EnableExtension(kExtensionId1);
+
+  // The extensions should now be marked with
+  // `ALLOWLIST_ACKNOWLEDGE_ENABLED_BY_USER'.
+  EXPECT_EQ(
+      ALLOWLIST_ACKNOWLEDGE_ENABLED_BY_USER,
+      extension_prefs()->GetExtensionAllowlistAcknowledgeState(kExtensionId1));
+  EXPECT_TRUE(IsEnabled(kExtensionId1));
+  EXPECT_EQ(ALLOWLIST_NOT_ALLOWLISTED,
+            extension_prefs()->GetExtensionAllowlistState(kExtensionId1));
+}
+
+TEST_F(ExtensionAllowlistUnitTest, ReenabledExtensionsAreNotReenforced) {
+  CreateExtensionService(/*enhanced_protection_enabled=*/true);
+
+  // Start with a not allowlisted extension that was re-enabled by user.
+  extension_prefs()->SetExtensionAllowlistState(kExtensionId1,
+                                                ALLOWLIST_NOT_ALLOWLISTED);
+  extension_prefs()->SetExtensionAllowlistAcknowledgeState(
+      kExtensionId1, ALLOWLIST_ACKNOWLEDGE_ENABLED_BY_USER);
+
+  // And an extension that became allowlisted after it was re-enabled by user.
+  extension_prefs()->SetExtensionAllowlistState(kExtensionId2,
+                                                ALLOWLIST_ALLOWLISTED);
+  extension_prefs()->SetExtensionAllowlistAcknowledgeState(
+      kExtensionId2, ALLOWLIST_ACKNOWLEDGE_ENABLED_BY_USER);
+
+  service()->Init();
+  // Even though ExtensionId1 is not allowlisted, it should stay enabled because
+  // it was re-enabled by user.
+  EXPECT_TRUE(IsEnabled(kExtensionId1));
+  // Assert that ExtensionId2 is enabled before testing the allowlist state
+  // change.
+  EXPECT_TRUE(IsEnabled(kExtensionId2));
+
+  // If `kExtensionId2` becomes not allowlisted again, it should stay enabled
+  // because the user already chose to re-enable it in the past.
+  PerformActionBasedOnOmahaAttributes(kExtensionId2,
+                                      /*is_malware=*/false,
+                                      /*is_allowlisted=*/false);
+  EXPECT_TRUE(IsEnabled(kExtensionId2));
+  EXPECT_EQ(
+      ALLOWLIST_ACKNOWLEDGE_ENABLED_BY_USER,
+      extension_prefs()->GetExtensionAllowlistAcknowledgeState(kExtensionId2));
+  EXPECT_EQ(ALLOWLIST_NOT_ALLOWLISTED,
+            extension_prefs()->GetExtensionAllowlistState(kExtensionId2));
+}
+
+TEST_F(ExtensionAllowlistUnitTest, TurnOffEnhancedProtection) {
+  CreateExtensionService(/*enhanced_protection_enabled=*/true);
+
+  // Start with 3 not allowlisted extensions.
+  extension_prefs()->SetExtensionAllowlistState(kExtensionId1,
+                                                ALLOWLIST_NOT_ALLOWLISTED);
+  extension_prefs()->SetExtensionAllowlistState(kExtensionId2,
+                                                ALLOWLIST_NOT_ALLOWLISTED);
+  extension_prefs()->SetExtensionAllowlistState(kExtensionId3,
+                                                ALLOWLIST_NOT_ALLOWLISTED);
+  extension_prefs()->SetExtensionAllowlistAcknowledgeState(
+      kExtensionId3, ALLOWLIST_ACKNOWLEDGE_ENABLED_BY_USER);
+
+  // They should get disabled by allowlist enforcement and have their
+  // acknowledge state set (except the extension re-enabled by user).
+  service()->Init();
+  EXPECT_TRUE(IsDisabled(kExtensionId1));
+  EXPECT_EQ(
+      ALLOWLIST_ACKNOWLEDGE_NEEDED,
+      extension_prefs()->GetExtensionAllowlistAcknowledgeState(kExtensionId1));
+
+  EXPECT_TRUE(IsDisabled(kExtensionId2));
+  EXPECT_EQ(
+      ALLOWLIST_ACKNOWLEDGE_NEEDED,
+      extension_prefs()->GetExtensionAllowlistAcknowledgeState(kExtensionId2));
+
+  EXPECT_TRUE(IsEnabled(kExtensionId3));
+  EXPECT_EQ(
+      ALLOWLIST_ACKNOWLEDGE_ENABLED_BY_USER,
+      extension_prefs()->GetExtensionAllowlistAcknowledgeState(kExtensionId3));
+
+  // Leave `kExtensionId1` with acknowledge needed and acknowledge
+  // `kExtensionId2`.
+  extension_prefs()->SetExtensionAllowlistAcknowledgeState(
+      kExtensionId2, ALLOWLIST_ACKNOWLEDGE_DONE);
+
+  // When turning off enhanced protection.
+  safe_browsing::SetSafeBrowsingState(profile()->GetPrefs(),
+                                      safe_browsing::STANDARD_PROTECTION);
+
+  // 'kExtensionId1' and 'kExtensionId2' should be re-enabled and have their
+  // acknowledge state reset.
+  EXPECT_TRUE(IsEnabled(kExtensionId1));
+  EXPECT_EQ(
+      ALLOWLIST_ACKNOWLEDGE_NONE,
+      extension_prefs()->GetExtensionAllowlistAcknowledgeState(kExtensionId1));
+
+  EXPECT_TRUE(IsEnabled(kExtensionId2));
+  EXPECT_EQ(
+      ALLOWLIST_ACKNOWLEDGE_NONE,
+      extension_prefs()->GetExtensionAllowlistAcknowledgeState(kExtensionId2));
+
+  // 'kExtensionId3' should remain enabled because it was already re-enabled by
+  // user.
+  EXPECT_TRUE(IsEnabled(kExtensionId3));
+  EXPECT_EQ(
+      ALLOWLIST_ACKNOWLEDGE_ENABLED_BY_USER,
+      extension_prefs()->GetExtensionAllowlistAcknowledgeState(kExtensionId3));
+}
+
+class ExtensionAllowlistWithFeatureDisabledUnitTest
+    : public ExtensionAllowlistUnitTestBase {
+ public:
+  ExtensionAllowlistWithFeatureDisabledUnitTest() {
+    feature_list_.InitAndDisableFeature(
+        extensions_features::kEnforceSafeBrowsingExtensionAllowlist);
+  }
+};
+
+TEST_F(ExtensionAllowlistWithFeatureDisabledUnitTest,
+       NoEnforcementWhenFeatureDisabled) {
   // Created with 3 installed extensions.
   CreateExtensionService(/*enhanced_protection_enabled=*/true);
 
diff --git a/chrome/browser/extensions/forced_extensions/DIR_METADATA b/chrome/browser/extensions/forced_extensions/DIR_METADATA
index 6e4e82d..8edd370 100644
--- a/chrome/browser/extensions/forced_extensions/DIR_METADATA
+++ b/chrome/browser/extensions/forced_extensions/DIR_METADATA
@@ -1,3 +1,3 @@
 monorail {
-  component: "Enterprise>ExtensionReliability"
+  component: "OS>Software>Enterprise>ExtensionReliability"
 }
diff --git a/chrome/browser/flag-metadata.json b/chrome/browser/flag-metadata.json
index 30ec624..14519b7 100644
--- a/chrome/browser/flag-metadata.json
+++ b/chrome/browser/flag-metadata.json
@@ -1819,17 +1819,17 @@
   {
     "name": "enable-google-srp-isolated-prerender-nsp",
     "owners": [ "//chrome/browser/prefetch/prefetch_proxy/OWNERS" ],
-    "expiry_milestone": 90
+    "expiry_milestone": 95
   },
   {
     "name": "enable-google-srp-isolated-prerender-probing",
     "owners": [ "//chrome/browser/prefetch/prefetch_proxy/OWNERS" ],
-    "expiry_milestone": 90
+    "expiry_milestone": 95
   },
   {
     "name": "enable-google-srp-isolated-prerenders",
     "owners": [ "//chrome/browser/prefetch/prefetch_proxy/OWNERS" ],
-    "expiry_milestone": 90
+    "expiry_milestone": 95
   },
   {
     "name": "enable-gpu-rasterization",
@@ -2412,7 +2412,7 @@
   {
     "name": "enable-surface-control",
     "owners": [ "vikassoni", "vasilyt" ],
-    "expiry_milestone": 90
+    "expiry_milestone": 100
   },
   {
     "name": "enable-swipe-to-move-cursor",
@@ -3652,7 +3652,7 @@
   {
     "name": "new-usb-backend",
     "owners": [ "reillyg@chromium.org" ],
-    "expiry_milestone": 90
+    "expiry_milestone": 100
   },
   {
     "name": "notification-scheduler",
@@ -3927,7 +3927,7 @@
   {
     "name": "omnibox-refined-focus-state",
     "owners": [ "yoangela", "chrome-omnibox-team@google.com" ],
-    "expiry_milestone": 90
+    "expiry_milestone": 95
   },
   {
     "name": "omnibox-rich-autocompletion",
@@ -4968,11 +4968,6 @@
     "expiry_milestone": 82
   },
   {
-    "name": "tabbed-app-overflow-menu-three-button-actionbar",
-    "owners": [ "gangwu" ],
-    "expiry_milestone": 91
-  },
-  {
     "name": "text-fragment-color-change",
     "owners": [ "cheickcisse@google.com" ],
     "expiry_milestone": 92
diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc
index 644ae74c..1d57cb4 100644
--- a/chrome/browser/flag_descriptions.cc
+++ b/chrome/browser/flag_descriptions.cc
@@ -3426,11 +3426,6 @@
     "Allows user to use touch gestures to move the text cursor around. This "
     "flag will only take effect on Android 11 and above.";
 
-const char kTabbedAppOverflowMenuThreeButtonActionbarName[] =
-    "Android tabbed app overflow menu three buttons actionbar";
-const char kTabbedAppOverflowMenuThreeButtonActionbarDescription[] =
-    "If enabled, the actionbar in the overflow menu will have 3 buttons.";
-
 const char kWalletRequiresFirstSyncSetupCompleteName[] =
     "Controls whether Wallet (GPay) integration on Android requires "
     "first-sync-setup to be complete";
diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h
index 2ad4087c..8e413317 100644
--- a/chrome/browser/flag_descriptions.h
+++ b/chrome/browser/flag_descriptions.h
@@ -1986,9 +1986,6 @@
 extern const char kSwipeToMoveCursorName[];
 extern const char kSwipeToMoveCursorDescription[];
 
-extern const char kTabbedAppOverflowMenuThreeButtonActionbarName[];
-extern const char kTabbedAppOverflowMenuThreeButtonActionbarDescription[];
-
 extern const char kWalletRequiresFirstSyncSetupCompleteName[];
 extern const char kWalletRequiresFirstSyncSetupCompleteDescription[];
 
diff --git a/chrome/browser/flags/android/chrome_feature_list.cc b/chrome/browser/flags/android/chrome_feature_list.cc
index d85f0841..fb43573 100644
--- a/chrome/browser/flags/android/chrome_feature_list.cc
+++ b/chrome/browser/flags/android/chrome_feature_list.cc
@@ -230,7 +230,6 @@
     &kTabReparenting,
     &kTabSwitcherOnReturn,
     &kTabToGTSAnimation,
-    &kTabbedAppOverflowMenuThreeButtonActionbar,
     &kTestDefaultDisabled,
     &kTestDefaultEnabled,
     &kThemeRefactorAndroid,
@@ -643,10 +642,6 @@
 const base::Feature kTabToGTSAnimation{"TabToGTSAnimation",
                                        base::FEATURE_ENABLED_BY_DEFAULT};
 
-const base::Feature kTabbedAppOverflowMenuThreeButtonActionbar{
-    "TabbedAppOverflowMenuThreeButtonActionbar",
-    base::FEATURE_DISABLED_BY_DEFAULT};
-
 const base::Feature kTestDefaultDisabled{"TestDefaultDisabled",
                                          base::FEATURE_DISABLED_BY_DEFAULT};
 
diff --git a/chrome/browser/flags/android/chrome_feature_list.h b/chrome/browser/flags/android/chrome_feature_list.h
index 980f1cf..c1414aa 100644
--- a/chrome/browser/flags/android/chrome_feature_list.h
+++ b/chrome/browser/flags/android/chrome_feature_list.h
@@ -121,7 +121,6 @@
 extern const base::Feature kTabReparenting;
 extern const base::Feature kTabSwitcherOnReturn;
 extern const base::Feature kTabToGTSAnimation;
-extern const base::Feature kTabbedAppOverflowMenuThreeButtonActionbar;
 extern const base::Feature kTestDefaultDisabled;
 extern const base::Feature kTestDefaultEnabled;
 extern const base::Feature kThemeRefactorAndroid;
diff --git a/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/CachedFeatureFlags.java b/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/CachedFeatureFlags.java
index 3825a8b..b5275e41 100644
--- a/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/CachedFeatureFlags.java
+++ b/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/CachedFeatureFlags.java
@@ -78,7 +78,6 @@
             put(ChromeFeatureList.TEST_DEFAULT_ENABLED, true);
             put(ChromeFeatureList.REPORT_FEED_USER_ACTIONS, false);
             put(ChromeFeatureList.INTEREST_FEED_V2, true);
-            put(ChromeFeatureList.TABBED_APP_OVERFLOW_MENU_THREE_BUTTON_ACTIONBAR, false);
             put(ChromeFeatureList.THEME_REFACTOR_ANDROID, false);
             put(ChromeFeatureList.USE_CHIME_ANDROID_SDK, false);
             put(ChromeFeatureList.CCT_INCOGNITO_AVAILABLE_TO_THIRD_PARTY, false);
diff --git a/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java b/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java
index 25acc16..ddf63ad 100644
--- a/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java
+++ b/chrome/browser/flags/android/java/src/org/chromium/chrome/browser/flags/ChromeFeatureList.java
@@ -448,8 +448,6 @@
     public static final String TAB_REPARENTING = "TabReparenting";
     public static final String TAB_SWITCHER_ON_RETURN = "TabSwitcherOnReturn";
     public static final String TAB_TO_GTS_ANIMATION = "TabToGTSAnimation";
-    public static final String TABBED_APP_OVERFLOW_MENU_THREE_BUTTON_ACTIONBAR =
-            "TabbedAppOverflowMenuThreeButtonActionbar";
     public static final String TEST_DEFAULT_DISABLED = "TestDefaultDisabled";
     public static final String TEST_DEFAULT_ENABLED = "TestDefaultEnabled";
     public static final String THEME_REFACTOR_ANDROID = "ThemeRefactorAndroid";
diff --git a/chrome/browser/gcm/gcm_profile_service_factory.cc b/chrome/browser/gcm/gcm_profile_service_factory.cc
index 429196f16..f156cfe 100644
--- a/chrome/browser/gcm/gcm_profile_service_factory.cc
+++ b/chrome/browser/gcm/gcm_profile_service_factory.cc
@@ -106,7 +106,8 @@
 
 // static
 GCMProfileServiceFactory* GCMProfileServiceFactory::GetInstance() {
-  return base::Singleton<GCMProfileServiceFactory>::get();
+  static base::NoDestructor<GCMProfileServiceFactory> instance;
+  return instance.get();
 }
 
 GCMProfileServiceFactory::GCMProfileServiceFactory()
diff --git a/chrome/browser/gcm/gcm_profile_service_factory.h b/chrome/browser/gcm/gcm_profile_service_factory.h
index 8786f9f..a4f8d65 100644
--- a/chrome/browser/gcm/gcm_profile_service_factory.h
+++ b/chrome/browser/gcm/gcm_profile_service_factory.h
@@ -7,7 +7,7 @@
 
 #include "base/compiler_specific.h"
 #include "base/macros.h"
-#include "base/memory/singleton.h"
+#include "base/no_destructor.h"
 #include "components/gcm_driver/system_encryptor.h"
 #include "components/keyed_service/content/browser_context_keyed_service_factory.h"
 
@@ -35,7 +35,7 @@
   };
 
  private:
-  friend struct base::DefaultSingletonTraits<GCMProfileServiceFactory>;
+  friend base::NoDestructor<GCMProfileServiceFactory>;
 
   GCMProfileServiceFactory();
   ~GCMProfileServiceFactory() override;
diff --git a/chrome/browser/lacros/automation_manager_lacros.cc b/chrome/browser/lacros/automation_manager_lacros.cc
new file mode 100644
index 0000000..2287c5b3
--- /dev/null
+++ b/chrome/browser/lacros/automation_manager_lacros.cc
@@ -0,0 +1,118 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/lacros/automation_manager_lacros.h"
+
+#include "base/pickle.h"
+#include "chrome/browser/ui/aura/accessibility/automation_manager_aura.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_list.h"
+#include "chrome/browser/ui/browser_window.h"
+#include "chromeos/lacros/lacros_chrome_service_impl.h"
+#include "extensions/browser/api/automation_internal/automation_event_router.h"
+#include "extensions/browser/api/automation_internal/automation_internal_api.h"
+#include "extensions/common/extension_messages.h"
+#include "ui/accessibility/ax_tree_id.h"
+#include "ui/aura/window.h"
+#include "ui/aura/window_tree_host.h"
+#include "ui/aura/window_tree_host_platform.h"
+#include "ui/platform_window/platform_window.h"
+
+AutomationManagerLacros::AutomationManagerLacros() {
+  chromeos::LacrosChromeServiceImpl* impl =
+      chromeos::LacrosChromeServiceImpl::Get();
+  if (!impl->IsAutomationAvailable())
+    return;
+  id_ = base::UnguessableToken::Create();
+  impl->automation_remote()->RegisterAutomationClient(
+      receiver_.BindNewPipeAndPassRemote(), id_);
+
+  // TODO(https://crbug.com/1185764): Register as a receiver for events from
+  // AutomationEventRouter.
+}
+
+AutomationManagerLacros::~AutomationManagerLacros() = default;
+
+void AutomationManagerLacros::DispatchAccessibilityEvents(
+    const ui::AXTreeID& tree_id,
+    std::vector<ui::AXTreeUpdate> updates,
+    const gfx::Point& mouse_location,
+    std::vector<ui::AXEvent> events) {
+  ExtensionMsg_AccessibilityEventBundleParams event_bundle;
+  event_bundle.tree_id = tree_id;
+  event_bundle.updates = std::move(updates);
+  event_bundle.mouse_location = mouse_location;
+  event_bundle.events = std::move(events);
+
+  // TODO(https://crbug.com/1185764): We'll likely want to push this up to
+  // AutomationManagerAura/AXTreeViews where we can directly retrieve the root
+  // window given a view or widget on which we're serializing accessibility
+  // data.
+  BrowserList* list = BrowserList::GetInstance();
+  std::string window_id;
+  if (!list->empty()) {
+    Browser* browser = list->get(0);
+    aura::Window* window = browser->window()->GetNativeWindow();
+
+    // On desktop aura there is one WindowTreeHost per top-level window.
+    aura::WindowTreeHost* window_tree_host = window->GetHost();
+    DCHECK(window_tree_host);
+    // Lacros is based on Ozone/Wayland, which uses PlatformWindow and
+    // aura::WindowTreeHostPlatform.
+    aura::WindowTreeHostPlatform* window_tree_host_platform =
+        static_cast<aura::WindowTreeHostPlatform*>(window_tree_host);
+    window_id =
+        window_tree_host_platform->platform_window()->GetWindowUniqueId();
+  }
+  bool is_root =
+      tree_id ==
+      AutomationManagerAura::GetInstance()->get_root_tree_id_deprecated();
+  base::Pickle pickle;
+  IPC::ParamTraits<ExtensionMsg_AccessibilityEventBundleParams>::Write(
+      &pickle, event_bundle);
+  std::string result(static_cast<const char*>(pickle.data()), pickle.size());
+  chromeos::LacrosChromeServiceImpl::Get()
+      ->automation_remote()
+      ->ReceiveEventPrototype(std::move(result), is_root, id_, window_id);
+}
+
+void AutomationManagerLacros::DispatchAccessibilityLocationChange(
+    const ExtensionMsg_AccessibilityLocationChangeParams& params) {
+  // TODO(https://crbug.com/1185764): Implement me.
+}
+void AutomationManagerLacros::DispatchTreeDestroyedEvent(
+    ui::AXTreeID tree_id,
+    content::BrowserContext* browser_context) {
+  // TODO(https://crbug.com/1185764): Implement me.
+}
+void AutomationManagerLacros::DispatchActionResult(
+    const ui::AXActionData& data,
+    bool result,
+    content::BrowserContext* browser_context) {
+  // TODO(https://crbug.com/1185764): Implement me.
+}
+void AutomationManagerLacros::DispatchGetTextLocationDataResult(
+    const ui::AXActionData& data,
+    const base::Optional<gfx::Rect>& rect) {
+  // TODO(https://crbug.com/1185764): Implement me.
+}
+
+void AutomationManagerLacros::Enable() {
+  AutomationManagerAura::GetInstance()->Enable();
+}
+
+void AutomationManagerLacros::EnableTree(const base::UnguessableToken& token) {
+  // TODO(https://crbug.com/1185764): Plumb this into
+  // AutomationInternalEnableTreeFunction.
+}
+
+void AutomationManagerLacros::PerformActionPrototype(
+    const base::UnguessableToken& token,
+    int32_t automation_node_id,
+    const std::string& action_type,
+    int32_t request_id,
+    base::Value optional_args) {
+  // TODO(https://crbug.com/1185764): Plumb this into
+  // AutomationInternalPerformActionFunction.
+}
diff --git a/chrome/browser/lacros/automation_manager_lacros.h b/chrome/browser/lacros/automation_manager_lacros.h
new file mode 100644
index 0000000..fe3f57a5e
--- /dev/null
+++ b/chrome/browser/lacros/automation_manager_lacros.h
@@ -0,0 +1,58 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_LACROS_AUTOMATION_MANAGER_LACROS_H_
+#define CHROME_BROWSER_LACROS_AUTOMATION_MANAGER_LACROS_H_
+
+#include "base/memory/weak_ptr.h"
+#include "base/unguessable_token.h"
+#include "chromeos/crosapi/mojom/automation.mojom.h"
+#include "extensions/browser/api/automation_internal/automation_event_router_interface.h"
+#include "mojo/public/cpp/bindings/receiver.h"
+
+// This class receives and forwards automation events to Ash. It can only be
+// used on the main thread.
+class AutomationManagerLacros
+    : public crosapi::mojom::AutomationClient,
+      public extensions::AutomationEventRouterInterface {
+ public:
+  AutomationManagerLacros();
+  AutomationManagerLacros(const AutomationManagerLacros&) = delete;
+  AutomationManagerLacros& operator=(const AutomationManagerLacros&) = delete;
+  ~AutomationManagerLacros() override;
+
+ private:
+  // extensions::AutomationEventRouterInterface:
+  void DispatchAccessibilityEvents(const ui::AXTreeID& tree_id,
+                                   std::vector<ui::AXTreeUpdate> updates,
+                                   const gfx::Point& mouse_location,
+                                   std::vector<ui::AXEvent> events) override;
+  void DispatchAccessibilityLocationChange(
+      const ExtensionMsg_AccessibilityLocationChangeParams& params) override;
+  void DispatchTreeDestroyedEvent(
+      ui::AXTreeID tree_id,
+      content::BrowserContext* browser_context) override;
+  void DispatchActionResult(const ui::AXActionData& data,
+                            bool result,
+                            content::BrowserContext* browser_context) override;
+  void DispatchGetTextLocationDataResult(
+      const ui::AXActionData& data,
+      const base::Optional<gfx::Rect>& rect) override;
+
+  // AutomationClient:
+  void Enable() override;
+  void EnableTree(const base::UnguessableToken& token) override;
+  void PerformActionPrototype(const base::UnguessableToken& tree_id,
+                              int32_t automation_node_id,
+                              const std::string& action_type,
+                              int32_t request_id,
+                              base::Value optional_args) override;
+
+  // A unique id that identifies this instance of Lacros.
+  base::UnguessableToken id_;
+  mojo::Receiver<crosapi::mojom::AutomationClient> receiver_{this};
+  base::WeakPtrFactory<AutomationManagerLacros> weak_ptr_factory_{this};
+};
+
+#endif  // CHROME_BROWSER_LACROS_AUTOMATION_MANAGER_LACROS_H_
diff --git a/chrome/browser/lacros/chrome_browser_main_extra_parts_lacros.cc b/chrome/browser/lacros/chrome_browser_main_extra_parts_lacros.cc
new file mode 100644
index 0000000..7b18928c
--- /dev/null
+++ b/chrome/browser/lacros/chrome_browser_main_extra_parts_lacros.cc
@@ -0,0 +1,16 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/lacros/chrome_browser_main_extra_parts_lacros.h"
+
+#include "chrome/browser/lacros/automation_manager_lacros.h"
+
+ChromeBrowserMainExtraPartsLacros::ChromeBrowserMainExtraPartsLacros() =
+    default;
+ChromeBrowserMainExtraPartsLacros::~ChromeBrowserMainExtraPartsLacros() =
+    default;
+
+void ChromeBrowserMainExtraPartsLacros::PostBrowserStart() {
+  automation_manager_ = std::make_unique<AutomationManagerLacros>();
+}
diff --git a/chrome/browser/lacros/chrome_browser_main_extra_parts_lacros.h b/chrome/browser/lacros/chrome_browser_main_extra_parts_lacros.h
new file mode 100644
index 0000000..82fa9b3
--- /dev/null
+++ b/chrome/browser/lacros/chrome_browser_main_extra_parts_lacros.h
@@ -0,0 +1,31 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_LACROS_CHROME_BROWSER_MAIN_EXTRA_PARTS_LACROS_H_
+#define CHROME_BROWSER_LACROS_CHROME_BROWSER_MAIN_EXTRA_PARTS_LACROS_H_
+
+#include "chrome/browser/chrome_browser_main_extra_parts.h"
+
+#include <memory>
+
+class AutomationManagerLacros;
+
+// Browser initialization for Lacros.
+class ChromeBrowserMainExtraPartsLacros : public ChromeBrowserMainExtraParts {
+ public:
+  ChromeBrowserMainExtraPartsLacros();
+  ChromeBrowserMainExtraPartsLacros(const ChromeBrowserMainExtraPartsLacros&) =
+      delete;
+  ChromeBrowserMainExtraPartsLacros& operator=(
+      const ChromeBrowserMainExtraPartsLacros&) = delete;
+  ~ChromeBrowserMainExtraPartsLacros() override;
+
+ private:
+  // Overridden from ChromeBrowserMainExtraParts:
+  void PostBrowserStart() override;
+
+  std::unique_ptr<AutomationManagerLacros> automation_manager_;
+};
+
+#endif  // CHROME_BROWSER_LACROS_CHROME_BROWSER_MAIN_EXTRA_PARTS_LACROS_H_
diff --git a/chrome/browser/lifetime/application_lifetime.cc b/chrome/browser/lifetime/application_lifetime.cc
index 67dbba0..8ce9c61 100644
--- a/chrome/browser/lifetime/application_lifetime.cc
+++ b/chrome/browser/lifetime/application_lifetime.cc
@@ -344,10 +344,12 @@
 
   // EndSession is invoked once per frame. Only do something the first time.
   static bool already_ended = false;
-  // We may get called in the middle of shutdown, e.g. http://crbug.com/70852
-  // In this case, do nothing.
-  if (already_ended || !content::NotificationService::current())
+  // We may get called in the middle of shutdown, e.g. https://crbug.com/70852
+  // and https://crbug.com/1187418.  In this case, do nothing.
+  if (already_ended || !content::NotificationService::current() ||
+      !g_browser_process) {
     return;
+  }
   already_ended = true;
 
   // ~ShutdownWatcherHelper uses IO (it joins a thread). We'll only trigger that
diff --git a/chrome/browser/login_detection/login_detection_browsertest.cc b/chrome/browser/login_detection/login_detection_browsertest.cc
index c4892d1..9994439 100644
--- a/chrome/browser/login_detection/login_detection_browsertest.cc
+++ b/chrome/browser/login_detection/login_detection_browsertest.cc
@@ -8,10 +8,14 @@
 #include "chrome/browser/login_detection/login_detection_tab_helper.h"
 #include "chrome/browser/login_detection/login_detection_type.h"
 #include "chrome/browser/login_detection/login_detection_util.h"
+#include "chrome/browser/optimization_guide/optimization_guide_keyed_service.h"
+#include "chrome/browser/optimization_guide/optimization_guide_keyed_service_factory.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/browser.h"
 #include "chrome/test/base/in_process_browser_test.h"
 #include "chrome/test/base/ui_test_utils.h"
+#include "components/optimization_guide/content/browser/optimization_guide_decider.h"
+#include "components/optimization_guide/core/optimization_guide_features.h"
 #include "components/site_isolation/features.h"
 #include "components/ukm/test_ukm_recorder.h"
 #include "content/public/test/browser_test.h"
@@ -28,11 +32,19 @@
       : https_test_server_(net::EmbeddedTestServer::TYPE_HTTPS) {
     scoped_feature_list_.InitWithFeaturesAndParameters(
         {{kLoginDetection, {}},
-         {site_isolation::features::kSiteIsolationForPasswordSites, {}}},
+         {site_isolation::features::kSiteIsolationForPasswordSites, {}},
+         {optimization_guide::features::kOptimizationHints, {}}},
         {});
   }
 
   void SetUpOnMainThread() override {
+    auto* optimization_guide_decider =
+        OptimizationGuideKeyedServiceFactory::GetForProfile(
+            browser()->profile());
+    optimization_guide_decider->AddHintForTesting(
+        GURL("https://www.optguideloggedin.com/page.html"),
+        optimization_guide::proto::LOGIN_DETECTION, base::nullopt);
+
     https_test_server_.ServeFilesFromSourceDirectory("chrome/test/data");
     ASSERT_TRUE(https_test_server_.Start());
     histogram_tester_ = std::make_unique<base::HistogramTester>();
@@ -119,4 +131,12 @@
   ExpectLoginDetectionTypeMetric(LoginDetectionType::kOauthLogin);
 }
 
+IN_PROC_BROWSER_TEST_F(LoginDetectionBrowserTest,
+                       OptimizationGuideDetectedBlacklist) {
+  ui_test_utils::NavigateToURL(
+      browser(), GURL("https://www.optguideloggedin.com/page.html"));
+  ExpectLoginDetectionTypeMetric(
+      LoginDetectionType::kOptimizationGuideDetected);
+}
+
 }  // namespace login_detection
diff --git a/chrome/browser/login_detection/login_detection_keyed_service.cc b/chrome/browser/login_detection/login_detection_keyed_service.cc
index 90c2c0b..0f7bc472 100644
--- a/chrome/browser/login_detection/login_detection_keyed_service.cc
+++ b/chrome/browser/login_detection/login_detection_keyed_service.cc
@@ -6,6 +6,8 @@
 
 #include "chrome/browser/login_detection/login_detection_prefs.h"
 #include "chrome/browser/login_detection/login_detection_util.h"
+#include "chrome/browser/optimization_guide/optimization_guide_keyed_service.h"
+#include "chrome/browser/optimization_guide/optimization_guide_keyed_service_factory.h"
 #include "chrome/browser/password_manager/account_password_store_factory.h"
 #include "chrome/browser/profiles/profile.h"
 #include "content/public/browser/child_process_security_policy.h"
@@ -37,7 +39,13 @@
           ServiceAccessType::EXPLICIT_ACCESS)),
       account_password_sites_(AccountPasswordStoreFactory::GetForProfile(
           profile,
-          ServiceAccessType::EXPLICIT_ACCESS)) {}
+          ServiceAccessType::EXPLICIT_ACCESS)) {
+  if (auto* optimization_guide_decider =
+          OptimizationGuideKeyedServiceFactory::GetForProfile(profile_)) {
+    optimization_guide_decider->RegisterOptimizationTypes(
+        {optimization_guide::proto::LOGIN_DETECTION});
+  }
+}
 
 LoginDetectionKeyedService::~LoginDetectionKeyedService() = default;
 
@@ -74,6 +82,15 @@
     return LoginDetectionType::kPreloadedPasswordSiteLogin;
   }
 
+  if (auto* optimization_guide_decider =
+          OptimizationGuideKeyedServiceFactory::GetForProfile(profile_)) {
+    if (optimization_guide_decider->CanApplyOptimization(
+            url, optimization_guide::proto::LOGIN_DETECTION, nullptr) ==
+        optimization_guide::OptimizationGuideDecision::kTrue) {
+      return LoginDetectionType::kOptimizationGuideDetected;
+    }
+  }
+
   // Check for sites saved in the password manager.
   if (profile_password_sites_.IsSiteInPasswordStore(url) ||
       account_password_sites_.IsSiteInPasswordStore(url)) {
diff --git a/chrome/browser/login_detection/login_detection_keyed_service_factory.cc b/chrome/browser/login_detection/login_detection_keyed_service_factory.cc
index 3884595..7cfb7bb 100644
--- a/chrome/browser/login_detection/login_detection_keyed_service_factory.cc
+++ b/chrome/browser/login_detection/login_detection_keyed_service_factory.cc
@@ -6,6 +6,7 @@
 
 #include "chrome/browser/login_detection/login_detection_keyed_service.h"
 #include "chrome/browser/login_detection/login_detection_util.h"
+#include "chrome/browser/optimization_guide/optimization_guide_keyed_service_factory.h"
 #include "chrome/browser/password_manager/account_password_store_factory.h"
 #include "chrome/browser/password_manager/password_store_factory.h"
 #include "chrome/browser/profiles/profile.h"
@@ -40,6 +41,7 @@
           BrowserContextDependencyManager::GetInstance()) {
   DependsOn(AccountPasswordStoreFactory::GetInstance());
   DependsOn(PasswordStoreFactory::GetInstance());
+  DependsOn(OptimizationGuideKeyedServiceFactory::GetInstance());
 }
 
 LoginDetectionKeyedServiceFactory::~LoginDetectionKeyedServiceFactory() =
diff --git a/chrome/browser/login_detection/login_detection_type.h b/chrome/browser/login_detection/login_detection_type.h
index dba46436..2621c9a 100644
--- a/chrome/browser/login_detection/login_detection_type.h
+++ b/chrome/browser/login_detection/login_detection_type.h
@@ -39,7 +39,11 @@
   // Successful popup based OAuth login flow was detected.
   kOauthPopUpFirstTimeLoginFlow,
 
-  kMaxValue = kOauthPopUpFirstTimeLoginFlow
+  // Treated as logged-in since the site was detected as commonly logged-in from
+  // optimization guide hints.
+  kOptimizationGuideDetected,
+
+  kMaxValue = kOptimizationGuideDetected
 };
 }  // namespace login_detection
 
diff --git a/chrome/browser/lookalikes/lookalike_url_navigation_throttle.cc b/chrome/browser/lookalikes/lookalike_url_navigation_throttle.cc
index 10c17d1..8cad620 100644
--- a/chrome/browser/lookalikes/lookalike_url_navigation_throttle.cc
+++ b/chrome/browser/lookalikes/lookalike_url_navigation_throttle.cc
@@ -230,6 +230,13 @@
     LookalikeUrlMatchType match_type,
     bool validation_succeeded) {
   if (validation_succeeded) {
+    // Add the lookalike URL to the allowlist.
+    // TODO(meacer): Use a proper key for caching here. At the very least, we
+    // should allowlist (lookalike, target) pairs. We should also cache some of
+    // the failure cases, e.g. when the lookalike site serves a manifest but it
+    // doesn't have an entry for the target site.
+    ReputationService::Get(profile_)->SetUserIgnore(lookalike_domain);
+
     Resume();
     return;
   }
diff --git a/chrome/browser/lookalikes/lookalike_url_navigation_throttle_browsertest.cc b/chrome/browser/lookalikes/lookalike_url_navigation_throttle_browsertest.cc
index af98d5c..4f59a3e 100644
--- a/chrome/browser/lookalikes/lookalike_url_navigation_throttle_browsertest.cc
+++ b/chrome/browser/lookalikes/lookalike_url_navigation_throttle_browsertest.cc
@@ -1795,6 +1795,25 @@
   histograms.ExpectBucketCount(
       DigitalAssetLinkCrossValidator::kEventHistogramName,
       DigitalAssetLinkCrossValidator::Event::kValidationSucceeded, 1);
+
+  // Try again. The first try should have added the lookalike site to the
+  // allowlist so we shouldn't fetch the manifests again.
+  TestInterstitialNotShown(browser(), MakeURL("googlé.com"));
+  CheckNoUkm();
+  // Ensure that there was indeed a lookalike match.
+  histograms.ExpectTotalCount(lookalikes::kHistogramName, 1);
+  histograms.ExpectBucketCount(lookalikes::kHistogramName,
+                               NavigationSuggestionEvent::kMatchSkeletonTop500,
+                               1);
+  // Validator histogram should remain unchanged.
+  histograms.ExpectTotalCount(
+      DigitalAssetLinkCrossValidator::kEventHistogramName, 2);
+  histograms.ExpectBucketCount(
+      DigitalAssetLinkCrossValidator::kEventHistogramName,
+      DigitalAssetLinkCrossValidator::Event::kStarted, 1);
+  histograms.ExpectBucketCount(
+      DigitalAssetLinkCrossValidator::kEventHistogramName,
+      DigitalAssetLinkCrossValidator::Event::kValidationSucceeded, 1);
 }
 
 // Similar to ValidAssetLinks_IgnoreInterstitial, but the lookalike manifest
diff --git a/chrome/browser/media/wv_test_license_server_config.cc b/chrome/browser/media/wv_test_license_server_config.cc
index 1bfcc28..f7aaf53 100644
--- a/chrome/browser/media/wv_test_license_server_config.cc
+++ b/chrome/browser/media/wv_test_license_server_config.cc
@@ -129,11 +129,13 @@
 }
 
 bool WVTestLicenseServerConfig::IsPlatformSupported() {
-#if (defined(OS_LINUX) || defined(OS_CHROMEOS)) && defined(ARCH_CPU_X86_64)
+// TODO(crbug.com/1175344): Reenable OS_LINUX once license server
+// (or Widevine CDM) updated.
+#if defined(OS_CHROMEOS) && defined(ARCH_CPU_X86_64)
   return true;
 #else
   return false;
-#endif // (defined(OS_LINUX) || defined(OS_CHROMEOS)) && defined(ARCH_CPU_X86_64)
+#endif  // defined(OS_CHROMEOS) && defined(ARCH_CPU_X86_64)
 }
 
 std::string WVTestLicenseServerConfig::GetServerURL() {
diff --git a/chrome/browser/metrics/perf/profile_provider_unittest_main.cc b/chrome/browser/metrics/perf/profile_provider_unittest_main.cc
index bfd2f115d..c2b8882 100644
--- a/chrome/browser/metrics/perf/profile_provider_unittest_main.cc
+++ b/chrome/browser/metrics/perf/profile_provider_unittest_main.cc
@@ -166,6 +166,7 @@
     StopSpinningCPU();
 
     profile_provider_.reset();
+    TestingBrowserProcess::DeleteInstance();
     chromeos::LoginState::Shutdown();
     chromeos::PowerManagerClient::Shutdown();
     chromeos::DBusThreadManager::Shutdown();
diff --git a/chrome/browser/notifications/non_persistent_notification_handler.cc b/chrome/browser/notifications/non_persistent_notification_handler.cc
index 34f88e0..082420f4 100644
--- a/chrome/browser/notifications/non_persistent_notification_handler.cc
+++ b/chrome/browser/notifications/non_persistent_notification_handler.cc
@@ -6,7 +6,6 @@
 
 #include "base/bind.h"
 #include "base/callback.h"
-#include "base/strings/nullable_string16.h"
 #include "build/build_config.h"
 #include "chrome/browser/notifications/notification_common.h"
 #include "chrome/browser/notifications/notification_permission_context.h"
diff --git a/chrome/browser/notifications/notification_platform_bridge_android.cc b/chrome/browser/notifications/notification_platform_bridge_android.cc
index 4260179..9490852 100644
--- a/chrome/browser/notifications/notification_platform_bridge_android.cc
+++ b/chrome/browser/notifications/notification_platform_bridge_android.cc
@@ -15,7 +15,6 @@
 #include "base/command_line.h"
 #include "base/files/file_path.h"
 #include "base/notreached.h"
-#include "base/strings/nullable_string16.h"
 #include "base/strings/utf_string_conversions.h"
 #include "chrome/android/chrome_jni_headers/ActionInfo_jni.h"
 #include "chrome/android/chrome_jni_headers/NotificationPlatformBridge_jni.h"
diff --git a/chrome/browser/notifications/notification_platform_bridge_lacros.cc b/chrome/browser/notifications/notification_platform_bridge_lacros.cc
index 36fcdd1..875cf4f 100644
--- a/chrome/browser/notifications/notification_platform_bridge_lacros.cc
+++ b/chrome/browser/notifications/notification_platform_bridge_lacros.cc
@@ -64,8 +64,12 @@
   mojo_note->timestamp = notification.timestamp();
   if (!notification.image().IsEmpty())
     mojo_note->image = notification.image().AsImageSkia();
-  if (!notification.small_image().IsEmpty())
+  if (!notification.small_image().IsEmpty()) {
     mojo_note->badge = notification.small_image().AsImageSkia();
+    mojo_note->badge_needs_additional_masking_has_value = true;
+    mojo_note->badge_needs_additional_masking =
+        notification.small_image_needs_additional_masking();
+  }
   for (const auto& item : notification.items()) {
     auto mojo_item = crosapi::mojom::NotificationItem::New();
     mojo_item->title = item.title;
diff --git a/chrome/browser/notifications/notification_platform_bridge_lacros_unittest.cc b/chrome/browser/notifications/notification_platform_bridge_lacros_unittest.cc
index 907c3e6..5af7bb1 100644
--- a/chrome/browser/notifications/notification_platform_bridge_lacros_unittest.cc
+++ b/chrome/browser/notifications/notification_platform_bridge_lacros_unittest.cc
@@ -178,6 +178,7 @@
             last_notification->fullscreen_visibility);
 
   ASSERT_FALSE(last_notification->badge.isNull());
+  EXPECT_TRUE(last_notification->badge_needs_additional_masking_has_value);
   EXPECT_TRUE(last_notification->badge.HasRepresentation(1.0f));
   EXPECT_TRUE(last_notification->badge.HasRepresentation(2.0f));
   EXPECT_TRUE(AreImagesEqual(badge, gfx::Image(last_notification->badge)));
diff --git a/chrome/browser/notifications/notification_platform_bridge_linux.cc b/chrome/browser/notifications/notification_platform_bridge_linux.cc
index 2258cdf4..a642b0a8 100644
--- a/chrome/browser/notifications/notification_platform_bridge_linux.cc
+++ b/chrome/browser/notifications/notification_platform_bridge_linux.cc
@@ -22,7 +22,6 @@
 #include "base/memory/ptr_util.h"
 #include "base/metrics/histogram_macros.h"
 #include "base/numerics/ranges.h"
-#include "base/strings/nullable_string16.h"
 #include "base/strings/string_number_conversions.h"
 #include "base/strings/string_split.h"
 #include "base/strings/string_util.h"
diff --git a/chrome/browser/notifications/notification_platform_bridge_mac.mm b/chrome/browser/notifications/notification_platform_bridge_mac.mm
index ce17b39..4151b15d3 100644
--- a/chrome/browser/notifications/notification_platform_bridge_mac.mm
+++ b/chrome/browser/notifications/notification_platform_bridge_mac.mm
@@ -18,7 +18,6 @@
 #include "base/mac/scoped_mach_port.h"
 #include "base/mac/scoped_nsobject.h"
 #include "base/optional.h"
-#include "base/strings/nullable_string16.h"
 #include "base/strings/string_number_conversions.h"
 #include "base/strings/sys_string_conversions.h"
 #include "base/strings/utf_string_conversions.h"
diff --git a/chrome/browser/prefs/browser_prefs.cc b/chrome/browser/prefs/browser_prefs.cc
index d170c8a..6b6c83fd 100644
--- a/chrome/browser/prefs/browser_prefs.cc
+++ b/chrome/browser/prefs/browser_prefs.cc
@@ -55,7 +55,6 @@
 #include "chrome/browser/prefetch/search_prefetch/search_prefetch_service.h"
 #include "chrome/browser/prefs/chrome_pref_service_factory.h"
 #include "chrome/browser/prefs/incognito_mode_prefs.h"
-#include "chrome/browser/prefs/origin_trial_prefs.h"
 #include "chrome/browser/prefs/session_startup_pref.h"
 #include "chrome/browser/printing/print_preview_sticky_settings.h"
 #include "chrome/browser/profiles/chrome_version_service.h"
@@ -101,6 +100,7 @@
 #include "components/dom_distiller/core/distilled_page_prefs.h"
 #include "components/dom_distiller/core/dom_distiller_features.h"
 #include "components/dom_distiller/core/pref_names.h"
+#include "components/embedder_support/origin_trials/origin_trial_prefs.h"
 #include "components/federated_learning/floc_id.h"
 #include "components/flags_ui/pref_service_flags_storage.h"
 #include "components/image_fetcher/core/cache/image_cache.h"
@@ -680,6 +680,7 @@
   ChromeMetricsServiceClient::RegisterPrefs(registry);
   ChromeTracingDelegate::RegisterPrefs(registry);
   component_updater::RegisterPrefs(registry);
+  embedder_support::OriginTrialPrefs::RegisterPrefs(registry);
   ExternalProtocolHandler::RegisterPrefs(registry);
   flags_ui::PrefServiceFlagsStorage::RegisterPrefs(registry);
   GpuModeManager::RegisterPrefs(registry);
@@ -691,7 +692,6 @@
   language::UlpLanguageCodeLocator::RegisterLocalStatePrefs(registry);
   memory::EnterpriseMemoryLimitPrefObserver::RegisterPrefs(registry);
   network_time::NetworkTimeTracker::RegisterPrefs(registry);
-  OriginTrialPrefs::RegisterPrefs(registry);
   password_manager::PasswordManager::RegisterLocalPrefs(registry);
   policy::BrowserPolicyConnector::RegisterPrefs(registry);
   policy::PolicyStatisticsCollector::RegisterPrefs(registry);
diff --git a/chrome/browser/prefs/origin_trial_prefs.h b/chrome/browser/prefs/origin_trial_prefs.h
deleted file mode 100644
index 3e8baafb..0000000
--- a/chrome/browser/prefs/origin_trial_prefs.h
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright 2016 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef CHROME_BROWSER_PREFS_ORIGIN_TRIAL_PREFS_H_
-#define CHROME_BROWSER_PREFS_ORIGIN_TRIAL_PREFS_H_
-
-class PrefRegistrySimple;
-
-class OriginTrialPrefs {
- public:
-  static void RegisterPrefs(PrefRegistrySimple* registry);
-};
-
-#endif  // CHROME_BROWSER_PREFS_ORIGIN_TRIAL_PREFS_H_
diff --git a/chrome/browser/query_tiles/tile_service_factory.cc b/chrome/browser/query_tiles/tile_service_factory.cc
index 2a0279d4..734ecb0 100644
--- a/chrome/browser/query_tiles/tile_service_factory.cc
+++ b/chrome/browser/query_tiles/tile_service_factory.cc
@@ -106,7 +106,8 @@
       SystemNetworkContextManager::GetInstance()->GetSharedURLLoaderFactory();
 
   base::Version version = version_info::GetVersion();
-  std::string channel_name = chrome::GetChannelName();
+  std::string channel_name =
+      chrome::GetChannelName(chrome::WithExtendedStable(true));
   std::string client_version =
       base::StringPrintf("%d.%d.%d.%s.chrome",
                          version.components()[0],  // Major
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/background_test.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/background_test.js
index d6b6efe..3a0cb59 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/background_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/background_test.js
@@ -2671,14 +2671,12 @@
           }
         });
 
-        const evt2 =
-            new CustomAutomationEvent(EventType.FOCUS, group2, '', '', []);
+        const evt2 = new CustomAutomationEvent(EventType.FOCUS, group2);
         const currentRange = ChromeVoxState.instance.currentRange;
         DesktopAutomationHandler.instance.onFocus(evt2);
         assertEquals(currentRange, ChromeVoxState.instance.currentRange);
 
-        const evt1 =
-            new CustomAutomationEvent(EventType.FOCUS, group1, '', '', []);
+        const evt1 = new CustomAutomationEvent(EventType.FOCUS, group1);
         mockFeedback
             .call(DesktopAutomationHandler.instance.onFocus.bind(
                 DesktopAutomationHandler.instance, evt1))
@@ -2905,8 +2903,7 @@
           }
         }());
         const button = root.find({role: RoleType.BUTTON});
-        const alertEvt =
-            new CustomAutomationEvent(EventType.ALERT, button, '', '', []);
+        const alertEvt = new CustomAutomationEvent(EventType.ALERT, button);
         mockFeedback
             .call(DesktopAutomationHandler.instance.onAlert.bind(
                 DesktopAutomationHandler.instance, alertEvt))
@@ -2929,8 +2926,7 @@
         }());
 
         const button = root.find({role: RoleType.BUTTON});
-        const alertEvt =
-            new CustomAutomationEvent(EventType.ALERT, button, '', '', []);
+        const alertEvt = new CustomAutomationEvent(EventType.ALERT, button);
         mockFeedback
             .call(DesktopAutomationHandler.instance.onAlert.bind(
                 DesktopAutomationHandler.instance, alertEvt))
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/command_handler.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/command_handler.js
index 6506d77..0ae7911 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/command_handler.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/command_handler.js
@@ -168,9 +168,9 @@
       ChromeVoxState.isReadingContinuously = false;
       return false;
     case 'toggleEarcons': {
-      AbstractEarcons.enabled = !AbstractEarcons.enabled;
-      const announce = AbstractEarcons.enabled ? Msgs.getMsg('earcons_on') :
-                                                 Msgs.getMsg('earcons_off');
+      ChromeVox.earcons.enabled = !ChromeVox.earcons.enabled;
+      const announce = ChromeVox.earcons.enabled ? Msgs.getMsg('earcons_on') :
+                                                   Msgs.getMsg('earcons_off');
       ChromeVox.tts.speak(
           announce, QueueMode.FLUSH, AbstractTts.PERSONALITY_ANNOTATION);
     }
@@ -1337,7 +1337,7 @@
   CommandHandler.imageNode_ = imageNode;
   if (imageNode.imageDataUrl) {
     const event = new CustomAutomationEvent(
-        EventType.IMAGE_FRAME_UPDATED, imageNode, 'page', '', []);
+        EventType.IMAGE_FRAME_UPDATED, imageNode, {eventFrom: 'page'});
     CommandHandler.onImageFrameUpdated_(event);
   } else {
     imageNode.getImageData(0, 0);
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/custom_automation_event.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/custom_automation_event.js
index cbe1724..e78510c6 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/custom_automation_event.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/custom_automation_event.js
@@ -24,18 +24,17 @@
   /**
    * @param {chrome.automation.EventType} type The event type.
    * @param {!chrome.automation.AutomationNode} target The event target.
-   * @param {string} eventFrom The source of this event.
-   * @param {string} eventFromAction The accessibility action that caused this
-   *     event.
-   * @param {!Array<chrome.automation.AutomationIntent>} intents Intents for
-   *     this event.
+   * @param {!{eventFrom: (string|undefined),
+   *           eventFromAction: (string|undefined),
+   *           intents: (!Array<chrome.automation.AutomationIntent>|undefined)
+   *        }} params
    */
-  constructor(type, target, eventFrom, eventFromAction, intents) {
+  constructor(type, target, params = {}) {
     this.type = type;
     this.target = target;
-    this.eventFrom = eventFrom;
-    this.eventFromAction = eventFromAction;
-    this.intents = intents;
+    this.eventFrom = params.eventFrom || '';
+    this.eventFromAction = params.eventFromAction || '';
+    this.intents = params.intents || [];
   }
 
   /**
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/desktop_automation_handler.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/desktop_automation_handler.js
index 5121e7f..e3db830 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/desktop_automation_handler.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/desktop_automation_handler.js
@@ -97,8 +97,10 @@
       chrome.automation.getFocus((function(focus) {
                                    if (focus) {
                                      const event = new CustomAutomationEvent(
-                                         EventType.FOCUS, focus, 'page',
-                                         ActionType.FOCUS, []);
+                                         EventType.FOCUS, focus, {
+                                           eventFrom: 'page',
+                                           eventFromAction: ActionType.FOCUS
+                                         });
                                      this.onFocus(event);
                                    }
                                  }).bind(this));
@@ -216,9 +218,12 @@
     if (selectionStart.state[StateType.EDITABLE]) {
       selectionStart =
           AutomationUtil.getEditableRoot(selectionStart) || selectionStart;
-      this.onEditableChanged_(new CustomAutomationEvent(
-          evt.type, selectionStart, evt.eventFrom, evt.eventFromAction,
-          evt.intents));
+      this.onEditableChanged_(
+          new CustomAutomationEvent(evt.type, selectionStart, {
+            eventFrom: evt.eventFrom,
+            eventFromAction: evt.eventFromAction,
+            intents: evt.intents
+          }));
     }
 
     // Non-editable selections are handled in |Background|.
@@ -274,8 +279,11 @@
     // category flush here or the focus events will all queue up.
     Output.forceModeForNextSpeechUtterance(QueueMode.CATEGORY_FLUSH);
 
-    const event = new CustomAutomationEvent(
-        EventType.FOCUS, node, evt.eventFrom, evt.eventFromAction, evt.intents);
+    const event = new CustomAutomationEvent(EventType.FOCUS, node, {
+      eventFrom: evt.eventFrom,
+      eventFromAction: evt.eventFromAction,
+      intents: evt.intents
+    });
     this.onEventDefault(event);
 
     // Refresh the handler, if needed, now that ChromeVox focus is up to date.
@@ -604,7 +612,8 @@
     chrome.automation.getFocus(function(focus) {
       if (focus) {
         const event = new CustomAutomationEvent(
-            EventType.FOCUS, focus, 'page', ActionType.FOCUS, []);
+            EventType.FOCUS, focus,
+            {eventFrom: 'page', eventFromAction: ActionType.FOCUS});
         this.onFocus(event);
       }
     }.bind(this));
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/earcons.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/earcons.js
index dffd3b41..d358663 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/earcons.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/earcons.js
@@ -11,10 +11,6 @@
   constructor() {
     super();
 
-    if (localStorage['earcons'] === 'false') {
-      AbstractEarcons.enabled = false;
-    }
-
     /**
      * @type {EarconEngine}
      * @private
@@ -50,7 +46,7 @@
    * @override
    */
   playEarcon(earcon, opt_location) {
-    if (!AbstractEarcons.enabled) {
+    if (!this.enabled) {
       return;
     }
     if (localStorage['enableEarconLogging'] === 'true') {
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/pointer_handler.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/pointer_handler.js
index 78a1c11..11ce705 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/pointer_handler.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/pointer_handler.js
@@ -194,8 +194,8 @@
 
     Output.forceModeForNextSpeechUtterance(QueueMode.FLUSH);
     DesktopAutomationHandler.instance.onEventDefault(new CustomAutomationEvent(
-        EventType.HOVER, target, '', chrome.automation.ActionType.HIT_TEST,
-        []));
+        EventType.HOVER, target,
+        {eventFromAction: chrome.automation.ActionType.HIT_TEST}));
   }
 
   /**
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/range_automation_handler.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/range_automation_handler.js
index 4cf8816f..2e3017b 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/range_automation_handler.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/range_automation_handler.js
@@ -192,9 +192,12 @@
       return;
     }
 
-    const event = new CustomAutomationEvent(
-        EventType.CHECKED_STATE_CHANGED, evt.target, evt.eventFrom,
-        evt.eventFromAction, evt.intents);
+    const event =
+        new CustomAutomationEvent(EventType.CHECKED_STATE_CHANGED, evt.target, {
+          eventFrom: evt.eventFrom,
+          eventFromAction: evt.eventFromAction,
+          intents: evt.intents
+        });
     this.onEventIfInRange(event);
   }
 
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/common/abstract_earcons.js b/chrome/browser/resources/chromeos/accessibility/chromevox/common/abstract_earcons.js
index ddf8e2c..769f1e9 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/common/abstract_earcons.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/common/abstract_earcons.js
@@ -103,19 +103,18 @@
   }
 
   /**
-   * Toggles earcons on or off.
-   * @return {boolean} True if earcons are now enabled; false otherwise.
+   * Whether or not earcons are enabled.
+   * @return {boolean} True if earcons are enabled.
    */
-  toggle() {
-    AbstractEarcons.enabled = !AbstractEarcons.enabled;
-    return AbstractEarcons.enabled;
+  get enabled() {
+    return localStorage['earcons'] === 'true';
+  }
+
+  /**
+   * Set whether or not earcons are enabled.
+   * @param {Boolean} value True turns on earcons, false turns off earcons.
+   */
+  set enabled(value) {
+    localStorage['earcons'] = value;
   }
 };
-
-
-/**
- * Public static flag set to enable or disable earcons. Callers should prefer
- * toggle(); however, this member is public for initialization.
- * @type {boolean}
- */
-AbstractEarcons.enabled = true;
diff --git a/chrome/browser/resources/management/management_browser_proxy.js b/chrome/browser/resources/management/management_browser_proxy.js
index 4636c7d..ea104c3 100644
--- a/chrome/browser/resources/management/management_browser_proxy.js
+++ b/chrome/browser/resources/management/management_browser_proxy.js
@@ -76,6 +76,7 @@
   APP_INFO_AND_ACTIVITY: 'app info and activity',
   LOGS: 'logs',
   PRINT: 'print',
+  PRINT_JOBS: 'print jobs',
   CROSTINI: 'crostini',
   USERNAME: 'username',
   EXTENSION: 'extension',
diff --git a/chrome/browser/resources/management/management_ui.js b/chrome/browser/resources/management/management_ui.js
index 90aa0b45..466680c 100644
--- a/chrome/browser/resources/management/management_ui.js
+++ b/chrome/browser/resources/management/management_ui.js
@@ -262,6 +262,8 @@
         return 'management:report';
       case DeviceReportingType.PRINT:
         return 'cr:print';
+      case DeviceReportingType.PRINT_JOBS:
+        return 'cr:print';
       case DeviceReportingType.CROSTINI:
         return 'management:linux';
       case DeviceReportingType.USERNAME:
diff --git a/chrome/browser/resources/memories/BUILD.gn b/chrome/browser/resources/memories/BUILD.gn
index 3232fd0..9f46fe6 100644
--- a/chrome/browser/resources/memories/BUILD.gn
+++ b/chrome/browser/resources/memories/BUILD.gn
@@ -41,11 +41,20 @@
 
 js_library("memory_card") {
   deps = [
+    ":top_visit",
     "//components/memories/core:mojo_bindings_webui_js",
     "//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
   ]
 }
 
+js_library("page_favicon") {
+  deps = [
+    "//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
+    "//ui/webui/resources/js:icon.m",
+    "//url/mojom:url_mojom_origin_webui_js",
+  ]
+}
+
 js_library("page_thumbnail") {
   deps = [
     ":utils",
@@ -56,15 +65,40 @@
   ]
 }
 
+js_library("top_visit") {
+  deps = [
+    ":visit_row",
+    "//components/memories/core:mojo_bindings_webui_js",
+    "//third_party/polymer/v3_0/components-chromium/iron-collapse:iron-collapse",
+    "//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
+    "//ui/webui/resources/cr_elements/cr_expand_button:cr_expand_button.m",
+  ]
+}
+
 js_library("utils") {
   deps = [ "//mojo/public/mojom/base:base_webui_js" ]
 }
 
+js_library("visit_row") {
+  deps = [
+    ":page_favicon",
+    ":utils",
+    "//components/memories/core:mojo_bindings_webui_js",
+    "//mojo/public/mojom/base:base_webui_js",
+    "//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
+    "//url/mojom:url_mojom_origin_webui_js",
+  ]
+}
+
 html_to_js("web_components_local") {
   js_files = [
     "app.js",
     "memory_card.js",
+    "page_favicon.js",
     "page_thumbnail.js",
+    "shared_vars.js",
+    "top_visit.js",
+    "visit_row.js",
   ]
 }
 
@@ -97,7 +131,11 @@
   in_files = [
     "app.js",
     "memory_card.js",
+    "page_favicon.js",
     "page_thumbnail.js",
+    "shared_vars.js",
+    "top_visit.js",
+    "visit_row.js",
   ]
   deps = [ ":web_components" ]
 }
diff --git a/chrome/browser/resources/memories/app.html b/chrome/browser/resources/memories/app.html
index d8bb9cb..dcd956e 100644
--- a/chrome/browser/resources/memories/app.html
+++ b/chrome/browser/resources/memories/app.html
@@ -1,6 +1,6 @@
 <style include="cr-shared-style">
   :host {
-    --memory-card-width: 742px;
+    color: var(--cr-primary-text-color);
   }
 
   .container {
@@ -18,9 +18,9 @@
   }
 
   page-thumbnail {
-    --border-radius: 16px;
-    --max-height: 100px;
-    --max-width: 100px;
+    --thumbnail-border-radius: 16px;
+    --thumbnail-max-height: 100px;
+    --thumbnail-max-width: 100px;
     margin-inline-end: 25px;
   }
 
@@ -30,7 +30,7 @@
   }
 
   .title .description {
-    color: var(--google-grey-refresh-700);
+    color: var(--cr-secondary-text-color);
     font-size: 12px;
     line-height: 20px;
   }
@@ -41,9 +41,9 @@
   }
 </style>
 <div class="container">
-  <div class="header" hidden$="[[!title_]]">
+  <div class="header" hidden="[[!result_.title]]">
     <page-thumbnail page="[[createPageWithThumbnail_(result_.thumbnailUrl)]]"
-        hidden$="[[!result_.thumbnailUrl]]">
+        hidden="[[!result_.thumbnailUrl]]">
     </page-thumbnail>
     <div class="title">
       <span class="description">$i18n{memoryTitleDescription}</span>
diff --git a/chrome/browser/resources/memories/app.js b/chrome/browser/resources/memories/app.js
index 46565e0e..7e9c235 100644
--- a/chrome/browser/resources/memories/app.js
+++ b/chrome/browser/resources/memories/app.js
@@ -4,6 +4,7 @@
 
 import './memory_card.js';
 import './page_thumbnail.js';
+import './shared_vars.js';
 import 'chrome://resources/cr_elements/shared_style_css.m.js';
 
 import {MemoriesResult, PageCallbackRouter, PageHandlerRemote} from '/chrome/browser/ui/webui/memories/memories.mojom-webui.js';
diff --git a/chrome/browser/resources/memories/memory_card.html b/chrome/browser/resources/memories/memory_card.html
index 844f85b..e4b09bdf3 100644
--- a/chrome/browser/resources/memories/memory_card.html
+++ b/chrome/browser/resources/memories/memory_card.html
@@ -4,9 +4,21 @@
     border: 1px solid var(--google-grey-refresh-300);
     border-radius: 16px;
     box-sizing: border-box;
-    color: var(--google-grey-900);
     margin: 24px 0;
     padding: 24px;
     width: var(--memory-card-width);
   }
+
+  .section .header {
+    font-size: 14px;
+    line-height: 20px;
+  }
 </style>
+<div class="section" hidden="[[!memory.topVisits.length]]">
+  <div class="header">$i18n{topVisitsSectionHeader}</div>
+  <div class="top-visits">
+    <template is="dom-repeat" items="[[memory.topVisits]]">
+      <top-visit visit="[[item]]"></top-visit>
+    </template>
+  </div>
+</div>
diff --git a/chrome/browser/resources/memories/memory_card.js b/chrome/browser/resources/memories/memory_card.js
index e6b187b..c8dbe020 100644
--- a/chrome/browser/resources/memories/memory_card.js
+++ b/chrome/browser/resources/memories/memory_card.js
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import './shared_vars.js';
+import './top_visit.js';
 import 'chrome://resources/cr_elements/shared_style_css.m.js';
 
 import {Memory} from '/components/memories/core/memories.mojom-webui.js';
diff --git a/chrome/browser/resources/memories/page_favicon.html b/chrome/browser/resources/memories/page_favicon.html
new file mode 100644
index 0000000..605e8141
--- /dev/null
+++ b/chrome/browser/resources/memories/page_favicon.html
@@ -0,0 +1,6 @@
+<style>
+  :host {
+    background-position: center;
+    background-repeat: no-repeat;
+  }
+</style>
diff --git a/chrome/browser/resources/memories/page_favicon.js b/chrome/browser/resources/memories/page_favicon.js
new file mode 100644
index 0000000..9eb8cde
--- /dev/null
+++ b/chrome/browser/resources/memories/page_favicon.js
@@ -0,0 +1,57 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {getFaviconForPageURL} from 'chrome://resources/js/icon.m.js';
+import {Url} from 'chrome://resources/mojo/url/mojom/url.mojom-webui.js';
+import {html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+
+/**
+ * @fileoverview This file provides a custom element displaying a page favicon.
+ */
+
+class PageFavicon extends PolymerElement {
+  static get is() {
+    return 'page-favicon';
+  }
+
+  static get template() {
+    return html`{__html_template__}`;
+  }
+
+  static get properties() {
+    return {
+      //========================================================================
+      // Public properties
+      //========================================================================
+
+      /**
+       * The URL for which the favicon is shown.
+       * @type {!Url}
+       */
+      url: Object,
+
+      /**
+       * The element's style attribute.
+       * @type {string}
+       */
+      style: {
+        type: String,
+        reflectToAttribute: true,
+        computed: `computeStyle_(url)`,
+      },
+    };
+  }
+
+  //============================================================================
+  // Helper methods
+  //============================================================================
+
+  /** @private */
+  computeStyle_() {
+    return `background-image:${
+        getFaviconForPageURL(this.url.url, false, '', 24)}`;
+  }
+}
+
+customElements.define(PageFavicon.is, PageFavicon);
diff --git a/chrome/browser/resources/memories/page_thumbnail.html b/chrome/browser/resources/memories/page_thumbnail.html
index 956d456..61102cc 100644
--- a/chrome/browser/resources/memories/page_thumbnail.html
+++ b/chrome/browser/resources/memories/page_thumbnail.html
@@ -1,10 +1,10 @@
-<style include="cr-shared-style">
+<style>
   :host {
     align-items: center;
     display: flex;
-    height: var(--max-height);
+    height: var(--thumbnail-max-height);
     justify-content: center;
-    width: var(--max-width);
+    width: var(--thumbnail-max-width);
   }
 
   a {
@@ -17,12 +17,12 @@
   }
 
   img {
-    border-radius: var(--border-radius, 0);
-    max-height: var(--max-height);
-    max-width: var(--max-width);
+    border-radius: var(--thumbnail-border-radius, 0);
+    max-height: var(--thumbnail-max-height);
+    max-width: var(--thumbnail-max-width);
   }
 </style>
 <a href$="[[page.url.url]]">
   <img title="[[decodeMojoString16_(page.title)]]"
-      src$="[[thumbnailSrc_(page.thumbnailUrl)]]">
+      src$="[[getThumbnailSrc_(page.thumbnailUrl)]]">
 </a>
diff --git a/chrome/browser/resources/memories/page_thumbnail.js b/chrome/browser/resources/memories/page_thumbnail.js
index 4ff87de5..780d6f4 100644
--- a/chrome/browser/resources/memories/page_thumbnail.js
+++ b/chrome/browser/resources/memories/page_thumbnail.js
@@ -2,8 +2,6 @@
 // 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/shared_style_css.m.js';
-
 import {WebPage} from '/components/memories/core/memories.mojom-webui.js';
 import {String16} from 'chrome://resources/mojo/mojo/public/mojom/base/string16.mojom-webui.js';
 import {Url} from 'chrome://resources/mojo/url/mojom/url.mojom-webui.js';
@@ -48,7 +46,7 @@
    * @return {string}
    * @private
    */
-  thumbnailSrc_(thumbnailUrl) {
+  getThumbnailSrc_(thumbnailUrl) {
     return thumbnailUrl ? `chrome://image?${thumbnailUrl.url}` : '';
   }
 
diff --git a/chrome/browser/resources/memories/shared_vars.html b/chrome/browser/resources/memories/shared_vars.html
new file mode 100644
index 0000000..41e1cf8f
--- /dev/null
+++ b/chrome/browser/resources/memories/shared_vars.html
@@ -0,0 +1,9 @@
+<custom-style>
+<style>
+  html {
+    --memory-card-width: 742px;
+    --visit-row-favicon-size: var(--visit-row-height);
+    --visit-row-height: 36px;
+  }
+</style>
+</custom-style>
diff --git a/chrome/browser/resources/memories/shared_vars.js b/chrome/browser/resources/memories/shared_vars.js
new file mode 100644
index 0000000..098a438
--- /dev/null
+++ b/chrome/browser/resources/memories/shared_vars.js
@@ -0,0 +1,10 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'chrome://resources/cr_elements/shared_vars_css.m.js';
+import 'chrome://resources/polymer/v3_0/paper-styles/color.js';
+
+const template = document.createElement('template');
+template.innerHTML = `{__html_template__}`;
+document.head.appendChild(template.content.cloneNode(true));
diff --git a/chrome/browser/resources/memories/top_visit.html b/chrome/browser/resources/memories/top_visit.html
new file mode 100644
index 0000000..8c4c3c8
--- /dev/null
+++ b/chrome/browser/resources/memories/top_visit.html
@@ -0,0 +1,55 @@
+<style>
+  :host {
+    display: block;
+    padding: 10px 0;
+    position: relative;
+  }
+
+  .vertical-line {
+    --vertical-line-width: 2px;
+    border-inline-start: var(--vertical-line-width) solid var(--google-grey-refresh-100);
+    height: 100%;
+    left: calc((var(--visit-row-favicon-size) - var(--vertical-line-width)) / 2);
+    position: absolute;
+  }
+
+  :host-context([dir='rtl']) .vertical-line {
+    left: unset;
+    right: calc((var(--visit-row-favicon-size) - var(--vertical-line-width)) / 2);
+  }
+
+  :host(:last-of-type) .vertical-line {
+    display: none;
+  }
+
+  cr-expand-button {
+    --cr-active-background-color: transparent;
+    --cr-hover-background-color: transparent;
+    --cr-section-vertical-padding: 0;
+  }
+
+  .related-visits {
+    margin-inline-start: 52px;
+  }
+
+  .related-visits visit-row {
+    padding: 4px 0;
+  }
+</style>
+<div class="vertical-line"></div>
+<template is="dom-if" if="[[visit.relatedVisits.length]]">
+  <cr-expand-button expanded="{{expanded_}}">
+    <visit-row is-top-visit visit="[[visit]]" on-visit-click="onVisitClick_">
+    </visit-row>
+  </cr-expand-button>
+  <iron-collapse opened="[[expanded_]]">
+    <div class="related-visits">
+      <template is="dom-repeat" items="[[visit.relatedVisits]]">
+        <visit-row visit="[[item]]"></visit-row>
+      </template>
+    </div>
+  </iron-collapse>
+</template>
+<template is="dom-if" if="[[!visit.relatedVisits.length]]">
+  <visit-row is-top-visit visit="[[visit]]"></visit-row>
+</template>
diff --git a/chrome/browser/resources/memories/top_visit.js b/chrome/browser/resources/memories/top_visit.js
new file mode 100644
index 0000000..16c3d05
--- /dev/null
+++ b/chrome/browser/resources/memories/top_visit.js
@@ -0,0 +1,66 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import './shared_vars.js';
+import './visit_row.js';
+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 {Visit} from '/components/memories/core/memories.mojom-webui.js';
+import {html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+
+/**
+ * @fileoverview This file provides a custom element displaying a top visit
+ * within a Memory. A top visit is a featured, i.e., visible, visit with an
+ * optional set of related visits which are not visible by default.
+ */
+
+class TopVisitElement extends PolymerElement {
+  static get is() {
+    return 'top-visit';
+  }
+
+  static get template() {
+    return html`{__html_template__}`;
+  }
+
+  static get properties() {
+    return {
+      //========================================================================
+      // Public properties
+      //========================================================================
+
+      /**
+       * The top visit to display
+       * @type {!Visit}
+       */
+      visit: Object,
+
+      //========================================================================
+      // Private properties
+      //========================================================================
+
+      /**
+       * Whether the related visits of the top visit are expanded/visible.
+       * @private {boolean}
+       */
+      expanded_: Boolean,
+    };
+  }
+
+  //============================================================================
+  // Event handlers
+  //============================================================================
+
+  /**
+   * @param {!CustomEvent<{event:!MouseEvent}>} e
+   * @private
+   */
+  onVisitClick_(e) {
+    // Prevent the enclosing <cr-expand-button> from receiving this event.
+    e.detail.event.stopImmediatePropagation();
+  }
+}
+
+customElements.define(TopVisitElement.is, TopVisitElement);
diff --git a/chrome/browser/resources/memories/visit_row.html b/chrome/browser/resources/memories/visit_row.html
new file mode 100644
index 0000000..d4d0b96
--- /dev/null
+++ b/chrome/browser/resources/memories/visit_row.html
@@ -0,0 +1,69 @@
+<style include="cr-shared-style">
+  :host {
+    display: block;
+    line-height: var(--visit-row-height);
+    position: relative;
+  }
+
+  .header {
+    display: flex;
+    justify-content: space-between;
+  }
+
+  .start-justtified {
+    color: inherit;
+    display: flex;
+    overflow: hidden;
+    text-decoration: none;
+    z-index: 1;
+  }
+
+  .end-justified {
+    display: flex;
+    flex-shrink: 0;
+  }
+
+  page-favicon {
+    flex-shrink: 0;
+    height: var(--visit-row-favicon-size);
+    margin-inline-end: 16px;
+    width: var(--visit-row-favicon-size);
+  }
+
+  :host([is-top-visit]) page-favicon {
+    background-color: var(--google-grey-refresh-100);
+    border-radius: 8px;
+  }
+
+  .title {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+
+  .hostname,
+  .separator,
+  .timestamp {
+    flex-shrink: 0;
+    margin-inline-start: 8px;
+  }
+
+  .hostname,
+  .timestamp {
+    color: var(--cr-secondary-text-color);
+  }
+</style>
+<div class="header">
+  <a class="start-justtified" href="[[visit.url.url]]" on-click="onClick_">
+    <page-favicon url="[[visit.url]]"></page-favicon>
+    <span class="title">[[decodeMojoString16_(visit.pageTitle)]]</span>
+    <span class="separator">–</span>
+    <span class="hostname">[[getHostnameFromUrl_(visit.url)]]</span>
+  </a>
+  <div class="end-justified">
+    <div class="timestamp">[[getTimeOfVisit_(visit)]]</div>
+    <cr-icon-button id="action" class="icon-more-vert"
+        hidden="[[visit.relatedVisits.length]]">
+    </cr-icon-button>
+  </div>
+</div>
diff --git a/chrome/browser/resources/memories/visit_row.js b/chrome/browser/resources/memories/visit_row.js
new file mode 100644
index 0000000..87cd375
--- /dev/null
+++ b/chrome/browser/resources/memories/visit_row.js
@@ -0,0 +1,111 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import './page_favicon.js';
+import './shared_vars.js';
+import 'chrome://resources/cr_elements/shared_style_css.m.js';
+
+import {Visit} from '/components/memories/core/memories.mojom-webui.js';
+import {String16} from 'chrome://resources/mojo/mojo/public/mojom/base/string16.mojom-webui.js';
+import {Url} from 'chrome://resources/mojo/url/mojom/url.mojom-webui.js';
+import {html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+
+import {decodeMojoString16} from './utils.js';
+
+/**
+ * @fileoverview This file provides a custom element displaying a visit to a
+ * page within a Memory. A visit features the page favicon, title, a timestamp,
+ * as well as an action menu.
+ */
+
+class VisitRowElement extends PolymerElement {
+  static get is() {
+    return 'visit-row';
+  }
+
+  static get template() {
+    return html`{__html_template__}`;
+  }
+
+  static get properties() {
+    return {
+      //========================================================================
+      // Public properties
+      //========================================================================
+
+      /**
+       * The visit to display.
+       * @type {!Visit}
+       */
+      visit: Object,
+
+      /**
+       * Whether the visit is a top visit.
+       * @type {boolean}
+       */
+      isTopVisit: {
+        type: Boolean,
+        reflectToAttribute: true,
+      },
+    };
+  }
+
+  //============================================================================
+  // Event handlers
+  //============================================================================
+
+  /**
+   * @param {!MouseEvent} event
+   * @private
+   */
+  onClick_(event) {
+    // Only handle main (usually the left) and auxiliary (usually the wheel or
+    // the middle) button presses.
+    if (event.button > 1) {
+      return;
+    }
+
+    this.dispatchEvent(new CustomEvent('visit-click', {
+      bubbles: true,
+      composed: true,
+      detail: {event},
+    }));
+  }
+
+  //============================================================================
+  // Helper methods
+  //============================================================================
+
+  /**
+   * Converts a Mojo String16 to a JS string.
+   * @param {String16} str
+   * @return {string}
+   * @private
+   */
+  decodeMojoString16_(str) {
+    return decodeMojoString16(str);
+  }
+
+  /**
+   * @param {!Url} url
+   * @return {string} The domain name of the URL without the leading 'www.'.
+   * @private
+   */
+  getHostnameFromUrl_(url) {
+    return new URL(url.url).hostname.replace(/^(www\.)/, '');
+  }
+
+  /**
+   * @param {!Visit} visit
+   * @return {string} Time of day or relative date of visit, e.g., "1 day ago",
+   *     depending on if the visit is a top visit.
+   * @private
+   */
+  getTimeOfVisit_(visit) {
+    return decodeMojoString16(
+        this.isTopVisit ? visit.relativeDate : visit.timeOfDay);
+  }
+}
+
+customElements.define(VisitRowElement.is, VisitRowElement);
diff --git a/chrome/browser/resources/new_tab_page/BUILD.gn b/chrome/browser/resources/new_tab_page/BUILD.gn
index c731aab4..637e406 100644
--- a/chrome/browser/resources/new_tab_page/BUILD.gn
+++ b/chrome/browser/resources/new_tab_page/BUILD.gn
@@ -80,6 +80,7 @@
     ":background_manager",
     ":browser_proxy",
     ":customize_dialog_types",
+    ":metrics_utils",
     ":one_google_bar_api",
     ":promo_browser_command_proxy",
     "modules:module_wrapper",
@@ -188,6 +189,11 @@
 js_library("utils") {
 }
 
+js_library("metrics_utils") {
+  deps = [ "//ui/webui/resources/js:load_time_data.m" ]
+  externs_list = [ "//third_party/closure_compiler/externs/metrics_private.js" ]
+}
+
 js_library("iframe") {
   deps = [
     "//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
@@ -273,6 +279,7 @@
     "new_tab_page.js",
     "promo_browser_command_proxy.js",
     "utils.js",
+    "metrics_utils.js",
   ]
 }
 
diff --git a/chrome/browser/resources/new_tab_page/app.js b/chrome/browser/resources/new_tab_page/app.js
index 2863e7c..06f7eee 100644
--- a/chrome/browser/resources/new_tab_page/app.js
+++ b/chrome/browser/resources/new_tab_page/app.js
@@ -23,6 +23,7 @@
 import {BackgroundManager} from './background_manager.js';
 import {BrowserProxy} from './browser_proxy.js';
 import {BackgroundSelection, BackgroundSelectionType, CustomizeDialogPage} from './customize_dialog_types.js';
+import {recordLoadDuration} from './metrics_utils.js';
 import {ModuleDescriptor} from './modules/module_descriptor.js';
 import {ModuleRegistry} from './modules/module_registry.js';
 import {oneGoogleBarApi} from './one_google_bar_api.js';
@@ -642,13 +643,16 @@
   onModulesLoadedAndVisibilityDeterminedChange_() {
     if (this.modulesLoadedAndVisibilityDetermined_ &&
         loadTimeData.getBoolean('modulesEnabled')) {
-      this.pageHandler_.onModulesRendered(BrowserProxy.getInstance().now());
+      recordLoadDuration(
+          'NewTabPage.Modules.ShownTime', BrowserProxy.getInstance().now());
       this.moduleDescriptors_.forEach(({id}) => {
         chrome.metricsPrivate.recordBoolean(
             `NewTabPage.Modules.EnabledOnNTPLoad.${id}`,
             !this.disabledModules_.all &&
                 !this.disabledModules_.ids.includes(id));
       });
+      chrome.metricsPrivate.recordBoolean(
+          'NewTabPage.Modules.VisibleOnNTPLoad', !this.disabledModules_.all);
     }
   }
 
diff --git a/chrome/browser/resources/new_tab_page/metrics_utils.js b/chrome/browser/resources/new_tab_page/metrics_utils.js
new file mode 100644
index 0000000..4671db6
--- /dev/null
+++ b/chrome/browser/resources/new_tab_page/metrics_utils.js
@@ -0,0 +1,70 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
+
+/**
+ * Records |durationMs| in the |metricName| histogram.
+ * @param {string} metricName
+ * @param {number} durationMs
+ */
+export function recordDuration(metricName, durationMs) {
+  chrome.metricsPrivate.recordValue(
+      {
+        metricName,
+        type: chrome.metricsPrivate.MetricTypeType.HISTOGRAM_LOG,
+        min: 1,
+        max: 60000,  // 60 seconds.
+        buckets: 100,
+      },
+      Math.floor(durationMs));
+}
+
+/**
+ * Records the duration between navigation start and |msSinceEpoch| in the
+ * |metricName| histogram.
+ * @param {string} metricName
+ * @param {number} msSinceEpoch
+ */
+export function recordLoadDuration(metricName, msSinceEpoch) {
+  recordDuration(
+      metricName,
+      msSinceEpoch - /** @type {number} */
+          (loadTimeData.getValue('navigationStartTime')));
+}
+
+/**
+ * Records |value| (expected to be between 0 and 10) into the ten-bucket
+ * |metricName| histogram.
+ * @param {string} metricName
+ * @param {number} value
+ */
+export function recordPerdecage(metricName, value) {
+  chrome.metricsPrivate.recordValue(
+      {
+        metricName,
+        type: chrome.metricsPrivate.MetricTypeType.HISTOGRAM_LINEAR,
+        min: 1,       // Choose 1 if real min is 0.
+        max: 11,      // Exclusive.
+        buckets: 12,  // Numbers 0-10 and unused overflow bucket of 11.
+      },
+      value);
+}
+
+/**
+ * Records that an event has happened rather than a value in the |metricName|
+ * histogram.
+ * @param {string} metricName
+ */
+export function recordOccurence(metricName) {
+  chrome.metricsPrivate.recordValue(
+      {
+        metricName,
+        type: chrome.metricsPrivate.MetricTypeType.HISTOGRAM_LINEAR,
+        min: 1,
+        max: 1,
+        buckets: 1,
+      },
+      1);
+}
diff --git a/chrome/browser/resources/new_tab_page/modules/BUILD.gn b/chrome/browser/resources/new_tab_page/modules/BUILD.gn
index 907101b..d13f3b22 100644
--- a/chrome/browser/resources/new_tab_page/modules/BUILD.gn
+++ b/chrome/browser/resources/new_tab_page/modules/BUILD.gn
@@ -10,7 +10,10 @@
 import("//ui/webui/webui_features.gni")
 
 js_library("module_descriptor") {
-  deps = [ "..:browser_proxy" ]
+  deps = [
+    "..:browser_proxy",
+    "..:metrics_utils",
+  ]
 }
 
 js_library("modules") {
@@ -28,7 +31,10 @@
 js_library("module_wrapper") {
   deps = [
     ":module_descriptor",
+    "..:browser_proxy",
+    "..:metrics_utils",
     "//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
+    "//ui/webui/resources/js:assert.m",
   ]
   externs_list = [ "//third_party/closure_compiler/externs/metrics_private.js" ]
 }
diff --git a/chrome/browser/resources/new_tab_page/modules/module_descriptor.js b/chrome/browser/resources/new_tab_page/modules/module_descriptor.js
index 879d47f..ba79866 100644
--- a/chrome/browser/resources/new_tab_page/modules/module_descriptor.js
+++ b/chrome/browser/resources/new_tab_page/modules/module_descriptor.js
@@ -3,7 +3,7 @@
 // found in the LICENSE file.
 
 import {BrowserProxy} from '../browser_proxy.js';
-import {mojoTimeDelta} from '../utils.js';
+import {recordDuration, recordLoadDuration} from '../metrics_utils.js';
 
 /**
  * @fileoverview Provides the module descriptor. Each module must create a
@@ -75,7 +75,10 @@
       return;
     }
     const loadEndTime = BrowserProxy.getInstance().now();
-    BrowserProxy.getInstance().handler.onModuleLoaded(
-        this.id_, mojoTimeDelta(loadEndTime - loadStartTime), loadEndTime);
+    const duration = loadEndTime - loadStartTime;
+    recordLoadDuration('NewTabPage.Modules.Loaded', loadEndTime);
+    recordLoadDuration(`NewTabPage.Modules.Loaded.${this.id_}`, loadEndTime);
+    recordDuration('NewTabPage.Modules.LoadDuration', duration);
+    recordDuration(`NewTabPage.Modules.LoadDuration.${this.id_}`, duration);
   }
 }
diff --git a/chrome/browser/resources/new_tab_page/modules/module_wrapper.js b/chrome/browser/resources/new_tab_page/modules/module_wrapper.js
index c03d2c91..350cd20 100644
--- a/chrome/browser/resources/new_tab_page/modules/module_wrapper.js
+++ b/chrome/browser/resources/new_tab_page/modules/module_wrapper.js
@@ -6,6 +6,8 @@
 import {html, microTask, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 
 import {BrowserProxy} from '../browser_proxy.js';
+import {recordLoadDuration, recordOccurence, recordPerdecage} from '../metrics_utils.js';
+
 import {ModuleDescriptor} from './module_descriptor.js';
 
 /** @fileoverview Element that implements the common module UI. */
@@ -42,15 +44,19 @@
     // Log at most one usage per module per NTP page load. This is possible,
     // if a user opens a link in a new tab.
     this.descriptor.element.addEventListener('usage', () => {
-      BrowserProxy.getInstance().handler.onModuleUsage(this.descriptor.id);
+      recordOccurence('NewTabPage.Modules.Usage');
+      recordOccurence(`NewTabPage.Modules.Usage.${this.descriptor.id}`);
     }, {once: true});
 
     // Install observer to log module header impression.
     const headerObserver = new IntersectionObserver(([{intersectionRatio}]) => {
       if (intersectionRatio >= 1.0) {
         headerObserver.disconnect();
-        BrowserProxy.getInstance().handler.onModuleImpression(
-            this.descriptor.id, BrowserProxy.getInstance().now());
+        const time = BrowserProxy.getInstance().now();
+        recordLoadDuration('NewTabPage.Modules.Impression', time);
+        recordLoadDuration(
+            `NewTabPage.Modules.Impression.${this.descriptor.id}`, time);
+        this.dispatchEvent(new Event('detect-impression'));
       }
     }, {threshold: 1.0});
 
@@ -62,17 +68,6 @@
           Math.floor(Math.max(intersectionPerdecage, intersectionRatio * 10));
     }, {threshold: [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]});
     window.addEventListener('unload', () => {
-      const recordPerdecage = (metricName, value) => {
-        chrome.metricsPrivate.recordValue(
-            {
-              metricName,
-              type: chrome.metricsPrivate.MetricTypeType.HISTOGRAM_LINEAR,
-              min: 1,       // Choose 1 if real min is 0.
-              max: 11,      // Exclusive.
-              buckets: 12,  // Numbers 0-10 and unused overflow bucket of 11.
-            },
-            value);
-      };
       recordPerdecage(
           'NewTabPage.Modules.ImpressionRatio', intersectionPerdecage);
       recordPerdecage(
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 98fa96e..05ce5c4 100644
--- a/chrome/browser/resources/new_tab_page/new_tab_page.js
+++ b/chrome/browser/resources/new_tab_page/new_tab_page.js
@@ -16,6 +16,7 @@
 export {BrowserProxy} from './browser_proxy.js';
 export {BackgroundSelectionType, CustomizeDialogPage} from './customize_dialog_types.js';
 export {ImgElement} from './img.js';
+export {recordDuration, recordLoadDuration, recordOccurence, recordPerdecage} from './metrics_utils.js';
 export {ChromeCartProxy} from './modules/cart/chrome_cart_proxy.js';
 export {chromeCartDescriptor} from './modules/cart/module.js';
 export {DriveProxy} from './modules/drive/drive_module_proxy.js';
diff --git a/chrome/browser/resources/settings/chromeos/internet_page/internet_detail_page.js b/chrome/browser/resources/settings/chromeos/internet_page/internet_detail_page.js
index f3696cd..40a430c 100644
--- a/chrome/browser/resources/settings/chromeos/internet_page/internet_detail_page.js
+++ b/chrome/browser/resources/settings/chromeos/internet_page/internet_detail_page.js
@@ -793,7 +793,7 @@
     }
 
     if (!properties) {
-      console.error('Details page: GUID no longer exists: ' + this.guid);
+      // Close the page if the network was removed and no longer exists.
       this.close();
       return;
     }
diff --git a/chrome/browser/resources/settings/chromeos/os_people_page/account_manager.html b/chrome/browser/resources/settings/chromeos/os_people_page/account_manager.html
index be0ce355..b226a4fd 100644
--- a/chrome/browser/resources/settings/chromeos/os_people_page/account_manager.html
+++ b/chrome/browser/resources/settings/chromeos/os_people_page/account_manager.html
@@ -46,7 +46,7 @@
 
       .profile-icon.device-account-icon {
         --profile-icon-size: 60px;
-        margin-top: 20px;
+        margin-top: 16px;
       }
 
       .device-account-container {
@@ -57,7 +57,8 @@
 
       .device-account-container .primary {
         font-weight: 500;
-        margin-top: 15px;
+        margin-bottom: 4px;
+        margin-top: 16px;
       }
 
       .account-list-item {
@@ -112,7 +113,7 @@
 
       #account-list-header > h2 {
         padding-bottom: 8px;
-        padding-top: 8px;
+        padding-top: 16px;
       }
 
       #account-list-header {
@@ -187,6 +188,7 @@
       .managed-message {
         color: var(--cr-secondary-text-color);
         justify-content: center;
+        margin-top: 16px;
       }
 
       .managed-message > iron-icon {
diff --git a/chrome/browser/speech/extension_api/tts_extension_api.cc b/chrome/browser/speech/extension_api/tts_extension_api.cc
index 43c4b9b..711fe15ac 100644
--- a/chrome/browser/speech/extension_api/tts_extension_api.cc
+++ b/chrome/browser/speech/extension_api/tts_extension_api.cc
@@ -309,11 +309,14 @@
     extensions::ExtensionHost* host =
         extensions::ProcessManager::Get(browser_context())
             ->GetBackgroundHostForExtension(extension()->id());
-    utterance = content::TtsUtterance::Create(host->host_contents());
-  } else {
-    utterance = content::TtsUtterance::Create(browser_context());
+
+    if (host && host->host_contents())
+      utterance = content::TtsUtterance::Create(host->host_contents());
   }
 
+  if (!utterance)
+    utterance = content::TtsUtterance::Create(browser_context());
+
   utterance->SetText(text);
   utterance->SetVoiceName(voice_name);
   utterance->SetSrcId(src_id);
diff --git a/chrome/browser/sync/test/integration/single_client_sessions_sync_test.cc b/chrome/browser/sync/test/integration/single_client_sessions_sync_test.cc
index 5157f44..53383aa 100644
--- a/chrome/browser/sync/test/integration/single_client_sessions_sync_test.cc
+++ b/chrome/browser/sync/test/integration/single_client_sessions_sync_test.cc
@@ -1001,8 +1001,14 @@
   base::test::ScopedFeatureList features_;
 };
 
+// TODO(1185289): Fails under msan.
+#if defined(MEMORY_SANITIZER)
+#define MAYBE_ShouldNotDeleteLastClosedTab DISABLED_ShouldNotDeleteLastClosedTab
+#else
+#define MAYBE_ShouldNotDeleteLastClosedTab ShouldNotDeleteLastClosedTab
+#endif
 IN_PROC_BROWSER_TEST_F(SingleClientSessionsWithDestroyProfileSyncTest,
-                       ShouldNotDeleteLastClosedTab) {
+                       MAYBE_ShouldNotDeleteLastClosedTab) {
   ASSERT_TRUE(SetupSync()) << "SetupSync() failed.";
   ASSERT_TRUE(CheckInitialState(0));
 
diff --git a/chrome/browser/sync/test/integration/two_client_sessions_sync_test.cc b/chrome/browser/sync/test/integration/two_client_sessions_sync_test.cc
index baeb699..e4534cd 100644
--- a/chrome/browser/sync/test/integration/two_client_sessions_sync_test.cc
+++ b/chrome/browser/sync/test/integration/two_client_sessions_sync_test.cc
@@ -269,8 +269,14 @@
   base::test::ScopedFeatureList features_;
 };
 
+// TODO(1185289): Fails under msan.
+#if defined(MEMORY_SANITIZER)
+#define MAYBE_ShouldNotSyncLastClosedTab DISABLED_ShouldNotSyncLastClosedTab
+#else
+#define MAYBE_ShouldNotSyncLastClosedTab ShouldNotSyncLastClosedTab
+#endif
 IN_PROC_BROWSER_TEST_F(TwoClientSessionsWithDestroyProfileSyncTest,
-                       ShouldNotSyncLastClosedTab) {
+                       MAYBE_ShouldNotSyncLastClosedTab) {
   ASSERT_TRUE(SetupSync()) << "SetupSync() failed.";
 
   ASSERT_TRUE(CheckInitialState(0));
diff --git a/chrome/browser/task_manager/sampling/task_manager_impl.cc b/chrome/browser/task_manager/sampling/task_manager_impl.cc
index 8a13e64..c6733e68 100644
--- a/chrome/browser/task_manager/sampling/task_manager_impl.cc
+++ b/chrome/browser/task_manager/sampling/task_manager_impl.cc
@@ -102,6 +102,11 @@
   return lazy_task_manager_instance.Pointer();
 }
 
+bool TaskManagerImpl::IsCreated() {
+  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+  return lazy_task_manager_instance.IsCreated();
+}
+
 void TaskManagerImpl::ActivateTask(TaskId task_id) {
   GetTaskByTaskId(task_id)->Activate();
 }
@@ -447,24 +452,6 @@
   return task->task_id();
 }
 
-void TaskManagerImpl::UpdateAccumulatedStatsNetworkForRoute(
-    int process_id,
-    int route_id,
-    int64_t recv_bytes,
-    int64_t sent_bytes) {
-  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
-  Task* task = GetTaskByRoute(process_id, route_id);
-  if (!task) {
-    // Orphaned/unaccounted activity is credited to the Browser process.
-    task = GetTaskByRoute(content::ChildProcessHost::kInvalidUniqueID,
-                          MSG_ROUTING_NONE);
-  }
-  if (!task)
-    return;
-  task->OnNetworkBytesRead(recv_bytes);
-  task->OnNetworkBytesSent(sent_bytes);
-}
-
 void TaskManagerImpl::TaskAdded(Task* task) {
   DCHECK(task);
 
@@ -529,6 +516,26 @@
   NotifyObserversOnTaskUnresponsive(task->task_id());
 }
 
+void TaskManagerImpl::UpdateAccumulatedStatsNetworkForRoute(
+    int process_id,
+    int route_id,
+    int64_t recv_bytes,
+    int64_t sent_bytes) {
+  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+  if (!is_running_)
+    return;
+  Task* task = GetTaskByRoute(process_id, route_id);
+  if (!task) {
+    // Orphaned/unaccounted activity is credited to the Browser process.
+    task = GetTaskByRoute(content::ChildProcessHost::kInvalidUniqueID,
+                          MSG_ROUTING_NONE);
+  }
+  if (!task)
+    return;
+  task->OnNetworkBytesRead(recv_bytes);
+  task->OnNetworkBytesSent(sent_bytes);
+}
+
 void TaskManagerImpl::OnVideoMemoryUsageStatsUpdate(
     const gpu::VideoMemoryUsageStats& gpu_memory_stats) {
   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
diff --git a/chrome/browser/task_manager/sampling/task_manager_impl.h b/chrome/browser/task_manager/sampling/task_manager_impl.h
index 7ed26c3e..38f5db4 100644
--- a/chrome/browser/task_manager/sampling/task_manager_impl.h
+++ b/chrome/browser/task_manager/sampling/task_manager_impl.h
@@ -42,6 +42,7 @@
   ~TaskManagerImpl() override;
 
   static TaskManagerImpl* GetInstance();
+  static bool IsCreated();
 
   // task_manager::TaskManagerInterface:
   void ActivateTask(TaskId task_id) override;
@@ -93,16 +94,17 @@
   bool IsRunningInVM(TaskId task_id) const override;
   TaskId GetTaskIdForWebContents(
       content::WebContents* web_contents) const override;
-  void UpdateAccumulatedStatsNetworkForRoute(int process_id,
-                                             int route_id,
-                                             int64_t recv_bytes,
-                                             int64_t sent_bytes) override;
 
   // task_manager::TaskProviderObserver:
   void TaskAdded(Task* task) override;
   void TaskRemoved(Task* task) override;
   void TaskUnresponsive(Task* task) override;
 
+  void UpdateAccumulatedStatsNetworkForRoute(int process_id,
+                                             int route_id,
+                                             int64_t recv_bytes,
+                                             int64_t sent_bytes);
+
  private:
   using PidToTaskGroupMap =
       std::map<base::ProcessId, std::unique_ptr<TaskGroup>>;
diff --git a/chrome/browser/task_manager/task_manager_interface.cc b/chrome/browser/task_manager/task_manager_interface.cc
index b22f3f9..044443c 100644
--- a/chrome/browser/task_manager/task_manager_interface.cc
+++ b/chrome/browser/task_manager/task_manager_interface.cc
@@ -40,6 +40,18 @@
   return TaskManagerImpl::GetInstance();
 }
 
+void TaskManagerInterface::UpdateAccumulatedStatsNetworkForRoute(
+    int process_id,
+    int route_id,
+    int64_t recv_bytes,
+    int64_t sent_bytes) {
+  // Don't create a task manager if it hasn't already been created.
+  if (TaskManagerImpl::IsCreated()) {
+    TaskManagerImpl::GetInstance()->UpdateAccumulatedStatsNetworkForRoute(
+        process_id, route_id, recv_bytes, sent_bytes);
+  }
+}
+
 void TaskManagerInterface::AddObserver(TaskManagerObserver* observer) {
   observers_.AddObserver(observer);
   observer->observed_task_manager_ = this;
diff --git a/chrome/browser/task_manager/task_manager_interface.h b/chrome/browser/task_manager/task_manager_interface.h
index b65e00a..8859bd9 100644
--- a/chrome/browser/task_manager/task_manager_interface.h
+++ b/chrome/browser/task_manager/task_manager_interface.h
@@ -45,6 +45,14 @@
   // create it first. Must be called on the UI thread.
   static TaskManagerInterface* GetTaskManager();
 
+  // Update the accumulated network stats with additional data sent/received
+  // for a route described by |process_id| and |route_id|. If the associated
+  // task cannot be found it will be attributed to the browser process task.
+  static void UpdateAccumulatedStatsNetworkForRoute(int process_id,
+                                                    int route_id,
+                                                    int64_t recv_bytes,
+                                                    int64_t sent_bytes);
+
   void AddObserver(TaskManagerObserver* observer);
   void RemoveObserver(TaskManagerObserver* observer);
 
@@ -224,14 +232,6 @@
   virtual TaskId GetTaskIdForWebContents(
       content::WebContents* web_contents) const = 0;
 
-  // Update the accumulated network stats with additional data sent/received
-  // for a route described by |process_id| and |route_id|. If the associated
-  // task cannot be found it will be attributed to the browser process task.
-  virtual void UpdateAccumulatedStatsNetworkForRoute(int process_id,
-                                                     int route_id,
-                                                     int64_t recv_bytes,
-                                                     int64_t sent_bytes) = 0;
-
   // Returns true if the resource |type| usage calculation is enabled and
   // the implementation should refresh its value (this means that at least one
   // of the observers require this value). False otherwise.
diff --git a/chrome/browser/task_manager/test_task_manager.cc b/chrome/browser/task_manager/test_task_manager.cc
index 3523dd1..76860a93 100644
--- a/chrome/browser/task_manager/test_task_manager.cc
+++ b/chrome/browser/task_manager/test_task_manager.cc
@@ -182,12 +182,6 @@
   return -1;
 }
 
-void TestTaskManager::UpdateAccumulatedStatsNetworkForRoute(
-    int process_id,
-    int route_id,
-    int64_t recv_bytes,
-    int64_t sent_bytes) {}
-
 base::TimeDelta TestTaskManager::GetRefreshTime() {
   return GetCurrentRefreshTime();
 }
diff --git a/chrome/browser/task_manager/test_task_manager.h b/chrome/browser/task_manager/test_task_manager.h
index 5aa17ab1..87ca264 100644
--- a/chrome/browser/task_manager/test_task_manager.h
+++ b/chrome/browser/task_manager/test_task_manager.h
@@ -72,10 +72,6 @@
   bool IsRunningInVM(TaskId task_id) const override;
   TaskId GetTaskIdForWebContents(
       content::WebContents* web_contents) const override;
-  void UpdateAccumulatedStatsNetworkForRoute(int process_id,
-                                             int route_id,
-                                             int64_t recv_bytes,
-                                             int64_t sent_bytes) override;
 
   base::TimeDelta GetRefreshTime();
   int64_t GetEnabledFlags();
diff --git a/chrome/browser/ui/android/appmenu/internal/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenu.java b/chrome/browser/ui/android/appmenu/internal/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenu.java
index 799252a..3cf99f06 100644
--- a/chrome/browser/ui/android/appmenu/internal/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenu.java
+++ b/chrome/browser/ui/android/appmenu/internal/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenu.java
@@ -15,8 +15,6 @@
 import android.os.Build;
 import android.os.SystemClock;
 import android.text.TextUtils;
-import android.text.format.DateUtils;
-import android.util.Pair;
 import android.view.Gravity;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
@@ -46,10 +44,8 @@
 import org.chromium.components.browser_ui.widget.highlight.ViewHighlighter;
 import org.chromium.ui.widget.Toast;
 
-import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Queue;
 
 /**
  * Shows a popup of menuitems anchored to a host view. When a item is selected we call
@@ -59,8 +55,6 @@
  */
 class AppMenu implements OnItemClickListener, OnKeyListener, AppMenuClickHandler {
     private static final float LAST_ITEM_SHOW_FRACTION = 0.5f;
-    @VisibleForTesting
-    static final long RECENT_SELECTED_MENUITEM_EXPIRATION_MS = 10 * DateUtils.SECOND_IN_MILLIS;
 
     private final Menu mMenu;
     private final int mItemRowHeight;
@@ -73,8 +67,7 @@
     private PopupWindow mPopup;
     private ListView mListView;
     private AppMenuAdapter mAdapter;
-    @VisibleForTesting
-    AppMenuHandlerImpl mHandler;
+    private AppMenuHandlerImpl mHandler;
     private View mFooterView;
     private int mCurrentScreenRotation = -1;
     private boolean mIsByPermanentButton;
@@ -82,9 +75,6 @@
     private long mMenuShownTimeMs;
     private boolean mSelectedItemBeforeDismiss;
 
-    // Selected menu item id and the timestamp.
-    private final Queue<Pair<Integer, Long>> mRecentSelectedMenuItems = new ArrayDeque<>();
-
     /**
      * Creates and sets up the App Menu.
      * @param menu Original menu created by the framework.
@@ -364,7 +354,6 @@
     @Override
     public void onItemClick(MenuItem menuItem) {
         if (menuItem.isEnabled()) {
-            recordSelectedMenuItem(menuItem.getItemId(), SystemClock.elapsedRealtime());
             mSelectedItemBeforeDismiss = true;
             dismiss();
             mHandler.onOptionsItemSelected(menuItem);
@@ -606,27 +595,4 @@
         }
         return mItemRowHeight;
     }
-
-    @VisibleForTesting
-    void recordSelectedMenuItem(int menuItemId, long timestamp) {
-        // Remove the selected MenuItems older than RECENT_SELECTED_MENUITEM_EXPIRATION_MS.
-        while (!mRecentSelectedMenuItems.isEmpty()
-                && (timestamp - mRecentSelectedMenuItems.peek().second
-                        > RECENT_SELECTED_MENUITEM_EXPIRATION_MS)) {
-            mRecentSelectedMenuItems.remove();
-        }
-        recordSelectionSequence(menuItemId);
-
-        mRecentSelectedMenuItems.add(new Pair<Integer, Long>(menuItemId, timestamp));
-    }
-
-    private void recordSelectionSequence(int menuItemId) {
-        for (Pair<Integer, Long> previousSelectedMenuItem : mRecentSelectedMenuItems) {
-            if (mHandler.recordAppMenuSimilarSelectionIfNeeded(
-                        previousSelectedMenuItem.first, menuItemId)) {
-                // Only record the similar selection once for one user action.
-                return;
-            }
-        }
-    }
 }
diff --git a/chrome/browser/ui/android/appmenu/internal/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenuHandlerImpl.java b/chrome/browser/ui/android/appmenu/internal/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenuHandlerImpl.java
index 64c95890..e804da7 100644
--- a/chrome/browser/ui/android/appmenu/internal/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenuHandlerImpl.java
+++ b/chrome/browser/ui/android/appmenu/internal/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenuHandlerImpl.java
@@ -349,16 +349,4 @@
     AppMenuPropertiesDelegate getDelegateForTests() {
         return mDelegate;
     }
-
-    /**
-     * Record the user selections if users make selected similar MenuItems.
-     *
-     * @param previousMenuItemId The previous selected MenuItem Id.
-     * @param currentMenuItemId The current selected MenuItem Id.
-     * @return Whether the selections is recorded.
-     */
-    boolean recordAppMenuSimilarSelectionIfNeeded(int previousMenuItemId, int currentMenuItemId) {
-        return mDelegate.recordAppMenuSimilarSelectionIfNeeded(
-                previousMenuItemId, currentMenuItemId);
-    }
 }
diff --git a/chrome/browser/ui/android/appmenu/internal/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenuTest.java b/chrome/browser/ui/android/appmenu/internal/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenuTest.java
index aeaa0518..79cd08b 100644
--- a/chrome/browser/ui/android/appmenu/internal/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenuTest.java
+++ b/chrome/browser/ui/android/appmenu/internal/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenuTest.java
@@ -4,8 +4,6 @@
 
 package org.chromium.chrome.browser.ui.appmenu;
 
-import static org.mockito.ArgumentMatchers.anyInt;
-
 import android.graphics.Rect;
 import android.view.KeyEvent;
 import android.view.Menu;
@@ -32,6 +30,7 @@
 import org.chromium.base.test.util.CommandLineFlags;
 import org.chromium.base.test.util.CriteriaHelper;
 import org.chromium.base.test.util.DisableIf;
+import org.chromium.base.test.util.DisabledTest;
 import org.chromium.chrome.browser.flags.ChromeSwitches;
 import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
 import org.chromium.chrome.browser.lifecycle.LifecycleObserver;
@@ -604,7 +603,8 @@
     @Test
     @MediumTest
     @DisableIf.Device(type = {UiDisableIf.TABLET})
-    public void testDragHelper_ClickItem_Disabled() throws Exception {
+    @DisabledTest(message = "crbug.com/1186468")
+    public void testDragHelper_ClickItem() throws Exception {
         AppMenuButtonHelperImpl buttonHelper =
                 (AppMenuButtonHelperImpl) mAppMenuHandler.createAppMenuButtonHelper();
 
@@ -767,30 +767,6 @@
         Assert.assertEquals(15, height);
     }
 
-    @Test
-    @SmallTest
-    public void testRecordSelectedMenuItem() throws TimeoutException {
-        showMenuAndAssert();
-        AppMenu appMenu = mAppMenuHandler.getAppMenu();
-        AppMenuHandlerImpl spiedHandler = Mockito.spy(mAppMenuHandler);
-        appMenu.mHandler = spiedHandler;
-
-        appMenu.recordSelectedMenuItem(R.id.menu_item_one, 1);
-        Mockito.verify(spiedHandler, Mockito.times(0))
-                .recordAppMenuSimilarSelectionIfNeeded(anyInt(), anyInt());
-
-        appMenu.recordSelectedMenuItem(R.id.menu_item_two, 2);
-        Mockito.verify(spiedHandler, Mockito.times(1))
-                .recordAppMenuSimilarSelectionIfNeeded(R.id.menu_item_one, R.id.menu_item_two);
-
-        appMenu.recordSelectedMenuItem(
-                R.id.menu_item_three, AppMenu.RECENT_SELECTED_MENUITEM_EXPIRATION_MS + 3);
-        Mockito.verify(spiedHandler, Mockito.times(0))
-                .recordAppMenuSimilarSelectionIfNeeded(R.id.menu_item_one, R.id.menu_item_three);
-        Mockito.verify(spiedHandler, Mockito.times(0))
-                .recordAppMenuSimilarSelectionIfNeeded(R.id.menu_item_two, R.id.menu_item_three);
-    }
-
     private void createMenuItem(
             List<MenuItem> menuItems, List<Integer> heightList, int id, int height) {
         Menu menu = mAppMenuHandler.getAppMenu().getMenu();
diff --git a/chrome/browser/ui/android/appmenu/internal/java/src/org/chromium/chrome/browser/ui/appmenu/TestAppMenuPropertiesDelegate.java b/chrome/browser/ui/android/appmenu/internal/java/src/org/chromium/chrome/browser/ui/appmenu/TestAppMenuPropertiesDelegate.java
index 417b65df9..0300bdf 100644
--- a/chrome/browser/ui/android/appmenu/internal/java/src/org/chromium/chrome/browser/ui/appmenu/TestAppMenuPropertiesDelegate.java
+++ b/chrome/browser/ui/android/appmenu/internal/java/src/org/chromium/chrome/browser/ui/appmenu/TestAppMenuPropertiesDelegate.java
@@ -27,7 +27,6 @@
     public int groupDividerId;
     public boolean enableAppIconRow;
     public boolean iconBeforeItem;
-    public boolean recordAppMenuSimilarSelection;
 
     @Override
     public void destroy() {}
@@ -115,10 +114,4 @@
     public boolean shouldShowIconBeforeItem() {
         return iconBeforeItem;
     }
-
-    @Override
-    public boolean recordAppMenuSimilarSelectionIfNeeded(
-            int previousMenuItemId, int currentMenuItemId) {
-        return recordAppMenuSimilarSelection;
-    }
 }
diff --git a/chrome/browser/ui/android/appmenu/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenuPropertiesDelegate.java b/chrome/browser/ui/android/appmenu/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenuPropertiesDelegate.java
index 8a92f3c..cd24d38 100644
--- a/chrome/browser/ui/android/appmenu/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenuPropertiesDelegate.java
+++ b/chrome/browser/ui/android/appmenu/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenuPropertiesDelegate.java
@@ -115,13 +115,4 @@
      *         should show the icon before the text.
      */
     boolean shouldShowIconBeforeItem();
-
-    /**
-     * Record the user selections if users make selected similar MenuItems.
-     *
-     * @param previousMenuItemId The previous selected MenuItem Id.
-     * @param currentMenuItemId The current selected MenuItem Id.
-     * @return Whether the pattern is recorded.
-     */
-    boolean recordAppMenuSimilarSelectionIfNeeded(int previousMenuItemId, int currentMenuItemId);
 }
diff --git a/chrome/browser/ui/ash/arc_custom_tab_modal_dialog_host.cc b/chrome/browser/ui/ash/arc_custom_tab_modal_dialog_host.cc
index 32f5b1a..2c44518 100644
--- a/chrome/browser/ui/ash/arc_custom_tab_modal_dialog_host.cc
+++ b/chrome/browser/ui/ash/arc_custom_tab_modal_dialog_host.cc
@@ -29,6 +29,9 @@
 ArcCustomTabModalDialogHost::~ArcCustomTabModalDialogHost() {
   for (auto& observer : observer_list_)
     observer.OnHostDestroying();
+
+  // |web_contents_| is deleted by the base class, so there's no need to call
+  // WebContentsModalDialogManager::SetDelegate(nullptr)
 }
 
 void ArcCustomTabModalDialogHost::MainFrameWasResized(bool width_changed) {
diff --git a/chrome/browser/ui/ash/arc_custom_tab_modal_dialog_host.h b/chrome/browser/ui/ash/arc_custom_tab_modal_dialog_host.h
index 4057248..d88cf78 100644
--- a/chrome/browser/ui/ash/arc_custom_tab_modal_dialog_host.h
+++ b/chrome/browser/ui/ash/arc_custom_tab_modal_dialog_host.h
@@ -33,7 +33,6 @@
 
 // Implements a WebContentsModalDialogHost for an ARC Custom Tab. This allows a
 // web contents modal dialog to be drawn in the ARC Custom Tab.
-// The WebContents hosted by this object must outlive it.
 class ArcCustomTabModalDialogHost
     : public content::WebContentsObserver,
       public web_modal::WebContentsModalDialogHost,
diff --git a/chrome/browser/ui/aura/accessibility/automation_manager_aura.h b/chrome/browser/ui/aura/accessibility/automation_manager_aura.h
index 73e2c81..48647d9 100644
--- a/chrome/browser/ui/aura/accessibility/automation_manager_aura.h
+++ b/chrome/browser/ui/aura/accessibility/automation_manager_aura.h
@@ -85,6 +85,10 @@
     cache_ = std::move(cache);
   }
 
+  // TODO(https://crbug.com/1185764): Figure out an appropriate design for
+  // Lacros a11y that does not require exposing the root tree id.
+  ui::AXTreeID get_root_tree_id_deprecated() { return tree_->tree_id(); }
+
  private:
   friend class base::NoDestructor<AutomationManagerAura>;
 
diff --git a/chrome/browser/ui/browser.cc b/chrome/browser/ui/browser.cc
index 19ca9ee..354072b 100644
--- a/chrome/browser/ui/browser.cc
+++ b/chrome/browser/ui/browser.cc
@@ -2354,8 +2354,8 @@
     // RenderWidgetHostView.
     RenderWidgetHostView* old_view = old_contents->GetMainFrame()->GetView();
     RenderWidgetHostView* new_view = new_contents->GetMainFrame()->GetView();
-    if (old_view && new_view && old_view->GetBackgroundColor())
-      new_view->SetBackgroundColor(*old_view->GetBackgroundColor());
+    if (old_view && new_view)
+      new_view->CopyBackgroundColorIfPresentFrom(*old_view);
   }
 #endif
 
diff --git a/chrome/browser/ui/search/ntp_user_data_logger.cc b/chrome/browser/ui/search/ntp_user_data_logger.cc
index 10e963f..78b71e4 100644
--- a/chrome/browser/ui/search/ntp_user_data_logger.cc
+++ b/chrome/browser/ui/search/ntp_user_data_logger.cc
@@ -368,7 +368,6 @@
 NTPUserDataLogger::NTPUserDataLogger(Profile* profile, const GURL& ntp_url)
     : has_emitted_(false),
       should_record_doodle_load_time_(true),
-      modules_visible_(false),
       during_startup_(!AfterStartupTaskUtils::IsBrowserStartupComplete()),
       ntp_url_(ntp_url),
       profile_(profile) {}
@@ -522,9 +521,6 @@
     case NTP_CUSTOMIZE_SHORTCUT_VISIBILITY_TOGGLE_CLICKED:
       RecordAction(LoggingEventToShortcutUserActionName(event));
       break;
-    case NTP_MODULES_SHOWN:
-      UMA_HISTOGRAM_LOAD_TIME("NewTabPage.Modules.ShownTime", time);
-      break;
     case NTP_APP_RENDERED:
       UMA_HISTOGRAM_LOAD_TIME("NewTabPage.MainUi.ShownTime", time);
       break;
@@ -569,37 +565,6 @@
   base::RecordAction(base::UserMetricsAction("MostVisited_Clicked"));
 }
 
-void NTPUserDataLogger::LogModuleImpression(const std::string& id,
-                                            base::TimeDelta time) {
-  UMA_HISTOGRAM_LOAD_TIME("NewTabPage.Modules.Impression", time);
-  base::UmaHistogramCustomTimes("NewTabPage.Modules.Impression." + id, time,
-                                base::TimeDelta::FromMilliseconds(1),
-                                base::TimeDelta::FromSeconds(60), 100);
-}
-
-void NTPUserDataLogger::LogModuleLoaded(const std::string& id,
-                                        base::TimeDelta duration,
-                                        base::TimeDelta time_since_navigation) {
-  UMA_HISTOGRAM_LOAD_TIME("NewTabPage.Modules.Loaded", time_since_navigation);
-  base::UmaHistogramCustomTimes("NewTabPage.Modules.Loaded." + id,
-                                time_since_navigation,
-                                base::TimeDelta::FromMilliseconds(1),
-                                base::TimeDelta::FromSeconds(60), 100);
-  UMA_HISTOGRAM_LOAD_TIME("NewTabPage.Modules.LoadDuration", duration);
-  base::UmaHistogramCustomTimes("NewTabPage.Modules.LoadDuration." + id,
-                                duration, base::TimeDelta::FromMilliseconds(1),
-                                base::TimeDelta::FromSeconds(60), 100);
-}
-
-void NTPUserDataLogger::LogModuleUsage(const std::string& id) {
-  UMA_HISTOGRAM_EXACT_LINEAR("NewTabPage.Modules.Usage", 1, 1);
-  base::UmaHistogramExactLinear("NewTabPage.Modules.Usage." + id, 1, 1);
-}
-
-void NTPUserDataLogger::SetModulesVisible(bool visible) {
-  modules_visible_ = visible;
-}
-
 bool NTPUserDataLogger::DefaultSearchProviderIsGoogle() const {
   return search::DefaultSearchProviderIsGoogle(profile_);
 }
@@ -700,11 +665,6 @@
     }
   }
 
-  if (base::FeatureList::IsEnabled(ntp_features::kModules)) {
-    base::UmaHistogramBoolean("NewTabPage.Modules.VisibleOnNTPLoad",
-                              modules_visible_);
-  }
-
   has_emitted_ = true;
   during_startup_ = false;
 }
diff --git a/chrome/browser/ui/search/ntp_user_data_logger.h b/chrome/browser/ui/search/ntp_user_data_logger.h
index db352a6..9501278 100644
--- a/chrome/browser/ui/search/ntp_user_data_logger.h
+++ b/chrome/browser/ui/search/ntp_user_data_logger.h
@@ -40,18 +40,6 @@
   // all others require Google as the default search provider.
   void LogEvent(NTPLoggingEventType event, base::TimeDelta time);
 
-  // Logs a module impression. Called when a module is loaded and can be seen by
-  // the user (scrolled into view).
-  void LogModuleImpression(const std::string& id, base::TimeDelta time);
-
-  // Logs a module is loaded on the NTP.
-  void LogModuleLoaded(const std::string& id,
-                       base::TimeDelta duration,
-                       base::TimeDelta time_since_navigation);
-
-  // Logs when a user interacts with a module which will result in a navigation.
-  void LogModuleUsage(const std::string& id);
-
   // Called when a search suggestion event occurs on the NTP that has an integer
   // value associated with it; N suggestions were shown on this NTP load, the
   // Nth suggestion was clicked, etc. |time| is the delta time from navigation
@@ -67,9 +55,6 @@
   // Logs a navigation on one of the NTP tiles by a given impression.
   void LogMostVisitedNavigation(const ntp_tiles::NTPTileImpression& impression);
 
-  // Sets visibility of modules to be later logged.
-  void SetModulesVisible(bool visible);
-
  private:
   // Returns whether Google is selected as the default search engine. Virtual
   // for testing.
@@ -116,8 +101,6 @@
 
   bool should_record_doodle_load_time_;
 
-  bool modules_visible_;
-
   // Are stats being logged during Chrome startup?
   bool during_startup_;
 
diff --git a/chrome/browser/ui/startup/startup_browser_creator.cc b/chrome/browser/ui/startup/startup_browser_creator.cc
index e9781479..bfb6024 100644
--- a/chrome/browser/ui/startup/startup_browser_creator.cc
+++ b/chrome/browser/ui/startup/startup_browser_creator.cc
@@ -819,7 +819,7 @@
     silent_launch = true;
   }
 
-#if !BUILDFLAG(IS_CHROMEOS_ASH) && !BUILDFLAG(IS_CHROMEOS_LACROS)
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
   if (base::FeatureList::IsEnabled(features::kOnConnectNative) &&
       command_line.HasSwitch(switches::kNativeMessagingConnectHost) &&
       command_line.HasSwitch(switches::kNativeMessagingConnectExtension)) {
diff --git a/chrome/browser/ui/webui/autofill_and_password_manager_internals/internals_ui_handler.cc b/chrome/browser/ui/webui/autofill_and_password_manager_internals/internals_ui_handler.cc
index e617165..ed72eda 100644
--- a/chrome/browser/ui/webui/autofill_and_password_manager_internals/internals_ui_handler.cc
+++ b/chrome/browser/ui/webui/autofill_and_password_manager_internals/internals_ui_handler.cc
@@ -39,7 +39,8 @@
   source->AddString(version_ui::kOfficial, version_info::IsOfficialBuild()
                                                ? "official"
                                                : "Developer build");
-  source->AddString(version_ui::kVersionModifier, chrome::GetChannelName());
+  source->AddString(version_ui::kVersionModifier,
+                    chrome::GetChannelName(chrome::WithExtendedStable(true)));
   source->AddString(version_ui::kCL, version_info::GetLastChange());
   source->AddString(version_ui::kUserAgent, embedder_support::GetUserAgent());
   source->AddString("app_locale", g_browser_process->GetApplicationLocale());
diff --git a/chrome/browser/ui/webui/management/management_ui.cc b/chrome/browser/ui/webui/management/management_ui.cc
index e00fd8c9..19c4f9c 100644
--- a/chrome/browser/ui/webui/management/management_ui.cc
+++ b/chrome/browser/ui/webui/management/management_ui.cc
@@ -66,6 +66,7 @@
     {kManagementReportAppInfoAndActivity,
      IDS_MANAGEMENT_REPORT_APP_INFO_AND_ACTIVITY},
     {kManagementPrinting, IDS_MANAGEMENT_REPORT_PRINTING},
+    {kManagementReportPrintJobs, IDS_MANAGEMENT_REPORT_PRINT_JOBS},
     {kManagementCrostini, IDS_MANAGEMENT_CROSTINI},
     {kManagementCrostiniContainerConfiguration,
      IDS_MANAGEMENT_CROSTINI_CONTAINER_CONFIGURATION},
diff --git a/chrome/browser/ui/webui/management/management_ui_handler.cc b/chrome/browser/ui/webui/management/management_ui_handler.cc
index e91b74e..2df1d67 100644
--- a/chrome/browser/ui/webui/management/management_ui_handler.cc
+++ b/chrome/browser/ui/webui/management/management_ui_handler.cc
@@ -167,6 +167,7 @@
 const char kManagementReportExtensions[] = "managementReportExtensions";
 const char kManagementReportAndroidApplications[] =
     "managementReportAndroidApplications";
+const char kManagementReportPrintJobs[] = "managementReportPrintJobs";
 const char kManagementPrinting[] = "managementPrinting";
 const char kManagementCrostini[] = "managementCrostini";
 const char kManagementCrostiniContainerConfiguration[] =
@@ -210,6 +211,7 @@
   kAppInfoAndActivity,
   kLogs,
   kPrint,
+  kPrintJobs,
   kCrostini,
   kUsername,
   kExtensions,
@@ -235,6 +237,8 @@
       return "logs";
     case DeviceReportingType::kPrint:
       return "print";
+    case DeviceReportingType::kPrintJobs:
+      return "print jobs";
     case DeviceReportingType::kCrostini:
       return "crostini";
     case DeviceReportingType::kUsername:
@@ -583,8 +587,17 @@
                               DeviceReportingType::kLogs);
   }
 
-  if (profile->GetPrefs()->GetBoolean(
-          prefs::kPrintingSendUsernameAndFilenameEnabled)) {
+  bool report_print_jobs = false;
+  chromeos::CrosSettings::Get()->GetBoolean(chromeos::kReportDevicePrintJobs,
+                                            &report_print_jobs);
+  if (report_print_jobs) {
+    AddDeviceReportingElement(report_sources, kManagementReportPrintJobs,
+                              DeviceReportingType::kPrintJobs);
+  }
+
+  bool report_print_username = profile->GetPrefs()->GetBoolean(
+      prefs::kPrintingSendUsernameAndFilenameEnabled);
+  if (report_print_username && !report_print_jobs) {
     AddDeviceReportingElement(report_sources, kManagementPrinting,
                               DeviceReportingType::kPrint);
   }
diff --git a/chrome/browser/ui/webui/management/management_ui_handler.h b/chrome/browser/ui/webui/management/management_ui_handler.h
index 781778e..a3bea9f 100644
--- a/chrome/browser/ui/webui/management/management_ui_handler.h
+++ b/chrome/browser/ui/webui/management/management_ui_handler.h
@@ -34,6 +34,7 @@
 extern const char kManagementReportUsers[];
 extern const char kManagementReportCrashReports[];
 extern const char kManagementReportAppInfoAndActivity[];
+extern const char kManagementReportPrintJobs[];
 extern const char kManagementPrinting[];
 extern const char kManagementCrostini[];
 extern const char kManagementCrostiniContainerConfiguration[];
diff --git a/chrome/browser/ui/webui/memories/memories_ui.cc b/chrome/browser/ui/webui/memories/memories_ui.cc
index 91857ef..9879cf20 100644
--- a/chrome/browser/ui/webui/memories/memories_ui.cc
+++ b/chrome/browser/ui/webui/memories/memories_ui.cc
@@ -35,11 +35,19 @@
   // TODO(crbug.com/1173908): Replace these with localized strings.
   source->AddString("memoryTitleDescription",
                     base::UTF8ToUTF16("Based on previous web activity"));
+  source->AddString("topVisitsSectionHeader",
+                    base::UTF8ToUTF16("From Chrome History"));
 
   webui::SetupWebUIDataSource(
       source, base::make_span(kMemoriesResources, kMemoriesResourcesSize),
       IDR_MEMORIES_MEMORIES_HTML);
 
+  content::URLDataSource::Add(profile,
+                              std::make_unique<SanitizedImageSource>(profile));
+  content::URLDataSource::Add(
+      profile, std::make_unique<FaviconSource>(
+                   profile, chrome::FaviconUrlFormat::kFavicon2));
+
   return source;
 }
 
diff --git a/chrome/browser/ui/webui/nacl_ui.cc b/chrome/browser/ui/webui/nacl_ui.cc
index d7a26e0..c684be3 100644
--- a/chrome/browser/ui/webui/nacl_ui.cc
+++ b/chrome/browser/ui/webui/nacl_ui.cc
@@ -187,8 +187,9 @@
 void NaClDomHandler::AddOperatingSystemInfo(base::ListValue* list) {
   // Obtain the Chrome version info.
   AddPair(list, l10n_util::GetStringUTF16(IDS_PRODUCT_NAME),
-          ASCIIToUTF16(version_info::GetVersionNumber() + " (" +
-                       chrome::GetChannelName() + ")"));
+          ASCIIToUTF16(
+              version_info::GetVersionNumber() + " (" +
+              chrome::GetChannelName(chrome::WithExtendedStable(true)) + ")"));
 
   // OS version information.
   // TODO(jvoung): refactor this to share the extra windows labeling
diff --git a/chrome/browser/ui/webui/new_tab_page/new_tab_page.mojom b/chrome/browser/ui/webui/new_tab_page/new_tab_page.mojom
index 1783caf2..3aed7f8 100644
--- a/chrome/browser/ui/webui/new_tab_page/new_tab_page.mojom
+++ b/chrome/browser/ui/webui/new_tab_page/new_tab_page.mojom
@@ -374,17 +374,6 @@
   OnVoiceSearchAction(VoiceSearchAction action);
   // Logs an error occurred while using voice search.
   OnVoiceSearchError(VoiceSearchError error);
-  // Logs a module impression. Called when a module is loaded and can be seen by
-  // the user (scrolled into view).
-  OnModuleImpression(string module_id, double time);
-  // Logs that a module loaded data for |duration| and finished successfully at
-  // |time|.
-  OnModuleLoaded(string module_id, mojo_base.mojom.TimeDelta duration,
-                 double time);
-  // Logs when a user interacts with a module which will result in a navigation.
-  OnModuleUsage(string module_id);
-  // Logs that the modules have been shown at |time|.
-  OnModulesRendered(double time);
   // Logs that the <ntp-app> element's |ready| callback method was called.
   OnAppRendered(double time);
 };
diff --git a/chrome/browser/ui/webui/new_tab_page/new_tab_page_handler.cc b/chrome/browser/ui/webui/new_tab_page/new_tab_page_handler.cc
index 0ec186df..9233d02 100644
--- a/chrome/browser/ui/webui/new_tab_page/new_tab_page_handler.cc
+++ b/chrome/browser/ui/webui/new_tab_page/new_tab_page_handler.cc
@@ -357,8 +357,6 @@
   instant_service_->UpdateNtpTheme();
   promo_service_observation_.Observe(promo_service_);
   one_google_bar_service_observation_.Observe(one_google_bar_service_);
-  logger_.SetModulesVisible(
-      profile_->GetPrefs()->GetBoolean(prefs::kNtpModulesVisible));
 }
 
 NewTabPageHandler::~NewTabPageHandler() {
@@ -966,29 +964,6 @@
   LogEvent(event);
 }
 
-void NewTabPageHandler::OnModuleImpression(const std::string& module_id,
-                                           double time) {
-  logger_.LogModuleImpression(
-      module_id, base::Time::FromJsTime(time) - ntp_navigation_start_time_);
-}
-
-void NewTabPageHandler::OnModuleLoaded(const std::string& module_id,
-                                       base::TimeDelta duration,
-                                       double time) {
-  logger_.LogModuleLoaded(
-      module_id, base::Time::FromJsTime(time) - ntp_navigation_start_time_,
-      duration);
-}
-
-void NewTabPageHandler::OnModuleUsage(const std::string& module_id) {
-  logger_.LogModuleUsage(module_id);
-}
-
-void NewTabPageHandler::OnModulesRendered(double time) {
-  logger_.LogEvent(NTP_MODULES_SHOWN,
-                   base::Time::FromJsTime(time) - ntp_navigation_start_time_);
-}
-
 void NewTabPageHandler::NtpThemeChanged(const NtpTheme& ntp_theme) {
   page_->SetTheme(MakeTheme(ntp_theme));
 }
diff --git a/chrome/browser/ui/webui/new_tab_page/new_tab_page_handler.h b/chrome/browser/ui/webui/new_tab_page/new_tab_page_handler.h
index 865b538..b6e8734d 100644
--- a/chrome/browser/ui/webui/new_tab_page/new_tab_page_handler.h
+++ b/chrome/browser/ui/webui/new_tab_page/new_tab_page_handler.h
@@ -131,12 +131,6 @@
   void OnVoiceSearchAction(
       new_tab_page::mojom::VoiceSearchAction action) override;
   void OnVoiceSearchError(new_tab_page::mojom::VoiceSearchError error) override;
-  void OnModuleImpression(const std::string& module_id, double time) override;
-  void OnModuleLoaded(const std::string& module_id,
-                      base::TimeDelta duration,
-                      double time) override;
-  void OnModuleUsage(const std::string& module_id) override;
-  void OnModulesRendered(double time) override;
 
  private:
   // InstantServiceObserver:
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 375fcec..e44c5cfe 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
@@ -57,7 +57,9 @@
 
 namespace {
 
-content::WebUIDataSource* CreateNewTabPageUiHtmlSource(Profile* profile) {
+content::WebUIDataSource* CreateNewTabPageUiHtmlSource(
+    Profile* profile,
+    const base::Time& navigation_start_time) {
   content::WebUIDataSource* source =
       content::WebUIDataSource::Create(chrome::kChromeUINewTabPageHost);
 
@@ -70,6 +72,7 @@
                              ->search_terms_data()
                              .GoogleBaseURLValue())
                         .spec());
+  source->AddDouble("navigationStartTime", navigation_start_time.ToJsTime());
 
   // Realbox.
   source->AddBoolean(
@@ -322,7 +325,7 @@
       // for the unlikely case where the NewTabPageHandler is created before we
       // received the DidStartNavigation event.
       navigation_start_time_(base::Time::Now()) {
-  auto* source = CreateNewTabPageUiHtmlSource(profile_);
+  auto* source = CreateNewTabPageUiHtmlSource(profile_, navigation_start_time_);
   source->AddBoolean("customBackgroundDisabledByPolicy",
                      instant_service_->IsCustomBackgroundDisabledByPolicy());
   source->AddBoolean(
@@ -451,6 +454,10 @@
     content::NavigationHandle* navigation_handle) {
   if (navigation_handle->IsInMainFrame()) {
     navigation_start_time_ = base::Time::Now();
+    std::unique_ptr<base::DictionaryValue> update(new base::DictionaryValue);
+    update->SetDouble("navigationStartTime", navigation_start_time_.ToJsTime());
+    content::WebUIDataSource::Update(profile_, chrome::kChromeUINewTabPageHost,
+                                     std::move(update));
   }
 }
 
diff --git a/chrome/browser/ui/webui/policy/policy_ui_handler.cc b/chrome/browser/ui/webui/policy/policy_ui_handler.cc
index 19333765..373e1d1b 100644
--- a/chrome/browser/ui/webui/policy/policy_ui_handler.cc
+++ b/chrome/browser/ui/webui/policy/policy_ui_handler.cc
@@ -1247,7 +1247,8 @@
         " %s", base::UTF16ToUTF8(cohort_version_info).c_str());
   }
 #endif
-  std::string channel_name = chrome::GetChannelName();
+  std::string channel_name =
+      chrome::GetChannelName(chrome::WithExtendedStable(true));
   std::string version = base::StringPrintf(
       "%s (%s)%s %s%s", version_info::GetVersionNumber().c_str(),
       l10n_util::GetStringUTF8(version_info::IsOfficialBuild()
diff --git a/chrome/browser/ui/webui/sanitized_image_source.cc b/chrome/browser/ui/webui/sanitized_image_source.cc
index c3873e6..ba7aa5d 100644
--- a/chrome/browser/ui/webui/sanitized_image_source.cc
+++ b/chrome/browser/ui/webui/sanitized_image_source.cc
@@ -97,6 +97,12 @@
   return "image/png";
 }
 
+bool SanitizedImageSource::ShouldReplaceExistingSource() {
+  // Leave the existing DataSource in place, otherwise we'll drop any pending
+  // requests on the floor.
+  return false;
+}
+
 void SanitizedImageSource::OnImageLoaded(
     network::SimpleURLLoader* loader,
     content::URLDataSource::GotDataCallback callback,
diff --git a/chrome/browser/ui/webui/sanitized_image_source.h b/chrome/browser/ui/webui/sanitized_image_source.h
index 6a873acc..e07120d 100644
--- a/chrome/browser/ui/webui/sanitized_image_source.h
+++ b/chrome/browser/ui/webui/sanitized_image_source.h
@@ -61,6 +61,7 @@
       const content::WebContents::Getter& wc_getter,
       content::URLDataSource::GotDataCallback callback) override;
   std::string GetMimeType(const std::string& path) override;
+  bool ShouldReplaceExistingSource() override;
 
  private:
   void OnImageLoaded(network::SimpleURLLoader* loader,
diff --git a/chrome/browser/ui/webui/settings/safety_check_handler.cc b/chrome/browser/ui/webui/settings/safety_check_handler.cc
index 02e5b65c..b87fd1d 100644
--- a/chrome/browser/ui/webui/settings/safety_check_handler.cc
+++ b/chrome/browser/ui/webui/settings/safety_check_handler.cc
@@ -599,7 +599,8 @@
           l10n_util::GetStringUTF16(version_info::IsOfficialBuild()
                                         ? IDS_VERSION_UI_OFFICIAL
                                         : IDS_VERSION_UI_UNOFFICIAL),
-          base::UTF8ToUTF16(chrome::GetChannelName()),
+          base::UTF8ToUTF16(
+              chrome::GetChannelName(chrome::WithExtendedStable(true))),
           l10n_util::GetStringUTF16(VersionUI::VersionProcessorVariation()));
     // This state is only used on Android for recording metrics. This codepath
     // is unreachable.
diff --git a/chrome/browser/ui/webui/settings/safety_check_handler_unittest.cc b/chrome/browser/ui/webui/settings/safety_check_handler_unittest.cc
index cccd942..4089c3e 100644
--- a/chrome/browser/ui/webui/settings/safety_check_handler_unittest.cc
+++ b/chrome/browser/ui/webui/settings/safety_check_handler_unittest.cc
@@ -531,7 +531,9 @@
       event, "Version " + version_info::GetVersionNumber() + " (" +
                  (version_info::IsOfficialBuild() ? "Official Build"
                                                   : "Developer Build") +
-                 ") " + chrome::GetChannelName() + processor_variation);
+                 ") " +
+                 chrome::GetChannelName(chrome::WithExtendedStable(true)) +
+                 processor_variation);
   histogram_tester_.ExpectBucketCount(
       "Settings.SafetyCheck.UpdatesResult",
       SafetyCheckHandler::UpdateStatus::kUnknown, 1);
diff --git a/chrome/browser/ui/webui/settings/settings_localized_strings_provider.cc b/chrome/browser/ui/webui/settings/settings_localized_strings_provider.cc
index a990d7b..051c252 100644
--- a/chrome/browser/ui/webui/settings/settings_localized_strings_provider.cc
+++ b/chrome/browser/ui/webui/settings/settings_localized_strings_provider.cc
@@ -280,7 +280,8 @@
           l10n_util::GetStringUTF16(version_info::IsOfficialBuild()
                                         ? IDS_VERSION_UI_OFFICIAL
                                         : IDS_VERSION_UI_UNOFFICIAL),
-          base::UTF8ToUTF16(chrome::GetChannelName()),
+          base::UTF8ToUTF16(
+              chrome::GetChannelName(chrome::WithExtendedStable(true))),
           l10n_util::GetStringUTF16(VersionUI::VersionProcessorVariation())));
   html_source->AddString(
       "aboutProductCopyright",
diff --git a/chrome/browser/ui/webui/version/version_ui.cc b/chrome/browser/ui/webui/version/version_ui.cc
index a2d5348f..44a0003 100644
--- a/chrome/browser/ui/webui/version/version_ui.cc
+++ b/chrome/browser/ui/webui/version/version_ui.cc
@@ -162,8 +162,9 @@
   // Data strings.
   html_source->AddString(version_ui::kVersion,
                          version_info::GetVersionNumber());
-  html_source->AddString(version_ui::kVersionModifier,
-                         chrome::GetChannelName());
+  html_source->AddString(
+      version_ui::kVersionModifier,
+      chrome::GetChannelName(chrome::WithExtendedStable(true)));
   html_source->AddString(version_ui::kJSEngine, "V8");
   html_source->AddString(version_ui::kJSVersion, V8_VERSION_STRING);
   html_source->AddString(
diff --git a/chrome/browser/video_tutorials/video_tutorial_service_factory.cc b/chrome/browser/video_tutorials/video_tutorial_service_factory.cc
index bed153b1..2fcfd72 100644
--- a/chrome/browser/video_tutorials/video_tutorial_service_factory.cc
+++ b/chrome/browser/video_tutorials/video_tutorial_service_factory.cc
@@ -83,7 +83,8 @@
       SystemNetworkContextManager::GetInstance()->GetSharedURLLoaderFactory();
 
   base::Version version = version_info::GetVersion();
-  std::string channel_name = chrome::GetChannelName();
+  std::string channel_name =
+      chrome::GetChannelName(chrome::WithExtendedStable(true));
   std::string client_version =
       base::StringPrintf("%d.%d.%d.%s.chrome",
                          version.components()[0],  // Major
diff --git a/chrome/build/mac.pgo.txt b/chrome/build/mac.pgo.txt
index 81d65be..1627d569 100644
--- a/chrome/build/mac.pgo.txt
+++ b/chrome/build/mac.pgo.txt
@@ -1 +1 @@
-chrome-mac-master-1615895772-90c2bf8c2781a73f4a574078d5f63a9efff1fc46.profdata
+chrome-mac-master-1615917424-c202f8fcc4f5829e9d5aef5ed69c9eb60ac98eed.profdata
diff --git a/chrome/common/search/ntp_logging_events.h b/chrome/common/search/ntp_logging_events.h
index 4b1c909b..4db85c6 100644
--- a/chrome/common/search/ntp_logging_events.h
+++ b/chrome/common/search/ntp_logging_events.h
@@ -23,6 +23,7 @@
   // Deleted: NTP_MOUSEOVER = 9
   // Deleted: NTP_TILE_LOADED = 10,
   // Deleted: NTP_ALL_TILES_RECEIVED = 12,
+  // Deleted: NTP_MODULES_SHOWN = 83,
 
   // All NTP tiles have finished loading (successfully or failing). Logged only
   // by the single-iframe version of the NTP.
@@ -173,9 +174,6 @@
   // Daily refresh was enabled by clicked 'Done' in the richer picker.
   NTP_BACKGROUND_DAILY_REFRESH_ENABLED = 82,
 
-  // The NTP modules were shown.
-  NTP_MODULES_SHOWN = 83,
-
   // The NTP <ntp-app> element was created and ready() was called.
   NTP_APP_RENDERED = 84,
 
diff --git a/chrome/renderer/media/chrome_speech_recognition_client.cc b/chrome/renderer/media/chrome_speech_recognition_client.cc
index cb356af..57d6e13 100644
--- a/chrome/renderer/media/chrome_speech_recognition_client.cc
+++ b/chrome/renderer/media/chrome_speech_recognition_client.cc
@@ -166,6 +166,7 @@
 
 void ChromeSpeechRecognitionClient::Reset() {
   is_recognizer_bound_ = false;
+  is_browser_requesting_transcription_ = true;
   speech_recognition_context_.reset();
   speech_recognition_recognizer_.reset();
   speech_recognition_client_receiver_.reset();
diff --git a/chrome/services/speech/soda/soda_client_unittest.cc b/chrome/services/speech/soda/soda_client_unittest.cc
index 2fde2fd4..04af72f 100644
--- a/chrome/services/speech/soda/soda_client_unittest.cc
+++ b/chrome/services/speech/soda/soda_client_unittest.cc
@@ -106,7 +106,8 @@
   config_msg.set_language_pack_directory(config_file_path.value().c_str());
   config_msg.set_simulate_realtime_testonly(false);
   config_msg.set_enable_lang_id(false);
-  config_msg.set_recognition_mode(soda::api::SerializedSodaConfigMsg::CAPTION);
+  config_msg.set_recognition_mode(
+      speech::soda::chrome::ExtendedSodaConfigMsg::CAPTION);
 
   // The test binary does not verify the execution context.
   config_msg.set_api_key("");
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index 4f0238c7..0c51b3a 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -2593,6 +2593,7 @@
         "../browser/ash/login/test/user_adding_screen_utils.h",
         "../browser/ash/login/test/webview_content_extractor.cc",
         "../browser/ash/login/test/webview_content_extractor.h",
+        "../browser/ash/login/ui/captive_portal_dialog_delegate_browsertest.cc",
         "../browser/ash/login/ui/captive_portal_window_browsertest.cc",
         "../browser/ash/login/ui/login_feedback_browsertest.cc",
         "../browser/ash/login/ui/login_web_dialog_browsertest.cc",
@@ -5640,20 +5641,15 @@
     } else {
       sources += [
         "../browser/extensions/api/enterprise_reporting_private/enterprise_reporting_private_unittest.cc",
+        "../browser/extensions/api/messaging/native_message_process_host_unittest.cc",
         "../browser/extensions/api/messaging/native_messaging_host_manifest_unittest.cc",
+        "../browser/extensions/api/messaging/native_messaging_launch_from_native_unittest.cc",
         "../browser/extensions/api/messaging/native_messaging_policy_handler_unittest.cc",
       ]
 
       deps += [ "//components/enterprise:test_support" ]
     }
-    if (!is_chromeos_ash && !is_chromeos_lacros) {
-      sources += [
-        "../browser/extensions/api/messaging/native_message_process_host_unittest.cc",
-        "../browser/extensions/api/messaging/native_messaging_launch_from_native_unittest.cc",
-      ]
-    }
   }
-
   if (use_aura) {
     deps += [
       "//ui/aura:test_support",
diff --git a/chrome/test/data/extensions/api_test/notifications/api/basic_app/index.js b/chrome/test/data/extensions/api_test/notifications/api/basic_app/index.js
index db2d7d8..ebac424c 100644
--- a/chrome/test/data/extensions/api_test/notifications/api/basic_app/index.js
+++ b/chrome/test/data/extensions/api_test/notifications/api/basic_app/index.js
@@ -11,6 +11,7 @@
         title: 'hi',
         message: 'there',
         type: 'basic',
+        appIconMaskUrl: redDot,
         buttons: [{title: 'Button'}, {title: 'Button'}]
       };
 
diff --git a/chrome/test/data/webui/cr_components/chromeos/cellular_setup/esim_flow_ui_test.js b/chrome/test/data/webui/cr_components/chromeos/cellular_setup/esim_flow_ui_test.js
index 13735e9..b7f0a21 100644
--- a/chrome/test/data/webui/cr_components/chromeos/cellular_setup/esim_flow_ui_test.js
+++ b/chrome/test/data/webui/cr_components/chromeos/cellular_setup/esim_flow_ui_test.js
@@ -274,7 +274,8 @@
       assertActivationCodePage(/*forwardButtonShouldBeEnabled=*/ true);
     });
 
-    test('Invalid activation code', async function() {
+    // TODO(crbug.com/1188665) Renable flaky test
+    test.skip('Invalid activation code', async function() {
       activationCodePage.barcodeDetectorClass_ = FakeBarcodeDetector;
       activationCodePage.initBarcodeDetector();
       await flushAsync();
diff --git a/chrome/test/data/webui/js/icon_test.js b/chrome/test/data/webui/js/icon_test.js
index 4281410..b94977e 100644
--- a/chrome/test/data/webui/js/icon_test.js
+++ b/chrome/test/data/webui/js/icon_test.js
@@ -4,22 +4,26 @@
 
 function testGetFaviconForPageURL() {
   var url = 'http://foo.com';
-  var expectedDesktop = '-webkit-image-set(' +
-      'url("chrome://favicon2/?size=16&scale_factor=1x&page_url=' +
-      encodeURIComponent('http://foo.com') +
-      '&allow_google_server_fallback=0") 1x, ' +
-      'url("chrome://favicon2/?size=16&scale_factor=2x&page_url=' +
-      encodeURIComponent('http://foo.com') +
-      '&allow_google_server_fallback=0") 2x)';
-  var expectedOther = '-webkit-image-set(' +
-      'url("chrome://favicon2/?size=16&scale_factor=1x&page_url=' +
-      encodeURIComponent('http://foo.com') +
-      '&allow_google_server_fallback=0") ' + window.devicePixelRatio + 'x)';
+  function getExpectedImageSet(size) {
+    var expectedDesktop = '-webkit-image-set(' +
+        `url("chrome://favicon2/?size=${size}&scale_factor=1x&page_url=` +
+        encodeURIComponent(url) + '&allow_google_server_fallback=0") 1x, ' +
+        `url("chrome://favicon2/?size=${size}&scale_factor=2x&page_url=` +
+        encodeURIComponent(url) + '&allow_google_server_fallback=0") 2x)';
+    var expectedOther = '-webkit-image-set(' +
+        `url("chrome://favicon2/?size=${size}&scale_factor=1x&page_url=` +
+        encodeURIComponent(url) + '&allow_google_server_fallback=0") ' +
+        window.devicePixelRatio + 'x)';
 
-  var isDesktop =
-      cr.isMac || cr.isChromeOS || cr.isWindows || cr.isLinux || cr.isLacros;
-  var expected = isDesktop ? expectedDesktop : expectedOther;
-  assertEquals(expected, cr.icon.getFaviconForPageURL(url));
+    var isDesktop =
+        cr.isMac || cr.isChromeOS || cr.isWindows || cr.isLinux || cr.isLacros;
+    return isDesktop ? expectedDesktop : expectedOther;
+  }
+
+  assertEquals(getExpectedImageSet(16), cr.icon.getFaviconForPageURL(url));
+  assertEquals(
+      getExpectedImageSet(24),
+      cr.icon.getFaviconForPageURL(url, false, '', 24));
 }
 
 function testGetFavicon() {
diff --git a/chrome/test/data/webui/new_tab_page/BUILD.gn b/chrome/test/data/webui/new_tab_page/BUILD.gn
index 178f5394..abbccd1 100644
--- a/chrome/test/data/webui/new_tab_page/BUILD.gn
+++ b/chrome/test/data/webui/new_tab_page/BUILD.gn
@@ -12,6 +12,7 @@
                   ]
   deps = [
     ":img_test",
+    ":metrics_utils_test",
     "modules:module_header_test",
   ]
 }
@@ -23,3 +24,11 @@
 
 js_library("metrics_test_support") {
 }
+
+js_library("metrics_utils_test") {
+  deps = [
+    ":metrics_test_support",
+    "//chrome/browser/resources/new_tab_page",
+    "//ui/webui/resources/js:load_time_data.m",
+  ]
+}
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 73883187..a42201e 100644
--- a/chrome/test/data/webui/new_tab_page/app_test.js
+++ b/chrome/test/data/webui/new_tab_page/app_test.js
@@ -6,7 +6,7 @@
 import {isMac} from 'chrome://resources/js/cr.m.js';
 import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
 import {PromiseResolver} from 'chrome://resources/js/promise_resolver.m.js';
-import {FakeMetricsPrivate} from 'chrome://test/new_tab_page/metrics_test_support.js';
+import {fakeMetricsPrivate, MetricsTracker} from 'chrome://test/new_tab_page/metrics_test_support.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 {eventToPromise, flushTasks} from 'chrome://test/test_util.m.js';
@@ -21,7 +21,7 @@
    */
   let testProxy;
 
-  /** @type {FakeMetricsPrivate} */
+  /** @type {MetricsTracker} */
   let metrics;
 
   /**
@@ -68,8 +68,7 @@
     moduleRegistry.setResultFor('getDescriptors', []);
     moduleRegistry.setResultFor('initializeModules', moduleResolver.promise);
     ModuleRegistry.instance_ = moduleRegistry;
-    metrics = new FakeMetricsPrivate();
-    chrome.metricsPrivate = metrics;
+    metrics = fakeMetricsPrivate();
 
     app = document.createElement('ntp-app');
     document.body.appendChild(app);
@@ -453,6 +452,13 @@
 
     [true, false].forEach(visible => {
       test(`modules appended to page if visibility ${visible}`, async () => {
+        // Arrange.
+        loadTimeData.overrideValues({
+          navigationStartTime: 0.0,
+        });
+        testProxy.setResultFor('now', 123);
+
+
         // Act.
         moduleResolver.resolve([
           {
@@ -474,11 +480,14 @@
         // Assert.
         const modules = app.shadowRoot.querySelectorAll('ntp-module-wrapper');
         assertEquals(2, modules.length);
-        assertEquals(1, testProxy.handler.getCallCount('onModulesRendered'));
+        assertEquals(1, metrics.count('NewTabPage.Modules.ShownTime'));
+        assertEquals(1, metrics.count('NewTabPage.Modules.ShownTime', 123));
         const histogram = 'NewTabPage.Modules.EnabledOnNTPLoad';
         assertEquals(1, metrics.count(`${histogram}.foo`, visible));
         assertEquals(1, metrics.count(`${histogram}.bar`, false));
         assertEquals(
+            1, metrics.count('NewTabPage.Modules.VisibleOnNTPLoad', visible));
+        assertEquals(
             1, testProxy.handler.getCallCount('updateDisabledModules'));
       });
     });
diff --git a/chrome/test/data/webui/new_tab_page/customize_modules_test.js b/chrome/test/data/webui/new_tab_page/customize_modules_test.js
index c5cdb3d..7bce7ad 100644
--- a/chrome/test/data/webui/new_tab_page/customize_modules_test.js
+++ b/chrome/test/data/webui/new_tab_page/customize_modules_test.js
@@ -6,7 +6,7 @@
 
 import {$$, BrowserProxy, ModuleDescriptor, ModuleRegistry} from 'chrome://new-tab-page/new_tab_page.js';
 import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
-import {FakeMetricsPrivate} from 'chrome://test/new_tab_page/metrics_test_support.js';
+import {fakeMetricsPrivate, MetricsTracker} from 'chrome://test/new_tab_page/metrics_test_support.js';
 import {assertNotStyle, assertStyle, createTestProxy} from 'chrome://test/new_tab_page/test_support.js';
 import {TestBrowserProxy} from 'chrome://test/test_browser_proxy.m.js';
 
@@ -32,7 +32,7 @@
    */
   let moduleRegistry;
 
-  /** @type {FakeMetricsPrivate} */
+  /** @type {MetricsTracker} */
   let metrics;
 
   /**
@@ -61,8 +61,7 @@
     BrowserProxy.instance_ = testProxy;
     moduleRegistry = TestBrowserProxy.fromClass(ModuleRegistry);
     ModuleRegistry.instance_ = moduleRegistry;
-    metrics = new FakeMetricsPrivate();
-    chrome.metricsPrivate = metrics;
+    metrics = fakeMetricsPrivate();
     loadTimeData.overrideValues({modulesVisibleManagedByPolicy: false});
   });
 
diff --git a/chrome/test/data/webui/new_tab_page/metrics_test_support.js b/chrome/test/data/webui/new_tab_page/metrics_test_support.js
index 059c3b3..3fc5692 100644
--- a/chrome/test/data/webui/new_tab_page/metrics_test_support.js
+++ b/chrome/test/data/webui/new_tab_page/metrics_test_support.js
@@ -2,19 +2,16 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-/**
- * A fake to intercept metrics logging calls and verify how many times they were
- * called.
- */
-export class FakeMetricsPrivate {
+/** Tracks metrics calls to verify metric logging in tests. */
+export class MetricsTracker {
   constructor() {
-    /** @private {!Map<string, !Array>} */
+    /** @private {!Map<string, !Array<*>>} */
     this.histogramMap_ = new Map();
   }
 
   /**
    * @param {string} metricName
-   * @param {string=} value
+   * @param {*=} value
    * @return {number}
    */
   count(metricName, value) {
@@ -23,30 +20,17 @@
         .length;
   }
 
-  /** @param {string} metricName */
-  recordUserAction(metricName) {
-    this.get_(metricName).push(0);
-  }
-
   /**
    * @param {string} metricName
-   * @param {string} value
+   * @param {*} value
    */
-  recordSparseHashable(metricName, value) {
+  record(metricName, value) {
     this.get_(metricName).push(value);
   }
 
   /**
    * @param {string} metricName
-   * @param {boolean} value
-   */
-  recordBoolean(metricName, value) {
-    this.get_(metricName).push(value);
-  }
-
-  /**
-   * @param {string} metricName
-   * @return {!Map<string, !Array>}
+   * @return {!Array<*>}
    * @private
    */
   get_(metricName) {
@@ -56,3 +40,17 @@
     return this.histogramMap_.get(metricName);
   }
 }
+
+/**
+ * Installs interceptors to metrics logging calls and forwards them to the
+ * returned |MetricsTracker| object.
+ * @return {MetricsTracker}
+ */
+export function fakeMetricsPrivate() {
+  const metrics = new MetricsTracker();
+  chrome.metricsPrivate.recordUserAction = (m) => metrics.record(m, 0);
+  chrome.metricsPrivate.recordSparseHashable = (m, v) => metrics.record(m, v);
+  chrome.metricsPrivate.recordBoolean = (m, v) => metrics.record(m, v);
+  chrome.metricsPrivate.recordValue = (m, v) => metrics.record(m.metricName, v);
+  return metrics;
+}
diff --git a/chrome/test/data/webui/new_tab_page/metrics_utils_test.js b/chrome/test/data/webui/new_tab_page/metrics_utils_test.js
new file mode 100644
index 0000000..0beab8e
--- /dev/null
+++ b/chrome/test/data/webui/new_tab_page/metrics_utils_test.js
@@ -0,0 +1,44 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {recordDuration, recordLoadDuration, recordPerdecage, recordOccurence} from 'chrome://new-tab-page/new_tab_page.js';
+import {fakeMetricsPrivate, MetricsTracker} from './metrics_test_support.js';
+import {assertEquals} from '../chai_assert.js';
+import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
+
+suite('NewTabPageMetricsUtilsTest', () => {
+  /** @type {MetricsTracker} */
+  let metrics;
+
+  setup(() => {
+    metrics = fakeMetricsPrivate();
+  });
+
+  test('recordDuration', () => {
+    recordDuration('foo.metric', 123.0);
+    assertEquals(1, metrics.count('foo.metric'));
+    assertEquals(1, metrics.count('foo.metric', 123));
+  });
+
+  test('recordLoadDuration', () => {
+    loadTimeData.overrideValues({
+      navigationStartTime: 5.0,
+    });
+    recordLoadDuration('foo.metric', 123.0);
+    assertEquals(1, metrics.count('foo.metric'));
+    assertEquals(1, metrics.count('foo.metric', 118));
+  });
+
+  test('recordPerdecage', () => {
+    recordPerdecage('foo.metric', 5);
+    assertEquals(1, metrics.count('foo.metric'));
+    assertEquals(1, metrics.count('foo.metric', 5));
+  });
+
+  test('recordOccurence', () => {
+    recordOccurence('foo.metric');
+    assertEquals(1, metrics.count('foo.metric'));
+    assertEquals(1, metrics.count('foo.metric', 1));
+  });
+});
diff --git a/chrome/test/data/webui/new_tab_page/modules/cart/module_test.js b/chrome/test/data/webui/new_tab_page/modules/cart/module_test.js
index 1f7eae1f..154032f 100644
--- a/chrome/test/data/webui/new_tab_page/modules/cart/module_test.js
+++ b/chrome/test/data/webui/new_tab_page/modules/cart/module_test.js
@@ -4,7 +4,7 @@
 
 import {$$, chromeCartDescriptor, ChromeCartProxy} from 'chrome://new-tab-page/new_tab_page.js';
 import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
-import {FakeMetricsPrivate} from 'chrome://test/new_tab_page/metrics_test_support.js';
+import {fakeMetricsPrivate, MetricsTracker} from 'chrome://test/new_tab_page/metrics_test_support.js';
 import {assertNotStyle, assertStyle} from 'chrome://test/new_tab_page/test_support.js';
 import {TestBrowserProxy} from 'chrome://test/test_browser_proxy.m.js';
 import {eventToPromise, flushTasks, isVisible} from 'chrome://test/test_util.m.js';
@@ -16,8 +16,8 @@
    */
   let testProxy;
 
-  /** @type {FakeMetricsPrivate} */
-  let fakeMetricsPrivate;
+  /** @type {MetricsTracker} */
+  let metrics;
 
   setup(() => {
     PolymerTest.clearBody();
@@ -26,8 +26,7 @@
     testProxy.handler =
         TestBrowserProxy.fromClass(chromeCart.mojom.CartHandlerRemote);
     ChromeCartProxy.instance_ = testProxy;
-    fakeMetricsPrivate = new FakeMetricsPrivate();
-    chrome.metricsPrivate = fakeMetricsPrivate;
+    metrics = fakeMetricsPrivate();
     // Not show welcome surface by default.
     testProxy.handler.setResultFor(
         'getWarmWelcomeVisible', Promise.resolve({visible: false}));
@@ -204,15 +203,14 @@
         loadTimeData.getString('modulesCartModuleMenuHideToastMessage'),
         hideToastMessage);
     assertEquals(1, testProxy.handler.getCallCount('hideCartModule'));
-    assertEquals(1, fakeMetricsPrivate.count('NewTabPage.Carts.HideModule'));
+    assertEquals(1, metrics.count('NewTabPage.Carts.HideModule'));
 
     // Act.
     hideRestoreCallback();
 
     // Assert.
     assertEquals(1, testProxy.handler.getCallCount('restoreHiddenCartModule'));
-    assertEquals(
-        1, fakeMetricsPrivate.count('NewTabPage.Carts.UndoHideModule'));
+    assertEquals(1, metrics.count('NewTabPage.Carts.UndoHideModule'));
 
     // Act.
     const waitForDisableEvent = eventToPromise('disable-module', moduleElement);
@@ -224,14 +222,13 @@
 
     // Assert.
     assertEquals('hello world', disableToastMessage);
-    assertEquals(1, fakeMetricsPrivate.count('NewTabPage.Carts.RemoveModule'));
+    assertEquals(1, metrics.count('NewTabPage.Carts.RemoveModule'));
 
     // Act.
     disableRestoreCallback();
 
     // Assert.
-    assertEquals(
-        1, fakeMetricsPrivate.count('NewTabPage.Carts.UndoRemoveModule'));
+    assertEquals(1, metrics.count('NewTabPage.Carts.UndoRemoveModule'));
   });
 
   test('dismiss and undo single cart item in module', async () => {
@@ -382,8 +379,7 @@
     // Assert.
     checkScrollButtonVisibility(moduleElement, true, true);
     checkVisibleRange(moduleElement, 4, 7);
-    assertEquals(
-        1, fakeMetricsPrivate.count('NewTabPage.Carts.RightScrollClick'));
+    assertEquals(1, metrics.count('NewTabPage.Carts.RightScrollClick'));
 
     // Act.
     waitForRightScrollVisibilityChange =
@@ -396,8 +392,7 @@
     // Assert.
     checkScrollButtonVisibility(moduleElement, true, false);
     checkVisibleRange(moduleElement, 6, 9);
-    assertEquals(
-        2, fakeMetricsPrivate.count('NewTabPage.Carts.RightScrollClick'));
+    assertEquals(2, metrics.count('NewTabPage.Carts.RightScrollClick'));
 
     // Act.
     waitForRightScrollVisibilityChange =
@@ -410,8 +405,7 @@
     // Assert.
     checkScrollButtonVisibility(moduleElement, true, true);
     checkVisibleRange(moduleElement, 2, 5);
-    assertEquals(
-        1, fakeMetricsPrivate.count('NewTabPage.Carts.LeftScrollClick'));
+    assertEquals(1, metrics.count('NewTabPage.Carts.LeftScrollClick'));
 
     // Act.
     waitForLeftScrollVisibilityChange =
@@ -424,8 +418,7 @@
     // Assert.
     checkScrollButtonVisibility(moduleElement, false, true);
     checkVisibleRange(moduleElement, 0, 3);
-    assertEquals(
-        2, fakeMetricsPrivate.count('NewTabPage.Carts.LeftScrollClick'));
+    assertEquals(2, metrics.count('NewTabPage.Carts.LeftScrollClick'));
 
     // Remove the observer.
     cartCarousel.removeEventListener('scroll', onScroll);
diff --git a/chrome/test/data/webui/new_tab_page/modules/module_descriptor_test.js b/chrome/test/data/webui/new_tab_page/modules/module_descriptor_test.js
index 14c0966..2b812d9 100644
--- a/chrome/test/data/webui/new_tab_page/modules/module_descriptor_test.js
+++ b/chrome/test/data/webui/new_tab_page/modules/module_descriptor_test.js
@@ -3,6 +3,8 @@
 // found in the LICENSE file.
 
 import {BrowserProxy, ModuleDescriptor} from 'chrome://new-tab-page/new_tab_page.js';
+import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
+import {fakeMetricsPrivate, MetricsTracker} from 'chrome://test/new_tab_page/metrics_test_support.js';
 import {createTestProxy} from 'chrome://test/new_tab_page/test_support.js';
 
 suite('NewTabPageModulesModuleDescriptorTest', () => {
@@ -12,8 +14,15 @@
    */
   let testProxy;
 
+  /** @type {MetricsTracker} */
+  let metrics;
+
   setup(() => {
     PolymerTest.clearBody();
+    loadTimeData.overrideValues({
+      navigationStartTime: 0.0,
+    });
+    metrics = fakeMetricsPrivate();
     testProxy = createTestProxy();
     BrowserProxy.instance_ = testProxy;
   });
@@ -33,11 +42,14 @@
 
     // Assert.
     assertEquals(element, moduleDescriptor.element);
-    assertEquals(1, testProxy.handler.getCallCount('onModuleLoaded'));
-    const [[id, delta, now]] = testProxy.handler.getArgs('onModuleLoaded');
-    assertEquals('foo', id);
-    assertEquals(128, now);
-    assertEquals(5000n, delta.microseconds);  // 128ms - 123ms === 5000µs.
+    assertEquals(1, metrics.count('NewTabPage.Modules.Loaded'));
+    assertEquals(1, metrics.count('NewTabPage.Modules.Loaded', 128));
+    assertEquals(1, metrics.count('NewTabPage.Modules.Loaded.foo'));
+    assertEquals(1, metrics.count('NewTabPage.Modules.Loaded.foo', 128));
+    assertEquals(1, metrics.count('NewTabPage.Modules.LoadDuration'));
+    assertEquals(1, metrics.count('NewTabPage.Modules.LoadDuration', 5));
+    assertEquals(1, metrics.count('NewTabPage.Modules.LoadDuration.foo'));
+    assertEquals(1, metrics.count('NewTabPage.Modules.LoadDuration.foo', 5));
   });
 
   test('instantiate module without data', async () => {
@@ -50,7 +62,10 @@
 
     // Assert.
     assertEquals(null, moduleDescriptor.element);
-    assertEquals(0, testProxy.handler.getCallCount('onModuleLoaded'));
+    assertEquals(0, metrics.count('NewTabPage.Modules.Loaded'));
+    assertEquals(0, metrics.count('NewTabPage.Modules.Loaded.foo'));
+    assertEquals(0, metrics.count('NewTabPage.Modules.LoadDuration'));
+    assertEquals(0, metrics.count('NewTabPage.Modules.LoadDuration.foo'));
   });
 
   test('module load times out', async () => {
diff --git a/chrome/test/data/webui/new_tab_page/modules/module_registry_test.js b/chrome/test/data/webui/new_tab_page/modules/module_registry_test.js
index d191c8f..9b5d2f3 100644
--- a/chrome/test/data/webui/new_tab_page/modules/module_registry_test.js
+++ b/chrome/test/data/webui/new_tab_page/modules/module_registry_test.js
@@ -4,7 +4,9 @@
 
 import {BrowserProxy, ModuleDescriptor, ModuleRegistry} from 'chrome://new-tab-page/new_tab_page.js';
 import {PromiseResolver} from 'chrome://resources/js/promise_resolver.m.js';
+import {fakeMetricsPrivate, MetricsTracker} from 'chrome://test/new_tab_page/metrics_test_support.js';
 import {createTestProxy} from 'chrome://test/new_tab_page/test_support.js';
+import {flushTasks} from 'chrome://test/test_util.m.js';
 
 suite('NewTabPageModulesModuleRegistryTest', () => {
   /**
@@ -13,9 +15,16 @@
    */
   let testProxy;
 
+  /** @type {MetricsTracker} */
+  let metrics;
+
   setup(async () => {
+    PolymerTest.clearBody();
+    loadTimeData.overrideValues({
+      navigationStartTime: 0.0,
+    });
+    metrics = fakeMetricsPrivate();
     testProxy = createTestProxy();
-    testProxy.setResultFor('now', 0);
     BrowserProxy.instance_ = testProxy;
   });
 
@@ -30,10 +39,15 @@
       new ModuleDescriptor('baz', 'bla', 300, () => bazModuleResolver.promise),
       new ModuleDescriptor('buz', 'blo', 400, () => Promise.resolve(fooModule)),
     ]);
+    testProxy.setResultFor('now', 5.0);
 
     // Act.
     const modulesPromise = ModuleRegistry.getInstance().initializeModules(0);
     testProxy.callbackRouterRemote.setDisabledModules(false, ['buz']);
+    // Wait for first batch of modules.
+    await flushTasks();
+    // Move time forward to test metrics.
+    testProxy.setResultFor('now', 123.0);
     // Delayed promise resolution to test async module instantiation.
     bazModuleResolver.resolve(bazModule);
     const modules = await modulesPromise;
@@ -47,5 +61,19 @@
     assertEquals('baz', modules[1].id);
     assertEquals(300, modules[1].heightPx);
     assertDeepEquals(bazModule, modules[1].element);
+    assertEquals(2, metrics.count('NewTabPage.Modules.Loaded'));
+    assertEquals(1, metrics.count('NewTabPage.Modules.Loaded', 5));
+    assertEquals(1, metrics.count('NewTabPage.Modules.Loaded', 123));
+    assertEquals(1, metrics.count('NewTabPage.Modules.Loaded.foo'));
+    assertEquals(1, metrics.count('NewTabPage.Modules.Loaded.foo', 5));
+    assertEquals(1, metrics.count('NewTabPage.Modules.Loaded.baz'));
+    assertEquals(1, metrics.count('NewTabPage.Modules.Loaded.baz', 123));
+    assertEquals(2, metrics.count('NewTabPage.Modules.LoadDuration'));
+    assertEquals(1, metrics.count('NewTabPage.Modules.LoadDuration', 0));
+    assertEquals(1, metrics.count('NewTabPage.Modules.LoadDuration', 118));
+    assertEquals(1, metrics.count('NewTabPage.Modules.LoadDuration.foo'));
+    assertEquals(1, metrics.count('NewTabPage.Modules.LoadDuration.foo', 0));
+    assertEquals(1, metrics.count('NewTabPage.Modules.LoadDuration.baz'));
+    assertEquals(1, metrics.count('NewTabPage.Modules.LoadDuration.baz', 118));
   });
 });
diff --git a/chrome/test/data/webui/new_tab_page/modules/module_wrapper_test.js b/chrome/test/data/webui/new_tab_page/modules/module_wrapper_test.js
index 66ba2cf..4c98eb2 100644
--- a/chrome/test/data/webui/new_tab_page/modules/module_wrapper_test.js
+++ b/chrome/test/data/webui/new_tab_page/modules/module_wrapper_test.js
@@ -2,14 +2,33 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import {$$} from 'chrome://new-tab-page/new_tab_page.js';
+import {$$, BrowserProxy} from 'chrome://new-tab-page/new_tab_page.js';
+import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
+import {fakeMetricsPrivate, MetricsTracker} from 'chrome://test/new_tab_page/metrics_test_support.js';
+import {createTestProxy} from 'chrome://test/new_tab_page/test_support.js';
+import {eventToPromise} from 'chrome://test/test_util.m.js';
 
 suite('NewTabPageModulesModuleWrapperTest', () => {
   /** @type {!ModuleWrapperElement} */
   let moduleWrapper;
 
+  /** @type {MetricsTracker} */
+  let metrics;
+
+  /**
+   * @implements {BrowserProxy}
+   * @extends {TestBrowserProxy}
+   */
+  let testProxy;
+
   setup(() => {
     PolymerTest.clearBody();
+    loadTimeData.overrideValues({
+      navigationStartTime: 0.0,
+    });
+    metrics = fakeMetricsPrivate();
+    testProxy = createTestProxy();
+    BrowserProxy.instance_ = testProxy;
     moduleWrapper = document.createElement('ntp-module-wrapper');
     document.body.appendChild(moduleWrapper);
   });
@@ -17,6 +36,9 @@
   test('renders module descriptor', async () => {
     // Arrange.
     const moduleElement = document.createElement('div');
+    const detectedImpression =
+        eventToPromise('detect-impression', moduleWrapper);
+    testProxy.setResultFor('now', 123);
 
     // Act.
     moduleWrapper.descriptor = {
@@ -24,11 +46,16 @@
       heightPx: 100,
       element: moduleElement,
     };
+    await detectedImpression;
 
     // Assert.
     assertEquals(100, $$(moduleWrapper, '#moduleElement').offsetHeight);
     assertDeepEquals(
         moduleElement, $$(moduleWrapper, '#moduleElement').children[0]);
+    assertEquals(1, metrics.count('NewTabPage.Modules.Impression'));
+    assertEquals(1, metrics.count('NewTabPage.Modules.Impression.foo'));
+    assertEquals(1, metrics.count('NewTabPage.Modules.Impression', 123));
+    assertEquals(1, metrics.count('NewTabPage.Modules.Impression.foo', 123));
   });
 
   test('descriptor can only be set once', () => {
@@ -46,4 +73,21 @@
       };
     });
   });
+
+  test('receiving usage events records usage', () => {
+    // Arrange.
+    const moduleElement = document.createElement('div');
+    moduleWrapper.descriptor = {
+      id: 'foo',
+      heightPx: 100,
+      element: moduleElement,
+    };
+
+    // Act.
+    moduleElement.dispatchEvent(new Event('usage'));
+
+    // Assert.
+    assertEquals(1, metrics.count('NewTabPage.Modules.Usage'));
+    assertEquals(1, metrics.count('NewTabPage.Modules.Usage.foo'));
+  });
 });
diff --git a/chrome/test/data/webui/new_tab_page/new_tab_page_browsertest.js b/chrome/test/data/webui/new_tab_page/new_tab_page_browsertest.js
index a97e747..a5445e7 100644
--- a/chrome/test/data/webui/new_tab_page/new_tab_page_browsertest.js
+++ b/chrome/test/data/webui/new_tab_page/new_tab_page_browsertest.js
@@ -64,6 +64,18 @@
 });
 
 // eslint-disable-next-line no-var
+var NewTabPageMetricsUtilsTest = class extends NewTabPageBrowserTest {
+  /** @override */
+  get browsePreload() {
+    return 'chrome://new-tab-page/test_loader.html?module=new_tab_page/metrics_utils_test.js';
+  }
+};
+
+TEST_F('NewTabPageMetricsUtilsTest', 'All', function() {
+  mocha.run();
+});
+
+// eslint-disable-next-line no-var
 var NewTabPageCustomizeShortcutsTest = class extends NewTabPageBrowserTest {
   /** @override */
   get browsePreload() {
diff --git a/chromecast/browser/webview/proto/webview.proto b/chromecast/browser/webview/proto/webview.proto
index 582c1c0..a20d56b 100644
--- a/chromecast/browser/webview/proto/webview.proto
+++ b/chromecast/browser/webview/proto/webview.proto
@@ -294,6 +294,12 @@
   int32 right = 4;
 }
 
+message GetUserAgentRequest {}
+
+message GetUserAgentResponse {
+  string user_agent = 1;
+}
+
 message WebviewRequest {
   // Unique identifier for the request. For requests that will have a response,
   // the response id will match the request id.
@@ -341,6 +347,7 @@
     ResizeRequest resize = 21;
     ClearCookiesRequest clear_cookies = 22;
     SetInsetsRequest set_insets = 23;
+    GetUserAgentRequest get_user_agent = 24;
   }
 }
 
@@ -365,6 +372,7 @@
     ClearCookiesResponse clear_cookies = 11;
     TextInputFocusEvent input_focus_event = 12;
     AssociateCastAppWindowResponse associate = 20;
+    GetUserAgentResponse get_user_agent = 21;
   }
 }
 
diff --git a/chromecast/browser/webview/web_content_controller.cc b/chromecast/browser/webview/web_content_controller.cc
index 8608900..34f502cd 100644
--- a/chromecast/browser/webview/web_content_controller.cc
+++ b/chromecast/browser/webview/web_content_controller.cc
@@ -13,6 +13,7 @@
 #include "chromecast/browser/webview/proto/webview.pb.h"
 #include "chromecast/browser/webview/webview_input_method_observer.h"
 #include "chromecast/browser/webview/webview_navigation_throttle.h"
+#include "chromecast/common/cast_content_client.h"
 #include "chromecast/graphics/cast_focus_client_aura.h"
 #include "content/public/browser/browser_context.h"
 #include "content/public/browser/browsing_data_remover.h"
@@ -184,6 +185,10 @@
       }
       break;
 
+    case webview::WebviewRequest::kGetUserAgent:
+      HandleGetUserAgent(request.id());
+      break;
+
     default:
       client_->OnError("Unknown request code");
       break;
@@ -529,6 +534,15 @@
     contents->GetTopLevelRenderWidgetHostView()->SetInsets(insets);
 }
 
+void WebContentController::HandleGetUserAgent(int64_t id) {
+  std::unique_ptr<webview::WebviewResponse> response =
+      std::make_unique<webview::WebviewResponse>();
+
+  response->set_id(id);
+  response->mutable_get_user_agent()->set_user_agent(shell::GetUserAgent());
+  client_->EnqueueSend(std::move(response));
+}
+
 viz::SurfaceId WebContentController::GetSurfaceId() {
   content::WebContents* web_contents = GetWebContents();
   // Web contents are destroyed before controller for cast apps.
diff --git a/chromecast/browser/webview/web_content_controller.h b/chromecast/browser/webview/web_content_controller.h
index f7a5117c..16a9bb8 100644
--- a/chromecast/browser/webview/web_content_controller.h
+++ b/chromecast/browser/webview/web_content_controller.h
@@ -116,6 +116,7 @@
   void HandleGetTitle(int64_t id);
   void HandleResize(const gfx::Size& size);
   void HandleSetInsets(const gfx::Insets& insets);
+  void HandleGetUserAgent(int64_t id);
 
   viz::SurfaceId GetSurfaceId();
   void ChannelModified(content::RenderFrameHost* frame,
diff --git a/chromecast/browser/webview/webview_browsertest.cc b/chromecast/browser/webview/webview_browsertest.cc
index b42c721..dbeb39a 100644
--- a/chromecast/browser/webview/webview_browsertest.cc
+++ b/chromecast/browser/webview/webview_browsertest.cc
@@ -420,4 +420,24 @@
   RunMessageLoop();
 }
 
+IN_PROC_BROWSER_TEST_F(WebviewTest, GetUserAgent) {
+  auto check = [](const std::unique_ptr<webview::WebviewResponse>& response) {
+    return response->has_get_user_agent();
+  };
+  EXPECT_CALL(client_, EnqueueSend(_)).Times(testing::AnyNumber());
+  EXPECT_CALL(client_, EnqueueSend(Truly(check)))
+      .Times(testing::AtLeast(1))
+      .WillOnce([this](std::unique_ptr<webview::WebviewResponse> response) {
+        EXPECT_NE(response->get_user_agent().user_agent(), "");
+        Quit();
+      });
+  WebviewController webview(context_.get(), &client_, true);
+
+  webview::WebviewRequest request;
+  request.mutable_get_user_agent();
+  SubmitWebviewRequest(&webview, request);
+
+  RunMessageLoop();
+}
+
 }  // namespace chromecast
diff --git a/chromeos/crosapi/mojom/BUILD.gn b/chromeos/crosapi/mojom/BUILD.gn
index 5df0b042..7cdb1cd 100644
--- a/chromeos/crosapi/mojom/BUILD.gn
+++ b/chromeos/crosapi/mojom/BUILD.gn
@@ -7,6 +7,7 @@
 mojom("mojom") {
   sources = [
     "account_manager.mojom",
+    "automation.mojom",
     "bitmap.mojom",
     "cert_database.mojom",
     "clipboard.mojom",
diff --git a/chromeos/crosapi/mojom/automation.mojom b/chromeos/crosapi/mojom/automation.mojom
new file mode 100644
index 0000000..d629655
--- /dev/null
+++ b/chromeos/crosapi/mojom/automation.mojom
@@ -0,0 +1,72 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+module crosapi.mojom;
+
+import "mojo/public/mojom/base/unguessable_token.mojom";
+import "mojo/public/mojom/base/values.mojom";
+
+// Interface for automation clients. Implemented by lacros-chrome. Used by
+// ash-chrome to enable automation and to perform actions.
+// Next version: 1
+// Next method id: 3
+[Stable, Uuid="8dd5f2a7-c24b-47c3-a096-a5d28c4764bb"]
+interface AutomationClient {
+  // Enables automation for the client. This will result in the client
+  // repeatedly calling ReceiveEventPrototype() on the Automation interface.
+  Enable@0();
+
+  // Enables automation for a particular subtree of the client. This will
+  // result in the client repeatedly calling ReceiveEventPrototype() on the
+  // Automation interface.
+  EnableTree@1(mojo_base.mojom.UnguessableToken token);
+
+  // All actions are forwarded to all clients. If the client has no matching
+  // |tree_id|, then it should do nothing. If |request_id| is -1, then there is
+  // no corresponding request_id -- mojo does not support optional primitives,
+  // and the underlying a11y code uses the same semantics for -1.
+  //
+  // This method is purely for prototyping and should not be called on
+  // production devices. The main problem is that |optional_args| should be a
+  // stable mojom struct, but we're not sure yet exactly what interface to
+  // stabilize for a11y.
+  PerformActionPrototype@2(
+      mojo_base.mojom.UnguessableToken tree_id, int32 automation_node_id,
+      string action_type, int32 request_id,
+      mojo_base.mojom.DictionaryValue optional_args);
+};
+
+// Interface for automation. Implemented by ash-chrome.
+// Next version: 1
+// Next method id: 2
+[Stable, Uuid="356a895e-b41a-4c45-9336-d8dc6d332f98"]
+interface Automation {
+  // A crosapi client can register itself as a automation client. This
+  // provides ash a mechanism to enable automation and send actions to the
+  // client. The client is uniquely identified by |token|.
+  RegisterAutomationClient@0(
+      pending_remote<AutomationClient> client,
+      mojo_base.mojom.UnguessableToken token);
+
+  // |event_bundle| is a serialized instance of
+  // |ExtensionMsg_AutomationEventBundleParams|. This is potentially not
+  // compatible across lacros/ash version skew. https://crbug.com/1182926.
+  // |root| refers to whether the associated accessibility tree is the root
+  // accessibility tree for Lacros. Ash needs this information so as to
+  // appropriately glue in Lacros's accessibility trees with the rest of ash's
+  // accessibility trees.
+  // |token| uniquely identifies the client.
+  // |window_id| is a short-term placeholder that identifies a lacros window in
+  // which to glue the entire a11y tree (which may contain the contents of more
+  // than 1 lacros window). This relies on the assumption that lacros has a
+  // window open.
+  //
+  // This method is purely for prototyping and should not be called on
+  // production devices. The main problem is we're not sure yet what interface
+  // to stabilize for a11y.
+  ReceiveEventPrototype@1(
+      string event_bundle, bool root, mojo_base.mojom.UnguessableToken token,
+      string window_id);
+};
+
diff --git a/chromeos/crosapi/mojom/crosapi.mojom b/chromeos/crosapi/mojom/crosapi.mojom
index 317072e..be069236 100644
--- a/chromeos/crosapi/mojom/crosapi.mojom
+++ b/chromeos/crosapi/mojom/crosapi.mojom
@@ -5,6 +5,7 @@
 module crosapi.mojom;
 
 import "chromeos/components/sensors/mojom/cros_sensor_service.mojom";
+import "chromeos/crosapi/mojom/automation.mojom";
 import "chromeos/crosapi/mojom/account_manager.mojom";
 import "chromeos/crosapi/mojom/cert_database.mojom";
 import "chromeos/crosapi/mojom/clipboard.mojom";
@@ -49,11 +50,17 @@
 // please note the milestone when you added it, to help us reason about
 // compatibility between the client applications and older ash-chrome binaries.
 //
-// Next version: 18
-// Next method id: 23
+// Next version: 19
+// Next method id: 24
 [Stable, Uuid="8b79c34f-2bf8-4499-979a-b17cac522c1e",
  RenamedFrom="crosapi.mojom.AshChromeService"]
 interface Crosapi {
+  // Binds the automation interface which allows ash to enableautomation
+  // for Lacros.
+  // Added in M91.
+  [MinVersion=18]
+  BindAutomation@23(pending_receiver<Automation> receiver);
+
   // Binds Chrome OS Account Manager for Identity management.
   // Added in M87.
   [MinVersion=4]
diff --git a/chromeos/crosapi/mojom/notification.mojom b/chromeos/crosapi/mojom/notification.mojom
index dd98b09..617a58b 100644
--- a/chromeos/crosapi/mojom/notification.mojom
+++ b/chromeos/crosapi/mojom/notification.mojom
@@ -144,4 +144,12 @@
   // Badge to show the source of the notification (e.g. a 16x16 browser icon).
   [MinVersion=1]
   gfx.mojom.ImageSkia? badge@23;
+
+  [MinVersion=2]
+  // Whether the bool badge_needs_additional_masking is set.
+  bool badge_needs_additional_masking_has_value@24;
+
+  [MinVersion=2]
+  // Whether the badge needs additional masking.
+  bool badge_needs_additional_masking@25;
 };
diff --git a/chromeos/lacros/lacros_chrome_service_impl.cc b/chromeos/lacros/lacros_chrome_service_impl.cc
index b5dfbbf..eb6a246 100644
--- a/chromeos/lacros/lacros_chrome_service_impl.cc
+++ b/chromeos/lacros/lacros_chrome_service_impl.cc
@@ -220,6 +220,12 @@
     crosapi_->BindFeedback(std::move(pending_receiver));
   }
 
+  void BindAutomationReceiver(
+      mojo::PendingReceiver<crosapi::mojom::Automation> pending_receiver) {
+    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+    crosapi_->BindAutomation(std::move(pending_receiver));
+  }
+
   void BindCertDbReceiver(
       mojo::PendingReceiver<crosapi::mojom::CertDatabase> pending_receiver) {
     DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
@@ -485,6 +491,15 @@
   delegate_->OnInitialized(*init_params_);
   did_bind_receiver_ = true;
 
+  if (IsAutomationAvailable()) {
+    never_blocking_sequence_->PostTask(
+        FROM_HERE,
+        base::BindOnce(
+            &LacrosChromeServiceNeverBlockingState::BindAutomationReceiver,
+            weak_sequenced_state_,
+            automation_remote_.BindNewPipeAndPassReceiver()));
+  }
+
   if (IsCertDbAvailable()) {
     never_blocking_sequence_->PostTask(
         FROM_HERE,
@@ -634,6 +649,12 @@
   g_disable_all_crosapi_for_tests = true;
 }
 
+bool LacrosChromeServiceImpl::IsAutomationAvailable() const {
+  base::Optional<uint32_t> version = CrosapiVersion();
+  return version && version.value() >=
+                        Crosapi::MethodMinVersions::kBindAutomationMinVersion;
+}
+
 bool LacrosChromeServiceImpl::IsAccountManagerAvailable() const {
   base::Optional<uint32_t> version = CrosapiVersion();
   return version &&
diff --git a/chromeos/lacros/lacros_chrome_service_impl.h b/chromeos/lacros/lacros_chrome_service_impl.h
index 7f90cfd..078abd6 100644
--- a/chromeos/lacros/lacros_chrome_service_impl.h
+++ b/chromeos/lacros/lacros_chrome_service_impl.h
@@ -19,6 +19,7 @@
 #include "base/sequenced_task_runner.h"
 #include "chromeos/components/sensors/mojom/cros_sensor_service.mojom.h"
 #include "chromeos/crosapi/mojom/account_manager.mojom.h"
+#include "chromeos/crosapi/mojom/automation.mojom.h"
 #include "chromeos/crosapi/mojom/cert_database.mojom.h"
 #include "chromeos/crosapi/mojom/crosapi.mojom.h"
 #include "chromeos/crosapi/mojom/device_attributes.mojom.h"
@@ -102,6 +103,7 @@
 
   // Each of these functions guards usage of access to the corresponding remote.
   // Keep these in alphabetical order.
+  bool IsAutomationAvailable() const;
   bool IsAccountManagerAvailable() const;
   bool IsCertDbAvailable() const;
   bool IsClipboardAvailable() const;
@@ -134,6 +136,13 @@
   // --------------------------------------------------------------------------
 
   // This must be called on the affine sequence.
+  mojo::Remote<crosapi::mojom::Automation>& automation_remote() {
+    DCHECK_CALLED_ON_VALID_SEQUENCE(affine_sequence_checker_);
+    DCHECK(IsAutomationAvailable());
+    return automation_remote_;
+  }
+
+  // This must be called on the affine sequence.
   mojo::Remote<crosapi::mojom::CertDatabase>& cert_database_remote() {
     DCHECK_CALLED_ON_VALID_SEQUENCE(affine_sequence_checker_);
     DCHECK(IsCertDbAvailable());
@@ -349,6 +358,7 @@
 
   // These members are affine to the affine sequence. They are initialized in
   // the constructor and are immediately available for use.
+  mojo::Remote<crosapi::mojom::Automation> automation_remote_;
   mojo::Remote<crosapi::mojom::CertDatabase> cert_database_remote_;
   mojo::Remote<crosapi::mojom::Clipboard> clipboard_remote_;
   mojo::Remote<crosapi::mojom::DeviceAttributes> device_attributes_remote_;
diff --git a/chromeos/network/cellular_metrics_logger.cc b/chromeos/network/cellular_metrics_logger.cc
index 3e1d2e06..7e472e1 100644
--- a/chromeos/network/cellular_metrics_logger.cc
+++ b/chromeos/network/cellular_metrics_logger.cc
@@ -30,6 +30,22 @@
 }  // namespace
 
 // static
+const char CellularMetricsLogger::kSimPinLockSuccessHistogram[] =
+    "Network.Cellular.Pin.LockSuccess";
+
+// static
+const char CellularMetricsLogger::kSimPinUnlockSuccessHistogram[] =
+    "Network.Cellular.Pin.UnlockSuccess";
+
+// static
+const char CellularMetricsLogger::kSimPinUnblockSuccessHistogram[] =
+    "Network.Cellular.Pin.UnblockSuccess";
+
+// static
+const char CellularMetricsLogger::kSimPinChangeSuccessHistogram[] =
+    "Network.Cellular.Pin.ChangeSuccess";
+
+// static
 const base::TimeDelta CellularMetricsLogger::kInitializationTimeout =
     base::TimeDelta::FromSeconds(15);
 
@@ -37,6 +53,52 @@
 const base::TimeDelta CellularMetricsLogger::kDisconnectRequestTimeout =
     base::TimeDelta::FromSeconds(5);
 
+// static
+CellularMetricsLogger::SimPinOperationResult
+CellularMetricsLogger::GetSimPinOperationResultForShillError(
+    const std::string& shill_error_name) {
+  if (shill_error_name == shill::kErrorResultFailure ||
+      shill_error_name == shill::kErrorResultInvalidArguments) {
+    return SimPinOperationResult::kErrorFailure;
+  }
+  if (shill_error_name == shill::kErrorResultNotSupported)
+    return SimPinOperationResult::kErrorNotSupported;
+  if (shill_error_name == shill::kErrorResultIncorrectPin)
+    return SimPinOperationResult::kErrorIncorrectPin;
+  if (shill_error_name == shill::kErrorResultPinBlocked)
+    return SimPinOperationResult::kErrorPinBlocked;
+  if (shill_error_name == shill::kErrorResultPinRequired)
+    return SimPinOperationResult::kErrorPinRequired;
+  if (shill_error_name == shill::kErrorResultNotFound)
+    return SimPinOperationResult::kErrorDeviceMissing;
+  return SimPinOperationResult::kErrorUnknown;
+}
+
+// static
+void CellularMetricsLogger::RecordSimPinOperationResult(
+    const SimPinOperation& pin_operation,
+    const base::Optional<std::string>& shill_error_name) {
+  SimPinOperationResult result =
+      shill_error_name.has_value()
+          ? GetSimPinOperationResultForShillError(*shill_error_name)
+          : SimPinOperationResult::kSuccess;
+
+  switch (pin_operation) {
+    case SimPinOperation::kLock:
+      UMA_HISTOGRAM_ENUMERATION(kSimPinLockSuccessHistogram, result);
+      return;
+    case SimPinOperation::kUnlock:
+      UMA_HISTOGRAM_ENUMERATION(kSimPinUnlockSuccessHistogram, result);
+      return;
+    case SimPinOperation::kUnblock:
+      UMA_HISTOGRAM_ENUMERATION(kSimPinUnblockSuccessHistogram, result);
+      return;
+    case SimPinOperation::kChange:
+      UMA_HISTOGRAM_ENUMERATION(kSimPinChangeSuccessHistogram, result);
+      return;
+  }
+}
+
 CellularMetricsLogger::ConnectionInfo::ConnectionInfo(
     const std::string& network_guid,
     bool is_connected)
diff --git a/chromeos/network/cellular_metrics_logger.h b/chromeos/network/cellular_metrics_logger.h
index 7fa7a217..8169254e 100644
--- a/chromeos/network/cellular_metrics_logger.h
+++ b/chromeos/network/cellular_metrics_logger.h
@@ -39,6 +39,25 @@
       public LoginState::Observer,
       public NetworkConnectionObserver {
  public:
+  // Histograms associated with SIM Pin operations.
+  static const char kSimPinLockSuccessHistogram[];
+  static const char kSimPinUnlockSuccessHistogram[];
+  static const char kSimPinUnblockSuccessHistogram[];
+  static const char kSimPinChangeSuccessHistogram[];
+
+  // PIN operations that are tracked by metrics.
+  enum class SimPinOperation {
+    kLock = 0,
+    kUnlock = 1,
+    kUnblock = 2,
+    kChange = 3,
+  };
+
+  // Records the result of pin operations performed.
+  static void RecordSimPinOperationResult(
+      const SimPinOperation& pin_operation,
+      const base::Optional<std::string>& shill_error_name = base::nullopt);
+
   CellularMetricsLogger();
   ~CellularMetricsLogger() override;
 
@@ -74,6 +93,11 @@
   FRIEND_TEST_ALL_PREFIXES(CellularMetricsLoggerTest,
                            CellularDisconnectionsTest);
 
+  FRIEND_TEST_ALL_PREFIXES(NetworkDeviceHandlerTest, RequirePin);
+  FRIEND_TEST_ALL_PREFIXES(NetworkDeviceHandlerTest, EnterPin);
+  FRIEND_TEST_ALL_PREFIXES(NetworkDeviceHandlerTest, UnblockPin);
+  FRIEND_TEST_ALL_PREFIXES(NetworkDeviceHandlerTest, ChangePin);
+
   // The amount of time after cellular device is added to device list,
   // after which cellular device is considered initialized.
   static const base::TimeDelta kInitializationTimeout;
@@ -141,6 +165,29 @@
     kMaxValue = kDisconnected
   };
 
+  // Result of PIN operations.
+  // These values are persisted to logs. Entries should not be renumbered
+  // and numeric values should never be reused.
+  // Note: With the exception of Success, enums should match the
+  // error names listed near the top of NetworkDeviceHandler.
+  enum class SimPinOperationResult {
+    kSuccess = 0,
+    kErrorDeviceMissing = 1,
+    kErrorFailure = 2,
+    kErrorIncorrectPin = 3,
+    kErrorNotFound = 4,
+    kErrorNotSupported = 5,
+    kErrorPinBlocked = 6,
+    kErrorPinRequired = 7,
+    kErrorTimeout = 8,
+    kErrorUnknown = 9,
+    kMaxValue = kErrorUnknown,
+  };
+
+  // Convert shill error name string to SimPinOperationResult enum.
+  static SimPinOperationResult GetSimPinOperationResultForShillError(
+      const std::string& shill_error_name);
+
   // Convert shill activation state string to PSimActivationState enum
   PSimActivationState PSimActivationStateToEnum(const std::string& state);
 
diff --git a/chromeos/network/network_device_handler_impl.cc b/chromeos/network/network_device_handler_impl.cc
index 25180122..a9e1c289 100644
--- a/chromeos/network/network_device_handler_impl.cc
+++ b/chromeos/network/network_device_handler_impl.cc
@@ -25,6 +25,7 @@
 #include "base/values.h"
 #include "chromeos/dbus/shill/shill_device_client.h"
 #include "chromeos/dbus/shill/shill_ipconfig_client.h"
+#include "chromeos/network/cellular_metrics_logger.h"
 #include "chromeos/network/device_state.h"
 #include "chromeos/network/network_event_log.h"
 #include "chromeos/network/network_state_handler.h"
@@ -100,6 +101,25 @@
   return error_data;
 }
 
+void HandleSimPinOperationSuccess(
+    const CellularMetricsLogger::SimPinOperation& pin_operation,
+    base::OnceClosure callback) {
+  CellularMetricsLogger::RecordSimPinOperationResult(pin_operation);
+  std::move(callback).Run();
+}
+
+void HandleSimPinOperationFailure(
+    const CellularMetricsLogger::SimPinOperation& pin_operation,
+    const std::string& device_path,
+    network_handler::ErrorCallback error_callback,
+    const std::string& shill_error_name,
+    const std::string& shill_error_message) {
+  CellularMetricsLogger::RecordSimPinOperationResult(pin_operation,
+                                                     shill_error_message);
+  HandleShillCallFailure(device_path, std::move(error_callback),
+                         shill_error_name, shill_error_message);
+}
+
 }  // namespace
 
 NetworkDeviceHandlerImpl::NetworkDeviceHandlerImpl() = default;
@@ -161,8 +181,12 @@
     network_handler::ErrorCallback error_callback) {
   NET_LOG(USER) << "Device.RequirePin: " << device_path << ": " << require_pin;
   ShillDeviceClient::Get()->RequirePin(
-      dbus::ObjectPath(device_path), pin, require_pin, std::move(callback),
-      base::BindOnce(&HandleShillCallFailure, device_path,
+      dbus::ObjectPath(device_path), pin, require_pin,
+      base::BindOnce(&HandleSimPinOperationSuccess,
+                     CellularMetricsLogger::SimPinOperation::kLock,
+                     std::move(callback)),
+      base::BindOnce(&HandleSimPinOperationFailure,
+                     CellularMetricsLogger::SimPinOperation::kLock, device_path,
                      std::move(error_callback)));
 }
 
@@ -173,9 +197,13 @@
     network_handler::ErrorCallback error_callback) {
   NET_LOG(USER) << "Device.EnterPin: " << device_path;
   ShillDeviceClient::Get()->EnterPin(
-      dbus::ObjectPath(device_path), pin, std::move(callback),
-      base::BindOnce(&HandleShillCallFailure, device_path,
-                     std::move(error_callback)));
+      dbus::ObjectPath(device_path), pin,
+      base::BindOnce(&HandleSimPinOperationSuccess,
+                     CellularMetricsLogger::SimPinOperation::kUnlock,
+                     std::move(callback)),
+      base::BindOnce(&HandleSimPinOperationFailure,
+                     CellularMetricsLogger::SimPinOperation::kUnlock,
+                     device_path, std::move(error_callback)));
 }
 
 void NetworkDeviceHandlerImpl::UnblockPin(
@@ -186,9 +214,13 @@
     network_handler::ErrorCallback error_callback) {
   NET_LOG(USER) << "Device.UnblockPin: " << device_path;
   ShillDeviceClient::Get()->UnblockPin(
-      dbus::ObjectPath(device_path), puk, new_pin, std::move(callback),
-      base::BindOnce(&HandleShillCallFailure, device_path,
-                     std::move(error_callback)));
+      dbus::ObjectPath(device_path), puk, new_pin,
+      base::BindOnce(&HandleSimPinOperationSuccess,
+                     CellularMetricsLogger::SimPinOperation::kUnblock,
+                     std::move(callback)),
+      base::BindOnce(&HandleSimPinOperationFailure,
+                     CellularMetricsLogger::SimPinOperation::kUnblock,
+                     device_path, std::move(error_callback)));
 }
 
 void NetworkDeviceHandlerImpl::ChangePin(
@@ -199,9 +231,13 @@
     network_handler::ErrorCallback error_callback) {
   NET_LOG(USER) << "Device.ChangePin: " << device_path;
   ShillDeviceClient::Get()->ChangePin(
-      dbus::ObjectPath(device_path), old_pin, new_pin, std::move(callback),
-      base::BindOnce(&HandleShillCallFailure, device_path,
-                     std::move(error_callback)));
+      dbus::ObjectPath(device_path), old_pin, new_pin,
+      base::BindOnce(&HandleSimPinOperationSuccess,
+                     CellularMetricsLogger::SimPinOperation::kChange,
+                     std::move(callback)),
+      base::BindOnce(&HandleSimPinOperationFailure,
+                     CellularMetricsLogger::SimPinOperation::kChange,
+                     device_path, std::move(error_callback)));
 }
 
 void NetworkDeviceHandlerImpl::SetCellularAllowRoaming(
diff --git a/chromeos/network/network_device_handler_unittest.cc b/chromeos/network/network_device_handler_unittest.cc
index e15cf850..f9ed547 100644
--- a/chromeos/network/network_device_handler_unittest.cc
+++ b/chromeos/network/network_device_handler_unittest.cc
@@ -8,12 +8,14 @@
 #include "base/logging.h"
 #include "base/macros.h"
 #include "base/run_loop.h"
+#include "base/test/metrics/histogram_tester.h"
 #include "base/test/task_environment.h"
 #include "base/values.h"
 #include "chromeos/dbus/dbus_thread_manager.h"
 #include "chromeos/dbus/shill/fake_shill_device_client.h"
 #include "chromeos/dbus/shill/shill_clients.h"
 #include "chromeos/dbus/shill/shill_manager_client.h"
+#include "chromeos/network/cellular_metrics_logger.h"
 #include "chromeos/network/network_device_handler_impl.h"
 #include "chromeos/network/network_handler_callbacks.h"
 #include "chromeos/network/network_state_handler.h"
@@ -429,12 +431,19 @@
 }
 
 TEST_F(NetworkDeviceHandlerTest, RequirePin) {
+  base::HistogramTester histogram_tester;
+
   // Test that the success callback gets called.
   network_device_handler_->RequirePin(kDefaultCellularDevicePath, true,
                                       kDefaultPin, GetSuccessCallback(),
                                       GetErrorCallback());
   base::RunLoop().RunUntilIdle();
   EXPECT_EQ(kResultSuccess, result_);
+  histogram_tester.ExpectTotalCount(
+      CellularMetricsLogger::kSimPinLockSuccessHistogram, 1);
+  histogram_tester.ExpectBucketCount(
+      CellularMetricsLogger::kSimPinLockSuccessHistogram,
+      CellularMetricsLogger::SimPinOperationResult::kSuccess, 1);
 
   // Test that the shill error propagates to the error callback.
   network_device_handler_->RequirePin(kUnknownCellularDevicePath, true,
@@ -442,23 +451,42 @@
                                       GetErrorCallback());
   base::RunLoop().RunUntilIdle();
   EXPECT_EQ(NetworkDeviceHandler::kErrorDeviceMissing, result_);
+
+  histogram_tester.ExpectTotalCount(
+      CellularMetricsLogger::kSimPinLockSuccessHistogram, 2);
+  histogram_tester.ExpectBucketCount(
+      CellularMetricsLogger::kSimPinLockSuccessHistogram,
+      CellularMetricsLogger::SimPinOperationResult::kErrorUnknown, 1);
 }
 
 TEST_F(NetworkDeviceHandlerTest, EnterPin) {
+  base::HistogramTester histogram_tester;
+
   // Test that the success callback gets called.
   network_device_handler_->EnterPin(kDefaultCellularDevicePath, kDefaultPin,
                                     GetSuccessCallback(), GetErrorCallback());
   base::RunLoop().RunUntilIdle();
   EXPECT_EQ(kResultSuccess, result_);
+  histogram_tester.ExpectTotalCount(
+      CellularMetricsLogger::kSimPinUnlockSuccessHistogram, 1);
+  histogram_tester.ExpectBucketCount(
+      CellularMetricsLogger::kSimPinUnlockSuccessHistogram,
+      CellularMetricsLogger::SimPinOperationResult::kSuccess, 1);
 
   // Test that the shill error propagates to the error callback.
   network_device_handler_->EnterPin(kUnknownCellularDevicePath, kDefaultPin,
                                     GetSuccessCallback(), GetErrorCallback());
   base::RunLoop().RunUntilIdle();
   EXPECT_EQ(NetworkDeviceHandler::kErrorDeviceMissing, result_);
+  histogram_tester.ExpectTotalCount(
+      CellularMetricsLogger::kSimPinUnlockSuccessHistogram, 2);
+  histogram_tester.ExpectBucketCount(
+      CellularMetricsLogger::kSimPinUnlockSuccessHistogram,
+      CellularMetricsLogger::SimPinOperationResult::kErrorUnknown, 1);
 }
 
 TEST_F(NetworkDeviceHandlerTest, UnblockPin) {
+  base::HistogramTester histogram_tester;
   const char kPuk[] = "12345678";
   const char kPin[] = "1234";
 
@@ -467,15 +495,26 @@
                                       GetSuccessCallback(), GetErrorCallback());
   base::RunLoop().RunUntilIdle();
   EXPECT_EQ(kResultSuccess, result_);
+  histogram_tester.ExpectTotalCount(
+      CellularMetricsLogger::kSimPinUnblockSuccessHistogram, 1);
+  histogram_tester.ExpectBucketCount(
+      CellularMetricsLogger::kSimPinUnblockSuccessHistogram,
+      CellularMetricsLogger::SimPinOperationResult::kSuccess, 1);
 
   // Test that the shill error propagates to the error callback.
   network_device_handler_->UnblockPin(kUnknownCellularDevicePath, kPin, kPuk,
                                       GetSuccessCallback(), GetErrorCallback());
   base::RunLoop().RunUntilIdle();
   EXPECT_EQ(NetworkDeviceHandler::kErrorDeviceMissing, result_);
+  histogram_tester.ExpectTotalCount(
+      CellularMetricsLogger::kSimPinUnblockSuccessHistogram, 2);
+  histogram_tester.ExpectBucketCount(
+      CellularMetricsLogger::kSimPinUnblockSuccessHistogram,
+      CellularMetricsLogger::SimPinOperationResult::kErrorUnknown, 1);
 }
 
 TEST_F(NetworkDeviceHandlerTest, ChangePin) {
+  base::HistogramTester histogram_tester;
   const char kNewPin[] = "1234";
   const char kIncorrectPin[] = "9999";
 
@@ -488,6 +527,11 @@
       kNewPin, GetSuccessCallback(), GetErrorCallback());
   base::RunLoop().RunUntilIdle();
   EXPECT_EQ(kResultSuccess, result_);
+  histogram_tester.ExpectTotalCount(
+      CellularMetricsLogger::kSimPinChangeSuccessHistogram, 1);
+  histogram_tester.ExpectBucketCount(
+      CellularMetricsLogger::kSimPinChangeSuccessHistogram,
+      CellularMetricsLogger::SimPinOperationResult::kSuccess, 1);
 
   // Test that the shill error propagates to the error callback.
   network_device_handler_->ChangePin(kDefaultCellularDevicePath, kIncorrectPin,
@@ -495,6 +539,11 @@
                                      GetErrorCallback());
   base::RunLoop().RunUntilIdle();
   EXPECT_EQ(NetworkDeviceHandler::kErrorIncorrectPin, result_);
+  histogram_tester.ExpectTotalCount(
+      CellularMetricsLogger::kSimPinChangeSuccessHistogram, 2);
+  histogram_tester.ExpectBucketCount(
+      CellularMetricsLogger::kSimPinChangeSuccessHistogram,
+      CellularMetricsLogger::SimPinOperationResult::kErrorUnknown, 1);
 }
 
 TEST_F(NetworkDeviceHandlerTest, AddWifiWakeOnPacketOfTypes) {
diff --git a/chromeos/profiles/atom.afdo.newest.txt b/chromeos/profiles/atom.afdo.newest.txt
index 10d21ca2..3297fe5 100644
--- a/chromeos/profiles/atom.afdo.newest.txt
+++ b/chromeos/profiles/atom.afdo.newest.txt
@@ -1 +1 @@
-chromeos-chrome-amd64-atom-90-4405.2-1614595422-benchmark-91.0.4435.0-r1-redacted.afdo.xz
+chromeos-chrome-amd64-atom-91-4430.19-1615803233-benchmark-91.0.4448.0-r1-redacted.afdo.xz
diff --git a/chromeos/profiles/bigcore.afdo.newest.txt b/chromeos/profiles/bigcore.afdo.newest.txt
index 27ce6fa..c48f52a 100644
--- a/chromeos/profiles/bigcore.afdo.newest.txt
+++ b/chromeos/profiles/bigcore.afdo.newest.txt
@@ -1 +1 @@
-chromeos-chrome-amd64-bigcore-90-4405.2-1614599389-benchmark-91.0.4435.0-r1-redacted.afdo.xz
+chromeos-chrome-amd64-bigcore-91-4430.19-1615806685-benchmark-91.0.4448.0-r1-redacted.afdo.xz
diff --git a/chromeos/services/assistant/assistant_manager_service_impl.cc b/chromeos/services/assistant/assistant_manager_service_impl.cc
index ac8dc5e..615953c 100644
--- a/chromeos/services/assistant/assistant_manager_service_impl.cc
+++ b/chromeos/services/assistant/assistant_manager_service_impl.cc
@@ -269,7 +269,6 @@
 
   media_host_->Stop();
   scoped_app_list_event_subscriber_.Reset();
-  scoped_action_observer_.Reset();
 
   // When user disables the feature, we also delete all data.
   if (!assistant_state()->settings_enabled().value())
@@ -499,32 +498,6 @@
   receive_url_response_ = url.spec();
 }
 
-void AssistantManagerServiceImpl::OnVerifyAndroidApp(
-    const std::vector<AndroidAppInfo>& apps_info,
-    const InteractionInfo& interaction) {
-  ENSURE_MAIN_THREAD(&AssistantManagerServiceImpl::OnVerifyAndroidApp,
-                     apps_info, interaction);
-  std::vector<AndroidAppInfo> result_apps_info;
-  for (auto& app_info : apps_info) {
-    AndroidAppInfo result_app_info(app_info);
-    AppStatus status = device_actions()->GetAndroidAppStatus(app_info);
-    result_app_info.status = status;
-    result_apps_info.emplace_back(result_app_info);
-  }
-  std::string interaction_proto = CreateVerifyProviderResponseInteraction(
-      interaction.interaction_id, result_apps_info);
-
-  assistant_client::VoicelessOptions options;
-  options.obfuscated_gaia_id = interaction.user_id;
-  // Set the request to be user initiated so that a new conversation will be
-  // created to handle the client OPs in the response of this request.
-  options.is_user_initiated = true;
-
-  assistant_manager_internal()->SendVoicelessInteraction(
-      interaction_proto, /*description=*/"verify_provider_response", options,
-      [](auto) {});
-}
-
 void AssistantManagerServiceImpl::OnStateChanged(
     chromeos::libassistant::mojom::ServiceState new_state) {
   using chromeos::libassistant::mojom::ServiceState;
@@ -572,8 +545,6 @@
 
   if (base::FeatureList::IsEnabled(assistant::features::kAssistantAppSupport))
     scoped_app_list_event_subscriber_.Observe(device_actions());
-
-  scoped_action_observer_.Observe(action_module());
 }
 
 bool AssistantManagerServiceImpl::IsServiceStarted() const {
diff --git a/chromeos/services/assistant/assistant_manager_service_impl.h b/chromeos/services/assistant/assistant_manager_service_impl.h
index 42e16dc..5dad430 100644
--- a/chromeos/services/assistant/assistant_manager_service_impl.h
+++ b/chromeos/services/assistant/assistant_manager_service_impl.h
@@ -18,7 +18,6 @@
 #include "base/scoped_observation.h"
 #include "base/synchronization/lock.h"
 #include "base/threading/thread.h"
-#include "chromeos/assistant/internal/action/assistant_action_observer.h"
 #include "chromeos/assistant/internal/action/cros_action_module.h"
 #include "chromeos/services/assistant/assistant_manager_service.h"
 #include "chromeos/services/assistant/assistant_settings_impl.h"
@@ -99,7 +98,6 @@
 // enabled/disabled in settings or switches to a non-primary profile.
 class COMPONENT_EXPORT(ASSISTANT_SERVICE) AssistantManagerServiceImpl
     : public AssistantManagerService,
-      public chromeos::assistant::action::AssistantActionObserver,
       public assistant_client::ConversationStateListener,
       public AppListEventSubscriber,
       private chromeos::libassistant::mojom::StateObserver,
@@ -168,10 +166,6 @@
   mojo::PendingReceiver<chromeos::libassistant::mojom::NotificationDelegate>
   GetPendingNotificationDelegate() override;
 
-  // AssistantActionObserver overrides:
-  void OnVerifyAndroidApp(const std::vector<AndroidAppInfo>& apps_info,
-                          const InteractionInfo& interaction) override;
-
   // chromeos::assistant::ConversationObserver overrides:
   void OnInteractionStarted(
       const AssistantInteractionMetadata& metadata) override;
@@ -295,9 +289,6 @@
                           &DeviceActions::RemoveAppListEventSubscriber>
       scoped_app_list_event_subscriber_{this};
   base::ObserverList<AssistantManagerService::StateObserver> state_observers_;
-  base::ScopedObservation<action::CrosActionModule,
-                          action::AssistantActionObserver>
-      scoped_action_observer_{this};
 
   base::WeakPtrFactory<AssistantManagerServiceImpl> weak_factory_;
 
diff --git a/chromeos/services/cellular_setup/esim_test_base.cc b/chromeos/services/cellular_setup/esim_test_base.cc
index 34e48fcd..bcbe579 100644
--- a/chromeos/services/cellular_setup/esim_test_base.cc
+++ b/chromeos/services/cellular_setup/esim_test_base.cc
@@ -29,7 +29,8 @@
 const char* ESimTestBase::kTestEuiccPath = "/org/chromium/Hermes/Euicc/0";
 const char* ESimTestBase::kTestEid = "12345678901234567890123456789012";
 
-ESimTestBase::ESimTestBase() {
+ESimTestBase::ESimTestBase()
+    : task_environment_(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {
   if (!ShillManagerClient::Get())
     shill_clients::InitializeFakes();
   if (!HermesManagerClient::Get())
diff --git a/chromeos/services/cellular_setup/esim_test_base.h b/chromeos/services/cellular_setup/esim_test_base.h
index c0cd307e..7e0e6d3 100644
--- a/chromeos/services/cellular_setup/esim_test_base.h
+++ b/chromeos/services/cellular_setup/esim_test_base.h
@@ -66,6 +66,8 @@
     return network_state_handler_.get();
   }
 
+  base::test::TaskEnvironment* task_environment() { return &task_environment_; }
+
  private:
   std::unique_ptr<NetworkStateHandler> network_state_handler_;
   std::unique_ptr<NetworkDeviceHandler> network_device_handler_;
diff --git a/chromeos/services/cellular_setup/euicc.cc b/chromeos/services/cellular_setup/euicc.cc
index 2abf100..af9a33a0 100644
--- a/chromeos/services/cellular_setup/euicc.cc
+++ b/chromeos/services/cellular_setup/euicc.cc
@@ -7,8 +7,10 @@
 #include <cstdint>
 #include <memory>
 
+#include "base/metrics/histogram_macros.h"
 #include "base/optional.h"
 #include "base/strings/strcat.h"
+#include "base/time/time.h"
 #include "chromeos/network/cellular_esim_connection_handler.h"
 #include "chromeos/network/cellular_esim_profile.h"
 #include "chromeos/network/cellular_inhibitor.h"
@@ -23,15 +25,35 @@
 #include "dbus/object_path.h"
 #include "mojo/public/cpp/bindings/pending_remote.h"
 
+namespace chromeos {
+namespace cellular_setup {
 namespace {
 
 // Prefix for EID when encoded in QR Code.
 const char kEidQrCodePrefix[] = "EID:";
 
-}  // namespace
+// Measures the time from which this function is called to when |callback|
+// is expected to run. The measured time difference should capture the time it
+// took for a profile to be fully downloaded from a provided activation code.
+Euicc::InstallProfileFromActivationCodeCallback
+CreateTimedInstallProfileCallback(
+    Euicc::InstallProfileFromActivationCodeCallback callback) {
+  return base::BindOnce(
+      [](Euicc::InstallProfileFromActivationCodeCallback callback,
+         base::Time installation_start_time, mojom::ProfileInstallResult result,
+         mojo::PendingRemote<mojom::ESimProfile> esim_profile_pending_remote)
+          -> void {
+        std::move(callback).Run(result, std::move(esim_profile_pending_remote));
+        if (result != mojom::ProfileInstallResult::kSuccess)
+          return;
+        UMA_HISTOGRAM_MEDIUM_TIMES(
+            "Network.Cellular.ESim.ProfileDownload.ActivationCode.Latency",
+            base::Time::Now() - installation_start_time);
+      },
+      std::move(callback), base::Time::Now());
+}
 
-namespace chromeos {
-namespace cellular_setup {
+}  // namespace
 
 Euicc::Euicc(const dbus::ObjectPath& path, ESimManager* esim_manager)
     : esim_manager_(esim_manager),
@@ -89,10 +111,10 @@
   // currently being installed to prevent multiple attempts for the same
   // activation code.
   NET_LOG(USER) << "Attempting installation with code " << activation_code;
-  esim_manager_->cellular_inhibitor()->InhibitCellularScanning(
-      base::BindOnce(&Euicc::PerformInstallProfileFromActivationCode,
-                     weak_ptr_factory_.GetWeakPtr(), activation_code,
-                     confirmation_code, std::move(callback)));
+  esim_manager_->cellular_inhibitor()->InhibitCellularScanning(base::BindOnce(
+      &Euicc::PerformInstallProfileFromActivationCode,
+      weak_ptr_factory_.GetWeakPtr(), activation_code, confirmation_code,
+      CreateTimedInstallProfileCallback(std::move(callback))));
 }
 
 void Euicc::RequestPendingProfiles(RequestPendingProfilesCallback callback) {
diff --git a/chromeos/services/cellular_setup/euicc_unittest.cc b/chromeos/services/cellular_setup/euicc_unittest.cc
index 8c1e9c9..a817f58 100644
--- a/chromeos/services/cellular_setup/euicc_unittest.cc
+++ b/chromeos/services/cellular_setup/euicc_unittest.cc
@@ -6,6 +6,7 @@
 
 #include "base/run_loop.h"
 #include "base/test/bind.h"
+#include "base/test/metrics/histogram_tester.h"
 #include "chromeos/dbus/hermes/hermes_euicc_client.h"
 #include "chromeos/dbus/hermes/hermes_profile_client.h"
 #include "chromeos/network/fake_network_connection_handler.h"
@@ -56,7 +57,8 @@
       const std::string& activation_code,
       const std::string& confirmation_code,
       bool wait_for_connect,
-      bool fail_connect) {
+      bool fail_connect,
+      const uint64_t& connect_latency_in_ms = 0) {
     mojom::ProfileInstallResult out_install_result;
     mojo::PendingRemote<mojom::ESimProfile> out_esim_profile;
 
@@ -73,6 +75,8 @@
 
     if (wait_for_connect) {
       base::RunLoop().RunUntilIdle();
+      task_environment()->AdvanceClock(
+          base::TimeDelta::FromMilliseconds(connect_latency_in_ms));
       EXPECT_LE(1u, network_connection_handler()->connect_calls().size());
       if (fail_connect) {
         network_connection_handler()
@@ -146,13 +150,23 @@
   EXPECT_EQ(mojom::ProfileInstallResult::kFailure, result_pair.first);
   ASSERT_FALSE(result_pair.second.is_valid());
 
+  base::HistogramTester histogram_tester;
+
   // Verify that install succeeds when valid activation code is passed.
+  const uint64_t connect_latency_in_ms = 3000;
   result_pair = InstallProfileFromActivationCode(
       euicc, euicc_test->GenerateFakeActivationCode(),
       /*confirmation_code=*/std::string(), /*wait_for_connect=*/true,
-      /*fail_connect=*/false);
+      /*fail_connect=*/false, connect_latency_in_ms);
   EXPECT_EQ(mojom::ProfileInstallResult::kSuccess, result_pair.first);
   ASSERT_TRUE(result_pair.second.is_valid());
+
+  histogram_tester.ExpectTimeBucketCount(
+      "Network.Cellular.ESim.ProfileDownload.ActivationCode.Latency",
+      base::TimeDelta::FromMilliseconds(connect_latency_in_ms), 1);
+
+  histogram_tester.ExpectTotalCount(
+      "Network.Cellular.ESim.ProfileDownload.ActivationCode.Latency", 1);
 }
 
 TEST_F(EuiccTest, InstallPendingProfileFromActivationCode) {
diff --git a/chromeos/services/libassistant/conversation_controller.cc b/chromeos/services/libassistant/conversation_controller.cc
index 3f588213..7cbcbefe8 100644
--- a/chromeos/services/libassistant/conversation_controller.cc
+++ b/chromeos/services/libassistant/conversation_controller.cc
@@ -398,6 +398,9 @@
 }
 
 // Called from Libassistant thread.
+// Note that OnVerifyAndroidApp() will be handled by |DisplayController|
+// directly since it stores an updated list of all installed Android Apps on the
+// device.
 void ConversationController::OnOpenAndroidApp(
     const chromeos::assistant::AndroidAppInfo& app_info,
     const chromeos::assistant::InteractionInfo& interaction) {
diff --git a/chromeos/services/libassistant/display_connection_impl.h b/chromeos/services/libassistant/display_connection_impl.h
index 4d24de5c..8b8737b2 100644
--- a/chromeos/services/libassistant/display_connection_impl.h
+++ b/chromeos/services/libassistant/display_connection_impl.h
@@ -42,6 +42,10 @@
   void OnAndroidAppListRefreshed(
       const std::vector<assistant::AndroidAppInfo>& apps_info);
 
+  const std::vector<assistant::AndroidAppInfo>& GetCachedAndroidAppList() {
+    return apps_info_;
+  }
+
  private:
   void SendDisplayRequestLocked();
 
diff --git a/chromeos/services/libassistant/display_controller.cc b/chromeos/services/libassistant/display_controller.cc
index fb1ce4a..cb765b4 100644
--- a/chromeos/services/libassistant/display_controller.cc
+++ b/chromeos/services/libassistant/display_controller.cc
@@ -6,6 +6,7 @@
 
 #include <memory>
 
+#include "chromeos/assistant/internal/internal_util.h"
 #include "chromeos/services/assistant/public/cpp/features.h"
 #include "chromeos/services/libassistant/display_connection_impl.h"
 #include "chromeos/services/libassistant/public/mojom/speech_recognition_observer.mojom.h"
@@ -14,6 +15,17 @@
 namespace chromeos {
 namespace libassistant {
 
+namespace {
+// A macro which ensures we are running on the main thread.
+#define ENSURE_MOJOM_THREAD(method, ...)                                    \
+  if (!mojom_task_runner_->RunsTasksInCurrentSequence()) {                  \
+    mojom_task_runner_->PostTask(                                           \
+        FROM_HERE,                                                          \
+        base::BindOnce(method, weak_factory_.GetWeakPtr(), ##__VA_ARGS__)); \
+    return;                                                                 \
+  }
+}  // namespace
+
 class DisplayController::EventObserver : public DisplayConnectionObserver {
  public:
   explicit EventObserver(DisplayController* parent) : parent_(parent) {}
@@ -69,7 +81,60 @@
 void DisplayController::OnAssistantManagerCreated(
     assistant_client::AssistantManager* assistant_manager,
     assistant_client::AssistantManagerInternal* assistant_manager_internal) {
+  DCHECK(assistant_manager_internal);
   assistant_manager_internal->SetDisplayConnection(display_connection_.get());
+
+  assistant_manager_internal_ = assistant_manager_internal;
+}
+
+void DisplayController::OnDestroyingAssistantManager(
+    assistant_client::AssistantManager* assistant_manager,
+    assistant_client::AssistantManagerInternal* assistant_manager_internal) {
+  assistant_manager_internal_ = nullptr;
+}
+
+// Called from Libassistant thread.
+void DisplayController::OnVerifyAndroidApp(
+    const std::vector<chromeos::assistant::AndroidAppInfo>& apps_info,
+    const chromeos::assistant::InteractionInfo& interaction) {
+  ENSURE_MOJOM_THREAD(&DisplayController::OnVerifyAndroidApp, apps_info,
+                      interaction);
+
+  std::vector<chromeos::assistant::AndroidAppInfo> result_apps_info;
+  for (auto& app_info : apps_info) {
+    chromeos::assistant::AndroidAppInfo result_app_info(app_info);
+    auto app_status = GetAndroidAppStatus(app_info.package_name);
+    result_app_info.status = app_status;
+    result_apps_info.emplace_back(result_app_info);
+  }
+
+  std::string interaction_proto = CreateVerifyProviderResponseInteraction(
+      interaction.interaction_id, result_apps_info);
+
+  assistant_client::VoicelessOptions options;
+  options.obfuscated_gaia_id = interaction.user_id;
+  // Set the request to be user initiated so that a new conversation will be
+  // created to handle the client OPs in the response of this request.
+  options.is_user_initiated = true;
+
+  assistant_manager_internal_->SendVoicelessInteraction(
+      interaction_proto, /*description=*/"verify_provider_response", options,
+      [](auto) {});
+}
+
+chromeos::assistant::AppStatus DisplayController::GetAndroidAppStatus(
+    const std::string& package_name) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  for (auto& app_info : display_connection_->GetCachedAndroidAppList()) {
+    if (app_info.package_name == package_name) {
+      DVLOG(1) << "Assistant: App is available on the device.";
+      return assistant::AppStatus::kAvailable;
+    }
+  }
+
+  DVLOG(1) << "Assistant: App is unavailable on the device";
+  return assistant::AppStatus::kUnavailable;
 }
 
 }  // namespace libassistant
diff --git a/chromeos/services/libassistant/display_controller.h b/chromeos/services/libassistant/display_controller.h
index e796183..581105e2 100644
--- a/chromeos/services/libassistant/display_controller.h
+++ b/chromeos/services/libassistant/display_controller.h
@@ -5,8 +5,11 @@
 #ifndef CHROMEOS_SERVICES_LIBASSISTANT_DISPLAY_CONTROLLER_H_
 #define CHROMEOS_SERVICES_LIBASSISTANT_DISPLAY_CONTROLLER_H_
 
+#include "base/sequenced_task_runner.h"
+#include "chromeos/assistant/internal/action/assistant_action_observer.h"
 #include "chromeos/services/libassistant/assistant_manager_observer.h"
 #include "chromeos/services/libassistant/public/mojom/display_controller.mojom.h"
+#include "libassistant/shared/internal_api/assistant_manager_internal.h"
 #include "mojo/public/cpp/bindings/receiver.h"
 #include "mojo/public/cpp/bindings/remote_set.h"
 
@@ -23,8 +26,10 @@
 
 class DisplayConnectionImpl;
 
-class DisplayController : public mojom::DisplayController,
-                          public AssistantManagerObserver {
+class DisplayController
+    : public mojom::DisplayController,
+      public AssistantManagerObserver,
+      public chromeos::assistant::action::AssistantActionObserver {
  public:
   explicit DisplayController(mojo::RemoteSet<mojom::SpeechRecognitionObserver>*
                                  speech_recognition_observers);
@@ -46,10 +51,24 @@
       assistant_client::AssistantManager* assistant_manager,
       assistant_client::AssistantManagerInternal* assistant_manager_internal)
       override;
+  void OnDestroyingAssistantManager(
+      assistant_client::AssistantManager* assistant_manager,
+      assistant_client::AssistantManagerInternal* assistant_manager_internal)
+      override;
+
+  // chromeos::assistant::action::AssistantActionObserver:
+  void OnVerifyAndroidApp(
+      const std::vector<chromeos::assistant::AndroidAppInfo>& apps_info,
+      const chromeos::assistant::InteractionInfo& interaction) override;
 
  private:
   class EventObserver;
 
+  // Checks if the requested Android App with |package_name| is available on the
+  // device.
+  chromeos::assistant::AppStatus GetAndroidAppStatus(
+      const std::string& package_name);
+
   mojo::Receiver<mojom::DisplayController> receiver_{this};
   std::unique_ptr<EventObserver> event_observer_;
   std::unique_ptr<DisplayConnectionImpl> display_connection_;
@@ -57,6 +76,17 @@
   // Owned by |LibassistantService|.
   mojo::RemoteSet<mojom::SpeechRecognitionObserver>&
       speech_recognition_observers_;
+
+  assistant_client::AssistantManagerInternal* assistant_manager_internal_ =
+      nullptr;
+
+  // The callbacks from Libassistant are called on a different sequence,
+  // so this sequence checker ensures that no other methods are called on the
+  // libassistant sequence.
+  SEQUENCE_CHECKER(sequence_checker_);
+
+  scoped_refptr<base::SequencedTaskRunner> mojom_task_runner_;
+  base::WeakPtrFactory<DisplayController> weak_factory_{this};
 };
 
 }  // namespace libassistant
diff --git a/chromeos/services/libassistant/libassistant_service.cc b/chromeos/services/libassistant/libassistant_service.cc
index d18852f..0a3a466f 100644
--- a/chromeos/services/libassistant/libassistant_service.cc
+++ b/chromeos/services/libassistant/libassistant_service.cc
@@ -83,6 +83,7 @@
   service_controller_.AddAndFireAssistantManagerObserver(&timer_controller_);
 
   conversation_controller_.AddActionObserver(&device_settings_controller_);
+  conversation_controller_.AddActionObserver(&display_controller_);
   platform_api_.SetAudioInputProvider(
       &audio_input_controller_.audio_input_provider());
 }
diff --git a/components/browser_ui/strings/android/browser_ui_strings.grd b/components/browser_ui/strings/android/browser_ui_strings.grd
index 14b768f..ac88a5de 100644
--- a/components/browser_ui/strings/android/browser_ui_strings.grd
+++ b/components/browser_ui/strings/android/browser_ui_strings.grd
@@ -300,9 +300,6 @@
       <message name="IDS_MENU_ITEM_MOVE_TO_TOP" desc="Option in item menu. User can click the 'Move to top' option to move the item up to the top of its list. [CHAR-LIMIT=24]">
         Move to top
       </message>
-      <message name="IDS_ALL" desc="Generic label for a button to show all items or options, for example a button to view all bookmarks. [CHAR-LIMIT=20]">
-        All
-      </message>
       <message name="IDS_JUST_ONCE" desc="Generic label for a user to select an option for just once">
         Just once
       </message>
diff --git a/components/browser_ui/strings/android/browser_ui_strings_grd/IDS_ALL.png.sha1 b/components/browser_ui/strings/android/browser_ui_strings_grd/IDS_ALL.png.sha1
deleted file mode 100644
index dbf500d..0000000
--- a/components/browser_ui/strings/android/browser_ui_strings_grd/IDS_ALL.png.sha1
+++ /dev/null
@@ -1 +0,0 @@
-6c3e2edf3a560e4fa08cdffdd9bbc0567b4197de
\ No newline at end of file
diff --git a/components/component_updater/android/BUILD.gn b/components/component_updater/android/BUILD.gn
index de9747bc..15ff397 100644
--- a/components/component_updater/android/BUILD.gn
+++ b/components/component_updater/android/BUILD.gn
@@ -70,6 +70,7 @@
   sources = [
     "component_loader_policy.cc",
     "component_loader_policy.h",
+    "component_loader_policy_forward.h",
   ]
   deps = [
     ":embedded_component_loader_jni_headers",
diff --git a/components/component_updater/android/component_loader_policy.cc b/components/component_updater/android/component_loader_policy.cc
index e647649..b4b557c 100644
--- a/components/component_updater/android/component_loader_policy.cc
+++ b/components/component_updater/android/component_loader_policy.cc
@@ -33,6 +33,7 @@
 #include "base/task/thread_pool.h"
 #include "base/values.h"
 #include "base/version.h"
+#include "components/component_updater/android/component_loader_policy_forward.h"
 #include "components/component_updater/android/embedded_component_loader_jni_headers/ComponentLoaderPolicyBridge_jni.h"
 #include "components/update_client/utils.h"
 
diff --git a/components/component_updater/android/component_loader_policy.h b/components/component_updater/android/component_loader_policy.h
index f897f50..e032a35 100644
--- a/components/component_updater/android/component_loader_policy.h
+++ b/components/component_updater/android/component_loader_policy.h
@@ -16,6 +16,7 @@
 #include "base/containers/flat_map.h"
 #include "base/memory/scoped_refptr.h"
 #include "base/sequence_checker.h"
+#include "components/component_updater/android/component_loader_policy_forward.h"
 
 namespace base {
 class Version;
@@ -74,9 +75,6 @@
   virtual void GetHash(std::vector<uint8_t>* hash) const = 0;
 };
 
-using ComponentLoaderPolicyVector =
-    std::vector<std::unique_ptr<ComponentLoaderPolicy>>;
-
 // Provides a bridge from Java to native to receive callbacks from the Java
 // loader and pass it to the wrapped ComponentLoaderPolicy instance.
 //
diff --git a/components/component_updater/android/component_loader_policy_forward.h b/components/component_updater/android/component_loader_policy_forward.h
new file mode 100644
index 0000000..f6f8753
--- /dev/null
+++ b/components/component_updater/android/component_loader_policy_forward.h
@@ -0,0 +1,19 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_COMPONENT_UPDATER_ANDROID_COMPONENT_LOADER_POLICY_FORWARD_H_
+#define COMPONENTS_COMPONENT_UPDATER_ANDROID_COMPONENT_LOADER_POLICY_FORWARD_H_
+
+#include <memory>
+#include <vector>
+
+namespace component_updater {
+class ComponentLoaderPolicy;
+
+using ComponentLoaderPolicyVector =
+    std::vector<std::unique_ptr<ComponentLoaderPolicy>>;
+
+}  // namespace component_updater
+
+#endif  // COMPONENTS_COMPONENT_UPDATER_ANDROID_COMPONENT_LOADER_POLICY_FORWARD_H_
diff --git a/components/component_updater/installer_policies/origin_trials_component_installer.cc b/components/component_updater/installer_policies/origin_trials_component_installer.cc
index 713c2e3..6122093 100644
--- a/components/component_updater/installer_policies/origin_trials_component_installer.cc
+++ b/components/component_updater/installer_policies/origin_trials_component_installer.cc
@@ -4,7 +4,9 @@
 
 #include "components/component_updater/installer_policies/origin_trials_component_installer.h"
 
+#include <iterator>
 #include <utility>
+#include <vector>
 
 #include "base/bind.h"
 #include "base/callback.h"
@@ -48,6 +50,20 @@
 
 }  // namespace
 
+// static
+void OriginTrialsComponentInstallerPolicy::GetComponentHash(
+    std::vector<uint8_t>* hash) {
+  if (!hash)
+    return;
+  hash->assign(std::begin(kOriginTrialSha2Hash),
+               std::end(kOriginTrialSha2Hash));
+}
+
+void OriginTrialsComponentInstallerPolicy::GetHash(
+    std::vector<uint8_t>* hash) const {
+  GetComponentHash(hash);
+}
+
 bool OriginTrialsComponentInstallerPolicy::VerifyInstallation(
     const base::DictionaryValue& manifest,
     const base::FilePath& install_dir) const {
@@ -83,14 +99,6 @@
   return base::FilePath(FILE_PATH_LITERAL("OriginTrials"));
 }
 
-void OriginTrialsComponentInstallerPolicy::GetHash(
-    std::vector<uint8_t>* hash) const {
-  if (!hash)
-    return;
-  hash->assign(kOriginTrialSha2Hash,
-               kOriginTrialSha2Hash + base::size(kOriginTrialSha2Hash));
-}
-
 std::string OriginTrialsComponentInstallerPolicy::GetName() const {
   return "Origin Trials";
 }
diff --git a/components/component_updater/installer_policies/origin_trials_component_installer.h b/components/component_updater/installer_policies/origin_trials_component_installer.h
index 9e81f42..1d682b8 100644
--- a/components/component_updater/installer_policies/origin_trials_component_installer.h
+++ b/components/component_updater/installer_policies/origin_trials_component_installer.h
@@ -21,6 +21,7 @@
 
 class OriginTrialsComponentInstallerPolicy : public ComponentInstallerPolicy {
  public:
+  static void GetComponentHash(std::vector<uint8_t>* hash);
   OriginTrialsComponentInstallerPolicy() = default;
   ~OriginTrialsComponentInstallerPolicy() override = default;
   OriginTrialsComponentInstallerPolicy(
diff --git a/components/embedder_support/BUILD.gn b/components/embedder_support/BUILD.gn
index d98e6eb..c78282a 100644
--- a/components/embedder_support/BUILD.gn
+++ b/components/embedder_support/BUILD.gn
@@ -32,6 +32,7 @@
 source_set("unit_tests") {
   testonly = true
   sources = [
+    "origin_trials/component_updater_utils_unittest.cc",
     "origin_trials/origin_trial_policy_impl_unittest.cc",
     "user_agent_utils_unittest.cc",
   ]
@@ -40,8 +41,11 @@
     ":browser_util",
     "//base",
     "//base/test:test_support",
+    "//components/component_updater/installer_policies",
     "//components/embedder_support",
     "//components/embedder_support/origin_trials",
+    "//components/prefs",
+    "//components/prefs:test_support",
     "//components/version_info:version_info",
     "//content/public/common",
     "//mojo/core/embedder:embedder",
diff --git a/components/embedder_support/origin_trials/BUILD.gn b/components/embedder_support/origin_trials/BUILD.gn
index 37c2d578..eac5ee1 100644
--- a/components/embedder_support/origin_trials/BUILD.gn
+++ b/components/embedder_support/origin_trials/BUILD.gn
@@ -4,16 +4,21 @@
 
 source_set("origin_trials") {
   sources = [
+    "component_updater_utils.cc",
+    "component_updater_utils.h",
     "features.cc",
     "features.h",
     "origin_trial_policy_impl.cc",
     "origin_trial_policy_impl.h",
+    "origin_trial_prefs.cc",
+    "origin_trial_prefs.h",
     "pref_names.cc",
     "pref_names.h",
   ]
   deps = [
     "//base",
     "//components/embedder_support",
+    "//components/prefs",
     "//content/public/common",
     "//third_party/blink/public/common",
   ]
diff --git a/components/embedder_support/origin_trials/DEPS b/components/embedder_support/origin_trials/DEPS
index 1235202..f453b7d3 100644
--- a/components/embedder_support/origin_trials/DEPS
+++ b/components/embedder_support/origin_trials/DEPS
@@ -1,4 +1,6 @@
 include_rules = [
+  "+components/component_updater/installer_policies",
+  "+components/prefs",
   "+content/public/common",
   "+third_party/blink/public/common",
   "+services/network/public/cpp/is_potentially_trustworthy.h",
diff --git a/components/embedder_support/origin_trials/component_updater_utils.cc b/components/embedder_support/origin_trials/component_updater_utils.cc
new file mode 100644
index 0000000..5ef281e
--- /dev/null
+++ b/components/embedder_support/origin_trials/component_updater_utils.cc
@@ -0,0 +1,61 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/embedder_support/origin_trials/component_updater_utils.h"
+
+#include <memory>
+#include <string>
+
+#include "base/check.h"
+#include "base/values.h"
+#include "components/embedder_support/origin_trials/pref_names.h"
+#include "components/prefs/pref_service.h"
+#include "components/prefs/scoped_user_pref_update.h"
+
+namespace {
+
+constexpr char kManifestPublicKeyPath[] = "origin-trials.public-key";
+constexpr char kManifestDisabledFeaturesPath[] =
+    "origin-trials.disabled-features";
+constexpr char kManifestDisabledTokenSignaturesPath[] =
+    "origin-trials.disabled-tokens.signatures";
+
+}  // namespace
+
+namespace embedder_support {
+
+void ReadOriginTrialsConfigAndPopulateLocalState(
+    PrefService* local_state,
+    std::unique_ptr<base::DictionaryValue> manifest) {
+  DCHECK(local_state);
+
+  std::string override_public_key;
+  if (manifest->GetString(kManifestPublicKeyPath, &override_public_key)) {
+    local_state->Set(prefs::kOriginTrialPublicKey,
+                     base::Value(override_public_key));
+  } else {
+    local_state->ClearPref(prefs::kOriginTrialPublicKey);
+  }
+  base::ListValue* override_disabled_feature_list = nullptr;
+  const bool manifest_has_disabled_features = manifest->GetList(
+      kManifestDisabledFeaturesPath, &override_disabled_feature_list);
+  if (manifest_has_disabled_features &&
+      !override_disabled_feature_list->empty()) {
+    ListPrefUpdate update(local_state, prefs::kOriginTrialDisabledFeatures);
+    update->Swap(override_disabled_feature_list);
+  } else {
+    local_state->ClearPref(prefs::kOriginTrialDisabledFeatures);
+  }
+  base::ListValue* disabled_tokens_list = nullptr;
+  const bool manifest_has_disabled_tokens = manifest->GetList(
+      kManifestDisabledTokenSignaturesPath, &disabled_tokens_list);
+  if (manifest_has_disabled_tokens && !disabled_tokens_list->empty()) {
+    ListPrefUpdate update(local_state, prefs::kOriginTrialDisabledTokens);
+    update->Swap(disabled_tokens_list);
+  } else {
+    local_state->ClearPref(prefs::kOriginTrialDisabledTokens);
+  }
+}
+
+}  // namespace embedder_support
diff --git a/components/embedder_support/origin_trials/component_updater_utils.h b/components/embedder_support/origin_trials/component_updater_utils.h
new file mode 100644
index 0000000..957fc77
--- /dev/null
+++ b/components/embedder_support/origin_trials/component_updater_utils.h
@@ -0,0 +1,25 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_EMBEDDER_SUPPORT_ORIGIN_TRIALS_COMPONENT_UPDATER_UTILS_H_
+#define COMPONENTS_EMBEDDER_SUPPORT_ORIGIN_TRIALS_COMPONENT_UPDATER_UTILS_H_
+
+#include <memory>
+
+#include "base/values.h"
+
+class PrefService;
+
+namespace embedder_support {
+
+// Read the configuration from |manifest| and set values in |local_state|.
+// If an individual configuration value is missing, reset values in
+// local_state|.
+void ReadOriginTrialsConfigAndPopulateLocalState(
+    PrefService* local_state,
+    std::unique_ptr<base::DictionaryValue> manifest);
+
+}  // namespace embedder_support
+
+#endif  // COMPONENTS_EMBEDDER_SUPPORT_ORIGIN_TRIALS_COMPONENT_UPDATER_UTILS_H_
diff --git a/components/embedder_support/origin_trials/component_updater_utils_unittest.cc b/components/embedder_support/origin_trials/component_updater_utils_unittest.cc
new file mode 100644
index 0000000..e18e6a8
--- /dev/null
+++ b/components/embedder_support/origin_trials/component_updater_utils_unittest.cc
@@ -0,0 +1,323 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/embedder_support/origin_trials/component_updater_utils.h"
+
+#include <string>
+#include <utility>
+
+#include "base/files/scoped_temp_dir.h"
+#include "base/values.h"
+#include "base/version.h"
+#include "components/component_updater/installer_policies/origin_trials_component_installer.h"
+#include "components/embedder_support/origin_trials/origin_trial_prefs.h"
+#include "components/embedder_support/origin_trials/pref_names.h"
+#include "components/prefs/scoped_user_pref_update.h"
+#include "components/prefs/testing_pref_service.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+namespace {
+
+// Mirror the constants used in the component installer. Do not share the
+// constants, as want to catch inadvertent changes in the tests. The keys will
+// will be generated server-side, so any changes need to be intentional and
+// coordinated.
+static const char kManifestOriginTrialsKey[] = "origin-trials";
+static const char kManifestPublicKeyPath[] = "origin-trials.public-key";
+static const char kManifestDisabledFeaturesPath[] =
+    "origin-trials.disabled-features";
+static const char kManifestDisabledTokensPath[] =
+    "origin-trials.disabled-tokens";
+static const char kManifestDisabledTokenSignaturesPath[] =
+    "origin-trials.disabled-tokens.signatures";
+
+static const char kExistingPublicKey[] = "existing public key";
+static const char kNewPublicKey[] = "new public key";
+static const char kExistingDisabledFeature[] = "already disabled";
+static const std::vector<std::string> kExistingDisabledFeatures = {
+    kExistingDisabledFeature};
+static const char kNewDisabledFeature1[] = "newly disabled 1";
+static const char kNewDisabledFeature2[] = "newly disabled 2";
+static const std::vector<std::string> kNewDisabledFeatures = {
+    kNewDisabledFeature1, kNewDisabledFeature2};
+static const char kExistingDisabledToken[] = "already disabled token";
+static const std::vector<std::string> kExistingDisabledTokens = {
+    kExistingDisabledToken};
+static const char kNewDisabledToken1[] = "newly disabled token 1";
+static const char kNewDisabledToken2[] = "newly disabled token 2";
+static const std::vector<std::string> kNewDisabledTokens = {kNewDisabledToken1,
+                                                            kNewDisabledToken2};
+
+}  // namespace
+
+namespace component_updater {
+
+class OriginTrialsComponentInstallerTest : public PlatformTest {
+ public:
+  OriginTrialsComponentInstallerTest() = default;
+
+  void SetUp() override {
+    PlatformTest::SetUp();
+
+    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
+    embedder_support::OriginTrialPrefs::RegisterPrefs(local_state_.registry());
+    policy_ = std::make_unique<OriginTrialsComponentInstallerPolicy>();
+  }
+
+  void LoadUpdates(std::unique_ptr<base::DictionaryValue> manifest) {
+    if (!manifest) {
+      manifest = std::make_unique<base::DictionaryValue>();
+      manifest->Set(kManifestOriginTrialsKey, std::make_unique<base::Value>());
+    }
+    ASSERT_TRUE(policy_->VerifyInstallation(*manifest, temp_dir_.GetPath()));
+    embedder_support::ReadOriginTrialsConfigAndPopulateLocalState(
+        local_state(), std::move(manifest));
+  }
+
+  void AddDisabledFeaturesToPrefs(const std::vector<std::string>& features) {
+    base::ListValue disabled_feature_list;
+    disabled_feature_list.AppendStrings(features);
+    ListPrefUpdate update(
+        local_state(), embedder_support::prefs::kOriginTrialDisabledFeatures);
+    update->Swap(&disabled_feature_list);
+  }
+
+  void CheckDisabledFeaturesPrefs(const std::vector<std::string>& features) {
+    ASSERT_FALSE(features.empty());
+
+    ASSERT_TRUE(local_state()->HasPrefPath(
+        embedder_support::prefs::kOriginTrialDisabledFeatures));
+
+    const base::ListValue* disabled_feature_list = local_state()->GetList(
+        embedder_support::prefs::kOriginTrialDisabledFeatures);
+    ASSERT_TRUE(disabled_feature_list);
+
+    ASSERT_EQ(features.size(), disabled_feature_list->GetSize());
+
+    std::string disabled_feature;
+    for (size_t i = 0; i < features.size(); ++i) {
+      const bool found = disabled_feature_list->GetString(i, &disabled_feature);
+      EXPECT_TRUE(found) << "Entry not found or not a string at index " << i;
+      if (!found) {
+        continue;
+      }
+      EXPECT_EQ(features[i], disabled_feature)
+          << "Feature lists differ at index " << i;
+    }
+  }
+
+  void AddDisabledTokensToPrefs(const std::vector<std::string>& tokens) {
+    base::ListValue disabled_token_list;
+    disabled_token_list.AppendStrings(tokens);
+    ListPrefUpdate update(local_state(),
+                          embedder_support::prefs::kOriginTrialDisabledTokens);
+    update->Swap(&disabled_token_list);
+  }
+
+  void CheckDisabledTokensPrefs(const std::vector<std::string>& tokens) {
+    ASSERT_FALSE(tokens.empty());
+
+    ASSERT_TRUE(local_state()->HasPrefPath(
+        embedder_support::prefs::kOriginTrialDisabledTokens));
+
+    const base::ListValue* disabled_token_list = local_state()->GetList(
+        embedder_support::prefs::kOriginTrialDisabledTokens);
+    ASSERT_TRUE(disabled_token_list);
+
+    ASSERT_EQ(tokens.size(), disabled_token_list->GetSize());
+
+    std::string disabled_token;
+    for (size_t i = 0; i < tokens.size(); ++i) {
+      const bool found = disabled_token_list->GetString(i, &disabled_token);
+      EXPECT_TRUE(found) << "Entry not found or not a string at index " << i;
+      if (!found) {
+        continue;
+      }
+      EXPECT_EQ(tokens[i], disabled_token)
+          << "Token lists differ at index " << i;
+    }
+  }
+
+  PrefService* local_state() { return &local_state_; }
+
+ protected:
+  base::ScopedTempDir temp_dir_;
+  TestingPrefServiceSimple local_state_;
+  std::unique_ptr<ComponentInstallerPolicy> policy_;
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(OriginTrialsComponentInstallerTest);
+};
+
+TEST_F(OriginTrialsComponentInstallerTest,
+       PublicKeyResetToDefaultWhenOverrideMissing) {
+  local_state()->SetString(embedder_support::prefs::kOriginTrialPublicKey,
+                           kExistingPublicKey);
+  ASSERT_EQ(
+      kExistingPublicKey,
+      local_state()->GetString(embedder_support::prefs::kOriginTrialPublicKey));
+
+  // Load with empty section in manifest
+  LoadUpdates(nullptr);
+
+  EXPECT_FALSE(local_state()->HasPrefPath(
+      embedder_support::prefs::kOriginTrialPublicKey));
+}
+
+TEST_F(OriginTrialsComponentInstallerTest, PublicKeySetWhenOverrideExists) {
+  ASSERT_FALSE(local_state()->HasPrefPath(
+      embedder_support::prefs::kOriginTrialPublicKey));
+
+  auto manifest = std::make_unique<base::DictionaryValue>();
+  manifest->SetString(kManifestPublicKeyPath, kNewPublicKey);
+  LoadUpdates(std::move(manifest));
+
+  EXPECT_EQ(kNewPublicKey, local_state()->GetString(
+                               embedder_support::prefs::kOriginTrialPublicKey));
+}
+
+TEST_F(OriginTrialsComponentInstallerTest,
+       DisabledFeaturesResetToDefaultWhenListMissing) {
+  AddDisabledFeaturesToPrefs(kExistingDisabledFeatures);
+  ASSERT_TRUE(local_state()->HasPrefPath(
+      embedder_support::prefs::kOriginTrialDisabledFeatures));
+
+  // Load with empty section in manifest
+  LoadUpdates(nullptr);
+
+  EXPECT_FALSE(local_state()->HasPrefPath(
+      embedder_support::prefs::kOriginTrialDisabledFeatures));
+}
+
+TEST_F(OriginTrialsComponentInstallerTest,
+       DisabledFeaturesResetToDefaultWhenListEmpty) {
+  AddDisabledFeaturesToPrefs(kExistingDisabledFeatures);
+  ASSERT_TRUE(local_state()->HasPrefPath(
+      embedder_support::prefs::kOriginTrialDisabledFeatures));
+
+  auto manifest = std::make_unique<base::DictionaryValue>();
+  auto disabled_feature_list = std::make_unique<base::ListValue>();
+  manifest->Set(kManifestDisabledFeaturesPath,
+                std::move(disabled_feature_list));
+
+  LoadUpdates(std::move(manifest));
+
+  EXPECT_FALSE(local_state()->HasPrefPath(
+      embedder_support::prefs::kOriginTrialDisabledFeatures));
+}
+
+TEST_F(OriginTrialsComponentInstallerTest, DisabledFeaturesSetWhenListExists) {
+  ASSERT_FALSE(local_state()->HasPrefPath(
+      embedder_support::prefs::kOriginTrialDisabledFeatures));
+
+  auto manifest = std::make_unique<base::DictionaryValue>();
+  auto disabled_feature_list = std::make_unique<base::ListValue>();
+  disabled_feature_list->AppendString(kNewDisabledFeature1);
+  manifest->Set(kManifestDisabledFeaturesPath,
+                std::move(disabled_feature_list));
+
+  LoadUpdates(std::move(manifest));
+
+  std::vector<std::string> features = {kNewDisabledFeature1};
+  CheckDisabledFeaturesPrefs(features);
+}
+
+TEST_F(OriginTrialsComponentInstallerTest,
+       DisabledFeaturesReplacedWhenListExists) {
+  AddDisabledFeaturesToPrefs(kExistingDisabledFeatures);
+  ASSERT_TRUE(local_state()->HasPrefPath(
+      embedder_support::prefs::kOriginTrialDisabledFeatures));
+
+  auto manifest = std::make_unique<base::DictionaryValue>();
+  auto disabled_feature_list = std::make_unique<base::ListValue>();
+  disabled_feature_list->AppendStrings(kNewDisabledFeatures);
+  manifest->Set(kManifestDisabledFeaturesPath,
+                std::move(disabled_feature_list));
+
+  LoadUpdates(std::move(manifest));
+
+  CheckDisabledFeaturesPrefs(kNewDisabledFeatures);
+}
+
+TEST_F(OriginTrialsComponentInstallerTest,
+       DisabledTokensResetToDefaultWhenListMissing) {
+  AddDisabledTokensToPrefs(kExistingDisabledTokens);
+  ASSERT_TRUE(local_state()->HasPrefPath(
+      embedder_support::prefs::kOriginTrialDisabledTokens));
+
+  // Load with empty section in manifest
+  LoadUpdates(nullptr);
+
+  EXPECT_FALSE(local_state()->HasPrefPath(
+      embedder_support::prefs::kOriginTrialDisabledTokens));
+}
+
+TEST_F(OriginTrialsComponentInstallerTest,
+       DisabledTokensResetToDefaultWhenKeyExistsAndListMissing) {
+  AddDisabledTokensToPrefs(kExistingDisabledTokens);
+  ASSERT_TRUE(local_state()->HasPrefPath(
+      embedder_support::prefs::kOriginTrialDisabledTokens));
+
+  // Load with disabled tokens key in manifest, but no list values
+  auto manifest = std::make_unique<base::DictionaryValue>();
+  manifest->Set(kManifestDisabledTokensPath, std::make_unique<base::Value>());
+
+  LoadUpdates(std::move(manifest));
+
+  EXPECT_FALSE(local_state()->HasPrefPath(
+      embedder_support::prefs::kOriginTrialDisabledTokens));
+}
+
+TEST_F(OriginTrialsComponentInstallerTest,
+       DisabledTokensResetToDefaultWhenListEmpty) {
+  AddDisabledTokensToPrefs(kExistingDisabledTokens);
+  ASSERT_TRUE(local_state()->HasPrefPath(
+      embedder_support::prefs::kOriginTrialDisabledTokens));
+
+  auto manifest = std::make_unique<base::DictionaryValue>();
+  auto disabled_token_list = std::make_unique<base::ListValue>();
+  manifest->Set(kManifestDisabledTokenSignaturesPath,
+                std::move(disabled_token_list));
+
+  LoadUpdates(std::move(manifest));
+
+  EXPECT_FALSE(local_state()->HasPrefPath(
+      embedder_support::prefs::kOriginTrialDisabledTokens));
+}
+
+TEST_F(OriginTrialsComponentInstallerTest, DisabledTokensSetWhenListExists) {
+  ASSERT_FALSE(local_state()->HasPrefPath(
+      embedder_support::prefs::kOriginTrialDisabledTokens));
+
+  auto manifest = std::make_unique<base::DictionaryValue>();
+  auto disabled_token_list = std::make_unique<base::ListValue>();
+  disabled_token_list->AppendString(kNewDisabledToken1);
+  manifest->Set(kManifestDisabledTokenSignaturesPath,
+                std::move(disabled_token_list));
+
+  LoadUpdates(std::move(manifest));
+
+  std::vector<std::string> tokens = {kNewDisabledToken1};
+  CheckDisabledTokensPrefs(tokens);
+}
+
+TEST_F(OriginTrialsComponentInstallerTest,
+       DisabledTokensReplacedWhenListExists) {
+  AddDisabledTokensToPrefs(kExistingDisabledTokens);
+  ASSERT_TRUE(local_state()->HasPrefPath(
+      embedder_support::prefs::kOriginTrialDisabledTokens));
+
+  auto manifest = std::make_unique<base::DictionaryValue>();
+  auto disabled_token_list = std::make_unique<base::ListValue>();
+  disabled_token_list->AppendStrings(kNewDisabledTokens);
+  manifest->Set(kManifestDisabledTokenSignaturesPath,
+                std::move(disabled_token_list));
+
+  LoadUpdates(std::move(manifest));
+
+  CheckDisabledTokensPrefs(kNewDisabledTokens);
+}
+
+}  // namespace component_updater
diff --git a/chrome/browser/prefs/origin_trial_prefs.cc b/components/embedder_support/origin_trials/origin_trial_prefs.cc
similarity index 82%
rename from chrome/browser/prefs/origin_trial_prefs.cc
rename to components/embedder_support/origin_trials/origin_trial_prefs.cc
index e71f126..719a74a 100644
--- a/chrome/browser/prefs/origin_trial_prefs.cc
+++ b/components/embedder_support/origin_trials/origin_trial_prefs.cc
@@ -2,11 +2,13 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "chrome/browser/prefs/origin_trial_prefs.h"
+#include "components/embedder_support/origin_trials/origin_trial_prefs.h"
 
 #include "components/embedder_support/origin_trials/pref_names.h"
 #include "components/prefs/pref_registry_simple.h"
 
+namespace embedder_support {
+
 // static
 void OriginTrialPrefs::RegisterPrefs(PrefRegistrySimple* registry) {
   registry->RegisterStringPref(embedder_support::prefs::kOriginTrialPublicKey,
@@ -16,3 +18,5 @@
   registry->RegisterListPref(
       embedder_support::prefs::kOriginTrialDisabledTokens);
 }
+
+}  // namespace embedder_support
diff --git a/components/embedder_support/origin_trials/origin_trial_prefs.h b/components/embedder_support/origin_trials/origin_trial_prefs.h
new file mode 100644
index 0000000..da723445
--- /dev/null
+++ b/components/embedder_support/origin_trials/origin_trial_prefs.h
@@ -0,0 +1,19 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_EMBEDDER_SUPPORT_ORIGIN_TRIALS_ORIGIN_TRIAL_PREFS_H_
+#define COMPONENTS_EMBEDDER_SUPPORT_ORIGIN_TRIALS_ORIGIN_TRIAL_PREFS_H_
+
+class PrefRegistrySimple;
+
+namespace embedder_support {
+
+class OriginTrialPrefs {
+ public:
+  static void RegisterPrefs(PrefRegistrySimple* registry);
+};
+
+}  // namespace embedder_support
+
+#endif  // COMPONENTS_EMBEDDER_SUPPORT_ORIGIN_TRIALS_ORIGIN_TRIAL_PREFS_H_
diff --git a/components/feed/core/proto/v2/store.proto b/components/feed/core/proto/v2/store.proto
index cfb97a5..67a0b71 100644
--- a/components/feed/core/proto/v2/store.proto
+++ b/components/feed/core/proto/v2/store.proto
@@ -71,6 +71,15 @@
     int64 expiry_time_ms = 2;
   }
 
+  // Metadata about a specific stream.
+  message StreamMetadata {
+    string stream_id = 1;
+    // If this stream has been viewed before, this timestamp matches the
+    // 'StreamData.last_added_time_millis' value of that stream. This is used to
+    // determine whether the user has already seen a stored stream's data.
+    int64 view_time_millis = 2;
+  }
+
   // Token used to read or write to the same storage.
   bytes consistency_token = 1;
   // ID for the next pending action.
@@ -83,6 +92,7 @@
   // "shared_state"
   //    records.
   int32 stream_schema_version = 4;
+  repeated StreamMetadata stream_metadata = 5;
 }
 
 // A set of StreamStructures that should be applied to a stream.
diff --git a/components/feed/core/v2/BUILD.gn b/components/feed/core/v2/BUILD.gn
index df5ca25c..bc300a0 100644
--- a/components/feed/core/v2/BUILD.gn
+++ b/components/feed/core/v2/BUILD.gn
@@ -19,6 +19,8 @@
     "feed_store.h",
     "feed_stream.cc",
     "feed_stream.h",
+    "feedstore_util.cc",
+    "feedstore_util.h",
     "image_fetcher.cc",
     "image_fetcher.h",
     "metrics_reporter.cc",
@@ -42,6 +44,8 @@
     "public/web_feed_subscriptions.h",
     "request_throttler.cc",
     "request_throttler.h",
+    "stream/unread_content_notifier.cc",
+    "stream/unread_content_notifier.h",
     "stream_model.cc",
     "stream_model.h",
     "stream_model/ephemeral_change.cc",
@@ -68,6 +72,8 @@
     "tasks/wait_for_store_initialize_task.h",
     "web_feed_index.cc",
     "web_feed_index.h",
+    "wire_response_translator.cc",
+    "wire_response_translator.h",
   ]
   deps = [
     ":common",
@@ -141,6 +147,7 @@
     "feed_network_impl_unittest.cc",
     "feed_store_unittest.cc",
     "feed_stream_unittest.cc",
+    "feedstore_util_unittest.cc",
     "image_fetcher_unittest.cc",
     "metrics_reporter_unittest.cc",
     "persistent_key_value_store_impl_unittest.cc",
@@ -156,6 +163,7 @@
     "test/proto_printer.h",
     "test/stream_builder.cc",
     "test/stream_builder.h",
+    "test/test_util.h",
     "web_feed_index_unittest.cc",
   ]
 
diff --git a/components/feed/core/v2/feed_store.cc b/components/feed/core/v2/feed_store.cc
index 841070e..4c0bd9f 100644
--- a/components/feed/core/v2/feed_store.cc
+++ b/components/feed/core/v2/feed_store.cc
@@ -18,6 +18,7 @@
 #include "base/strings/string_util.h"
 #include "base/task/post_task.h"
 #include "base/task/thread_pool.h"
+#include "components/feed/core/v2/feedstore_util.h"
 #include "components/feed/core/v2/protocol_translator.h"
 #include "components/leveldb_proto/public/proto_database_provider.h"
 
@@ -39,15 +40,6 @@
 constexpr char kMetadataKey[] = "m";
 constexpr char kSubscribedFeedsKey[] = "subs";
 constexpr char kRecommendedIndexKey[] = "recommendedIndex";
-constexpr base::StringPiece kForYouStreamId{"i"};
-constexpr base::StringPiece kFollowStreamId{"w"};
-
-base::StringPiece StreamId(const StreamType& stream_type) {
-  if (stream_type.IsForYou())
-    return kForYouStreamId;
-  DCHECK(stream_type.IsWebFeed());
-  return kFollowStreamId;
-}
 
 leveldb::ReadOptions CreateReadOptions() {
   leveldb::ReadOptions opts;
@@ -64,7 +56,7 @@
   return base::StrCat({"S/", stream_id});
 }
 std::string StreamDataKey(const StreamType& stream_type) {
-  return StreamDataKey(StreamId(stream_type));
+  return StreamDataKey(feedstore::StreamId(stream_type));
 }
 std::string ContentKey(const base::StringPiece stream_type,
                        const feedwire::ContentId& content_id) {
@@ -73,7 +65,7 @@
 }
 std::string ContentKey(const StreamType& stream_type,
                        const feedwire::ContentId& content_id) {
-  return ContentKey(StreamId(stream_type), content_id);
+  return ContentKey(feedstore::StreamId(stream_type), content_id);
 }
 std::string SharedStateKey(const base::StringPiece stream_type,
                            const feedwire::ContentId& content_id) {
@@ -82,7 +74,7 @@
 }
 std::string SharedStateKey(const StreamType& stream_type,
                            const feedwire::ContentId& content_id) {
-  return SharedStateKey(StreamId(stream_type), content_id);
+  return SharedStateKey(feedstore::StreamId(stream_type), content_id);
 }
 std::string LocalActionKey(int64_t id) {
   return kLocalActionPrefix + base::NumberToString(id);
@@ -103,7 +95,7 @@
 class StreamKeyMatcher {
  public:
   explicit StreamKeyMatcher(const StreamType& stream_type) {
-    stream_id_ = StreamId(stream_type).as_string();
+    stream_id_ = feedstore::StreamId(stream_type).as_string();
     stream_id_plus_slash_ = stream_id_ + '/';
   }
 
@@ -240,7 +232,7 @@
     int32_t structure_set_sequence_number,
     const StreamType& stream_type,
     std::unique_ptr<StreamModelUpdateRequest> update_request) {
-  base::StringPiece stream_id = StreamId(stream_type);
+  base::StringPiece stream_id = feedstore::StreamId(stream_type);
   auto updates = std::make_unique<
       std::vector<std::pair<std::string, feedstore::Record>>>();
   update_request->stream_data.set_stream_id(stream_id.as_string());
@@ -371,8 +363,9 @@
   };
 
   database_->LoadEntriesWithFilter(
-      base::BindRepeating(filter, StreamDataKey(stream_type),
-                          base::StrCat({"T/", StreamId(stream_type), "/"})),
+      base::BindRepeating(
+          filter, StreamDataKey(stream_type),
+          base::StrCat({"T/", feedstore::StreamId(stream_type), "/"})),
       CreateReadOptions(),
       /*target_prefix=*/"",
       base::BindOnce(&FeedStore::OnLoadStreamFinished, GetWeakPtr(),
@@ -393,13 +386,13 @@
       switch (record.data_case()) {
         case feedstore::Record::kStreamData:
           result.stream_data = std::move(*record.mutable_stream_data());
-          DLOG_IF(ERROR,
-                  result.stream_data.stream_id() != StreamId(stream_type))
+          DLOG_IF(ERROR, result.stream_data.stream_id() !=
+                             feedstore::StreamId(stream_type))
               << "Read a record with the wrong stream_id";
           break;
         case feedstore::Record::kStreamStructures:
           DLOG_IF(ERROR, record.stream_structures().stream_id() !=
-                             StreamId(stream_type))
+                             feedstore::StreamId(stream_type))
               << "Read a record with the wrong stream_id";
           result.stream_structures.push_back(
               std::move(*record.mutable_stream_structures()));
@@ -488,7 +481,7 @@
     const StreamType& stream_type,
     int32_t sequence_number,
     std::vector<feedstore::DataOperation> operations) {
-  base::StringPiece stream_id = StreamId(stream_type);
+  base::StringPiece stream_id = feedstore::StreamId(stream_type);
   std::vector<feedstore::Record> records;
   feedstore::Record structures_record;
   feedstore::StreamStructureSet& structure_set =
@@ -502,7 +495,7 @@
       records.push_back(std::move(record));
     }
   }
-  structure_set.set_stream_id(StreamId(stream_type).as_string());
+  structure_set.set_stream_id(feedstore::StreamId(stream_type).as_string());
   structure_set.set_sequence_number(sequence_number);
 
   records.push_back(std::move(structures_record));
diff --git a/components/feed/core/v2/feed_store_unittest.cc b/components/feed/core/v2/feed_store_unittest.cc
index aca417e..f5fb1ff 100644
--- a/components/feed/core/v2/feed_store_unittest.cc
+++ b/components/feed/core/v2/feed_store_unittest.cc
@@ -19,6 +19,7 @@
 #include "components/feed/core/v2/test/callback_receiver.h"
 #include "components/feed/core/v2/test/proto_printer.h"
 #include "components/feed/core/v2/test/stream_builder.h"
+#include "components/feed/core/v2/test/test_util.h"
 #include "components/feed/feed_feature_list.h"
 #include "components/leveldb_proto/testing/fake_db.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -26,15 +27,6 @@
 namespace feed {
 namespace {
 
-// This is EXPECT_EQ, but also dumps the string values for ease of reading.
-#define EXPECT_STRINGS_EQUAL(WANT, GOT)                                   \
-  {                                                                       \
-    std::string want_param_ = (WANT), got_param_ = (GOT);                 \
-    EXPECT_EQ(want_param_, got_param_) << "Wanted:\n"                     \
-                                       << (want_param_) << "\nBut got:\n" \
-                                       << (got_param_);                   \
-  }
-
 using LoadStreamResult = FeedStore::LoadStreamResult;
 
 std::string KeyForContentId(base::StringPiece prefix,
diff --git a/components/feed/core/v2/feed_stream.cc b/components/feed/core/v2/feed_stream.cc
index c415f239..a92f48b 100644
--- a/components/feed/core/v2/feed_stream.cc
+++ b/components/feed/core/v2/feed_stream.cc
@@ -12,6 +12,7 @@
 #include "base/feature_list.h"
 #include "base/metrics/histogram_macros.h"
 #include "base/notreached.h"
+#include "base/stl_util.h"
 #include "base/threading/thread_task_runner_handle.h"
 #include "base/time/time.h"
 #include "components/feed/core/common/pref_names.h"
@@ -23,6 +24,7 @@
 #include "components/feed/core/v2/enums.h"
 #include "components/feed/core/v2/feed_network.h"
 #include "components/feed/core/v2/feed_store.h"
+#include "components/feed/core/v2/feedstore_util.h"
 #include "components/feed/core/v2/image_fetcher.h"
 #include "components/feed/core/v2/metrics_reporter.h"
 #include "components/feed/core/v2/offline_page_spy.h"
@@ -32,6 +34,7 @@
 #include "components/feed/core/v2/public/refresh_task_scheduler.h"
 #include "components/feed/core/v2/public/types.h"
 #include "components/feed/core/v2/scheduling.h"
+#include "components/feed/core/v2/stream/unread_content_notifier.h"
 #include "components/feed/core/v2/stream_model.h"
 #include "components/feed/core/v2/surface_updater.h"
 #include "components/feed/core/v2/tasks/clear_all_task.h"
@@ -104,70 +107,6 @@
   FeedStream* stream_;
 };
 
-RefreshResponseData FeedStream::WireResponseTranslator::TranslateWireResponse(
-    feedwire::Response response,
-    StreamModelUpdateRequest::Source source,
-    bool was_signed_in_request,
-    base::Time current_time) const {
-  return ::feed::TranslateWireResponse(std::move(response), source,
-                                       was_signed_in_request, current_time);
-}
-
-FeedStream::Metadata::Metadata(FeedStore* store) : store_(store) {}
-FeedStream::Metadata::~Metadata() = default;
-
-void FeedStream::Metadata::Populate(feedstore::Metadata metadata) {
-  metadata_ = std::move(metadata);
-}
-
-const std::string& FeedStream::Metadata::GetConsistencyToken() const {
-  return metadata_.consistency_token();
-}
-
-void FeedStream::Metadata::SetConsistencyToken(std::string consistency_token) {
-  metadata_.set_consistency_token(std::move(consistency_token));
-  store_->WriteMetadata(metadata_, base::DoNothing());
-}
-
-const std::string& FeedStream::Metadata::GetSessionIdToken() const {
-  return metadata_.session_id().token();
-}
-
-base::Time FeedStream::Metadata::GetSessionIdExpiryTime() const {
-  return base::Time::FromDeltaSinceWindowsEpoch(
-      base::TimeDelta::FromMilliseconds(
-          metadata_.session_id().expiry_time_ms()));
-}
-
-void FeedStream::Metadata::SetSessionId(std::string token,
-                                        base::Time expiry_time) {
-  feedstore::Metadata::SessionID* session_id = metadata_.mutable_session_id();
-  session_id->set_token(std::move(token));
-  session_id->set_expiry_time_ms(
-      expiry_time.ToDeltaSinceWindowsEpoch().InMilliseconds());
-  store_->WriteMetadata(metadata_, base::DoNothing());
-}
-
-void FeedStream::Metadata::MaybeUpdateSessionId(
-    base::Optional<std::string> token) {
-  if (token && metadata_.session_id().token() != *token) {
-    base::Time expiry_time =
-        token->empty() ? base::Time()
-                       : base::Time::Now() + GetFeedConfig().session_id_max_age;
-    SetSessionId(*token, expiry_time);
-  }
-}
-
-LocalActionId FeedStream::Metadata::GetNextActionId() {
-  uint32_t id = metadata_.next_action_id();
-  // Never use 0, as that's an invalid LocalActionId.
-  if (id == 0)
-    ++id;
-  metadata_.set_next_action_id(id + 1);
-  store_->WriteMetadata(metadata_, base::DoNothing());
-  return LocalActionId(id);
-}
-
 FeedStream::Stream::Stream() = default;
 FeedStream::Stream::~Stream() = default;
 
@@ -194,7 +133,6 @@
       chrome_info_(chrome_info),
       task_queue_(this),
       request_throttler_(profile_prefs),
-      metadata_(feed_store),
       notice_card_tracker_(profile_prefs) {
   static WireResponseTranslator default_translator;
   wire_response_translator_ = &default_translator;
@@ -270,7 +208,7 @@
 }
 
 void FeedStream::InitializeComplete(WaitForStoreInitializeTask::Result result) {
-  metadata_.Populate(result.metadata);
+  metadata_ = std::move(result.metadata);
   // TODO(crbug/1152592): Test that the index is populated once there's an API
   // to access the data.
   web_feed_index_.Populate(result.web_feed_startup_data);
@@ -284,7 +222,7 @@
       result.loaded_new_content_from_network, result.stored_content_age,
       std::move(result.latencies));
   UpdateIsActivityLoggingEnabled(result.stream_type);
-
+  MaybeNotifyHasUnreadContent(result.stream_type);
   stream.model_loading_in_progress = false;
   stream.surface_updater->LoadStreamComplete(stream.model != nullptr,
                                              result.final_status);
@@ -320,7 +258,18 @@
 }
 
 std::string FeedStream::GetSessionId() const {
-  return GetMetadata()->GetSessionIdToken();
+  return metadata_.session_id().token();
+}
+void FeedStream::SetMetadata(feedstore::Metadata metadata) {
+  metadata_ = std::move(metadata);
+  store_->WriteMetadata(metadata_, base::DoNothing());
+}
+bool FeedStream::SetMetadata(base::Optional<feedstore::Metadata> metadata) {
+  if (metadata) {
+    SetMetadata(std::move(*metadata));
+    return true;
+  }
+  return false;
 }
 
 void FeedStream::PrefetchImage(const GURL& url) {
@@ -358,6 +307,23 @@
   ScheduleModelUnloadIfNoSurfacesAttached(surface->GetStreamType());
 }
 
+void FeedStream::AddUnreadContentObserver(const StreamType& stream_type,
+                                          UnreadContentObserver* observer) {
+  GetStream(stream_type)
+      .unread_content_notifiers.emplace_back(observer->GetWeakPtr());
+  MaybeNotifyHasUnreadContent(stream_type);
+}
+
+void FeedStream::RemoveUnreadContentObserver(const StreamType& stream_type,
+                                             UnreadContentObserver* observer) {
+  Stream& stream = GetStream(stream_type);
+  auto predicate = [&](const UnreadContentNotifier& notifier) {
+    UnreadContentObserver* ptr = notifier.observer().get();
+    return ptr == nullptr || observer == ptr;
+  };
+  base::EraseIf(stream.unread_content_notifiers, predicate);
+}
+
 void FeedStream::ScheduleModelUnloadIfNoSurfacesAttached(
     const StreamType& stream_type) {
   Stream& stream = GetStream(stream_type);
@@ -423,10 +389,10 @@
   metrics_reporter_->OnLoadMoreBegin(surface.GetSurfaceId());
   stream.surface_updater->SetLoadingMore(true);
 
-  // Have at most one in-flight LoadMore() request. Send the result to all
-  // requestors.
-  load_more_complete_callbacks_.push_back(std::move(callback));
-  if (load_more_complete_callbacks_.size() == 1) {
+  // Have at most one in-flight LoadMore() request per stream. Send the result
+  // to all requestors.
+  stream.load_more_complete_callbacks.push_back(std::move(callback));
+  if (stream.load_more_complete_callbacks.size() == 1) {
     task_queue_.AddTask(std::make_unique<LoadMoreTask>(
         surface.GetStreamType(), this,
         base::BindOnce(&FeedStream::LoadMoreComplete, base::Unretained(this))));
@@ -439,7 +405,7 @@
   metrics_reporter_->OnLoadMore(result.final_status);
   stream.surface_updater->SetLoadingMore(false);
   std::vector<base::OnceCallback<void(bool)>> moved_callbacks =
-      std::move(load_more_complete_callbacks_);
+      std::move(stream.load_more_complete_callbacks);
   bool success = result.final_status == LoadStreamStatus::kLoadedFromNetwork;
   for (auto& callback : moved_callbacks) {
     std::move(callback).Run(success);
@@ -736,7 +702,7 @@
     if (stream->model->signed_in()) {
       result.client_instance_id = GetClientInstanceId();
     } else {
-      result.session_id = GetMetadata()->GetSessionIdToken();
+      result.session_id = GetSessionId();
     }
   } else {
     // The request is for the first page of the feed. Use client_instance_id
@@ -745,9 +711,9 @@
     if (delegate_->IsSignedIn() &&
         !ShouldForceSignedOutFeedQueryRequest(stream_type)) {
       result.client_instance_id = GetClientInstanceId();
-    } else if (!GetMetadata()->GetSessionIdToken().empty() &&
-               GetMetadata()->GetSessionIdExpiryTime() > base::Time::Now()) {
-      result.session_id = GetMetadata()->GetSessionIdToken();
+    } else if (!GetSessionId().empty() && feedstore::GetSessionIdExpiryTime(
+                                              metadata_) > base::Time::Now()) {
+      result.session_id = GetSessionId();
     }
   }
 
@@ -827,6 +793,8 @@
 }
 
 void FeedStream::BackgroundRefreshComplete(LoadStreamTask::Result result) {
+  if (!result.last_added_time.is_null())
+    GetStream(result.stream_type).last_updated_time = result.last_added_time;
   metrics_reporter_->OnBackgroundRefresh(result.final_status);
   if (result.loaded_new_content_from_network) {
     if (result.stream_type.IsForYou())
@@ -834,6 +802,8 @@
   }
   MaybeReportNewSuggestionsAvailable(result);
 
+  MaybeNotifyHasUnreadContent(result.stream_type);
+
   // Add prefetch images to task queue without waiting to finish
   // since we treat them as best-effort.
   if (result.stream_type.IsForYou())
@@ -863,7 +833,6 @@
 
 void FeedStream::ClearAll() {
   metrics_reporter_->OnClearAll(base::Time::Now() - GetLastFetchTime());
-
   task_queue_.AddTask(std::make_unique<ClearAllTask>(this));
 }
 
@@ -871,7 +840,8 @@
   // Clear any experiments stored.
   feed::prefs::SetExperiments({}, *profile_prefs_);
   feed::prefs::ClearClientInstanceId(*profile_prefs_);
-  metadata_.Populate(feedstore::Metadata());
+  SetMetadata(feedstore::MakeMetadata());
+
   delegate_->ClearAll();
 
   for (auto& item : streams_) {
@@ -915,11 +885,13 @@
   stream.model = std::move(model);
   stream.model->SetStreamType(stream_type);
   stream.model->SetStoreObserver(this);
+  stream.last_updated_time = stream.model->GetLastAddedTime();
   stream.surface_updater->SetModel(stream.model.get());
   if (stream.type.IsForYou()) {
     offline_page_spy_->SetModel(stream.model.get());
   }
   ScheduleModelUnloadIfNoSurfacesAttached(stream_type);
+  MaybeNotifyHasUnreadContent(stream_type);
 }
 
 void FeedStream::SetRequestSchedule(const StreamType& stream_type,
@@ -994,14 +966,22 @@
                                    const std::string& slice_id) {
   Stream& stream = GetStream(stream_type);
   int index = stream.surface_updater->GetSliceIndexFromSliceId(slice_id);
-  if (index >= 0) {
-    if (stream_type.IsForYou()) {
-      UpdateShownSlicesUploadCondition(index);
-      notice_card_tracker_.OnSliceViewed(index);
+  if (index < 0)
+    return;
+
+  if (stream.model) {
+    if (SetMetadata(SetStreamViewTime(metadata_, stream_type,
+                                      stream.model->GetLastAddedTime()))) {
+      MaybeNotifyHasUnreadContent(stream_type);
     }
     metrics_reporter_->ContentSliceViewed(stream_type, index);
   }
+  if (stream_type.IsForYou()) {
+    UpdateShownSlicesUploadCondition(index);
+    notice_card_tracker_.OnSliceViewed(index);
+  }
 }
+
 // TODO(crbug/1147237): Rename this method and related members?
 bool FeedStream::CanUploadActions() const {
   // TODO(crbug/1152592): Determine notice card behavior with web feeds.
@@ -1064,6 +1044,27 @@
   can_upload_actions_with_notice_card_ =
       HasReachedConditionsToUploadActionsWithNoticeCard();
 }
+
+// Notifies observers if 'HasUnreadContent' has changed for `stream_type`.
+// Stream content has been seen if StreamData::last_added_time_millis ==
+// Metadata::StreamMetadata::view_time_millis. This should be called: when the
+// model is loaded, when a refresh is attempted, and when content is viewed.
+void FeedStream::MaybeNotifyHasUnreadContent(const StreamType& stream_type) {
+  Stream& stream = GetStream(stream_type);
+  // Don't notify if we don't know the update time.
+  if (stream.last_updated_time.is_null())
+    return;
+
+  const bool has_new_content =
+      feedstore::GetStreamViewTime(metadata_, stream_type) !=
+          stream.last_updated_time &&
+      !stream.last_updated_time.is_null();
+
+  for (auto& o : stream.unread_content_notifiers) {
+    o.NotifyIfValueChanged(has_new_content);
+  }
+}
+
 void FeedStream::ReportFeedViewed(SurfaceId surface_id) {
   metrics_reporter_->FeedViewed(surface_id);
 }
diff --git a/components/feed/core/v2/feed_stream.h b/components/feed/core/v2/feed_stream.h
index ad2eadda..6198ffe 100644
--- a/components/feed/core/v2/feed_stream.h
+++ b/components/feed/core/v2/feed_stream.h
@@ -28,6 +28,7 @@
 #include "components/feed/core/v2/tasks/load_stream_task.h"
 #include "components/feed/core/v2/tasks/wait_for_store_initialize_task.h"
 #include "components/feed/core/v2/web_feed_index.h"
+#include "components/feed/core/v2/wire_response_translator.h"
 #include "components/offline_pages/core/prefetch/suggestions_provider.h"
 #include "components/offline_pages/task/task_queue.h"
 
@@ -39,6 +40,9 @@
 }  // namespace offline_pages
 
 namespace feed {
+namespace feed_stream {
+class UnreadContentNotifier;
+}
 class FeedNetwork;
 class FeedStore;
 class ImageFetcher;
@@ -48,7 +52,6 @@
 class PersistentKeyValueStoreImpl;
 class StreamModel;
 class SurfaceUpdater;
-struct StreamModelUpdateRequest;
 
 // Implements FeedApi. |FeedStream| additionally exposes functionality
 // needed by other classes within the Feed component.
@@ -71,45 +74,6 @@
     virtual void RegisterExperiments(const Experiments& experiments) = 0;
   };
 
-  // Forwards to |feed::TranslateWireResponse()| by default. Can be overridden
-  // for testing.
-  class WireResponseTranslator {
-   public:
-    WireResponseTranslator() = default;
-    ~WireResponseTranslator() = default;
-    virtual RefreshResponseData TranslateWireResponse(
-        feedwire::Response response,
-        StreamModelUpdateRequest::Source source,
-        bool was_signed_in_request,
-        base::Time current_time) const;
-  };
-
-  class Metadata {
-   public:
-    explicit Metadata(FeedStore* store);
-    ~Metadata();
-
-    void Populate(feedstore::Metadata metadata);
-
-    const std::string& GetConsistencyToken() const;
-    void SetConsistencyToken(std::string consistency_token);
-
-    const std::string& GetSessionIdToken() const;
-    base::Time GetSessionIdExpiryTime() const;
-    void SetSessionId(std::string token, base::Time expiry_time);
-    void MaybeUpdateSessionId(base::Optional<std::string> token);
-
-    LocalActionId GetNextActionId();
-
-    const feedstore::Metadata& GetMetadataProtoForTesting() const {
-      return metadata_;
-    }
-
-   private:
-    FeedStore* store_;
-    feedstore::Metadata metadata_;
-  };
-
   FeedStream(RefreshTaskScheduler* refresh_task_scheduler,
              MetricsReporter* metrics_reporter,
              Delegate* delegate,
@@ -132,6 +96,10 @@
   std::string GetSessionId() const override;
   void AttachSurface(FeedStreamSurface*) override;
   void DetachSurface(FeedStreamSurface*) override;
+  void AddUnreadContentObserver(const StreamType& stream_type,
+                                UnreadContentObserver* observer) override;
+  void RemoveUnreadContentObserver(const StreamType& stream_type,
+                                   UnreadContentObserver* observer) override;
   bool IsArticlesListVisible() override;
   std::string GetClientInstanceId() const override;
   void ExecuteRefreshTask(RefreshTaskId task_id) override;
@@ -223,8 +191,10 @@
   FeedNetwork* GetNetwork() { return feed_network_; }
   FeedStore* GetStore() { return store_; }
   RequestThrottler* GetRequestThrottler() { return &request_throttler_; }
-  Metadata* GetMetadata() { return &metadata_; }
-  const Metadata* GetMetadata() const { return &metadata_; }
+  const feedstore::Metadata& GetMetadata() const { return metadata_; }
+  void SetMetadata(feedstore::Metadata metadata);
+  bool SetMetadata(base::Optional<feedstore::Metadata> metadata);
+
   MetricsReporter* GetMetricsReporter() const { return metrics_reporter_; }
 
   void PrefetchImage(const GURL& url);
@@ -295,8 +265,13 @@
   bool CanUploadActions() const;
   void SetLastStreamLoadHadNoticeCard(bool value);
 
+  base::WeakPtr<FeedStream> GetWeakPtr() {
+    return weak_ptr_factory_.GetWeakPtr();
+  }
+
  private:
   class OfflineSuggestionsProvider;
+  using UnreadContentNotifier = feed_stream::UnreadContentNotifier;
 
   struct Stream {
     Stream();
@@ -313,12 +288,13 @@
     // |UnloadModel()|.
     std::unique_ptr<StreamModel> model;
     int unload_on_detach_sequence_number = 0;
+    // When new content was last added to this stream. Populated when we attempt
+    // to load the model or background refresh.
+    base::Time last_updated_time;
+    std::vector<UnreadContentNotifier> unread_content_notifiers;
+    std::vector<base::OnceCallback<void(bool)>> load_more_complete_callbacks;
   };
 
-  base::WeakPtr<FeedStream> GetWeakPtr() {
-    return weak_ptr_factory_.GetWeakPtr();
-  }
-
   void InitializeComplete(WaitForStoreInitializeTask::Result result);
 
   // Re-evaluate whether or not activity logging should currently be enabled.
@@ -356,6 +332,7 @@
   bool CanLogViews() const;
 
   void UpdateCanUploadActionsWithNoticeCard();
+  void MaybeNotifyHasUnreadContent(const StreamType& stream_type);
 
   Stream& GetStream(const StreamType& type);
   Stream* FindStream(const StreamType& type);
@@ -387,10 +364,9 @@
   // Mutable state.
   RequestThrottler request_throttler_;
   base::TimeTicks signed_out_for_you_refreshes_until_;
-  std::vector<base::OnceCallback<void(bool)>> load_more_complete_callbacks_;
 
   // State loaded at startup:
-  Metadata metadata_;
+  feedstore::Metadata metadata_;
   WebFeedIndex web_feed_index_;
 
   bool is_activity_logging_enabled_ = false;
@@ -398,6 +374,8 @@
   // feed.
   bool can_upload_actions_with_notice_card_ = false;
 
+  base::ObserverList<UnreadContentObserver> unread_content_observers_;
+
   // To allow tests to wait on task queue idle.
   base::RepeatingClosure idle_callback_;
 
diff --git a/components/feed/core/v2/feed_stream_unittest.cc b/components/feed/core/v2/feed_stream_unittest.cc
index cafc2189..f4874df 100644
--- a/components/feed/core/v2/feed_stream_unittest.cc
+++ b/components/feed/core/v2/feed_stream_unittest.cc
@@ -38,6 +38,7 @@
 #include "components/feed/core/shared_prefs/pref_names.h"
 #include "components/feed/core/v2/config.h"
 #include "components/feed/core/v2/feed_network.h"
+#include "components/feed/core/v2/feedstore_util.h"
 #include "components/feed/core/v2/image_fetcher.h"
 #include "components/feed/core/v2/metrics_reporter.h"
 #include "components/feed/core/v2/persistent_key_value_store_impl.h"
@@ -52,6 +53,8 @@
 #include "components/feed/core/v2/test/callback_receiver.h"
 #include "components/feed/core/v2/test/proto_printer.h"
 #include "components/feed/core/v2/test/stream_builder.h"
+#include "components/feed/core/v2/test/test_util.h"
+#include "components/feed/core/v2/wire_response_translator.h"
 #include "components/feed/feed_feature_list.h"
 #include "components/leveldb_proto/public/proto_database_provider.h"
 #include "components/offline_pages/core/client_namespace_constants.h"
@@ -154,24 +157,14 @@
   return msg;
 }
 
-// This is EXPECT_EQ, but also dumps the string values for ease of reading.
-#define EXPECT_STRINGS_EQUAL(WANT, GOT)                                       \
-  {                                                                           \
-    std::string want = (WANT), got = (GOT);                                   \
-    EXPECT_EQ(want, got) << "Wanted:\n" << (want) << "\nBut got:\n" << (got); \
+class TestUnreadContentObserver : public FeedApi::UnreadContentObserver {
+ public:
+  void HasUnreadContentChanged(bool has_unread_content) override {
+    calls.push_back(has_unread_content);
   }
 
-// Although time is mocked through TaskEnvironment, it does drift by small
-// amounts.
-const base::TimeDelta kEpsilon = base::TimeDelta::FromMilliseconds(5);
-#define EXPECT_TIME_EQ(WANT, GOT)          \
-  {                                        \
-    base::Time want = (WANT), got = (GOT); \
-    if (got != want) {                     \
-      EXPECT_LT(want - kEpsilon, got);     \
-      EXPECT_GT(want + kEpsilon, got);     \
-    }                                      \
-  }
+  std::vector<bool> calls;
+};
 
 class TestSurfaceBase : public FeedStreamSurface {
  public:
@@ -191,7 +184,7 @@
 
   void Attach(FeedStream* stream) {
     EXPECT_FALSE(stream_);
-    stream_ = stream;
+    stream_ = stream->GetWeakPtr();
     stream_->AttachSurface(this);
   }
 
@@ -290,7 +283,7 @@
   }
 
   // The stream if it was attached using the constructor.
-  FeedStream* stream_ = nullptr;
+  base::WeakPtr<FeedStream> stream_;
   std::vector<std::string> described_updates_;
   std::map<std::string, std::string> data_store_entries_;
 };
@@ -469,9 +462,9 @@
   base::Optional<feedwire::Response> injected_response_;
 };
 
-// Forwards to |FeedStream::WireResponseTranslator| unless a response is
+// Forwards to |WireResponseTranslator| unless a response is
 // injected.
-class TestWireResponseTranslator : public FeedStream::WireResponseTranslator {
+class TestWireResponseTranslator : public WireResponseTranslator {
  public:
   RefreshResponseData TranslateWireResponse(
       feedwire::Response response,
@@ -485,7 +478,7 @@
       injected_responses_.erase(injected_responses_.begin());
       return result;
     }
-    return FeedStream::WireResponseTranslator::TranslateWireResponse(
+    return WireResponseTranslator::TranslateWireResponse(
         std::move(response), source, was_signed_in_request, current_time);
   }
   void InjectResponse(std::unique_ptr<StreamModelUpdateRequest> response,
@@ -1096,7 +1089,11 @@
 }
 
 TEST_P(FeedStreamTestForAllStreamTypes, LoadFromNetwork) {
-  stream_->GetMetadata()->SetConsistencyToken("token");
+  {
+    auto metadata = stream_->GetMetadata();
+    metadata.set_consistency_token("token");
+    stream_->SetMetadata(metadata);
+  }
 
   // Store is empty, so we should fallback to a network request.
   response_translator_.InjectResponse(MakeTypicalInitialModelState());
@@ -1231,7 +1228,6 @@
                                       GetFeedConfig().stale_content_threshold -
                                       base::TimeDelta::FromMinutes(1)),
       base::DoNothing());
-  stream_->GetMetadata()->SetConsistencyToken("token-1");
 
   // Store is stale, so we should fallback to a network request.
   response_translator_.InjectResponse(MakeTypicalInitialModelState());
@@ -1239,10 +1235,6 @@
   WaitForIdleTaskQueue();
 
   ASSERT_TRUE(network_.query_request_sent);
-  // The stored continutation token should be sent.
-  EXPECT_EQ(
-      "token-1",
-      network_.query_request_sent->feed_request().consistency_token().token());
   EXPECT_TRUE(response_translator_.InjectedResponseConsumed());
   ASSERT_TRUE(surface.initial_state);
 }
@@ -1258,7 +1250,6 @@
       MakeTypicalInitialModelState(
           /*first_cluster_id=*/0, kTestTimeEpoch - kContentAge),
       base::DoNothing());
-  stream_->GetMetadata()->SetConsistencyToken("token-1");
 
   // Store is stale, so we should fallback to a network request.
   response_translator_.InjectResponse(MakeTypicalInitialModelState());
@@ -1266,10 +1257,6 @@
   WaitForIdleTaskQueue();
 
   ASSERT_TRUE(network_.query_request_sent);
-  // The stored continutation token should be sent.
-  EXPECT_EQ(
-      "token-1",
-      network_.query_request_sent->feed_request().consistency_token().token());
   EXPECT_TRUE(response_translator_.InjectedResponseConsumed());
   ASSERT_TRUE(surface.initial_state);
   EXPECT_EQ(LoadStreamStatus::kDataInStoreIsExpired,
@@ -1289,7 +1276,6 @@
       MakeTypicalInitialModelState(
           /*first_cluster_id=*/0, kTestTimeEpoch - kContentAge),
       base::DoNothing());
-  stream_->GetMetadata()->SetConsistencyToken("token-1");
 
   // Store is stale, so we should fallback to a network request. Since we didn't
   // inject a network response, the network update will fail.
@@ -1421,7 +1407,7 @@
 
   // Validate the downstream consumption of the response.
   EXPECT_EQ("loading -> 2 slices", surface.DescribeUpdates());
-  EXPECT_EQ(kSessionId, stream_->GetMetadata()->GetSessionIdToken());
+  EXPECT_EQ(kSessionId, stream_->GetMetadata().session_id().token());
   EXPECT_FALSE(stream_->GetModel(surface.GetStreamType())->signed_in());
 
   // Advance the clock beyond the forced signed out period.
@@ -1438,7 +1424,7 @@
   // contained the session id.
   ASSERT_EQ(2, network_.send_query_call_count);
   EXPECT_TRUE(network_.forced_signed_out_request);
-  EXPECT_EQ(kSessionId, stream_->GetMetadata()->GetSessionIdToken());
+  EXPECT_EQ(kSessionId, stream_->GetMetadata().session_id().token());
   EXPECT_EQ(network_.query_request_sent->feed_request()
                 .client_info()
                 .chrome_client_info()
@@ -1462,7 +1448,7 @@
 
   // The model should now be in the signed-in state.
   EXPECT_TRUE(stream_->GetModel(kForYouStream)->signed_in());
-  EXPECT_TRUE(stream_->GetMetadata()->GetSessionIdToken().empty());
+  EXPECT_TRUE(stream_->GetMetadata().session_id().token().empty());
 }
 
 TEST_F(FeedStreamTest, WebFeedUsesSignedInRequestAfterHistoryIsDeleted) {
@@ -1486,7 +1472,7 @@
 
   EXPECT_EQ("loading -> 2 slices", surface.DescribeUpdates());
   EXPECT_FALSE(network_.forced_signed_out_request);
-  EXPECT_TRUE(stream_->GetMetadata()->GetSessionIdToken().empty());
+  EXPECT_TRUE(stream_->GetMetadata().session_id().token().empty());
 }
 
 TEST_F(FeedStreamTest, ShouldMakeFeedQueryRequestConsumesQuota) {
@@ -1629,6 +1615,79 @@
                    "ContentSuggestions.Feed.CardAction.OpenInNewTab"));
 }
 
+TEST_F(FeedStreamTest, HasUnreadContentAfterLoadFromNetwork) {
+  response_translator_.InjectResponse(MakeTypicalInitialModelState());
+  TestUnreadContentObserver observer;
+  stream_->AddUnreadContentObserver(kForYouStream, &observer);
+  TestForYouSurface surface(stream_.get());
+
+  WaitForIdleTaskQueue();
+
+  EXPECT_EQ(std::vector<bool>({true}), observer.calls);
+}
+
+TEST_F(FeedStreamTest, RemovedUnreadContentObserverDoesNotReceiveCalls) {
+  response_translator_.InjectResponse(MakeTypicalInitialModelState());
+  TestUnreadContentObserver observer;
+  stream_->AddUnreadContentObserver(kForYouStream, &observer);
+  stream_->RemoveUnreadContentObserver(kForYouStream, &observer);
+  TestForYouSurface surface(stream_.get());
+
+  WaitForIdleTaskQueue();
+
+  EXPECT_EQ(std::vector<bool>(), observer.calls);
+}
+
+TEST_F(FeedStreamTest, DeletedUnreadContentObserverDoesNotCrash) {
+  response_translator_.InjectResponse(MakeTypicalInitialModelState());
+  {
+    TestUnreadContentObserver observer;
+    stream_->AddUnreadContentObserver(kForYouStream, &observer);
+  }
+  TestForYouSurface surface(stream_.get());
+
+  WaitForIdleTaskQueue();
+}
+
+TEST_F(FeedStreamTest, HasUnreadContentAfterLoadFromStore) {
+  store_->OverwriteStream(kForYouStream, MakeTypicalInitialModelState(),
+                          base::DoNothing());
+
+  TestUnreadContentObserver observer;
+  stream_->AddUnreadContentObserver(kForYouStream, &observer);
+  TestForYouSurface surface(stream_.get());
+
+  WaitForIdleTaskQueue();
+
+  EXPECT_EQ(std::vector<bool>({true}), observer.calls);
+}
+
+TEST_F(FeedStreamTest, ReportSliceViewedUpdatesObservers) {
+  response_translator_.InjectResponse(MakeTypicalInitialModelState());
+  TestUnreadContentObserver observer;
+  stream_->AddUnreadContentObserver(kForYouStream, &observer);
+  TestForYouSurface surface(stream_.get());
+
+  WaitForIdleTaskQueue();
+
+  stream_->ReportSliceViewed(
+      surface.GetSurfaceId(), surface.GetStreamType(),
+      surface.initial_state->updated_slices(1).slice().slice_id());
+  task_environment_.RunUntilIdle();
+
+  EXPECT_EQ(std::vector<bool>({true, false}), observer.calls);
+
+  // Verify that the fact the stream was viewed persists.
+  CreateStream();
+
+  TestUnreadContentObserver observer2;
+  stream_->AddUnreadContentObserver(kForYouStream, &observer2);
+  TestForYouSurface surface2(stream_.get());
+  WaitForIdleTaskQueue();
+
+  EXPECT_EQ(std::vector<bool>({false}), observer2.calls);
+}
+
 TEST_P(FeedStreamTestForAllStreamTypes, LoadMoreAppendsContent) {
   response_translator_.InjectResponse(MakeTypicalInitialModelState());
   TestSurface surface(stream_.get());
@@ -1715,12 +1774,17 @@
 }
 
 TEST_F(FeedStreamTest, LoadMoreSendsTokens) {
+  {
+    auto metadata = stream_->GetMetadata();
+    metadata.set_consistency_token("token");
+    stream_->SetMetadata(metadata);
+  }
+
   response_translator_.InjectResponse(MakeTypicalInitialModelState());
   TestForYouSurface surface(stream_.get());
   WaitForIdleTaskQueue();
   ASSERT_EQ("loading -> 2 slices", surface.DescribeUpdates());
 
-  stream_->GetMetadata()->SetConsistencyToken("token-1");
   response_translator_.InjectResponse(MakeTypicalNextPageState(2));
   CallbackReceiver<bool> callback;
   stream_->LoadMore(surface, callback.Bind());
@@ -1729,7 +1793,7 @@
   ASSERT_EQ("2 slices +spinner -> 4 slices", surface.DescribeUpdates());
 
   EXPECT_EQ(
-      "token-1",
+      "token",
       network_.query_request_sent->feed_request().consistency_token().token());
   EXPECT_EQ("page-2", network_.query_request_sent->feed_request()
                           .feed_query()
@@ -1737,7 +1801,6 @@
                           .next_page_token()
                           .next_page_token());
 
-  stream_->GetMetadata()->SetConsistencyToken("token-2");
   response_translator_.InjectResponse(MakeTypicalNextPageState(3));
   stream_->LoadMore(surface, callback.Bind());
 
@@ -1745,7 +1808,7 @@
   ASSERT_EQ("4 slices +spinner -> 6 slices", surface.DescribeUpdates());
 
   EXPECT_EQ(
-      "token-2",
+      "token",
       network_.query_request_sent->feed_request().consistency_token().token());
   EXPECT_EQ("page-3", network_.query_request_sent->feed_request()
                           .feed_query()
@@ -1930,8 +1993,8 @@
   ASSERT_EQ("loading -> 2 slices -> loading -> cant-refresh",
             surface.DescribeUpdates());
 
-  EXPECT_EQ("", DumpStoreState());
-  EXPECT_EQ("", stream_->GetMetadata()->GetConsistencyToken());
+  EXPECT_EQ("{\n}\n\n", DumpStoreState());
+  EXPECT_EQ("", stream_->GetMetadata().consistency_token());
   EXPECT_FALSE(stream_->IsActivityLoggingEnabled());
 }
 
@@ -2299,7 +2362,7 @@
   WaitForIdleTaskQueue();
 
   EXPECT_EQ(1, network_.GetActionRequestSent()->feed_actions_size());
-  EXPECT_EQ("token-12", stream_->GetMetadata()->GetConsistencyToken());
+  EXPECT_EQ("token-12", stream_->GetMetadata().consistency_token());
 
   // Uploaded action should have been erased from the store.
   network_.ClearTestData();
@@ -2499,26 +2562,25 @@
 }
 
 TEST_F(FeedStreamTest, MetadataLoadedWhenDatabaseInitialized) {
-  ASSERT_TRUE(stream_->GetMetadata());
-  // Verify the schema has been updated to the current version.
-  EXPECT_EQ((int)FeedStore::kCurrentStreamSchemaVersion,
-            stream_->GetMetadata()
-                ->GetMetadataProtoForTesting()
-                .stream_schema_version());
-
   const auto kExpiry = kTestTimeEpoch + base::TimeDelta::FromDays(1234);
-  stream_->GetMetadata()->SetSessionId("session-id", kExpiry);
-  stream_->GetMetadata()->SetConsistencyToken("token");
-  EXPECT_EQ(1, stream_->GetMetadata()->GetNextActionId().GetUnsafeValue());
+  {
+    // Write some metadata so it can be loaded when FeedStream starts up.
+    feedstore::Metadata initial_metadata;
+    feedstore::SetSessionId(initial_metadata, "session-id", kExpiry);
+    initial_metadata.set_consistency_token("token");
+    store_->WriteMetadata(initial_metadata, base::DoNothing());
+  }
 
   // Creating a stream should load metadata.
   CreateStream();
 
-  ASSERT_TRUE(stream_->GetMetadata());
-  EXPECT_EQ("session-id", stream_->GetMetadata()->GetSessionIdToken());
-  EXPECT_TIME_EQ(kExpiry, stream_->GetMetadata()->GetSessionIdExpiryTime());
-  EXPECT_EQ("token", stream_->GetMetadata()->GetConsistencyToken());
-  EXPECT_EQ(2, stream_->GetMetadata()->GetNextActionId().GetUnsafeValue());
+  EXPECT_EQ("session-id", stream_->GetMetadata().session_id().token());
+  EXPECT_TIME_EQ(kExpiry,
+                 feedstore::GetSessionIdExpiryTime(stream_->GetMetadata()));
+  EXPECT_EQ("token", stream_->GetMetadata().consistency_token());
+  // Verify the schema has been updated to the current version.
+  EXPECT_EQ((int)FeedStore::kCurrentStreamSchemaVersion,
+            stream_->GetMetadata().stream_schema_version());
 }
 
 TEST_F(FeedStreamTest, ModelUnloadsAfterTimeout) {
@@ -2775,8 +2837,6 @@
 }
 
 TEST_F(FeedStreamTest, SendsClientInstanceId) {
-  stream_->GetMetadata()->SetConsistencyToken("token");
-
   // Store is empty, so we should fallback to a network request.
   response_translator_.InjectResponse(MakeTypicalInitialModelState());
   TestForYouSurface surface(stream_.get());
@@ -2864,50 +2924,6 @@
                   .notice_card_acknowledged());
 }
 
-TEST_F(FeedStreamTest, GetSetAndUpdateSessionId) {
-  const std::string kToken1 = "token1";
-  const std::string kToken2 = "token2";
-  const base::Time kExpiryTime1 =
-      kTestTimeEpoch + base::TimeDelta::FromHours(2);
-  const base::Time kExpiryTime2 =
-      kTestTimeEpoch + GetFeedConfig().session_id_max_age;
-  ASSERT_NE(kExpiryTime1, kExpiryTime2);
-
-  // The stream metadata is initialized with an empty token and expiry time.
-  EXPECT_TRUE(stream_->GetMetadata()->GetSessionIdToken().empty());
-  EXPECT_TRUE(stream_->GetMetadata()->GetSessionIdExpiryTime().is_null());
-
-  // Verify that directly calling SetSessionId works as expected.
-  stream_->GetMetadata()->SetSessionId(kToken1, kExpiryTime1);
-  EXPECT_EQ(kToken1, stream_->GetMetadata()->GetSessionIdToken());
-  EXPECT_TIME_EQ(kExpiryTime1,
-                 stream_->GetMetadata()->GetSessionIdExpiryTime());
-
-  // Updating the token with nullopt is a NOP.
-  stream_->GetMetadata()->MaybeUpdateSessionId(base::nullopt);
-  EXPECT_EQ(kToken1, stream_->GetMetadata()->GetSessionIdToken());
-  EXPECT_TIME_EQ(kExpiryTime1,
-                 stream_->GetMetadata()->GetSessionIdExpiryTime());
-
-  // Updating the token with the same value is a NOP.
-  stream_->GetMetadata()->MaybeUpdateSessionId(kToken1);
-  EXPECT_EQ(kToken1, stream_->GetMetadata()->GetSessionIdToken());
-  EXPECT_TIME_EQ(kExpiryTime1,
-                 stream_->GetMetadata()->GetSessionIdExpiryTime());
-
-  // Updating the token with a different value resets the token and assigns a
-  // new expiry time.
-  stream_->GetMetadata()->MaybeUpdateSessionId(kToken2);
-  EXPECT_EQ(kToken2, stream_->GetMetadata()->GetSessionIdToken());
-  EXPECT_TIME_EQ(kExpiryTime2,
-                 stream_->GetMetadata()->GetSessionIdExpiryTime());
-
-  // Updating the token with the empty string clears its value.
-  stream_->GetMetadata()->MaybeUpdateSessionId("");
-  EXPECT_TRUE(stream_->GetMetadata()->GetSessionIdToken().empty());
-  EXPECT_TRUE(stream_->GetMetadata()->GetSessionIdExpiryTime().is_null());
-}
-
 TEST_F(FeedStreamTest, SignedOutSessionIdConsistency) {
   const std::string kSessionToken1("session-token-1");
   const std::string kSessionToken2("session-token-2");
@@ -2934,9 +2950,9 @@
   EXPECT_FALSE(network_.query_request_sent->feed_request()
                    .client_info()
                    .has_chrome_client_info());
-  EXPECT_EQ(kSessionToken1, stream_->GetMetadata()->GetSessionIdToken());
+  EXPECT_EQ(kSessionToken1, stream_->GetMetadata().session_id().token());
   const base::Time kSessionToken1ExpiryTime =
-      stream_->GetMetadata()->GetSessionIdExpiryTime();
+      feedstore::GetSessionIdExpiryTime(stream_->GetMetadata());
 
   // (2) LoadMore: the server returns the same session-id token
   //     - this should trigger a network request
@@ -2958,9 +2974,9 @@
                                 .client_info()
                                 .chrome_client_info()
                                 .session_id());
-  EXPECT_EQ(kSessionToken1, stream_->GetMetadata()->GetSessionIdToken());
+  EXPECT_EQ(kSessionToken1, stream_->GetMetadata().session_id().token());
   EXPECT_TIME_EQ(kSessionToken1ExpiryTime,
-                 stream_->GetMetadata()->GetSessionIdExpiryTime());
+                 feedstore::GetSessionIdExpiryTime(stream_->GetMetadata()));
 
   // (3) LoadMore: the server omits returning a session-id token
   //     - this should trigger a network request
@@ -2981,9 +2997,9 @@
                                 .client_info()
                                 .chrome_client_info()
                                 .session_id());
-  EXPECT_EQ(kSessionToken1, stream_->GetMetadata()->GetSessionIdToken());
+  EXPECT_EQ(kSessionToken1, stream_->GetMetadata().session_id().token());
   EXPECT_TIME_EQ(kSessionToken1ExpiryTime,
-                 stream_->GetMetadata()->GetSessionIdExpiryTime());
+                 feedstore::GetSessionIdExpiryTime(stream_->GetMetadata()));
 
   // (4) LoadMore: the server returns new session id.
   //     - this should trigger a network request
@@ -3005,26 +3021,27 @@
                                 .client_info()
                                 .chrome_client_info()
                                 .session_id());
-  EXPECT_EQ(kSessionToken2, stream_->GetMetadata()->GetSessionIdToken());
+  EXPECT_EQ(kSessionToken2, stream_->GetMetadata().session_id().token());
   EXPECT_TIME_EQ(kSessionToken1ExpiryTime + base::TimeDelta::FromSeconds(3),
-                 stream_->GetMetadata()->GetSessionIdExpiryTime());
+                 feedstore::GetSessionIdExpiryTime(stream_->GetMetadata()));
 }
 
 TEST_F(FeedStreamTest, ClearAllResetsSessionId) {
   is_signed_in_ = false;
 
   // Initialize a session id.
-  stream_->GetMetadata()->MaybeUpdateSessionId("session-id");
-  ASSERT_FALSE(stream_->GetMetadata()->GetSessionIdToken().empty());
-  ASSERT_FALSE(stream_->GetMetadata()->GetSessionIdExpiryTime().is_null());
+  feedstore::Metadata metadata = stream_->GetMetadata();
+  metadata = *feedstore::MaybeUpdateSessionId(metadata, "session-id");
+  stream_->SetMetadata(metadata);
 
   // Trigger a ClearAll.
   stream_->OnCacheDataCleared();
   WaitForIdleTaskQueue();
 
   // Session-ID should be wiped.
-  EXPECT_TRUE(stream_->GetMetadata()->GetSessionIdToken().empty());
-  EXPECT_TRUE(stream_->GetMetadata()->GetSessionIdExpiryTime().is_null());
+  EXPECT_TRUE(stream_->GetMetadata().session_id().token().empty());
+  EXPECT_TRUE(
+      feedstore::GetSessionIdExpiryTime(stream_->GetMetadata()).is_null());
 }
 
 TEST_F(FeedStreamTest, SignedOutSessionIdExpiry) {
@@ -3048,7 +3065,7 @@
   EXPECT_FALSE(network_.query_request_sent->feed_request()
                    .client_info()
                    .has_chrome_client_info());
-  EXPECT_EQ(kSessionToken1, stream_->GetMetadata()->GetSessionIdToken());
+  EXPECT_EQ(kSessionToken1, stream_->GetMetadata().session_id().token());
 
   // (2) Reload the stream from the network:
   //     - Detach the surface, advance the clock beyond the stale content
@@ -3067,7 +3084,7 @@
                                 .client_info()
                                 .chrome_client_info()
                                 .session_id());
-  EXPECT_EQ(kSessionToken1, stream_->GetMetadata()->GetSessionIdToken());
+  EXPECT_EQ(kSessionToken1, stream_->GetMetadata().session_id().token());
 
   // (3) Reload the stream from the network:
   //     - Detach the surface, advance the clock beyond the session id max age
@@ -3078,7 +3095,7 @@
   surface.Detach();
   task_environment_.FastForwardBy(GetFeedConfig().session_id_max_age -
                                   GetFeedConfig().stale_content_threshold);
-  ASSERT_LT(stream_->GetMetadata()->GetSessionIdExpiryTime(),
+  ASSERT_LT(feedstore::GetSessionIdExpiryTime(stream_->GetMetadata()),
             base::Time::Now());
   response_translator_.InjectResponse(model_generator.MakeFirstPage(),
                                       kSessionToken2);
@@ -3088,7 +3105,7 @@
   EXPECT_FALSE(network_.query_request_sent->feed_request()
                    .client_info()
                    .has_chrome_client_info());
-  EXPECT_EQ(kSessionToken2, stream_->GetMetadata()->GetSessionIdToken());
+  EXPECT_EQ(kSessionToken2, stream_->GetMetadata().session_id().token());
 }
 
 TEST_F(FeedStreamTest, SessionIdPersistsAcrossStreamLoads) {
@@ -3116,8 +3133,9 @@
   CreateStream();
   WaitForIdleTaskQueue();
   ASSERT_EQ(1, network_.send_query_call_count);
-  EXPECT_EQ(kSessionToken, stream_->GetMetadata()->GetSessionIdToken());
-  EXPECT_TIME_EQ(kExpiryTime, stream_->GetMetadata()->GetSessionIdExpiryTime());
+  EXPECT_EQ(kSessionToken, stream_->GetMetadata().session_id().token());
+  EXPECT_TIME_EQ(kExpiryTime,
+                 feedstore::GetSessionIdExpiryTime(stream_->GetMetadata()));
 }
 
 TEST_F(FeedStreamTest, PersistentKeyValueStoreIsClearedOnClearAll) {
diff --git a/components/feed/core/v2/feedstore_util.cc b/components/feed/core/v2/feedstore_util.cc
new file mode 100644
index 0000000..7ae7aea
--- /dev/null
+++ b/components/feed/core/v2/feedstore_util.cc
@@ -0,0 +1,133 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/feed/core/v2/feedstore_util.h"
+
+#include "components/feed/core/v2/config.h"
+#include "components/feed/core/v2/feed_store.h"
+
+namespace feedstore {
+using feed::LocalActionId;
+using feed::StreamType;
+
+namespace {
+int64_t ToTimestampMillis(base::Time t) {
+  return (t - base::Time::UnixEpoch()).InMilliseconds();
+}
+base::Time FromTimestampMillis(int64_t millis) {
+  return base::Time::UnixEpoch() + base::TimeDelta::FromMilliseconds(millis);
+}
+}  // namespace
+
+base::StringPiece StreamId(const StreamType& stream_type) {
+  if (stream_type.IsForYou())
+    return kForYouStreamId;
+  DCHECK(stream_type.IsWebFeed());
+  return kFollowStreamId;
+}
+
+void SetLastAddedTime(base::Time t, feedstore::StreamData& data) {
+  data.set_last_added_time_millis(ToTimestampMillis(t));
+}
+
+base::Time GetLastAddedTime(const feedstore::StreamData& data) {
+  return FromTimestampMillis(data.last_added_time_millis());
+}
+
+base::Time GetSessionIdExpiryTime(const Metadata& metadata) {
+  return base::Time::FromDeltaSinceWindowsEpoch(
+      base::TimeDelta::FromMilliseconds(
+          metadata.session_id().expiry_time_ms()));
+}
+
+void SetSessionId(Metadata& metadata,
+                  std::string token,
+                  base::Time expiry_time) {
+  Metadata::SessionID* session_id = metadata.mutable_session_id();
+  session_id->set_token(std::move(token));
+  session_id->set_expiry_time_ms(
+      expiry_time.ToDeltaSinceWindowsEpoch().InMilliseconds());
+}
+
+base::Optional<Metadata> MaybeUpdateSessionId(
+    const Metadata& metadata,
+    base::Optional<std::string> token) {
+  if (token && metadata.session_id().token() != *token) {
+    base::Time expiry_time =
+        token->empty()
+            ? base::Time()
+            : base::Time::Now() + feed::GetFeedConfig().session_id_max_age;
+    auto new_metadata = metadata;
+    SetSessionId(new_metadata, *token, expiry_time);
+    return new_metadata;
+  }
+  return base::nullopt;
+}
+
+LocalActionId GetNextActionId(Metadata& metadata) {
+  uint32_t id = metadata.next_action_id();
+  // Never use 0, as that's an invalid LocalActionId.
+  if (id == 0)
+    ++id;
+  metadata.set_next_action_id(id + 1);
+  return LocalActionId(id);
+}
+
+const Metadata::StreamMetadata* FindMetadataForStream(
+    const Metadata& metadata,
+    const StreamType& stream_type) {
+  base::StringPiece id = StreamId(stream_type);
+  for (const auto& sm : metadata.stream_metadata()) {
+    if (sm.stream_id() == id)
+      return &sm;
+  }
+  return nullptr;
+}
+
+Metadata::StreamMetadata& MetadataForStream(Metadata& metadata,
+                                            const StreamType& stream_type) {
+  const Metadata::StreamMetadata* existing =
+      FindMetadataForStream(metadata, stream_type);
+  if (existing)
+    return *const_cast<Metadata::StreamMetadata*>(existing);
+  Metadata::StreamMetadata* sm = metadata.add_stream_metadata();
+  sm->set_stream_id(StreamId(stream_type).as_string());
+  return *sm;
+}
+
+void SetStreamViewTime(Metadata& metadata,
+                       const StreamType& stream_type,
+                       base::Time stream_last_added_time) {
+  Metadata::StreamMetadata& sm = MetadataForStream(metadata, stream_type);
+  sm.set_view_time_millis(ToTimestampMillis(stream_last_added_time));
+}
+
+base::Time GetStreamViewTime(const Metadata& metadata,
+                             const StreamType& stream_type) {
+  base::Time result;
+  const Metadata::StreamMetadata* sm =
+      FindMetadataForStream(metadata, stream_type);
+  if (sm)
+    result = FromTimestampMillis(sm->view_time_millis());
+  return result;
+}
+
+feedstore::Metadata MakeMetadata() {
+  feedstore::Metadata md;
+  md.set_stream_schema_version(feed::FeedStore::kCurrentStreamSchemaVersion);
+  return md;
+}
+
+base::Optional<Metadata> SetStreamViewTime(const Metadata& metadata,
+                                           const StreamType& stream_type,
+                                           base::Time stream_last_added_time) {
+  base::Optional<Metadata> result;
+  if (GetStreamViewTime(metadata, stream_type) != stream_last_added_time) {
+    result = metadata;
+    SetStreamViewTime(*result, stream_type, stream_last_added_time);
+  }
+  return result;
+}
+
+}  // namespace feedstore
diff --git a/components/feed/core/v2/feedstore_util.h b/components/feed/core/v2/feedstore_util.h
new file mode 100644
index 0000000..aa1b00b9
--- /dev/null
+++ b/components/feed/core/v2/feedstore_util.h
@@ -0,0 +1,51 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_FEED_CORE_V2_FEEDSTORE_UTIL_H_
+#define COMPONENTS_FEED_CORE_V2_FEEDSTORE_UTIL_H_
+
+#include <string>
+#include "base/optional.h"
+#include "base/time/time.h"
+#include "components/feed/core/proto/v2/store.pb.h"
+#include "components/feed/core/v2/public/feed_api.h"
+#include "components/feed/core/v2/types.h"
+
+namespace feedstore {
+class Metadata;
+
+constexpr base::StringPiece kForYouStreamId{"i"};
+constexpr base::StringPiece kFollowStreamId{"w"};
+
+base::StringPiece StreamId(const feed::StreamType& stream_type);
+
+///////////////////////////////////////////////////
+// Functions that operate on feedstore proto types.
+
+void SetLastAddedTime(base::Time t, feedstore::StreamData& data);
+
+base::Time GetLastAddedTime(const feedstore::StreamData& data);
+base::Time GetSessionIdExpiryTime(const feedstore::Metadata& metadata);
+base::Time GetStreamViewTime(const Metadata& metadata,
+                             const feed::StreamType& stream_type);
+feedstore::Metadata MakeMetadata();
+
+// Mutations of Metadata. Metadata will need stored again after being changed,
+// call `FeedStream::SetMetadata()`.
+void SetSessionId(feedstore::Metadata& metadata,
+                  std::string token,
+                  base::Time expiry_time);
+base::Optional<Metadata> MaybeUpdateSessionId(
+    const feedstore::Metadata& metadata,
+    base::Optional<std::string> token);
+feed::LocalActionId GetNextActionId(feedstore::Metadata& metadata);
+const feedstore::Metadata::StreamMetadata* FindMetadataForStream(
+    const feed::StreamType& stream_type);
+base::Optional<Metadata> SetStreamViewTime(const Metadata& metadata,
+                                           const feed::StreamType& stream_type,
+                                           base::Time stream_last_added_time);
+
+}  // namespace feedstore
+
+#endif  // COMPONENTS_FEED_CORE_V2_FEEDSTORE_UTIL_H_
diff --git a/components/feed/core/v2/feedstore_util_unittest.cc b/components/feed/core/v2/feedstore_util_unittest.cc
new file mode 100644
index 0000000..2dbe7f3
--- /dev/null
+++ b/components/feed/core/v2/feedstore_util_unittest.cc
@@ -0,0 +1,70 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/feed/core/v2/feedstore_util.h"
+
+#include <string>
+#include "base/test/task_environment.h"
+#include "components/feed/core/v2/config.h"
+#include "components/feed/core/v2/test/test_util.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace feedstore {
+namespace {
+base::Time kTestTimeEpoch = base::Time::UnixEpoch();
+const base::Time kExpiryTime1 = kTestTimeEpoch + base::TimeDelta::FromHours(2);
+
+const std::string Token1() {
+  return "token1";
+}
+const std::string Token2() {
+  return "token2";
+}
+
+TEST(feedstore_util_test, SetSessionId) {
+  Metadata metadata;
+
+  // Verify that directly calling SetSessionId works as expected.
+  SetSessionId(metadata, Token1(), kExpiryTime1);
+
+  EXPECT_EQ(Token1(), metadata.session_id().token());
+  EXPECT_TIME_EQ(kExpiryTime1, GetSessionIdExpiryTime(metadata));
+}
+
+TEST(feedstore_util_test, MaybeUpdateSessionId) {
+  base::test::TaskEnvironment task_environment{
+      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
+
+  feedstore::Metadata metadata;
+  SetSessionId(metadata, Token1(), kExpiryTime1);
+
+  // Updating the token with nullopt is a NOP.
+  EXPECT_FALSE(MaybeUpdateSessionId(metadata, base::nullopt));
+
+  // Updating the token with the same value is a NOP.
+  EXPECT_FALSE(MaybeUpdateSessionId(metadata, Token1()));
+
+  // Updating the token with a different value resets the token and assigns a
+  // new expiry time.
+  base::Optional<Metadata> metadata2 = MaybeUpdateSessionId(metadata, Token2());
+  ASSERT_TRUE(metadata2);
+  EXPECT_EQ(Token2(), metadata2->session_id().token());
+  EXPECT_TIME_EQ(base::Time::Now() + feed::GetFeedConfig().session_id_max_age,
+                 GetSessionIdExpiryTime(*metadata2));
+
+  // Updating the token with the empty string clears its value.
+  base::Optional<Metadata> metadata3 = MaybeUpdateSessionId(*metadata2, "");
+  EXPECT_TRUE(metadata3->session_id().token().empty());
+  EXPECT_TRUE(GetSessionIdExpiryTime(*metadata3).is_null());
+}
+
+TEST(feedstore_util_test, GetNextActionId) {
+  Metadata metadata;
+
+  EXPECT_EQ(feed::LocalActionId(1), GetNextActionId(metadata));
+  EXPECT_EQ(feed::LocalActionId(2), GetNextActionId(metadata));
+}
+
+}  // namespace
+}  // namespace feedstore
diff --git a/components/feed/core/v2/notice_card_tracker.cc b/components/feed/core/v2/notice_card_tracker.cc
index 99d8a42..823b3a14 100644
--- a/components/feed/core/v2/notice_card_tracker.cc
+++ b/components/feed/core/v2/notice_card_tracker.cc
@@ -26,6 +26,22 @@
 NoticeCardTracker::NoticeCardTracker(PrefService* profile_prefs)
     : profile_prefs_(profile_prefs) {
   DCHECK(profile_prefs_);
+  views_count_ = prefs::GetNoticeCardViewsCount(*profile_prefs_);
+  clicks_count_ = prefs::GetNoticeCardClicksCount(*profile_prefs_);
+
+  views_count_threshold_ = base::GetFieldTrialParamByFeatureAsInt(
+      feed::kInterestFeedNoticeCardAutoDismiss,
+      kNoticeCardViewsCountThresholdParamName, 3);
+  DCHECK(views_count_threshold_ >= 0);
+
+  clicks_count_threshold_ = base::GetFieldTrialParamByFeatureAsInt(
+      feed::kInterestFeedNoticeCardAutoDismiss,
+      kNoticeCardClicksCountThresholdParamName, 1);
+  DCHECK(clicks_count_threshold_ >= 0);
+
+  DCHECK(views_count_threshold_ > 0 || clicks_count_threshold_ > 0)
+      << "all notice card auto-dismiss thresholds are set to 0 when there "
+         "should be at least one threshold above 0";
 }
 
 void NoticeCardTracker::OnSliceViewed(int index) {
@@ -40,28 +56,13 @@
   if (!base::FeatureList::IsEnabled(feed::kInterestFeedNoticeCardAutoDismiss))
     return false;
 
-  int views_count_threshold = base::GetFieldTrialParamByFeatureAsInt(
-      feed::kInterestFeedNoticeCardAutoDismiss,
-      kNoticeCardViewsCountThresholdParamName, 3);
-  DCHECK(views_count_threshold >= 0);
-  int clicks_count_threshold = base::GetFieldTrialParamByFeatureAsInt(
-      feed::kInterestFeedNoticeCardAutoDismiss,
-      kNoticeCardClicksCountThresholdParamName, 1);
-  DCHECK(clicks_count_threshold >= 0);
-
-  DCHECK(views_count_threshold > 0 || clicks_count_threshold > 0)
-      << "all notice card auto-dismiss thresholds are set to 0 when there "
-         "should be at least one threshold above 0";
-
-  if (views_count_threshold > 0 &&
-      prefs::GetNoticeCardViewsCount(*profile_prefs_) >=
-          views_count_threshold) {
+  base::AutoLock auto_lock_views(views_count_lock_);
+  if (views_count_threshold_ > 0 && views_count_ >= views_count_threshold_) {
     return true;
   }
 
-  if (clicks_count_threshold > 0 &&
-      prefs::GetNoticeCardClicksCount(*profile_prefs_) >=
-          clicks_count_threshold) {
+  base::AutoLock auto_lock_clicks(clicks_count_lock_);
+  if (clicks_count_threshold_ > 0 && clicks_count_ >= clicks_count_threshold_) {
     return true;
   }
 
@@ -82,17 +83,23 @@
   }
   return true;
 }
+
 void NoticeCardTracker::MaybeUpdateNoticeCardViewsCount(int index) {
   if (!HasNoticeCardActionsCountPrerequisites(index))
     return;
 
   prefs::IncrementNoticeCardViewsCount(*profile_prefs_);
+  base::AutoLock auto_lock(views_count_lock_);
+  views_count_++;
 }
+
 void NoticeCardTracker::MaybeUpdateNoticeCardClicksCount(int index) {
   if (!HasNoticeCardActionsCountPrerequisites(index))
     return;
 
   prefs::IncrementNoticeCardClicksCount(*profile_prefs_);
+  base::AutoLock auto_lock(clicks_count_lock_);
+  clicks_count_++;
 }
 
 }  // namespace feed
diff --git a/components/feed/core/v2/notice_card_tracker.h b/components/feed/core/v2/notice_card_tracker.h
index db9f7181..ab604c1 100644
--- a/components/feed/core/v2/notice_card_tracker.h
+++ b/components/feed/core/v2/notice_card_tracker.h
@@ -5,6 +5,9 @@
 #ifndef COMPONENTS_FEED_CORE_V2_NOTICE_CARD_TRACKER_H_
 #define COMPONENTS_FEED_CORE_V2_NOTICE_CARD_TRACKER_H_
 
+#include "base/synchronization/lock.h"
+#include "base/thread_annotations.h"
+
 class PrefService;
 
 namespace feed {
@@ -31,7 +34,8 @@
   // Get signals based on the actions.
 
   // Indicates whether there were enough views or clicks done on the notice
-  // card to consider it as acknowledged by the user.
+  // card to consider it as acknowledged by the user. This is safe to call in a
+  // background thread.
   bool HasAcknowledgedNoticeCard() const;
 
  private:
@@ -40,8 +44,24 @@
   void MaybeUpdateNoticeCardClicksCount(int index);
 
   PrefService* profile_prefs_;
+
+  // The number of views of the notice card.
+  mutable base::Lock views_count_lock_;
+  int views_count_ GUARDED_BY(views_count_lock_);
+
+  // The number of clicks/taps of the notice card.
+  mutable base::Lock clicks_count_lock_;
+  int clicks_count_ GUARDED_BY(clicks_count_lock_);
+
+  // The number of views of the notice card to consider it acknowledged by the
+  // user.
+  int views_count_threshold_;
+
+  // The number of clicks/taps of the notice card to consider it acknowledged by
+  // the user.
+  int clicks_count_threshold_;
 };
 
 }  // namespace feed
 
-#endif  // COMPONENTS_FEED_CORE_V2_NOTICE_CARD_TRACKER_H_
\ No newline at end of file
+#endif  // COMPONENTS_FEED_CORE_V2_NOTICE_CARD_TRACKER_H_
diff --git a/components/feed/core/v2/proto_util.cc b/components/feed/core/v2/proto_util.cc
index 1b72f0b..e8424cbc 100644
--- a/components/feed/core/v2/proto_util.cc
+++ b/components/feed/core/v2/proto_util.cc
@@ -259,15 +259,3 @@
 }
 
 }  // namespace feed
-
-namespace feedstore {
-void SetLastAddedTime(base::Time t, feedstore::StreamData& data) {
-  data.set_last_added_time_millis(
-      (t - base::Time::UnixEpoch()).InMilliseconds());
-}
-
-base::Time GetLastAddedTime(const feedstore::StreamData& data) {
-  return base::Time::UnixEpoch() +
-         base::TimeDelta::FromMilliseconds(data.last_added_time_millis());
-}
-}  // namespace feedstore
diff --git a/components/feed/core/v2/proto_util.h b/components/feed/core/v2/proto_util.h
index 95fd50d..8314df7 100644
--- a/components/feed/core/v2/proto_util.h
+++ b/components/feed/core/v2/proto_util.h
@@ -18,7 +18,6 @@
 }  // namespace feedwire
 namespace feedstore {
 class Content;
-class StreamData;
 }  // namespace feedstore
 
 // Helper functions/classes for dealing with feed proto messages.
@@ -62,11 +61,4 @@
 
 }  // namespace feed
 
-namespace feedstore {
-
-void SetLastAddedTime(base::Time t, feedstore::StreamData& data);
-base::Time GetLastAddedTime(const feedstore::StreamData& data);
-
-}  // namespace feedstore
-
 #endif  // COMPONENTS_FEED_CORE_V2_PROTO_UTIL_H_
diff --git a/components/feed/core/v2/protocol_translator.cc b/components/feed/core/v2/protocol_translator.cc
index ea51901..b796f62 100644
--- a/components/feed/core/v2/protocol_translator.cc
+++ b/components/feed/core/v2/protocol_translator.cc
@@ -19,6 +19,7 @@
 #include "components/feed/core/proto/v2/wire/request_schedule.pb.h"
 #include "components/feed/core/proto/v2/wire/stream_structure.pb.h"
 #include "components/feed/core/proto/v2/wire/token.pb.h"
+#include "components/feed/core/v2/feedstore_util.h"
 #include "components/feed/core/v2/metrics_reporter.h"
 #include "components/feed/core/v2/proto_util.h"
 
diff --git a/components/feed/core/v2/public/feed_api.cc b/components/feed/core/v2/public/feed_api.cc
index 7fda6010..3ff23a9 100644
--- a/components/feed/core/v2/public/feed_api.cc
+++ b/components/feed/core/v2/public/feed_api.cc
@@ -42,6 +42,9 @@
 FeedApi::FeedApi() = default;
 FeedApi::~FeedApi() = default;
 
+FeedApi::UnreadContentObserver::UnreadContentObserver() = default;
+FeedApi::UnreadContentObserver::~UnreadContentObserver() = default;
+
 FeedStreamSurface::FeedStreamSurface(StreamType stream_type)
     : stream_type_(stream_type) {
   static SurfaceId::Generator id_generator;
diff --git a/components/feed/core/v2/public/feed_api.h b/components/feed/core/v2/public/feed_api.h
index 678c142..170b8ea 100644
--- a/components/feed/core/v2/public/feed_api.h
+++ b/components/feed/core/v2/public/feed_api.h
@@ -116,12 +116,38 @@
 
   // TODO(crbug/1152592): Implement subscriptions().
   // WebFeedSubscriptions& subscriptions() = 0;
+  // Observes whether there is unread content for a specific stream type.
+  // In some cases, this information will not be known until after stream
+  // data is loaded from the database. This observer will not be notified until
+  // the information is available.
+  class UnreadContentObserver {
+   public:
+    UnreadContentObserver();
+    virtual ~UnreadContentObserver();
+    virtual void HasUnreadContentChanged(bool has_unread_content) = 0;
+    UnreadContentObserver(const UnreadContentObserver&) = delete;
+    UnreadContentObserver& operator=(const UnreadContentObserver&) = delete;
+
+    base::WeakPtr<UnreadContentObserver> GetWeakPtr() {
+      return weak_ptr_factory_.GetWeakPtr();
+    }
+
+   private:
+    base::WeakPtrFactory<UnreadContentObserver> weak_ptr_factory_{this};
+  };
 
   // Attach/detach a surface. Surfaces should be attached when content is
   // required for display, and detached when content is no longer shown.
   virtual void AttachSurface(FeedStreamSurface*) = 0;
   virtual void DetachSurface(FeedStreamSurface*) = 0;
 
+  // Begin/stop observing a stream type. An observer instance should not be
+  // added twice without first being removed.
+  virtual void AddUnreadContentObserver(const StreamType& stream_type,
+                                        UnreadContentObserver* observer) = 0;
+  virtual void RemoveUnreadContentObserver(const StreamType& stream_type,
+                                           UnreadContentObserver* observer) = 0;
+
   virtual bool IsArticlesListVisible() = 0;
 
   // Returns true if activity logging is enabled. The returned value is
diff --git a/components/feed/core/v2/stream/README.md b/components/feed/core/v2/stream/README.md
new file mode 100644
index 0000000..1820f65
--- /dev/null
+++ b/components/feed/core/v2/stream/README.md
@@ -0,0 +1 @@
+This directory contains implementation details for FeedStream.
diff --git a/components/feed/core/v2/stream/unread_content_notifier.cc b/components/feed/core/v2/stream/unread_content_notifier.cc
new file mode 100644
index 0000000..b17f959
--- /dev/null
+++ b/components/feed/core/v2/stream/unread_content_notifier.cc
@@ -0,0 +1,35 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/feed/core/v2/stream/unread_content_notifier.h"
+
+#include "base/bind.h"
+#include "base/threading/sequenced_task_runner_handle.h"
+
+namespace feed {
+namespace feed_stream {
+
+UnreadContentNotifier::~UnreadContentNotifier() = default;
+UnreadContentNotifier::UnreadContentNotifier(UnreadContentNotifier&&) = default;
+UnreadContentNotifier& UnreadContentNotifier::operator=(
+    UnreadContentNotifier&&) = default;
+
+UnreadContentNotifier::UnreadContentNotifier(
+    base::WeakPtr<UnreadContentObserver> observer)
+    : observer_(observer) {}
+
+void UnreadContentNotifier::NotifyIfValueChanged(bool has_unread_content) {
+  bool changed = !is_initialized_ || has_unread_content_ != has_unread_content;
+  has_unread_content_ = has_unread_content;
+  is_initialized_ = true;
+  if (changed) {
+    base::SequencedTaskRunnerHandle::Get()->PostTask(
+        FROM_HERE,
+        base::BindOnce(&UnreadContentObserver::HasUnreadContentChanged,
+                       observer_, has_unread_content));
+  }
+}
+
+}  // namespace feed_stream
+}  // namespace feed
diff --git a/components/feed/core/v2/stream/unread_content_notifier.h b/components/feed/core/v2/stream/unread_content_notifier.h
new file mode 100644
index 0000000..8168d032d
--- /dev/null
+++ b/components/feed/core/v2/stream/unread_content_notifier.h
@@ -0,0 +1,38 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_FEED_CORE_V2_STREAM_UNREAD_CONTENT_NOTIFIER_H_
+#define COMPONENTS_FEED_CORE_V2_STREAM_UNREAD_CONTENT_NOTIFIER_H_
+
+#include "components/feed/core/v2/public/feed_api.h"
+
+namespace feed {
+namespace feed_stream {
+
+// Wraps and notifies a `UnreadContentObserver`.
+class UnreadContentNotifier {
+ public:
+  using UnreadContentObserver = FeedApi::UnreadContentObserver;
+  explicit UnreadContentNotifier(base::WeakPtr<UnreadContentObserver> observer);
+  ~UnreadContentNotifier();
+  UnreadContentNotifier(UnreadContentNotifier&&);
+  UnreadContentNotifier& operator=(UnreadContentNotifier&&);
+  UnreadContentNotifier(const UnreadContentNotifier&) = delete;
+  UnreadContentNotifier& operator=(const UnreadContentNotifier&) = delete;
+
+  // Notify the observer if it `has_unread_content` has changed.
+  void NotifyIfValueChanged(bool has_unread_content);
+
+  base::WeakPtr<UnreadContentObserver> observer() const { return observer_; }
+
+ private:
+  base::WeakPtr<UnreadContentObserver> observer_;
+  bool is_initialized_ = false;
+  bool has_unread_content_ = false;
+};
+
+}  // namespace feed_stream
+}  // namespace feed
+
+#endif  // COMPONENTS_FEED_CORE_V2_STREAM_UNREAD_CONTENT_NOTIFIER_H_
diff --git a/components/feed/core/v2/stream_model.cc b/components/feed/core/v2/stream_model.cc
index 6448dce6..5d3f1ea 100644
--- a/components/feed/core/v2/stream_model.cc
+++ b/components/feed/core/v2/stream_model.cc
@@ -15,6 +15,8 @@
 #include "base/strings/string_number_conversions.h"
 #include "components/feed/core/proto/v2/store.pb.h"
 #include "components/feed/core/proto/v2/wire/content_id.pb.h"
+#include "components/feed/core/v2/feedstore_util.h"
+#include "components/feed/core/v2/proto_util.h"
 #include "components/feed/core/v2/protocol_translator.h"
 
 namespace feed {
@@ -247,6 +249,10 @@
   return stream_data_.next_page_token();
 }
 
+base::Time StreamModel::GetLastAddedTime() const {
+  return feedstore::GetLastAddedTime(stream_data_);
+}
+
 std::string StreamModel::DumpStateForTesting() {
   std::stringstream ss;
   ss << "StreamModel{\n";
diff --git a/components/feed/core/v2/stream_model.h b/components/feed/core/v2/stream_model.h
index 81bd01e05..f72d859 100644
--- a/components/feed/core/v2/stream_model.h
+++ b/components/feed/core/v2/stream_model.h
@@ -131,6 +131,9 @@
   bool RejectEphemeralChange(EphemeralChangeId id);
 
   const std::string& GetNextPageToken() const;
+  // Time the client received this stream data. 'NextPage' requests do not
+  // change this time.
+  base::Time GetLastAddedTime() const;
 
   // Outputs a string representing the model state for debugging or testing.
   std::string DumpStateForTesting();
diff --git a/components/feed/core/v2/tasks/load_more_task.cc b/components/feed/core/v2/tasks/load_more_task.cc
index 3873062..b5562536 100644
--- a/components/feed/core/v2/tasks/load_more_task.cc
+++ b/components/feed/core/v2/tasks/load_more_task.cc
@@ -10,15 +10,18 @@
 #include "base/callback_helpers.h"
 #include "base/check.h"
 #include "base/time/time.h"
+#include "components/feed/core/proto/v2/store.pb.h"
 #include "components/feed/core/proto/v2/wire/client_info.pb.h"
 #include "components/feed/core/proto/v2/wire/feed_request.pb.h"
 #include "components/feed/core/proto/v2/wire/request.pb.h"
 #include "components/feed/core/v2/feed_network.h"
 #include "components/feed/core/v2/feed_stream.h"
+#include "components/feed/core/v2/feedstore_util.h"
 #include "components/feed/core/v2/proto_util.h"
 #include "components/feed/core/v2/protocol_translator.h"
 #include "components/feed/core/v2/stream_model.h"
 #include "components/feed/core/v2/tasks/upload_actions_task.h"
+#include "components/feed/core/v2/wire_response_translator.h"
 
 namespace feed {
 
@@ -74,7 +77,7 @@
       NetworkRequestType::kNextPage,
       CreateFeedQueryLoadMoreRequest(
           stream_->GetRequestMetadata(stream_type_, /*is_for_next_page=*/true),
-          stream_->GetMetadata()->GetConsistencyToken(),
+          stream_->GetMetadata().consistency_token(),
           stream_->GetModel(stream_type_)->GetNextPageToken()),
       force_signed_out_request,
       base::BindOnce(&LoadMoreTask::QueryRequestComplete, GetWeakPtr()));
@@ -100,7 +103,12 @@
   loaded_new_content_from_network_ =
       !translated_response.model_update_request->stream_structures.empty();
 
-  stream_->GetMetadata()->MaybeUpdateSessionId(translated_response.session_id);
+  base::Optional<feedstore::Metadata> updated_metadata =
+      feedstore::MaybeUpdateSessionId(stream_->GetMetadata(),
+                                      translated_response.session_id);
+  if (updated_metadata) {
+    stream_->SetMetadata(std::move(*updated_metadata));
+  }
 
   model->Update(std::move(translated_response.model_update_request));
 
diff --git a/components/feed/core/v2/tasks/load_stream_from_store_task.cc b/components/feed/core/v2/tasks/load_stream_from_store_task.cc
index 1a63087..24793ff 100644
--- a/components/feed/core/v2/tasks/load_stream_from_store_task.cc
+++ b/components/feed/core/v2/tasks/load_stream_from_store_task.cc
@@ -12,6 +12,7 @@
 #include "components/feed/core/v2/config.h"
 #include "components/feed/core/v2/feed_store.h"
 #include "components/feed/core/v2/feed_stream.h"
+#include "components/feed/core/v2/feedstore_util.h"
 #include "components/feed/core/v2/proto_util.h"
 #include "components/feed/core/v2/protocol_translator.h"
 #include "components/feed/core/v2/public/feed_api.h"
@@ -65,8 +66,8 @@
     return;
   }
   if (!ignore_staleness_) {
-    content_age_ =
-        base::Time::Now() - feedstore::GetLastAddedTime(result.stream_data);
+    last_added_time_ = feedstore::GetLastAddedTime(result.stream_data);
+    content_age_ = base::Time::Now() - last_added_time_;
     if (content_age_ > GetFeedConfig().content_expiration_threshold) {
       Complete(LoadStreamStatus::kDataInStoreIsExpired);
       return;
@@ -145,6 +146,7 @@
     task_result.status = status;
   }
   task_result.content_age = content_age_;
+  task_result.last_added_time = last_added_time_;
   std::move(result_callback_).Run(std::move(task_result));
   TaskComplete();
 }
diff --git a/components/feed/core/v2/tasks/load_stream_from_store_task.h b/components/feed/core/v2/tasks/load_stream_from_store_task.h
index 29a69f9..96e35e0 100644
--- a/components/feed/core/v2/tasks/load_stream_from_store_task.h
+++ b/components/feed/core/v2/tasks/load_stream_from_store_task.h
@@ -38,6 +38,8 @@
     // How long since the loaded content was fetched from the server.
     // May be zero if content is not loaded.
     base::TimeDelta content_age;
+    // Last time the stream was fetched from the network.
+    base::Time last_added_time;
   };
 
   enum class LoadType {
@@ -80,6 +82,7 @@
   std::unique_ptr<StreamModelUpdateRequest> update_request_;
   std::vector<feedstore::StoredAction> pending_actions_;
   base::TimeDelta content_age_;
+  base::Time last_added_time_;
 
   base::WeakPtrFactory<LoadStreamFromStoreTask> weak_ptr_factory_{this};
 };
diff --git a/components/feed/core/v2/tasks/load_stream_task.cc b/components/feed/core/v2/tasks/load_stream_task.cc
index 906c960..ae72afe 100644
--- a/components/feed/core/v2/tasks/load_stream_task.cc
+++ b/components/feed/core/v2/tasks/load_stream_task.cc
@@ -16,6 +16,7 @@
 #include "components/feed/core/proto/v2/wire/request.pb.h"
 #include "components/feed/core/v2/feed_network.h"
 #include "components/feed/core/v2/feed_stream.h"
+#include "components/feed/core/v2/feedstore_util.h"
 #include "components/feed/core/v2/metrics_reporter.h"
 #include "components/feed/core/v2/proto_util.h"
 #include "components/feed/core/v2/protocol_translator.h"
@@ -92,6 +93,7 @@
   load_from_store_status_ = result.status;
   latencies_->StepComplete(LoadLatencyTimes::kLoadFromStore);
   stored_content_age_ = result.content_age;
+  last_added_time_ = result.last_added_time;
 
   // Phase 2.
   //  - If loading from store works, update the model.
@@ -142,7 +144,7 @@
       CreateFeedQueryRefreshRequest(
           GetRequestReason(load_type_),
           stream_->GetRequestMetadata(stream_type_, /*is_for_next_page=*/false),
-          stream_->GetMetadata()->GetConsistencyToken()),
+          stream_->GetMetadata().consistency_token()),
       force_signed_out_request,
       base::BindOnce(&LoadStreamTask::QueryRequestComplete, GetWeakPtr()));
 }
@@ -174,6 +176,8 @@
     return Done(LoadStreamStatus::kProtoTranslationFailed);
 
   loaded_new_content_from_network_ = true;
+  last_added_time_ = feedstore::GetLastAddedTime(
+      response_data.model_update_request->stream_data);
 
   stream_->GetStore()->OverwriteStream(
       stream_type_,
@@ -186,7 +190,12 @@
   stream_->SetLastStreamLoadHadNoticeCard(isNoticeCardFulfilled);
   MetricsReporter::NoticeCardFulfilled(isNoticeCardFulfilled);
 
-  stream_->GetMetadata()->MaybeUpdateSessionId(response_data.session_id);
+  base::Optional<feedstore::Metadata> updated_metadata =
+      feedstore::MaybeUpdateSessionId(stream_->GetMetadata(),
+                                      response_data.session_id);
+  if (updated_metadata) {
+    stream_->SetMetadata(std::move(*updated_metadata));
+  }
   if (response_data.experiments)
     experiments_ = *response_data.experiments;
 
@@ -215,6 +224,7 @@
   result.stream_type = stream_type_;
   result.load_from_store_status = load_from_store_status_;
   result.stored_content_age = stored_content_age_;
+  result.last_added_time = last_added_time_;
   result.final_status = status;
   result.load_type = load_type_;
   result.network_response_info = network_response_info_;
diff --git a/components/feed/core/v2/tasks/load_stream_task.h b/components/feed/core/v2/tasks/load_stream_task.h
index 8bcb5f6..864bdde 100644
--- a/components/feed/core/v2/tasks/load_stream_task.h
+++ b/components/feed/core/v2/tasks/load_stream_task.h
@@ -50,6 +50,9 @@
     LoadStreamStatus load_from_store_status = LoadStreamStatus::kNoStatus;
     // Age of content loaded from local storage. Zero if none was loaded.
     base::TimeDelta stored_content_age;
+    // Last time the stream was fetched from the network. This may be either
+    // a previous fetch time, or the one triggered by `LoadStreamTask`.
+    base::Time last_added_time;
     LoadType load_type;
 
     // Information about the network request, if one was made.
@@ -94,6 +97,7 @@
   base::Optional<NetworkResponseInfo> network_response_info_;
   bool loaded_new_content_from_network_ = false;
   base::TimeDelta stored_content_age_;
+  base::Time last_added_time_;
   Experiments experiments_;
 
   std::unique_ptr<LoadLatencyTimes> latencies_;
diff --git a/components/feed/core/v2/tasks/upload_actions_task.cc b/components/feed/core/v2/tasks/upload_actions_task.cc
index 476227b..af20692 100644
--- a/components/feed/core/v2/tasks/upload_actions_task.cc
+++ b/components/feed/core/v2/tasks/upload_actions_task.cc
@@ -15,6 +15,7 @@
 #include "components/feed/core/v2/feed_network.h"
 #include "components/feed/core/v2/feed_store.h"
 #include "components/feed/core/v2/feed_stream.h"
+#include "components/feed/core/v2/feedstore_util.h"
 #include "components/feed/core/v2/metrics_reporter.h"
 #include "components/feed/core/v2/request_throttler.h"
 
@@ -137,14 +138,15 @@
 UploadActionsTask::~UploadActionsTask() = default;
 
 void UploadActionsTask::Run() {
-  consistency_token_ = stream_->GetMetadata()->GetConsistencyToken();
+  consistency_token_ = stream_->GetMetadata().consistency_token();
 
   // From constructor 1: If there is an action to store, store it and maybe try
   // to upload all pending actions.
   if (wire_action_) {
     StoredAction action;
-    int32_t action_id =
-        stream_->GetMetadata()->GetNextActionId().GetUnsafeValue();
+    feedstore::Metadata metadata = stream_->GetMetadata();
+    int32_t action_id = feedstore::GetNextActionId(metadata).GetUnsafeValue();
+    stream_->SetMetadata(std::move(metadata));
     action.set_id(action_id);
     wire_action_->mutable_client_data()->set_sequence_number(action_id);
     *action.mutable_action() = std::move(*wire_action_);
@@ -301,8 +303,9 @@
 void UploadActionsTask::UpdateTokenAndFinish() {
   if (consistency_token_.empty())
     return Done(UploadActionsStatus::kFinishedWithoutUpdatingConsistencyToken);
-
-  stream_->GetMetadata()->SetConsistencyToken(consistency_token_);
+  feedstore::Metadata metadata = stream_->GetMetadata();
+  metadata.set_consistency_token(consistency_token_);
+  stream_->SetMetadata(metadata);
   Done(UploadActionsStatus::kUpdatedConsistencyToken);
 }
 
diff --git a/components/feed/core/v2/test/stream_builder.cc b/components/feed/core/v2/test/stream_builder.cc
index 4d37c83..4d81922 100644
--- a/components/feed/core/v2/test/stream_builder.cc
+++ b/components/feed/core/v2/test/stream_builder.cc
@@ -8,6 +8,7 @@
 
 #include "base/strings/strcat.h"
 #include "base/strings/string_number_conversions.h"
+#include "components/feed/core/v2/feedstore_util.h"
 #include "components/feed/core/v2/proto_util.h"
 #include "components/feed/core/v2/protocol_translator.h"
 
@@ -187,7 +188,7 @@
   initial_update->stream_data.set_logging_enabled(logging_enabled);
   initial_update->stream_data.set_privacy_notice_fulfilled(
       privacy_notice_fulfilled);
-  SetLastAddedTime(last_added_time, initial_update->stream_data);
+  feedstore::SetLastAddedTime(last_added_time, initial_update->stream_data);
 
   return initial_update;
 }
@@ -216,7 +217,7 @@
   initial_update->stream_data.set_logging_enabled(logging_enabled);
   initial_update->stream_data.set_privacy_notice_fulfilled(
       privacy_notice_fulfilled);
-  SetLastAddedTime(last_added_time, initial_update->stream_data);
+  feedstore::SetLastAddedTime(last_added_time, initial_update->stream_data);
 
   return initial_update;
 }
diff --git a/components/feed/core/v2/test/test_util.h b/components/feed/core/v2/test/test_util.h
new file mode 100644
index 0000000..6cd4857
--- /dev/null
+++ b/components/feed/core/v2/test/test_util.h
@@ -0,0 +1,36 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_FEED_CORE_V2_TEST_TEST_UTIL_H_
+#define COMPONENTS_FEED_CORE_V2_TEST_TEST_UTIL_H_
+
+#include "base/time/time.h"
+
+// Some functionality shared among feed tests.
+namespace feed {
+
+// Although time is mocked through TaskEnvironment, it does drift by small
+// amounts.
+const base::TimeDelta kEpsilon = base::TimeDelta::FromMilliseconds(5);
+#define EXPECT_TIME_EQ(WANT, GOT)                    \
+  {                                                  \
+    base::Time want___ = (WANT), got___ = (GOT);     \
+    if (got___ != want___) {                         \
+      EXPECT_LT(want___ - ::feed::kEpsilon, got___); \
+      EXPECT_GT(want___ + ::feed::kEpsilon, got___); \
+    }                                                \
+  }
+
+// This is EXPECT_EQ, but also dumps the string values for ease of reading.
+#define EXPECT_STRINGS_EQUAL(WANT, GOT)                     \
+  {                                                         \
+    std::string want___ = (WANT), got___ = (GOT);           \
+    EXPECT_EQ(want___, got___) << "Wanted:\n"               \
+                               << want___ << "\nBut got:\n" \
+                               << got___;                   \
+  }
+
+}  // namespace feed
+
+#endif  // COMPONENTS_FEED_CORE_V2_TEST_TEST_UTIL_H_
diff --git a/components/feed/core/v2/wire_response_translator.cc b/components/feed/core/v2/wire_response_translator.cc
new file mode 100644
index 0000000..daefe46
--- /dev/null
+++ b/components/feed/core/v2/wire_response_translator.cc
@@ -0,0 +1,18 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/feed/core/v2/wire_response_translator.h"
+
+namespace feed {
+
+RefreshResponseData WireResponseTranslator::TranslateWireResponse(
+    feedwire::Response response,
+    StreamModelUpdateRequest::Source source,
+    bool was_signed_in_request,
+    base::Time current_time) const {
+  return ::feed::TranslateWireResponse(std::move(response), source,
+                                       was_signed_in_request, current_time);
+}
+
+}  // namespace feed
diff --git a/components/feed/core/v2/wire_response_translator.h b/components/feed/core/v2/wire_response_translator.h
new file mode 100644
index 0000000..c4b8b99
--- /dev/null
+++ b/components/feed/core/v2/wire_response_translator.h
@@ -0,0 +1,27 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_FEED_CORE_V2_WIRE_RESPONSE_TRANSLATOR_H_
+#define COMPONENTS_FEED_CORE_V2_WIRE_RESPONSE_TRANSLATOR_H_
+
+#include "components/feed/core/v2/protocol_translator.h"
+
+namespace feed {
+
+// Forwards to |feed::TranslateWireResponse()| by default. Can be overridden
+// for testing.
+class WireResponseTranslator {
+ public:
+  WireResponseTranslator() = default;
+  ~WireResponseTranslator() = default;
+  virtual RefreshResponseData TranslateWireResponse(
+      feedwire::Response response,
+      StreamModelUpdateRequest::Source source,
+      bool was_signed_in_request,
+      base::Time current_time) const;
+};
+
+}  // namespace feed
+
+#endif  // COMPONENTS_FEED_CORE_V2_WIRE_RESPONSE_TRANSLATOR_H_
diff --git a/components/management_strings.grdp b/components/management_strings.grdp
index 52cdefe..5df9a3d7 100644
--- a/components/management_strings.grdp
+++ b/components/management_strings.grdp
@@ -113,6 +113,9 @@
     <message name="IDS_MANAGEMENT_REPORT_PRINTING" desc="Message stating that administrators can see names of printed files.">
       Names of files that you print
     </message>
+    <message name="IDS_MANAGEMENT_REPORT_PRINT_JOBS" desc="Message stating that administrators can see printing history associated with user, device, and printer.">
+      Printing history
+    </message>
     <message name="IDS_MANAGEMENT_CROSTINI" desc="Message stating that administrators can see Crostini usage">
       Linux apps installed and when they were last used
     </message>
diff --git a/components/management_strings_grdp/IDS_MANAGEMENT_REPORT_PRINT_JOBS.png.sha1 b/components/management_strings_grdp/IDS_MANAGEMENT_REPORT_PRINT_JOBS.png.sha1
new file mode 100644
index 0000000..c82f077
--- /dev/null
+++ b/components/management_strings_grdp/IDS_MANAGEMENT_REPORT_PRINT_JOBS.png.sha1
@@ -0,0 +1 @@
+31438fb5bab0db2e341f1c427db0057ef142baba
\ No newline at end of file
diff --git a/components/messages/android/BUILD.gn b/components/messages/android/BUILD.gn
index f5e8df4..ad4d3794 100644
--- a/components/messages/android/BUILD.gn
+++ b/components/messages/android/BUILD.gn
@@ -9,19 +9,13 @@
 # probably move MessageBanner* classes to internal/.
 android_library("java") {
   sources = [
-    "java/src/org/chromium/components/messages/MessageAutoDismissTimer.java",
-    "java/src/org/chromium/components/messages/MessageBannerCoordinator.java",
-    "java/src/org/chromium/components/messages/MessageBannerMediator.java",
     "java/src/org/chromium/components/messages/MessageBannerProperties.java",
-    "java/src/org/chromium/components/messages/MessageBannerView.java",
-    "java/src/org/chromium/components/messages/MessageBannerViewBinder.java",
     "java/src/org/chromium/components/messages/MessageContainer.java",
     "java/src/org/chromium/components/messages/MessageDispatcher.java",
     "java/src/org/chromium/components/messages/MessageDispatcherBridge.java",
     "java/src/org/chromium/components/messages/MessageDispatcherProvider.java",
     "java/src/org/chromium/components/messages/MessageStateHandler.java",
     "java/src/org/chromium/components/messages/MessageWrapper.java",
-    "java/src/org/chromium/components/messages/SingleActionMessage.java",
   ]
   resources_package = "org.chromium.components.messages"
   annotation_processor_deps = [ "//base/android/jni_generator:jni_processor" ]
@@ -42,11 +36,7 @@
 }
 
 android_resources("java_resources") {
-  sources = [
-    "java/res/drawable/message_bg_tinted.xml",
-    "java/res/layout/message_banner_view.xml",
-    "java/res/values/dimens.xml",
-  ]
+  sources = [ "java/res/values/dimens.xml" ]
   deps = [
     "//components/browser_ui/strings/android:browser_ui_strings_grd",
     "//components/browser_ui/styles/android:java_resources",
@@ -123,44 +113,12 @@
   deps = [ "//base" ]
 }
 
-android_library("javatests") {
-  testonly = true
-  sources = [
-    "java/src/org/chromium/components/messages/MessageBannerRenderTest.java",
-    "java/src/org/chromium/components/messages/MessageBannerViewTest.java",
-    "java/src/org/chromium/components/messages/SingleActionMessageTest.java",
-  ]
-  resources_package = "org.chromium.components.messages"
-  deps = [
-    ":java",
-    ":java_resources",
-    "//base:base_java",
-    "//base:base_java_test_support",
-    "//content/public/test/android:content_java_test_support",
-    "//third_party/android_deps:espresso_java",
-    "//third_party/android_support_test_runner:rules_java",
-    "//third_party/android_support_test_runner:runner_java",
-    "//third_party/androidx:androidx_annotation_annotation_java",
-    "//third_party/androidx:androidx_appcompat_appcompat_java",
-    "//third_party/androidx:androidx_appcompat_appcompat_resources_java",
-    "//third_party/androidx:androidx_core_core_java",
-    "//third_party/androidx:androidx_test_runner_java",
-    "//third_party/junit",
-    "//third_party/mockito:mockito_java",
-    "//ui/android:ui_java",
-    "//ui/android:ui_java_test_support",
-  ]
-}
-
 java_library("junit") {
   # Skip platform checks since Robolectric depends on requires_android targets.
   bypass_platform_checks = true
   testonly = true
-  sources = [
-    "java/src/org/chromium/components/messages/MessageAutoDismissTimerTest.java",
-    "java/src/org/chromium/components/messages/MessageBannerMediatorUnitTest.java",
-    "java/src/org/chromium/components/messages/MessageWrapperTest.java",
-  ]
+  sources =
+      [ "java/src/org/chromium/components/messages/MessageWrapperTest.java" ]
   deps = [
     ":java",
     "//base:base_java",
diff --git a/components/messages/android/internal/BUILD.gn b/components/messages/android/internal/BUILD.gn
index ae777d2..169afced 100644
--- a/components/messages/android/internal/BUILD.gn
+++ b/components/messages/android/internal/BUILD.gn
@@ -6,19 +6,32 @@
 
 android_library("java") {
   sources = [
+    "java/src/org/chromium/components/messages/MessageAutoDismissTimer.java",
+    "java/src/org/chromium/components/messages/MessageBannerCoordinator.java",
+    "java/src/org/chromium/components/messages/MessageBannerMediator.java",
+    "java/src/org/chromium/components/messages/MessageBannerView.java",
+    "java/src/org/chromium/components/messages/MessageBannerViewBinder.java",
     "java/src/org/chromium/components/messages/MessageDispatcherImpl.java",
     "java/src/org/chromium/components/messages/MessageQueueManager.java",
     "java/src/org/chromium/components/messages/MessageScopeChange.java",
     "java/src/org/chromium/components/messages/MessagesFactory.java",
     "java/src/org/chromium/components/messages/ScopeChangeController.java",
+    "java/src/org/chromium/components/messages/SingleActionMessage.java",
   ]
 
+  resources_package = "org.chromium.components.messages"
+
   deps = [
+    ":java_resources",
     "..:java",
+    "..:java_resources",
     "..:manager_java",
     "//base:base_java",
+    "//components/browser_ui/widget/android:java",
     "//content/public/android:content_java",
     "//third_party/androidx:androidx_annotation_annotation_java",
+    "//third_party/androidx:androidx_appcompat_appcompat_resources_java",
+    "//third_party/androidx:androidx_core_core_java",
     "//ui/android:ui_full_java",
   ]
 }
@@ -28,6 +41,8 @@
   bypass_platform_checks = true
   testonly = true
   sources = [
+    "java/src/org/chromium/components/messages/MessageAutoDismissTimerTest.java",
+    "java/src/org/chromium/components/messages/MessageBannerMediatorUnitTest.java",
     "java/src/org/chromium/components/messages/MessageQueueManagerTest.java",
     "java/src/org/chromium/components/messages/ScopeChangeControllerTest.java",
   ]
@@ -35,10 +50,51 @@
     ":java",
     "..:java",
     "..:manager_java",
+    "//base:base_java",
+    "//base:base_java_test_support",
     "//base:base_junit_test_support",
+    "//components/browser_ui/widget/android:java",
     "//content/public/android:content_java",
+    "//third_party/android_deps:robolectric_all_java",
     "//third_party/androidx:androidx_test_runner_java",
     "//third_party/junit",
     "//third_party/mockito:mockito_java",
+    "//ui/android:ui_java",
+  ]
+}
+
+android_library("javatests") {
+  testonly = true
+  sources = [
+    "java/src/org/chromium/components/messages/MessageBannerRenderTest.java",
+    "java/src/org/chromium/components/messages/MessageBannerViewTest.java",
+    "java/src/org/chromium/components/messages/SingleActionMessageTest.java",
+  ]
+  deps = [
+    ":java",
+    ":java_resources",
+    "..:java",
+    "//base:base_java",
+    "//base:base_java_test_support",
+    "//content/public/test/android:content_java_test_support",
+    "//third_party/android_deps:espresso_java",
+    "//third_party/androidx:androidx_test_runner_java",
+    "//third_party/junit",
+    "//third_party/mockito:mockito_java",
+    "//ui/android:ui_java",
+    "//ui/android:ui_java_test_support",
+  ]
+}
+
+android_resources("java_resources") {
+  sources = [
+    "java/res/drawable/message_bg_tinted.xml",
+    "java/res/layout/message_banner_view.xml",
+    "java/res/values/dimens.xml",
+  ]
+  deps = [
+    "//components/browser_ui/strings/android:browser_ui_strings_grd",
+    "//components/browser_ui/styles/android:java_resources",
+    "//ui/android:ui_java_resources",
   ]
 }
diff --git a/components/messages/android/java/res/drawable/message_bg_tinted.xml b/components/messages/android/internal/java/res/drawable/message_bg_tinted.xml
similarity index 100%
rename from components/messages/android/java/res/drawable/message_bg_tinted.xml
rename to components/messages/android/internal/java/res/drawable/message_bg_tinted.xml
diff --git a/components/messages/android/java/res/layout/message_banner_view.xml b/components/messages/android/internal/java/res/layout/message_banner_view.xml
similarity index 96%
rename from components/messages/android/java/res/layout/message_banner_view.xml
rename to components/messages/android/internal/java/res/layout/message_banner_view.xml
index e44aba6..bafa2d26 100644
--- a/components/messages/android/java/res/layout/message_banner_view.xml
+++ b/components/messages/android/internal/java/res/layout/message_banner_view.xml
@@ -27,7 +27,7 @@
         android:id="@+id/message_icon"
         android:paddingStart="@dimen/message_icon_padding"
         android:paddingEnd="@dimen/message_icon_padding"
-        android:tint="@color/default_icon_color_blue"
+        app:tint="@color/default_icon_color_blue"
         android:layout_weight="0"
         android:layout_width="wrap_content"
         android:layout_height="match_parent"
@@ -56,7 +56,7 @@
     <!--  Content description is set programmatically according to secondary button icon. -->
     <org.chromium.components.browser_ui.widget.listmenu.ListMenuButton
         android:id="@+id/message_secondary_button"
-        android:tint="@color/default_icon_color_secondary"
+        app:tint="@color/default_icon_color_secondary"
         android:layout_weight="0"
         android:paddingStart="@dimen/message_icon_padding"
         android:paddingEnd="@dimen/message_icon_padding"
diff --git a/components/messages/android/internal/java/res/values/dimens.xml b/components/messages/android/internal/java/res/values/dimens.xml
new file mode 100644
index 0000000..b0394de
--- /dev/null
+++ b/components/messages/android/internal/java/res/values/dimens.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2021 The Chromium Authors. All rights reserved.
+     Use of this source code is governed by a BSD-style license that can be
+     found in the LICENSE file. -->
+
+<resources>
+
+    <dimen name="message_banner_height">64dp</dimen>
+    <dimen name="message_banner_radius">12dp</dimen>
+    <dimen name="message_banner_vertical_padding">12dp</dimen>
+    <dimen name="message_banner_elevation">6dp</dimen>
+    <!-- Min width to ensure the min touchable size. -->
+    <dimen name="message_banner_button_min_width">12dp</dimen>
+    <dimen name="message_icon_padding">12dp</dimen>
+    <dimen name="message_divider_margin">12dp</dimen>
+    <dimen name="message_shadow_lateral_margin">12dp</dimen>
+    <dimen name="message_shadow_bottom_margin">16dp</dimen>
+    <dimen name="message_vertical_hide_threshold">16dp</dimen>
+    <dimen name="message_horizontal_hide_threshold">24dp</dimen>
+    <dimen name="message_max_horizontal_translation">360dp</dimen>
+    <dimen name="message_max_width">380dp</dimen>
+
+</resources>
\ No newline at end of file
diff --git a/components/messages/android/java/src/org/chromium/components/messages/MessageAutoDismissTimer.java b/components/messages/android/internal/java/src/org/chromium/components/messages/MessageAutoDismissTimer.java
similarity index 100%
rename from components/messages/android/java/src/org/chromium/components/messages/MessageAutoDismissTimer.java
rename to components/messages/android/internal/java/src/org/chromium/components/messages/MessageAutoDismissTimer.java
diff --git a/components/messages/android/java/src/org/chromium/components/messages/MessageAutoDismissTimerTest.java b/components/messages/android/internal/java/src/org/chromium/components/messages/MessageAutoDismissTimerTest.java
similarity index 100%
rename from components/messages/android/java/src/org/chromium/components/messages/MessageAutoDismissTimerTest.java
rename to components/messages/android/internal/java/src/org/chromium/components/messages/MessageAutoDismissTimerTest.java
diff --git a/components/messages/android/java/src/org/chromium/components/messages/MessageBannerCoordinator.java b/components/messages/android/internal/java/src/org/chromium/components/messages/MessageBannerCoordinator.java
similarity index 100%
rename from components/messages/android/java/src/org/chromium/components/messages/MessageBannerCoordinator.java
rename to components/messages/android/internal/java/src/org/chromium/components/messages/MessageBannerCoordinator.java
diff --git a/components/messages/android/java/src/org/chromium/components/messages/MessageBannerMediator.java b/components/messages/android/internal/java/src/org/chromium/components/messages/MessageBannerMediator.java
similarity index 100%
rename from components/messages/android/java/src/org/chromium/components/messages/MessageBannerMediator.java
rename to components/messages/android/internal/java/src/org/chromium/components/messages/MessageBannerMediator.java
diff --git a/components/messages/android/java/src/org/chromium/components/messages/MessageBannerMediatorUnitTest.java b/components/messages/android/internal/java/src/org/chromium/components/messages/MessageBannerMediatorUnitTest.java
similarity index 100%
rename from components/messages/android/java/src/org/chromium/components/messages/MessageBannerMediatorUnitTest.java
rename to components/messages/android/internal/java/src/org/chromium/components/messages/MessageBannerMediatorUnitTest.java
diff --git a/components/messages/android/java/src/org/chromium/components/messages/MessageBannerRenderTest.java b/components/messages/android/internal/java/src/org/chromium/components/messages/MessageBannerRenderTest.java
similarity index 100%
rename from components/messages/android/java/src/org/chromium/components/messages/MessageBannerRenderTest.java
rename to components/messages/android/internal/java/src/org/chromium/components/messages/MessageBannerRenderTest.java
diff --git a/components/messages/android/java/src/org/chromium/components/messages/MessageBannerView.java b/components/messages/android/internal/java/src/org/chromium/components/messages/MessageBannerView.java
similarity index 100%
rename from components/messages/android/java/src/org/chromium/components/messages/MessageBannerView.java
rename to components/messages/android/internal/java/src/org/chromium/components/messages/MessageBannerView.java
diff --git a/components/messages/android/java/src/org/chromium/components/messages/MessageBannerViewBinder.java b/components/messages/android/internal/java/src/org/chromium/components/messages/MessageBannerViewBinder.java
similarity index 100%
rename from components/messages/android/java/src/org/chromium/components/messages/MessageBannerViewBinder.java
rename to components/messages/android/internal/java/src/org/chromium/components/messages/MessageBannerViewBinder.java
diff --git a/components/messages/android/java/src/org/chromium/components/messages/MessageBannerViewTest.java b/components/messages/android/internal/java/src/org/chromium/components/messages/MessageBannerViewTest.java
similarity index 100%
rename from components/messages/android/java/src/org/chromium/components/messages/MessageBannerViewTest.java
rename to components/messages/android/internal/java/src/org/chromium/components/messages/MessageBannerViewTest.java
diff --git a/components/messages/android/java/src/org/chromium/components/messages/SingleActionMessage.java b/components/messages/android/internal/java/src/org/chromium/components/messages/SingleActionMessage.java
similarity index 100%
rename from components/messages/android/java/src/org/chromium/components/messages/SingleActionMessage.java
rename to components/messages/android/internal/java/src/org/chromium/components/messages/SingleActionMessage.java
diff --git a/components/messages/android/java/src/org/chromium/components/messages/SingleActionMessageTest.java b/components/messages/android/internal/java/src/org/chromium/components/messages/SingleActionMessageTest.java
similarity index 78%
rename from components/messages/android/java/src/org/chromium/components/messages/SingleActionMessageTest.java
rename to components/messages/android/internal/java/src/org/chromium/components/messages/SingleActionMessageTest.java
index 7e5c7ada..e579814 100644
--- a/components/messages/android/java/src/org/chromium/components/messages/SingleActionMessageTest.java
+++ b/components/messages/android/internal/java/src/org/chromium/components/messages/SingleActionMessageTest.java
@@ -15,6 +15,9 @@
 import androidx.test.filters.MediumTest;
 
 import org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -26,16 +29,28 @@
 
 import org.chromium.base.ApiCompatibilityUtils;
 import org.chromium.base.Callback;
+import org.chromium.base.test.BaseActivityTestRule;
 import org.chromium.base.test.BaseJUnit4ClassRunner;
 import org.chromium.base.test.util.CallbackHelper;
+import org.chromium.content_public.browser.test.util.TestThreadUtils;
 import org.chromium.ui.modelutil.PropertyModel;
-import org.chromium.ui.test.util.DummyUiActivityTestCase;
+import org.chromium.ui.test.util.DisableAnimationsTestRule;
+import org.chromium.ui.test.util.DummyUiActivity;
 
 /**
  * Tests for {@link SingleActionMessage}.
  */
 @RunWith(BaseJUnit4ClassRunner.class)
-public class SingleActionMessageTest extends DummyUiActivityTestCase {
+public class SingleActionMessageTest {
+    @ClassRule
+    public static DisableAnimationsTestRule sDisableAnimationsRule =
+            new DisableAnimationsTestRule();
+    @ClassRule
+    public static BaseActivityTestRule<DummyUiActivity> sActivityTestRule =
+            new BaseActivityTestRule<>(DummyUiActivity.class);
+
+    private static Activity sActivity;
+
     @Rule
     public MockitoRule mMockitoRule = MockitoJUnit.rule();
     @Mock
@@ -45,23 +60,29 @@
     private SingleActionMessage.DismissCallback mEmptyDismissCallback =
             (model, dismissReason) -> {};
 
-    @Override
-    public void setUpTest() throws Exception {
-        super.setUpTest();
+    @BeforeClass
+    public static void setupSuite() {
+        sActivityTestRule.launchActivity(null);
+        TestThreadUtils.runOnUiThreadBlocking(
+                () -> { sActivity = sActivityTestRule.getActivity(); });
+    }
+
+    @Before
+    public void setupTest() throws Exception {
         mDismissCallback = new CallbackHelper();
     }
 
     @Test
     @MediumTest
     public void testAddAndRemoveSingleActionMessage() throws Exception {
-        MessageContainer container = new MessageContainer(getActivity(), null);
+        MessageContainer container = new MessageContainer(sActivity, null);
         PropertyModel model = createBasicSingleActionMessageModel();
         SingleActionMessage message = new SingleActionMessage(
                 container, model, mEmptyDismissCallback, () -> 0, () -> 0L, mAnimatorStartCallback);
         final MessageBannerCoordinator messageBanner = Mockito.mock(MessageBannerCoordinator.class);
         doNothing().when(messageBanner).show(any(Runnable.class));
         doNothing().when(messageBanner).setOnTouchRunnable(any(Runnable.class));
-        final MessageBannerView view = new MessageBannerView(getActivity(), null);
+        final MessageBannerView view = new MessageBannerView(sActivity, null);
         view.setId(R.id.message_banner);
         message.setMessageBannerForTesting(messageBanner);
         message.setViewForTesting(view);
@@ -85,7 +106,7 @@
     @Test
     @MediumTest
     public void testAutoDismissDuration() {
-        MessageContainer container = new MessageContainer(getActivity(), null);
+        MessageContainer container = new MessageContainer(sActivity, null);
         PropertyModel model = createBasicSingleActionMessageModel();
         long duration = 42;
         SingleActionMessage message = new SingleActionMessage(container, model,
@@ -97,7 +118,7 @@
     @Test(expected = IllegalStateException.class)
     @MediumTest
     public void testAddMultipleSingleActionMessage() {
-        MessageContainer container = new MessageContainer(getActivity(), null);
+        MessageContainer container = new MessageContainer(sActivity, null);
         PropertyModel m1 = createBasicSingleActionMessageModel();
         PropertyModel m2 = createBasicSingleActionMessageModel();
         SingleActionMessage message1 = new SingleActionMessage(
@@ -105,7 +126,7 @@
         final MessageBannerCoordinator messageBanner1 =
                 Mockito.mock(MessageBannerCoordinator.class);
         doNothing().when(messageBanner1).show(any(Runnable.class));
-        final MessageBannerView view1 = new MessageBannerView(getActivity(), null);
+        final MessageBannerView view1 = new MessageBannerView(sActivity, null);
         view1.setId(R.id.message_banner);
         message1.setMessageBannerForTesting(messageBanner1);
         message1.setViewForTesting(view1);
@@ -114,7 +135,7 @@
         final MessageBannerCoordinator messageBanner2 =
                 Mockito.mock(MessageBannerCoordinator.class);
         doNothing().when(messageBanner2).show(any(Runnable.class));
-        final MessageBannerView view2 = new MessageBannerView(getActivity(), null);
+        final MessageBannerView view2 = new MessageBannerView(sActivity, null);
         view2.setId(R.id.message_banner);
         message2.setMessageBannerForTesting(messageBanner2);
         message2.setViewForTesting(view2);
@@ -123,13 +144,12 @@
     }
 
     private PropertyModel createBasicSingleActionMessageModel() {
-        Activity activity = getActivity();
         return new PropertyModel.Builder(MessageBannerProperties.SINGLE_ACTION_MESSAGE_KEYS)
                 .with(MessageBannerProperties.TITLE, "test")
                 .with(MessageBannerProperties.DESCRIPTION, "Description")
                 .with(MessageBannerProperties.ICON,
                         ApiCompatibilityUtils.getDrawable(
-                                activity.getResources(), android.R.drawable.ic_menu_add))
+                                sActivity.getResources(), android.R.drawable.ic_menu_add))
                 .with(MessageBannerProperties.ON_PRIMARY_ACTION, () -> {})
                 .with(MessageBannerProperties.ON_TOUCH_RUNNABLE, () -> {})
                 .with(MessageBannerProperties.ON_DISMISSED,
diff --git a/components/messages/android/java/res/values/dimens.xml b/components/messages/android/java/res/values/dimens.xml
index 44a6549..256349c 100644
--- a/components/messages/android/java/res/values/dimens.xml
+++ b/components/messages/android/java/res/values/dimens.xml
@@ -5,21 +5,7 @@
 
 <resources>
 
-    <dimen name="message_banner_height">64dp</dimen>
-    <dimen name="message_banner_radius">12dp</dimen>
-    <dimen name="message_banner_vertical_padding">12dp</dimen>
-    <dimen name="message_banner_elevation">6dp</dimen>
-    <!-- Min width to ensure the min touchable size. -->
-    <dimen name="message_banner_button_min_width">12dp</dimen>
-    <dimen name="message_icon_padding">12dp</dimen>
-    <dimen name="message_divider_margin">12dp</dimen>
     <dimen name="message_shadow_top_margin">8dp</dimen>
-    <dimen name="message_shadow_lateral_margin">12dp</dimen>
-    <dimen name="message_shadow_bottom_margin">16dp</dimen>
     <dimen name="message_bubble_inset">8dp</dimen>
-    <dimen name="message_vertical_hide_threshold">16dp</dimen>
-    <dimen name="message_horizontal_hide_threshold">24dp</dimen>
-    <dimen name="message_max_horizontal_translation">360dp</dimen>
-    <dimen name="message_max_width">380dp</dimen>
 
 </resources>
\ No newline at end of file
diff --git a/components/messages/android/java/src/org/chromium/components/messages/MessageContainer.java b/components/messages/android/java/src/org/chromium/components/messages/MessageContainer.java
index b85bc85..e7ebee5 100644
--- a/components/messages/android/java/src/org/chromium/components/messages/MessageContainer.java
+++ b/components/messages/android/java/src/org/chromium/components/messages/MessageContainer.java
@@ -18,6 +18,8 @@
  * Container holding messages.
  */
 public class MessageContainer extends FrameLayout {
+    private View mMessageBannerView;
+
     public MessageContainer(@NonNull Context context, @Nullable AttributeSet attrs) {
         super(context, attrs);
     }
@@ -28,10 +30,11 @@
      * @param view The message view to display on the screen.
      */
     void addMessage(View view) {
-        if (getChildCount() != 0) {
+        if (mMessageBannerView != null) {
             throw new IllegalStateException(
                     "Should not contain any view when adding a new message.");
         }
+        mMessageBannerView = view;
         addView(view);
         // TODO(sinansahin): clipChildren should be set to false only when the message is in motion.
         ViewUtils.setAncestorsShouldClipChildren(this, false);
@@ -42,11 +45,21 @@
      * @param view The message which should be removed.
      */
     void removeMessage(View view) {
-        if (indexOfChild(view) < 0) {
+        if (mMessageBannerView != view) {
             throw new IllegalStateException("The given view is not being shown.");
         }
         ViewUtils.setAncestorsShouldClipChildren(this, true);
         removeAllViews();
+        mMessageBannerView = null;
+    }
+
+    public int getMessageBannerHeight() {
+        assert mMessageBannerView != null;
+        return mMessageBannerView.getHeight();
+    }
+
+    public int getMessageShadowTopMargin() {
+        return getResources().getDimensionPixelOffset(R.dimen.message_shadow_top_margin);
     }
 
     /**
@@ -55,14 +68,13 @@
      * @param runnable The {@link Runnable}.
      */
     void runAfterInitialMessageLayout(Runnable runnable) {
-        final View message = findViewById(R.id.message_banner);
-        assert message != null;
-        if (message.getHeight() > 0) {
+        assert mMessageBannerView != null;
+        if (mMessageBannerView.getHeight() > 0) {
             runnable.run();
             return;
         }
 
-        message.addOnLayoutChangeListener(new OnLayoutChangeListener() {
+        mMessageBannerView.addOnLayoutChangeListener(new OnLayoutChangeListener() {
             @Override
             public void onLayoutChange(View v, int left, int top, int right, int bottom,
                     int oldLeft, int oldTop, int oldRight, int oldBottom) {
diff --git a/components/optimization_guide/core/hints_processing_util.cc b/components/optimization_guide/core/hints_processing_util.cc
index 4a482da..7b7fee5 100644
--- a/components/optimization_guide/core/hints_processing_util.cc
+++ b/components/optimization_guide/core/hints_processing_util.cc
@@ -55,6 +55,8 @@
       return "LinkPerformance";
     case proto::OptimizationType::SHOPPING_PAGE_PREDICTOR:
       return "ShoppingPagePredictor";
+    case proto::OptimizationType::LOGIN_DETECTION:
+      return "LoginDetection";
   }
   NOTREACHED();
   return std::string();
diff --git a/components/optimization_guide/proto/hints.proto b/components/optimization_guide/proto/hints.proto
index 149f2f9..ad67a0d 100644
--- a/components/optimization_guide/proto/hints.proto
+++ b/components/optimization_guide/proto/hints.proto
@@ -138,6 +138,9 @@
   // increased to reduce the number of non-shopping page predictions for
   // shopping pages.
   SHOPPING_PAGE_PREDICTOR = 15;
+  // This optimization provides information about hosts that are identified as
+  // commonly logged-in.
+  LOGIN_DETECTION = 16;
 }
 
 // Presents semantics for how page load URLs should be matched.
diff --git a/components/proxy_config/proxy_prefs.h b/components/proxy_config/proxy_prefs.h
index 8af504f..e955c90 100644
--- a/components/proxy_config/proxy_prefs.h
+++ b/components/proxy_config/proxy_prefs.h
@@ -19,8 +19,12 @@
   // Direct connection to the network, other proxy preferences are ignored.
   MODE_DIRECT = 0,
 
-  // Try to retrieve a PAC script from http://wpad/wpad.dat or fall back to
-  // direct connection.
+  // Try to auto-detect the PAC script location.
+  // On Windows and Chrome OS, DHCP is tried first (DHCP Option 252), and DNS
+  // (resolving http://wpad/wpad.dat) is tried second.
+  // On other platforms, only DNS is tried.
+  // If no PAC script can be found by this method, fall back to direct
+  // connection.
   MODE_AUTO_DETECT = 1,
 
   // Try to retrieve a PAC script from kProxyPacURL or fall back to direct
diff --git a/components/services/storage/dom_storage/legacy_dom_storage_database.cc b/components/services/storage/dom_storage/legacy_dom_storage_database.cc
index 2f15263..716f171 100644
--- a/components/services/storage/dom_storage/legacy_dom_storage_database.cc
+++ b/components/services/storage/dom_storage/legacy_dom_storage_database.cc
@@ -56,7 +56,7 @@
     std::u16string key = statement.ColumnString16(0);
     std::u16string value;
     statement.ColumnBlobAsString16(1, &value);
-    (*result)[key] = base::NullableString16(value, false);
+    (*result)[key] = value;
   }
   known_to_be_empty_ = result->empty();
 
@@ -90,9 +90,9 @@
   auto it = changes.begin();
   for (; it != changes.end(); ++it) {
     sql::Statement statement;
-    std::u16string key = it->first;
-    base::NullableString16 value = it->second;
-    if (value.is_null()) {
+    const std::u16string& key = it->first;
+    const base::Optional<std::u16string>& value = it->second;
+    if (!value.has_value()) {
       statement.Assign(db_->GetCachedStatement(
           SQL_FROM_HERE, "DELETE FROM ItemTable WHERE key=?"));
       statement.BindString16(0, key);
@@ -101,8 +101,8 @@
       statement.Assign(db_->GetCachedStatement(
           SQL_FROM_HERE, "INSERT INTO ItemTable VALUES (?,?)"));
       statement.BindString16(0, key);
-      statement.BindBlob(1, value.string().data(),
-                         value.string().length() * sizeof(char16_t));
+      statement.BindBlob(1, value.value().data(),
+                         value.value().length() * sizeof(char16_t));
       known_to_be_empty_ = false;
       did_insert = true;
     }
diff --git a/components/services/storage/dom_storage/legacy_dom_storage_database.h b/components/services/storage/dom_storage/legacy_dom_storage_database.h
index 055c7fc..5227ba9 100644
--- a/components/services/storage/dom_storage/legacy_dom_storage_database.h
+++ b/components/services/storage/dom_storage/legacy_dom_storage_database.h
@@ -11,7 +11,6 @@
 
 #include "base/files/file_path.h"
 #include "base/gtest_prod_util.h"
-#include "base/strings/nullable_string16.h"
 #include "sql/database.h"
 
 namespace base {
@@ -25,7 +24,7 @@
 class FilesystemProxy;
 
 using LegacyDomStorageValuesMap =
-    std::map<std::u16string, base::NullableString16>;
+    std::map<std::u16string, base::Optional<std::u16string>>;
 
 // Represents a SQLite based backing for DOM storage data. This
 // class is designed to be used on a single thread.
@@ -35,17 +34,16 @@
                            std::unique_ptr<FilesystemProxy> filesystem_proxy);
   virtual ~LegacyDomStorageDatabase();  // virtual for unit testing
 
-  // Reads all the key, value pairs stored in the database and returns
-  // them. |result| is assumed to be empty and any duplicate keys will
-  // be overwritten. If the database exists on disk then it will be
-  // opened. If it does not exist then it will not be created and
-  // |result| will be unmodified.
+  // Reads all the key, value pairs stored in the database and returns them.
+  // |result| is assumed to be empty and any duplicate keys will be overwritten.
+  // If the database exists on disk then it will be opened. If it does not exist
+  // then it will not be created and |result| will be unmodified.
   void ReadAllValues(LegacyDomStorageValuesMap* result);
 
-  // Updates the backing database. Will remove all keys before updating
-  // the database if |clear_all_first| is set. Then all entries in
-  // |changes| will be examined - keys mapped to a null NullableString16
-  // will be removed and all others will be inserted/updated as appropriate.
+  // Updates the backing database. Will remove all keys before updating the
+  // database if |clear_all_first| is set. Then all entries in |changes| will be
+  // examined - keys mapped to a nullopt value will be removed and all others
+  // will be inserted/updated as appropriate.
   bool CommitChanges(bool clear_all_first,
                      const LegacyDomStorageValuesMap& changes);
 
diff --git a/components/services/storage/dom_storage/legacy_dom_storage_database_unittest.cc b/components/services/storage/dom_storage/legacy_dom_storage_database_unittest.cc
index ae0dc1e2..d4cf288 100644
--- a/components/services/storage/dom_storage/legacy_dom_storage_database_unittest.cc
+++ b/components/services/storage/dom_storage/legacy_dom_storage_database_unittest.cc
@@ -8,6 +8,7 @@
 #include "base/files/file_util.h"
 #include "base/files/scoped_temp_dir.h"
 #include "base/path_service.h"
+#include "base/stl_util.h"
 #include "base/strings/utf_string_conversions.h"
 #include "components/services/storage/public/cpp/filesystem/filesystem_proxy.h"
 #include "sql/statement.h"
@@ -54,23 +55,18 @@
 
   LegacyDomStorageValuesMap::const_iterator it = values_read.begin();
   for (; it != values_read.end(); ++it) {
-    std::u16string key = it->first;
-    base::NullableString16 value = it->second;
-    base::NullableString16 expected_value = expected.find(key)->second;
-    EXPECT_EQ(expected_value.string(), value.string());
-    EXPECT_EQ(expected_value.is_null(), value.is_null());
+    const std::u16string& key = it->first;
+    const base::Optional<std::u16string>& value = it->second;
+    const base::Optional<std::u16string>& expected_value =
+        expected.find(key)->second;
+    EXPECT_EQ(expected_value, value);
   }
 }
 
 void CreateMapWithValues(LegacyDomStorageValuesMap* values) {
-  std::u16string kCannedKeys[] = {ASCIIToUTF16("test"), ASCIIToUTF16("company"),
-                                  ASCIIToUTF16("date"), ASCIIToUTF16("empty")};
-  base::NullableString16 kCannedValues[] = {
-      base::NullableString16(ASCIIToUTF16("123"), false),
-      base::NullableString16(ASCIIToUTF16("Google"), false),
-      base::NullableString16(ASCIIToUTF16("18-01-2012"), false),
-      base::NullableString16(std::u16string(), false)};
-  for (unsigned i = 0; i < sizeof(kCannedKeys) / sizeof(kCannedKeys[0]); i++)
+  std::u16string kCannedKeys[] = {u"test", u"company", u"date", u"empty"};
+  std::u16string kCannedValues[] = {u"123", u"Google", u"18-01-2012", u""};
+  for (unsigned i = 0; i < base::size(kCannedKeys); i++)
     (*values)[kCannedKeys[i]] = kCannedValues[i];
 }
 
@@ -139,7 +135,7 @@
     ASSERT_TRUE(db.CommitChanges(false, storage));
     auto it = storage.begin();
     for (; it != storage.end(); ++it)
-      it->second = base::NullableString16();
+      it->second = base::nullopt;
     ASSERT_TRUE(db.CommitChanges(false, storage));
   }
   EXPECT_FALSE(base::PathExists(file_name));
@@ -161,8 +157,7 @@
   // Reading an empty db should not open the database.
   EXPECT_FALSE(db.IsOpen());
 
-  values[ASCIIToUTF16("key")] =
-      base::NullableString16(ASCIIToUTF16("value"), false);
+  values[u"key"] = u"value";
   db.CommitChanges(false, values);
   // Writing content should open the database.
   EXPECT_TRUE(db.IsOpen());
@@ -208,8 +203,7 @@
 
   // Insert some values, clearing the database first.
   storage.clear();
-  storage[ASCIIToUTF16("another_key")] =
-      base::NullableString16(ASCIIToUTF16("test"), false);
+  storage[u"another_key"] = u"test";
   ASSERT_TRUE(db.CommitChanges(true, storage));
   CheckValuesMatch(&db, storage);
 
@@ -223,8 +217,8 @@
   LegacyDomStorageDatabase db(MakeFilesystemProxy());
 
   ASSERT_TRUE(db.LazyOpen(true));
-  const std::u16string kCannedKey = ASCIIToUTF16("test");
-  const base::NullableString16 kCannedValue(ASCIIToUTF16("data"), false);
+  const std::u16string kCannedKey = u"test";
+  const std::u16string kCannedValue = u"data";
   LegacyDomStorageValuesMap expected;
   expected[kCannedKey] = kCannedValue;
 
@@ -235,7 +229,7 @@
   LegacyDomStorageValuesMap values;
   // A null string in the map should mean that that key gets
   // removed.
-  values[kCannedKey] = base::NullableString16();
+  values[kCannedKey] = base::nullopt;
   EXPECT_TRUE(db.CommitChanges(false, values));
 
   expected.clear();
@@ -267,16 +261,15 @@
   EXPECT_TRUE(db.IsOpen());
   EXPECT_EQ(2u, values.size());
 
-  LegacyDomStorageValuesMap::const_iterator it =
-      values.find(ASCIIToUTF16("value"));
+  LegacyDomStorageValuesMap::const_iterator it = values.find(u"value");
   EXPECT_TRUE(it != values.end());
-  EXPECT_EQ(ASCIIToUTF16("I am in local storage!"), it->second.string());
+  EXPECT_EQ(u"I am in local storage!", it->second.value());
 
-  it = values.find(ASCIIToUTF16("timestamp"));
+  it = values.find(u"timestamp");
   EXPECT_TRUE(it != values.end());
-  EXPECT_EQ(ASCIIToUTF16("1326738338841"), it->second.string());
+  EXPECT_EQ(u"1326738338841", it->second.value());
 
-  it = values.find(ASCIIToUTF16("not_there"));
+  it = values.find(u"not_there");
   EXPECT_TRUE(it == values.end());
 }
 
diff --git a/components/services/storage/dom_storage/local_storage_impl.cc b/components/services/storage/dom_storage/local_storage_impl.cc
index 724475d..33d8bdbb 100644
--- a/components/services/storage/dom_storage/local_storage_impl.cc
+++ b/components/services/storage/dom_storage/local_storage_impl.cc
@@ -128,7 +128,7 @@
   auto values = std::make_unique<StorageAreaImpl::ValueMap>();
   for (const auto& it : map) {
     (*values)[LocalStorageImpl::MigrateString(it.first)] =
-        LocalStorageImpl::MigrateString(it.second.string());
+        LocalStorageImpl::MigrateString(it.second.value());
   }
   reply_task_runner->PostTask(
       FROM_HERE, base::BindOnce(std::move(callback), std::move(values)));
diff --git a/components/services/storage/dom_storage/local_storage_impl_unittest.cc b/components/services/storage/dom_storage/local_storage_impl_unittest.cc
index 0d6741f..08dbed9c 100644
--- a/components/services/storage/dom_storage/local_storage_impl_unittest.cc
+++ b/components/services/storage/dom_storage/local_storage_impl_unittest.cc
@@ -696,8 +696,8 @@
         old_db_path, std::make_unique<FilesystemProxy>(
                          FilesystemProxy::UNRESTRICTED, local_storage_path));
     LegacyDomStorageValuesMap data;
-    data[key] = base::NullableString16(value, false);
-    data[key2] = base::NullableString16(value, false);
+    data[key] = value;
+    data[key2] = value;
     db.CommitChanges(false, data);
   }
   EXPECT_TRUE(base::PathExists(old_db_path));
diff --git a/components/services/storage/dom_storage/session_storage_impl_unittest.cc b/components/services/storage/dom_storage/session_storage_impl_unittest.cc
index dcc4224..49ad460 100644
--- a/components/services/storage/dom_storage/session_storage_impl_unittest.cc
+++ b/components/services/storage/dom_storage/session_storage_impl_unittest.cc
@@ -183,8 +183,8 @@
     auto db = base::MakeRefCounted<TestingLegacySessionStorageDatabase>(
         old_db_path, base::ThreadTaskRunnerHandle::Get().get());
     LegacyDomStorageValuesMap data;
-    data[key] = base::NullableString16(value, false);
-    data[key2] = base::NullableString16(value, false);
+    data[key] = value;
+    data[key2] = value;
     EXPECT_TRUE(db->CommitAreaChanges(namespace_id1, origin1, false, data));
     EXPECT_TRUE(db->CloneNamespace(namespace_id1, namespace_id2));
   }
diff --git a/components/services/storage/dom_storage/session_storage_metadata_unittest.cc b/components/services/storage/dom_storage/session_storage_metadata_unittest.cc
index d45b704..7ebf1ffe 100644
--- a/components/services/storage/dom_storage/session_storage_metadata_unittest.cc
+++ b/components/services/storage/dom_storage/session_storage_metadata_unittest.cc
@@ -485,8 +485,8 @@
   key2.push_back(0xd83d);
   key2.push_back(0xde00);
   LegacyDomStorageValuesMap data;
-  data[key] = base::NullableString16(value, false);
-  data[key2] = base::NullableString16(value, false);
+  data[key] = value;
+  data[key2] = value;
   EXPECT_TRUE(old_ss_database_->CommitAreaChanges(test_namespace1_id_,
                                                   test_origin1_, false, data));
   EXPECT_TRUE(old_ss_database_->CloneNamespace(test_namespace1_id_,
diff --git a/components/services/storage/dom_storage/testing_legacy_session_storage_database.cc b/components/services/storage/dom_storage/testing_legacy_session_storage_database.cc
index aee1163c..436cef3b 100644
--- a/components/services/storage/dom_storage/testing_legacy_session_storage_database.cc
+++ b/components/services/storage/dom_storage/testing_legacy_session_storage_database.cc
@@ -719,15 +719,14 @@
     std::u16string key16 =
         base::UTF8ToUTF16(key.substr(map_start_key.length()));
     if (only_keys) {
-      (*result)[key16] = base::NullableString16();
+      (*result)[key16] = base::nullopt;
     } else {
       // Convert the raw data stored in std::string (it->value()) to raw data
       // stored in std::u16string.
       size_t len = it->value().size() / sizeof(char16_t);
       const char16_t* data_ptr =
           reinterpret_cast<const char16_t*>(it->value().data());
-      (*result)[key16] =
-          base::NullableString16(std::u16string(data_ptr, len), false);
+      (*result)[key16] = std::u16string(data_ptr, len);
     }
   }
   return true;
@@ -738,15 +737,15 @@
     const LegacyDomStorageValuesMap& values,
     leveldb::WriteBatch* batch) {
   for (auto it = values.begin(); it != values.end(); ++it) {
-    base::NullableString16 value = it->second;
+    const base::Optional<std::u16string>& value = it->second;
     std::string key = MapKey(map_id, base::UTF16ToUTF8(it->first));
-    if (value.is_null()) {
+    if (!value.has_value()) {
       batch->Delete(key);
     } else {
       // Convert the raw data stored in std::u16string to raw data stored in
       // std::string.
-      const char* data = reinterpret_cast<const char*>(value.string().data());
-      size_t size = value.string().size() * 2;
+      const char* data = reinterpret_cast<const char*>(value.value().data());
+      size_t size = value.value().size() * 2;
       batch->Put(key, leveldb::Slice(data, size));
     }
   }
diff --git a/components/services/storage/dom_storage/testing_legacy_session_storage_database.h b/components/services/storage/dom_storage/testing_legacy_session_storage_database.h
index 5ed8dd0..21dd47af 100644
--- a/components/services/storage/dom_storage/testing_legacy_session_storage_database.h
+++ b/components/services/storage/dom_storage/testing_legacy_session_storage_database.h
@@ -76,8 +76,8 @@
 
   // Updates the data for |namespace_id| and |origin|. Will remove all keys
   // before updating the database if |clear_all_first| is set. Then all entries
-  // in |changes| will be examined - keys mapped to a null NullableString16 will
-  // be removed and all others will be inserted/updated as appropriate. It is
+  // in |changes| will be examined - keys mapped to a nullopt value will be
+  // removed and all others will be inserted/updated as appropriate. It is
   // allowed to write data into a shallow copy created by CloneNamespace, and in
   // that case the copy will be made deep before writing the values.
   bool CommitAreaChanges(const std::string& namespace_id,
diff --git a/components/viz/common/quads/compositor_render_pass_unittest.cc b/components/viz/common/quads/compositor_render_pass_unittest.cc
index f99301f..0b83d642 100644
--- a/components/viz/common/quads/compositor_render_pass_unittest.cc
+++ b/components/viz/common/quads/compositor_render_pass_unittest.cc
@@ -301,8 +301,8 @@
   auto* quad = pass->quad_list.AllocateAndConstruct<SolidColorDrawQuad>();
   gfx::Rect quad_rect(1, 2, 3, 4);
   quad->SetNew(quad_state, quad_rect, quad_rect, SkColor(), false);
-  pass->ReplaceExistingQuadWithOpaqueTransparentSolidColor(
-      pass->quad_list.begin());
+  pass->ReplaceExistingQuadWithSolidColor(pass->quad_list.begin(), SkColor(),
+                                          SkBlendMode::kSrcOver);
   EXPECT_EQ(pass->quad_list.begin()->rect, quad_rect);
 }
 
@@ -312,10 +312,49 @@
   auto* quad = pass->quad_list.AllocateAndConstruct<SolidColorDrawQuad>();
   gfx::Rect quad_rect(1, 2, 3, 4);
   quad->SetNew(quad_state, quad_rect, quad_rect, SkColor(), false);
-  pass->ReplaceExistingQuadWithOpaqueTransparentSolidColor(
-      pass->quad_list.begin());
+  pass->ReplaceExistingQuadWithSolidColor(pass->quad_list.begin(), SkColor(),
+                                          SkBlendMode::kSrcOver);
   EXPECT_FALSE(pass->quad_list.begin()->shared_quad_state->are_contents_opaque);
 }
 
+TEST(CompositorRenderPassTest, ReplacedQuadsGetColor) {
+  auto pass = CompositorRenderPass::Create();
+  const SharedQuadState* quad_state = pass->CreateAndAppendSharedQuadState();
+  auto* quad = pass->quad_list.AllocateAndConstruct<SolidColorDrawQuad>();
+  const gfx::Rect quad_rect(1, 2, 3, 4);
+  quad->SetNew(quad_state, quad_rect, quad_rect, SK_ColorRED, false);
+  pass->ReplaceExistingQuadWithSolidColor(pass->quad_list.begin(),
+                                          SK_ColorGREEN, SkBlendMode::kSrcOver);
+  EXPECT_EQ(SK_ColorGREEN, quad->color);
+}
+
+TEST(CompositorRenderPassTest, ReplacedQuadsGetBlendMode) {
+  auto pass = CompositorRenderPass::Create();
+  SharedQuadState* quad_state = pass->CreateAndAppendSharedQuadState();
+  // Make |are_contents_opaque| already false, to test that the blend mode is
+  // recognized as a reason for needing a new |SharedQuadState|.
+  quad_state->are_contents_opaque = false;
+  auto* quad = pass->quad_list.AllocateAndConstruct<SolidColorDrawQuad>();
+  const gfx::Rect quad_rect(1, 2, 3, 4);
+  quad->SetNew(quad_state, quad_rect, quad_rect, SkColor(), false);
+  pass->ReplaceExistingQuadWithSolidColor(pass->quad_list.begin(), SkColor(),
+                                          SkBlendMode::kDstOut);
+  EXPECT_EQ(SkBlendMode::kDstOut, quad->shared_quad_state->blend_mode);
+}
+
+TEST(CompositorRenderPassTest,
+     ReplacedQuadsKeepOldSharedQuadStateWhenPossible) {
+  auto pass = CompositorRenderPass::Create();
+  SharedQuadState* quad_state = pass->CreateAndAppendSharedQuadState();
+  quad_state->are_contents_opaque = false;
+  quad_state->blend_mode = SkBlendMode::kSoftLight;
+  auto* quad = pass->quad_list.AllocateAndConstruct<SolidColorDrawQuad>();
+  const gfx::Rect quad_rect(1, 2, 3, 4);
+  quad->SetNew(quad_state, quad_rect, quad_rect, SK_ColorRED, false);
+  pass->ReplaceExistingQuadWithSolidColor(
+      pass->quad_list.begin(), SK_ColorGREEN, SkBlendMode::kSoftLight);
+  EXPECT_EQ(quad_state, quad->shared_quad_state);
+}
+
 }  // namespace
 }  // namespace viz
diff --git a/components/viz/common/quads/draw_quad.h b/components/viz/common/quads/draw_quad.h
index 4900fb6..d80735d 100644
--- a/components/viz/common/quads/draw_quad.h
+++ b/components/viz/common/quads/draw_quad.h
@@ -76,9 +76,13 @@
 
   bool IsDebugQuad() const { return material == Material::kDebugBorder; }
 
-  bool ShouldDrawWithBlending() const {
+  bool ShouldDrawWithBlendingForReasonOtherThanMaskFilter() const {
     return needs_blending || shared_quad_state->opacity < 1.0f ||
-           shared_quad_state->blend_mode != SkBlendMode::kSrcOver ||
+           shared_quad_state->blend_mode != SkBlendMode::kSrcOver;
+  }
+
+  bool ShouldDrawWithBlending() const {
+    return ShouldDrawWithBlendingForReasonOtherThanMaskFilter() ||
            !shared_quad_state->mask_filter_info.IsEmpty();
   }
 
diff --git a/components/viz/common/quads/render_pass_internal.cc b/components/viz/common/quads/render_pass_internal.cc
index 6c9ac6a..ba6383c 100644
--- a/components/viz/common/quads/render_pass_internal.cc
+++ b/components/viz/common/quads/render_pass_internal.cc
@@ -35,24 +35,25 @@
   return shared_quad_state_list.AllocateAndConstruct<SharedQuadState>();
 }
 
-void RenderPassInternal::ReplaceExistingQuadWithOpaqueTransparentSolidColor(
-    QuadList::Iterator at) {
-  // In order to fill the backbuffer with transparent black, the replacement
-  // solid color quad needs to set |needs_blending| to false, and
-  // ShouldDrawWithBlending() returns false so it is drawn without blending.
-  const gfx::Rect rect = at->rect;
-  bool needs_blending = false;
+void RenderPassInternal::ReplaceExistingQuadWithSolidColor(
+    QuadList::Iterator at,
+    SkColor color,
+    SkBlendMode blend_mode) {
   const SharedQuadState* shared_quad_state = at->shared_quad_state;
-  if (shared_quad_state->are_contents_opaque) {
+  if (shared_quad_state->are_contents_opaque ||
+      shared_quad_state->blend_mode != blend_mode) {
     auto* new_shared_quad_state =
         shared_quad_state_list.AllocateAndCopyFrom(shared_quad_state);
     new_shared_quad_state->are_contents_opaque = false;
+    new_shared_quad_state->blend_mode = blend_mode;
     shared_quad_state = new_shared_quad_state;
   }
 
-  auto* replacement = quad_list.ReplaceExistingElement<SolidColorDrawQuad>(at);
-  replacement->SetAll(shared_quad_state, rect, rect /* visible_rect */,
-                      needs_blending, SK_ColorTRANSPARENT, true);
+  const gfx::Rect rect = at->rect;
+  quad_list.ReplaceExistingElement<SolidColorDrawQuad>(at)->SetAll(
+      shared_quad_state, rect, /*visible_rect=*/rect,
+      /*needs_blending=*/false, color,
+      /*force_anti_aliasing_off=*/true);
 }
 
 }  // namespace viz
diff --git a/components/viz/common/quads/render_pass_internal.h b/components/viz/common/quads/render_pass_internal.h
index a3a1bbf3..9165c5c48 100644
--- a/components/viz/common/quads/render_pass_internal.h
+++ b/components/viz/common/quads/render_pass_internal.h
@@ -31,9 +31,10 @@
  public:
   SharedQuadState* CreateAndAppendSharedQuadState();
 
-  // Replaces a quad in |quad_list| with a transparent black SolidColorQuad.
-  void ReplaceExistingQuadWithOpaqueTransparentSolidColor(
-      QuadList::Iterator at);
+  // Replaces a quad in |quad_list| with a |SolidColorDrawQuad|.
+  void ReplaceExistingQuadWithSolidColor(QuadList::Iterator at,
+                                         SkColor color,
+                                         SkBlendMode blend_mode);
 
   // These are in the space of the render pass' physical pixels.
   gfx::Rect output_rect;
diff --git a/components/viz/service/display/ca_layer_overlay.cc b/components/viz/service/display/ca_layer_overlay.cc
index 171b2e6..284d8f0 100644
--- a/components/viz/service/display/ca_layer_overlay.cc
+++ b/components/viz/service/display/ca_layer_overlay.cc
@@ -363,7 +363,8 @@
         break;
       }
 
-      render_pass->ReplaceExistingQuadWithOpaqueTransparentSolidColor(it);
+      render_pass->ReplaceExistingQuadWithSolidColor(it, SK_ColorTRANSPARENT,
+                                                     SkBlendMode::kSrcOver);
       ca_layer_overlays->push_back(ca_layer);
     }
   }
diff --git a/components/viz/service/display/dc_layer_overlay.cc b/components/viz/service/display/dc_layer_overlay.cc
index d51453f3..4ef9ac4 100644
--- a/components/viz/service/display/dc_layer_overlay.cc
+++ b/components/viz/service/display/dc_layer_overlay.cc
@@ -890,7 +890,8 @@
     // When the opacity == 1.0, drawing with transparent will be done without
     // blending and will have the proper effect of completely clearing the
     // layer.
-    render_pass->ReplaceExistingQuadWithOpaqueTransparentSolidColor(it);
+    render_pass->ReplaceExistingQuadWithSolidColor(it, SK_ColorTRANSPARENT,
+                                                   SkBlendMode::kSrcOver);
     is_opaque = true;
   }
 
diff --git a/components/viz/service/display/overlay_candidate.cc b/components/viz/service/display/overlay_candidate.cc
index fa02f20..49d1388 100644
--- a/components/viz/service/display/overlay_candidate.cc
+++ b/components/viz/service/display/overlay_candidate.cc
@@ -145,9 +145,6 @@
   if (sqs->opacity != 1.f)
     return false;
 
-  // We can't support overlays with mask filter.
-  if (!sqs->mask_filter_info.IsEmpty())
-    return false;
   // We support only kSrc (no blending) and kSrcOver (blending with premul).
   if (!(sqs->blend_mode == SkBlendMode::kSrc ||
         sqs->blend_mode == SkBlendMode::kSrcOver)) {
@@ -315,7 +312,9 @@
 
   candidate->clip_rect = sqs->clip_rect;
   candidate->is_clipped = sqs->is_clipped;
-  candidate->is_opaque = !quad->ShouldDrawWithBlending();
+  candidate->is_opaque =
+      !quad->ShouldDrawWithBlendingForReasonOtherThanMaskFilter();
+  candidate->has_mask_filter = !sqs->mask_filter_info.IsEmpty();
   // For underlays the function 'EstimateVisibleDamage()' is called to update
   // |damage_area_estimate| to more accurately reflect the actual visible
   // damage.
@@ -345,6 +344,10 @@
   candidate->display_rect = gfx::RectF(quad->rect);
   transform.TransformRect(&candidate->display_rect);
   candidate->transform = overlay_transform;
+  candidate->is_opaque =
+      !quad->ShouldDrawWithBlendingForReasonOtherThanMaskFilter();
+  candidate->has_mask_filter =
+      !quad->shared_quad_state->mask_filter_info.IsEmpty();
   // For underlays the function 'EstimateVisibleDamage()' is called to update
   // |damage_area_estimate| to more accurately reflect the actual visible
   // damage.
diff --git a/components/viz/service/display/overlay_candidate.h b/components/viz/service/display/overlay_candidate.h
index 0d7ee6be..c04e71bd 100644
--- a/components/viz/service/display/overlay_candidate.h
+++ b/components/viz/service/display/overlay_candidate.h
@@ -101,6 +101,8 @@
   bool is_clipped = false;
   // If the quad doesn't require blending.
   bool is_opaque = false;
+  // If the quad has a mask filter.
+  bool has_mask_filter = false;
   // Texture resource to present in an overlay.
   ResourceId resource_id = kInvalidResourceId;
   // Mailbox from resource_id. It is used by SkiaRenderer.
diff --git a/components/viz/service/display/overlay_processor_using_strategy.cc b/components/viz/service/display/overlay_processor_using_strategy.cc
index 3c41bbe..a6488c6 100644
--- a/components/viz/service/display/overlay_processor_using_strategy.cc
+++ b/components/viz/service/display/overlay_processor_using_strategy.cc
@@ -240,12 +240,14 @@
   DCHECK_LE(candidates->size(), 1U);
 
   gfx::Rect this_frame_overlay_rect;
+  bool has_mask_filter = false;
   bool is_opaque_overlay = false;
   bool is_underlay = false;
   uint32_t exclude_overlay_index = OverlayCandidate::kInvalidDamageIndex;
 
   for (const OverlayCandidate& overlay : *candidates) {
     this_frame_overlay_rect = GetOverlayDamageRectForOutputSurface(overlay);
+    has_mask_filter = overlay.has_mask_filter;
     if (overlay.plane_z_order >= 0) {
       // If an overlay candidate comes from output surface, its z-order should
       // be 0.
@@ -272,13 +274,11 @@
       exclude_overlay_index, surface_damage_rect_list, *damage_rect,
       this_frame_overlay_rect, is_opaque_overlay && !is_underlay);
 
-  // Track the overlay_rect from frame to frame. If it is the same and nothing
-  // is on top of it then that rect doesn't need to be damaged because the
-  // drawing is occurring on a different plane. If it is different then that
-  // indicates that a different overlay has been chosen and the previous
-  // overlay rect should be damaged because it has changed planes from the
-  // overlay plane to the main plane. https://crbug.com/875879
+  // Drawing on the overlay_rect usually occurs on a different plane, but we
+  // still need to damage the overlay_rect when certain changes occur from one
+  // frame to the next.  https://crbug.com/875879  https://crbug.com/1107460
   if ((!previous_is_underlay && is_underlay) ||
+      has_mask_filter != previous_has_mask_filter_ ||
       this_frame_overlay_rect != previous_frame_overlay_rect_) {
     damage_rect->Union(previous_frame_overlay_rect_);
 
@@ -293,6 +293,7 @@
   }
 
   previous_frame_overlay_rect_ = this_frame_overlay_rect;
+  previous_has_mask_filter_ = has_mask_filter;
   previous_is_underlay = is_underlay;
 }
 
diff --git a/components/viz/service/display/overlay_processor_using_strategy.h b/components/viz/service/display/overlay_processor_using_strategy.h
index 2b9220d..9ed5151 100644
--- a/components/viz/service/display/overlay_processor_using_strategy.h
+++ b/components/viz/service/display/overlay_processor_using_strategy.h
@@ -175,6 +175,7 @@
 
   gfx::Rect overlay_damage_rect_;
   bool previous_is_underlay = false;
+  bool previous_has_mask_filter_ = false;
   gfx::Rect previous_frame_overlay_rect_;
 
   struct OverlayPrioritizationConfig {
diff --git a/components/viz/service/display/overlay_strategy_single_on_top.cc b/components/viz/service/display/overlay_strategy_single_on_top.cc
index 5303586..0a29619 100644
--- a/components/viz/service/display/overlay_strategy_single_on_top.cc
+++ b/components/viz/service/display/overlay_strategy_single_on_top.cc
@@ -41,6 +41,7 @@
     if (OverlayCandidate::FromDrawQuad(
             resource_provider, surface_damage_rect_list, output_color_matrix,
             *it, GetPrimaryPlaneDisplayRect(primary_plane), &candidate) &&
+        !candidate.has_mask_filter &&
         !OverlayCandidate::IsOccluded(candidate, quad_list->cbegin(), it)) {
       // If the candidate has been promoted previously and has not changed
       // (resource ID is the same) for 3 frames, do not use it as Overlay as
@@ -95,6 +96,7 @@
     if (OverlayCandidate::FromDrawQuad(
             resource_provider, surface_damage_rect_list, output_color_matrix,
             *it, GetPrimaryPlaneDisplayRect(primary_plane), &candidate) &&
+        !candidate.has_mask_filter &&
         !OverlayCandidate::IsOccluded(candidate, quad_list->cbegin(), it)) {
       candidates->push_back({it, candidate, this});
     }
diff --git a/components/viz/service/display/overlay_strategy_underlay.cc b/components/viz/service/display/overlay_strategy_underlay.cc
index 4008f9f..15080528 100644
--- a/components/viz/service/display/overlay_strategy_underlay.cc
+++ b/components/viz/service/display/overlay_strategy_underlay.cc
@@ -75,9 +75,15 @@
     }
 
     // If the candidate can be handled by an overlay, create a pass for it. We
-    // need to switch out the video quad with a black transparent one.
+    // need to switch out the video quad with an underlay hole quad.
     if (new_candidate_list.back().overlay_handled) {
-      render_pass->ReplaceExistingQuadWithOpaqueTransparentSolidColor(it);
+      if (candidate.has_mask_filter) {
+        render_pass->ReplaceExistingQuadWithSolidColor(it, SK_ColorBLACK,
+                                                       SkBlendMode::kDstOut);
+      } else {
+        render_pass->ReplaceExistingQuadWithSolidColor(it, SK_ColorTRANSPARENT,
+                                                       SkBlendMode::kSrcOver);
+      }
       candidate_list->swap(new_candidate_list);
       return true;
     }
@@ -164,10 +170,16 @@
   }
 
   // If the candidate can be handled by an overlay, create a pass for it. We
-  // need to switch out the video quad with a black transparent one.
+  // need to switch out the video quad with an underlay hole quad.
   if (new_candidate_list.back().overlay_handled) {
-    render_pass->ReplaceExistingQuadWithOpaqueTransparentSolidColor(
-        proposed_candidate->quad_iter);
+    if (proposed_candidate->candidate.has_mask_filter) {
+      render_pass->ReplaceExistingQuadWithSolidColor(
+          proposed_candidate->quad_iter, SK_ColorBLACK, SkBlendMode::kDstOut);
+    } else {
+      render_pass->ReplaceExistingQuadWithSolidColor(
+          proposed_candidate->quad_iter, SK_ColorTRANSPARENT,
+          SkBlendMode::kSrcOver);
+    }
     candidate_list->swap(new_candidate_list);
 
     return true;
diff --git a/components/viz/service/display/overlay_strategy_underlay_cast.cc b/components/viz/service/display/overlay_strategy_underlay_cast.cc
index bae8d3a..045f9220 100644
--- a/components/viz/service/display/overlay_strategy_underlay_cast.cc
+++ b/components/viz/service/display/overlay_strategy_underlay_cast.cc
@@ -122,7 +122,13 @@
           VideoHoleDrawQuad::MaterialCast(*it)->overlay_plane_id);
 #endif
 
-      render_pass->ReplaceExistingQuadWithOpaqueTransparentSolidColor(it);
+      if (candidate.has_mask_filter) {
+        render_pass->ReplaceExistingQuadWithSolidColor(it, SK_ColorBLACK,
+                                                       SkBlendMode::kDstOut);
+      } else {
+        render_pass->ReplaceExistingQuadWithSolidColor(it, SK_ColorTRANSPARENT,
+                                                       SkBlendMode::kSrcOver);
+      }
 
       break;
     }
@@ -255,7 +261,13 @@
           VideoHoleDrawQuad::MaterialCast(*it)->overlay_plane_id);
 #endif
 
-      render_pass->ReplaceExistingQuadWithOpaqueTransparentSolidColor(it);
+      if (candidate.has_mask_filter) {
+        render_pass->ReplaceExistingQuadWithSolidColor(it, SK_ColorBLACK,
+                                                       SkBlendMode::kDstOut);
+      } else {
+        render_pass->ReplaceExistingQuadWithSolidColor(it, SK_ColorTRANSPARENT,
+                                                       SkBlendMode::kSrcOver);
+      }
 
       break;
     }
diff --git a/components/viz/service/display/overlay_unittest.cc b/components/viz/service/display/overlay_unittest.cc
index d0f4a8c..a9565798 100644
--- a/components/viz/service/display/overlay_unittest.cc
+++ b/components/viz/service/display/overlay_unittest.cc
@@ -2190,6 +2190,56 @@
   }
 }
 
+// A candidate with a mask filter must go to underlay, and not single on top.
+// Also, the quad must be replaced by a black quad with |SkBlendMode::kDstOut|.
+TEST_F(TransitionOverlayTypeTest, MaskFilterBringsUnderlay) {
+  auto pass = CreateRenderPass();
+  damage_rect_ = kOverlayRect;
+
+  SharedQuadState* default_damaged_shared_quad_state =
+      pass->shared_quad_state_list.AllocateAndCopyFrom(
+          pass->shared_quad_state_list.back());
+
+  SurfaceDamageRectList surface_damage_rect_list;
+  auto* sqs = pass->shared_quad_state_list.front();
+  sqs->overlay_damage_index = 0;
+  surface_damage_rect_list.emplace_back(damage_rect_);
+  sqs->mask_filter_info =
+      gfx::MaskFilterInfo(gfx::RectF(kOverlayRect), gfx::RoundedCornersF(1.f));
+  CreateFullscreenCandidateQuad(resource_provider_.get(),
+                                child_resource_provider_.get(),
+                                child_provider_.get(), sqs, pass.get());
+  CreateFullscreenOpaqueQuad(resource_provider_.get(),
+                             default_damaged_shared_quad_state, pass.get());
+
+  OverlayCandidateList candidate_list;
+  OverlayProcessorInterface::FilterOperationsMap render_pass_filters;
+  OverlayProcessorInterface::FilterOperationsMap render_pass_backdrop_filters;
+  AggregatedRenderPassList pass_list;
+  pass_list.push_back(std::move(pass));
+
+  overlay_processor_->ProcessForOverlays(
+      resource_provider_.get(), &pass_list, GetIdentityColorMatrix(),
+      render_pass_filters, render_pass_backdrop_filters,
+      std::move(surface_damage_rect_list), nullptr, &candidate_list,
+      &damage_rect_, &content_bounds_);
+
+  ASSERT_EQ(candidate_list.size(), 1U);
+  EXPECT_LT(candidate_list.front().plane_z_order, 0);
+
+  ASSERT_EQ(1U, pass_list.size());
+  ASSERT_EQ(2U, pass_list.front()->quad_list.size());
+  EXPECT_EQ(SK_ColorBLACK, static_cast<SolidColorDrawQuad*>(
+                               pass_list.front()->quad_list.front())
+                               ->color);
+  EXPECT_FALSE(pass_list.front()
+                   ->quad_list.front()
+                   ->shared_quad_state->are_contents_opaque);
+  EXPECT_EQ(
+      SkBlendMode::kDstOut,
+      pass_list.front()->quad_list.front()->shared_quad_state->blend_mode);
+}
+
 // The first time an underlay is scheduled its damage must not be excluded.
 TEST_F(UnderlayTest, InitialUnderlayDamageNotExcluded) {
   auto pass = CreateRenderPass();
@@ -2372,6 +2422,58 @@
   }
 }
 
+// Underlay damage cannot be excluded if the underlay has a mask filter in the
+// current frame but did not in the previous frame or vice versa.
+TEST_F(
+    UnderlayTest,
+    DamageNotExcludedForConsecutiveUnderlaysIfOneHasMaskFilterAndOtherDoesNot) {
+  constexpr bool kHasMaskFilter[] = {true, false, true,  false, true,
+                                     true, true,  false, false, false};
+
+  for (int i = 0; i < 10; ++i) {
+    SCOPED_TRACE(i);
+
+    auto pass = CreateRenderPass();
+    damage_rect_ = kOverlayRect;
+
+    SharedQuadState* default_damaged_shared_quad_state =
+        pass->shared_quad_state_list.AllocateAndCopyFrom(
+            pass->shared_quad_state_list.back());
+
+    SurfaceDamageRectList surface_damage_rect_list;
+    auto* sqs = pass->shared_quad_state_list.front();
+    sqs->overlay_damage_index = 0;
+    surface_damage_rect_list.emplace_back(damage_rect_);
+    if (kHasMaskFilter[i]) {
+      sqs->mask_filter_info = gfx::MaskFilterInfo(gfx::RectF(kOverlayRect),
+                                                  gfx::RoundedCornersF(1.f));
+    }
+    CreateCandidateQuadAt(resource_provider_.get(),
+                          child_resource_provider_.get(), child_provider_.get(),
+                          sqs, pass.get(), kOverlayRect);
+    CreateFullscreenOpaqueQuad(resource_provider_.get(),
+                               default_damaged_shared_quad_state, pass.get());
+
+    OverlayCandidateList candidate_list;
+    OverlayProcessorInterface::FilterOperationsMap render_pass_filters;
+    OverlayProcessorInterface::FilterOperationsMap render_pass_backdrop_filters;
+    AggregatedRenderPassList pass_list;
+    pass_list.push_back(std::move(pass));
+
+    overlay_processor_->ProcessForOverlays(
+        resource_provider_.get(), &pass_list, GetIdentityColorMatrix(),
+        render_pass_filters, render_pass_backdrop_filters,
+        std::move(surface_damage_rect_list), nullptr, &candidate_list,
+        &damage_rect_, &content_bounds_);
+    if (i == 0) {
+      EXPECT_FALSE(damage_rect_.IsEmpty());
+    } else {
+      EXPECT_EQ(damage_rect_.IsEmpty(),
+                kHasMaskFilter[i] == kHasMaskFilter[i - 1]);
+    }
+  }
+}
+
 TEST_F(UnderlayTest, DamageExcludedForCandidateAndThoseOccluded) {
   for (int i = 0; i < 3; ++i) {
     SCOPED_TRACE(i);
@@ -3067,6 +3169,45 @@
   ASSERT_TRUE(candidate_list.empty());
   EXPECT_TRUE(content_bounds_.empty());
 }
+
+TEST_F(UnderlayCastTest, OverlayPromotionWithMaskFilter) {
+  auto pass = CreateRenderPass();
+
+  SurfaceDamageRectList surface_damage_rect_list;
+  auto* sqs = pass->shared_quad_state_list.front();
+  sqs->overlay_damage_index = 0;
+  surface_damage_rect_list.emplace_back(damage_rect_);
+  sqs->mask_filter_info =
+      gfx::MaskFilterInfo(gfx::RectF(kOverlayRect), gfx::RoundedCornersF(1.f));
+  CreateVideoHoleDrawQuadAt(sqs, pass.get(), kOverlayRect);
+
+  OverlayCandidateList candidate_list;
+  OverlayProcessorInterface::FilterOperationsMap render_pass_filters;
+  OverlayProcessorInterface::FilterOperationsMap render_pass_backdrop_filters;
+  AggregatedRenderPassList pass_list;
+  pass_list.push_back(std::move(pass));
+
+  overlay_processor_->ProcessForOverlays(
+      resource_provider_.get(), &pass_list, GetIdentityColorMatrix(),
+      render_pass_filters, render_pass_backdrop_filters,
+      std::move(surface_damage_rect_list), nullptr, &candidate_list,
+      &damage_rect_, &content_bounds_);
+
+  ASSERT_EQ(1U, content_bounds_.size());
+  EXPECT_TRUE(content_bounds_.front().IsEmpty());
+
+  ASSERT_EQ(1U, pass_list.size());
+  ASSERT_EQ(1U, pass_list.front()->quad_list.size());
+  EXPECT_EQ(SK_ColorBLACK, static_cast<SolidColorDrawQuad*>(
+                               pass_list.front()->quad_list.front())
+                               ->color);
+  EXPECT_FALSE(pass_list.front()
+                   ->quad_list.front()
+                   ->shared_quad_state->are_contents_opaque);
+  EXPECT_EQ(
+      SkBlendMode::kDstOut,
+      pass_list.front()->quad_list.front()->shared_quad_state->blend_mode);
+}
 #endif
 
 #if defined(ALWAYS_ENABLE_BLENDING_FOR_PRIMARY)
diff --git a/components/viz/service/display/surface_aggregator_unittest.cc b/components/viz/service/display/surface_aggregator_unittest.cc
index 7cbe095..5d6a31a6 100644
--- a/components/viz/service/display/surface_aggregator_unittest.cc
+++ b/components/viz/service/display/surface_aggregator_unittest.cc
@@ -41,6 +41,7 @@
 #include "components/viz/service/surfaces/pending_copy_output_request.h"
 #include "components/viz/service/surfaces/surface.h"
 #include "components/viz/service/surfaces/surface_manager.h"
+#include "components/viz/test/begin_frame_args_test.h"
 #include "components/viz/test/compositor_frame_helpers.h"
 #include "components/viz/test/fake_compositor_frame_sink_client.h"
 #include "components/viz/test/fake_surface_observer.h"
@@ -115,6 +116,8 @@
       base::TimeTicks() + base::TimeDelta::FromSeconds(1);
 };
 
+}  // namespace
+
 class SurfaceAggregatorTest : public testing::Test, public DisplayTimeSource {
  public:
   explicit SurfaceAggregatorTest(bool use_damage_rect)
@@ -5597,6 +5600,16 @@
                                   gfx::OVERLAY_TRANSFORM_NONE);
   }
 
+  void SendBeginFrame(CompositorFrameSinkSupport* support, uint64_t id) {
+    BeginFrameArgs args =
+        CreateBeginFrameArgsForTesting(BEGINFRAME_FROM_HERE, 0, id);
+    support->OnBeginFrame(args);
+  }
+
+  void EnableDocumentTransitions(CompositorFrameSinkSupport* support) {
+    support->document_transitions_enabled_ = true;
+  }
+
  protected:
   ServerSharedBitmapManager shared_bitmap_manager_;
   FrameSinkManagerImpl manager_;
@@ -5604,12 +5617,10 @@
   std::unique_ptr<SurfaceAggregator> aggregator_;
 };
 
-void SubmitCompositorFrameWithResources(
+CompositorFrame BuildCompositorFrameWithResources(
     const std::vector<ResourceId>& resource_ids,
     bool valid,
-    SurfaceId child_id,
-    CompositorFrameSinkSupport* support,
-    SurfaceId surface_id) {
+    SurfaceId child_id) {
   CompositorFrame frame = MakeEmptyCompositorFrame();
   auto pass = CompositorRenderPass::Create();
   pass->SetNew(CompositorRenderPassId{1}, gfx::Rect(0, 0, 20, 20), gfx::Rect(),
@@ -5653,6 +5664,16 @@
                  secure_output_only, protected_video_type);
   }
   frame.render_pass_list.push_back(std::move(pass));
+  return frame;
+}
+
+void SubmitCompositorFrameWithResources(
+    const std::vector<ResourceId>& resource_ids,
+    bool valid,
+    SurfaceId child_id,
+    CompositorFrameSinkSupport* support,
+    SurfaceId surface_id) {
+  auto frame = BuildCompositorFrameWithResources(resource_ids, valid, child_id);
   support->SubmitCompositorFrame(surface_id.local_surface_id(),
                                  std::move(frame));
 }
@@ -8947,8 +8968,94 @@
   }
 }
 
+TEST_F(SurfaceAggregatorWithResourcesTest, TransitionDirectiveFrameBehind) {
+  FakeCompositorFrameSinkClient client;
+  auto support = std::make_unique<CompositorFrameSinkSupport>(
+      &client, &manager_, kArbitraryRootFrameSinkId, kRootIsRoot);
+  EnableDocumentTransitions(support.get());
+
+  LocalSurfaceId local_surface_id(7u, base::UnguessableToken::Create());
+  SurfaceId surface_id(support->frame_sink_id(), local_surface_id);
+
+  // Create and submit a 'save' frame.
+  SendBeginFrame(support.get(), 1);
+  {
+    auto frame = BuildCompositorFrameWithResources({}, true, SurfaceId());
+    frame.metadata.transition_directives.emplace_back(
+        1, CompositorFrameTransitionDirective::Type::kSave,
+        CompositorFrameTransitionDirective::Effect::kCoverLeft,
+        base::TimeDelta::FromMilliseconds(500));
+
+    support->SubmitCompositorFrame(local_surface_id, std::move(frame));
+    auto* surface = support->GetLastCreatedSurfaceForTesting();
+    ASSERT_TRUE(surface);
+    surface->GetSurfaceSavedFrameStorage()->CompleteForTesting();
+  }
+  AggregateFrame(surface_id);
+
+  // Create and submit an 'animate' frame.
+  SendBeginFrame(support.get(), 2);
+  {
+    auto frame = BuildCompositorFrameWithResources({}, true, SurfaceId());
+    frame.metadata.transition_directives.emplace_back(
+        2, CompositorFrameTransitionDirective::Type::kAnimate);
+    support->SubmitCompositorFrame(local_surface_id, std::move(frame));
+  }
+  AggregateFrame(surface_id);
+
+  // Create and submit a frame with some resources.
+  SendBeginFrame(support.get(), 3);
+  {
+    std::vector<ResourceId> ids = {ResourceId(11), ResourceId(12),
+                                   ResourceId(13)};
+    SubmitCompositorFrameWithResources(ids, true, SurfaceId(), support.get(),
+                                       surface_id);
+  }
+  auto frame = AggregateFrame(surface_id);
+  auto count_textures = [](const AggregatedFrame& frame) {
+    size_t result = 0;
+    for (auto& render_pass : frame.render_pass_list) {
+      for (auto* quad : render_pass->quad_list) {
+        if (quad->material == DrawQuad::Material::kTextureContent)
+          ++result;
+      }
+    }
+    return result;
+  };
+  // We should have 4 referenced textures (1 from interpolation and 3 from the
+  // original frame).
+  EXPECT_EQ(count_textures(frame), 4u);
+
+  // At this point we will interpolate with the above frame (resources 11, 12,
+  // 13).
+  SendBeginFrame(support.get(), 4);
+  {
+    std::vector<ResourceId> ids = {ResourceId(15), ResourceId(16),
+                                   ResourceId(17)};
+    // This will cause an activation which will unref 11, 12, 13. So, the
+    // activation must also interpolate a new frame.
+    SubmitCompositorFrameWithResources(ids, true, SurfaceId(), support.get(),
+                                       surface_id);
+  }
+  // Ensure that the interpolated frame is not using unreffed resources
+  // (otherwise this would DCHECK).
+  frame = AggregateFrame(surface_id);
+  // We should have 4 referenced textures (1 from interpolation and 3 (different
+  // ones) from the original frame).
+  EXPECT_EQ(count_textures(frame), 4u);
+
+  ASSERT_EQ(3u, client.returned_resources().size());
+  ResourceId returned_ids[3];
+  for (size_t i = 0; i < 3; ++i) {
+    returned_ids[i] = client.returned_resources()[i].id;
+  }
+  // We expect that 11, 12, and 13 are now returned.
+  EXPECT_THAT(returned_ids,
+              testing::WhenSorted(testing::ElementsAreArray(
+                  {ResourceId(11), ResourceId(12), ResourceId(13)})));
+}
+
 INSTANTIATE_TEST_SUITE_P(,
                          SurfaceAggregatorValidSurfaceWithMergingPassesTest,
                          testing::Bool());
-}  // namespace
 }  // namespace viz
diff --git a/components/viz/service/frame_sinks/compositor_frame_sink_support.cc b/components/viz/service/frame_sinks/compositor_frame_sink_support.cc
index c2a02f2d..7018272 100644
--- a/components/viz/service/frame_sinks/compositor_frame_sink_support.cc
+++ b/components/viz/service/frame_sinks/compositor_frame_sink_support.cc
@@ -161,12 +161,10 @@
     bool started_animation =
         surface_animation_manager_.ProcessTransitionDirectives(
             transition_directives, surface->GetSurfaceSavedFrameStorage());
-    // If processing the new directives caused us to start an animation, then
-    // interpoate the frame immediately. This is needed since if we wait until
-    // the next BeginFrame to do the first interpolation, then we maybe have
-    // already drawn this destination frame.
-    if (started_animation)
-      surface_animation_manager_.InterpolateFrame(surface);
+
+    // If we started an animation, then we must need a begin frame for the code
+    // below to work properly.
+    DCHECK(!started_animation || surface_animation_manager_.NeedsBeginFrame());
 
     // The above call can cause us to start an animation, meaning we need begin
     // frames. If that's the case, make sure to update the begin frame
@@ -175,6 +173,18 @@
       UpdateNeedsBeginFramesInternal();
   }
 
+  // If surface animation manager needs a frame, then we should interpolate
+  // here. Note that we also interpolate in OnBeginFrame. The reason for two
+  // calls is that we might not receive and active a frame from the client in
+  // time to draw, which is why OnBeginFrame interpolates and damages the
+  // surface. Here, we only interpolate in case we did receive a new frame. We
+  // always use the latest frame, because it may have new resources that need to
+  // be reffed by SurfaceAggregator. If we don't do this, then they will be
+  // unreffed and cleaned up causing a DCHECK in subsequent frames that do use
+  // the frame.
+  if (surface_animation_manager_.NeedsBeginFrame())
+    surface_animation_manager_.InterpolateFrame(surface);
+
   if (surface->surface_id() == last_activated_surface_id_)
     return;
 
diff --git a/components/viz/service/frame_sinks/compositor_frame_sink_support.h b/components/viz/service/frame_sinks/compositor_frame_sink_support.h
index 9acefecb..2fdd13b 100644
--- a/components/viz/service/frame_sinks/compositor_frame_sink_support.h
+++ b/components/viz/service/frame_sinks/compositor_frame_sink_support.h
@@ -212,6 +212,7 @@
   friend class CompositorFrameSinkSupportTest;
   friend class DisplayTest;
   friend class FrameSinkManagerTest;
+  friend class SurfaceAggregatorWithResourcesTest;
 
   // Creates a surface reference from the top-level root to |surface_id|.
   SurfaceReference MakeTopLevelRootReference(const SurfaceId& surface_id);
@@ -369,7 +370,7 @@
   std::unique_ptr<power_scheduler::PowerModeVoter> animation_power_mode_voter_;
 
   // Represents whether the DocumentTransition feature is enabled.
-  const bool document_transitions_enabled_;
+  bool document_transitions_enabled_;
 
   base::WeakPtrFactory<CompositorFrameSinkSupport> weak_factory_{this};
 
diff --git a/components/viz/service/surfaces/surface_saved_frame.cc b/components/viz/service/surfaces/surface_saved_frame.cc
index 54fb8d08..a4872013 100644
--- a/components/viz/service/surfaces/surface_saved_frame.cc
+++ b/components/viz/service/surfaces/surface_saved_frame.cc
@@ -32,9 +32,7 @@
 }
 
 bool SurfaceSavedFrame::IsValid() const {
-  // TODO(crbug.com/1174129): This needs to be updated with software copies as
-  // well.
-  return HasTextureResult();
+  return result_.has_value();
 }
 
 void SurfaceSavedFrame::RequestCopyOfOutput(Surface* surface) {
@@ -63,45 +61,44 @@
     return;
 
   auto copy_output_texture = *result->GetTextureResult();
-  texture_result_.mailbox = copy_output_texture.mailbox;
-  texture_result_.sync_token = copy_output_texture.sync_token;
-  texture_result_.size = result->size();
-  texture_result_.release_callback = result->TakeTextureOwnership();
+  DCHECK(!result_.has_value());
+  result_.emplace();
+  result_->mailbox = copy_output_texture.mailbox;
+  result_->sync_token = copy_output_texture.sync_token;
+  result_->size = result->size();
+  result_->release_callback = result->TakeTextureOwnership();
+  result_->is_software = false;
 }
 
-bool SurfaceSavedFrame::HasTextureResult() const {
-  return texture_result_.release_callback && !texture_result_.mailbox.IsZero();
-}
-
-SurfaceSavedFrame::TextureResult SurfaceSavedFrame::TakeTextureResult() {
-  DCHECK(HasTextureResult());
-  // Note that the TextureResult move constructor resets sufficient state in the
-  // member so that HasTextureResult() returns false afterwards, effectively
-  // clearing the member variable.
-  return std::move(texture_result_);
+base::Optional<SurfaceSavedFrame::OutputCopyResult>
+SurfaceSavedFrame::TakeResult() {
+  return std::move(result_);
 }
 
 void SurfaceSavedFrame::CompleteSavedFrameForTesting(
     base::OnceCallback<void(const gpu::SyncToken&, bool)> release_callback) {
-  texture_result_.mailbox = gpu::Mailbox::GenerateForSharedImage();
-  texture_result_.release_callback =
+  result_.emplace();
+  result_->mailbox = gpu::Mailbox::GenerateForSharedImage();
+  result_->release_callback =
       SingleReleaseCallback::Create(std::move(release_callback));
-  texture_result_.size = kDefaultTextureSizeForTesting;
+  result_->size = kDefaultTextureSizeForTesting;
+  result_->is_software = true;
   DCHECK(IsValid());
 }
 
-SurfaceSavedFrame::TextureResult::TextureResult() = default;
-SurfaceSavedFrame::TextureResult::TextureResult(TextureResult&& other) {
+SurfaceSavedFrame::OutputCopyResult::OutputCopyResult() = default;
+SurfaceSavedFrame::OutputCopyResult::OutputCopyResult(
+    OutputCopyResult&& other) {
   *this = std::move(other);
 }
 
-SurfaceSavedFrame::TextureResult::~TextureResult() {
+SurfaceSavedFrame::OutputCopyResult::~OutputCopyResult() {
   if (release_callback)
     release_callback->Run(sync_token, /*is_lost=*/false);
 }
 
-SurfaceSavedFrame::TextureResult& SurfaceSavedFrame::TextureResult::operator=(
-    TextureResult&& other) {
+SurfaceSavedFrame::OutputCopyResult&
+SurfaceSavedFrame::OutputCopyResult::operator=(OutputCopyResult&& other) {
   mailbox = std::move(other.mailbox);
   other.mailbox = gpu::Mailbox();
 
@@ -111,6 +108,9 @@
   size = std::move(other.size);
 
   release_callback = std::move(other.release_callback);
+
+  is_software = other.is_software;
+  other.is_software = false;
   return *this;
 }
 
diff --git a/components/viz/service/surfaces/surface_saved_frame.h b/components/viz/service/surfaces/surface_saved_frame.h
index 4eff862..343e6f7 100644
--- a/components/viz/service/surfaces/surface_saved_frame.h
+++ b/components/viz/service/surfaces/surface_saved_frame.h
@@ -9,6 +9,7 @@
 
 #include "base/compiler_specific.h"
 #include "base/memory/weak_ptr.h"
+#include "base/optional.h"
 #include "base/time/time.h"
 #include "components/viz/common/frame_sinks/copy_output_result.h"
 #include "components/viz/common/quads/compositor_frame_transition_directive.h"
@@ -24,16 +25,17 @@
   using TransitionDirectiveCompleteCallback =
       base::RepeatingCallback<void(uint32_t)>;
 
-  struct TextureResult {
-    TextureResult();
-    TextureResult(TextureResult&& other);
-    ~TextureResult();
+  struct OutputCopyResult {
+    OutputCopyResult();
+    OutputCopyResult(OutputCopyResult&& other);
+    ~OutputCopyResult();
 
-    TextureResult& operator=(TextureResult&& other);
+    OutputCopyResult& operator=(OutputCopyResult&& other);
 
     gpu::Mailbox mailbox;
     gpu::SyncToken sync_token;
     gfx::Size size;
+    bool is_software = false;
     std::unique_ptr<SingleReleaseCallback> release_callback;
   };
 
@@ -50,10 +52,7 @@
   // frame.
   void RequestCopyOfOutput(Surface* surface);
 
-  // Takes the root texture result.
-  // TODO(crbug.com/1174141): We need to support more than just the root result.
-  bool HasTextureResult() const;
-  TextureResult TakeTextureResult() WARN_UNUSED_RESULT;
+  base::Optional<OutputCopyResult> TakeResult() WARN_UNUSED_RESULT;
 
   // For testing functionality that ensures that we have a valid frame.
   void CompleteSavedFrameForTesting(
@@ -65,7 +64,7 @@
   CompositorFrameTransitionDirective directive_;
   TransitionDirectiveCompleteCallback directive_finished_callback_;
 
-  TextureResult texture_result_;
+  base::Optional<OutputCopyResult> result_;
 
   base::WeakPtrFactory<SurfaceSavedFrame> weak_factory_{this};
 };
diff --git a/components/viz/service/transitions/surface_animation_manager.cc b/components/viz/service/transitions/surface_animation_manager.cc
index 40f7c5e..7ec8327 100644
--- a/components/viz/service/transitions/surface_animation_manager.cc
+++ b/components/viz/service/transitions/surface_animation_manager.cc
@@ -230,6 +230,12 @@
   for (auto& render_pass : active_frame.render_pass_list) {
     if (render_pass->id > max_id)
       max_id = render_pass->id;
+    // TODO(vmpstr): If we are doing an interpolation, we fail requested copy
+    // requests, since we can't copy them into an interpolated frame below. The
+    // todo is to change DeepCopy into a function that copies everything and
+    // moves copy output requests, since we can satisfy the requests on the
+    // interpolated frame.
+    render_pass->copy_requests.clear();
     interpolated_frame.render_pass_list.emplace_back(render_pass->DeepCopy());
   }
 
diff --git a/components/viz/service/transitions/transferable_resource_tracker.cc b/components/viz/service/transitions/transferable_resource_tracker.cc
index 5ef22346..8bca734 100644
--- a/components/viz/service/transitions/transferable_resource_tracker.cc
+++ b/components/viz/service/transitions/transferable_resource_tracker.cc
@@ -29,27 +29,32 @@
 TransferableResource TransferableResourceTracker::ImportResource(
     std::unique_ptr<SurfaceSavedFrame> saved_frame) {
   DCHECK(saved_frame);
-  DCHECK(saved_frame->IsValid());
-  if (saved_frame->HasTextureResult())
-    return ImportTextureResult(saved_frame->TakeTextureResult());
+  // Since we will be dereferencing this blindly, CHECK that the frame is indeed
+  // valid.
+  CHECK(saved_frame->IsValid());
 
-  NOTREACHED();
-  return TransferableResource();
-}
+  SurfaceSavedFrame::OutputCopyResult output_copy = *saved_frame->TakeResult();
 
-TransferableResource TransferableResourceTracker::ImportTextureResult(
-    SurfaceSavedFrame::TextureResult texture) {
-  TransferableResource result =
-      TransferableResource::MakeGL(texture.mailbox, GL_LINEAR, GL_TEXTURE_2D,
-                                   texture.sync_token, texture.size,
-                                   /*is_overlay_candidate=*/false);
-  result.id = GetNextAvailableResourceId();
+  TransferableResource resource;
+  if (output_copy.is_software) {
+    // TODO(vmpstr): This needs to be updated and tested in software. For
+    // example, we don't currently have a release callback in software, although
+    // tests do set one up.
+    resource = TransferableResource::MakeSoftware(output_copy.mailbox,
+                                                  output_copy.size, RGBA_8888);
+  } else {
+    resource = TransferableResource::MakeGL(
+        output_copy.mailbox, GL_LINEAR, GL_TEXTURE_2D, output_copy.sync_token,
+        output_copy.size,
+        /*is_overlay_candidate=*/false);
+  }
 
-  DCHECK(!base::Contains(managed_resources_, result.id));
+  resource.id = GetNextAvailableResourceId();
+  DCHECK(!base::Contains(managed_resources_, resource.id));
   managed_resources_.emplace(
-      result.id,
-      TransferableResourceHolder(result, std::move(texture.release_callback)));
-  return result;
+      resource.id, TransferableResourceHolder(
+                       resource, std::move(output_copy.release_callback)));
+  return resource;
 }
 
 void TransferableResourceTracker::RefResource(ResourceId id) {
diff --git a/components/viz/service/transitions/transferable_resource_tracker.h b/components/viz/service/transitions/transferable_resource_tracker.h
index 98743ac..bdf92e1 100644
--- a/components/viz/service/transitions/transferable_resource_tracker.h
+++ b/components/viz/service/transitions/transferable_resource_tracker.h
@@ -43,9 +43,6 @@
 
   ResourceId GetNextAvailableResourceId();
 
-  TransferableResource ImportTextureResult(
-      SurfaceSavedFrame::TextureResult result);
-
   static_assert(std::is_same<decltype(kInvalidResourceId.GetUnsafeValue()),
                              uint32_t>::value,
                 "ResourceId underlying type should be uint32_t");
diff --git a/content/browser/dom_storage/dom_storage_browsertest.cc b/content/browser/dom_storage/dom_storage_browsertest.cc
index e70d7d1..e2658bb 100644
--- a/content/browser/dom_storage/dom_storage_browsertest.cc
+++ b/content/browser/dom_storage/dom_storage_browsertest.cc
@@ -181,8 +181,7 @@
         std::make_unique<storage::FilesystemProxy>(
             storage::FilesystemProxy::UNRESTRICTED, legacy_local_storage_path));
     storage::LegacyDomStorageValuesMap data;
-    data[base::ASCIIToUTF16("foo")] =
-        base::NullableString16(base::ASCIIToUTF16("bar"), false);
+    data[u"foo"] = u"bar";
     db.CommitChanges(false, data);
     EXPECT_TRUE(base::PathExists(db_path));
   }
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 6fcab67..4b19362 100644
--- a/content/browser/media/session/media_session_service_impl_browsertest.cc
+++ b/content/browser/media/session/media_session_service_impl_browsertest.cc
@@ -5,6 +5,7 @@
 #include "content/browser/media/session/media_session_service_impl.h"
 
 #include "base/command_line.h"
+#include "base/test/scoped_feature_list.h"
 #include "build/build_config.h"
 #include "content/browser/media/session/media_session_impl.h"
 #include "content/browser/media/session/media_session_player_observer.h"
@@ -16,6 +17,7 @@
 #include "content/public/test/content_browser_test_utils.h"
 #include "content/shell/browser/shell.h"
 #include "media/base/media_content_type.h"
+#include "media/base/media_switches.h"
 #include "services/media_session/public/cpp/test/mock_media_session.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
@@ -97,6 +99,15 @@
     "navigator.mediaSession.metadata = new MediaMetadata({ title: \"foo\" });\n"
     "navigator.mediaSession.setActionHandler(\"seekforward\", _ => {});";
 
+char kSetUpWebRTCMediaSessionScript[] =
+    "navigator.mediaSession.playbackState = \"playing\";\n"
+    "navigator.mediaSession.metadata = new MediaMetadata({ title: \"foo\" });\n"
+    "navigator.mediaSession.setMicrophoneActive(true);\n"
+    "navigator.mediaSession.setCameraActive(true);\n"
+    "navigator.mediaSession.setActionHandler(\"togglemicrophone\", _ => {});\n"
+    "navigator.mediaSession.setActionHandler(\"togglecamera\", _ => {});\n"
+    "navigator.mediaSession.setActionHandler(\"hangup\", _ => {});";
+
 const int kPlayerId = 0;
 
 }  // anonymous namespace
@@ -236,4 +247,62 @@
   EXPECT_EQ(1u, GetService()->actions().size());
 }
 
+// Browser tests with the MediaSessionWebRTC feature enabled.
+// TODO(steimel): Merge with above tests when the feature is enabled by default.
+class MediaSessionServiceImplWebRTCBrowserTest
+    : public MediaSessionServiceImplBrowserTest {
+ protected:
+  void SetUpCommandLine(base::CommandLine* command_line) override {
+    MediaSessionServiceImplBrowserTest::SetUpCommandLine(command_line);
+    feature_list_.InitAndEnableFeature(media::kMediaSessionWebRTC);
+  }
+
+  bool ExecuteScriptToSetUpWebRTCMediaSessionSync() {
+    bool result = ExecuteScript(shell(), kSetUpWebRTCMediaSessionScript);
+    media_session::test::MockMediaSessionMojoObserver observer(*GetSession());
+
+    std::set<media_session::mojom::MediaSessionAction> expected_actions;
+    expected_actions.insert(media_session::mojom::MediaSessionAction::kPlay);
+    expected_actions.insert(media_session::mojom::MediaSessionAction::kPause);
+    expected_actions.insert(media_session::mojom::MediaSessionAction::kStop);
+    expected_actions.insert(
+        media_session::mojom::MediaSessionAction::kToggleMicrophone);
+    expected_actions.insert(
+        media_session::mojom::MediaSessionAction::kToggleCamera);
+    expected_actions.insert(media_session::mojom::MediaSessionAction::kHangUp);
+
+    observer.WaitForExpectedActions(expected_actions);
+    return result;
+  }
+
+ private:
+  base::test::ScopedFeatureList feature_list_;
+};
+
+IN_PROC_BROWSER_TEST_F(MediaSessionServiceImplWebRTCBrowserTest,
+                       MicrophoneAndCameraStatesInitiallyUnknown) {
+  EXPECT_TRUE(NavigateToURL(shell(), GetTestUrl(".", "title1.html")));
+  EnsurePlayer();
+
+  EXPECT_TRUE(ExecuteScriptToSetUpMediaSessionSync());
+
+  media_session::test::MockMediaSessionMojoObserver observer(*GetSession());
+  observer.WaitForMicrophoneState(
+      media_session::mojom::MicrophoneState::kUnknown);
+  observer.WaitForCameraState(media_session::mojom::CameraState::kUnknown);
+}
+
+IN_PROC_BROWSER_TEST_F(MediaSessionServiceImplWebRTCBrowserTest,
+                       MicrophoneAndCameraStatesCanBeSet) {
+  EXPECT_TRUE(NavigateToURL(shell(), GetTestUrl(".", "title1.html")));
+  EnsurePlayer();
+
+  EXPECT_TRUE(ExecuteScriptToSetUpWebRTCMediaSessionSync());
+
+  media_session::test::MockMediaSessionMojoObserver observer(*GetSession());
+  observer.WaitForMicrophoneState(
+      media_session::mojom::MicrophoneState::kUnmuted);
+  observer.WaitForCameraState(media_session::mojom::CameraState::kTurnedOn);
+}
+
 }  // namespace content
diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index f2d027d3..3fe6115 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -1139,6 +1139,7 @@
     // Creating a RFH in kActive state implies that it is the RFH for a
     // newly-created FTN, which should not have committed a real load yet.
     DCHECK(!frame_tree_node_->has_committed_real_load());
+
     if (parent_) {
       SetPolicyContainerHost(parent_->policy_container_host()->Clone());
     } else if (frame_tree_node_->opener()) {
@@ -1149,6 +1150,14 @@
     } else {
       SetPolicyContainerHost(base::MakeRefCounted<PolicyContainerHost>());
     }
+
+    // The initial empty document gets its sandbox flags from either:
+    // 1. The parent + iframe.sandbox for <iframe>.
+    // 2. The opener for popup, when "allow-popups-to-escape-sandbox" isn't set.
+    //
+    // Both are already computed and stored in the FrameTreeNode. So only a copy
+    // is needed here.
+    active_sandbox_flags_ = frame_tree_node_->active_sandbox_flags();
   }
 
   if (!base::FeatureList::IsEnabled(
@@ -9008,7 +9017,13 @@
   DCHECK(!navigation_request->IsServedFromBackForwardCache());
 
   ResetPermissionsPolicy();
-  active_sandbox_flags_ = params.sandbox_flags;
+  // There are two type of navigations committing new documents:
+  // 1. The regular ones. They can change sandbox_flags.
+  // 2. The synchronous re-navigation to about:blank. A second about:blank
+  //    document commits on top of initial one. No properties are changed,
+  //    including sandbox_flags.
+  if (navigation_request->IsWaitingToCommit())
+    active_sandbox_flags_ = navigation_request->SandboxFlagsToCommit();
   permissions_policy_header_ = params.permissions_policy_header;
   permissions_policy_->SetHeaderPolicy(params.permissions_policy_header);
   document_policy_ = blink::DocumentPolicy::CreateWithHeaderPolicy({
@@ -9031,16 +9046,6 @@
   renderer_reported_scheduler_tracked_features_ = 0;
   browser_reported_scheduler_tracked_features_ = 0;
 
-  // TODO(https://crbug.com/1041376): The sandbox flags computed from the
-  // browser must match with the ones computed from the renderer process.
-  // Ultimately, the one from the browser process should supersede the
-  // renderer one. The browser will just "push" the correct value.
-  //
-  // This currently doesn't match for about blank in the WPT test:
-  // window-open-blank-from-different-initiator-after-slow.html
-  DCHECK(navigation_request->GetURL().IsAboutBlank() ||
-         params.sandbox_flags == navigation_request->SandboxFlagsToCommit());
-
   // TODO(https://crbug.com/888079): The origin computed from the browser must
   // match the one reported from the renderer process.
   VerifyThatBrowserAndRendererCalculatedOriginsToCommitMatch(navigation_request,
diff --git a/content/browser/renderer_host/render_widget_host_view_android.cc b/content/browser/renderer_host/render_widget_host_view_android.cc
index f970514..54ac4f2 100644
--- a/content/browser/renderer_host/render_widget_host_view_android.cc
+++ b/content/browser/renderer_host/render_widget_host_view_android.cc
@@ -2369,9 +2369,7 @@
     RenderWidgetHostView* view) {
   DCHECK(!static_cast<RenderWidgetHostViewBase*>(view)
               ->IsRenderWidgetHostViewChildFrame());
-  base::Optional<SkColor> color = view->GetBackgroundColor();
-  if (color)
-    SetBackgroundColor(*color);
+  CopyBackgroundColorIfPresentFrom(*view);
 
   RenderWidgetHostViewAndroid* view_android =
       static_cast<RenderWidgetHostViewAndroid*>(view);
diff --git a/content/browser/renderer_host/render_widget_host_view_aura.cc b/content/browser/renderer_host/render_widget_host_view_aura.cc
index 624f7f5..a989d1c 100644
--- a/content/browser/renderer_host/render_widget_host_view_aura.cc
+++ b/content/browser/renderer_host/render_widget_host_view_aura.cc
@@ -2597,9 +2597,7 @@
               ->IsRenderWidgetHostViewChildFrame());
   RenderWidgetHostViewAura* view_aura =
       static_cast<RenderWidgetHostViewAura*>(view);
-  base::Optional<SkColor> color = view_aura->GetBackgroundColor();
-  if (color)
-    SetBackgroundColor(*color);
+  CopyBackgroundColorIfPresentFrom(*view);
 
   DCHECK(delegated_frame_host_) << "Cannot be invoked during destruction.";
   DCHECK(view_aura->delegated_frame_host_);
diff --git a/content/browser/renderer_host/render_widget_host_view_base.cc b/content/browser/renderer_host/render_widget_host_view_base.cc
index fd202aa..cb85ae3 100644
--- a/content/browser/renderer_host/render_widget_host_view_base.cc
+++ b/content/browser/renderer_host/render_widget_host_view_base.cc
@@ -314,6 +314,33 @@
   return default_background_color_;
 }
 
+bool RenderWidgetHostViewBase::IsBackgroundColorOpaque() {
+  base::Optional<SkColor> bg_color = GetBackgroundColor();
+  return bg_color ? SkColorGetA(*bg_color) == SK_AlphaOPAQUE : true;
+}
+
+void RenderWidgetHostViewBase::CopyBackgroundColorIfPresentFrom(
+    const RenderWidgetHostView& other) {
+  const RenderWidgetHostViewBase& other_base =
+      static_cast<const RenderWidgetHostViewBase&>(other);
+  if (!other_base.content_background_color_ &&
+      !other_base.default_background_color_) {
+    return;
+  }
+  if (content_background_color_ == other_base.content_background_color_ &&
+      default_background_color_ == other_base.default_background_color_) {
+    return;
+  }
+  bool was_opaque = IsBackgroundColorOpaque();
+  content_background_color_ = other_base.content_background_color_;
+  default_background_color_ = other_base.default_background_color_;
+  UpdateBackgroundColor();
+  bool opaque = IsBackgroundColorOpaque();
+  if (was_opaque != opaque && host()->owner_delegate()) {
+    host()->owner_delegate()->SetBackgroundOpaque(opaque);
+  }
+}
+
 bool RenderWidgetHostViewBase::IsMouseLocked() {
   return false;
 }
diff --git a/content/browser/renderer_host/render_widget_host_view_base.h b/content/browser/renderer_host/render_widget_host_view_base.h
index db55b0b..4329bc2c 100644
--- a/content/browser/renderer_host/render_widget_host_view_base.h
+++ b/content/browser/renderer_host/render_widget_host_view_base.h
@@ -97,6 +97,8 @@
   bool LockKeyboard(base::Optional<base::flat_set<ui::DomCode>> codes) override;
   void SetBackgroundColor(SkColor color) override;
   base::Optional<SkColor> GetBackgroundColor() override;
+  void CopyBackgroundColorIfPresentFrom(
+      const RenderWidgetHostView& other) override;
   void UnlockKeyboard() override;
   bool IsKeyboardLocked() override;
   base::flat_map<std::string, std::string> GetKeyboardLayoutMap() override;
@@ -611,6 +613,10 @@
                                         const gfx::PointF& point,
                                         gfx::PointF* transformed_point) const;
 
+  // Helper function to return whether the current background color is fully
+  // opaque.
+  bool IsBackgroundColorOpaque();
+
   bool view_stopped_flinging_for_test() const {
     return view_stopped_flinging_for_test_;
   }
diff --git a/content/browser/webrtc/resources/OWNERS b/content/browser/webrtc/resources/OWNERS
new file mode 100644
index 0000000..da6e6df
--- /dev/null
+++ b/content/browser/webrtc/resources/OWNERS
@@ -0,0 +1,2 @@
+hbos@chromium.org
+philipp.hancke@googlemail.com
diff --git a/content/browser/webrtc/resources/data_series.js b/content/browser/webrtc/resources/data_series.js
index 428d0dc..c39b1a9 100644
--- a/content/browser/webrtc/resources/data_series.js
+++ b/content/browser/webrtc/resources/data_series.js
@@ -36,8 +36,8 @@
       return {};
     }
 
-    var values = [];
-    for (var i = 0; i < this.dataPoints_.length; ++i) {
+    const values = [];
+    for (let i = 0; i < this.dataPoints_.length; ++i) {
       values.push(this.dataPoints_[i].value);
     }
     return {
@@ -52,7 +52,7 @@
    * DataPoints are assumed to be received in chronological order.
    */
   addPoint(timeTicks, value) {
-    var time = new Date(timeTicks);
+    const time = new Date(timeTicks);
     this.dataPoints_.push(new DataPoint(time, value));
 
     if (this.dataPoints_.length > MAX_STATS_DATA_POINT_BUFFER_SIZE) {
@@ -104,11 +104,11 @@
    * Returns the cached |values| in the specified time period.
    */
   getValuesInternal_(startTime, stepSize, count) {
-    var values = [];
-    var nextPoint = 0;
-    var currentValue = 0;
-    var time = startTime;
-    for (var i = 0; i < count; ++i) {
+    const values = [];
+    let nextPoint = 0;
+    let currentValue = 0;
+    let time = startTime;
+    for (let i = 0; i < count; ++i) {
       while (nextPoint < this.dataPoints_.length &&
              this.dataPoints_[nextPoint].time < time) {
         currentValue = this.dataPoints_[nextPoint].value;
diff --git a/content/browser/webrtc/resources/dump_creator.js b/content/browser/webrtc/resources/dump_creator.js
index 39dd506..13aec065 100644
--- a/content/browser/webrtc/resources/dump_creator.js
+++ b/content/browser/webrtc/resources/dump_creator.js
@@ -5,9 +5,9 @@
 import {$} from 'chrome://resources/js/util.m.js';
 
 /** A list of getUserMedia requests. */
-export var userMediaRequests = [];
+export const userMediaRequests = [];
 /** A map from peer connection id to the PeerConnectionRecord. */
-export var peerConnectionDataStore = {};
+export const peerConnectionDataStore = {};
 
 // Also duplicating on window since tests access these from C++.
 window.userMediaRequests = userMediaRequests;
@@ -31,10 +31,10 @@
 
     this.root_.className = 'peer-connection-dump-root';
     containerElement.appendChild(this.root_);
-    var summary = document.createElement('summary');
+    const summary = document.createElement('summary');
     this.root_.appendChild(summary);
     summary.textContent = 'Create Dump';
-    var content = document.createElement('div');
+    const content = document.createElement('div');
     this.root_.appendChild(content);
 
     content.appendChild($('dump-template').content.cloneNode(true));
@@ -72,7 +72,7 @@
     // https://crbug.com/817391
     this.root_.getElementsByTagName('input')[1].disabled = !mutable;
     if (!mutable) {
-      var label = this.root_.getElementsByTagName('label')[2];
+      const label = this.root_.getElementsByTagName('label')[2];
       label.style = 'color:red;';
       label.textContent =
           ' WebRTC event logging\'s state was set by a command line flag.';
@@ -85,16 +85,16 @@
    * @private
    */
   onDownloadData_() {
-    var dumpObject = {
+    const dumpObject = {
       'getUserMedia': userMediaRequests,
       'PeerConnections': peerConnectionDataStore,
       'UserAgent': navigator.userAgent,
     };
-    var textBlob =
+    const textBlob =
         new Blob([JSON.stringify(dumpObject, null, 1)], {type: 'octet/stream'});
-    var URL = window.URL.createObjectURL(textBlob);
+    const URL = window.URL.createObjectURL(textBlob);
 
-    var anchor = this.root_.getElementsByTagName('a')[0];
+    const anchor = this.root_.getElementsByTagName('a')[0];
     anchor.href = URL;
     anchor.download = 'webrtc_internals_dump.txt';
     // The default action of the anchor will download the URL.
@@ -106,7 +106,7 @@
    * @private
    */
   onAudioDebugRecordingsChanged_() {
-    var enabled = this.root_.getElementsByTagName('input')[0].checked;
+    const enabled = this.root_.getElementsByTagName('input')[0].checked;
     if (enabled) {
       chrome.send('enableAudioDebugRecordings');
     } else {
@@ -120,7 +120,7 @@
    * @private
    */
   onEventLogRecordingsChanged_() {
-    var enabled = this.root_.getElementsByTagName('input')[1].checked;
+    const enabled = this.root_.getElementsByTagName('input')[1].checked;
     if (enabled) {
       chrome.send('enableEventLogRecordings');
     } else {
diff --git a/content/browser/webrtc/resources/peer_connection_update_table.js b/content/browser/webrtc/resources/peer_connection_update_table.js
index 2816857..6bd31be 100644
--- a/content/browser/webrtc/resources/peer_connection_update_table.js
+++ b/content/browser/webrtc/resources/peer_connection_update_table.js
@@ -71,18 +71,18 @@
    * @param {!PeerConnectionUpdateEntry} update The update to add.
    */
   addPeerConnectionUpdate(peerConnectionElement, update) {
-    var tableElement = this.ensureUpdateContainer_(peerConnectionElement);
+    const tableElement = this.ensureUpdateContainer_(peerConnectionElement);
 
-    var row = document.createElement('tr');
+    const row = document.createElement('tr');
     tableElement.firstChild.appendChild(row);
 
-    var time = new Date(parseFloat(update.time));
+    const time = new Date(parseFloat(update.time));
     const timeItem = document.createElement('td');
     timeItem.textContent = time.toLocaleString();
     row.appendChild(timeItem);
 
     // map internal event names to spec event names.
-    var type = {
+    let type = {
       onRenegotiationNeeded: 'negotiationneeded',
       signalingStateChange: 'signalingstatechange',
       iceGatheringStateChange: 'icegatheringstatechange',
@@ -103,7 +103,7 @@
 
     if (update.type === 'onIceCandidate' || update.type === 'addIceCandidate') {
       // extract ICE candidate type from the field following typ.
-      var candidateType = update.value.match(/(?: typ )(host|srflx|relay)/);
+      const candidateType = update.value.match(/(?: typ )(host|srflx|relay)/);
       if (candidateType) {
         type += ' (' + candidateType[1] + ')';
       }
@@ -121,8 +121,8 @@
     summary.textContent = type;
     row.appendChild(summaryItem);
 
-    var valueContainer = document.createElement('pre');
-    var details = row.cells[1].childNodes[0];
+    const valueContainer = document.createElement('pre');
+    const details = row.cells[1].childNodes[0];
     details.appendChild(valueContainer);
 
     // Highlight ICE failures and failure callbacks.
@@ -133,7 +133,7 @@
       valueContainer.parentElement.classList.add('update-log-failure');
     }
 
-    var value = update.value;
+    let {value} = update;
     // map internal names and values to names and events from the
     // specification. This is a display change which shall not
     // change the JSON dump.
@@ -211,10 +211,10 @@
    * @private
    */
   ensureUpdateContainer_(peerConnectionElement) {
-    var tableId = peerConnectionElement.id + this.UPDATE_LOG_ID_SUFFIX_;
-    var tableElement = $(tableId);
+    const tableId = peerConnectionElement.id + this.UPDATE_LOG_ID_SUFFIX_;
+    let tableElement = $(tableId);
     if (!tableElement) {
-      var tableContainer = document.createElement('div');
+      const tableContainer = document.createElement('div');
       tableContainer.className = this.UPDATE_LOG_CONTAINER_CLASS_;
       peerConnectionElement.appendChild(tableContainer);
 
diff --git a/content/browser/webrtc/resources/ssrc_info_manager.js b/content/browser/webrtc/resources/ssrc_info_manager.js
index 4888f40..96bd005 100644
--- a/content/browser/webrtc/resources/ssrc_info_manager.js
+++ b/content/browser/webrtc/resources/ssrc_info_manager.js
@@ -25,7 +25,7 @@
   // TODO(jiayl): remove the fallback to id once the Libjingle change is rolled
   // to Chrome.
   if (report.stats && report.stats.values) {
-    for (var i = 0; i < report.stats.values.length - 1; i += 2) {
+    for (let i = 0; i < report.stats.values.length - 1; i += 2) {
       if (report.stats.values[i] === 'ssrc') {
         return report.stats.values[i + 1];
       }
@@ -47,7 +47,7 @@
     this.streamInfoContainer_ = {};
 
     /**
-     * The string separating attibutes in an SDP.
+     * The string separating attributes in an SDP.
      * @type {string}
      * @const
      * @private
@@ -87,28 +87,29 @@
    * @param {string} sdp The SDP string.
    */
   addSsrcStreamInfo(sdp) {
-    var attributes = sdp.split(this.ATTRIBUTE_SEPARATOR_);
-    for (var i = 0; i < attributes.length; ++i) {
+    const attributes = sdp.split(this.ATTRIBUTE_SEPARATOR_);
+    for (let i = 0; i < attributes.length; ++i) {
       // Check if this is a ssrc attribute.
       if (attributes[i].indexOf(this.SSRC_ATTRIBUTE_PREFIX_) !== 0) {
         continue;
       }
 
-      var nextFieldIndex = attributes[i].search(this.FIELD_SEPARATOR_REGEX_);
+      let nextFieldIndex = attributes[i].search(this.FIELD_SEPARATOR_REGEX_);
 
       if (nextFieldIndex === -1) {
         continue;
       }
 
-      var ssrc = attributes[i].substring(
+      const ssrc = attributes[i].substring(
           this.SSRC_ATTRIBUTE_PREFIX_.length, nextFieldIndex);
       if (!this.streamInfoContainer_[ssrc]) {
         this.streamInfoContainer_[ssrc] = {};
       }
 
       // Make |rest| starting at the next field.
-      var rest = attributes[i].substring(nextFieldIndex + 1);
-      var name, value;
+      let rest = attributes[i].substring(nextFieldIndex + 1);
+      let name;
+      let value;
       while (rest.length > 0) {
         nextFieldIndex = rest.search(this.FIELD_SEPARATOR_REGEX_);
         if (nextFieldIndex === -1) {
@@ -149,8 +150,8 @@
 
     parentElement.className = this.SSRC_INFO_BLOCK_CLASS;
 
-    var fieldElement;
-    for (var property in this.streamInfoContainer_[ssrc]) {
+    let fieldElement;
+    for (const property in this.streamInfoContainer_[ssrc]) {
       fieldElement = document.createElement('div');
       parentElement.appendChild(fieldElement);
       fieldElement.textContent =
diff --git a/content/browser/webrtc/resources/stats_graph_helper.js b/content/browser/webrtc/resources/stats_graph_helper.js
index d2c87cc..eca482d 100644
--- a/content/browser/webrtc/resources/stats_graph_helper.js
+++ b/content/browser/webrtc/resources/stats_graph_helper.js
@@ -18,15 +18,15 @@
 import {GetSsrcFromReport} from './ssrc_info_manager.js';
 import {TimelineGraphView} from './timeline_graph_view.js';
 
-var STATS_GRAPH_CONTAINER_HEADING_CLASS = 'stats-graph-container-heading';
+const STATS_GRAPH_CONTAINER_HEADING_CLASS = 'stats-graph-container-heading';
 
-var RECEIVED_PROPAGATION_DELTA_LABEL =
+const RECEIVED_PROPAGATION_DELTA_LABEL =
     'googReceivedPacketGroupPropagationDeltaDebug';
-var RECEIVED_PACKET_GROUP_ARRIVAL_TIME_LABEL =
+const RECEIVED_PACKET_GROUP_ARRIVAL_TIME_LABEL =
     'googReceivedPacketGroupArrivalTimeDebug';
 
 // Specifies which stats should be drawn on the 'bweCompound' graph and how.
-var bweCompoundGraphConfig = {
+const bweCompoundGraphConfig = {
   googAvailableSendBandwidth: {color: 'red'},
   googTargetEncBitrateCorrected: {color: 'purple'},
   googActualEncBitrate: {color: 'orange'},
@@ -36,11 +36,11 @@
 
 // Converts the last entry of |srcDataSeries| from the total amount to the
 // amount per second.
-var totalToPerSecond = function(srcDataSeries) {
-  var length = srcDataSeries.dataPoints_.length;
+const totalToPerSecond = function(srcDataSeries) {
+  const length = srcDataSeries.dataPoints_.length;
   if (length >= 2) {
-    var lastDataPoint = srcDataSeries.dataPoints_[length - 1];
-    var secondLastDataPoint = srcDataSeries.dataPoints_[length - 2];
+    const lastDataPoint = srcDataSeries.dataPoints_[length - 1];
+    const secondLastDataPoint = srcDataSeries.dataPoints_[length - 2];
     return Math.floor(
         (lastDataPoint.value - secondLastDataPoint.value) * 1000 /
         (lastDataPoint.time - secondLastDataPoint.time));
@@ -50,7 +50,7 @@
 };
 
 // Converts the value of total bytes to bits per second.
-var totalBytesToBitsPerSecond = function(srcDataSeries) {
+const totalBytesToBitsPerSecond = function(srcDataSeries) {
   return totalToPerSecond(srcDataSeries) * 8;
 };
 
@@ -58,7 +58,7 @@
 // |convertedName| is the name of the converted value, |convertFunction|
 // is the function used to calculate the new converted value based on the
 // original dataSeries.
-var dataConversionConfig = {
+const dataConversionConfig = {
   packetsSent: {
     convertedName: 'packetsSentPerSecond',
     convertFunction: totalToPerSecond,
@@ -79,9 +79,9 @@
   // TODO (jiayl): remove this when the unit bug is fixed.
   googTargetEncBitrate: {
     convertedName: 'googTargetEncBitrateCorrected',
-    convertFunction: function(srcDataSeries) {
-      var length = srcDataSeries.dataPoints_.length;
-      var lastDataPoint = srcDataSeries.dataPoints_[length - 1];
+    convertFunction(srcDataSeries) {
+      const length = srcDataSeries.dataPoints_.length;
+      const lastDataPoint = srcDataSeries.dataPoints_[length - 1];
       if (lastDataPoint.value < 5000) {
         return lastDataPoint.value * 1000;
       }
@@ -93,7 +93,7 @@
 
 // The object contains the stats names that should not be added to the graph,
 // even if they are numbers.
-var statsNameBlackList = {
+const statsNameBlackList = {
   'ssrc': true,
   'googTrackId': true,
   'googComponent': true,
@@ -128,7 +128,7 @@
 }
 
 function readReportStat(report, stat) {
-  let values = report.stats.values;
+  const values = report.stats.values;
   for (let i = 0; i < values.length; i += 2) {
     if (values[i] === stat) {
       return values[i + 1];
@@ -150,10 +150,10 @@
   return false;
 }
 
-var graphViews = {};
+const graphViews = {};
 // Export on |window| since tests access this directly from C++.
 window.graphViews = graphViews;
-let graphElementsByPeerConnectionId = new Map();
+const graphElementsByPeerConnectionId = new Map();
 
 // Returns number parsed from |value|, or NaN if the stats name is black-listed.
 function getNumberFromValue(name, value) {
@@ -170,9 +170,9 @@
 // |peerConnectionElement|.
 export function drawSingleReport(
     peerConnectionElement, report, isLegacyReport) {
-  var reportType = report.type;
-  var reportId = report.id;
-  var stats = report.stats;
+  const reportType = report.type;
+  const reportId = report.id;
+  const stats = report.stats;
   if (!stats || !stats.values) {
     return;
   }
@@ -181,16 +181,16 @@
       Array.from(peerConnectionElement.childNodes) :
       [];
 
-  for (var i = 0; i < stats.values.length - 1; i = i + 2) {
-    var rawLabel = stats.values[i];
+  for (let i = 0; i < stats.values.length - 1; i = i + 2) {
+    const rawLabel = stats.values[i];
     // Propagation deltas are handled separately.
     if (rawLabel === RECEIVED_PROPAGATION_DELTA_LABEL) {
       drawReceivedPropagationDelta(
           peerConnectionElement, report, stats.values[i + 1]);
       continue;
     }
-    var rawDataSeriesId = reportId + '-' + rawLabel;
-    var rawValue = getNumberFromValue(rawLabel, stats.values[i + 1]);
+    const rawDataSeriesId = reportId + '-' + rawLabel;
+    const rawValue = getNumberFromValue(rawLabel, stats.values[i + 1]);
     if (isNaN(rawValue)) {
       // We do not draw non-numerical values, but still want to record it in the
       // data series.
@@ -199,9 +199,9 @@
           [stats.values[i + 1]]);
       continue;
     }
-    var finalDataSeriesId = rawDataSeriesId;
-    var finalLabel = rawLabel;
-    var finalValue = rawValue;
+    let finalDataSeriesId = rawDataSeriesId;
+    let finalLabel = rawLabel;
+    let finalValue = rawValue;
     // We need to convert the value if dataConversionConfig[rawLabel] exists.
     if (isLegacyReport && dataConversionConfig[rawLabel]) {
       // Updates the original dataSeries before the conversion.
@@ -232,20 +232,20 @@
     }
 
     // Updates the graph.
-    var graphType =
+    const graphType =
         bweCompoundGraphConfig[finalLabel] ? 'bweCompound' : finalLabel;
-    var graphViewId =
+    const graphViewId =
         peerConnectionElement.id + '-' + reportId + '-' + graphType;
 
     if (!graphViews[graphViewId]) {
       graphViews[graphViewId] =
           createStatsGraphView(peerConnectionElement, report, graphType);
-      var date = new Date(stats.timestamp);
+      const date = new Date(stats.timestamp);
       graphViews[graphViewId].setDateRange(date, date);
     }
     // Adds the new dataSeries to the graphView. We have to do it here to cover
     // both the simple and compound graph cases.
-    var dataSeries =
+    const dataSeries =
         peerConnectionDataStore[peerConnectionElement.id].getDataSeries(
             finalDataSeriesId);
     if (!graphViews[graphViewId].hasDataSeries(dataSeries)) {
@@ -292,7 +292,7 @@
 // each data point, and |values| is the list of the data point values.
 function addDataSeriesPoints(
     peerConnectionElement, dataSeriesId, label, times, values) {
-  var dataSeries =
+  let dataSeries =
       peerConnectionDataStore[peerConnectionElement.id].getDataSeries(
           dataSeriesId);
   if (!dataSeries) {
@@ -303,7 +303,7 @@
       dataSeries.setColor(bweCompoundGraphConfig[label].color);
     }
   }
-  for (var i = 0; i < times.length; ++i) {
+  for (let i = 0; i < times.length; ++i) {
     dataSeries.addPoint(times[i], values[i]);
   }
 }
@@ -313,11 +313,11 @@
 // ['googReceivedPacketGroupArrivalTimeDebug', '[123456, 234455, 344566]',
 //  'googReceivedPacketGroupPropagationDeltaDebug', '[23, 45, 56]', ...].
 function drawReceivedPropagationDelta(peerConnectionElement, report, deltas) {
-  var reportId = report.id;
-  var stats = report.stats;
-  var times = null;
+  const reportId = report.id;
+  const stats = report.stats;
+  let times = null;
   // Find the packet group arrival times.
-  for (var i = 0; i < stats.values.length - 1; i = i + 2) {
+  for (let i = 0; i < stats.values.length - 1; i = i + 2) {
     if (stats.values[i] === RECEIVED_PACKET_GROUP_ARRIVAL_TIME_LABEL) {
       times = stats.values[i + 1];
       break;
@@ -338,20 +338,20 @@
   }
 
   // Update the data series.
-  var dataSeriesId = reportId + '-' + RECEIVED_PROPAGATION_DELTA_LABEL;
+  const dataSeriesId = reportId + '-' + RECEIVED_PROPAGATION_DELTA_LABEL;
   addDataSeriesPoints(
       peerConnectionElement, dataSeriesId, RECEIVED_PROPAGATION_DELTA_LABEL,
       times, deltas);
   // Update the graph.
-  var graphViewId = peerConnectionElement.id + '-' + reportId + '-' +
+  const graphViewId = peerConnectionElement.id + '-' + reportId + '-' +
       RECEIVED_PROPAGATION_DELTA_LABEL;
-  var date = new Date(times[times.length - 1]);
+  const date = new Date(times[times.length - 1]);
   if (!graphViews[graphViewId]) {
     graphViews[graphViewId] = createStatsGraphView(
         peerConnectionElement, report, RECEIVED_PROPAGATION_DELTA_LABEL);
     graphViews[graphViewId].setScale(10);
     graphViews[graphViewId].setDateRange(date, date);
-    var dataSeries =
+    const dataSeries =
         peerConnectionDataStore[peerConnectionElement.id].getDataSeries(
             dataSeriesId);
     graphViews[graphViewId].addDataSeries(dataSeries);
@@ -386,9 +386,9 @@
 // Ensures a div container to hold all stats graphs for one track is created as
 // a child of |peerConnectionElement|.
 function ensureStatsGraphTopContainer(peerConnectionElement, report) {
-  var containerId = peerConnectionElement.id + '-' + report.type + '-' +
+  const containerId = peerConnectionElement.id + '-' + report.type + '-' +
       report.id + '-graph-container';
-  var container = $(containerId);
+  let container = $(containerId);
   if (!container) {
     container = document.createElement('details');
     container.id = containerId;
@@ -400,13 +400,13 @@
         STATS_GRAPH_CONTAINER_HEADING_CLASS;
     container.firstChild.firstChild.textContent =
         'Stats graphs for ' + report.id + ' (' + report.type + ')';
-    var statsType = getSsrcReportType(report);
+    const statsType = getSsrcReportType(report);
     if (statsType !== '') {
       container.firstChild.firstChild.textContent += ' (' + statsType + ')';
     }
 
     if (report.type === 'ssrc') {
-      var ssrcInfoElement = document.createElement('div');
+      const ssrcInfoElement = document.createElement('div');
       container.firstChild.appendChild(ssrcInfoElement);
       ssrcInfoManager.populateSsrcInfo(
           ssrcInfoElement, GetSsrcFromReport(report));
@@ -418,14 +418,14 @@
 // Creates the container elements holding a timeline graph
 // and the TimelineGraphView object.
 function createStatsGraphView(peerConnectionElement, report, statsName) {
-  var topContainer =
+  const topContainer =
       ensureStatsGraphTopContainer(peerConnectionElement, report);
 
-  var graphViewId =
+  const graphViewId =
       peerConnectionElement.id + '-' + report.id + '-' + statsName;
-  var divId = graphViewId + '-div';
-  var canvasId = graphViewId + '-canvas';
-  var container = document.createElement('div');
+  const divId = graphViewId + '-div';
+  const canvasId = graphViewId + '-canvas';
+  const container = document.createElement('div');
   container.className = 'stats-graph-sub-container';
 
   topContainer.appendChild(container);
@@ -444,9 +444,9 @@
 // Creates the legend section for the bweCompound graph.
 // Returns the legend element.
 function createBweCompoundLegend(peerConnectionElement, reportId) {
-  var legend = document.createElement('div');
-  for (var prop in bweCompoundGraphConfig) {
-    var div = document.createElement('div');
+  const legend = document.createElement('div');
+  for (const prop in bweCompoundGraphConfig) {
+    const div = document.createElement('div');
     legend.appendChild(div);
     div.appendChild($('checkbox-template').content.cloneNode(true));
     div.appendChild(document.createTextNode(prop));
@@ -454,8 +454,8 @@
     div.dataSeriesId = reportId + '-' + prop;
     div.graphViewId =
         peerConnectionElement.id + '-' + reportId + '-bweCompound';
-    div.firstChild.addEventListener('click', function(event) {
-      var target =
+    div.firstChild.addEventListener('click', event => {
+      const target =
           peerConnectionDataStore[peerConnectionElement.id].getDataSeries(
               event.target.parentNode.dataSeriesId);
       target.show(event.target.checked);
diff --git a/content/browser/webrtc/resources/stats_rates_calculator.js b/content/browser/webrtc/resources/stats_rates_calculator.js
index ccebdc2..c4de59d 100644
--- a/content/browser/webrtc/resources/stats_rates_calculator.js
+++ b/content/browser/webrtc/resources/stats_rates_calculator.js
@@ -52,7 +52,7 @@
   // Gets the calculated metrics associated with |originalName| in the order
   // that they were added, or an empty list if there are no associated metrics.
   getCalculatedMetrics(originalName) {
-    let calculatedMetrics =
+    const calculatedMetrics =
         this.calculatedMetricsByOriginalName.get(originalName);
     if (!calculatedMetrics) {
       return [];
@@ -62,7 +62,7 @@
 
   toString() {
     let str = '{id:"' + this.id + '"';
-    for (let originalName of this.calculatedMetricsByOriginalName.keys()) {
+    for (const originalName of this.calculatedMetricsByOriginalName.keys()) {
       const calculatedMetrics =
           this.calculatedMetricsByOriginalName.get(originalName);
       str += ',' + originalName + ':[';
@@ -125,7 +125,7 @@
 
   toInternalsReportList() {
     const result = [];
-    for (let stats of this.statsById.values()) {
+    for (const stats of this.statsById.values()) {
       const internalReport = {
         id: stats.id,
         type: stats.type,
@@ -158,14 +158,14 @@
 
   toString() {
     let str = '';
-    for (let stats of this.statsById.values()) {
+    for (const stats of this.statsById.values()) {
       if (str !== '') {
         str += ',';
       }
       str += JSON.stringify(stats);
     }
     let str2 = '';
-    for (let stats of this.calculatedStatsById.values()) {
+    for (const stats of this.calculatedStatsById.values()) {
       if (str2 !== '') {
         str2 += ',';
       }
@@ -180,7 +180,7 @@
 
   getByType(type) {
     const result = [];
-    for (let stats of this.statsById.values()) {
+    for (const stats of this.statsById.values()) {
       if (stats.type === type) {
         result.push(stats);
       }
diff --git a/content/browser/webrtc/resources/stats_table.js b/content/browser/webrtc/resources/stats_table.js
index af96e8e..67f5919 100644
--- a/content/browser/webrtc/resources/stats_table.js
+++ b/content/browser/webrtc/resources/stats_table.js
@@ -35,7 +35,7 @@
     if (report.type === 'codec') {
       return;
     }
-    var statsTable = this.ensureStatsTable_(peerConnectionElement, report);
+    const statsTable = this.ensureStatsTable_(peerConnectionElement, report);
 
     if (report.stats) {
       this.addStatsToTable_(
@@ -44,8 +44,8 @@
   }
 
   clearStatsLists(peerConnectionElement) {
-    let containerId = peerConnectionElement.id + '-table-container';
-    let container = $(containerId);
+    const containerId = peerConnectionElement.id + '-table-container';
+    const container = $(containerId);
     if (container) {
       peerConnectionElement.removeChild(container);
       this.ensureStatsTableContainer_(peerConnectionElement);
@@ -61,13 +61,13 @@
    * @private
    */
   ensureStatsTableContainer_(peerConnectionElement) {
-    var containerId = peerConnectionElement.id + '-table-container';
-    var container = $(containerId);
+    const containerId = peerConnectionElement.id + '-table-container';
+    let container = $(containerId);
     if (!container) {
       container = document.createElement('div');
       container.id = containerId;
       container.className = 'stats-table-container';
-      var head = document.createElement('div');
+      const head = document.createElement('div');
       head.textContent = 'Stats Tables';
       container.appendChild(head);
       peerConnectionElement.appendChild(container);
@@ -88,14 +88,14 @@
    * @private
    */
   ensureStatsTable_(peerConnectionElement, report) {
-    var tableId = peerConnectionElement.id + '-table-' + report.id;
-    var table = $(tableId);
+    const tableId = peerConnectionElement.id + '-table-' + report.id;
+    let table = $(tableId);
     if (!table) {
-      var container = this.ensureStatsTableContainer_(peerConnectionElement);
-      var details = document.createElement('details');
+      const container = this.ensureStatsTableContainer_(peerConnectionElement);
+      const details = document.createElement('details');
       container.appendChild(details);
 
-      var summary = document.createElement('summary');
+      const summary = document.createElement('summary');
       summary.textContent = report.id + ' (' + report.type + ')';
       details.appendChild(summary);
 
@@ -126,9 +126,9 @@
    * @private
    */
   addStatsToTable_(statsTable, time, statsData) {
-    var date = new Date(time);
+    const date = new Date(time);
     this.updateStatsTableRow_(statsTable, 'timestamp', date.toLocaleString());
-    for (var i = 0; i < statsData.length - 1; i = i + 2) {
+    for (let i = 0; i < statsData.length - 1; i = i + 2) {
       this.updateStatsTableRow_(statsTable, statsData[i], statsData[i + 1]);
     }
   }
@@ -143,9 +143,9 @@
    * @private
    */
   updateStatsTableRow_(statsTable, rowName, value) {
-    var trId = statsTable.id + '-' + rowName;
-    var trElement = $(trId);
-    var activeConnectionClass = 'stats-table-active-connection';
+    const trId = statsTable.id + '-' + rowName;
+    let trElement = $(trId);
+    const activeConnectionClass = 'stats-table-active-connection';
     if (!trElement) {
       trElement = document.createElement('tr');
       trElement.id = trId;
diff --git a/content/browser/webrtc/resources/tab_view.js b/content/browser/webrtc/resources/tab_view.js
index 6edcd7d..85cb3a5 100644
--- a/content/browser/webrtc/resources/tab_view.js
+++ b/content/browser/webrtc/resources/tab_view.js
@@ -49,14 +49,14 @@
       throw 'Tab already exists: ' + id;
     }
 
-    var head = document.createElement('span');
+    const head = document.createElement('span');
     head.className = this.TAB_HEAD_CLASS_;
     head.textContent = title;
     head.title = title;
     this.headBar_.appendChild(head);
     head.addEventListener('click', this.switchTab_.bind(this, id));
 
-    var body = document.createElement('div');
+    const body = document.createElement('div');
     body.className = this.TAB_BODY_CLASS_;
     body.id = id;
     this.root_.appendChild(body);
@@ -114,4 +114,4 @@
     this.root_.appendChild(this.headBar_);
     this.headBar_.style.textAlign = 'center';
   }
-};
+}
diff --git a/content/browser/webrtc/resources/timeline_graph_view.js b/content/browser/webrtc/resources/timeline_graph_view.js
index 9a82774..bb89b43 100644
--- a/content/browser/webrtc/resources/timeline_graph_view.js
+++ b/content/browser/webrtc/resources/timeline_graph_view.js
@@ -5,26 +5,26 @@
 import {$} from 'chrome://resources/js/util.m.js';
 
 // Maximum number of labels placed vertically along the sides of the graph.
-var MAX_VERTICAL_LABELS = 6;
+const MAX_VERTICAL_LABELS = 6;
 
 // Vertical spacing between labels and between the graph and labels.
-var LABEL_VERTICAL_SPACING = 4;
+const LABEL_VERTICAL_SPACING = 4;
 // Horizontal spacing between vertically placed labels and the edges of the
 // graph.
-var LABEL_HORIZONTAL_SPACING = 3;
+const LABEL_HORIZONTAL_SPACING = 3;
 // Horizintal spacing between two horitonally placed labels along the bottom
 // of the graph.
-var LABEL_LABEL_HORIZONTAL_SPACING = 25;
+const LABEL_LABEL_HORIZONTAL_SPACING = 25;
 
 // Length of ticks, in pixels, next to y-axis labels.  The x-axis only has
 // one set of labels, so it can use lines instead.
-var Y_AXIS_TICK_LENGTH = 10;
+const Y_AXIS_TICK_LENGTH = 10;
 
-var GRID_COLOR = '#CCC';
-var TEXT_COLOR = '#000';
-var BACKGROUND_COLOR = '#FFF';
+const GRID_COLOR = '#CCC';
+const TEXT_COLOR = '#000';
+const BACKGROUND_COLOR = '#FFF';
 
-var MAX_DECIMAL_PRECISION = 3;
+const MAX_DECIMAL_PRECISION = 3;
 
 /**
  * A TimelineGraphView displays a timeline graph on a canvas element.
@@ -59,7 +59,7 @@
 
   // Returns the total length of the graph, in pixels.
   getLength_() {
-    var timeRange = this.endTime_ - this.startTime_;
+    const timeRange = this.endTime_ - this.startTime_;
     // Math.floor is used to ignore the last partial area, of length less
     // than this.scale_.
     return Math.floor(timeRange / this.scale_);
@@ -78,7 +78,7 @@
    * repaint.
    */
   updateScrollbarRange_(resetPosition) {
-    var scrollbarRange = this.getLength_() - this.canvas_.width;
+    let scrollbarRange = this.getLength_() - this.canvas_.width;
     if (scrollbarRange < 0) {
       scrollbarRange = 0;
     }
@@ -133,7 +133,7 @@
   setDataSeries(dataSeries) {
     // Simply recreates the Graph.
     this.graph_ = new Graph();
-    for (var i = 0; i < dataSeries.length; ++i) {
+    for (let i = 0; i < dataSeries.length; ++i) {
       this.graph_.addDataSeries(dataSeries[i]);
     }
     this.repaint();
@@ -160,17 +160,17 @@
 
     this.repaintTimerRunning_ = false;
 
-    var width = this.canvas_.width;
-    var height = this.canvas_.height;
-    var context = this.canvas_.getContext('2d');
+    const width = this.canvas_.width;
+    let height = this.canvas_.height;
+    const context = this.canvas_.getContext('2d');
 
     // Clear the canvas.
     context.fillStyle = BACKGROUND_COLOR;
     context.fillRect(0, 0, width, height);
 
     // Try to get font height in pixels.  Needed for layout.
-    var fontHeightString = context.font.match(/([0-9]+)px/)[1];
-    var fontHeight = parseInt(fontHeightString);
+    const fontHeightString = context.font.match(/([0-9]+)px/)[1];
+    const fontHeight = parseInt(fontHeightString);
 
     // Safety check, to avoid drawing anything too ugly.
     if (fontHeightString.length === 0 || fontHeight <= 0 ||
@@ -187,17 +187,17 @@
     context.translate(0.5, 0.5);
 
     // Figure out what time values to display.
-    var position = this.scrollbar_.position_;
+    let position = this.scrollbar_.position_;
     // If the entire time range is being displayed, align the right edge of
     // the graph to the end of the time range.
     if (this.scrollbar_.range_ === 0) {
       position = this.getLength_() - this.canvas_.width;
     }
-    var visibleStartTime = this.startTime_ + position * this.scale_;
+    const visibleStartTime = this.startTime_ + position * this.scale_;
 
     // Make space at the bottom of the graph for the time labels, and then
     // draw the labels.
-    var textHeight = height;
+    const textHeight = height;
     height -= fontHeight + LABEL_VERTICAL_SPACING;
     this.drawTimeLabels(context, width, height, textHeight, visibleStartTime);
 
@@ -227,11 +227,11 @@
    */
   drawTimeLabels(context, width, height, textHeight, startTime) {
     // Draw the labels 1 minute apart.
-    var timeStep = 1000 * 60;
+    const timeStep = 1000 * 60;
 
     // Find the time for the first label.  This time is a perfect multiple of
     // timeStep because of how UTC times work.
-    var time = Math.ceil(startTime / timeStep) * timeStep;
+    let time = Math.ceil(startTime / timeStep) * timeStep;
 
     context.textBaseline = 'bottom';
     context.textAlign = 'center';
@@ -240,11 +240,11 @@
 
     // Draw labels and vertical grid lines.
     while (true) {
-      var x = Math.round((time - startTime) / this.scale_);
+      const x = Math.round((time - startTime) / this.scale_);
       if (x >= width) {
         break;
       }
-      var text = (new Date(time)).toLocaleTimeString();
+      const text = (new Date(time)).toLocaleTimeString();
       context.fillText(text, x, textHeight);
       context.beginPath();
       context.lineTo(x, 0);
@@ -310,7 +310,7 @@
   }
 
   hasDataSeries(dataSeries) {
-    for (var i = 0; i < this.dataSeries_.length; ++i) {
+    for (let i = 0; i < this.dataSeries_.length; ++i) {
       if (this.dataSeries_[i] === dataSeries) {
         return true;
       }
@@ -342,13 +342,14 @@
     this.scale_ = scale;
 
     // Find largest value.
-    var max = 0, min = 0;
-    for (var i = 0; i < this.dataSeries_.length; ++i) {
-      var values = this.getValues(this.dataSeries_[i]);
+    let max = 0;
+    let min = 0;
+    for (let i = 0; i < this.dataSeries_.length; ++i) {
+      const values = this.getValues(this.dataSeries_[i]);
       if (!values) {
         continue;
       }
-      for (var j = 0; j < values.length; ++j) {
+      for (let j = 0; j < values.length; ++j) {
         if (values[j] > max) {
           max = values[j];
         } else if (values[j] < min) {
@@ -373,10 +374,10 @@
     }
 
     // Find appropriate units to use.
-    var units = ['', 'k', 'M', 'G', 'T', 'P'];
+    const units = ['', 'k', 'M', 'G', 'T', 'P'];
     // Units to use for labels.  0 is '1', 1 is K, etc.
     // We start with 1, and work our way up.
-    var unit = 1;
+    let unit = 1;
     minValue /= 1024;
     maxValue /= 1024;
     while (units[unit + 1] && maxValue - minValue >= 1024) {
@@ -389,7 +390,7 @@
     this.layoutLabelsBasic_(minValue, maxValue, MAX_DECIMAL_PRECISION);
 
     // Append units to labels.
-    for (var i = 0; i < this.labels_.length; ++i) {
+    for (let i = 0; i < this.labels_.length; ++i) {
       this.labels_[i] += ' ' + units[unit];
     }
 
@@ -405,7 +406,7 @@
    */
   layoutLabelsBasic_(minValue, maxValue, maxDecimalDigits) {
     this.labels_ = [];
-    var range = maxValue - minValue;
+    const range = maxValue - minValue;
     // No labels if the range is 0.
     if (range === 0) {
       this.min_ = this.max_ = maxValue;
@@ -415,10 +416,10 @@
     // The maximum number of equally spaced labels allowed.  |fontHeight_|
     // is doubled because the top two labels are both drawn in the same
     // gap.
-    var minLabelSpacing = 2 * this.fontHeight_ + LABEL_VERTICAL_SPACING;
+    const minLabelSpacing = 2 * this.fontHeight_ + LABEL_VERTICAL_SPACING;
 
     // The + 1 is for the top label.
-    var maxLabels = 1 + this.height_ / minLabelSpacing;
+    let maxLabels = 1 + this.height_ / minLabelSpacing;
     if (maxLabels < 2) {
       maxLabels = 2;
     } else if (maxLabels > MAX_VERTICAL_LABELS) {
@@ -426,10 +427,10 @@
     }
 
     // Initial try for step size between consecutive labels.
-    var stepSize = Math.pow(10, -maxDecimalDigits);
+    let stepSize = Math.pow(10, -maxDecimalDigits);
     // Number of digits to the right of the decimal of |stepSize|.
     // Used for formatting label strings.
-    var stepSizeDecimalDigits = maxDecimalDigits;
+    let stepSizeDecimalDigits = maxDecimalDigits;
 
     // Pick a reasonable step size.
     while (true) {
@@ -465,7 +466,7 @@
     this.min_ = Math.floor(minValue / stepSize) * stepSize;
 
     // Create labels.
-    for (var label = this.max_; label >= this.min_; label -= stepSize) {
+    for (let label = this.max_; label >= this.min_; label -= stepSize) {
       this.labels_.push(label.toFixed(stepSizeDecimalDigits));
     }
   }
@@ -474,17 +475,15 @@
    * Draws tick marks for each of the labels in |labels_|.
    */
   drawTicks(context) {
-    var x1;
-    var x2;
-    x1 = this.width_ - 1;
-    x2 = this.width_ - 1 - Y_AXIS_TICK_LENGTH;
+    const x1 = this.width_ - 1;
+    const x2 = this.width_ - 1 - Y_AXIS_TICK_LENGTH;
 
     context.fillStyle = GRID_COLOR;
     context.beginPath();
-    for (var i = 1; i < this.labels_.length - 1; ++i) {
+    for (let i = 1; i < this.labels_.length - 1; ++i) {
       // The rounding is needed to avoid ugly 2-pixel wide anti-aliased
       // lines.
-      var y = Math.round(this.height_ * i / (this.labels_.length - 1));
+      const y = Math.round(this.height_ * i / (this.labels_.length - 1));
       context.moveTo(x1, y);
       context.lineTo(x2, y);
     }
@@ -497,22 +496,22 @@
   drawLines(context) {
     // Factor by which to scale all values to convert them to a number from
     // 0 to height - 1.
-    var scale = 0;
-    var bottom = this.height_ - 1;
+    let scale = 0;
+    const bottom = this.height_ - 1;
     if (this.max_) {
       scale = bottom / (this.max_ - this.min_);
     }
 
     // Draw in reverse order, so earlier data series are drawn on top of
     // subsequent ones.
-    for (var i = this.dataSeries_.length - 1; i >= 0; --i) {
-      var values = this.getValues(this.dataSeries_[i]);
+    for (let i = this.dataSeries_.length - 1; i >= 0; --i) {
+      const values = this.getValues(this.dataSeries_[i]);
       if (!values) {
         continue;
       }
       context.strokeStyle = this.dataSeries_[i].getColor();
       context.beginPath();
-      for (var x = 0; x < values.length; ++x) {
+      for (let x = 0; x < values.length; ++x) {
         // The rounding is needed to avoid ugly 2-pixel wide anti-aliased
         // horizontal lines.
         context.lineTo(x, bottom - Math.round((values[x] - this.min_) * scale));
@@ -528,7 +527,7 @@
     if (this.labels_.length === 0) {
       return;
     }
-    var x = this.width_ - LABEL_HORIZONTAL_SPACING;
+    const x = this.width_ - LABEL_HORIZONTAL_SPACING;
 
     // Set up the context.
     context.fillStyle = TEXT_COLOR;
@@ -541,8 +540,8 @@
 
     // Draw all the other labels.
     context.textBaseline = 'bottom';
-    var step = (this.height_ - 1) / (this.labels_.length - 1);
-    for (var i = 1; i < this.labels_.length; ++i) {
+    const step = (this.height_ - 1) / (this.labels_.length - 1);
+    for (let i = 1; i < this.labels_.length; ++i) {
       context.fillText(this.labels_[i], x, step * i);
     }
   }
diff --git a/content/browser/webrtc/resources/webrtc_internals.js b/content/browser/webrtc/resources/webrtc_internals.js
index 29db9906..d13d820f 100644
--- a/content/browser/webrtc/resources/webrtc_internals.js
+++ b/content/browser/webrtc/resources/webrtc_internals.js
@@ -14,18 +14,18 @@
 import {StatsTable} from './stats_table.js';
 import {TabView} from './tab_view.js';
 
-var USER_MEDIA_TAB_ID = 'user-media-tab-id';
+const USER_MEDIA_TAB_ID = 'user-media-tab-id';
 
 const OPTION_GETSTATS_STANDARD = 'Standardized (promise-based) getStats() API';
 const OPTION_GETSTATS_LEGACY =
     'Legacy Non-Standard (callback-based) getStats() API';
 let currentGetStatsMethod = OPTION_GETSTATS_STANDARD;
 
-var tabView = null;
-var ssrcInfoManager = null;
-var peerConnectionUpdateTable = null;
-var statsTable = null;
-var dumpCreator = null;
+let tabView = null;
+let ssrcInfoManager = null;
+let peerConnectionUpdateTable = null;
+let statsTable = null;
+let dumpCreator = null;
 
 // Exporting these on window since they are directly accessed by tests.
 window.setCurrentGetStatsMethod = function(method) {
@@ -92,7 +92,7 @@
    *   "value".
    */
   addUpdate(update) {
-    var time = new Date(parseFloat(update.time));
+    const time = new Date(parseFloat(update.time));
     this.record_.updateLog.push({
       time: time.toLocaleString(),
       type: update.type,
@@ -181,9 +181,9 @@
 }
 
 function requestStats() {
-  if (currentGetStatsMethod == OPTION_GETSTATS_STANDARD) {
+  if (currentGetStatsMethod === OPTION_GETSTATS_STANDARD) {
     requestStandardStats();
-  } else if (currentGetStatsMethod == OPTION_GETSTATS_LEGACY) {
+  } else if (currentGetStatsMethod === OPTION_GETSTATS_LEGACY) {
     requestLegacyStats();
   }
 }
@@ -238,8 +238,8 @@
  * @param {!PeerConnectionUpdateEntry} data The peer connection update data.
  */
 function extractSsrcInfo(data) {
-  if (data.type == 'setLocalDescription' ||
-      data.type == 'setRemoteDescription') {
+  if (data.type === 'setLocalDescription' ||
+      data.type === 'setRemoteDescription') {
     ssrcInfoManager.addSsrcStreamInfo(data.value);
   }
 }
@@ -254,7 +254,7 @@
  * @return {!Element} the new DIV element.
  */
 function appendChildWithText(parent, tag, text) {
-  var child = document.createElement(tag);
+  const child = document.createElement(tag);
   child.textContent = text;
   parent.appendChild(child);
   return child;
@@ -284,7 +284,7 @@
  *     connection.
  */
 function removePeerConnection(data) {
-  var element = $(getPeerConnectionId(data));
+  const element = $(getPeerConnectionId(data));
   if (element) {
     delete peerConnectionDataStore[element.id];
     tabView.removeTab(element.id);
@@ -299,7 +299,7 @@
  *     rtcConfiguration, and constraints of a peer connection.
  */
 function addPeerConnection(data) {
-  var id = getPeerConnectionId(data);
+  const id = getPeerConnectionId(data);
 
   if (!peerConnectionDataStore[id]) {
     peerConnectionDataStore[id] = new PeerConnectionRecord();
@@ -307,12 +307,12 @@
   peerConnectionDataStore[id].initialize(
       data.url, data.rtcConfiguration, data.constraints);
 
-  var peerConnectionElement = $(id);
+  let peerConnectionElement = $(id);
   if (!peerConnectionElement) {
     peerConnectionElement = tabView.addTab(id, data.url + ' [' + id + ']');
   }
 
-  var p = document.createElement('p');
+  const p = document.createElement('p');
   p.textContent =
       data.url + ', ' + data.rtcConfiguration + ', ' + data.constraints;
   peerConnectionElement.appendChild(p);
@@ -327,7 +327,7 @@
  * @param {!PeerConnectionUpdateEntry} data The peer connection update data.
  */
 function updatePeerConnection(data) {
-  var peerConnectionElement = $(getPeerConnectionId(data));
+  const peerConnectionElement = $(getPeerConnectionId(data));
   addPeerConnectionUpdate(peerConnectionElement, data);
 }
 
@@ -340,14 +340,14 @@
  *     constraints, and an array of updates as the log.
  */
 function updateAllPeerConnections(data) {
-  for (var i = 0; i < data.length; ++i) {
-    var peerConnection = addPeerConnection(data[i]);
+  for (let i = 0; i < data.length; ++i) {
+    const peerConnection = addPeerConnection(data[i]);
 
-    var log = data[i].log;
+    const log = data[i].log;
     if (!log) {
       continue;
     }
-    for (var j = 0; j < log.length; ++j) {
+    for (let j = 0; j < log.length; ++j) {
       addPeerConnectionUpdate(peerConnection, log[j]);
     }
   }
@@ -367,7 +367,7 @@
   if (currentGetStatsMethod != OPTION_GETSTATS_STANDARD) {
     return;  // Obsolete!
   }
-  var peerConnectionElement = $(getPeerConnectionId(data));
+  const peerConnectionElement = $(getPeerConnectionId(data));
   if (!peerConnectionElement) {
     return;
   }
@@ -380,8 +380,8 @@
   const r = StatsReport.fromInternalsReportList(data.reports);
   statsRatesCalculator.addStatsReport(r);
   data.reports = statsRatesCalculator.currentReport.toInternalsReportList();
-  for (var i = 0; i < data.reports.length; ++i) {
-    var report = data.reports[i];
+  for (let i = 0; i < data.reports.length; ++i) {
+    const report = data.reports[i];
     statsTable.addStatsReport(peerConnectionElement, report);
     drawSingleReport(peerConnectionElement, report, false);
   }
@@ -400,13 +400,13 @@
   if (currentGetStatsMethod != OPTION_GETSTATS_LEGACY) {
     return;  // Obsolete!
   }
-  var peerConnectionElement = $(getPeerConnectionId(data));
+  const peerConnectionElement = $(getPeerConnectionId(data));
   if (!peerConnectionElement) {
     return;
   }
 
-  for (var i = 0; i < data.reports.length; ++i) {
-    var report = data.reports[i];
+  for (let i = 0; i < data.reports.length; ++i) {
+    const report = data.reports[i];
     statsTable.addStatsReport(peerConnectionElement, report);
     drawSingleReport(peerConnectionElement, report, true);
   }
@@ -426,7 +426,7 @@
     tabView.addTab(USER_MEDIA_TAB_ID, 'GetUserMedia Requests');
   }
 
-  var requestDiv = document.createElement('div');
+  const requestDiv = document.createElement('div');
   requestDiv.className = 'user-media-request-div-class';
   requestDiv.rid = data.rid;
   $(USER_MEDIA_TAB_ID).appendChild(requestDiv);
@@ -450,19 +450,19 @@
  * @param {!Object} data The object containing rid {number}, the render id.
  */
 function removeGetUserMediaForRenderer(data) {
-  for (var i = userMediaRequests.length - 1; i >= 0; --i) {
-    if (userMediaRequests[i].rid == data.rid) {
+  for (let i = userMediaRequests.length - 1; i >= 0; --i) {
+    if (userMediaRequests[i].rid === data.rid) {
       userMediaRequests.splice(i, 1);
     }
   }
 
-  var requests = $(USER_MEDIA_TAB_ID).childNodes;
-  for (var i = 0; i < requests.length; ++i) {
-    if (requests[i].rid == data.rid) {
+  const requests = $(USER_MEDIA_TAB_ID).childNodes;
+  for (let i = 0; i < requests.length; ++i) {
+    if (requests[i].rid === data.rid) {
       $(USER_MEDIA_TAB_ID).removeChild(requests[i]);
     }
   }
-  if ($(USER_MEDIA_TAB_ID).childNodes.length == 0) {
+  if ($(USER_MEDIA_TAB_ID).childNodes.length === 0) {
     tabView.removeTab(USER_MEDIA_TAB_ID);
   }
 }
diff --git a/content/child/runtime_features.cc b/content/child/runtime_features.cc
index 71f8542d..94d69bc 100644
--- a/content/child/runtime_features.cc
+++ b/content/child/runtime_features.cc
@@ -266,6 +266,7 @@
     {wf::EnableMediaEngagementBypassAutoplayPolicies,
      media::kMediaEngagementBypassAutoplayPolicies},
     {wf::EnableMediaFeeds, media::kMediaFeeds},
+    {wf::EnableMediaSessionWebRTC, media::kMediaSessionWebRTC},
     {wf::EnableMouseSubframeNoImplicitCapture,
      features::kMouseSubframeNoImplicitCapture},
     {wf::EnableNeverSlowMode, features::kNeverSlowMode},
diff --git a/content/common/navigation_client.mojom b/content/common/navigation_client.mojom
index e6fee95..c623787b 100644
--- a/content/common/navigation_client.mojom
+++ b/content/common/navigation_client.mojom
@@ -12,7 +12,6 @@
 import "services/network/public/mojom/url_loader.mojom";
 import "services/network/public/mojom/url_loader_factory.mojom";
 import "services/network/public/mojom/url_response_head.mojom";
-import "services/network/public/mojom/web_sandbox_flags.mojom";
 import "third_party/blink/public/mojom/commit_result/commit_result.mojom";
 import "third_party/blink/public/mojom/permissions_policy/document_policy_feature.mojom";
 import "third_party/blink/public/mojom/permissions_policy/permissions_policy.mojom";
@@ -121,10 +120,6 @@
   // RenderFrameProxies.
   url.mojom.Origin origin;
 
-  // TODO(https://crbug.com/1041376): Remove this, once we trust the value
-  // computed by the browser process.
-  network.mojom.WebSandboxFlags sandbox_flags;
-
   // The 'Permissions-Policy' headers applied to the document.
   // https://w3c.github.io/webappsec-permissions-policy/#permissions-policy-http-header-field
   // Note: For backward compatibility, this field also contains
diff --git a/content/public/browser/render_widget_host_view.h b/content/public/browser/render_widget_host_view.h
index 4a9d541..444da0c 100644
--- a/content/public/browser/render_widget_host_view.h
+++ b/content/public/browser/render_widget_host_view.h
@@ -157,6 +157,9 @@
   virtual void SetBackgroundColor(SkColor color) = 0;
   // GetBackgroundColor returns the current background color of the view.
   virtual base::Optional<SkColor> GetBackgroundColor() = 0;
+  // Copy background color from another view if other view has background color.
+  virtual void CopyBackgroundColorIfPresentFrom(
+      const RenderWidgetHostView& other) = 0;
 
   // Return value indicates whether the mouse is locked successfully or a
   // reason why it failed.
diff --git a/content/public/common/content_features.cc b/content/public/common/content_features.cc
index 81e1a34..967f7dd 100644
--- a/content/public/common/content_features.cc
+++ b/content/public/common/content_features.cc
@@ -981,7 +981,7 @@
     "RetryGetVideoCaptureDeviceInfos", base::FEATURE_DISABLED_BY_DEFAULT};
 
 const base::Feature kDesktopCaptureMacV2{"DesktopCaptureMacV2",
-                                         base::FEATURE_ENABLED_BY_DEFAULT};
+                                         base::FEATURE_DISABLED_BY_DEFAULT};
 
 const base::Feature kWindowCaptureMacV2{"WindowCaptureMacV2",
                                         base::FEATURE_DISABLED_BY_DEFAULT};
diff --git a/content/renderer/render_frame_impl.cc b/content/renderer/render_frame_impl.cc
index ca0f2893..e24fe5b 100644
--- a/content/renderer/render_frame_impl.cc
+++ b/content/renderer/render_frame_impl.cc
@@ -4007,7 +4007,6 @@
 void RenderFrameImpl::DidCommitNavigation(
     blink::WebHistoryCommitType commit_type,
     bool should_reset_browser_interface_broker,
-    network::mojom::WebSandboxFlags sandbox_flags,
     const blink::ParsedPermissionsPolicy& permissions_policy_header,
     const blink::DocumentPolicyFeatureState& document_policy_header) {
   CHECK_EQ(NavigationCommitState::kWillCommit, navigation_commit_state_);
@@ -4120,7 +4119,7 @@
       GetTransitionType(frame_->GetDocumentLoader(), IsMainFrame());
 
   DidCommitNavigationInternal(
-      commit_type, transition, sandbox_flags, permissions_policy_header,
+      commit_type, transition, permissions_policy_header,
       document_policy_header,
       should_reset_browser_interface_broker
           ? mojom::DidCommitProvisionalLoadInterfaceParams::New(
@@ -4282,7 +4281,7 @@
   same_document_params->is_history_api_navigation = is_history_api_navigation;
   same_document_params->is_client_redirect = is_client_redirect;
   DidCommitNavigationInternal(
-      commit_type, transition, network::mojom::WebSandboxFlags(),
+      commit_type, transition,
       blink::ParsedPermissionsPolicy(),     // permissions_policy_header
       blink::DocumentPolicyFeatureState(),  // document_policy_header
       nullptr,                              // interface_params
@@ -4758,7 +4757,6 @@
 RenderFrameImpl::MakeDidCommitProvisionalLoadParams(
     blink::WebHistoryCommitType commit_type,
     ui::PageTransition transition,
-    network::mojom::WebSandboxFlags sandbox_flags,
     const blink::ParsedPermissionsPolicy& permissions_policy_header,
     const blink::DocumentPolicyFeatureState& document_policy_header,
     const base::Optional<base::UnguessableToken>& embedding_token) {
@@ -4809,7 +4807,6 @@
   WebSecurityOrigin frame_origin = frame_document.GetSecurityOrigin();
   params->origin = frame_origin;
 
-  params->sandbox_flags = sandbox_flags;
   params->permissions_policy_header = permissions_policy_header;
   params->document_policy_header = document_policy_header;
 
@@ -5037,7 +5034,6 @@
 void RenderFrameImpl::DidCommitNavigationInternal(
     blink::WebHistoryCommitType commit_type,
     ui::PageTransition transition,
-    network::mojom::WebSandboxFlags sandbox_flags,
     const blink::ParsedPermissionsPolicy& permissions_policy_header,
     const blink::DocumentPolicyFeatureState& document_policy_header,
     mojom::DidCommitProvisionalLoadInterfaceParamsPtr interface_params,
@@ -5055,7 +5051,7 @@
   // after the browser process has already been informed of the provisional
   // load committing.
   auto params = MakeDidCommitProvisionalLoadParams(
-      commit_type, transition, sandbox_flags, permissions_policy_header,
+      commit_type, transition, permissions_policy_header,
       document_policy_header, embedding_token);
 
   if (same_document_params) {
diff --git a/content/renderer/render_frame_impl.h b/content/renderer/render_frame_impl.h
index b15a0cd..5e1bd3e 100644
--- a/content/renderer/render_frame_impl.h
+++ b/content/renderer/render_frame_impl.h
@@ -547,7 +547,6 @@
   void DidCommitNavigation(
       blink::WebHistoryCommitType commit_type,
       bool should_reset_browser_interface_broker,
-      network::mojom::WebSandboxFlags sandbox_flags,
       const blink::ParsedPermissionsPolicy& permissions_policy_header,
       const blink::DocumentPolicyFeatureState& document_policy_header) override;
   void DidCommitDocumentReplacementNavigation(
@@ -1029,7 +1028,6 @@
   mojom::DidCommitProvisionalLoadParamsPtr MakeDidCommitProvisionalLoadParams(
       blink::WebHistoryCommitType commit_type,
       ui::PageTransition transition,
-      network::mojom::WebSandboxFlags sandbox_flags,
       const blink::ParsedPermissionsPolicy& permissions_policy_header,
       const blink::DocumentPolicyFeatureState& document_policy_header,
       const base::Optional<base::UnguessableToken>& embedding_token);
@@ -1054,7 +1052,6 @@
   void DidCommitNavigationInternal(
       blink::WebHistoryCommitType commit_type,
       ui::PageTransition transition,
-      network::mojom::WebSandboxFlags sandbox_flags,
       const blink::ParsedPermissionsPolicy& permissions_policy_header,
       const blink::DocumentPolicyFeatureState& document_policy_header,
       mojom::DidCommitProvisionalLoadInterfaceParamsPtr interface_params,
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index 07167c9..e3f52d6 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -662,6 +662,19 @@
       "//fuchsia/engine:web_engine_with_webui",
     ]
   }
+  group("performance_web_engine_test_suite") {
+    testonly = true
+    data = [
+      "//content/test/gpu/run_telemetry_test_fuchsia.py",
+      "//content/test/gpu/",
+    ]
+    data_deps = [
+      "//fuchsia/engine:web_engine_shell",
+      "//fuchsia/engine:web_engine_with_webui",
+      "//testing:run_perf_test",
+      "//tools/perf/:perf",
+    ]
+  }
 }
 
 group("telemetry_gpu_integration_test_scripts_only") {
diff --git a/content/test/gpu/fuchsia_util.py b/content/test/gpu/fuchsia_util.py
new file mode 100644
index 0000000..32207df
--- /dev/null
+++ b/content/test/gpu/fuchsia_util.py
@@ -0,0 +1,85 @@
+# Copyright 2021 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import argparse
+import logging
+import os
+import shutil
+import subprocess
+import sys
+import tempfile
+
+from gpu_tests import path_util
+
+sys.path.insert(0,
+                os.path.join(path_util.GetChromiumSrcDir(), 'build', 'fuchsia'))
+from common_args import (AddCommonArgs, ConfigureLogging,
+                         GetDeploymentTargetForArgs)
+from symbolizer import RunSymbolizer
+
+
+def RunTestOnFuchsiaDevice(script_cmd):
+  """Preps Fuchsia device with pave and package update, then runs script."""
+  parser = argparse.ArgumentParser()
+  AddCommonArgs(parser)
+  args, test_args = parser.parse_known_args()
+  ConfigureLogging(args)
+
+  additional_target_args = {}
+
+  # If output_dir is not set, assume the script is being launched
+  # from the output directory.
+  if not args.out_dir:
+    args.out_dir = os.getcwd()
+    additional_target_args['out_dir'] = args.out_dir
+
+  # Create a temporary log file that Telemetry will look to use to build
+  # an artifact when tests fail.
+  temp_log_file = False
+  if not args.system_log_file:
+    args.system_log_file = os.path.join(tempfile.mkdtemp(), 'system-log')
+    temp_log_file = True
+    additional_target_args['system_log_file'] = args.system_log_file
+
+  package_names = ['web_engine_with_webui', 'web_engine_shell']
+  web_engine_dir = os.path.join(args.out_dir, 'gen', 'fuchsia', 'engine')
+
+  # Pass all other arguments to the integration tests.
+  script_cmd.extend(test_args)
+  try:
+    with GetDeploymentTargetForArgs(additional_target_args) as target:
+      target.Start()
+      fuchsia_device_address, fuchsia_ssh_port = target._GetEndpoint()
+      script_cmd.extend(['--chromium-output-directory', args.out_dir])
+      script_cmd.extend(['--fuchsia-device-address', fuchsia_device_address])
+      script_cmd.extend(['--fuchsia-ssh-config', target._GetSshConfigPath()])
+      if fuchsia_ssh_port:
+        script_cmd.extend(['--fuchsia-ssh-port', str(fuchsia_ssh_port)])
+      script_cmd.extend(['--fuchsia-system-log-file', args.system_log_file])
+      # Add to the script
+      if args.verbose:
+        script_cmd.append('-v')
+
+      # Set up logging of WebEngine
+      listener = target.RunCommandPiped(['log_listener'],
+                                        stdout=subprocess.PIPE,
+                                        stderr=subprocess.STDOUT)
+      build_ids_paths = map(
+          lambda package_name: os.path.join(web_engine_dir, package_name,
+                                            'ids.txt'), package_names)
+      RunSymbolizer(listener.stdout, open(args.system_log_file, 'w'),
+                    build_ids_paths)
+
+      # Keep the Amber repository live while the test runs.
+      with target.GetAmberRepo():
+        # Install necessary packages on the device.
+        far_files = map(
+            lambda package_name: os.path.join(web_engine_dir, package_name,
+                                              package_name + '.far'),
+            package_names)
+        target.InstallPackage(far_files)
+        return subprocess.call(script_cmd)
+  finally:
+    if temp_log_file:
+      shutil.rmtree(os.path.dirname(args.system_log_file))
diff --git a/content/test/gpu/run_gpu_integration_test_fuchsia.py b/content/test/gpu/run_gpu_integration_test_fuchsia.py
index 7e33505..128ab57 100755
--- a/content/test/gpu/run_gpu_integration_test_fuchsia.py
+++ b/content/test/gpu/run_gpu_integration_test_fuchsia.py
@@ -4,90 +4,20 @@
 # found in the LICENSE file.
 """Wrapper for running gpu integration tests on Fuchsia devices."""
 
-import argparse
-import logging
 import os
 import shutil
-import subprocess
 import sys
-import tempfile
 
+import fuchsia_util
 from gpu_tests import path_util
 
-sys.path.insert(0,
-                os.path.join(path_util.GetChromiumSrcDir(), 'build', 'fuchsia'))
-from common_args import (AddCommonArgs, ConfigureLogging,
-                         GetDeploymentTargetForArgs)
-from symbolizer import RunSymbolizer
-
 
 def main():
-  parser = argparse.ArgumentParser()
-  AddCommonArgs(parser)
-  args, gpu_test_args = parser.parse_known_args()
-  ConfigureLogging(args)
-
-  additional_target_args = {}
-
-  # If output_dir is not set, assume the script is being launched
-  # from the output directory.
-  if not args.out_dir:
-    args.out_dir = os.getcwd()
-    additional_target_args['out_dir'] = args.out_dir
-
-  # Create a temporary log file that Telemetry will look to use to build
-  # an artifact when tests fail.
-  temp_log_file = False
-  if not args.system_log_file:
-    args.system_log_file = os.path.join(tempfile.mkdtemp(), 'system-log')
-    temp_log_file = True
-    additional_target_args['system_log_file'] = args.system_log_file
-
-  package_names = ['web_engine_with_webui', 'web_engine_shell']
-  web_engine_dir = os.path.join(args.out_dir, 'gen', 'fuchsia', 'engine')
   gpu_script = [
       os.path.join(path_util.GetChromiumSrcDir(), 'content', 'test', 'gpu',
                    'run_gpu_integration_test.py')
   ]
-
-  # Pass all other arguments to the gpu integration tests.
-  gpu_script.extend(gpu_test_args)
-  try:
-    with GetDeploymentTargetForArgs(additional_target_args) as target:
-      target.Start()
-      fuchsia_device_address, fuchsia_ssh_port = target._GetEndpoint()
-      gpu_script.extend(['--chromium-output-directory', args.out_dir])
-      gpu_script.extend(['--fuchsia-device-address', fuchsia_device_address])
-      gpu_script.extend(['--fuchsia-ssh-config', target._GetSshConfigPath()])
-      if fuchsia_ssh_port:
-        gpu_script.extend(['--fuchsia-ssh-port', str(fuchsia_ssh_port)])
-      gpu_script.extend(['--fuchsia-system-log-file', args.system_log_file])
-      if args.verbose:
-        gpu_script.append('-v')
-
-      # Set up logging of WebEngine
-      listener = target.RunCommandPiped(['log_listener'],
-                                        stdout=subprocess.PIPE,
-                                        stderr=subprocess.STDOUT)
-      build_ids_paths = map(
-          lambda package_name: os.path.join(
-              web_engine_dir, package_name, 'ids.txt'),
-          package_names)
-      RunSymbolizer(listener.stdout, open(args.system_log_file, 'w'),
-                    build_ids_paths)
-
-      # Keep the Amber repository live while the test runs.
-      with target.GetAmberRepo():
-        # Install necessary packages on the device.
-        far_files = map(
-            lambda package_name: os.path.join(
-                web_engine_dir, package_name, package_name + '.far'),
-            package_names)
-        target.InstallPackage(far_files)
-        return subprocess.call(gpu_script)
-  finally:
-    if temp_log_file:
-      shutil.rmtree(os.path.dirname(args.system_log_file))
+  return fuchsia_util.RunTestOnFuchsiaDevice(gpu_script)
 
 
 if __name__ == '__main__':
diff --git a/content/test/gpu/run_telemetry_benchmark_fuchsia.py b/content/test/gpu/run_telemetry_benchmark_fuchsia.py
new file mode 100755
index 0000000..1ff7570
--- /dev/null
+++ b/content/test/gpu/run_telemetry_benchmark_fuchsia.py
@@ -0,0 +1,23 @@
+#!/usr/bin/env vpython
+# Copyright 2021 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""Wrapper for running Telemetry benchmarks on Fuchsia devices."""
+
+import os
+import sys
+
+import fuchsia_util
+from gpu_tests import path_util
+
+
+def main():
+  telemetry_script = [
+      os.path.join(path_util.GetChromiumSrcDir(), 'tools', 'perf',
+                   'run_benchmark')
+  ]
+  return fuchsia_util.RunTestOnFuchsiaDevice(telemetry_script)
+
+
+if __name__ == '__main__':
+  sys.exit(main())
diff --git a/content/test/mock_platform_notification_service.cc b/content/test/mock_platform_notification_service.cc
index 1d8a0687..96e59b4 100644
--- a/content/test/mock_platform_notification_service.cc
+++ b/content/test/mock_platform_notification_service.cc
@@ -10,7 +10,6 @@
 #include "base/bind.h"
 #include "base/callback_helpers.h"
 #include "base/guid.h"
-#include "base/strings/nullable_string16.h"
 #include "base/strings/utf_string_conversions.h"
 #include "content/public/browser/browser_context.h"
 #include "content/public/browser/browser_task_traits.h"
diff --git a/content/test/navigation_simulator_impl.cc b/content/test/navigation_simulator_impl.cc
index 58fef0d..7e79362 100644
--- a/content/test/navigation_simulator_impl.cc
+++ b/content/test/navigation_simulator_impl.cc
@@ -1331,10 +1331,8 @@
   } else {
     params->should_update_history = true;
     if (same_document) {
-      params->sandbox_flags = current_rfh->active_sandbox_flags();
       params->origin = current_rfh->GetLastCommittedOrigin();
     } else {
-      params->sandbox_flags = request_->SandboxFlagsToCommit();
       params->origin =
           origin_.value_or(request_->GetOriginForURLLoaderFactory());
     }
diff --git a/content/test/test_render_view_host.cc b/content/test/test_render_view_host.cc
index 9c7a4077..fbc091f 100644
--- a/content/test/test_render_view_host.cc
+++ b/content/test/test_render_view_host.cc
@@ -168,9 +168,7 @@
 
 void TestRenderWidgetHostView::TakeFallbackContentFrom(
     RenderWidgetHostView* view) {
-  base::Optional<SkColor> color = view->GetBackgroundColor();
-  if (color)
-    SetBackgroundColor(*color);
+  CopyBackgroundColorIfPresentFrom(*view);
 }
 
 blink::mojom::PointerLockResult TestRenderWidgetHostView::LockMouse(bool) {
diff --git a/content/web_test/browser/web_test_control_host.cc b/content/web_test/browser/web_test_control_host.cc
index ded0dcfa..12a94df0 100644
--- a/content/web_test/browser/web_test_control_host.cc
+++ b/content/web_test/browser/web_test_control_host.cc
@@ -27,7 +27,6 @@
 #include "base/run_loop.h"
 #include "base/single_thread_task_runner.h"
 #include "base/stl_util.h"
-#include "base/strings/nullable_string16.h"
 #include "base/strings/string_number_conversions.h"
 #include "base/strings/string_util.h"
 #include "base/strings/stringprintf.h"
diff --git a/content/web_test/renderer/test_runner.cc b/content/web_test/renderer/test_runner.cc
index f0ce8736..0ee2aba 100644
--- a/content/web_test/renderer/test_runner.cc
+++ b/content/web_test/renderer/test_runner.cc
@@ -17,7 +17,6 @@
 #include "base/logging.h"
 #include "base/macros.h"
 #include "base/stl_util.h"
-#include "base/strings/nullable_string16.h"
 #include "base/strings/stringprintf.h"
 #include "base/strings/utf_string_conversions.h"
 #include "build/build_config.h"
diff --git a/docs/asan.md b/docs/asan.md
index db215fc..0e0e1a5 100644
--- a/docs/asan.md
+++ b/docs/asan.md
@@ -98,7 +98,7 @@
 ```shell
 out/asan/base_unittests \
     --gtest_filter=ToolsSanityTest.DISABLED_AddressSanitizerLocalOOBCrashTest \
-    --gtest_also_run_disabled_tests 2>&1 | tools/valgrind/asan/asan_symbolize.py
+    --gtest_also_run_disabled_tests
 ```
 
 The test will crash with the following error report:
@@ -124,9 +124,9 @@
 ## Run chrome under ASan
 
 And finally, have fun with the `out/Release/chrome` binary. The filter script
-`tools/valgrind/asan/asan_symbolize.py` should be used to symbolize the output.
-(Note that `asan_symbolize.py` is absolutely necessary if you need the symbols -
-there is no built-in symbolizer for ASan in Chrome).
+`tools/valgrind/asan/asan_symbolize.py` can be used to symbolize the output,
+although it shouldn't be necessary on Linux and Windows, where Chrome uses the
+llvm-symbolizer in its source tree by default.
 
 ASan should perfectly work with Chrome's sandbox. You should only need to run
 with `--no-sandbox` on Linux if you're debugging ASan.
diff --git a/extensions/browser/allowlist_state.h b/extensions/browser/allowlist_state.h
index f2bf6ab7..ae49511 100644
--- a/extensions/browser/allowlist_state.h
+++ b/extensions/browser/allowlist_state.h
@@ -18,6 +18,22 @@
   ALLOWLIST_ALLOWLISTED = 1,
   // The extension is not included in the Safe Browsing extension allowlist.
   ALLOWLIST_NOT_ALLOWLISTED = 2,
+  ALLOWLIST_LAST = 3
+};
+
+// The acknowledge states for the Safe Browsing CRX allowlist enforcements.
+enum AllowlistAcknowledgeState {
+  ALLOWLIST_ACKNOWLEDGE_NONE = 0,
+  // Used to notify the user that an extension was disabled or re-enabled by the
+  // allowlist enforcement.
+  ALLOWLIST_ACKNOWLEDGE_NEEDED = 1,
+  // State set when the user dismiss the notification in the extension menu.
+  ALLOWLIST_ACKNOWLEDGE_DONE = 2,
+  // The user clicked through the install friction dialog or re-enabled the
+  // extension after it was disabled. The extension should not be disabled again
+  // from the allowlist enforcement.
+  ALLOWLIST_ACKNOWLEDGE_ENABLED_BY_USER = 3,
+  ALLOWLIST_ACKNOWLEDGE_LAST = 4
 };
 
 }  // namespace extensions
diff --git a/extensions/browser/extension_prefs.cc b/extensions/browser/extension_prefs.cc
index 5d6b633..dc0ecb3b 100644
--- a/extensions/browser/extension_prefs.cc
+++ b/extensions/browser/extension_prefs.cc
@@ -29,6 +29,7 @@
 #include "extensions/browser/api/declarative_net_request/utils.h"
 #include "extensions/browser/app_sorting.h"
 #include "extensions/browser/blocklist_state.h"
+#include "extensions/browser/disable_reason.h"
 #include "extensions/browser/event_router.h"
 #include "extensions/browser/extension_pref_store.h"
 #include "extensions/browser/extension_prefs_factory.h"
@@ -80,6 +81,9 @@
 // Indicates whether an extension is included in the Safe Browsing allowlist.
 constexpr const char kPrefAllowlist[] = "allowlist";
 
+// Indicates the enforcement state for the Safe Browsing allowlist.
+constexpr const char kPrefAllowlistAcknowledge[] = "allowlist_acknowledge";
+
 // If extension is greylisted.
 constexpr const char kPrefBlocklistState[] = "blacklist_state";
 
@@ -1079,10 +1083,15 @@
 
 AllowlistState ExtensionPrefs::GetExtensionAllowlistState(
     const std::string& extension_id) const {
-  int value;
+  int value = 0;
   if (!ReadPrefAsInteger(extension_id, kPrefAllowlist, &value))
     return ALLOWLIST_UNDEFINED;
 
+  if (value < 0 || value >= ALLOWLIST_LAST) {
+    LOG(ERROR) << "Bad pref 'allowlist' for extension '" << extension_id << "'";
+    return ALLOWLIST_UNDEFINED;
+  }
+
   return static_cast<AllowlistState>(value);
 }
 
@@ -1096,6 +1105,30 @@
   }
 }
 
+AllowlistAcknowledgeState ExtensionPrefs::GetExtensionAllowlistAcknowledgeState(
+    const std::string& extension_id) const {
+  int value = 0;
+  if (!ReadPrefAsInteger(extension_id, kPrefAllowlistAcknowledge, &value))
+    return ALLOWLIST_ACKNOWLEDGE_NONE;
+
+  if (value < 0 || value >= ALLOWLIST_ACKNOWLEDGE_LAST) {
+    LOG(ERROR) << "Bad pref 'allowlist_acknowledge' for extension '"
+               << extension_id << "'";
+    return ALLOWLIST_ACKNOWLEDGE_NONE;
+  }
+
+  return static_cast<AllowlistAcknowledgeState>(value);
+}
+
+void ExtensionPrefs::SetExtensionAllowlistAcknowledgeState(
+    const std::string& extension_id,
+    AllowlistAcknowledgeState state) {
+  if (state != GetExtensionAllowlistAcknowledgeState(extension_id)) {
+    UpdateExtensionPref(extension_id, kPrefAllowlistAcknowledge,
+                        std::make_unique<base::Value>(state));
+  }
+}
+
 namespace {
 
 // Serializes a 64bit integer as a string value.
diff --git a/extensions/browser/extension_prefs.h b/extensions/browser/extension_prefs.h
index 0bf3375..61c4105 100644
--- a/extensions/browser/extension_prefs.h
+++ b/extensions/browser/extension_prefs.h
@@ -391,6 +391,14 @@
   void SetExtensionAllowlistState(const std::string& extension_id,
                                   AllowlistState state);
 
+  // Gets the Safe Browsing allowlist acknowledge state.
+  AllowlistAcknowledgeState GetExtensionAllowlistAcknowledgeState(
+      const std::string& extension_id) const;
+
+  // Sets the Safe Browsing allowlist acknowledge state.
+  void SetExtensionAllowlistAcknowledgeState(const std::string& extension_id,
+                                             AllowlistAcknowledgeState state);
+
   // Increment the count of how many times we prompted the user to acknowledge
   // the given extension, and return the new count.
   int IncrementAcknowledgePromptCount(const std::string& extension_id);
diff --git a/extensions/common/api/alarms.idl b/extensions/common/api/alarms.idl
index e5a0aeae..50a7bd0a 100644
--- a/extensions/common/api/alarms.idl
+++ b/extensions/common/api/alarms.idl
@@ -75,18 +75,16 @@
 
     // Retrieves details about the specified alarm.
     // |name|: The name of the alarm to get. Defaults to the empty string.
-    [supportsPromises] static void get(
-        optional DOMString name,
-	AlarmCallback callback);
+    [supportsPromises] static void get(optional DOMString name,
+                                       AlarmCallback callback);
 
     // Gets an array of all the alarms.
     [supportsPromises] static void getAll(AlarmListCallback callback);
 
     // Clears the alarm with the given name.
     // |name|: The name of the alarm to clear. Defaults to the empty string.
-    [supportsPromises] static void clear(
-        optional DOMString name,
-	optional ClearCallback callback);
+    [supportsPromises] static void clear(optional DOMString name,
+                                         optional ClearCallback callback);
 
     // Clears all alarms.
     [supportsPromises] static void clearAll(optional ClearCallback callback);
diff --git a/extensions/common/api/bluetooth_low_energy.idl b/extensions/common/api/bluetooth_low_energy.idl
index aa3b58e0..3fd9168 100644
--- a/extensions/common/api/bluetooth_low_energy.idl
+++ b/extensions/common/api/bluetooth_low_energy.idl
@@ -236,16 +236,14 @@
     // may be other apps with open connections.
     // |deviceAddress|: The Bluetooth address of the remote device.
     // |callback|: Called when the disconnect request has completed.
-    [supportsPromises] static void disconnect(
-        DOMString deviceAddress,
-        optional ResultCallback callback);
+    [supportsPromises] static void disconnect(DOMString deviceAddress,
+                                              optional ResultCallback callback);
 
     // Get the GATT service with the given instance ID.
     // |serviceId|: The instance ID of the requested GATT service.
     // |callback|: Called with the requested Service object.
-    [supportsPromises] static void getService(
-        DOMString serviceId,
-	ServiceCallback callback);
+    [supportsPromises] static void getService(DOMString serviceId,
+                                              ServiceCallback callback);
 
     // Create a locally hosted GATT service. This service can be registered
     // to be available on a local GATT server.
@@ -256,7 +254,7 @@
     // |callback|: Called with the created services's unique ID.
     [supportsPromises] static void createService(
         Service service,
-	CreateServiceCallback callback);
+        CreateServiceCallback callback);
 
     // Get all the GATT services that were discovered on the remote device with
     // the given device address.
@@ -269,9 +267,8 @@
     // |deviceAddress|: The Bluetooth address of the remote device whose GATT
     // services should be returned.
     // |callback|: Called with the list of requested Service objects.
-    [supportsPromises] static void getServices(
-        DOMString deviceAddress,
-	ServicesCallback callback);
+    [supportsPromises] static void getServices(DOMString deviceAddress,
+                                               ServicesCallback callback);
 
     // Get the GATT characteristic with the given instance ID that belongs to
     // the given GATT service, if the characteristic exists.
@@ -319,9 +316,8 @@
     // |descriptorId|: The instance ID of the requested GATT characteristic
     // descriptor.
     // |callback|: Called with the requested Descriptor object.
-    [supportsPromises] static void getDescriptor(
-        DOMString descriptorId,
-        DescriptorCallback callback);
+    [supportsPromises] static void getDescriptor(DOMString descriptorId,
+                                                 DescriptorCallback callback);
 
     // Create a locally hosted GATT descriptor. This descriptor must
     // be hosted under a valid characteristic. If the characteristic ID is not
@@ -344,9 +340,8 @@
     // descriptors should be returned.
     // |callback|: Called with the list of descriptors that belong to the given
     // characteristic.
-    [supportsPromises] static void getDescriptors(
-        DOMString characteristicId,
-        DescriptorsCallback callback);
+    [supportsPromises] static void getDescriptors(DOMString characteristicId,
+                                                  DescriptorsCallback callback);
 
     // Retrieve the value of a specified characteristic from a remote
     // peripheral.
@@ -418,7 +413,7 @@
     // the result of the read request.
     [supportsPromises] static void readDescriptorValue(
         DOMString descriptorId,
-	DescriptorCallback callback);
+        DescriptorCallback callback);
 
     // Write the value of a specified characteristic descriptor from a remote
     // peripheral.
@@ -439,9 +434,8 @@
     // true. The peripheral permission may not be available to all apps.
     // |serviceId|: Unique ID of a created service.
     // |callback|: Callback with the result of the register operation.
-    [supportsPromises] static void registerService(
-        DOMString serviceId,
-	ResultCallback callback);
+    [supportsPromises] static void registerService(DOMString serviceId,
+                                                   ResultCallback callback);
 
     // Unregister the given service with the local GATT server. If the service
     // ID is invalid, the lastError will be set.
@@ -450,9 +444,8 @@
     // true. The peripheral permission may not be available to all apps.
     // |serviceId|: Unique ID of a current registered service.
     // |callback|: Callback with the result of the register operation.
-    [supportsPromises] static void unregisterService(
-        DOMString serviceId,
-	ResultCallback callback);
+    [supportsPromises] static void unregisterService(DOMString serviceId,
+                                                     ResultCallback callback);
 
     // Remove the specified service, unregistering it if it was registered.
     // If the service ID is invalid, the lastError will be set.
@@ -463,7 +456,7 @@
     // |callback|: Callback called once the service is removed.
     [supportsPromises] static void removeService(
         DOMString serviceId,
-	optional ResultCallback callback);
+        optional ResultCallback callback);
 
     // Create an advertisement and register it for advertising. To call this
     // function, the app must have the bluetooth:low_energy and
@@ -483,7 +476,7 @@
     // advertising. Returns the id of the created advertisement.
     [supportsPromises] static void registerAdvertisement(
         Advertisement advertisement,
-	RegisterAdvertisementCallback callback);
+        RegisterAdvertisementCallback callback);
 
     // Unregisters an advertisement and stops its advertising. If the
     // advertisement fails to unregister the only way to stop advertising
@@ -512,7 +505,7 @@
     // |callback|: Called once the interval has been set.
     [supportsPromises] static void setAdvertisingInterval(
         long minInterval,
-	long maxInterval,
+        long maxInterval,
         ResultCallback callback);
 
     // Sends a response for a characteristic or descriptor read/write
diff --git a/ios/chrome/app/strings/ios_strings.grd b/ios/chrome/app/strings/ios_strings.grd
index 1a7c1888..683cb03 100644
--- a/ios/chrome/app/strings/ios_strings.grd
+++ b/ios/chrome/app/strings/ios_strings.grd
@@ -2127,6 +2127,9 @@
       <message name="IDS_IOS_COOKIES_BLOCK_THIRD_PARTY_INCOGNITO" desc="Label that informs the user that Third-Party cookies are blocked only in Incognito">
         Block in Incognito
       </message>
+      <message name="IDS_IOS_RETURN_TO_RECENT_TAB_TITLE" desc="Title of the Return to Recent Tab Content Suggestons Tile.">
+          Open Most Recent Tab
+          </message>
       <message name="IDS_IOS_SETTINGS_PASSWORDS_SAVED_HEADING" desc="The title for a list of username/site/password items. These items are already saved by the browser and can be deleted/edited. [Length: one line] [iOS only]">
       Saved Passwords
       </message>
diff --git a/ios/chrome/app/strings/ios_strings_grd/IDS_IOS_RETURN_TO_RECENT_TAB_TITLE.png.sha1 b/ios/chrome/app/strings/ios_strings_grd/IDS_IOS_RETURN_TO_RECENT_TAB_TITLE.png.sha1
new file mode 100644
index 0000000..505c5e7
--- /dev/null
+++ b/ios/chrome/app/strings/ios_strings_grd/IDS_IOS_RETURN_TO_RECENT_TAB_TITLE.png.sha1
@@ -0,0 +1 @@
+2b59f020db8ddc5f5edd9c40d74e473030e312f2
\ No newline at end of file
diff --git a/ios/chrome/browser/main/BUILD.gn b/ios/chrome/browser/main/BUILD.gn
index ae4eb73..aa4db98 100644
--- a/ios/chrome/browser/main/BUILD.gn
+++ b/ios/chrome/browser/main/BUILD.gn
@@ -61,6 +61,7 @@
     "//ios/chrome/browser/snapshots",
     "//ios/chrome/browser/tabs",
     "//ios/chrome/browser/ui/commands",
+    "//ios/chrome/browser/ui/start_surface",
     "//ios/chrome/browser/url_loading",
     "//ios/chrome/browser/web",
     "//ios/chrome/browser/web:tab_id_tab_helper",
diff --git a/ios/chrome/browser/main/browser_agent_util.mm b/ios/chrome/browser/main/browser_agent_util.mm
index 5df3a24..b13580cf 100644
--- a/ios/chrome/browser/main/browser_agent_util.mm
+++ b/ios/chrome/browser/main/browser_agent_util.mm
@@ -20,6 +20,7 @@
 #import "ios/chrome/browser/snapshots/snapshot_browser_agent.h"
 #import "ios/chrome/browser/tabs/closing_web_state_observer_browser_agent.h"
 #include "ios/chrome/browser/tabs/synced_window_delegate_browser_agent.h"
+#import "ios/chrome/browser/ui/start_surface/start_surface_recent_tab_browser_agent.h"
 #import "ios/chrome/browser/url_loading/url_loading_browser_agent.h"
 #import "ios/chrome/browser/url_loading/url_loading_notifier_browser_agent.h"
 #import "ios/chrome/browser/web/web_navigation_browser_agent.h"
@@ -72,6 +73,9 @@
   if (!browser->GetBrowserState()->IsOffTheRecord())
     TabUsageRecorderBrowserAgent::CreateForBrowser(browser);
 
+  if (!browser->GetBrowserState()->IsOffTheRecord())
+    StartSurfaceRecentTabBrowserAgent::CreateForBrowser(browser);
+
   // This needs to be called last in case any downstream browser agents need to
   // access upstream agents created earlier in this function.
   ios::GetChromeBrowserProvider()->AttachBrowserAgents(browser);
diff --git a/ios/chrome/browser/ui/content_suggestions/BUILD.gn b/ios/chrome/browser/ui/content_suggestions/BUILD.gn
index 68dcccd..6621b8e 100644
--- a/ios/chrome/browser/ui/content_suggestions/BUILD.gn
+++ b/ios/chrome/browser/ui/content_suggestions/BUILD.gn
@@ -36,6 +36,7 @@
     ":metrics",
     "//base",
     "//components/favicon/core",
+    "//components/favicon/ios",
     "//components/feature_engagement/public",
     "//components/feed/core/shared_prefs:feed_shared_prefs",
     "//components/ntp_snippets",
@@ -88,6 +89,7 @@
     "//ios/chrome/browser/ui/reading_list",
     "//ios/chrome/browser/ui/settings/utils:utils",
     "//ios/chrome/browser/ui/sharing",
+    "//ios/chrome/browser/ui/start_surface",
     "//ios/chrome/browser/ui/start_surface:feature_flags",
     "//ios/chrome/browser/ui/toolbar/public",
     "//ios/chrome/browser/ui/util",
@@ -177,6 +179,7 @@
     "//ios/chrome/browser/ui/omnibox:omnibox_internal",
     "//ios/chrome/browser/ui/omnibox:omnibox_popup_shared",
     "//ios/chrome/browser/ui/overscroll_actions",
+    "//ios/chrome/browser/ui/start_surface:feature_flags",
     "//ios/chrome/browser/ui/thumb_strip:public",
     "//ios/chrome/browser/ui/toolbar/buttons",
     "//ios/chrome/browser/ui/toolbar/public",
diff --git a/ios/chrome/browser/ui/content_suggestions/cells/BUILD.gn b/ios/chrome/browser/ui/content_suggestions/cells/BUILD.gn
index fb8bbe3..cdc45de 100644
--- a/ios/chrome/browser/ui/content_suggestions/cells/BUILD.gn
+++ b/ios/chrome/browser/ui/content_suggestions/cells/BUILD.gn
@@ -11,6 +11,8 @@
     "content_suggestions_most_visited_action_item.mm",
     "content_suggestions_most_visited_item.h",
     "content_suggestions_most_visited_item.mm",
+    "content_suggestions_return_to_recent_tab_item.h",
+    "content_suggestions_return_to_recent_tab_item.mm",
   ]
   deps = [
     ":cells_ui",
diff --git a/ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_return_to_recent_tab_item.h b/ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_return_to_recent_tab_item.h
new file mode 100644
index 0000000..b2eb8f8
--- /dev/null
+++ b/ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_return_to_recent_tab_item.h
@@ -0,0 +1,51 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef IOS_CHROME_BROWSER_UI_CONTENT_SUGGESTIONS_CELLS_CONTENT_SUGGESTIONS_RETURN_TO_RECENT_TAB_ITEM_H_
+#define IOS_CHROME_BROWSER_UI_CONTENT_SUGGESTIONS_CELLS_CONTENT_SUGGESTIONS_RETURN_TO_RECENT_TAB_ITEM_H_
+
+#import "ios/chrome/browser/ui/collection_view/cells/collection_view_item.h"
+
+#import <MaterialComponents/MaterialCollectionCells.h>
+#import <UIKit/UIKit.h>
+
+#import "ios/chrome/browser/ui/content_suggestions/cells/suggested_content.h"
+
+@protocol ContentSuggestionsGestureCommands;
+@class FaviconAttributes;
+
+// Item containing a Return to Recent Tab Start Surface tile.
+@interface ContentSuggestionsReturnToRecentTabItem
+    : CollectionViewItem <SuggestedContent>
+
+// Favicon image of the page of the most recent tab.
+@property(nonatomic, strong) UIImage* icon;
+
+// Title of the most recent tab tile.
+@property(nonatomic, copy) NSString* title;
+
+// Subtitle of the most recent tab tile.
+@property(nonatomic, copy) NSString* subtitle;
+
+// Command handler for the accessibility custom actions.
+@property(nonatomic, weak) id<ContentSuggestionsGestureCommands> commandHandler;
+
+@end
+
+@interface ContentSuggestionsReturnToRecentTabCell : MDCCollectionViewCell
+
+// Sets the title of the most recent tab tile.
+- (void)setTitle:(NSString*)title;
+
+// sets the subtitle of the most recent tab tile.
+- (void)setSubtitle:(NSString*)subtitle;
+
++ (CGSize)defaultSize;
+
+// Sets the image that should be displayed at the leading edge of the cell.
+- (void)setIconImage:(UIImage*)image;
+
+@end
+
+#endif  // IOS_CHROME_BROWSER_UI_CONTENT_SUGGESTIONS_CELLS_CONTENT_SUGGESTIONS_RETURN_TO_RECENT_TAB_ITEM_H_
diff --git a/ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_return_to_recent_tab_item.mm b/ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_return_to_recent_tab_item.mm
new file mode 100644
index 0000000..0797ae5
--- /dev/null
+++ b/ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_return_to_recent_tab_item.mm
@@ -0,0 +1,97 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_return_to_recent_tab_item.h"
+
+#import "ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_gesture_commands.h"
+#include "ui/base/l10n/l10n_util.h"
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+namespace {
+const CGSize regularCellSize = {/*width=*/343, /*height=*/72};
+}
+
+@implementation ContentSuggestionsReturnToRecentTabItem
+@synthesize metricsRecorded;
+@synthesize suggestionIdentifier;
+
+- (instancetype)initWithType:(NSInteger)type {
+  self = [super initWithType:type];
+  if (self) {
+    self.cellClass = [ContentSuggestionsReturnToRecentTabCell class];
+  }
+  return self;
+}
+
+- (void)configureCell:(ContentSuggestionsReturnToRecentTabCell*)cell {
+  [super configureCell:cell];
+  [cell setTitle:self.title];
+  [cell setSubtitle:self.subtitle];
+  cell.accessibilityLabel = self.title;
+  if (self.icon) {
+    [cell setIconImage:self.icon];
+  }
+  cell.accessibilityCustomActions = [self customActions];
+}
+
+- (CGFloat)cellHeightForWidth:(CGFloat)width {
+  return [ContentSuggestionsReturnToRecentTabCell defaultSize].height;
+}
+
+// Custom action for a cell configured with this item.
+- (NSArray<UIAccessibilityCustomAction*>*)customActions {
+  UIAccessibilityCustomAction* openMostRecentTab =
+      [[UIAccessibilityCustomAction alloc]
+          initWithName:@"Open Most Recent Tab"
+                target:self
+              selector:@selector(openMostRecentTab)];
+
+  return @[ openMostRecentTab ];
+}
+
+- (BOOL)openMostRecentTab {
+  // TODO:(crbug.com/1173160) implement.
+  return YES;
+}
+
+@end
+
+#pragma mark - ContentSuggestionsReturnToRecentTabCell
+
+@interface ContentSuggestionsReturnToRecentTabCell ()
+
+// Favicon image.
+@property(nonatomic, strong) UIImageView* iconImageView;
+
+// Title of the most recent tab tile.
+@property(nonatomic, strong, readonly) UILabel* titleLabel;
+
+// Subtitle of the most recent tab tile.
+@property(nonatomic, strong, readonly) UILabel* subtitleLabel;
+
+@end
+
+@implementation ContentSuggestionsReturnToRecentTabCell
+
+- (void)setTitle:(NSString*)title {
+  self.titleLabel.text = title;
+}
+
+- (void)setSubtitle:(NSString*)subtitle {
+  self.subtitleLabel.text = subtitle;
+}
+
++ (CGSize)defaultSize {
+  return regularCellSize;
+}
+
+- (void)setIconImage:(UIImage*)image {
+  _iconImageView.image = image;
+  _iconImageView.hidden = image == nil;
+}
+
+@end
diff --git a/ios/chrome/browser/ui/content_suggestions/content_suggestions_collection_updater.h b/ios/chrome/browser/ui/content_suggestions/content_suggestions_collection_updater.h
index 09bfc7d..67a1bdd 100644
--- a/ios/chrome/browser/ui/content_suggestions/content_suggestions_collection_updater.h
+++ b/ios/chrome/browser/ui/content_suggestions/content_suggestions_collection_updater.h
@@ -25,6 +25,7 @@
   ContentSuggestionTypeArticle,
   ContentSuggestionTypeReadingList,
   ContentSuggestionTypeMostVisited,
+  ContentSuggestionTypeReturnToRecentTab,
   ContentSuggestionTypePromo,
   ContentSuggestionTypeLearnMore,
   ContentSuggestionTypeDiscover,
diff --git a/ios/chrome/browser/ui/content_suggestions/content_suggestions_collection_updater.mm b/ios/chrome/browser/ui/content_suggestions/content_suggestions_collection_updater.mm
index 1f7e4b53..c7ed780 100644
--- a/ios/chrome/browser/ui/content_suggestions/content_suggestions_collection_updater.mm
+++ b/ios/chrome/browser/ui/content_suggestions/content_suggestions_collection_updater.mm
@@ -54,6 +54,7 @@
   ItemTypePromo,
   ItemTypeLearnMore,
   ItemTypeDiscover,
+  ItemTypeReturnToRecentTab,
   ItemTypeUnknown,
 };
 
@@ -64,6 +65,7 @@
   SectionIdentifierReadingList,
   SectionIdentifierMostVisited,
   SectionIdentifierLogo,
+  SectionIdentifierReturnToRecentTab,
   SectionIdentifierPromo,
   SectionIdentifierLearnMore,
   SectionIdentifierDiscover,
@@ -78,6 +80,8 @@
     return ContentSuggestionTypeEmpty;
   if (type == ItemTypeReadingList)
     return ContentSuggestionTypeReadingList;
+  if (type == ItemTypeReturnToRecentTab)
+    return ContentSuggestionTypeReturnToRecentTab;
   if (type == ItemTypeMostVisited)
     return ContentSuggestionTypeMostVisited;
   if (type == ItemTypePromo)
@@ -99,6 +103,8 @@
       return ItemTypeArticle;
     case ContentSuggestionsSectionReadingList:
       return ItemTypeReadingList;
+    case ContentSuggestionsSectionReturnToRecentTab:
+      return ItemTypeReturnToRecentTab;
     case ContentSuggestionsSectionMostVisited:
       return ItemTypeMostVisited;
     case ContentSuggestionsSectionPromo:
@@ -125,6 +131,8 @@
       return SectionIdentifierMostVisited;
     case ContentSuggestionsSectionLogo:
       return SectionIdentifierLogo;
+    case ContentSuggestionsSectionReturnToRecentTab:
+      return SectionIdentifierReturnToRecentTab;
     case ContentSuggestionsSectionPromo:
       return SectionIdentifierPromo;
     case ContentSuggestionsSectionLearnMore:
diff --git a/ios/chrome/browser/ui/content_suggestions/content_suggestions_commands.h b/ios/chrome/browser/ui/content_suggestions/content_suggestions_commands.h
index ee07d09..caa4dc35 100644
--- a/ios/chrome/browser/ui/content_suggestions/content_suggestions_commands.h
+++ b/ios/chrome/browser/ui/content_suggestions/content_suggestions_commands.h
@@ -14,21 +14,23 @@
 // Opens the Reading List.
 - (void)openReadingList;
 // Opens the page associated with the item at |indexPath|.
-- (void)openPageForItemAtIndexPath:(nonnull NSIndexPath*)indexPath;
+- (void)openPageForItemAtIndexPath:(NSIndexPath*)indexPath;
 // Opens the Most Visited associated with this |item| at the |mostVisitedItem|.
-- (void)openMostVisitedItem:(nonnull CollectionViewItem*)item
+- (void)openMostVisitedItem:(CollectionViewItem*)item
                     atIndex:(NSInteger)mostVisitedIndex;
+// Handles the actions tapping the "Return to Recent Tab" item that returns the
+// user to the last opened tab.
+- (void)openMostRecentTab:(CollectionViewItem*)item;
 // Displays a context menu for the |suggestionItem|.
-- (void)displayContextMenuForSuggestion:
-            (nonnull CollectionViewItem*)suggestionItem
+- (void)displayContextMenuForSuggestion:(CollectionViewItem*)suggestionItem
                                 atPoint:(CGPoint)touchLocation
-                            atIndexPath:(nonnull NSIndexPath*)indexPath
+                            atIndexPath:(NSIndexPath*)indexPath
                         readLaterAction:(BOOL)readLaterAction;
 // Displays a context menu for the |mostVisitedItem|.
 - (void)displayContextMenuForMostVisitedItem:
-            (nonnull CollectionViewItem*)mostVisitedItem
+            (CollectionViewItem*)mostVisitedItem
                                      atPoint:(CGPoint)touchLocation
-                                 atIndexPath:(nonnull NSIndexPath*)indexPath;
+                                 atIndexPath:(NSIndexPath*)indexPath;
 // Dismisses the context menu if it is displayed.
 - (void)dismissModals;
 // Handles the actions following a tap on the promo.
diff --git a/ios/chrome/browser/ui/content_suggestions/content_suggestions_coordinator.h b/ios/chrome/browser/ui/content_suggestions/content_suggestions_coordinator.h
index 5089742a..c6fa5580 100644
--- a/ios/chrome/browser/ui/content_suggestions/content_suggestions_coordinator.h
+++ b/ios/chrome/browser/ui/content_suggestions/content_suggestions_coordinator.h
@@ -84,6 +84,8 @@
 // Constrains the named layout guide for the Discover header menu button.
 - (void)constrainDiscoverHeaderMenuButtonNamedGuide;
 
+// Configure Content Suggestions if showing the Start Surface.
+- (void)configureStartSurfaceIfNeeded;
 @end
 
 #endif  // IOS_CHROME_BROWSER_UI_CONTENT_SUGGESTIONS_CONTENT_SUGGESTIONS_COORDINATOR_H_
diff --git a/ios/chrome/browser/ui/content_suggestions/content_suggestions_coordinator.mm b/ios/chrome/browser/ui/content_suggestions/content_suggestions_coordinator.mm
index ec7edbc..cfdc765 100644
--- a/ios/chrome/browser/ui/content_suggestions/content_suggestions_coordinator.mm
+++ b/ios/chrome/browser/ui/content_suggestions/content_suggestions_coordinator.mm
@@ -9,6 +9,7 @@
 #include "base/metrics/user_metrics.h"
 #include "base/metrics/user_metrics_action.h"
 #include "base/scoped_observer.h"
+#include "base/strings/sys_string_conversions.h"
 #import "components/feature_engagement/public/event_constants.h"
 #import "components/feature_engagement/public/tracker.h"
 #include "components/feed/core/shared_prefs/pref_names.h"
@@ -74,10 +75,14 @@
 #import "ios/chrome/browser/ui/overscroll_actions/overscroll_actions_controller.h"
 #import "ios/chrome/browser/ui/settings/utils/pref_backed_boolean.h"
 #import "ios/chrome/browser/ui/sharing/sharing_coordinator.h"
+#import "ios/chrome/browser/ui/start_surface/start_surface_features.h"
+#import "ios/chrome/browser/ui/start_surface/start_surface_recent_tab_browser_agent.h"
+#import "ios/chrome/browser/ui/start_surface/start_surface_util.h"
 #import "ios/chrome/browser/ui/util/named_guide.h"
 #import "ios/chrome/browser/ui/util/uikit_ui_util.h"
 #import "ios/chrome/browser/url_loading/url_loading_browser_agent.h"
 #import "ios/chrome/browser/url_loading/url_loading_params.h"
+#import "ios/chrome/browser/web_state_list/web_state_list.h"
 #include "ios/chrome/grit/ios_strings.h"
 #import "ios/public/provider/chrome/browser/chrome_browser_provider.h"
 #import "ios/public/provider/chrome/browser/discover_feed/discover_feed_provider.h"
@@ -99,6 +104,10 @@
     OverscrollActionsControllerDelegate,
     ThemeChangeDelegate,
     URLDropDelegate> {
+  // Observer bridge for mediator to listen to
+  // StartSurfaceRecentTabRemovalObserverBridge.
+  std::unique_ptr<StartSurfaceRecentTabRemovalObserverBridge>
+      _startSurfaceObserver;
 }
 
 @property(nonatomic, strong)
@@ -245,6 +254,9 @@
         self.contentSuggestionsExpanded;
   }
   self.contentSuggestionsMediator.discoverFeedDelegate = self;
+  self.contentSuggestionsMediator.webStateList =
+      self.browser->GetWebStateList();
+  [self configureStartSurfaceIfNeeded];
 
   self.headerController.promoCanShow =
       [self.contentSuggestionsMediator notificationPromo]->CanShow();
@@ -355,6 +367,11 @@
         ->RemoveFeedViewController(self.discoverFeedViewController);
   }
   self.contentSuggestionsExpanded = nil;
+  if (_startSurfaceObserver) {
+    StartSurfaceRecentTabBrowserAgent::FromBrowser(self.browser)
+        ->RemoveObserver(_startSurfaceObserver.get());
+    _startSurfaceObserver.reset();
+  }
   _started = NO;
 }
 
@@ -394,6 +411,12 @@
   }
 }
 
+- (void)viewDidDisappear {
+  if (ShouldShowReturnToMostRecentTabForStartSurface()) {
+    [self.contentSuggestionsMediator hideRecentTabTile];
+  }
+}
+
 #pragma mark - OverscrollActionsControllerDelegate
 
 - (void)overscrollActionsController:(OverscrollActionsController*)controller
@@ -710,6 +733,30 @@
 
 #pragma mark - Helpers
 
+- (void)configureStartSurfaceIfNeeded {
+  SceneState* scene =
+      SceneStateBrowserAgent::FromBrowser(self.browser)->GetSceneState();
+  BOOL shouldShowReturnToRecentTabTile =
+      scene.modifytVisibleNTPForStartSurface &&
+      ShouldShowReturnToMostRecentTabForStartSurface();
+  if (shouldShowReturnToRecentTabTile) {
+    web::WebState* most_recent_tab =
+        StartSurfaceRecentTabBrowserAgent::FromBrowser(self.browser)
+            ->most_recent_tab();
+    DCHECK(most_recent_tab);
+    [self.contentSuggestionsMediator
+        configureMostRecentTabItemWithWebState:most_recent_tab];
+    if (!_startSurfaceObserver) {
+      _startSurfaceObserver =
+          std::make_unique<StartSurfaceRecentTabRemovalObserverBridge>(
+              self.contentSuggestionsMediator);
+      StartSurfaceRecentTabBrowserAgent::FromBrowser(self.browser)
+          ->AddObserver(_startSurfaceObserver.get());
+    }
+    scene.modifytVisibleNTPForStartSurface = NO;
+  }
+}
+
 // Creates, configures and returns a DiscoverFeed ViewController.
 - (UIViewController*)discoverFeed {
   if (!IsDiscoverFeedEnabled() || IsRefactoredNTP() ||
diff --git a/ios/chrome/browser/ui/content_suggestions/content_suggestions_mediator.h b/ios/chrome/browser/ui/content_suggestions/content_suggestions_mediator.h
index fdb6564..865f924 100644
--- a/ios/chrome/browser/ui/content_suggestions/content_suggestions_mediator.h
+++ b/ios/chrome/browser/ui/content_suggestions/content_suggestions_mediator.h
@@ -14,6 +14,7 @@
 #import "ios/chrome/browser/ui/content_suggestions/content_suggestions_mediator.h"
 #import "ios/chrome/browser/ui/content_suggestions/content_suggestions_metrics_recorder.h"
 #import "ios/chrome/browser/ui/settings/utils/pref_backed_boolean.h"
+#import "ios/chrome/browser/ui/start_surface/start_surface_recent_tab_removal_observer_bridge.h"
 
 namespace favicon {
 class LargeIconService;
@@ -41,13 +42,15 @@
 class LargeIconCache;
 class NotificationPromoWhatsNew;
 class ReadingListModel;
+class WebStateList;
 
 // Mediator for ContentSuggestions. Makes the interface between a
 // ntp_snippets::ContentSuggestionsService and the Objective-C services using
 // its data.
 @interface ContentSuggestionsMediator
-    : NSObject<ContentSuggestionsDataSource,
-               ContentSuggestionsMetricsRecorderDelegate>
+    : NSObject <ContentSuggestionsDataSource,
+                ContentSuggestionsMetricsRecorderDelegate,
+                StartSurfaceRecentTabRemovalObserving>
 
 // Initialize the mediator with the |contentService| to mediate.
 - (instancetype)
@@ -91,6 +94,9 @@
 // The consumer for this mediator.
 @property(nonatomic, weak) id<ContentSuggestionsConsumer> consumer;
 
+// WebStateList associated with this mediator.
+@property(nonatomic, assign) WebStateList* webStateList;
+
 // Disconnects the mediator.
 - (void)disconnect;
 
@@ -109,6 +115,12 @@
 // Get the maximum number of sites shown.
 + (NSUInteger)maxSitesShown;
 
+// Configures the most recent tab item for |webState|.
+- (void)configureMostRecentTabItemWithWebState:(web::WebState*)webState;
+
+// Indicates that the "Return to Recent Tab" tile should be hidden.
+- (void)hideRecentTabTile;
+
 @end
 
 #endif  // IOS_CHROME_BROWSER_UI_CONTENT_SUGGESTIONS_CONTENT_SUGGESTIONS_MEDIATOR_H_
diff --git a/ios/chrome/browser/ui/content_suggestions/content_suggestions_mediator.mm b/ios/chrome/browser/ui/content_suggestions/content_suggestions_mediator.mm
index 67bcd86..1fed49b 100644
--- a/ios/chrome/browser/ui/content_suggestions/content_suggestions_mediator.mm
+++ b/ios/chrome/browser/ui/content_suggestions/content_suggestions_mediator.mm
@@ -8,6 +8,7 @@
 #include "base/mac/foundation_util.h"
 #include "base/optional.h"
 #include "base/strings/sys_string_conversions.h"
+#include "components/favicon/ios/web_favicon_driver.h"
 #include "components/ntp_snippets/category.h"
 #include "components/ntp_snippets/category_info.h"
 #include "components/ntp_snippets/content_suggestion.h"
@@ -28,6 +29,7 @@
 #import "ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_learn_more_item.h"
 #import "ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_most_visited_action_item.h"
 #import "ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_most_visited_item.h"
+#import "ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_return_to_recent_tab_item.h"
 #import "ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_whats_new_item.h"
 #import "ios/chrome/browser/ui/content_suggestions/cells/suggested_content.h"
 #import "ios/chrome/browser/ui/content_suggestions/content_suggestions_category_wrapper.h"
@@ -47,10 +49,13 @@
 #include "ios/chrome/browser/ui/ntp/ntp_tile_saver.h"
 #import "ios/chrome/browser/ui/start_surface/start_surface_features.h"
 #include "ios/chrome/browser/ui/util/ui_util.h"
+#import "ios/chrome/browser/web_state_list/web_state_list.h"
 #include "ios/chrome/common/app_group/app_group_constants.h"
+#include "ios/chrome/grit/ios_strings.h"
 #include "ios/public/provider/chrome/browser/chrome_browser_provider.h"
 #import "ios/public/provider/chrome/browser/discover_feed/discover_feed_observer_bridge.h"
 #include "ios/public/provider/chrome/browser/images/branded_image_provider.h"
+#include "ui/base/l10n/l10n_util_mac.h"
 
 #if !defined(__has_feature) || !__has_feature(objc_arc)
 #error "This file requires ARC support."
@@ -104,6 +109,12 @@
 // Section Info for the logo and omnibox section.
 @property(nonatomic, strong)
     ContentSuggestionsSectionInformation* logoSectionInfo;
+// Section Info for the "Return to Recent Tab" section.
+@property(nonatomic, strong)
+    ContentSuggestionsSectionInformation* returnToRecentTabSectionInfo;
+// Item for the "Return to Recent Tab" tile.
+@property(nonatomic, strong)
+    ContentSuggestionsReturnToRecentTabItem* returnToRecentTabItem;
 // Section Info for the What's New promo section.
 @property(nonatomic, strong)
     ContentSuggestionsSectionInformation* promoSectionInfo;
@@ -139,7 +150,8 @@
 @property(nonatomic, strong) ContentSuggestionsDiscoverItem* discoverItem;
 // Number of unread items in reading list model.
 @property(nonatomic, assign) NSInteger readingListUnreadCount;
-
+// Whether to show the most recent tab tile.
+@property(nonatomic, assign) BOOL showMostRecentTabStartSurfaceTile;
 // Whether the incognito mode is available.
 @property(nonatomic, assign) BOOL incognitoAvailable;
 
@@ -267,6 +279,47 @@
   return kMaxNumMostVisitedTiles;
 }
 
+- (void)configureMostRecentTabItemWithWebState:(web::WebState*)webState {
+  DCHECK(IsStartSurfaceEnabled());
+  self.returnToRecentTabSectionInfo = ReturnToRecentTabSectionInformation();
+  if (!self.returnToRecentTabItem) {
+    self.returnToRecentTabItem =
+        [[ContentSuggestionsReturnToRecentTabItem alloc] initWithType:0];
+  }
+
+  // Retrieve favicon associated with the page.
+  favicon::WebFaviconDriver* driver =
+      favicon::WebFaviconDriver::FromWebState(webState);
+  if (driver->FaviconIsValid()) {
+    gfx::Image favicon = driver->GetFavicon();
+    if (!favicon.IsEmpty()) {
+      self.returnToRecentTabItem.icon = favicon.ToUIImage();
+    }
+  }
+
+  self.returnToRecentTabItem.title =
+      l10n_util::GetNSString(IDS_IOS_RETURN_TO_RECENT_TAB_TITLE);
+  self.returnToRecentTabItem.subtitle =
+      base::SysUTF16ToNSString(webState->GetTitle());
+  self.showMostRecentTabStartSurfaceTile = YES;
+
+  // TODO(crbug.com/1187303): Create insert section to add a section.
+  [self.dataSink reloadAllData];
+}
+
+- (void)hideRecentTabTile {
+  DCHECK(IsStartSurfaceEnabled());
+  self.showMostRecentTabStartSurfaceTile = NO;
+  [self.dataSink clearSection:self.returnToRecentTabSectionInfo];
+}
+
+#pragma mark - StartSurfaceRecentTabRemovalObserving
+
+- (void)mostRecentTabWasRemoved:(web::WebState*)web_state {
+  DCHECK(IsStartSurfaceEnabled());
+  [self hideRecentTabTile];
+}
+
 #pragma mark - ContentSuggestionsDataSource
 
 - (NSArray<ContentSuggestionsSectionInformation*>*)sectionsInfo {
@@ -275,6 +328,11 @@
 
   [sectionsInfo addObject:self.logoSectionInfo];
 
+  if (self.showMostRecentTabStartSurfaceTile) {
+    DCHECK(IsStartSurfaceEnabled());
+    [sectionsInfo addObject:self.returnToRecentTabSectionInfo];
+  }
+
   if (_notificationPromo->CanShow()) {
     [sectionsInfo addObject:self.promoSectionInfo];
   }
@@ -327,6 +385,9 @@
       item.text = base::SysUTF8ToNSString(_notificationPromo->promo_text());
       [convertedSuggestions addObject:item];
     }
+  } else if (sectionInfo == self.returnToRecentTabSectionInfo) {
+    DCHECK(IsStartSurfaceEnabled());
+    [convertedSuggestions addObject:self.returnToRecentTabItem];
   } else if (sectionInfo == self.mostVisitedSectionInfo) {
     [convertedSuggestions addObjectsFromArray:self.mostVisitedItems];
     if (!ShouldHideShortcutsForStartSurface()) {
diff --git a/ios/chrome/browser/ui/content_suggestions/content_suggestions_view_controller.mm b/ios/chrome/browser/ui/content_suggestions/content_suggestions_view_controller.mm
index 47bb324..3cf99cc 100644
--- a/ios/chrome/browser/ui/content_suggestions/content_suggestions_view_controller.mm
+++ b/ios/chrome/browser/ui/content_suggestions/content_suggestions_view_controller.mm
@@ -37,6 +37,7 @@
 #import "ios/chrome/browser/ui/ntp/new_tab_page_header_constants.h"
 #import "ios/chrome/browser/ui/ntp_tile_views/ntp_tile_layout_util.h"
 #import "ios/chrome/browser/ui/overscroll_actions/overscroll_actions_controller.h"
+#import "ios/chrome/browser/ui/start_surface/start_surface_features.h"
 #import "ios/chrome/browser/ui/toolbar/public/toolbar_utils.h"
 #import "ios/chrome/browser/ui/ui_feature_flags.h"
 #import "ios/chrome/browser/ui/util/menu_util.h"
@@ -341,6 +342,9 @@
 - (void)viewDidDisappear:(BOOL)animated {
   [super viewDidDisappear:animated];
   self.headerSynchronizer.showing = NO;
+  if (ShouldShowReturnToMostRecentTabForStartSurface()) {
+    [self.audience viewDidDisappear];
+  }
 }
 
 - (void)didMoveToParentViewController:(UIViewController*)parent {
@@ -426,6 +430,9 @@
       [self.suggestionCommandHandler openMostVisitedItem:item
                                                  atIndex:indexPath.item];
       break;
+    case ContentSuggestionTypeReturnToRecentTab:
+      [self.suggestionCommandHandler openMostRecentTab:item];
+      break;
     case ContentSuggestionTypePromo:
       [self dismissSection:indexPath.section];
       [self.suggestionCommandHandler handlePromoTapped];
diff --git a/ios/chrome/browser/ui/content_suggestions/content_suggestions_view_controller_audience.h b/ios/chrome/browser/ui/content_suggestions/content_suggestions_view_controller_audience.h
index 450c9e7..920bcc8f 100644
--- a/ios/chrome/browser/ui/content_suggestions/content_suggestions_view_controller_audience.h
+++ b/ios/chrome/browser/ui/content_suggestions/content_suggestions_view_controller_audience.h
@@ -20,6 +20,9 @@
 // called multiple times.
 - (void)discoverFeedShown;
 
+// Notifies the audience of the UIKit viewDidDisappear: callback.
+- (void)viewDidDisappear;
+
 @end
 
 #endif  // IOS_CHROME_BROWSER_UI_CONTENT_SUGGESTIONS_CONTENT_SUGGESTIONS_VIEW_CONTROLLER_AUDIENCE_H_
diff --git a/ios/chrome/browser/ui/content_suggestions/identifier/content_suggestions_section_information.h b/ios/chrome/browser/ui/content_suggestions/identifier/content_suggestions_section_information.h
index d853b1a..5397557 100644
--- a/ios/chrome/browser/ui/content_suggestions/identifier/content_suggestions_section_information.h
+++ b/ios/chrome/browser/ui/content_suggestions/identifier/content_suggestions_section_information.h
@@ -19,6 +19,7 @@
 // different ID.
 typedef NS_ENUM(NSInteger, ContentSuggestionsSectionID) {
   ContentSuggestionsSectionLogo = 0,
+  ContentSuggestionsSectionReturnToRecentTab,
   ContentSuggestionsSectionPromo,
   ContentSuggestionsSectionMostVisited,
   ContentSuggestionsSectionArticles,
diff --git a/ios/chrome/browser/ui/content_suggestions/mediator_util.h b/ios/chrome/browser/ui/content_suggestions/mediator_util.h
index d4d3900b..aaff7a9 100644
--- a/ios/chrome/browser/ui/content_suggestions/mediator_util.h
+++ b/ios/chrome/browser/ui/content_suggestions/mediator_util.h
@@ -47,6 +47,10 @@
     ContentSuggestionsCategoryWrapper* category,
     const std::string& id_in_category);
 
+// Creates and returns a SectionInfo for the section containing the "Return to
+// Recent Tab" tile for the Start Surface.
+ContentSuggestionsSectionInformation* ReturnToRecentTabSectionInformation();
+
 // Creates and returns a SectionInfo for the section containing the logo and
 // omnibox.
 ContentSuggestionsSectionInformation* LogoSectionInformation();
diff --git a/ios/chrome/browser/ui/content_suggestions/mediator_util.mm b/ios/chrome/browser/ui/content_suggestions/mediator_util.mm
index 7c913eae..6029ef8f 100644
--- a/ios/chrome/browser/ui/content_suggestions/mediator_util.mm
+++ b/ios/chrome/browser/ui/content_suggestions/mediator_util.mm
@@ -120,6 +120,10 @@
   return sectionInfo;
 }
 
+ContentSuggestionsSectionInformation* ReturnToRecentTabSectionInformation() {
+  return EmptySectionInfo(ContentSuggestionsSectionReturnToRecentTab);
+}
+
 ContentSuggestionsSectionInformation* PromoSectionInformation() {
   return EmptySectionInfo(ContentSuggestionsSectionPromo);
 }
diff --git a/ios/chrome/browser/ui/content_suggestions/ntp_home_mediator.mm b/ios/chrome/browser/ui/content_suggestions/ntp_home_mediator.mm
index a3e15172..07d5eac0 100644
--- a/ios/chrome/browser/ui/content_suggestions/ntp_home_mediator.mm
+++ b/ios/chrome/browser/ui/content_suggestions/ntp_home_mediator.mm
@@ -33,6 +33,7 @@
 #import "ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_item.h"
 #import "ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_most_visited_action_item.h"
 #import "ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_most_visited_item.h"
+#import "ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_return_to_recent_tab_item.h"
 #import "ios/chrome/browser/ui/content_suggestions/content_suggestions_alert_factory.h"
 #import "ios/chrome/browser/ui/content_suggestions/content_suggestions_header_synchronizer.h"
 #import "ios/chrome/browser/ui/content_suggestions/content_suggestions_mediator.h"
@@ -53,6 +54,7 @@
 #import "ios/chrome/browser/url_loading/url_loading_browser_agent.h"
 #import "ios/chrome/browser/url_loading/url_loading_params.h"
 #import "ios/chrome/browser/voice/voice_search_availability.h"
+#import "ios/chrome/browser/web_state_list/web_state_list.h"
 #import "ios/chrome/common/ui/favicon/favicon_attributes.h"
 #include "ios/chrome/grit/ios_strings.h"
 #import "ios/public/provider/chrome/browser/chrome_browser_provider.h"
@@ -431,6 +433,17 @@
   [self.NTPMetrics recordAction:new_tab_page_uma::ACTION_OPENED_LEARN_MORE];
 }
 
+- (void)openMostRecentTab:(CollectionViewItem*)item {
+  DCHECK([item isKindOfClass:[ContentSuggestionsReturnToRecentTabItem class]]);
+  [self.suggestionsMediator hideRecentTabTile];
+  WebStateList* web_state_list = self.browser->GetWebStateList();
+  web::WebState* web_state =
+      StartSurfaceRecentTabBrowserAgent::FromBrowser(self.browser)
+          ->most_recent_tab();
+  int index = web_state_list->GetIndexOfWebState(web_state);
+  web_state_list->ActivateWebStateAt(index);
+}
+
 #pragma mark - ContentSuggestionsGestureCommands
 
 - (void)openNewTabWithSuggestionsItem:(ContentSuggestionsItem*)item
diff --git a/ios/chrome/browser/ui/find_bar/find_in_page_egtest.mm b/ios/chrome/browser/ui/find_bar/find_in_page_egtest.mm
index 9de3659..8c3c056 100644
--- a/ios/chrome/browser/ui/find_bar/find_in_page_egtest.mm
+++ b/ios/chrome/browser/ui/find_bar/find_in_page_egtest.mm
@@ -130,7 +130,8 @@
 // Tests that Find In Page search term retention is working as expected, e.g.
 // the search term is persisted between FIP runs, but in incognito search term
 // is not retained and not autofilled.
-- (void)testFindInPageRetainsSearchTerm {
+// TODO(crbug.com/1188709) : Fix failing test.
+- (void)DISABLED_testFindInPageRetainsSearchTerm {
   // Type "find".
   [self typeFindInPageText:@"find"];
   [self assertResultStringIsResult:1 outOfTotal:2];
diff --git a/ios/chrome/browser/ui/main/scene_controller.mm b/ios/chrome/browser/ui/main/scene_controller.mm
index 6b0a46a..a110cc9 100644
--- a/ios/chrome/browser/ui/main/scene_controller.mm
+++ b/ios/chrome/browser/ui/main/scene_controller.mm
@@ -91,6 +91,7 @@
 #import "ios/chrome/browser/ui/main/ui_blocker_scene_agent.h"
 #import "ios/chrome/browser/ui/scoped_ui_blocker/scoped_ui_blocker.h"
 #import "ios/chrome/browser/ui/settings/settings_navigation_controller.h"
+#import "ios/chrome/browser/ui/start_surface/start_surface_recent_tab_browser_agent.h"
 #import "ios/chrome/browser/ui/start_surface/start_surface_scene_agent.h"
 #import "ios/chrome/browser/ui/start_surface/start_surface_util.h"
 #include "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_grid_coordinator.h"
@@ -559,10 +560,13 @@
   if (!ShouldShowStartSurfaceForSceneState(self.sceneState)) {
     return;
   }
+
   self.sceneState.modifytVisibleNTPForStartSurface = YES;
+  Browser* browser = self.currentInterface.browser;
+  StartSurfaceRecentTabBrowserAgent::FromBrowser(browser)->SaveMostRecentTab();
 
   // Activate the existing NTP tab for the Start surface.
-  WebStateList* webStateList = self.currentInterface.browser->GetWebStateList();
+  WebStateList* webStateList = browser->GetWebStateList();
   for (int i = 0; i < webStateList->count(); i++) {
     if (IsURLNtp(webStateList->GetWebStateAt(i)->GetVisibleURL())) {
       webStateList->ActivateWebStateAt(i);
@@ -574,7 +578,6 @@
   OpenNewTabCommand* command =
       [OpenNewTabCommand commandWithIncognito:self.currentInterface.incognito];
   command.userInitiated = NO;
-  Browser* browser = self.currentInterface.browser;
   id<ApplicationCommands> applicationHandler =
       HandlerForProtocol(browser->GetCommandDispatcher(), ApplicationCommands);
   [applicationHandler openURLInNewTab:command];
diff --git a/ios/chrome/browser/ui/ntp/new_tab_page_coordinator.mm b/ios/chrome/browser/ui/ntp/new_tab_page_coordinator.mm
index 9eb1c560..51cf121c 100644
--- a/ios/chrome/browser/ui/ntp/new_tab_page_coordinator.mm
+++ b/ios/chrome/browser/ui/ntp/new_tab_page_coordinator.mm
@@ -416,6 +416,9 @@
 }
 
 - (void)ntpDidChangeVisibility:(BOOL)visible {
+  if (visible) {
+    [self.contentSuggestionsCoordinator configureStartSurfaceIfNeeded];
+  }
   self.viewPresented = visible;
   [self updateVisible];
 }
diff --git a/ios/chrome/browser/ui/start_surface/BUILD.gn b/ios/chrome/browser/ui/start_surface/BUILD.gn
index 7912b6a..28920a58 100644
--- a/ios/chrome/browser/ui/start_surface/BUILD.gn
+++ b/ios/chrome/browser/ui/start_surface/BUILD.gn
@@ -18,6 +18,10 @@
 
 source_set("start_surface") {
   sources = [
+    "start_surface_recent_tab_browser_agent.h",
+    "start_surface_recent_tab_browser_agent.mm",
+    "start_surface_recent_tab_removal_observer_bridge.h",
+    "start_surface_recent_tab_removal_observer_bridge.mm",
     "start_surface_scene_agent.h",
     "start_surface_scene_agent.mm",
     "start_surface_util.h",
@@ -29,7 +33,9 @@
   deps = [
     ":feature_flags",
     "//base",
+    "//ios/chrome/browser/ui/main:browser_interface_provider",
     "//ios/chrome/browser/ui/main:observing_scene_agent",
     "//ios/chrome/browser/ui/main:scene_state_header",
+    "//ios/chrome/browser/web_state_list",
   ]
 }
diff --git a/ios/chrome/browser/ui/start_surface/start_surface_recent_tab_browser_agent.h b/ios/chrome/browser/ui/start_surface/start_surface_recent_tab_browser_agent.h
new file mode 100644
index 0000000..75e4f39
--- /dev/null
+++ b/ios/chrome/browser/ui/start_surface/start_surface_recent_tab_browser_agent.h
@@ -0,0 +1,84 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef IOS_CHROME_BROWSER_UI_START_SURFACE_START_SURFACE_RECENT_TAB_BROWSER_AGENT_H_
+#define IOS_CHROME_BROWSER_UI_START_SURFACE_START_SURFACE_RECENT_TAB_BROWSER_AGENT_H_
+
+#include "base/observer_list.h"
+#include "ios/chrome/browser/main/browser_observer.h"
+#import "ios/chrome/browser/main/browser_user_data.h"
+#import "ios/chrome/browser/web_state_list/web_state_list_observer.h"
+
+namespace web {
+class WebState;
+}  // namespace web
+
+class Browser;
+
+// Interface for listening to the removal of the most recent tab.
+class StartSurfaceRecentTabRemovalObserver {
+ public:
+  StartSurfaceRecentTabRemovalObserver() {}
+
+  // Not copyable or moveable.
+  StartSurfaceRecentTabRemovalObserver(
+      const StartSurfaceRecentTabRemovalObserver&) = delete;
+  StartSurfaceRecentTabRemovalObserver& operator=(
+      const StartSurfaceRecentTabRemovalObserver&) = delete;
+
+  // Notifies the receiver that the most recent tab was removed.
+  virtual void MostRecentTabRemoved(web::WebState* web_state) {}
+
+ protected:
+  virtual ~StartSurfaceRecentTabRemovalObserver() = default;
+};
+
+// Browser Agent that manages the most recent WebState for the Start Surface and
+// listens to WebStateListObserver for instances of that WebState's removal.
+class StartSurfaceRecentTabBrowserAgent
+    : public BrowserUserData<StartSurfaceRecentTabBrowserAgent>,
+      BrowserObserver,
+      public WebStateListObserver {
+ public:
+  // Notifies the Browser Agent to save the most recent WebState.
+  void SaveMostRecentTab();
+  // Returns the most recent WebState.
+  web::WebState* most_recent_tab() { return most_recent_tab_; }
+  // Add/Remove observers for this Browser Agent.
+  void AddObserver(StartSurfaceRecentTabRemovalObserver* observer);
+  void RemoveObserver(StartSurfaceRecentTabRemovalObserver* observer);
+
+  ~StartSurfaceRecentTabBrowserAgent() override;
+
+  // Not copyable or moveable.
+  StartSurfaceRecentTabBrowserAgent(const StartSurfaceRecentTabBrowserAgent&) =
+      delete;
+  StartSurfaceRecentTabBrowserAgent& operator=(
+      const StartSurfaceRecentTabBrowserAgent&) = delete;
+
+ private:
+  // Constructor used by CreateForBrowser().
+  friend class BrowserUserData<StartSurfaceRecentTabBrowserAgent>;
+  explicit StartSurfaceRecentTabBrowserAgent(Browser* browser);
+  BROWSER_USER_DATA_KEY_DECL();
+
+  // BrowserObserver
+  void BrowserDestroyed(Browser* browser) override;
+
+  // WebStateListObserver:
+  void WebStateDetachedAt(WebStateList* web_state_list,
+                          web::WebState* web_state,
+                          int index) override;
+
+  // A list of observers notified when the most recent tab is removed. Weak
+  // references.
+  base::ObserverList<StartSurfaceRecentTabRemovalObserver, true>::Unchecked
+      observers_;
+  // The most recent tab managed by this Browser Agent.
+  web::WebState* most_recent_tab_ = nullptr;
+  // Browser.
+  Browser* browser_ = nullptr;
+};
+
+#endif  // IOS_CHROME_BROWSER_UI_START_SURFACE_START_SURFACE_RECENT_TAB_BROWSER_AGENT_H_
diff --git a/ios/chrome/browser/ui/start_surface/start_surface_recent_tab_browser_agent.mm b/ios/chrome/browser/ui/start_surface/start_surface_recent_tab_browser_agent.mm
new file mode 100644
index 0000000..b055a56
--- /dev/null
+++ b/ios/chrome/browser/ui/start_surface/start_surface_recent_tab_browser_agent.mm
@@ -0,0 +1,70 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "ios/chrome/browser/ui/start_surface/start_surface_recent_tab_browser_agent.h"
+
+#import "ios/chrome/browser/ui/main/scene_state_browser_agent.h"
+#import "ios/chrome/browser/ui/start_surface/start_surface_util.h"
+#import "ios/chrome/browser/web_state_list/web_state_list.h"
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+#pragma mark - StartSurfaceBrowserAgent
+
+BROWSER_USER_DATA_KEY_IMPL(StartSurfaceRecentTabBrowserAgent)
+
+StartSurfaceRecentTabBrowserAgent::StartSurfaceRecentTabBrowserAgent(
+    Browser* browser)
+    : browser_(browser) {
+  browser_->AddObserver(this);
+  browser_->GetWebStateList()->AddObserver(this);
+}
+
+StartSurfaceRecentTabBrowserAgent::~StartSurfaceRecentTabBrowserAgent() =
+    default;
+
+#pragma mark - Public
+
+void StartSurfaceRecentTabBrowserAgent::SaveMostRecentTab() {
+  most_recent_tab_ = browser_->GetWebStateList()->GetActiveWebState();
+}
+
+void StartSurfaceRecentTabBrowserAgent::AddObserver(
+    StartSurfaceRecentTabRemovalObserver* observer) {
+  DCHECK(!observers_.HasObserver(observer));
+  observers_.AddObserver(observer);
+}
+
+void StartSurfaceRecentTabBrowserAgent::RemoveObserver(
+    StartSurfaceRecentTabRemovalObserver* observer) {
+  observers_.RemoveObserver(observer);
+}
+
+#pragma mark - BrowserObserver
+
+void StartSurfaceRecentTabBrowserAgent::BrowserDestroyed(Browser* browser) {
+  browser_->GetWebStateList()->RemoveObserver(this);
+  browser_->RemoveObserver(this);
+}
+
+#pragma mark - WebStateListObserver
+
+void StartSurfaceRecentTabBrowserAgent::WebStateDetachedAt(
+    WebStateList* web_state_list,
+    web::WebState* web_state,
+    int index) {
+  if (!most_recent_tab_) {
+    return;
+  }
+
+  if (most_recent_tab_ == web_state) {
+    for (auto& observer : observers_) {
+      observer.MostRecentTabRemoved(most_recent_tab_);
+    }
+    most_recent_tab_ = nullptr;
+    return;
+  }
+}
diff --git a/ios/chrome/browser/ui/start_surface/start_surface_recent_tab_removal_observer_bridge.h b/ios/chrome/browser/ui/start_surface/start_surface_recent_tab_removal_observer_bridge.h
new file mode 100644
index 0000000..f2859d4
--- /dev/null
+++ b/ios/chrome/browser/ui/start_surface/start_surface_recent_tab_removal_observer_bridge.h
@@ -0,0 +1,46 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef IOS_CHROME_BROWSER_UI_START_SURFACE_START_SURFACE_RECENT_TAB_REMOVAL_OBSERVER_BRIDGE_H_
+#define IOS_CHROME_BROWSER_UI_START_SURFACE_START_SURFACE_RECENT_TAB_REMOVAL_OBSERVER_BRIDGE_H_
+
+#import <Foundation/Foundation.h>
+
+#import "ios/chrome/browser/ui/start_surface/start_surface_recent_tab_browser_agent.h"
+
+namespace web {
+class WebState;
+}  // namespace web
+
+// Protocol that corresponds to StartSurfaceRecentTabRemovalObserver API. Allows
+// registering Objective-C objects to listen to removal of the most recent tab.
+@protocol StartSurfaceRecentTabRemovalObserving <NSObject>
+@optional
+// Notifies the receiver that the most recent tab was removed.
+- (void)mostRecentTabWasRemoved:(web::WebState*)web_state;
+@end
+
+// Bridge to use an id<StartSurfaceRecentTabRemovalObserving> as a
+// StartSurfaceRecentTabRemovalObserver.
+class StartSurfaceRecentTabRemovalObserverBridge
+    : public StartSurfaceRecentTabRemovalObserver {
+ public:
+  StartSurfaceRecentTabRemovalObserverBridge(
+      id<StartSurfaceRecentTabRemovalObserving> delegate);
+  ~StartSurfaceRecentTabRemovalObserverBridge() override;
+
+  // Not copyable or moveable.
+  StartSurfaceRecentTabRemovalObserverBridge(
+      const StartSurfaceRecentTabRemovalObserverBridge&) = delete;
+  StartSurfaceRecentTabRemovalObserverBridge& operator=(
+      const StartSurfaceRecentTabRemovalObserverBridge&) = delete;
+
+ private:
+  // StartSurfaceBrowserAgentObserver.
+  void MostRecentTabRemoved(web::WebState* web_state) override;
+
+  __weak id<StartSurfaceRecentTabRemovalObserving> delegate_ = nil;
+};
+
+#endif  // IOS_CHROME_BROWSER_UI_START_SURFACE_START_SURFACE_RECENT_TAB_REMOVAL_OBSERVER_BRIDGE_H_
diff --git a/ios/chrome/browser/ui/start_surface/start_surface_recent_tab_removal_observer_bridge.mm b/ios/chrome/browser/ui/start_surface/start_surface_recent_tab_removal_observer_bridge.mm
new file mode 100644
index 0000000..b26dc032
--- /dev/null
+++ b/ios/chrome/browser/ui/start_surface/start_surface_recent_tab_removal_observer_bridge.mm
@@ -0,0 +1,26 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "ios/chrome/browser/ui/start_surface/start_surface_recent_tab_removal_observer_bridge.h"
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+StartSurfaceRecentTabRemovalObserverBridge::
+    StartSurfaceRecentTabRemovalObserverBridge(
+        id<StartSurfaceRecentTabRemovalObserving> delegate)
+    : delegate_(delegate) {}
+
+StartSurfaceRecentTabRemovalObserverBridge::
+    ~StartSurfaceRecentTabRemovalObserverBridge() = default;
+
+void StartSurfaceRecentTabRemovalObserverBridge::MostRecentTabRemoved(
+    web::WebState* web_state) {
+  const SEL selector = @selector(mostRecentTabWasRemoved:);
+  if (![delegate_ respondsToSelector:selector])
+    return;
+
+  [delegate_ mostRecentTabWasRemoved:web_state];
+}
diff --git a/ios/showcase/content_suggestions/sc_content_suggestions_data_source.mm b/ios/showcase/content_suggestions/sc_content_suggestions_data_source.mm
index 572f7e0..1662a84 100644
--- a/ios/showcase/content_suggestions/sc_content_suggestions_data_source.mm
+++ b/ios/showcase/content_suggestions/sc_content_suggestions_data_source.mm
@@ -120,6 +120,7 @@
       return @[ learnMore ];
     }
     case ContentSuggestionsSectionLogo:
+    case ContentSuggestionsSectionReturnToRecentTab:
     case ContentSuggestionsSectionPromo:
     case ContentSuggestionsSectionDiscover:
     case ContentSuggestionsSectionUnknown:
diff --git a/ipc/ipc_message_utils.cc b/ipc/ipc_message_utils.cc
index f9477ed..473fb14 100644
--- a/ipc/ipc_message_utils.cc
+++ b/ipc/ipc_message_utils.cc
@@ -11,7 +11,6 @@
 #include "base/json/json_writer.h"
 #include "base/logging.h"
 #include "base/memory/ptr_util.h"
-#include "base/strings/nullable_string16.h"
 #include "base/strings/string_number_conversions.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/time/time.h"
@@ -1205,34 +1204,6 @@
   l->append(json);
 }
 
-void ParamTraits<base::NullableString16>::Write(base::Pickle* m,
-                                                const param_type& p) {
-  WriteParam(m, p.string());
-  WriteParam(m, p.is_null());
-}
-
-bool ParamTraits<base::NullableString16>::Read(const base::Pickle* m,
-                                               base::PickleIterator* iter,
-                                               param_type* r) {
-  std::u16string string;
-  if (!ReadParam(m, iter, &string))
-    return false;
-  bool is_null;
-  if (!ReadParam(m, iter, &is_null))
-    return false;
-  *r = base::NullableString16(string, is_null);
-  return true;
-}
-
-void ParamTraits<base::NullableString16>::Log(const param_type& p,
-                                              std::string* l) {
-  l->append("(");
-  LogParam(p.string(), l);
-  l->append(", ");
-  LogParam(p.is_null(), l);
-  l->append(")");
-}
-
 void ParamTraits<base::File::Info>::Write(base::Pickle* m,
                                           const param_type& p) {
   WriteParam(m, p.size);
diff --git a/ipc/ipc_message_utils.h b/ipc/ipc_message_utils.h
index 413bf5c..97e7f0a 100644
--- a/ipc/ipc_message_utils.h
+++ b/ipc/ipc_message_utils.h
@@ -52,7 +52,6 @@
 class DictionaryValue;
 class FilePath;
 class ListValue;
-class NullableString16;
 class Time;
 class TimeDelta;
 class TimeTicks;
@@ -762,16 +761,6 @@
 };
 
 template <>
-struct COMPONENT_EXPORT(IPC) ParamTraits<base::NullableString16> {
-  typedef base::NullableString16 param_type;
-  static void Write(base::Pickle* m, const param_type& p);
-  static bool Read(const base::Pickle* m,
-                   base::PickleIterator* iter,
-                   param_type* r);
-  static void Log(const param_type& p, std::string* l);
-};
-
-template <>
 struct COMPONENT_EXPORT(IPC) ParamTraits<base::File::Info> {
   typedef base::File::Info param_type;
   static void Write(base::Pickle* m, const param_type& p);
diff --git a/media/base/android/java/src/org/chromium/media/MediaCodecUtil.java b/media/base/android/java/src/org/chromium/media/MediaCodecUtil.java
index 8410ecf5..a7cfe41 100644
--- a/media/base/android/java/src/org/chromium/media/MediaCodecUtil.java
+++ b/media/base/android/java/src/org/chromium/media/MediaCodecUtil.java
@@ -419,7 +419,13 @@
 
             // MediaTek decoders do not work properly on vp8. See http://crbug.com/446974 and
             // http://crbug.com/597836.
-            if (Build.HARDWARE.startsWith("mt")) return false;
+            if (Build.HARDWARE.startsWith("mt")) {
+                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) return false;
+
+                // The following chipsets have been confirmed by MediaTek to work on P+
+                return Build.HARDWARE.startsWith("mt5599") || Build.HARDWARE.startsWith("mt5895")
+                        || Build.HARDWARE.startsWith("m7332");
+            }
         } else if (mime.equals(MimeTypes.VIDEO_VP9)) {
             // Nexus Player VP9 decoder performs poorly at >= 1080p resolution.
             if (Build.MODEL.equals("Nexus Player")) {
diff --git a/media/base/android/media_codec_util.cc b/media/base/android/media_codec_util.cc
index ab1975a..b917029 100644
--- a/media/base/android/media_codec_util.cc
+++ b/media/base/android/media_codec_util.cc
@@ -31,6 +31,7 @@
 using base::android::SDK_VERSION_KITKAT;
 using base::android::SDK_VERSION_LOLLIPOP;
 using base::android::SDK_VERSION_LOLLIPOP_MR1;
+using base::android::SDK_VERSION_P;
 
 namespace media {
 
@@ -385,12 +386,17 @@
   // MediaTek hardware vp9 is known crashy, see http://crbug.com/446974 and
   // http://crbug.com/597836.
   if (base::StartsWith(codec_name, "OMX.MTK.", base::CompareCase::SENSITIVE)) {
-    if (codec == kCodecVP8)
-      return true;
+    if (codec == kCodecVP8) {
+      // We may still reject VP8 hardware decoding later on certain chipsets,
+      // see isDecoderSupportedForDevice(). We don't have the the chipset ID
+      // here to check now though.
+      return base::android::BuildInfo::GetInstance()->sdk_int() < SDK_VERSION_P;
+    }
 
-    if (codec == kCodecVP9)
+    if (codec == kCodecVP9) {
       return base::android::BuildInfo::GetInstance()->sdk_int() <
              SDK_VERSION_LOLLIPOP;
+    }
 
     return false;
   }
diff --git a/media/base/media_switches.cc b/media/base/media_switches.cc
index 21c30667..ac2bd4b 100644
--- a/media/base/media_switches.cc
+++ b/media/base/media_switches.cc
@@ -836,6 +836,10 @@
 const base::Feature kMediaPowerExperiment{"MediaPowerExperiment",
                                           base::FEATURE_DISABLED_BY_DEFAULT};
 
+// Enable WebRTC actions for the Media Session API.
+const base::Feature kMediaSessionWebRTC{"MediaSessionWebRTC",
+                                        base::FEATURE_DISABLED_BY_DEFAULT};
+
 // Enables flash to be ducked by audio focus. This is enabled on Chrome OS which
 // has audio focus enabled.
 const base::Feature kAudioFocusDuckFlash {
diff --git a/media/base/media_switches.h b/media/base/media_switches.h
index 013babd..2955055 100644
--- a/media/base/media_switches.h
+++ b/media/base/media_switches.h
@@ -164,6 +164,7 @@
 MEDIA_EXPORT extern const base::Feature kMediaLearningSmoothnessExperiment;
 MEDIA_EXPORT extern const base::Feature kMediaOptimizer;
 MEDIA_EXPORT extern const base::Feature kMediaPowerExperiment;
+MEDIA_EXPORT extern const base::Feature kMediaSessionWebRTC;
 MEDIA_EXPORT extern const base::Feature kMemoryPressureBasedSourceBufferGC;
 MEDIA_EXPORT extern const base::Feature kOverlayFullscreenVideo;
 MEDIA_EXPORT extern const base::Feature kPictureInPicture;
diff --git a/media/gpu/vaapi/vaapi_video_decoder.cc b/media/gpu/vaapi/vaapi_video_decoder.cc
index 8800452..e04ae1a 100644
--- a/media/gpu/vaapi/vaapi_video_decoder.cc
+++ b/media/gpu/vaapi/vaapi_video_decoder.cc
@@ -163,7 +163,8 @@
   DVLOGF(2) << config.AsHumanReadableString();
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   DCHECK(config.IsValidConfig());
-  DCHECK(state_ == State::kUninitialized || state_ == State::kWaitingForInput);
+  DCHECK(state_ == State::kError || state_ == State::kUninitialized ||
+         state_ == State::kWaitingForInput);
 
   // Reinitializing the decoder is allowed if there are no pending decodes.
   if (current_decode_task_ || !decode_task_queue_.empty()) {
@@ -173,43 +174,6 @@
     return;
   }
 
-  if (config.is_encrypted()) {
-#if !BUILDFLAG(IS_CHROMEOS_ASH)
-    std::move(init_cb).Run(StatusCode::kEncryptedContentUnsupported);
-    return;
-#else
-    if (!cdm_context || !cdm_context->GetChromeOsCdmContext()) {
-      LOG(ERROR) << "Cannot support encrypted stream w/out ChromeOsCdmContext";
-      std::move(init_cb).Run(StatusCode::kDecoderMissingCdmForEncryptedContent);
-      return;
-    }
-    if (config.codec() != kCodecH264 && config.codec() != kCodecVP9 &&
-        config.codec() != kCodecHEVC) {
-      VLOGF(1)
-          << "Vaapi decoder does not support this codec for encrypted content";
-      std::move(init_cb).Run(StatusCode::kEncryptedContentUnsupported);
-      return;
-    }
-    cdm_event_cb_registration_ = cdm_context->RegisterEventCB(
-        base::BindRepeating(&VaapiVideoDecoder::OnCdmContextEvent,
-                            weak_this_factory_.GetWeakPtr()));
-    cdm_context_ref_ = cdm_context->GetChromeOsCdmContext()->GetCdmContextRef();
-#endif
-#if BUILDFLAG(ENABLE_PLATFORM_HEVC)
-  } else if (config.codec() == kCodecHEVC &&
-             !base::CommandLine::ForCurrentProcess()->HasSwitch(
-                 switches::kEnableClearHevcForTesting)) {
-    DVLOG(1) << "Clear HEVC content is not supported";
-    std::move(init_cb).Run(StatusCode::kClearContentUnsupported);
-    return;
-#endif
-  }
-
-  // We expect the decoder to have released all output buffers (by the client
-  // triggering a flush or reset), even if the
-  // DecoderInterface API doesn't explicitly specify this.
-  DCHECK(output_frames_.empty());
-
   if (state_ != State::kUninitialized) {
     DVLOGF(3) << "Reinitializing decoder";
 
@@ -230,8 +194,62 @@
     DCHECK(vaapi_wrapper_->HasOneRef());
     vaapi_wrapper_ = nullptr;
     decoder_delegate_ = nullptr;
+
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+    // |cdm_context_ref_| is reset after |decoder_| because we passed
+    // |cdm_context_ref_->GetCdmContext()| when creating the |decoder_|, so we
+    // don't want |decoder_| to have a dangling pointer. We also destroy
+    // |cdm_event_cb_registration_| before |cdm_context_ref_| so that we have a
+    // CDM at the moment of destroying the callback registration.
+    cdm_event_cb_registration_ = nullptr;
+    cdm_context_ref_ = nullptr;
+#endif
+
     SetState(State::kUninitialized);
   }
+  DCHECK(!current_decode_task_);
+  DCHECK(decode_task_queue_.empty());
+
+  // Destroying the |decoder_| during re-initialization should release all
+  // output buffers (and there should be no output buffers to begin with if the
+  // decoder was previously uninitialized).
+  DCHECK(output_frames_.empty());
+
+  if (config.is_encrypted()) {
+#if !BUILDFLAG(IS_CHROMEOS_ASH)
+    SetState(State::kError);
+    std::move(init_cb).Run(StatusCode::kEncryptedContentUnsupported);
+    return;
+#else
+    if (!cdm_context || !cdm_context->GetChromeOsCdmContext()) {
+      LOG(ERROR) << "Cannot support encrypted stream w/out ChromeOsCdmContext";
+      SetState(State::kError);
+      std::move(init_cb).Run(StatusCode::kDecoderMissingCdmForEncryptedContent);
+      return;
+    }
+    if (config.codec() != kCodecH264 && config.codec() != kCodecVP9 &&
+        config.codec() != kCodecHEVC) {
+      VLOGF(1)
+          << "Vaapi decoder does not support this codec for encrypted content";
+      SetState(State::kError);
+      std::move(init_cb).Run(StatusCode::kEncryptedContentUnsupported);
+      return;
+    }
+    cdm_event_cb_registration_ = cdm_context->RegisterEventCB(
+        base::BindRepeating(&VaapiVideoDecoder::OnCdmContextEvent,
+                            weak_this_factory_.GetWeakPtr()));
+    cdm_context_ref_ = cdm_context->GetChromeOsCdmContext()->GetCdmContextRef();
+#endif
+#if BUILDFLAG(ENABLE_PLATFORM_HEVC)
+  } else if (config.codec() == kCodecHEVC &&
+             !base::CommandLine::ForCurrentProcess()->HasSwitch(
+                 switches::kEnableClearHevcForTesting)) {
+    DVLOG(1) << "Clear HEVC content is not supported";
+    SetState(State::kError);
+    std::move(init_cb).Run(StatusCode::kClearContentUnsupported);
+    return;
+#endif
+  }
 
   // Initialize VAAPI wrapper.
   const VideoCodecProfile profile = config.profile();
@@ -250,6 +268,7 @@
   if (!vaapi_wrapper_.get()) {
     VLOGF(1) << "Failed initializing VAAPI for profile "
              << GetProfileName(profile);
+    SetState(State::kError);
     std::move(init_cb).Run(StatusCode::kDecoderUnsupportedProfile);
     return;
   }
@@ -259,6 +278,7 @@
   encryption_scheme_ = config.encryption_scheme();
   auto accel_status = CreateAcceleratedVideoDecoder();
   if (!accel_status.is_ok()) {
+    SetState(State::kError);
     std::move(init_cb).Run(std::move(accel_status));
     return;
   }
@@ -994,7 +1014,7 @@
   // Check whether the state change is valid.
   switch (state) {
     case State::kUninitialized:
-      DCHECK_EQ(state_, State::kWaitingForInput);
+      DCHECK(state_ == State::kWaitingForInput || state_ == State::kError);
       break;
     case State::kWaitingForInput:
       DCHECK(decode_task_queue_.empty());
diff --git a/net/base/features.cc b/net/base/features.cc
index 0fdfedf6..884efbe4 100644
--- a/net/base/features.cc
+++ b/net/base/features.cc
@@ -141,6 +141,8 @@
 #if defined(OS_MAC)
 const base::FeatureParam<int> kCertVerifierBuiltinImpl{
     &kCertVerifierBuiltinFeature, "impl", 0};
+const base::FeatureParam<int> kCertVerifierBuiltinCacheSize{
+    &kCertVerifierBuiltinFeature, "cachesize", 0};
 #endif /* defined(OS_MAC) */
 #endif
 
@@ -152,6 +154,8 @@
 #if defined(OS_MAC)
 const base::FeatureParam<int> kCertDualVerificationTrialImpl{
     &kCertDualVerificationTrialFeature, "impl", 0};
+const base::FeatureParam<int> kCertDualVerificationTrialCacheSize{
+    &kCertDualVerificationTrialFeature, "cachesize", 0};
 #endif /* defined(OS_MAC) */
 #endif
 
diff --git a/net/base/features.h b/net/base/features.h
index 7023f41..fe39446 100644
--- a/net/base/features.h
+++ b/net/base/features.h
@@ -218,6 +218,7 @@
 NET_EXPORT extern const base::Feature kCertVerifierBuiltinFeature;
 #if defined(OS_MAC)
 NET_EXPORT extern const base::FeatureParam<int> kCertVerifierBuiltinImpl;
+NET_EXPORT extern const base::FeatureParam<int> kCertVerifierBuiltinCacheSize;
 #endif /* defined(OS_MAC) */
 #endif /* BUILDFLAG(BUILTIN_CERT_VERIFIER_FEATURE_SUPPORTED) */
 
@@ -225,6 +226,8 @@
 NET_EXPORT extern const base::Feature kCertDualVerificationTrialFeature;
 #if defined(OS_MAC)
 NET_EXPORT extern const base::FeatureParam<int> kCertDualVerificationTrialImpl;
+NET_EXPORT extern const base::FeatureParam<int>
+    kCertDualVerificationTrialCacheSize;
 #endif /* defined(OS_MAC) */
 #endif /* BUILDFLAG(BUILTIN_CERT_VERIFIER_FEATURE_SUPPORTED) */
 
diff --git a/net/cert/internal/system_trust_store.cc b/net/cert/internal/system_trust_store.cc
index 21b3123..326ef5c 100644
--- a/net/cert/internal/system_trust_store.cc
+++ b/net/cert/internal/system_trust_store.cc
@@ -210,9 +210,23 @@
     return kDefaultTrustImpl;
   }
 
+  static size_t GetTrustStoreCacheSize() {
+    if (base::FeatureList::IsEnabled(features::kCertVerifierBuiltinFeature) &&
+        features::kCertVerifierBuiltinCacheSize.Get() > 0) {
+      return features::kCertVerifierBuiltinCacheSize.Get();
+    }
+    if (base::FeatureList::IsEnabled(
+            features::kCertDualVerificationTrialFeature) &&
+        features::kCertDualVerificationTrialCacheSize.Get() > 0) {
+      return features::kCertDualVerificationTrialCacheSize.Get();
+    }
+    constexpr size_t kDefaultCacheSize = 512;
+    return kDefaultCacheSize;
+  }
+
   static TrustStoreMac* GetGlobalTrustStoreMac() {
     static base::NoDestructor<TrustStoreMac> static_trust_store_mac(
-        kSecPolicyAppleSSL, GetTrustStoreImplParam());
+        kSecPolicyAppleSSL, GetTrustStoreImplParam(), GetTrustStoreCacheSize());
     return static_trust_store_mac.get();
   }
 };
diff --git a/net/cert/internal/trust_store_mac.cc b/net/cert/internal/trust_store_mac.cc
index dffc3474..e4c362f5 100644
--- a/net/cert/internal/trust_store_mac.cc
+++ b/net/cert/internal/trust_store_mac.cc
@@ -756,8 +756,9 @@
   DISALLOW_COPY_AND_ASSIGN(TrustImplMRUCache);
 };
 
-TrustStoreMac::TrustStoreMac(CFStringRef policy_oid, TrustImplType impl) {
-  constexpr size_t cache_size = 512;  // TODO(mattm): make this a param.
+TrustStoreMac::TrustStoreMac(CFStringRef policy_oid,
+                             TrustImplType impl,
+                             size_t cache_size) {
   switch (impl) {
     case TrustImplType::kUnknown:
       DCHECK(false);
diff --git a/net/cert/internal/trust_store_mac.h b/net/cert/internal/trust_store_mac.h
index 92a2386..69e4a8e 100644
--- a/net/cert/internal/trust_store_mac.h
+++ b/net/cert/internal/trust_store_mac.h
@@ -105,8 +105,9 @@
   // |policy_oid|. For list of possible policy values, see:
   // https://developer.apple.com/reference/security/1667150-certificate_key_and_trust_servic/1670151-standard_policies_for_specific_c?language=objc
   // |impl| selects which internal implementation is used for checking trust
-  // settings.
-  TrustStoreMac(CFStringRef policy_oid, TrustImplType impl);
+  // settings, and the interpretation of |cache_size| varies depending on
+  // |impl|.
+  TrustStoreMac(CFStringRef policy_oid, TrustImplType impl, size_t cache_size);
   ~TrustStoreMac() override;
 
   // Initializes the trust cache, if it isn't already initialized.
diff --git a/net/cert/internal/trust_store_mac_unittest.cc b/net/cert/internal/trust_store_mac_unittest.cc
index a62b85a..bfe7301 100644
--- a/net/cert/internal/trust_store_mac_unittest.cc
+++ b/net/cert/internal/trust_store_mac_unittest.cc
@@ -30,6 +30,8 @@
 
 namespace {
 
+constexpr size_t kDefaultCacheSize = 512;
+
 // The PEM block header used for DER certificates
 const char kCertificateHeader[] = "CERTIFICATE";
 
@@ -120,7 +122,8 @@
   test_keychain_search_list->AddKeychain(keychain);
 
   TrustStoreMac trust_store(kSecPolicyAppleSSL,
-                            TrustStoreMac::TrustImplType::kDomainCache);
+                            TrustStoreMac::TrustImplType::kDomainCache,
+                            kDefaultCacheSize);
 
   scoped_refptr<ParsedCertificate> a_by_b, b_by_c, b_by_f, c_by_d, c_by_e,
       f_by_e, d_by_d, e_by_e;
@@ -237,7 +240,8 @@
        "/System/Library/Keychains/SystemRootCertificates.keychain"},
       &find_certificate_system_roots_output));
 
-  TrustStoreMac trust_store(kSecPolicyAppleX509Basic, GetParam());
+  TrustStoreMac trust_store(kSecPolicyAppleX509Basic, GetParam(),
+                            kDefaultCacheSize);
 
   base::ScopedCFTypeRef<SecPolicyRef> sec_policy(SecPolicyCreateBasicX509());
   ASSERT_TRUE(sec_policy);
diff --git a/net/spdy/alps_decoder.cc b/net/spdy/alps_decoder.cc
index 24e07973..56d8305 100644
--- a/net/spdy/alps_decoder.cc
+++ b/net/spdy/alps_decoder.cc
@@ -44,6 +44,10 @@
     return Error::kForbiddenFrame;
   }
 
+  if (settings_parser_.settings_ack_received()) {
+    return Error::kSettingsWithAck;
+  }
+
   if (decoder_adapter_.state() !=
       http2::Http2DecoderAdapter::SPDY_READY_FOR_FRAME) {
     return Error::kNotOnFrameBoundary;
@@ -85,6 +89,10 @@
   settings_[id] = value;
 }
 
+void AlpsDecoder::SettingsParser::OnSettingsAck() {
+  settings_ack_received_ = true;
+}
+
 AlpsDecoder::AcceptChParser::AcceptChParser() = default;
 AlpsDecoder::AcceptChParser::~AcceptChParser() = default;
 
diff --git a/net/spdy/alps_decoder.h b/net/spdy/alps_decoder.h
index 4332fd6..fd62d3a 100644
--- a/net/spdy/alps_decoder.h
+++ b/net/spdy/alps_decoder.h
@@ -72,6 +72,7 @@
     ~SettingsParser() override;
 
     bool forbidden_frame_received() const { return forbidden_frame_received_; }
+    bool settings_ack_received() const { return settings_ack_received_; }
     int settings_frame_count() const { return settings_frame_count_; }
     // Number of SETTINGS frames received.
     const spdy::SettingsMap& GetSettings() const { return settings_; }
@@ -83,10 +84,13 @@
                         uint8_t flags) override;
     void OnSettings() override;
     void OnSetting(spdy::SpdySettingsId id, uint32_t value) override;
+    void OnSettingsAck() override;
 
    private:
     // True if a forbidden HTTP/2 frame has been received.
     bool forbidden_frame_received_ = false;
+    // True if a SETTINGS frame with ACK flag has been received.
+    bool settings_ack_received_ = false;
     // Number of SETTINGS frames received.
     int settings_frame_count_ = 0;
     // Accumulated setting parameters.
diff --git a/net/spdy/alps_decoder_test.cc b/net/spdy/alps_decoder_test.cc
index c0ad4af..1c30710 100644
--- a/net/spdy/alps_decoder_test.cc
+++ b/net/spdy/alps_decoder_test.cc
@@ -189,6 +189,17 @@
   EXPECT_EQ(AlpsDecoder::Error::kFramingError, error);
 }
 
+TEST(AlpsDecoderTest, SettingsAck) {
+  AlpsDecoder decoder;
+  AlpsDecoder::Error error =
+      decoder.Decode(HexDecode("000000"       // length
+                               "04"           // type SETTINGS
+                               "01"           // ACK flag
+                               "00000000"));  // stream ID
+
+  EXPECT_EQ(AlpsDecoder::Error::kSettingsWithAck, error);
+}
+
 // According to https://httpwg.org/specs/rfc7540.html#FrameHeader:
 // "Flags that have no defined semantics for a particular frame type MUST be
 // ignored [...]"
diff --git a/remoting/host/BUILD.gn b/remoting/host/BUILD.gn
index b986097..ec797a60 100644
--- a/remoting/host/BUILD.gn
+++ b/remoting/host/BUILD.gn
@@ -26,7 +26,7 @@
     deps += [ ":remoting_host_branded" ]
   }
 
-  if (!is_chromeos_ash && !is_chromeos_lacros && !is_android && !is_ios) {
+  if (!is_chromeos_ash && !is_android && !is_ios) {
     deps += [
       "//remoting/host:remoting_native_messaging_host",
       "//remoting/host:remoting_native_messaging_manifests",
diff --git a/remoting/host/clipboard_x11.cc b/remoting/host/clipboard_x11.cc
index e997fcb..d237d30 100644
--- a/remoting/host/clipboard_x11.cc
+++ b/remoting/host/clipboard_x11.cc
@@ -42,9 +42,9 @@
   // Underlying X11 clipboard implementation.
   XServerClipboard x_server_clipboard_;
 
-  // Connection to the X server, used by |x_server_clipboard_|. This is created
-  // and owned by this class.
-  std::unique_ptr<x11::Connection> connection_;
+  // Connection to the X server, used by |x_server_clipboard_|. This must only
+  // be accessed on the input thread.
+  x11::Connection* connection_;
 
   // Watcher used to handle X11 events from |display_|.
   std::unique_ptr<base::FileDescriptorWatcher::Controller>
@@ -64,19 +64,13 @@
 
 void ClipboardX11::Start(
     std::unique_ptr<protocol::ClipboardStub> client_clipboard) {
-  // TODO(lambroslambrou): Share the X connection with InputInjector.
-  DCHECK(!connection_);
-  connection_ = std::make_unique<x11::Connection>();
-  if (!connection_->Ready()) {
-    LOG(ERROR) << "Couldn't open X display";
-    return;
-  }
+  connection_ = x11::Connection::Get();
   connection_->AddEventObserver(this);
   client_clipboard_.swap(client_clipboard);
 
   x_server_clipboard_.Init(
-      connection_.get(), base::BindRepeating(&ClipboardX11::OnClipboardChanged,
-                                             base::Unretained(this)));
+      connection_, base::BindRepeating(&ClipboardX11::OnClipboardChanged,
+                                       base::Unretained(this)));
 
   x_connection_watch_controller_ = base::FileDescriptorWatcher::WatchReadable(
       connection_->GetFd(),
diff --git a/remoting/host/input_injector_x11.cc b/remoting/host/input_injector_x11.cc
index 9524616d..d6d7e691 100644
--- a/remoting/host/input_injector_x11.cc
+++ b/remoting/host/input_injector_x11.cc
@@ -89,7 +89,7 @@
       scoped_refptr<base::SingleThreadTaskRunner> task_runner);
   ~InputInjectorX11() override;
 
-  bool Init();
+  void Init();
 
   // Clipboard stub interface.
   void InjectClipboardEvent(const ClipboardEvent& event) override;
@@ -110,7 +110,7 @@
    public:
     explicit Core(scoped_refptr<base::SingleThreadTaskRunner> task_runner);
 
-    bool Init();
+    void Init();
 
     // Mirrors the ClipboardStub interface.
     void InjectClipboardEvent(const ClipboardEvent& event);
@@ -167,8 +167,8 @@
     // "tick" being injected.
     ScrollDirection latest_tick_y_direction_ = ScrollDirection::NONE;
 
-    // X11 graphics context.
-    x11::Connection connection_;
+    // X11 graphics context. Must only be accessed on the input thread.
+    x11::Connection* connection_;
 
     // Number of buttons we support.
     // Left, Right, Middle, VScroll Up/Down, HScroll Left/Right, back, forward.
@@ -203,8 +203,8 @@
   core_->Stop();
 }
 
-bool InputInjectorX11::Init() {
-  return core_->Init();
+void InputInjectorX11::Init() {
+  core_->Init();
 }
 
 void InputInjectorX11::InjectClipboardEvent(const ClipboardEvent& event) {
@@ -236,19 +236,17 @@
     scoped_refptr<base::SingleThreadTaskRunner> task_runner)
     : task_runner_(task_runner) {}
 
-bool InputInjectorX11::Core::Init() {
-  CHECK(connection_.Ready());
-
-  if (!task_runner_->BelongsToCurrentThread())
-    task_runner_->PostTask(FROM_HERE,
-                           base::BindOnce(&Core::InitClipboard, this));
-
-  if (!IgnoreXServerGrabs(&connection_, true)) {
-    LOG(ERROR) << "Server does not support XTest.";
-    return false;
+void InputInjectorX11::Core::Init() {
+  if (!task_runner_->BelongsToCurrentThread()) {
+    task_runner_->PostTask(FROM_HERE, base::BindOnce(&Core::Init, this));
+    return;
   }
-  InitMouseButtonMap();
-  return true;
+
+  connection_ = x11::Connection::Get();
+  if (!IgnoreXServerGrabs(connection_, true)) {
+    LOG(ERROR) << "XTEST not supported, cannot inject key/mouse events.";
+  }
+  InitClipboard();
 }
 
 void InputInjectorX11::Core::InjectClipboardEvent(const ClipboardEvent& event) {
@@ -273,6 +271,10 @@
     return;
   }
 
+  if (!connection_->xtest().present()) {
+    return;
+  }
+
   int keycode =
       ui::KeycodeConverter::UsbKeycodeToNativeKeycode(event.usb_keycode());
 
@@ -290,7 +292,7 @@
         return;
       // Key is already held down, so lift the key up to ensure this repeated
       // press takes effect.
-      connection_.xtest().FakeInput({x11::KeyEvent::Release, keycode});
+      connection_->xtest().FakeInput({x11::KeyEvent::Release, keycode});
     }
 
     if (!IsLockKey(static_cast<x11::KeyCode>(keycode))) {
@@ -328,8 +330,8 @@
   }
 
   auto opcode = event.pressed() ? x11::KeyEvent::Press : x11::KeyEvent::Release;
-  connection_.xtest().FakeInput({opcode, keycode});
-  connection_.Flush();
+  connection_->xtest().FakeInput({opcode, keycode});
+  connection_->Flush();
 }
 
 void InputInjectorX11::Core::InjectTextEvent(const TextEvent& event) {
@@ -339,11 +341,15 @@
     return;
   }
 
+  if (!connection_->xtest().present()) {
+    return;
+  }
+
   // Release all keys before injecting text event. This is necessary to avoid
   // any interference with the currently pressed keys. E.g. if Shift is pressed
   // when TextEvent is received.
   for (int key : pressed_keys_)
-    connection_.xtest().FakeInput({x11::KeyEvent::Release, key});
+    connection_->xtest().FakeInput({x11::KeyEvent::Release, key});
   pressed_keys_.clear();
 
   const std::string text = event.text();
@@ -367,26 +373,26 @@
 }
 
 bool InputInjectorX11::Core::IsAutoRepeatEnabled() {
-  if (auto reply = connection_.GetKeyboardControl().Sync())
+  if (auto reply = connection_->GetKeyboardControl().Sync())
     return reply->global_auto_repeat == x11::AutoRepeatMode::On;
   LOG(ERROR) << "Failed to get keyboard auto-repeat status, assuming ON.";
   return true;
 }
 
 void InputInjectorX11::Core::SetAutoRepeatEnabled(bool mode) {
-  connection_.ChangeKeyboardControl(
+  connection_->ChangeKeyboardControl(
       {.auto_repeat_mode =
            mode ? x11::AutoRepeatMode::On : x11::AutoRepeatMode::Off});
-  connection_.Flush();
+  connection_->Flush();
 }
 
 bool InputInjectorX11::Core::IsLockKey(x11::KeyCode keycode) {
-  auto state = connection_.xkb().GetState().Sync();
+  auto state = connection_->xkb().GetState().Sync();
   if (!state)
     return false;
   auto mods = state->baseMods | state->latchedMods | state->lockedMods;
   auto keysym =
-      connection_.KeycodeToKeysym(keycode, static_cast<unsigned>(mods));
+      connection_->KeycodeToKeysym(keycode, static_cast<unsigned>(mods));
   if (state && keysym)
     return keysym == XK_Caps_Lock || keysym == XK_Num_Lock;
   else
@@ -417,7 +423,7 @@
   }
 
   if (update_mask) {
-    connection_.xkb().LatchLockState(
+    connection_->xkb().LatchLockState(
         {static_cast<x11::Xkb::DeviceSpec>(x11::Xkb::Id::UseCoreKbd),
          static_cast<x11::ModMask>(update_mask),
          static_cast<x11::ModMask>(lock_values)});
@@ -425,14 +431,20 @@
 }
 
 void InputInjectorX11::Core::InjectScrollWheelClicks(int button, int count) {
+  DCHECK(task_runner_->BelongsToCurrentThread());
+
+  if (!connection_->xtest().present()) {
+    return;
+  }
+
   if (button < 0) {
     LOG(WARNING) << "Ignoring unmapped scroll wheel button";
     return;
   }
   for (int i = 0; i < count; i++) {
     // Generate a button-down and a button-up to simulate a wheel click.
-    connection_.xtest().FakeInput({x11::ButtonEvent::Press, button});
-    connection_.xtest().FakeInput({x11::ButtonEvent::Release, button});
+    connection_->xtest().FakeInput({x11::ButtonEvent::Press, button});
+    connection_->xtest().FakeInput({x11::ButtonEvent::Release, button});
   }
 }
 
@@ -443,11 +455,15 @@
     return;
   }
 
+  if (!connection_->xtest().present()) {
+    return;
+  }
+
   if (event.has_delta_x() && event.has_delta_y() &&
       (event.delta_x() != 0 || event.delta_y() != 0)) {
     latest_mouse_position_.set(-1, -1);
     VLOG(3) << "Moving mouse by " << event.delta_x() << "," << event.delta_y();
-    connection_.xtest().FakeInput({
+    connection_->xtest().FakeInput({
         .type = x11::MotionNotifyEvent::opcode,
         .detail = true,
         .rootX = event.delta_x(),
@@ -478,10 +494,10 @@
 
       VLOG(3) << "Moving mouse to " << latest_mouse_position_.x() << ","
               << latest_mouse_position_.y();
-      connection_.xtest().FakeInput({
+      connection_->xtest().FakeInput({
           .type = x11::MotionNotifyEvent::opcode,
           .detail = false,
-          .root = connection_.default_root(),
+          .root = connection_->default_root(),
           .rootX = latest_mouse_position_.x(),
           .rootY = latest_mouse_position_.y(),
       });
@@ -500,7 +516,7 @@
             << (event.button_down() ? "down " : "up ") << button_number;
     auto opcode = event.button_down() ? x11::ButtonEvent::Press
                                       : x11::ButtonEvent::Release;
-    connection_.xtest().FakeInput({opcode, button_number});
+    connection_->xtest().FakeInput({opcode, button_number});
   }
 
   // remotedesktop.google.com currently sends scroll events in pixels, which
@@ -569,10 +585,12 @@
                             abs(ticks_x));
   }
 
-  connection_.Flush();
+  connection_->Flush();
 }
 
 void InputInjectorX11::Core::InitMouseButtonMap() {
+  DCHECK(task_runner_->BelongsToCurrentThread());
+
   // TODO(rmsousa): Run this on global/device mapping change events.
 
   // Do not touch global pointer mapping, since this may affect the local user.
@@ -580,7 +598,7 @@
   // Note that if a user has a global mapping that completely disables a button
   // (by assigning 0 to it), we won't be able to inject it.
   std::vector<uint8_t> pointer_mapping;
-  if (auto reply = connection_.GetPointerMapping().Sync())
+  if (auto reply = connection_->GetPointerMapping().Sync())
     pointer_mapping = std::move(reply->map);
   for (int& i : pointer_button_map_)
     i = -1;
@@ -594,7 +612,7 @@
       LOG(ERROR) << "Global pointer mapping does not support button " << i + 1;
   }
 
-  if (!connection_.QueryExtension("XInputExtension").Sync()) {
+  if (!connection_->QueryExtension("XInputExtension").Sync()) {
     // If XInput is not available, we're done. But it would be very unusual to
     // have a server that supports XTest but not XInput, so log it as an error.
     LOG(ERROR) << "X Input extension not available";
@@ -607,7 +625,7 @@
   // may have mistakenly applied left-handed preferences to the XTEST device.
   uint8_t device_id = 0;
   bool device_found = false;
-  if (auto devices = connection_.xinput().ListInputDevices().Sync()) {
+  if (auto devices = connection_->xinput().ListInputDevices().Sync()) {
     for (size_t i = 0; i < devices->devices.size(); i++) {
       const auto& device_info = devices->devices[i];
       const std::string& name = devices->names[i].name;
@@ -626,27 +644,27 @@
     return;
   }
 
-  auto device = connection_.xinput().OpenDevice({device_id}).Sync();
+  auto device = connection_->xinput().OpenDevice({device_id}).Sync();
   if (!device) {
     LOG(ERROR) << "Cannot open XTest device.";
     return;
   }
 
   if (auto mapping =
-          connection_.xinput().GetDeviceButtonMapping({device_id}).Sync()) {
+          connection_->xinput().GetDeviceButtonMapping({device_id}).Sync()) {
     size_t num_device_buttons = mapping->map.size();
     std::vector<uint8_t> new_mapping;
     for (size_t i = 0; i < num_device_buttons; i++)
       new_mapping.push_back(i + 1);
-    if (!connection_.xinput()
+    if (!connection_->xinput()
              .SetDeviceButtonMapping({device_id, new_mapping})
              .Sync()) {
       LOG(ERROR) << "Failed to set XTest device button mapping";
     }
   }
 
-  connection_.xinput().CloseDevice({device_id});
-  connection_.Flush();
+  connection_->xinput().CloseDevice({device_id});
+  connection_->Flush();
 }
 
 int InputInjectorX11::Core::MouseButtonToX11ButtonNumber(
@@ -692,7 +710,7 @@
   clipboard_->Start(std::move(client_clipboard));
 
   character_injector_ = std::make_unique<X11CharacterInjector>(
-      std::make_unique<X11KeyboardImpl>(&connection_));
+      std::make_unique<X11KeyboardImpl>(connection_));
 
   // Disable auto-repeat, if necessary, to avoid triggering auto-repeat
   // if network congestion delays the key-up event from the client. This is
@@ -722,10 +740,8 @@
 std::unique_ptr<InputInjector> InputInjector::Create(
     scoped_refptr<base::SingleThreadTaskRunner> main_task_runner,
     scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner) {
-  std::unique_ptr<InputInjectorX11> injector(
-      new InputInjectorX11(main_task_runner));
-  if (!injector->Init())
-    return nullptr;
+  auto injector = std::make_unique<InputInjectorX11>(main_task_runner);
+  injector->Init();
   return std::move(injector);
 }
 
diff --git a/remoting/host/it2me/BUILD.gn b/remoting/host/it2me/BUILD.gn
index 1a82d84..f252c618 100644
--- a/remoting/host/it2me/BUILD.gn
+++ b/remoting/host/it2me/BUILD.gn
@@ -72,32 +72,20 @@
   }
 }
 
-if (is_chromeos_ash || is_chromeos_lacros) {
+if (is_chromeos_ash) {
   source_set("chrome_os_host") {
     sources = [
-      "it2me_native_messaging_host_allowed_origins.cc",
-      "it2me_native_messaging_host_allowed_origins.h",
+      "it2me_native_messaging_host_chromeos.cc",
+      "it2me_native_messaging_host_chromeos.h",
     ]
 
-    if (is_chromeos_lacros) {
-      sources += [
-        "it2me_native_messaging_host_lacros.cc",
-        "it2me_native_messaging_host_lacros.h",
-      ]
-    } else {
-      sources += [
-        "it2me_native_messaging_host_chromeos.cc",
-        "it2me_native_messaging_host_chromeos.h",
-      ]
+    deps = [
+      ":common",
+      "//skia",
+    ]
 
-      deps = [
-        ":common",
-        "//skia",
-      ]
-
-      if (use_ozone) {
-        deps += [ "//ui/ozone" ]
-      }
+    if (use_ozone) {
+      deps += [ "//ui/ozone" ]
     }
   }
 }
diff --git a/remoting/host/it2me/it2me_native_messaging_host_allowed_origins.cc b/remoting/host/it2me/it2me_native_messaging_host_allowed_origins.cc
deleted file mode 100644
index aa33552f8..0000000
--- a/remoting/host/it2me/it2me_native_messaging_host_allowed_origins.cc
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright 2021 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "remoting/host/it2me/it2me_native_messaging_host_allowed_origins.h"
-
-#include "base/stl_util.h"
-
-namespace remoting {
-
-// If you modify the list of allowed_origins, don't forget to update
-// remoting/host/it2me/com.google.chrome.remote_assistance.json.jinja2
-// to keep the two lists in sync.
-const char* const kIt2MeOrigins[] = {
-    "chrome-extension://inomeogfingihgjfjlpeplalcfajhgai/",
-    "chrome-extension://hpodccmdligbeohchckkeajbfohibipg/"};
-
-const size_t kIt2MeOriginsSize = base::size(kIt2MeOrigins);
-
-const char kIt2MeNativeMessageHostName[] =
-    "com.google.chrome.remote_assistance";
-
-}  // namespace remoting
diff --git a/remoting/host/it2me/it2me_native_messaging_host_allowed_origins.h b/remoting/host/it2me/it2me_native_messaging_host_allowed_origins.h
deleted file mode 100644
index 6d02878..0000000
--- a/remoting/host/it2me/it2me_native_messaging_host_allowed_origins.h
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright 2021 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-#ifndef REMOTING_HOST_IT2ME_IT2ME_NATIVE_MESSAGING_HOST_ALLOWED_ORIGINS_H_
-#define REMOTING_HOST_IT2ME_IT2ME_NATIVE_MESSAGING_HOST_ALLOWED_ORIGINS_H_
-
-#include <stddef.h>
-
-namespace remoting {
-
-// The set of origins which are allowed to instantiate an It2Me host.
-extern const char* const kIt2MeOrigins[];
-
-// The number of entries defined in |kIt2MeOrigins|.
-extern const size_t kIt2MeOriginsSize;
-
-// The name used to register the It2Me native message host.
-extern const char kIt2MeNativeMessageHostName[];
-
-}  // namespace remoting
-
-#endif  // REMOTING_HOST_IT2ME_IT2ME_NATIVE_MESSAGING_HOST_ALLOWED_ORIGINS_H_
diff --git a/remoting/host/it2me/it2me_native_messaging_host_chromeos.cc b/remoting/host/it2me/it2me_native_messaging_host_chromeos.cc
index 0316368..6f261b0 100644
--- a/remoting/host/it2me/it2me_native_messaging_host_chromeos.cc
+++ b/remoting/host/it2me/it2me_native_messaging_host_chromeos.cc
@@ -7,7 +7,6 @@
 #include <memory>
 
 #include "base/lazy_instance.h"
-#include "base/stl_util.h"
 #include "base/task/post_task.h"
 #include "base/task/task_traits.h"
 #include "base/task/thread_pool.h"
@@ -38,13 +37,4 @@
   return host;
 }
 
-// If you modify the list of allowed_origins, don't forget to update
-// remoting/host/it2me/com.google.chrome.remote_assistance.json.jinja2
-// to keep the two lists in sync.
-const char* const kRemotingIt2MeOrigins[] = {
-    "chrome-extension://inomeogfingihgjfjlpeplalcfajhgai/",
-    "chrome-extension://hpodccmdligbeohchckkeajbfohibipg/"};
-
-const size_t kRemotingIt2MeOriginsCount = base::size(kRemotingIt2MeOrigins);
-
 }  // namespace remoting
diff --git a/remoting/host/it2me/it2me_native_messaging_host_chromeos.h b/remoting/host/it2me/it2me_native_messaging_host_chromeos.h
index 63b88cc..c371e43 100644
--- a/remoting/host/it2me/it2me_native_messaging_host_chromeos.h
+++ b/remoting/host/it2me/it2me_native_messaging_host_chromeos.h
@@ -22,18 +22,13 @@
 
 // Creates native messaging host on ChromeOS. Must be called on the UI thread
 // of the browser process.
+
 std::unique_ptr<extensions::NativeMessageHost>
 CreateIt2MeNativeMessagingHostForChromeOS(
     scoped_refptr<base::SingleThreadTaskRunner> io_runnner,
     scoped_refptr<base::SingleThreadTaskRunner> ui_runnner,
     policy::PolicyService* policy_service);
 
-// The set of origins which are allowed to instantiate an It2Me host.
-extern const char* const kRemotingIt2MeOrigins[];
-
-// The number of entries defined in |kRemotingIt2MeOrigins|.
-extern const size_t kRemotingIt2MeOriginsCount;
-
 }  // namespace remoting
 
 #endif  // REMOTING_HOST_IT2ME_IT2ME_NATIVE_MESSAGING_HOST_CHROMEOS_H_
diff --git a/remoting/host/it2me/it2me_native_messaging_host_lacros.cc b/remoting/host/it2me/it2me_native_messaging_host_lacros.cc
deleted file mode 100644
index a242356..0000000
--- a/remoting/host/it2me/it2me_native_messaging_host_lacros.cc
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright 2021 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "remoting/host/it2me/it2me_native_messaging_host_lacros.h"
-
-#include <memory>
-
-#include "base/notreached.h"
-
-namespace remoting {
-
-std::unique_ptr<extensions::NativeMessageHost>
-CreateIt2MeNativeMessagingHostForLacros(
-    scoped_refptr<base::SingleThreadTaskRunner> io_runnner,
-    scoped_refptr<base::SingleThreadTaskRunner> ui_runnner) {
-  // TODO(joedow): Implement a remote support host for LaCrOS.
-  NOTIMPLEMENTED();
-  return nullptr;
-}
-
-}  // namespace remoting
diff --git a/remoting/host/it2me/it2me_native_messaging_host_lacros.h b/remoting/host/it2me/it2me_native_messaging_host_lacros.h
deleted file mode 100644
index 93feeeb..0000000
--- a/remoting/host/it2me/it2me_native_messaging_host_lacros.h
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright 2021 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-#ifndef REMOTING_HOST_IT2ME_IT2ME_NATIVE_MESSAGING_HOST_LACROS_H_
-#define REMOTING_HOST_IT2ME_IT2ME_NATIVE_MESSAGING_HOST_LACROS_H_
-
-#include <memory>
-
-#include "base/memory/scoped_refptr.h"
-#include "extensions/browser/api/messaging/native_message_host.h"
-
-namespace base {
-class SingleThreadTaskRunner;
-}  // namespace base
-
-namespace remoting {
-
-// Creates native messaging host on Lacros. Must be called on the UI thread of
-// the browser process.
-
-std::unique_ptr<extensions::NativeMessageHost>
-CreateIt2MeNativeMessagingHostForLacros(
-    scoped_refptr<base::SingleThreadTaskRunner> io_runnner,
-    scoped_refptr<base::SingleThreadTaskRunner> ui_runnner);
-
-}  // namespace remoting
-
-#endif  // REMOTING_HOST_IT2ME_IT2ME_NATIVE_MESSAGING_HOST_LACROS_H_
diff --git a/services/media_session/public/cpp/test/mock_media_session.cc b/services/media_session/public/cpp/test/mock_media_session.cc
index 572cf447..980f146 100644
--- a/services/media_session/public/cpp/test/mock_media_session.cc
+++ b/services/media_session/public/cpp/test/mock_media_session.cc
@@ -70,6 +70,8 @@
   } else {
     if (wanted_state_ == session_info_->state ||
         session_info_->playback_state == wanted_playback_state_ ||
+        session_info_->microphone_state == wanted_microphone_state_ ||
+        session_info_->camera_state == wanted_camera_state_ ||
         (wanted_audio_video_states_ &&
          base::ranges::is_permutation(*session_info_->audio_video_states,
                                       *wanted_audio_video_states_))) {
@@ -156,6 +158,26 @@
   StartWaiting();
 }
 
+void MockMediaSessionMojoObserver::WaitForMicrophoneState(
+    mojom::MicrophoneState wanted_state) {
+  if (session_info_ && session_info_->microphone_state == wanted_state)
+    return;
+
+  wanted_microphone_state_ = wanted_state;
+  StartWaiting();
+  wanted_microphone_state_.reset();
+}
+
+void MockMediaSessionMojoObserver::WaitForCameraState(
+    mojom::CameraState wanted_state) {
+  if (session_info_ && session_info_->camera_state == wanted_state)
+    return;
+
+  wanted_camera_state_ = wanted_state;
+  StartWaiting();
+  wanted_camera_state_.reset();
+}
+
 void MockMediaSessionMojoObserver::WaitForAudioVideoStates(
     const std::vector<mojom::MediaAudioVideoState>& wanted_states) {
   if (session_info_ && base::ranges::is_permutation(
diff --git a/services/media_session/public/cpp/test/mock_media_session.h b/services/media_session/public/cpp/test/mock_media_session.h
index a81e339e..3af3ec6 100644
--- a/services/media_session/public/cpp/test/mock_media_session.h
+++ b/services/media_session/public/cpp/test/mock_media_session.h
@@ -49,6 +49,8 @@
 
   void WaitForState(mojom::MediaSessionInfo::SessionState wanted_state);
   void WaitForPlaybackState(mojom::MediaPlaybackState wanted_state);
+  void WaitForMicrophoneState(mojom::MicrophoneState wanted_state);
+  void WaitForCameraState(mojom::CameraState wanted_state);
 
   // Blocks until the set of audio/video states for the players in the media
   // session matches |wanted_states|. The order is not important.
@@ -120,6 +122,8 @@
 
   base::Optional<mojom::MediaSessionInfo::SessionState> wanted_state_;
   base::Optional<mojom::MediaPlaybackState> wanted_playback_state_;
+  base::Optional<mojom::MicrophoneState> wanted_microphone_state_;
+  base::Optional<mojom::CameraState> wanted_camera_state_;
   base::Optional<std::vector<mojom::MediaAudioVideoState>>
       wanted_audio_video_states_;
   std::unique_ptr<base::RunLoop> run_loop_;
diff --git a/services/network/url_loader.cc b/services/network/url_loader.cc
index 6bcaca9..436ee8ea 100644
--- a/services/network/url_loader.cc
+++ b/services/network/url_loader.cc
@@ -495,17 +495,14 @@
                                   mojo::SimpleWatcher::ArmingPolicy::MANUAL,
                                   base::SequencedTaskRunnerHandle::Get()),
       want_raw_headers_(request.report_raw_headers),
-      report_raw_headers_(false),
       devtools_request_id_(request.devtools_request_id),
-      has_user_activation_(false),
+      request_mode_(request.mode),
       request_destination_(request.destination),
       resource_scheduler_client_(std::move(resource_scheduler_client)),
       keepalive_statistics_recorder_(std::move(keepalive_statistics_recorder)),
-      first_auth_attempt_(true),
       custom_proxy_pre_cache_headers_(request.custom_proxy_pre_cache_headers),
       custom_proxy_post_cache_headers_(request.custom_proxy_post_cache_headers),
       fetch_window_id_(request.fetch_window_id),
-      origin_policy_manager_(nullptr),
       trust_token_helper_factory_(std::move(trust_token_helper_factory)),
       origin_access_list_(origin_access_list),
       cookie_observer_(std::move(cookie_observer)),
@@ -591,8 +588,6 @@
   url_request_->SetUserData(kUserDataKey,
                             std::make_unique<UnownedPointer>(this));
 
-  request_mode_ = request.mode;
-
   if (request.trusted_params) {
     has_user_activation_ = request.trusted_params->has_user_activation;
 
diff --git a/services/network/url_loader.h b/services/network/url_loader.h
index e8f8e720..89d3ffe 100644
--- a/services/network/url_loader.h
+++ b/services/network/url_loader.h
@@ -402,9 +402,9 @@
   DeleteCallback delete_callback_;
 
   int32_t options_;
-  bool corb_detachable_;
-  int resource_type_;
-  bool is_load_timing_enabled_;
+  const bool corb_detachable_;
+  const int resource_type_;
+  const bool is_load_timing_enabled_;
   bool has_received_response_ = false;
 
   // URLLoaderFactory is guaranteed to outlive URLLoader, so it is safe to
@@ -413,7 +413,7 @@
   // This also belongs to URLLoaderFactory and outlives this loader.
   mojom::CrossOriginEmbedderPolicyReporter* const coep_reporter_;
 
-  uint32_t request_id_;
+  const uint32_t request_id_;
   const int keepalive_request_size_;
   const bool keepalive_;
   const bool do_not_prompt_for_login_;
@@ -455,7 +455,7 @@
   // Whether client requested raw headers.
   const bool want_raw_headers_;
   // Whether we actually should report them.
-  bool report_raw_headers_;
+  bool report_raw_headers_ = false;
   net::HttpRawRequestHeaders raw_request_headers_;
   scoped_refptr<const net::HttpResponseHeaders> raw_response_headers_;
 
@@ -492,9 +492,9 @@
   // encoded body size was reported to the client.
   int64_t reported_total_encoded_bytes_ = 0;
 
-  mojom::RequestMode request_mode_;
+  const mojom::RequestMode request_mode_;
 
-  bool has_user_activation_;
+  bool has_user_activation_ = false;
 
   mojom::RequestDestination request_destination_ =
       mojom::RequestDestination::kEmpty;
@@ -503,7 +503,7 @@
 
   base::WeakPtr<KeepaliveStatisticsRecorder> keepalive_statistics_recorder_;
 
-  bool first_auth_attempt_;
+  bool first_auth_attempt_ = true;
 
   std::unique_ptr<ScopedThrottlingToken> throttling_token_;
 
@@ -519,7 +519,7 @@
   std::unique_ptr<FileOpenerForUpload> file_opener_for_upload_;
 
   // Will only be set for requests that have |obey_origin_policy| set.
-  mojom::OriginPolicyManager* origin_policy_manager_;
+  mojom::OriginPolicyManager* origin_policy_manager_ = nullptr;
 
   // If the request is configured for Trust Tokens
   // (https://github.com/WICG/trust-token-api) protocol operations, annotates
diff --git a/testing/buildbot/chromium.perf.calibration.json b/testing/buildbot/chromium.perf.calibration.json
index 8515823d9..6c764861 100644
--- a/testing/buildbot/chromium.perf.calibration.json
+++ b/testing/buildbot/chromium.perf.calibration.json
@@ -39,7 +39,8 @@
         "trigger_script": {
           "args": [
             "--multiple-dimension-script-verbose",
-            "True"
+            "True",
+            "--use-dynamic-shards"
           ],
           "requires_simultaneous_shard_dispatch": true,
           "script": "//testing/trigger_scripts/perf_device_trigger.py"
diff --git a/testing/buildbot/chromium.perf.fyi.json b/testing/buildbot/chromium.perf.fyi.json
index 373033fc..34b5a961 100644
--- a/testing/buildbot/chromium.perf.fyi.json
+++ b/testing/buildbot/chromium.perf.fyi.json
@@ -235,22 +235,23 @@
     "isolated_scripts": [
       {
         "args": [
-          "hardware_accelerated_feature",
-          "--show-stdout",
-          "--browser=web-engine-shell",
-          "--passthrough",
           "-v",
-          "--extra-browser-args=--enable-logging=stderr --js-flags=--expose-gc",
+          "--browser=web-engine-shell",
+          "--upload-results",
+          "--test-shard-map-filename=fuchsia-perf-fyi_map.json",
+          "--output-format=histograms",
+          "--experimental-tbmv3-metrics",
           "--device=custom",
-          "--custom-device-target=internal.astro_target"
+          "--custom-device-target=internal.astro_target",
+          "--story-filter=load:chrome:blank"
         ],
-        "isolate_name": "fuchsia_telemetry_gpu_integration_test",
+        "isolate_name": "performance_web_engine_test_suite",
         "merge": {
           "script": "//tools/perf/process_perf_results.py"
         },
-        "name": "fuchsia_telemetry_gpu_integration_test",
+        "name": "performance_web_engine_test_suite",
         "override_compile_targets": [
-          "fuchsia_telemetry_gpu_integration_test"
+          "performance_web_engine_test_suite"
         ],
         "swarming": {
           "can_use_on_swarming_builders": true,
@@ -266,7 +267,8 @@
           "hard_timeout": 21600,
           "ignore_task_failure": false,
           "io_timeout": 21600,
-          "service_account": "chrome-tester@chops-service-accounts.iam.gserviceaccount.com"
+          "service_account": "chrome-tester@chops-service-accounts.iam.gserviceaccount.com",
+          "shards": 1
         },
         "trigger_script": {
           "args": [
diff --git a/testing/buildbot/chromium.perf.json b/testing/buildbot/chromium.perf.json
index 90dcd3ab..b84194e 100644
--- a/testing/buildbot/chromium.perf.json
+++ b/testing/buildbot/chromium.perf.json
@@ -1118,6 +1118,7 @@
           "can_use_on_swarming_builders": true,
           "dimension_sets": [
             {
+              "device_status": "available",
               "device_type": "eve",
               "gpu": null,
               "os": "ChromeOS",
diff --git a/testing/buildbot/filters/android.emulator_p.chrome_public_test_apk.filter b/testing/buildbot/filters/android.emulator_p.chrome_public_test_apk.filter
index 955151e..2febd24c 100644
--- a/testing/buildbot/filters/android.emulator_p.chrome_public_test_apk.filter
+++ b/testing/buildbot/filters/android.emulator_p.chrome_public_test_apk.filter
@@ -124,3 +124,6 @@
 
 # crbug.com/1163913
 -org.chromium.chrome.browser.autofill_assistant.AutofillAssistantTriggerScriptIntegrationTest.dontShowOnboardingIfAcceptedInDifferentTab
+
+# crbug.com/1187536
+-org.chromium.chrome.browser.customtabs.CustomTabExternalNavigationTest.testIntentPickerNotShownForNormalUrl
diff --git a/testing/buildbot/filters/android.pie_arm64_rel.chrome_public_test_apk.filter b/testing/buildbot/filters/android.pie_arm64_rel.chrome_public_test_apk.filter
index 0e57593d..5aeeee8 100644
--- a/testing/buildbot/filters/android.pie_arm64_rel.chrome_public_test_apk.filter
+++ b/testing/buildbot/filters/android.pie_arm64_rel.chrome_public_test_apk.filter
@@ -93,3 +93,5 @@
 # crbug.com/1169250
 -org.chromium.chrome.browser.omnibox.LocationBarTest.testFocusLogic_keyboardVisibility
 
+# crbug.com/1183540: Re-enable once the tests do not cause timeout.
+-org.chromium.chrome.browser.contextualsearch.ContextualSearchManagerTest.*
diff --git a/testing/buildbot/gn_isolate_map.pyl b/testing/buildbot/gn_isolate_map.pyl
index 96946b77..4d0980f3 100644
--- a/testing/buildbot/gn_isolate_map.pyl
+++ b/testing/buildbot/gn_isolate_map.pyl
@@ -1449,6 +1449,14 @@
     "label": "//chrome/test:performance_test_suite_eve",
     "type": "generated_script",
   },
+  "performance_web_engine_test_suite": {
+    "args": [
+      "../../content/test/run_gpu_integration_test_fuchsia.py",
+    ],
+    "label": "//content/test:performance_web_engine_test_suite",
+    "script": "//testing/scripts/run_performance_tests.py",
+    "type": "script",
+  },
   "performance_webview_test_suite": {
     "args": [
       "remove",
diff --git a/testing/trigger_scripts/perf_device_trigger.py b/testing/trigger_scripts/perf_device_trigger.py
index d09bbe27..46043f4 100755
--- a/testing/trigger_scripts/perf_device_trigger.py
+++ b/testing/trigger_scripts/perf_device_trigger.py
@@ -165,8 +165,9 @@
     selected_config.sort()
 
     if verbose:
-      for shard_index, bot_id in selected_config:
-        print('Shard %d\n\tBot: %s' % (shard_index, bot_id))
+      for shard_index, bot_index in selected_config:
+        print('Shard %d\n\tBot: %s' %
+              (shard_index, self._bot_configs[bot_index]['id']))
 
     return selected_config
 
diff --git a/third_party/blink/common/unique_name/unique_name_helper_unittest.cc b/third_party/blink/common/unique_name/unique_name_helper_unittest.cc
index 0b8d4ac20..1afb041e 100644
--- a/third_party/blink/common/unique_name/unique_name_helper_unittest.cc
+++ b/third_party/blink/common/unique_name/unique_name_helper_unittest.cc
@@ -10,7 +10,6 @@
 
 #include "base/auto_reset.h"
 #include "base/optional.h"
-#include "base/strings/nullable_string16.h"
 #include "base/strings/string_piece.h"
 #include "base/strings/utf_string_conversions.h"
 #include "testing/gtest/include/gtest/gtest.h"
diff --git a/third_party/blink/public/mojom/web_feature/web_feature.mojom b/third_party/blink/public/mojom/web_feature/web_feature.mojom
index e4048506..de4becd 100644
--- a/third_party/blink/public/mojom/web_feature/web_feature.mojom
+++ b/third_party/blink/public/mojom/web_feature/web_feature.mojom
@@ -3160,6 +3160,8 @@
   kPermissionsPolicyHeader = 3850,
   kWebAppManifestUrlHandlers = 3851,
   kLaxAllowingUnsafeCookies = 3852,
+  kV8MediaSession_SetMicrophoneActive_Method = 3853,
+  kV8MediaSession_SetCameraActive_Method = 3854,
 
   // Add new features immediately above this line. Don't change assigned
   // numbers of any item, and don't reuse removed slots.
diff --git a/third_party/blink/public/platform/web_runtime_features.h b/third_party/blink/public/platform/web_runtime_features.h
index bf2f6b3..d0ad622 100644
--- a/third_party/blink/public/platform/web_runtime_features.h
+++ b/third_party/blink/public/platform/web_runtime_features.h
@@ -124,6 +124,7 @@
   BLINK_PLATFORM_EXPORT static void EnableLazyImageVisibleLoadTimeMetrics(bool);
   BLINK_PLATFORM_EXPORT static void EnableMediaFeeds(bool);
   BLINK_PLATFORM_EXPORT static void EnableMediaSession(bool);
+  BLINK_PLATFORM_EXPORT static void EnableMediaSessionWebRTC(bool);
   BLINK_PLATFORM_EXPORT static void EnableNetInfoDownlinkMax(bool);
   BLINK_PLATFORM_EXPORT static void EnableNeverSlowMode(bool);
   BLINK_PLATFORM_EXPORT static void EnableNotificationContentImage(bool);
diff --git a/third_party/blink/public/web/web_local_frame_client.h b/third_party/blink/public/web/web_local_frame_client.h
index a51b59c..883f833 100644
--- a/third_party/blink/public/web/web_local_frame_client.h
+++ b/third_party/blink/public/web/web_local_frame_client.h
@@ -364,7 +364,6 @@
   virtual void DidCommitNavigation(
       WebHistoryCommitType commit_type,
       bool should_reset_browser_interface_broker,
-      network::mojom::WebSandboxFlags sandbox_flags,
       const ParsedPermissionsPolicy& permissions_policy_header,
       const DocumentPolicyFeatureState& document_policy_header) {}
 
diff --git a/third_party/blink/renderer/bindings/core/v8/serialization/serialization_tag.h b/third_party/blink/renderer/bindings/core/v8/serialization/serialization_tag.h
index 1eb157a3..f105535 100644
--- a/third_party/blink/renderer/bindings/core/v8/serialization/serialization_tag.h
+++ b/third_party/blink/renderer/bindings/core/v8/serialization/serialization_tag.h
@@ -104,6 +104,7 @@
   kRTCEncodedAudioFrameTag = 'A',  // uint32_t -> transferred audio frame ID
   kRTCEncodedVideoFrameTag = 'V',  // uint32_t -> transferred video frame ID
 
+  kAudioFrameTag = 'a',  // uint32_t -> transferred audio frame data
   kVideoFrameTag = 'v',  // uint32_t -> transferred video frame ID
 
   // The following tags were used by the Shape Detection API implementation
diff --git a/third_party/blink/renderer/bindings/modules/v8/serialization/v8_script_value_deserializer_for_modules.cc b/third_party/blink/renderer/bindings/modules/v8/serialization/v8_script_value_deserializer_for_modules.cc
index 327c0e8..59b5e37 100644
--- a/third_party/blink/renderer/bindings/modules/v8/serialization/v8_script_value_deserializer_for_modules.cc
+++ b/third_party/blink/renderer/bindings/modules/v8/serialization/v8_script_value_deserializer_for_modules.cc
@@ -22,6 +22,9 @@
 #include "third_party/blink/renderer/modules/peerconnection/rtc_encoded_audio_frame_delegate.h"
 #include "third_party/blink/renderer/modules/peerconnection/rtc_encoded_video_frame.h"
 #include "third_party/blink/renderer/modules/peerconnection/rtc_encoded_video_frame_delegate.h"
+#include "third_party/blink/renderer/modules/webcodecs/audio_frame.h"
+#include "third_party/blink/renderer/modules/webcodecs/audio_frame_attachment.h"
+#include "third_party/blink/renderer/modules/webcodecs/audio_frame_serialization_data.h"
 #include "third_party/blink/renderer/modules/webcodecs/video_frame.h"
 #include "third_party/blink/renderer/modules/webcodecs/video_frame_attachment.h"
 
@@ -75,6 +78,8 @@
       return ReadRTCEncodedAudioFrame();
     case kRTCEncodedVideoFrameTag:
       return ReadRTCEncodedVideoFrame();
+    case kAudioFrameTag:
+      return ReadAudioFrame();
     case kVideoFrameTag:
       return ReadVideoFrame();
     default:
@@ -418,6 +423,28 @@
   return MakeGarbageCollected<RTCEncodedVideoFrame>(frames[index]);
 }
 
+AudioFrame* V8ScriptValueDeserializerForModules::ReadAudioFrame() {
+  if (!RuntimeEnabledFeatures::WebCodecsEnabled(
+          ExecutionContext::From(GetScriptState()))) {
+    return nullptr;
+  }
+
+  uint32_t index;
+  if (!ReadUint32(&index))
+    return nullptr;
+
+  const auto* attachment =
+      GetSerializedScriptValue()->GetAttachmentIfExists<AudioFrameAttachment>();
+  if (!attachment)
+    return nullptr;
+
+  const auto& serialization_data = attachment->SerializationData();
+  if (index >= attachment->size())
+    return nullptr;
+
+  return MakeGarbageCollected<AudioFrame>(serialization_data[index].get());
+}
+
 VideoFrame* V8ScriptValueDeserializerForModules::ReadVideoFrame() {
   if (!RuntimeEnabledFeatures::WebCodecsEnabled(
           ExecutionContext::From(GetScriptState()))) {
diff --git a/third_party/blink/renderer/bindings/modules/v8/serialization/v8_script_value_deserializer_for_modules.h b/third_party/blink/renderer/bindings/modules/v8/serialization/v8_script_value_deserializer_for_modules.h
index 90408ee..9a01275 100644
--- a/third_party/blink/renderer/bindings/modules/v8/serialization/v8_script_value_deserializer_for_modules.h
+++ b/third_party/blink/renderer/bindings/modules/v8/serialization/v8_script_value_deserializer_for_modules.h
@@ -10,6 +10,7 @@
 
 namespace blink {
 
+class AudioFrame;
 class CryptoKey;
 class FileSystemHandle;
 class RTCEncodedAudioFrame;
@@ -49,6 +50,7 @@
   FileSystemHandle* ReadFileSystemHandle(SerializationTag tag);
   RTCEncodedAudioFrame* ReadRTCEncodedAudioFrame();
   RTCEncodedVideoFrame* ReadRTCEncodedVideoFrame();
+  AudioFrame* ReadAudioFrame();
   VideoFrame* ReadVideoFrame();
 };
 
diff --git a/third_party/blink/renderer/bindings/modules/v8/serialization/v8_script_value_serializer_for_modules.cc b/third_party/blink/renderer/bindings/modules/v8/serialization/v8_script_value_serializer_for_modules.cc
index c60809e..0deeb89 100644
--- a/third_party/blink/renderer/bindings/modules/v8/serialization/v8_script_value_serializer_for_modules.cc
+++ b/third_party/blink/renderer/bindings/modules/v8/serialization/v8_script_value_serializer_for_modules.cc
@@ -11,6 +11,7 @@
 #include "third_party/blink/renderer/bindings/core/v8/serialization/v8_script_value_serializer.h"
 #include "third_party/blink/renderer/bindings/core/v8/v8_dom_rect_read_only.h"
 #include "third_party/blink/renderer/bindings/modules/v8/serialization/web_crypto_sub_tags.h"
+#include "third_party/blink/renderer/bindings/modules/v8/v8_audio_frame.h"
 #include "third_party/blink/renderer/bindings/modules/v8/v8_crypto_key.h"
 #include "third_party/blink/renderer/bindings/modules/v8/v8_dom_file_system.h"
 #include "third_party/blink/renderer/bindings/modules/v8/v8_file_system_directory_handle.h"
@@ -25,6 +26,9 @@
 #include "third_party/blink/renderer/modules/peerconnection/rtc_encoded_audio_frame_delegate.h"
 #include "third_party/blink/renderer/modules/peerconnection/rtc_encoded_video_frame.h"
 #include "third_party/blink/renderer/modules/peerconnection/rtc_encoded_video_frame_delegate.h"
+#include "third_party/blink/renderer/modules/webcodecs/audio_frame.h"
+#include "third_party/blink/renderer/modules/webcodecs/audio_frame_attachment.h"
+#include "third_party/blink/renderer/modules/webcodecs/audio_frame_serialization_data.h"
 #include "third_party/blink/renderer/modules/webcodecs/video_frame.h"
 #include "third_party/blink/renderer/modules/webcodecs/video_frame_attachment.h"
 #include "third_party/blink/renderer/platform/bindings/script_wrappable.h"
@@ -118,6 +122,26 @@
     }
     return WriteVideoFrameHandle(std::move(handle));
   }
+  if (wrapper_type_info == V8AudioFrame::GetWrapperTypeInfo() &&
+      RuntimeEnabledFeatures::WebCodecsEnabled(
+          ExecutionContext::From(GetScriptState()))) {
+    if (IsForStorage()) {
+      exception_state.ThrowDOMException(
+          DOMExceptionCode::kDataCloneError,
+          "An AudioFrame cannot be serialized for "
+          "storage.");
+      return false;
+    }
+    std::unique_ptr<AudioFrameSerializationData> data =
+        wrappable->ToImpl<AudioFrame>()->GetSerializationData();
+    if (!data) {
+      exception_state.ThrowDOMException(DOMExceptionCode::kDataCloneError,
+                                        "An AudioFrame could not be cloned "
+                                        "because it was closed.");
+      return false;
+    }
+    return WriteAudioFrameSerializationData(std::move(data));
+  }
   return false;
 }
 
@@ -358,4 +382,18 @@
   return true;
 }
 
+bool V8ScriptValueSerializerForModules::WriteAudioFrameSerializationData(
+    std::unique_ptr<AudioFrameSerializationData> audio_data) {
+  auto* attachment =
+      GetSerializedScriptValue()->GetOrCreateAttachment<AudioFrameAttachment>();
+  auto& serialization_data = attachment->SerializationData();
+  serialization_data.push_back(std::move(audio_data));
+  const uint32_t index = static_cast<uint32_t>(serialization_data.size() - 1);
+
+  WriteTag(kAudioFrameTag);
+  WriteUint32(index);
+
+  return true;
+}
+
 }  // namespace blink
diff --git a/third_party/blink/renderer/bindings/modules/v8/serialization/v8_script_value_serializer_for_modules.h b/third_party/blink/renderer/bindings/modules/v8/serialization/v8_script_value_serializer_for_modules.h
index 3ea3283c..013d608 100644
--- a/third_party/blink/renderer/bindings/modules/v8/serialization/v8_script_value_serializer_for_modules.h
+++ b/third_party/blink/renderer/bindings/modules/v8/serialization/v8_script_value_serializer_for_modules.h
@@ -12,6 +12,7 @@
 
 namespace blink {
 
+class AudioFrameSerializationData;
 class FileSystemHandle;
 class RTCEncodedAudioFrame;
 class RTCEncodedVideoFrame;
@@ -38,6 +39,8 @@
   bool WriteRTCEncodedAudioFrame(RTCEncodedAudioFrame*);
   bool WriteRTCEncodedVideoFrame(RTCEncodedVideoFrame*);
   bool WriteVideoFrameHandle(scoped_refptr<VideoFrameHandle>);
+  bool WriteAudioFrameSerializationData(
+      std::unique_ptr<AudioFrameSerializationData>);
 };
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/bindings/modules/v8/serialization/v8_script_value_serializer_for_modules_test.cc b/third_party/blink/renderer/bindings/modules/v8/serialization/v8_script_value_serializer_for_modules_test.cc
index 170fa8a..c35aa36c 100644
--- a/third_party/blink/renderer/bindings/modules/v8/serialization/v8_script_value_serializer_for_modules_test.cc
+++ b/third_party/blink/renderer/bindings/modules/v8/serialization/v8_script_value_serializer_for_modules_test.cc
@@ -19,6 +19,7 @@
 #include "third_party/blink/renderer/bindings/core/v8/v8_dom_exception.h"
 #include "third_party/blink/renderer/bindings/core/v8/v8_dom_rect_read_only.h"
 #include "third_party/blink/renderer/bindings/modules/v8/serialization/v8_script_value_deserializer_for_modules.h"
+#include "third_party/blink/renderer/bindings/modules/v8/v8_audio_frame.h"
 #include "third_party/blink/renderer/bindings/modules/v8/v8_crypto_key.h"
 #include "third_party/blink/renderer/bindings/modules/v8/v8_dom_file_system.h"
 #include "third_party/blink/renderer/bindings/modules/v8/v8_rtc_certificate.h"
@@ -28,6 +29,8 @@
 #include "third_party/blink/renderer/modules/filesystem/dom_file_system.h"
 #include "third_party/blink/renderer/modules/peerconnection/rtc_certificate.h"
 #include "third_party/blink/renderer/modules/peerconnection/rtc_certificate_generator.h"
+#include "third_party/blink/renderer/modules/webcodecs/audio_frame.h"
+#include "third_party/blink/renderer/modules/webcodecs/audio_frame_serialization_data.h"
 #include "third_party/blink/renderer/modules/webcodecs/video_frame.h"
 #include "third_party/blink/renderer/platform/bindings/exception_state.h"
 #include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
@@ -1022,7 +1025,7 @@
   scoped_refptr<media::VideoFrame> media_frame =
       media::VideoFrame::CreateBlackFrame(kFrameSize);
 
-  // Create and destroy the frame.
+  // Create and close the frame.
   auto* blink_frame = MakeGarbageCollected<VideoFrame>(
       media_frame, scope.GetExecutionContext());
   blink_frame->close();
@@ -1035,5 +1038,84 @@
       "DataCloneError", scope.GetScriptState(), exception_state));
 }
 
+TEST(V8ScriptValueSerializerForModulesTest, RoundTripAudioFrame) {
+  V8TestingScope scope;
+
+  const unsigned kChannels = 2;
+  const unsigned kSampleRate = 8000;
+  const unsigned kFrames = 500;
+  constexpr base::TimeDelta kTimestamp = base::TimeDelta::FromMilliseconds(314);
+
+  auto audio_bus = media::AudioBus::Create(kChannels, kFrames);
+
+  // Populate each frame with a unique value.
+  const unsigned kTotalFrames = (kFrames * kChannels);
+  const float kFramesMultiplier = 1.0 / kTotalFrames;
+  for (unsigned ch = 0; ch < kChannels; ++ch) {
+    float* data = audio_bus->channel(ch);
+    for (unsigned i = 0; i < kFrames; ++i)
+      data[i] = (i + ch * kFrames) * kFramesMultiplier;
+  }
+
+  auto audio_serialization_data = AudioFrameSerializationData::Wrap(
+      std::move(audio_bus), kSampleRate, kTimestamp);
+
+  auto* audio_frame =
+      MakeGarbageCollected<AudioFrame>(std::move(audio_serialization_data));
+
+  // Round trip the frame and make sure the size is the same.
+  v8::Local<v8::Value> wrapper = ToV8(audio_frame, scope.GetScriptState());
+  v8::Local<v8::Value> result = RoundTripForModules(wrapper, scope);
+
+  // The data should have been copied, not transferred.
+  EXPECT_TRUE(audio_frame->buffer());
+
+  ASSERT_TRUE(V8AudioFrame::HasInstance(result, scope.GetIsolate()));
+
+  AudioFrame* new_frame = V8AudioFrame::ToImpl(result.As<v8::Object>());
+  EXPECT_EQ(base::TimeDelta::FromMicroseconds(new_frame->timestamp()),
+            kTimestamp);
+  EXPECT_EQ(new_frame->buffer()->numberOfChannels(), kChannels);
+  EXPECT_EQ(new_frame->buffer()->sampleRate(), kSampleRate);
+  EXPECT_EQ(new_frame->buffer()->length(), kFrames);
+
+  // Make sure the data wasn't changed during the transfer.
+  for (unsigned ch = 0; ch < kChannels; ++ch) {
+    float* src_data = audio_frame->buffer()->getChannelData(ch)->Data();
+    float* dst_data = new_frame->buffer()->getChannelData(ch)->Data();
+    for (unsigned i = 0; i < kFrames; ++i) {
+      EXPECT_EQ(src_data[i], dst_data[i]);
+    }
+  }
+
+  // Closing the original |audio_frame| should not affect |new_frame|.
+  audio_frame->close();
+  EXPECT_TRUE(new_frame->buffer());
+}
+
+TEST(V8ScriptValueSerializerForModulesTest, ClosedAudioFrameThrows) {
+  V8TestingScope scope;
+  ExceptionState exception_state(scope.GetIsolate(),
+                                 ExceptionState::kExecutionContext, "Window",
+                                 "postMessage");
+
+  auto audio_bus = media::AudioBus::Create(/*channels=*/2, /*frames=*/500);
+  auto audio_serialization_data = AudioFrameSerializationData::Wrap(
+      std::move(audio_bus), /*sample_rate=*/8000,
+      base::TimeDelta::FromMilliseconds(314));
+
+  // Create and close the frame.
+  auto* audio_frame =
+      MakeGarbageCollected<AudioFrame>(std::move(audio_serialization_data));
+  audio_frame->close();
+
+  // Serializing the closed frame should throw an error.
+  v8::Local<v8::Value> wrapper = ToV8(audio_frame, scope.GetScriptState());
+  EXPECT_FALSE(V8ScriptValueSerializer(scope.GetScriptState())
+                   .Serialize(wrapper, exception_state));
+  EXPECT_TRUE(HadDOMExceptionInModulesTest(
+      "DataCloneError", scope.GetScriptState(), exception_state));
+}
+
 }  // namespace
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/exported/web_document_subresource_filter_test.cc b/third_party/blink/renderer/core/exported/web_document_subresource_filter_test.cc
index 6340e3b..58e63c2 100644
--- a/third_party/blink/renderer/core/exported/web_document_subresource_filter_test.cc
+++ b/third_party/blink/renderer/core/exported/web_document_subresource_filter_test.cc
@@ -73,7 +73,6 @@
   void DidCommitNavigation(
       WebHistoryCommitType commit_type,
       bool should_reset_browser_interface_broker,
-      network::mojom::WebSandboxFlags sandbox_flags,
       const ParsedPermissionsPolicy& permissions_policy_header,
       const DocumentPolicyFeatureState& document_policy_header) override {
     subresource_filter_ =
diff --git a/third_party/blink/renderer/core/frame/local_frame_client.h b/third_party/blink/renderer/core/frame/local_frame_client.h
index 20a509a..2344c10 100644
--- a/third_party/blink/renderer/core/frame/local_frame_client.h
+++ b/third_party/blink/renderer/core/frame/local_frame_client.h
@@ -138,7 +138,6 @@
       HistoryItem* item,
       WebHistoryCommitType commit_type,
       bool should_reset_browser_interface_broker,
-      network::mojom::WebSandboxFlags sandbox_flags,
       const blink::ParsedPermissionsPolicy& permissions_policy_header,
       const blink::DocumentPolicyFeatureState& document_policy_header) = 0;
   virtual void DispatchDidFailLoad(const ResourceError&,
diff --git a/third_party/blink/renderer/core/frame/local_frame_client_impl.cc b/third_party/blink/renderer/core/frame/local_frame_client_impl.cc
index 76a5edba..cbddca0 100644
--- a/third_party/blink/renderer/core/frame/local_frame_client_impl.cc
+++ b/third_party/blink/renderer/core/frame/local_frame_client_impl.cc
@@ -404,7 +404,6 @@
     HistoryItem* item,
     WebHistoryCommitType commit_type,
     bool should_reset_browser_interface_broker,
-    network::mojom::WebSandboxFlags sandbox_flags,
     const blink::ParsedPermissionsPolicy& permissions_policy_header,
     const blink::DocumentPolicyFeatureState& document_policy_header) {
   if (!web_frame_->Parent()) {
@@ -414,7 +413,7 @@
 
   if (web_frame_->Client()) {
     web_frame_->Client()->DidCommitNavigation(
-        commit_type, should_reset_browser_interface_broker, sandbox_flags,
+        commit_type, should_reset_browser_interface_broker,
         permissions_policy_header, document_policy_header);
 
     // With local to local swap it's possible for the frame to be deleted as a
diff --git a/third_party/blink/renderer/core/frame/local_frame_client_impl.h b/third_party/blink/renderer/core/frame/local_frame_client_impl.h
index 244a0028..76643c8 100644
--- a/third_party/blink/renderer/core/frame/local_frame_client_impl.h
+++ b/third_party/blink/renderer/core/frame/local_frame_client_impl.h
@@ -100,7 +100,6 @@
       HistoryItem*,
       WebHistoryCommitType,
       bool should_reset_browser_interface_broker,
-      network::mojom::WebSandboxFlags sandbox_flags,
       const blink::ParsedPermissionsPolicy& permissions_policy_header,
       const blink::DocumentPolicyFeatureState& document_policy_header) override;
   void DispatchDidFailLoad(const ResourceError&, WebHistoryCommitType) override;
diff --git a/third_party/blink/renderer/core/frame/web_frame_test.cc b/third_party/blink/renderer/core/frame/web_frame_test.cc
index f09cb07..80d2d66 100644
--- a/third_party/blink/renderer/core/frame/web_frame_test.cc
+++ b/third_party/blink/renderer/core/frame/web_frame_test.cc
@@ -4235,7 +4235,6 @@
   void DidCommitNavigation(
       WebHistoryCommitType commit_type,
       bool should_reset_browser_interface_broker,
-      network::mojom::WebSandboxFlags sandbox_flags,
       const ParsedPermissionsPolicy& permissions_policy_header,
       const DocumentPolicyFeatureState& document_policy_header) override {
     Frame()->View()->ResetScrollAndScaleState();
@@ -6502,7 +6501,6 @@
   void DidCommitNavigation(
       WebHistoryCommitType commit_type,
       bool should_reset_browser_interface_broker,
-      network::mojom::WebSandboxFlags sandbox_flags,
       const ParsedPermissionsPolicy& permissions_policy_header,
       const DocumentPolicyFeatureState& document_policy_header) final {
     did_load_ = true;
@@ -9642,7 +9640,6 @@
   void DidCommitNavigation(
       WebHistoryCommitType history_commit_type,
       bool should_reset_browser_interface_broker,
-      network::mojom::WebSandboxFlags sandbox_flags,
       const ParsedPermissionsPolicy& permissions_policy_header,
       const DocumentPolicyFeatureState& document_policy_header) override {
     history_commit_type_ = history_commit_type;
@@ -9909,7 +9906,6 @@
   void DidCommitNavigation(
       WebHistoryCommitType history_commit_type,
       bool should_reset_browser_interface_broker,
-      network::mojom::WebSandboxFlags sandbox_flags,
       const ParsedPermissionsPolicy& permissions_policy_header,
       const DocumentPolicyFeatureState& document_policy_header) final {
     history_commit_type_ = history_commit_type;
diff --git a/third_party/blink/renderer/core/frame/web_local_frame_client_test.cc b/third_party/blink/renderer/core/frame/web_local_frame_client_test.cc
index 23d5f08..b726aa2d 100644
--- a/third_party/blink/renderer/core/frame/web_local_frame_client_test.cc
+++ b/third_party/blink/renderer/core/frame/web_local_frame_client_test.cc
@@ -34,12 +34,11 @@
   void DidCommitNavigation(
       WebHistoryCommitType commit_type,
       bool should_reset_browser_interface_broker,
-      network::mojom::WebSandboxFlags sandbox_flags,
       const ParsedPermissionsPolicy& permissions_policy_header,
       const DocumentPolicyFeatureState& document_policy_header) override {
     calls_.push_back("DidCommitNavigation");
     TestWebFrameClient::DidCommitNavigation(
-        commit_type, should_reset_browser_interface_broker, sandbox_flags,
+        commit_type, should_reset_browser_interface_broker,
         permissions_policy_header, document_policy_header);
   }
 
diff --git a/third_party/blink/renderer/core/inspector/inspector_trace_events.h b/third_party/blink/renderer/core/inspector/inspector_trace_events.h
index 2ec962a..e46bb392 100644
--- a/third_party/blink/renderer/core/inspector/inspector_trace_events.h
+++ b/third_party/blink/renderer/core/inspector/inspector_trace_events.h
@@ -519,6 +519,7 @@
 
 CORE_EXPORT String ToHexString(const void* p);
 CORE_EXPORT void SetCallStack(TracedValue*);
+CORE_EXPORT void SetCallStack(perfetto::TracedDictionary&);
 
 }  // namespace blink
 
diff --git a/third_party/blink/renderer/core/layout/ng/table/ng_table_layout_algorithm_helpers.cc b/third_party/blink/renderer/core/layout/ng/table/ng_table_layout_algorithm_helpers.cc
index 58ce313..cea1314 100644
--- a/third_party/blink/renderer/core/layout/ng/table/ng_table_layout_algorithm_helpers.cc
+++ b/third_party/blink/renderer/core/layout/ng/table/ng_table_layout_algorithm_helpers.cc
@@ -771,7 +771,7 @@
   if (distributable_block_size <= LayoutUnit())
     return;
 
-  // Step 1: percentage rows grow to their percentage size.
+  // Step 1: percentage rows grow to no more than their percentage size.
   if (percent_rows_with_deficit_count > 0) {
     float ratio = std::min(
         distributable_block_size.ToFloat() / percentage_block_size_deficit,
@@ -792,6 +792,9 @@
     }
     last_row->block_size += remaining_deficit;
     distributed_block_size += remaining_deficit;
+    // Rounding errors might cause us to distribute more than available length.
+    distributed_block_size =
+        std::min(distributed_block_size, distributable_block_size);
     distributable_block_size -= distributed_block_size;
   }
   DCHECK_GE(distributable_block_size, LayoutUnit());
diff --git a/third_party/blink/renderer/core/loader/document_loader.cc b/third_party/blink/renderer/core/loader/document_loader.cc
index ea436a6a..ed8dcb31 100644
--- a/third_party/blink/renderer/core/loader/document_loader.cc
+++ b/third_party/blink/renderer/core/loader/document_loader.cc
@@ -2199,7 +2199,6 @@
       GetLocalFrameClient().DispatchDidCommitLoad(
           history_item_.Get(), LoadTypeToCommitType(load_type_),
           previous_window != frame_->DomWindow(),
-          frame_->DomWindow()->GetSandboxFlags(),
           security_init.PermissionsPolicyHeader(),
           document_policy_.feature_state);
     }
diff --git a/third_party/blink/renderer/core/loader/empty_clients.h b/third_party/blink/renderer/core/loader/empty_clients.h
index 10a75b72..9f9e6c6 100644
--- a/third_party/blink/renderer/core/loader/empty_clients.h
+++ b/third_party/blink/renderer/core/loader/empty_clients.h
@@ -258,7 +258,6 @@
       HistoryItem* item,
       WebHistoryCommitType commit_type,
       bool should_reset_browser_interface_broker,
-      network::mojom::WebSandboxFlags sandbox_flags,
       const blink::ParsedPermissionsPolicy& permissions_policy_header,
       const blink::DocumentPolicyFeatureState& document_policy_header)
       override {}
diff --git a/third_party/blink/renderer/core/paint/svg_foreign_object_painter.cc b/third_party/blink/renderer/core/paint/svg_foreign_object_painter.cc
index 5d0f5254..81b649f 100644
--- a/third_party/blink/renderer/core/paint/svg_foreign_object_painter.cc
+++ b/third_party/blink/renderer/core/paint/svg_foreign_object_painter.cc
@@ -54,8 +54,8 @@
   // ScopedSVGPaintState only applies masks (and clips-within-clips) here.
   ScopedSVGPaintState paint_state(layout_svg_foreign_object_, paint_info);
 
-  PaintTiming& timing = PaintTiming::From(
-      layout_svg_foreign_object_.GetElement()->GetDocument().TopDocument());
+  PaintTiming& timing =
+      PaintTiming::From(layout_svg_foreign_object_.GetDocument());
   timing.MarkFirstContentfulPaint();
 
   BlockPainter(layout_svg_foreign_object_).Paint(paint_info);
diff --git a/third_party/blink/renderer/core/paint/svg_image_painter.cc b/third_party/blink/renderer/core/paint/svg_image_painter.cc
index af754d84..9afdc91 100644
--- a/third_party/blink/renderer/core/paint/svg_image_painter.cc
+++ b/third_party/blink/renderer/core/paint/svg_image_painter.cc
@@ -107,8 +107,7 @@
       layout_svg_image_, image->Size(), *image_content,
       paint_info.context.GetPaintController().CurrentPaintChunkProperties(),
       EnclosingIntRect(dest_rect));
-  PaintTiming& timing = PaintTiming::From(
-      layout_svg_image_.GetElement()->GetDocument().TopDocument());
+  PaintTiming& timing = PaintTiming::From(layout_svg_image_.GetDocument());
   timing.MarkFirstContentfulPaint();
 }
 
diff --git a/third_party/blink/renderer/core/paint/svg_inline_text_box_painter.cc b/third_party/blink/renderer/core/paint/svg_inline_text_box_painter.cc
index dbbcc44b..59f116d 100644
--- a/third_party/blink/renderer/core/paint/svg_inline_text_box_painter.cc
+++ b/third_party/blink/renderer/core/paint/svg_inline_text_box_painter.cc
@@ -504,8 +504,7 @@
   context.GetPaintController().SetTextPainted();
 
   if (!scaled_font.ShouldSkipDrawing()) {
-    PaintTiming& timing = PaintTiming::From(
-        text_layout_object.GetNode()->GetDocument().TopDocument());
+    PaintTiming& timing = PaintTiming::From(text_layout_object.GetDocument());
     timing.MarkFirstContentfulPaint();
     PaintTimingDetector::NotifyTextPaint(EnclosingIntRect(
         InlineLayoutObject().VisualRectInLocalSVGCoordinates()));
diff --git a/third_party/blink/renderer/core/paint/svg_shape_painter.cc b/third_party/blink/renderer/core/paint/svg_shape_painter.cc
index 1f593a8..3c2ced9 100644
--- a/third_party/blink/renderer/core/paint/svg_shape_painter.cc
+++ b/third_party/blink/renderer/core/paint/svg_shape_painter.cc
@@ -162,8 +162,7 @@
                        DarkModeFilter::ElementRole::kSVG);
     }
   }
-  PaintTiming& timing = PaintTiming::From(
-      layout_svg_shape_.GetElement()->GetDocument().TopDocument());
+  PaintTiming& timing = PaintTiming::From(layout_svg_shape_.GetDocument());
   timing.MarkFirstContentfulPaint();
 }
 
@@ -188,8 +187,7 @@
       context.DrawPath(use_path->GetSkPath(), flags,
                        DarkModeFilter::ElementRole::kSVG);
   }
-  PaintTiming& timing = PaintTiming::From(
-      layout_svg_shape_.GetElement()->GetDocument().TopDocument());
+  PaintTiming& timing = PaintTiming::From(layout_svg_shape_.GetDocument());
   timing.MarkFirstContentfulPaint();
 }
 
diff --git a/third_party/blink/renderer/modules/mediasession/media_session.cc b/third_party/blink/renderer/modules/mediasession/media_session.cc
index 7cbfdaf..07a978f0c 100644
--- a/third_party/blink/renderer/modules/mediasession/media_session.cc
+++ b/third_party/blink/renderer/modules/mediasession/media_session.cc
@@ -44,6 +44,11 @@
   DEFINE_STATIC_LOCAL(const AtomicString, skip_ad_action_name, ("skipad"));
   DEFINE_STATIC_LOCAL(const AtomicString, stop_action_name, ("stop"));
   DEFINE_STATIC_LOCAL(const AtomicString, seek_to_action_name, ("seekto"));
+  DEFINE_STATIC_LOCAL(const AtomicString, toggle_microphone_action_name,
+                      ("togglemicrophone"));
+  DEFINE_STATIC_LOCAL(const AtomicString, toggle_camera_action_name,
+                      ("togglecamera"));
+  DEFINE_STATIC_LOCAL(const AtomicString, hang_up_action_name, ("hangup"));
 
   switch (action) {
     case MediaSessionAction::kPlay:
@@ -64,6 +69,12 @@
       return stop_action_name;
     case MediaSessionAction::kSeekTo:
       return seek_to_action_name;
+    case MediaSessionAction::kToggleMicrophone:
+      return toggle_microphone_action_name;
+    case MediaSessionAction::kToggleCamera:
+      return toggle_camera_action_name;
+    case MediaSessionAction::kHangUp:
+      return hang_up_action_name;
     default:
       NOTREACHED();
   }
@@ -90,6 +101,12 @@
     return MediaSessionAction::kStop;
   if ("seekto" == action_name)
     return MediaSessionAction::kSeekTo;
+  if ("togglemicrophone" == action_name)
+    return MediaSessionAction::kToggleMicrophone;
+  if ("togglecamera" == action_name)
+    return MediaSessionAction::kToggleCamera;
+  if ("hangup" == action_name)
+    return MediaSessionAction::kHangUp;
 
   NOTREACHED();
   return base::nullopt;
@@ -196,6 +213,16 @@
     UseCounter::Count(window, WebFeature::kMediaSessionSkipAd);
   }
 
+  if (!RuntimeEnabledFeatures::MediaSessionWebRTCEnabled()) {
+    if ("togglemicrophone" == action || "togglecamera" == action ||
+        "hangup" == action) {
+      exception_state.ThrowTypeError("The provided value '" + action +
+                                     "' is not a valid enum "
+                                     "value of type MediaSessionAction.");
+      return;
+    }
+  }
+
   if (handler) {
     auto add_result = action_handlers_.Set(action, handler);
 
@@ -272,6 +299,31 @@
   RecalculatePositionState(/*was_set=*/true);
 }
 
+void MediaSession::setMicrophoneActive(bool active) {
+  auto* service = GetService();
+  if (!service)
+    return;
+
+  if (active) {
+    service->SetMicrophoneState(
+        media_session::mojom::MicrophoneState::kUnmuted);
+  } else {
+    service->SetMicrophoneState(media_session::mojom::MicrophoneState::kMuted);
+  }
+}
+
+void MediaSession::setCameraActive(bool active) {
+  auto* service = GetService();
+  if (!service)
+    return;
+
+  if (active) {
+    service->SetCameraState(media_session::mojom::CameraState::kTurnedOn);
+  } else {
+    service->SetCameraState(media_session::mojom::CameraState::kTurnedOff);
+  }
+}
+
 void MediaSession::NotifyActionChange(const String& action,
                                       ActionChangeType type) {
   mojom::blink::MediaSessionService* service = GetService();
diff --git a/third_party/blink/renderer/modules/mediasession/media_session.h b/third_party/blink/renderer/modules/mediasession/media_session.h
index 767ebea..1bc16f9 100644
--- a/third_party/blink/renderer/modules/mediasession/media_session.h
+++ b/third_party/blink/renderer/modules/mediasession/media_session.h
@@ -51,6 +51,10 @@
 
   void setPositionState(MediaPositionState*, ExceptionState&);
 
+  void setMicrophoneActive(bool active);
+
+  void setCameraActive(bool active);
+
   // Called by the MediaMetadata owned by |this| when it has updates. Also used
   // internally when a new MediaMetadata object is set.
   void OnMetadataChanged();
diff --git a/third_party/blink/renderer/modules/mediasession/media_session.idl b/third_party/blink/renderer/modules/mediasession/media_session.idl
index 3f9e8bafb..7bf2fe1c 100644
--- a/third_party/blink/renderer/modules/mediasession/media_session.idl
+++ b/third_party/blink/renderer/modules/mediasession/media_session.idl
@@ -21,7 +21,10 @@
   "seekforward",
   "skipad",
   "stop",
-  "seekto"
+  "seekto",
+  "togglemicrophone",
+  "togglecamera",
+  "hangup"
 };
 
 callback MediaSessionActionHandler = void (MediaSessionActionDetails details);
@@ -38,4 +41,10 @@
 
     [Measure, RaisesException, RuntimeEnabled=MediaSessionPosition]
     void setPositionState(optional MediaPositionState state = {});
+
+    [Measure, RuntimeEnabled=MediaSessionWebRTC]
+    void setMicrophoneActive(boolean active);
+
+    [Measure, RuntimeEnabled=MediaSessionWebRTC]
+    void setCameraActive(boolean active);
 };
diff --git a/third_party/blink/renderer/modules/peerconnection/peer_connection_tracker.cc b/third_party/blink/renderer/modules/peerconnection/peer_connection_tracker.cc
index 58df4c85..d7b6b34 100644
--- a/third_party/blink/renderer/modules/peerconnection/peer_connection_tracker.cc
+++ b/third_party/blink/renderer/modules/peerconnection/peer_connection_tracker.cc
@@ -323,11 +323,12 @@
     default:
       NOTREACHED();
   }
-  return sdp_semantics_str;
+  return "\"" + sdp_semantics_str + "\"";
 }
 
 String SerializeConfiguration(
-    const webrtc::PeerConnectionInterface::RTCConfiguration& config) {
+    const webrtc::PeerConnectionInterface::RTCConfiguration& config,
+    bool usesInsertableStreams) {
   StringBuilder result;
   // TODO(hbos): Add serialization of certificate.
   result.Append("{ iceServers: ");
@@ -340,9 +341,12 @@
   result.Append(SerializeRtcpMuxPolicy(config.rtcp_mux_policy));
   result.Append(", iceCandidatePoolSize: ");
   result.AppendNumber(config.ice_candidate_pool_size);
-  result.Append(", sdpSemantics: \"");
+  result.Append(", sdpSemantics: ");
   result.Append(SerializeSdpSemantics(config.sdp_semantics));
-  result.Append("\" }");
+  if (usesInsertableStreams) {
+    result.Append(", encodedInsertableStreams: true");
+  }
+  result.Append(" }");
   return result.ToString();
 }
 
@@ -834,7 +838,11 @@
   auto info = blink::mojom::blink::PeerConnectionInfo::New();
 
   info->lid = GetNextLocalID();
-  info->rtc_configuration = SerializeConfiguration(config);
+  bool usesInsertableStreams =
+      pc_handler->force_encoded_audio_insertable_streams() &&
+      pc_handler->force_encoded_video_insertable_streams();
+  info->rtc_configuration =
+      SerializeConfiguration(config, usesInsertableStreams);
 
   info->constraints = SerializeMediaConstraints(constraints);
   if (frame)
@@ -950,8 +958,12 @@
   if (id == -1)
     return;
 
-  SendPeerConnectionUpdate(id, "setConfiguration",
-                           SerializeConfiguration(config));
+  bool usesInsertableStreams =
+      pc_handler->force_encoded_audio_insertable_streams() &&
+      pc_handler->force_encoded_video_insertable_streams();
+  SendPeerConnectionUpdate(
+      id, "setConfiguration",
+      SerializeConfiguration(config, usesInsertableStreams));
 }
 
 void PeerConnectionTracker::TrackAddIceCandidate(
diff --git a/third_party/blink/renderer/modules/peerconnection/rtc_peer_connection.idl b/third_party/blink/renderer/modules/peerconnection/rtc_peer_connection.idl
index d88ca64..74c28ea 100644
--- a/third_party/blink/renderer/modules/peerconnection/rtc_peer_connection.idl
+++ b/third_party/blink/renderer/modules/peerconnection/rtc_peer_connection.idl
@@ -70,8 +70,8 @@
 ] interface RTCPeerConnection : EventTarget {
     // TODO(guidou): There should only be one constructor argument.
     [CallWith=ExecutionContext, RaisesException] constructor(optional RTCConfiguration configuration = {}, optional any mediaConstraints);
-    [CallWith=ScriptState, RaisesException] Promise<RTCSessionDescription> createOffer(optional RTCOfferOptions options = {});
-    [CallWith=ScriptState, RaisesException] Promise<RTCSessionDescription> createAnswer(optional RTCAnswerOptions options = {});
+    [CallWith=ScriptState, RaisesException] Promise<RTCSessionDescriptionInit> createOffer(optional RTCOfferOptions options = {});
+    [CallWith=ScriptState, RaisesException] Promise<RTCSessionDescriptionInit> createAnswer(optional RTCAnswerOptions options = {});
     [CallWith=ScriptState, RaisesException] Promise<void> setLocalDescription(optional RTCSessionDescriptionInit description = {});
     readonly attribute RTCSessionDescription? localDescription;
     readonly attribute RTCSessionDescription? currentLocalDescription;
diff --git a/third_party/blink/renderer/modules/webcodecs/BUILD.gn b/third_party/blink/renderer/modules/webcodecs/BUILD.gn
index 3c5a5489..a575126 100644
--- a/third_party/blink/renderer/modules/webcodecs/BUILD.gn
+++ b/third_party/blink/renderer/modules/webcodecs/BUILD.gn
@@ -17,6 +17,8 @@
     "audio_encoder.h",
     "audio_frame.cc",
     "audio_frame.h",
+    "audio_frame_attachment.cc",
+    "audio_frame_attachment.h",
     "audio_frame_serialization_data.cc",
     "audio_frame_serialization_data.h",
     "codec_config_eval.h",
diff --git a/third_party/blink/renderer/modules/webcodecs/audio_frame.cc b/third_party/blink/renderer/modules/webcodecs/audio_frame.cc
index d9ab76d..3f4d5c5b 100644
--- a/third_party/blink/renderer/modules/webcodecs/audio_frame.cc
+++ b/third_party/blink/renderer/modules/webcodecs/audio_frame.cc
@@ -47,31 +47,10 @@
   buffer->ReadAllFrames(wrapped_channels);
 }
 
-std::unique_ptr<AudioFrameSerializationData>
-AudioFrame::GetSerializationData() {
-  DCHECK(buffer_);
-
-  // Copy buffer unaligned memory into media::AudioBus' aligned memory.
-  // TODO(https://crbug.com/1168418): reevaluate if this copy is necessary after
-  // our changes. E.g. If we can ever guarantee AudioBuffer's memory alignment,
-  // we could save this copy here, by using buffer_->GetSharedAudioBuffer() and
-  // wrapping it directly.
-  auto data_copy =
-      media::AudioBus::Create(buffer_->numberOfChannels(), buffer_->length());
-
-  for (int i = 0; i < data_copy->channels(); ++i) {
-    size_t byte_length = buffer_->getChannelData(i)->byteLength();
-    DCHECK_EQ(byte_length, data_copy->frames() * sizeof(float));
-    float* buffer_data_src = buffer_->getChannelData(i)->Data();
-    memcpy(data_copy->channel(i), buffer_data_src, byte_length);
-  }
-
-  return AudioFrameSerializationData::Wrap(
-      std::move(data_copy), buffer_->sampleRate(),
-      base::TimeDelta::FromMicroseconds(timestamp_));
-}
-
 AudioFrame::AudioFrame(std::unique_ptr<AudioFrameSerializationData> data)
+    : AudioFrame(data.get()) {}
+
+AudioFrame::AudioFrame(AudioFrameSerializationData* data)
     : timestamp_(data->timestamp().InMicroseconds()) {
   media::AudioBus* data_bus = data->data();
 
@@ -100,6 +79,31 @@
   }
 }
 
+std::unique_ptr<AudioFrameSerializationData>
+AudioFrame::GetSerializationData() {
+  if (!buffer_)
+    return nullptr;
+
+  // Copy buffer unaligned memory into media::AudioBus' aligned memory.
+  // TODO(https://crbug.com/1168418): reevaluate if this copy is necessary after
+  // our changes. E.g. If we can ever guarantee AudioBuffer's memory alignment,
+  // we could save this copy here, by using buffer_->GetSharedAudioBuffer() and
+  // wrapping it directly.
+  auto data_copy =
+      media::AudioBus::Create(buffer_->numberOfChannels(), buffer_->length());
+
+  for (int i = 0; i < data_copy->channels(); ++i) {
+    size_t byte_length = buffer_->getChannelData(i)->byteLength();
+    DCHECK_EQ(byte_length, data_copy->frames() * sizeof(float));
+    float* buffer_data_src = buffer_->getChannelData(i)->Data();
+    memcpy(data_copy->channel(i), buffer_data_src, byte_length);
+  }
+
+  return AudioFrameSerializationData::Wrap(
+      std::move(data_copy), buffer_->sampleRate(),
+      base::TimeDelta::FromMicroseconds(timestamp_));
+}
+
 void AudioFrame::close() {
   buffer_.Clear();
 }
diff --git a/third_party/blink/renderer/modules/webcodecs/audio_frame.h b/third_party/blink/renderer/modules/webcodecs/audio_frame.h
index ab27cdda..647f1d52 100644
--- a/third_party/blink/renderer/modules/webcodecs/audio_frame.h
+++ b/third_party/blink/renderer/modules/webcodecs/audio_frame.h
@@ -26,6 +26,9 @@
   explicit AudioFrame(scoped_refptr<media::AudioBuffer>);
   explicit AudioFrame(std::unique_ptr<AudioFrameSerializationData> data);
 
+  // Makes an internal copy of the data.
+  explicit AudioFrame(AudioFrameSerializationData* data);
+
   // audio_frame.idl implementation.
   explicit AudioFrame(AudioFrameInit*);
   void close();
diff --git a/third_party/blink/renderer/modules/webcodecs/audio_frame_attachment.cc b/third_party/blink/renderer/modules/webcodecs/audio_frame_attachment.cc
new file mode 100644
index 0000000..1b2892a
--- /dev/null
+++ b/third_party/blink/renderer/modules/webcodecs/audio_frame_attachment.cc
@@ -0,0 +1,11 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/modules/webcodecs/audio_frame_attachment.h"
+
+namespace blink {
+
+const void* const AudioFrameAttachment::kAttachmentKey = nullptr;
+
+}  // namespace blink
diff --git a/third_party/blink/renderer/modules/webcodecs/audio_frame_attachment.h b/third_party/blink/renderer/modules/webcodecs/audio_frame_attachment.h
new file mode 100644
index 0000000..5364c597
--- /dev/null
+++ b/third_party/blink/renderer/modules/webcodecs/audio_frame_attachment.h
@@ -0,0 +1,47 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_MODULES_WEBCODECS_AUDIO_FRAME_ATTACHMENT_H_
+#define THIRD_PARTY_BLINK_RENDERER_MODULES_WEBCODECS_AUDIO_FRAME_ATTACHMENT_H_
+
+#include "base/optional.h"
+#include "third_party/blink/renderer/bindings/core/v8/serialization/serialized_script_value.h"
+#include "third_party/blink/renderer/modules/modules_export.h"
+
+namespace blink {
+
+class AudioFrameSerializationData;
+
+// Used to serialize audio frames.
+class MODULES_EXPORT AudioFrameAttachment
+    : public SerializedScriptValue::Attachment {
+ public:
+  using SerializationDataVector =
+      Vector<std::unique_ptr<AudioFrameSerializationData>>;
+
+  static const void* const kAttachmentKey;
+  AudioFrameAttachment() = default;
+  ~AudioFrameAttachment() override = default;
+
+  bool IsLockedToAgentCluster() const override {
+    return !audio_serialization_data_.IsEmpty();
+  }
+
+  size_t size() const { return audio_serialization_data_.size(); }
+
+  SerializationDataVector& SerializationData() {
+    return audio_serialization_data_;
+  }
+
+  const SerializationDataVector& SerializationData() const {
+    return audio_serialization_data_;
+  }
+
+ private:
+  SerializationDataVector audio_serialization_data_;
+};
+
+}  // namespace blink
+
+#endif  // THIRD_PARTY_BLINK_RENDERER_MODULES_WEBCODECS_VIDEO_FRAME_ATTACHMENT_H_
diff --git a/third_party/blink/renderer/modules/webcodecs/audio_frame_serialization_data.cc b/third_party/blink/renderer/modules/webcodecs/audio_frame_serialization_data.cc
index d0ec790..09a679e 100644
--- a/third_party/blink/renderer/modules/webcodecs/audio_frame_serialization_data.cc
+++ b/third_party/blink/renderer/modules/webcodecs/audio_frame_serialization_data.cc
@@ -37,4 +37,5 @@
   return std::make_unique<BasicAudioFrameSerializationData>(
       std::move(data), sample_rate, timestamp);
 }
+
 }  // namespace blink
diff --git a/third_party/blink/renderer/modules/websockets/inspector_websocket_events.cc b/third_party/blink/renderer/modules/websockets/inspector_websocket_events.cc
index 409f11ae..fa73c0d 100644
--- a/third_party/blink/renderer/modules/websockets/inspector_websocket_events.cc
+++ b/third_party/blink/renderer/modules/websockets/inspector_websocket_events.cc
@@ -5,6 +5,7 @@
 #include "third_party/blink/renderer/modules/websockets/inspector_websocket_events.h"
 
 #include <memory>
+#include "base/trace_event/trace_event.h"
 #include "third_party/blink/renderer/core/execution_context/execution_context.h"
 #include "third_party/blink/renderer/core/frame/local_dom_window.h"
 #include "third_party/blink/renderer/core/frame/local_frame.h"
@@ -15,49 +16,45 @@
 
 namespace blink {
 
-std::unique_ptr<TracedValue> InspectorWebSocketCreateEvent::Data(
-    ExecutionContext* execution_context,
-    uint64_t identifier,
-    const KURL& url,
-    const String& protocol) {
+void InspectorWebSocketCreateEvent::Data(perfetto::TracedValue context,
+                                         ExecutionContext* execution_context,
+                                         uint64_t identifier,
+                                         const KURL& url,
+                                         const String& protocol) {
+  auto dict = std::move(context).WriteDictionary();
   DCHECK(execution_context->IsContextThread());
-  auto value = std::make_unique<TracedValue>();
-  value->SetInteger("identifier", static_cast<int>(identifier));
-  value->SetString("url", url.GetString());
+  dict.Add("identifier", identifier);
+  dict.Add("url", url.GetString());
   if (auto* window = DynamicTo<LocalDOMWindow>(execution_context)) {
-    value->SetString("frame", IdentifiersFactory::FrameId(window->GetFrame()));
+    dict.Add("frame", IdentifiersFactory::FrameId(window->GetFrame()));
   } else if (auto* scope = DynamicTo<WorkerGlobalScope>(execution_context)) {
-    value->SetString("workerId",
-                     IdentifiersFactory::IdFromToken(
-                         scope->GetThread()->GetDevToolsWorkerToken()));
+    dict.Add("workerId", IdentifiersFactory::IdFromToken(
+                             scope->GetThread()->GetDevToolsWorkerToken()));
   } else {
     NOTREACHED()
         << "WebSocket is available only in Window and WorkerGlobalScope";
   }
   if (!protocol.IsNull())
-    value->SetString("webSocketProtocol", protocol);
-  SetCallStack(value.get());
-  return value;
+    dict.Add("webSocketProtocol", protocol);
+  SetCallStack(dict);
 }
 
-std::unique_ptr<TracedValue> InspectorWebSocketEvent::Data(
-    ExecutionContext* execution_context,
-    uint64_t identifier) {
+void InspectorWebSocketEvent::Data(perfetto::TracedValue context,
+                                   ExecutionContext* execution_context,
+                                   uint64_t identifier) {
   DCHECK(execution_context->IsContextThread());
-  auto value = std::make_unique<TracedValue>();
-  value->SetInteger("identifier", static_cast<int>(identifier));
+  auto dict = std::move(context).WriteDictionary();
+  dict.Add("identifier", identifier);
   if (auto* window = DynamicTo<LocalDOMWindow>(execution_context)) {
-    value->SetString("frame", IdentifiersFactory::FrameId(window->GetFrame()));
+    dict.Add("frame", IdentifiersFactory::FrameId(window->GetFrame()));
   } else if (auto* scope = DynamicTo<WorkerGlobalScope>(execution_context)) {
-    value->SetString("workerId",
-                     IdentifiersFactory::IdFromToken(
-                         scope->GetThread()->GetDevToolsWorkerToken()));
+    dict.Add("workerId", IdentifiersFactory::IdFromToken(
+                             scope->GetThread()->GetDevToolsWorkerToken()));
   } else {
     NOTREACHED()
         << "WebSocket is available only in Window and WorkerGlobalScope";
   }
-  SetCallStack(value.get());
-  return value;
+  SetCallStack(dict);
 }
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/modules/websockets/inspector_websocket_events.h b/third_party/blink/renderer/modules/websockets/inspector_websocket_events.h
index bb49b2e..22ce6dbe 100644
--- a/third_party/blink/renderer/modules/websockets/inspector_websocket_events.h
+++ b/third_party/blink/renderer/modules/websockets/inspector_websocket_events.h
@@ -8,10 +8,9 @@
 #include <memory>
 #include "third_party/blink/renderer/core/inspector/inspector_trace_events.h"
 #include "third_party/blink/renderer/platform/heap/handle.h"
-#include "third_party/blink/renderer/platform/instrumentation/tracing/trace_event.h"
-#include "third_party/blink/renderer/platform/instrumentation/tracing/traced_value.h"
 #include "third_party/blink/renderer/platform/wtf/forward.h"
 #include "third_party/blink/renderer/platform/wtf/functional.h"
+#include "third_party/perfetto/include/perfetto/tracing/traced_value_forward.h"
 
 namespace blink {
 
@@ -22,18 +21,20 @@
   STATIC_ONLY(InspectorWebSocketCreateEvent);
 
  public:
-  static std::unique_ptr<TracedValue> Data(ExecutionContext*,
-                                           uint64_t identifier,
-                                           const KURL&,
-                                           const String& protocol);
+  static void Data(perfetto::TracedValue context,
+                   ExecutionContext*,
+                   uint64_t identifier,
+                   const KURL&,
+                   const String& protocol);
 };
 
 class InspectorWebSocketEvent {
   STATIC_ONLY(InspectorWebSocketEvent);
 
  public:
-  static std::unique_ptr<TracedValue> Data(ExecutionContext*,
-                                           uint64_t identifier);
+  static void Data(perfetto::TracedValue context,
+                   ExecutionContext*,
+                   uint64_t identifier);
 };
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/modules/websockets/websocket_channel_impl.cc b/third_party/blink/renderer/modules/websockets/websocket_channel_impl.cc
index 12ed1eb..6eef0e8 100644
--- a/third_party/blink/renderer/modules/websockets/websocket_channel_impl.cc
+++ b/third_party/blink/renderer/modules/websockets/websocket_channel_impl.cc
@@ -279,10 +279,9 @@
     throttle_passed_ = true;
   }
 
-  TRACE_EVENT_INSTANT1("devtools.timeline", "WebSocketCreate",
-                       TRACE_EVENT_SCOPE_THREAD, "data",
-                       InspectorWebSocketCreateEvent::Data(
-                           execution_context_, identifier_, url, protocol));
+  DEVTOOLS_TIMELINE_TRACE_EVENT_INSTANT(
+      "WebSocketCreate", InspectorWebSocketCreateEvent::Data,
+      execution_context_, identifier_, url, protocol);
   probe::DidCreateWebSocket(execution_context_, identifier_, url, protocol);
   return true;
 }
@@ -413,9 +412,9 @@
 void WebSocketChannelImpl::Disconnect() {
   DVLOG(1) << this << " disconnect()";
   if (identifier_) {
-    TRACE_EVENT_INSTANT1(
-        "devtools.timeline", "WebSocketDestroy", TRACE_EVENT_SCOPE_THREAD,
-        "data", InspectorWebSocketEvent::Data(execution_context_, identifier_));
+    DEVTOOLS_TIMELINE_TRACE_EVENT_INSTANT("WebSocketDestroy",
+                                          InspectorWebSocketEvent::Data,
+                                          execution_context_, identifier_);
     probe::DidCloseWebSocket(execution_context_, identifier_);
   }
 
@@ -454,10 +453,9 @@
   DVLOG(1) << this << " OnOpeningHandshakeStarted(" << request->url.GetString()
            << ")";
 
-  TRACE_EVENT_INSTANT1(
-      "devtools.timeline", "WebSocketSendHandshakeRequest",
-      TRACE_EVENT_SCOPE_THREAD, "data",
-      InspectorWebSocketEvent::Data(execution_context_, identifier_));
+  DEVTOOLS_TIMELINE_TRACE_EVENT_INSTANT("WebSocketSendHandshakeRequest",
+                                        InspectorWebSocketEvent::Data,
+                                        execution_context_, identifier_);
   probe::WillSendWebSocketHandshakeRequest(execution_context_, identifier_,
                                            request.get());
   handshake_request_ = std::move(request);
@@ -483,10 +481,9 @@
   const String& extensions = response->extensions;
   DVLOG(1) << this << " OnConnectionEstablished(" << protocol << ", "
            << extensions << ")";
-  TRACE_EVENT_INSTANT1(
-      "devtools.timeline", "WebSocketReceiveHandshakeResponse",
-      TRACE_EVENT_SCOPE_THREAD, "data",
-      InspectorWebSocketEvent::Data(execution_context_, identifier_));
+  DEVTOOLS_TIMELINE_TRACE_EVENT_INSTANT("WebSocketReceiveHandshakeResponse",
+                                        InspectorWebSocketEvent::Data,
+                                        execution_context_, identifier_);
   probe::DidReceiveWebSocketHandshakeResponse(execution_context_, identifier_,
                                               handshake_request_.get(),
                                               response.get());
@@ -553,9 +550,9 @@
            << reason << ")";
 
   if (identifier_) {
-    TRACE_EVENT_INSTANT1(
-        "devtools.timeline", "WebSocketDestroy", TRACE_EVENT_SCOPE_THREAD,
-        "data", InspectorWebSocketEvent::Data(execution_context_, identifier_));
+    DEVTOOLS_TIMELINE_TRACE_EVENT_INSTANT("WebSocketDestroy",
+                                          InspectorWebSocketEvent::Data,
+                                          execution_context_, identifier_);
     probe::DidCloseWebSocket(execution_context_, identifier_);
     identifier_ = 0;
   }
diff --git a/third_party/blink/renderer/platform/BUILD.gn b/third_party/blink/renderer/platform/BUILD.gn
index a946609..e04ec75 100644
--- a/third_party/blink/renderer/platform/BUILD.gn
+++ b/third_party/blink/renderer/platform/BUILD.gn
@@ -1470,6 +1470,8 @@
     "weborigin/security_origin_hash.h",
     "weborigin/security_policy.cc",
     "weborigin/security_policy.h",
+    "webrtc/convert_to_webrtc_video_frame_buffer.cc",
+    "webrtc/convert_to_webrtc_video_frame_buffer.h",
     "webrtc/legacy_webrtc_video_frame_adapter.cc",
     "webrtc/legacy_webrtc_video_frame_adapter.h",
     "webrtc/peer_connection_remote_audio_source.cc",
@@ -1478,6 +1480,8 @@
     "webrtc/track_observer.h",
     "webrtc/webrtc_logging.cc",
     "webrtc/webrtc_source.h",
+    "webrtc/webrtc_video_frame_adapter.cc",
+    "webrtc/webrtc_video_frame_adapter.h",
     "webrtc/webrtc_video_utils.cc",
     "webrtc/webrtc_video_utils.h",
     "widget/compositing/layer_tree_settings.cc",
@@ -2145,7 +2149,9 @@
     "weborigin/scheme_registry_test.cc",
     "weborigin/security_origin_test.cc",
     "weborigin/security_policy_test.cc",
+    "webrtc/convert_to_webrtc_video_frame_buffer_test.cc",
     "webrtc/legacy_webrtc_video_frame_adapter_test.cc",
+    "webrtc/webrtc_video_frame_adapter_test.cc",
     "widget/compositing/layer_tree_settings_unittest.cc",
     "widget/compositing/layer_tree_view_unittest.cc",
     "widget/compositing/render_frame_metadata_observer_impl_unittest.cc",
diff --git a/third_party/blink/renderer/platform/exported/DEPS b/third_party/blink/renderer/platform/exported/DEPS
index 25a72a15..8dc689b 100644
--- a/third_party/blink/renderer/platform/exported/DEPS
+++ b/third_party/blink/renderer/platform/exported/DEPS
@@ -8,7 +8,6 @@
 
 specific_include_rules = {
     "notification_data_conversions_test.cc": [
-        "+base/strings/nullable_string16.h",
         "+base/strings/utf_string_conversions.h",
     ],
 }
diff --git a/third_party/blink/renderer/platform/exported/web_runtime_features.cc b/third_party/blink/renderer/platform/exported/web_runtime_features.cc
index 2d8d8c7..998b4cae 100644
--- a/third_party/blink/renderer/platform/exported/web_runtime_features.cc
+++ b/third_party/blink/renderer/platform/exported/web_runtime_features.cc
@@ -251,6 +251,10 @@
   RuntimeEnabledFeatures::SetMediaSessionEnabled(enable);
 }
 
+void WebRuntimeFeatures::EnableMediaSessionWebRTC(bool enable) {
+  RuntimeEnabledFeatures::SetMediaSessionWebRTCEnabled(enable);
+}
+
 void WebRuntimeFeatures::EnableNotificationContentImage(bool enable) {
   RuntimeEnabledFeatures::SetNotificationContentImageEnabled(enable);
 }
diff --git a/third_party/blink/renderer/platform/peerconnection/rtc_video_decoder_adapter.cc b/third_party/blink/renderer/platform/peerconnection/rtc_video_decoder_adapter.cc
index b382c416..ae3221d 100644
--- a/third_party/blink/renderer/platform/peerconnection/rtc_video_decoder_adapter.cc
+++ b/third_party/blink/renderer/platform/peerconnection/rtc_video_decoder_adapter.cc
@@ -332,12 +332,19 @@
     GetDecoderCounter()->IncrementCount();
   }
 
+#if defined(OS_ANDROID) && !BUILDFLAG(ENABLE_FFMPEG_VIDEO_DECODERS)
+  const bool has_software_fallback =
+      video_codec_type_ != webrtc::kVideoCodecH264;
+#else
+  const bool has_software_fallback = true;
+#endif
+
   // Don't allow hardware decode for small videos if there are too many
   // decoder instances.  This includes the case where our resolution drops while
   // too many decoders exist.
   {
     base::AutoLock auto_lock(lock_);
-    if (current_resolution_ < kMinResolution &&
+    if (has_software_fallback && current_resolution_ < kMinResolution &&
         GetDecoderCounter()->Count() > kMaxDecoderInstances) {
       // Decrement the count and clear the flag, so that other decoders don't
       // fall back also.
@@ -428,7 +435,8 @@
       return WEBRTC_VIDEO_CODEC_FALLBACK_SOFTWARE;
     }
 
-    if (pending_buffers_.size() >= kMaxPendingBuffers) {
+    if (has_software_fallback &&
+        pending_buffers_.size() >= kMaxPendingBuffers) {
       // We are severely behind. Drop pending buffers and request a keyframe to
       // catch up as quickly as possible.
       DVLOG(2) << "Pending buffers overflow";
diff --git a/third_party/blink/renderer/platform/peerconnection/rtc_video_decoder_stream_adapter.cc b/third_party/blink/renderer/platform/peerconnection/rtc_video_decoder_stream_adapter.cc
index 8d5db14..252c463 100644
--- a/third_party/blink/renderer/platform/peerconnection/rtc_video_decoder_stream_adapter.cc
+++ b/third_party/blink/renderer/platform/peerconnection/rtc_video_decoder_stream_adapter.cc
@@ -438,8 +438,15 @@
       // drop any other non-key frame.
       key_frame_required_ = true;
 
+#if defined(OS_ANDROID) && !BUILDFLAG(ENABLE_FFMPEG_VIDEO_DECODERS)
+      const bool has_software_fallback =
+          video_codec_type_ != webrtc::kVideoCodecH264;
+#else
+      const bool has_software_fallback = true;
+#endif
       // If we hit the absolute limit, then give up.
-      if (pending_buffer_count_ >= kAbsoluteMaxPendingBuffers) {
+      if (has_software_fallback &&
+          pending_buffer_count_ >= kAbsoluteMaxPendingBuffers) {
         has_error_ = true;
         PostCrossThreadTask(
             *media_task_runner_.get(), FROM_HERE,
diff --git a/third_party/blink/renderer/platform/runtime_enabled_features.json5 b/third_party/blink/renderer/platform/runtime_enabled_features.json5
index fc61624..87bdbd85 100644
--- a/third_party/blink/renderer/platform/runtime_enabled_features.json5
+++ b/third_party/blink/renderer/platform/runtime_enabled_features.json5
@@ -1291,6 +1291,9 @@
       status: "stable",
     },
     {
+      name: "MediaSessionWebRTC",
+    },
+    {
       name: "MediaSourceExperimental",
       status: "experimental",
     },
diff --git a/third_party/blink/renderer/platform/webrtc/DEPS b/third_party/blink/renderer/platform/webrtc/DEPS
index 53af1825..d15aba99 100644
--- a/third_party/blink/renderer/platform/webrtc/DEPS
+++ b/third_party/blink/renderer/platform/webrtc/DEPS
@@ -4,11 +4,20 @@
 ]
 
 specific_include_rules = {
+    "webrtc_video_frame_adapter\.cc": [
+        "+base/dcheck_is_on.h",
+        "+gpu/command_buffer/client/raster_interface.h",
+    ],
+    "webrtc_video_frame_adapter\.h": [
+        "+components/viz/common/gpu/raster_context_provider.h",
+        "+media/video/gpu_video_accelerator_factories.h",
+    ],
     "legacy_webrtc_video_frame_adapter\.cc": [
         "+media/video/gpu_video_accelerator_factories.h",
         "+gpu/command_buffer/client/raster_interface.h",
     ],
     "legacy_webrtc_video_frame_adapter\.h": [
         "+components/viz/common/gpu/raster_context_provider.h",
+        "+media/video/gpu_video_accelerator_factories.h",
     ],
 }
diff --git a/third_party/blink/renderer/platform/webrtc/convert_to_webrtc_video_frame_buffer.cc b/third_party/blink/renderer/platform/webrtc/convert_to_webrtc_video_frame_buffer.cc
new file mode 100644
index 0000000..a2ec18b
--- /dev/null
+++ b/third_party/blink/renderer/platform/webrtc/convert_to_webrtc_video_frame_buffer.cc
@@ -0,0 +1,433 @@
+// Copyright 2015 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 "third_party/blink/renderer/platform/webrtc/convert_to_webrtc_video_frame_buffer.h"
+
+#include "base/callback_helpers.h"
+#include "base/containers/span.h"
+#include "base/logging.h"
+#include "base/strings/stringprintf.h"
+#include "media/base/video_util.h"
+#include "third_party/blink/public/common/features.h"
+#include "third_party/blink/public/platform/modules/webrtc/webrtc_logging.h"
+#include "third_party/blink/public/platform/platform.h"
+#include "third_party/blink/renderer/platform/scheduler/public/post_cross_thread_task.h"
+#include "third_party/blink/renderer/platform/scheduler/public/thread.h"
+#include "third_party/blink/renderer/platform/webrtc/webrtc_video_frame_adapter.h"
+#include "third_party/blink/renderer/platform/wtf/cross_thread_functional.h"
+#include "third_party/blink/renderer/platform/wtf/functional.h"
+#include "third_party/libyuv/include/libyuv/convert.h"
+#include "third_party/libyuv/include/libyuv/convert_from_argb.h"
+#include "third_party/libyuv/include/libyuv/scale.h"
+#include "third_party/webrtc/api/video/i420_buffer.h"
+#include "third_party/webrtc/common_video/include/video_frame_buffer.h"
+#include "third_party/webrtc/common_video/libyuv/include/webrtc_libyuv.h"
+#include "third_party/webrtc/rtc_base/ref_counted_object.h"
+#include "ui/gfx/gpu_memory_buffer.h"
+
+namespace {
+
+class I420FrameAdapter : public webrtc::I420BufferInterface {
+ public:
+  explicit I420FrameAdapter(scoped_refptr<media::VideoFrame> frame)
+      : frame_(std::move(frame)) {
+    DCHECK_EQ(frame_->format(), media::PIXEL_FORMAT_I420);
+    DCHECK_EQ(frame_->visible_rect().size(), frame_->natural_size());
+  }
+
+  int width() const override { return frame_->visible_rect().width(); }
+  int height() const override { return frame_->visible_rect().height(); }
+
+  const uint8_t* DataY() const override {
+    return frame_->visible_data(media::VideoFrame::kYPlane);
+  }
+
+  const uint8_t* DataU() const override {
+    return frame_->visible_data(media::VideoFrame::kUPlane);
+  }
+
+  const uint8_t* DataV() const override {
+    return frame_->visible_data(media::VideoFrame::kVPlane);
+  }
+
+  int StrideY() const override {
+    return frame_->stride(media::VideoFrame::kYPlane);
+  }
+
+  int StrideU() const override {
+    return frame_->stride(media::VideoFrame::kUPlane);
+  }
+
+  int StrideV() const override {
+    return frame_->stride(media::VideoFrame::kVPlane);
+  }
+
+ protected:
+  scoped_refptr<media::VideoFrame> frame_;
+};
+
+class I420AFrameAdapter : public webrtc::I420ABufferInterface {
+ public:
+  explicit I420AFrameAdapter(scoped_refptr<media::VideoFrame> frame)
+      : frame_(std::move(frame)) {
+    DCHECK_EQ(frame_->format(), media::PIXEL_FORMAT_I420A);
+    DCHECK_EQ(frame_->visible_rect().size(), frame_->natural_size());
+  }
+
+  int width() const override { return frame_->visible_rect().width(); }
+  int height() const override { return frame_->visible_rect().height(); }
+
+  const uint8_t* DataY() const override {
+    return frame_->visible_data(media::VideoFrame::kYPlane);
+  }
+
+  const uint8_t* DataU() const override {
+    return frame_->visible_data(media::VideoFrame::kUPlane);
+  }
+
+  const uint8_t* DataV() const override {
+    return frame_->visible_data(media::VideoFrame::kVPlane);
+  }
+
+  const uint8_t* DataA() const override {
+    return frame_->visible_data(media::VideoFrame::kAPlane);
+  }
+
+  int StrideY() const override {
+    return frame_->stride(media::VideoFrame::kYPlane);
+  }
+
+  int StrideU() const override {
+    return frame_->stride(media::VideoFrame::kUPlane);
+  }
+
+  int StrideV() const override {
+    return frame_->stride(media::VideoFrame::kVPlane);
+  }
+
+  int StrideA() const override {
+    return frame_->stride(media::VideoFrame::kAPlane);
+  }
+
+ protected:
+  scoped_refptr<media::VideoFrame> frame_;
+};
+
+class NV12FrameAdapter : public webrtc::NV12BufferInterface {
+ public:
+  explicit NV12FrameAdapter(scoped_refptr<media::VideoFrame> frame)
+      : frame_(std::move(frame)) {
+    DCHECK_EQ(frame_->format(), media::PIXEL_FORMAT_NV12);
+    DCHECK_EQ(frame_->visible_rect().size(), frame_->natural_size());
+  }
+
+  int width() const override { return frame_->visible_rect().width(); }
+  int height() const override { return frame_->visible_rect().height(); }
+
+  const uint8_t* DataY() const override {
+    return frame_->visible_data(media::VideoFrame::kYPlane);
+  }
+
+  const uint8_t* DataUV() const override {
+    return frame_->visible_data(media::VideoFrame::kUVPlane);
+  }
+
+  int StrideY() const override {
+    return frame_->stride(media::VideoFrame::kYPlane);
+  }
+
+  int StrideUV() const override {
+    return frame_->stride(media::VideoFrame::kUVPlane);
+  }
+
+  rtc::scoped_refptr<webrtc::I420BufferInterface> ToI420() override {
+    rtc::scoped_refptr<webrtc::I420Buffer> i420_buffer;
+    i420_buffer = webrtc::I420Buffer::Create(width(), height());
+    libyuv::NV12ToI420(DataY(), StrideY(), DataUV(), StrideUV(),
+                       i420_buffer->MutableDataY(), i420_buffer->StrideY(),
+                       i420_buffer->MutableDataU(), i420_buffer->StrideU(),
+                       i420_buffer->MutableDataV(), i420_buffer->StrideV(),
+                       width(), height());
+    return i420_buffer;
+  }
+
+ protected:
+  scoped_refptr<media::VideoFrame> frame_;
+};
+
+rtc::scoped_refptr<webrtc::VideoFrameBuffer> MakeFrameAdapter(
+    scoped_refptr<media::VideoFrame> video_frame) {
+  switch (video_frame->format()) {
+    case media::PIXEL_FORMAT_I420:
+      return new rtc::RefCountedObject<I420FrameAdapter>(
+          std::move(video_frame));
+    case media::PIXEL_FORMAT_I420A:
+      return new rtc::RefCountedObject<I420AFrameAdapter>(
+          std::move(video_frame));
+    case media::PIXEL_FORMAT_NV12:
+      return new rtc::RefCountedObject<NV12FrameAdapter>(
+          std::move(video_frame));
+    default:
+      NOTREACHED();
+      return nullptr;
+  }
+}
+
+scoped_refptr<media::VideoFrame> MakeScaledI420VideoFrame(
+    scoped_refptr<media::VideoFrame> source_frame,
+    scoped_refptr<blink::WebRtcVideoFrameAdapter::SharedResources>
+        shared_resources) {
+  // ARGB pixel format may be produced by readback of texture backed frames.
+  DCHECK(source_frame->format() == media::PIXEL_FORMAT_NV12 ||
+         source_frame->format() == media::PIXEL_FORMAT_I420 ||
+         source_frame->format() == media::PIXEL_FORMAT_I420A ||
+         source_frame->format() == media::PIXEL_FORMAT_ARGB);
+  const bool has_alpha = source_frame->format() == media::PIXEL_FORMAT_I420A;
+  // Convert to I420 and scale to the natural size specified in
+  // |source_frame|.
+  auto dst_frame = shared_resources->CreateFrame(
+      has_alpha ? media::PIXEL_FORMAT_I420A : media::PIXEL_FORMAT_I420,
+      source_frame->natural_size(), gfx::Rect(source_frame->natural_size()),
+      source_frame->natural_size(), source_frame->timestamp());
+  if (!dst_frame) {
+    LOG(ERROR) << "Failed to create I420 frame from pool.";
+    return nullptr;
+  }
+  dst_frame->metadata().MergeMetadataFrom(source_frame->metadata());
+
+  switch (source_frame->format()) {
+    case media::PIXEL_FORMAT_I420A:
+      libyuv::ScalePlane(source_frame->visible_data(media::VideoFrame::kAPlane),
+                         source_frame->stride(media::VideoFrame::kAPlane),
+                         source_frame->visible_rect().width(),
+                         source_frame->visible_rect().height(),
+                         dst_frame->data(media::VideoFrame::kAPlane),
+                         dst_frame->stride(media::VideoFrame::kAPlane),
+                         dst_frame->coded_size().width(),
+                         dst_frame->coded_size().height(),
+                         libyuv::kFilterBilinear);
+      // Fallthrough to I420 in order to scale the YUV planes as well.
+      ABSL_FALLTHROUGH_INTENDED;
+    case media::PIXEL_FORMAT_I420:
+      libyuv::I420Scale(source_frame->visible_data(media::VideoFrame::kYPlane),
+                        source_frame->stride(media::VideoFrame::kYPlane),
+                        source_frame->visible_data(media::VideoFrame::kUPlane),
+                        source_frame->stride(media::VideoFrame::kUPlane),
+                        source_frame->visible_data(media::VideoFrame::kVPlane),
+                        source_frame->stride(media::VideoFrame::kVPlane),
+                        source_frame->visible_rect().width(),
+                        source_frame->visible_rect().height(),
+                        dst_frame->data(media::VideoFrame::kYPlane),
+                        dst_frame->stride(media::VideoFrame::kYPlane),
+                        dst_frame->data(media::VideoFrame::kUPlane),
+                        dst_frame->stride(media::VideoFrame::kUPlane),
+                        dst_frame->data(media::VideoFrame::kVPlane),
+                        dst_frame->stride(media::VideoFrame::kVPlane),
+                        dst_frame->coded_size().width(),
+                        dst_frame->coded_size().height(),
+                        libyuv::kFilterBilinear);
+      break;
+    case media::PIXEL_FORMAT_NV12: {
+      webrtc::NV12ToI420Scaler scaler;
+      scaler.NV12ToI420Scale(
+          source_frame->visible_data(media::VideoFrame::kYPlane),
+          source_frame->stride(media::VideoFrame::kYPlane),
+          source_frame->visible_data(media::VideoFrame::kUVPlane),
+          source_frame->stride(media::VideoFrame::kUVPlane),
+          source_frame->visible_rect().width(),
+          source_frame->visible_rect().height(),
+          dst_frame->data(media::VideoFrame::kYPlane),
+          dst_frame->stride(media::VideoFrame::kYPlane),
+          dst_frame->data(media::VideoFrame::kUPlane),
+          dst_frame->stride(media::VideoFrame::kUPlane),
+          dst_frame->data(media::VideoFrame::kVPlane),
+          dst_frame->stride(media::VideoFrame::kVPlane),
+          dst_frame->coded_size().width(), dst_frame->coded_size().height());
+    } break;
+    case media::PIXEL_FORMAT_ARGB: {
+      auto visible_size = source_frame->visible_rect().size();
+      if (visible_size == dst_frame->coded_size()) {
+        // Direct conversion to dst_frame with no scaling.
+        libyuv::ARGBToI420(
+            source_frame->visible_data(media::VideoFrame::kARGBPlane),
+            source_frame->stride(media::VideoFrame::kARGBPlane),
+            dst_frame->data(media::VideoFrame::kYPlane),
+            dst_frame->stride(media::VideoFrame::kYPlane),
+            dst_frame->data(media::VideoFrame::kUPlane),
+            dst_frame->stride(media::VideoFrame::kUPlane),
+            dst_frame->data(media::VideoFrame::kVPlane),
+            dst_frame->stride(media::VideoFrame::kVPlane), visible_size.width(),
+            visible_size.height());
+      } else {
+        // Convert to I420 tmp image and then scale to the dst_frame.
+        auto tmp_frame = shared_resources->CreateTemporaryFrame(
+            media::PIXEL_FORMAT_I420, visible_size, gfx::Rect(visible_size),
+            visible_size, source_frame->timestamp());
+        libyuv::ARGBToI420(
+            source_frame->visible_data(media::VideoFrame::kARGBPlane),
+            source_frame->stride(media::VideoFrame::kARGBPlane),
+            tmp_frame->data(media::VideoFrame::kYPlane),
+            tmp_frame->stride(media::VideoFrame::kYPlane),
+            tmp_frame->data(media::VideoFrame::kUPlane),
+            tmp_frame->stride(media::VideoFrame::kUPlane),
+            tmp_frame->data(media::VideoFrame::kVPlane),
+            tmp_frame->stride(media::VideoFrame::kVPlane), visible_size.width(),
+            visible_size.height());
+        libyuv::I420Scale(
+            tmp_frame->data(media::VideoFrame::kYPlane),
+            tmp_frame->stride(media::VideoFrame::kYPlane),
+            tmp_frame->data(media::VideoFrame::kUPlane),
+            tmp_frame->stride(media::VideoFrame::kUPlane),
+            tmp_frame->data(media::VideoFrame::kVPlane),
+            tmp_frame->stride(media::VideoFrame::kVPlane), visible_size.width(),
+            visible_size.height(), dst_frame->data(media::VideoFrame::kYPlane),
+            dst_frame->stride(media::VideoFrame::kYPlane),
+            dst_frame->data(media::VideoFrame::kUPlane),
+            dst_frame->stride(media::VideoFrame::kUPlane),
+            dst_frame->data(media::VideoFrame::kVPlane),
+            dst_frame->stride(media::VideoFrame::kVPlane),
+            dst_frame->coded_size().width(), dst_frame->coded_size().height(),
+            libyuv::kFilterBilinear);
+      }
+    } break;
+    default:
+      NOTREACHED();
+  }
+  return dst_frame;
+}
+
+scoped_refptr<media::VideoFrame> MakeScaledNV12VideoFrame(
+    scoped_refptr<media::VideoFrame> source_frame,
+    scoped_refptr<blink::WebRtcVideoFrameAdapter::SharedResources>
+        shared_resources) {
+  DCHECK_EQ(source_frame->format(), media::PIXEL_FORMAT_NV12);
+  auto dst_frame = shared_resources->CreateFrame(
+      media::PIXEL_FORMAT_NV12, source_frame->natural_size(),
+      gfx::Rect(source_frame->natural_size()), source_frame->natural_size(),
+      source_frame->timestamp());
+  dst_frame->metadata().MergeMetadataFrom(source_frame->metadata());
+  const auto& nv12_planes = dst_frame->layout().planes();
+  libyuv::NV12Scale(source_frame->visible_data(media::VideoFrame::kYPlane),
+                    source_frame->stride(media::VideoFrame::kYPlane),
+                    source_frame->visible_data(media::VideoFrame::kUVPlane),
+                    source_frame->stride(media::VideoFrame::kUVPlane),
+                    source_frame->visible_rect().width(),
+                    source_frame->visible_rect().height(),
+                    dst_frame->data(media::VideoFrame::kYPlane),
+                    nv12_planes[media::VideoFrame::kYPlane].stride,
+                    dst_frame->data(media::VideoFrame::kUVPlane),
+                    nv12_planes[media::VideoFrame::kUVPlane].stride,
+                    dst_frame->coded_size().width(),
+                    dst_frame->coded_size().height(), libyuv::kFilterBox);
+  return dst_frame;
+}
+
+scoped_refptr<media::VideoFrame> MaybeConvertAndScaleFrame(
+    scoped_refptr<media::VideoFrame> source_frame,
+    scoped_refptr<blink::WebRtcVideoFrameAdapter::SharedResources>
+        shared_resources) {
+  if (!source_frame)
+    return nullptr;
+  // Texture frames may be readback in ARGB format.
+  RTC_DCHECK(source_frame->format() == media::PIXEL_FORMAT_I420 ||
+             source_frame->format() == media::PIXEL_FORMAT_I420A ||
+             source_frame->format() == media::PIXEL_FORMAT_NV12 ||
+             source_frame->format() == media::PIXEL_FORMAT_ARGB);
+  RTC_DCHECK(shared_resources);
+
+  const bool allow_nv12_output =
+      base::FeatureList::IsEnabled(blink::features::kWebRtcLibvpxEncodeNV12);
+  const bool source_is_i420 =
+      source_frame->format() == media::PIXEL_FORMAT_I420 ||
+      source_frame->format() == media::PIXEL_FORMAT_I420A;
+  const bool source_is_nv12 =
+      source_frame->format() == media::PIXEL_FORMAT_NV12;
+  const bool no_scaling_needed =
+      source_frame->natural_size() == source_frame->visible_rect().size();
+
+  if (((source_is_nv12 && allow_nv12_output) || source_is_i420) &&
+      no_scaling_needed) {
+    // |source_frame| already has correct pixel format and resolution.
+    return source_frame;
+  } else if (source_is_nv12 && allow_nv12_output) {
+    // Output NV12 only if it is allowed and no conversion is needed.
+    return MakeScaledNV12VideoFrame(std::move(source_frame),
+                                    std::move(shared_resources));
+  } else {
+    return MakeScaledI420VideoFrame(std::move(source_frame),
+                                    std::move(shared_resources));
+  }
+}
+
+}  // anonymous namespace
+
+namespace blink {
+
+// static
+bool CanConvertToWebRtcVideoFrameBuffer(const media::VideoFrame* frame) {
+  // Currently accept I420, I420A, NV12 formats in a mapped frame,
+  // or a texture or GPU memory buffer frame.
+  return (frame->IsMappable() &&
+          base::Contains(GetPixelFormatsMappableToWebRtcVideoFrameBuffer(),
+                         frame->format())) ||
+         frame->storage_type() ==
+             media::VideoFrame::STORAGE_GPU_MEMORY_BUFFER ||
+         frame->HasTextures();
+}
+
+// static
+base::span<const media::VideoPixelFormat>
+GetPixelFormatsMappableToWebRtcVideoFrameBuffer() {
+  static constexpr const media::VideoPixelFormat
+      kGetPixelFormatsMappableToWebRtcVideoFrameBuffer[] = {
+          media::PIXEL_FORMAT_I420, media::PIXEL_FORMAT_I420A,
+          media::PIXEL_FORMAT_NV12};
+  return base::make_span(kGetPixelFormatsMappableToWebRtcVideoFrameBuffer);
+}
+
+rtc::scoped_refptr<webrtc::VideoFrameBuffer> ConvertToWebRtcVideoFrameBuffer(
+    scoped_refptr<media::VideoFrame> video_frame,
+    scoped_refptr<WebRtcVideoFrameAdapter::SharedResources> shared_resources) {
+  DCHECK(CanConvertToWebRtcVideoFrameBuffer(video_frame.get()))
+      << "Can not create WebRTC frame buffer for frame "
+      << video_frame->AsHumanReadableString();
+
+  if (video_frame->storage_type() ==
+      media::VideoFrame::StorageType::STORAGE_GPU_MEMORY_BUFFER) {
+    auto converted_frame =
+        shared_resources
+            ? shared_resources->ConstructVideoFrameFromGpu(video_frame)
+            : nullptr;
+    converted_frame =
+        MaybeConvertAndScaleFrame(converted_frame, shared_resources);
+    if (!converted_frame) {
+      return MakeFrameAdapter(media::VideoFrame::CreateColorFrame(
+          video_frame->natural_size(), 0u, 0x80, 0x80,
+          video_frame->timestamp()));
+    }
+    return MakeFrameAdapter(std::move(converted_frame));
+  } else if (video_frame->HasTextures()) {
+    auto converted_frame =
+        shared_resources
+            ? shared_resources->ConstructVideoFrameFromTexture(video_frame)
+            : nullptr;
+    converted_frame =
+        MaybeConvertAndScaleFrame(converted_frame, shared_resources);
+    if (!converted_frame) {
+      DLOG(ERROR) << "Texture backed frame cannot be accessed.";
+      return MakeFrameAdapter(media::VideoFrame::CreateColorFrame(
+          video_frame->natural_size(), 0u, 0x80, 0x80,
+          video_frame->timestamp()));
+    }
+    return MakeFrameAdapter(std::move(converted_frame));
+  }
+
+  // Since scaling is required, hard-apply both the cropping and scaling
+  // before we hand the frame over to WebRTC.
+  scoped_refptr<media::VideoFrame> scaled_frame =
+      MaybeConvertAndScaleFrame(video_frame, shared_resources);
+  return MakeFrameAdapter(std::move(scaled_frame));
+}
+
+}  // namespace blink
diff --git a/third_party/blink/renderer/platform/webrtc/convert_to_webrtc_video_frame_buffer.h b/third_party/blink/renderer/platform/webrtc/convert_to_webrtc_video_frame_buffer.h
new file mode 100644
index 0000000..9c421dee
--- /dev/null
+++ b/third_party/blink/renderer/platform/webrtc/convert_to_webrtc_video_frame_buffer.h
@@ -0,0 +1,30 @@
+// Copyright 2015 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 THIRD_PARTY_BLINK_RENDERER_PLATFORM_WEBRTC_CONVERT_TO_WEBRTC_VIDEO_FRAME_BUFFER_H_
+#define THIRD_PARTY_BLINK_RENDERER_PLATFORM_WEBRTC_CONVERT_TO_WEBRTC_VIDEO_FRAME_BUFFER_H_
+
+#include "base/containers/span.h"
+#include "base/memory/ref_counted.h"
+#include "media/base/video_frame.h"
+#include "third_party/blink/renderer/platform/platform_export.h"
+#include "third_party/blink/renderer/platform/webrtc/webrtc_video_frame_adapter.h"
+#include "third_party/webrtc/api/video/video_frame_buffer.h"
+
+namespace blink {
+
+PLATFORM_EXPORT bool CanConvertToWebRtcVideoFrameBuffer(
+    const media::VideoFrame* frame);
+
+PLATFORM_EXPORT base::span<const media::VideoPixelFormat>
+GetPixelFormatsMappableToWebRtcVideoFrameBuffer();
+
+PLATFORM_EXPORT rtc::scoped_refptr<webrtc::VideoFrameBuffer>
+ConvertToWebRtcVideoFrameBuffer(
+    scoped_refptr<media::VideoFrame> video_frame,
+    scoped_refptr<WebRtcVideoFrameAdapter::SharedResources> shared_resources);
+
+}  // namespace blink
+
+#endif  // THIRD_PARTY_BLINK_RENDERER_PLATFORM_WEBRTC_CONVERT_TO_WEBRTC_VIDEO_FRAME_BUFFER_H_
diff --git a/third_party/blink/renderer/platform/webrtc/convert_to_webrtc_video_frame_buffer_test.cc b/third_party/blink/renderer/platform/webrtc/convert_to_webrtc_video_frame_buffer_test.cc
new file mode 100644
index 0000000..789be57
--- /dev/null
+++ b/third_party/blink/renderer/platform/webrtc/convert_to_webrtc_video_frame_buffer_test.cc
@@ -0,0 +1,335 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/platform/webrtc/convert_to_webrtc_video_frame_buffer.h"
+
+#include "base/strings/strcat.h"
+#include "base/test/scoped_feature_list.h"
+#include "media/base/video_frame.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/common/features.h"
+#include "third_party/blink/renderer/platform/testing/video_frame_utils.h"
+#include "third_party/blink/renderer/platform/webrtc/testing/mock_webrtc_video_frame_adapter_shared_resources.h"
+#include "third_party/blink/renderer/platform/webrtc/webrtc_video_frame_adapter.h"
+#include "third_party/webrtc/api/video/video_frame_buffer.h"
+#include "third_party/webrtc/rtc_base/ref_counted_object.h"
+#include "ui/gfx/gpu_memory_buffer.h"
+
+using ::testing::_;
+using ::testing::Return;
+
+namespace blink {
+
+class ConvertToWebRtcVideoFrameBufferParamTest
+    : public ::testing::TestWithParam<
+          std::tuple<media::VideoFrame::StorageType, media::VideoPixelFormat>> {
+ public:
+  ConvertToWebRtcVideoFrameBufferParamTest()
+      : resources_(new WebRtcVideoFrameAdapter::SharedResources(nullptr)) {}
+
+ protected:
+  scoped_refptr<WebRtcVideoFrameAdapter::SharedResources> resources_;
+};
+
+namespace {
+std::vector<ConvertToWebRtcVideoFrameBufferParamTest::ParamType> TestParams() {
+  std::vector<ConvertToWebRtcVideoFrameBufferParamTest::ParamType> test_params;
+  // All formats for owned memory.
+  for (media::VideoPixelFormat format :
+       GetPixelFormatsMappableToWebRtcVideoFrameBuffer()) {
+    test_params.emplace_back(
+        media::VideoFrame::StorageType::STORAGE_OWNED_MEMORY, format);
+  }
+  test_params.emplace_back(
+      media::VideoFrame::StorageType::STORAGE_GPU_MEMORY_BUFFER,
+      media::VideoPixelFormat::PIXEL_FORMAT_NV12);
+  return test_params;
+}
+}  // namespace
+
+TEST_P(ConvertToWebRtcVideoFrameBufferParamTest, ToI420) {
+  const gfx::Size kCodedSize(1280, 960);
+  const gfx::Rect kVisibleRect(0, 120, 1280, 720);
+  const gfx::Size kNaturalSize(640, 360);
+
+  media::VideoFrame::StorageType storage_type = std::get<0>(GetParam());
+  media::VideoPixelFormat pixel_format = std::get<1>(GetParam());
+  scoped_refptr<media::VideoFrame> frame = CreateTestFrame(
+      kCodedSize, kVisibleRect, kNaturalSize, storage_type, pixel_format);
+  rtc::scoped_refptr<webrtc::VideoFrameBuffer> frame_buffer =
+      ConvertToWebRtcVideoFrameBuffer(std::move(frame), resources_);
+
+  // The I420 frame should have the same size as the natural size.
+  auto i420_frame = frame_buffer->ToI420();
+  EXPECT_EQ(i420_frame->width(), kNaturalSize.width());
+  EXPECT_EQ(i420_frame->height(), kNaturalSize.height());
+}
+
+INSTANTIATE_TEST_CASE_P(
+    ConvertToWebRtcVideoFrameBufferParamTest,
+    ConvertToWebRtcVideoFrameBufferParamTest,
+    ::testing::ValuesIn(TestParams()),
+    [](const auto& info) {
+      return base::StrCat(
+          {media::VideoFrame::StorageTypeToString(std::get<0>(info.param)), "_",
+           media::VideoPixelFormatToString(std::get<1>(info.param))});
+    });
+
+TEST(ConvertToWebRtcVideoFrameBufferTest, ToI420DownScaleGmb) {
+  base::test::ScopedFeatureList scoped_feautre_list;
+  scoped_feautre_list.InitAndDisableFeature(
+      blink::features::kWebRtcLibvpxEncodeNV12);
+  const gfx::Size kCodedSize(1280, 960);
+  const gfx::Rect kVisibleRect(0, 120, 1280, 720);
+  const gfx::Size kNaturalSize(640, 360);
+  scoped_refptr<WebRtcVideoFrameAdapter::SharedResources> resources =
+      new WebRtcVideoFrameAdapter::SharedResources(nullptr);
+  auto gmb_frame =
+      CreateTestFrame(kCodedSize, kVisibleRect, kNaturalSize,
+                      media::VideoFrame::STORAGE_GPU_MEMORY_BUFFER);
+
+  // The adapter should report width and height from the natural size for
+  // VideoFrame backed by GpuMemoryBuffer.
+  rtc::scoped_refptr<webrtc::VideoFrameBuffer> gmb_frame_buffer =
+      ConvertToWebRtcVideoFrameBuffer(std::move(gmb_frame), resources);
+  EXPECT_EQ(gmb_frame_buffer->width(), kNaturalSize.width());
+  EXPECT_EQ(gmb_frame_buffer->height(), kNaturalSize.height());
+
+  // The I420 frame should have the same size as the natural size
+  auto i420_frame = gmb_frame_buffer->ToI420();
+  ASSERT_TRUE(i420_frame);
+  EXPECT_EQ(i420_frame->width(), kNaturalSize.width());
+  EXPECT_EQ(i420_frame->height(), kNaturalSize.height());
+  auto* get_i420_frame = gmb_frame_buffer->GetI420();
+  ASSERT_TRUE(get_i420_frame);
+  EXPECT_EQ(get_i420_frame->width(), kNaturalSize.width());
+  EXPECT_EQ(get_i420_frame->height(), kNaturalSize.height());
+}
+
+TEST(ConvertToWebRtcVideoFrameBufferTest, ToI420ADownScale) {
+  const gfx::Size kCodedSize(1280, 960);
+  const gfx::Rect kVisibleRect(0, 120, 1280, 720);
+  const gfx::Size kNaturalSize(640, 360);
+  scoped_refptr<WebRtcVideoFrameAdapter::SharedResources> resources =
+      new WebRtcVideoFrameAdapter::SharedResources(nullptr);
+
+  // The adapter should report width and height from the natural size for
+  // VideoFrame backed by owned memory.
+  auto owned_memory_frame =
+      CreateTestFrame(kCodedSize, kVisibleRect, kNaturalSize,
+                      media::VideoFrame::STORAGE_OWNED_MEMORY,
+                      media::VideoPixelFormat::PIXEL_FORMAT_I420A);
+  rtc::scoped_refptr<webrtc::VideoFrameBuffer> owned_memory_frame_buffer =
+      ConvertToWebRtcVideoFrameBuffer(std::move(owned_memory_frame), resources);
+  EXPECT_EQ(owned_memory_frame_buffer->width(), kNaturalSize.width());
+  EXPECT_EQ(owned_memory_frame_buffer->height(), kNaturalSize.height());
+
+  // The I420A frame should have the same size as the natural size
+  auto i420a_frame = owned_memory_frame_buffer->ToI420();
+  ASSERT_TRUE(i420a_frame);
+  EXPECT_EQ(webrtc::VideoFrameBuffer::Type::kI420A, i420a_frame->type());
+  EXPECT_EQ(i420a_frame->width(), kNaturalSize.width());
+  EXPECT_EQ(i420a_frame->height(), kNaturalSize.height());
+}
+
+TEST(ConvertToWebRtcVideoFrameBufferTest,
+     Nv12WrapsGmbWhenNoScalingNeeededWithFeature) {
+  base::test::ScopedFeatureList scoped_feautre_list;
+  scoped_feautre_list.InitAndEnableFeature(
+      blink::features::kWebRtcLibvpxEncodeNV12);
+  const gfx::Size kCodedSize(1280, 960);
+  const gfx::Rect kVisibleRect(0, 120, 1280, 720);
+  // Same size as visible rect so no scaling.
+  const gfx::Size kNaturalSize = kVisibleRect.size();
+  scoped_refptr<WebRtcVideoFrameAdapter::SharedResources> resources =
+      new WebRtcVideoFrameAdapter::SharedResources(nullptr);
+
+  auto gmb_frame =
+      CreateTestFrame(kCodedSize, kVisibleRect, kNaturalSize,
+                      media::VideoFrame::STORAGE_GPU_MEMORY_BUFFER);
+
+  // The adapter should report width and height from the natural size for
+  // VideoFrame backed by GpuMemoryBuffer.
+  rtc::scoped_refptr<webrtc::VideoFrameBuffer> gmb_frame_buffer =
+      ConvertToWebRtcVideoFrameBuffer(std::move(gmb_frame), resources);
+  EXPECT_EQ(gmb_frame_buffer->width(), kNaturalSize.width());
+  EXPECT_EQ(gmb_frame_buffer->height(), kNaturalSize.height());
+
+  // Under feature, expect that the adapted frame is NV12 with frame should
+  // have the same size as the natural size.
+  auto* nv12_frame = gmb_frame_buffer->GetNV12();
+  ASSERT_TRUE(nv12_frame);
+  EXPECT_EQ(webrtc::VideoFrameBuffer::Type::kNV12, nv12_frame->type());
+  EXPECT_EQ(nv12_frame->width(), kNaturalSize.width());
+  EXPECT_EQ(nv12_frame->height(), kNaturalSize.height());
+
+  // Even though we have an NV12 frame, ToI420 should return an I420 frame.
+  EXPECT_FALSE(gmb_frame_buffer->GetI420());
+  auto i420_frame = gmb_frame_buffer->ToI420();
+  ASSERT_TRUE(i420_frame);
+  EXPECT_EQ(i420_frame->width(), kNaturalSize.width());
+  EXPECT_EQ(i420_frame->height(), kNaturalSize.height());
+}
+
+TEST(ConvertToWebRtcVideoFrameBufferTest, Nv12ScalesGmbWithFeature) {
+  base::test::ScopedFeatureList scoped_feautre_list;
+  scoped_feautre_list.InitAndEnableFeature(
+      blink::features::kWebRtcLibvpxEncodeNV12);
+  const gfx::Size kCodedSize(1280, 960);
+  const gfx::Rect kVisibleRect(0, 120, 1280, 720);
+  const gfx::Size kNaturalSize(640, 360);
+  scoped_refptr<WebRtcVideoFrameAdapter::SharedResources> resources =
+      new WebRtcVideoFrameAdapter::SharedResources(nullptr);
+
+  auto gmb_frame =
+      CreateTestFrame(kCodedSize, kVisibleRect, kNaturalSize,
+                      media::VideoFrame::STORAGE_GPU_MEMORY_BUFFER);
+
+  // The adapter should report width and height from the natural size for
+  // VideoFrame backed by GpuMemoryBuffer.
+  rtc::scoped_refptr<webrtc::VideoFrameBuffer> gmb_frame_buffer =
+      ConvertToWebRtcVideoFrameBuffer(gmb_frame, resources);
+  EXPECT_EQ(gmb_frame_buffer->width(), kNaturalSize.width());
+  EXPECT_EQ(gmb_frame_buffer->height(), kNaturalSize.height());
+
+  // Under feature, expect that the adapted frame is NV12 with frame should
+  // have the same size as the natural size.
+  auto* nv12_frame = gmb_frame_buffer->GetNV12();
+  ASSERT_TRUE(nv12_frame);
+  EXPECT_EQ(webrtc::VideoFrameBuffer::Type::kNV12, nv12_frame->type());
+  EXPECT_EQ(nv12_frame->width(), kNaturalSize.width());
+  EXPECT_EQ(nv12_frame->height(), kNaturalSize.height());
+
+  // Even though we have an NV12 frame, ToI420 should return an I420 frame.
+  EXPECT_FALSE(gmb_frame_buffer->GetI420());
+  auto i420_frame = gmb_frame_buffer->ToI420();
+  ASSERT_TRUE(i420_frame);
+  EXPECT_EQ(i420_frame->width(), kNaturalSize.width());
+  EXPECT_EQ(i420_frame->height(), kNaturalSize.height());
+}
+
+TEST(ConvertToWebRtcVideoFrameBufferTest, Nv12OwnedMemoryFrame) {
+  const gfx::Size kCodedSize(1280, 960);
+  const gfx::Rect kVisibleRect(0, 120, 1280, 720);
+  const gfx::Size kNaturalSize = kVisibleRect.size();
+  scoped_refptr<WebRtcVideoFrameAdapter::SharedResources> resources =
+      new WebRtcVideoFrameAdapter::SharedResources(nullptr);
+
+  // The adapter should report width and height from the natural size for
+  // VideoFrame backed by owned memory.
+  auto owned_memory_frame =
+      CreateTestFrame(kCodedSize, kVisibleRect, kNaturalSize,
+                      media::VideoFrame::STORAGE_OWNED_MEMORY,
+                      media::VideoPixelFormat::PIXEL_FORMAT_NV12);
+  rtc::scoped_refptr<webrtc::VideoFrameBuffer> owned_memory_frame_buffer =
+      ConvertToWebRtcVideoFrameBuffer(std::move(owned_memory_frame), resources);
+  EXPECT_EQ(owned_memory_frame_buffer->width(), kNaturalSize.width());
+  EXPECT_EQ(owned_memory_frame_buffer->height(), kNaturalSize.height());
+
+  // The NV12 frame should have the same size as the visible rect size
+  auto* nv12_frame = owned_memory_frame_buffer->GetNV12();
+  ASSERT_TRUE(nv12_frame);
+  EXPECT_EQ(webrtc::VideoFrameBuffer::Type::kNV12, nv12_frame->type());
+  EXPECT_EQ(nv12_frame->width(), kVisibleRect.size().width());
+  EXPECT_EQ(nv12_frame->height(), kVisibleRect.size().height());
+}
+
+TEST(ConvertToWebRtcVideoFrameBufferTest, Nv12ScaleOwnedMemoryFrame) {
+  const gfx::Size kCodedSize(1280, 960);
+  const gfx::Rect kVisibleRect(0, 120, 1280, 720);
+  const gfx::Size kNaturalSize(640, 360);
+  scoped_refptr<WebRtcVideoFrameAdapter::SharedResources> resources =
+      new WebRtcVideoFrameAdapter::SharedResources(nullptr);
+
+  // The adapter should report width and height from the natural size for
+  // VideoFrame backed by owned memory.
+  auto owned_memory_frame =
+      CreateTestFrame(kCodedSize, kVisibleRect, kNaturalSize,
+                      media::VideoFrame::STORAGE_OWNED_MEMORY,
+                      media::VideoPixelFormat::PIXEL_FORMAT_NV12);
+  rtc::scoped_refptr<webrtc::VideoFrameBuffer> owned_memory_frame_buffer =
+      ConvertToWebRtcVideoFrameBuffer(std::move(owned_memory_frame), resources);
+  EXPECT_EQ(owned_memory_frame_buffer->width(), kNaturalSize.width());
+  EXPECT_EQ(owned_memory_frame_buffer->height(), kNaturalSize.height());
+
+  // The NV12 frame should have the same size as the natural size.
+  auto* nv12_frame = owned_memory_frame_buffer->GetNV12();
+  ASSERT_TRUE(nv12_frame);
+  EXPECT_EQ(webrtc::VideoFrameBuffer::Type::kNV12, nv12_frame->type());
+  EXPECT_EQ(nv12_frame->width(), kNaturalSize.width());
+  EXPECT_EQ(nv12_frame->height(), kNaturalSize.height());
+}
+
+TEST(ConvertToWebRtcVideoFrameBufferTest,
+     TextureFrameIsBlackWithNoSharedResources) {
+  const gfx::Size kCodedSize(1280, 960);
+  const gfx::Rect kVisibleRect(0, 120, 1280, 720);
+  const gfx::Size kNaturalSize(640, 360);
+
+  // The adapter should report width and height from the natural size for
+  // VideoFrame backed by owned memory.
+  auto owned_memory_frame = CreateTestFrame(
+      kCodedSize, kVisibleRect, kNaturalSize, media::VideoFrame::STORAGE_OPAQUE,
+      media::VideoPixelFormat::PIXEL_FORMAT_NV12);
+  rtc::scoped_refptr<webrtc::VideoFrameBuffer> frame_buffer =
+      ConvertToWebRtcVideoFrameBuffer(std::move(owned_memory_frame), nullptr);
+  EXPECT_EQ(frame_buffer->width(), kNaturalSize.width());
+  EXPECT_EQ(frame_buffer->height(), kNaturalSize.height());
+
+  // The NV12 frame should have the same size as the natural size, but be black
+  // since we can't handle the texture with no shared resources.
+  auto i420_frame = frame_buffer->ToI420();
+  ASSERT_TRUE(i420_frame);
+  EXPECT_EQ(i420_frame->width(), kNaturalSize.width());
+  EXPECT_EQ(i420_frame->height(), kNaturalSize.height());
+  EXPECT_EQ(0x0, i420_frame->DataY()[0]);
+  EXPECT_EQ(0x80, i420_frame->DataU()[0]);
+  EXPECT_EQ(0x80, i420_frame->DataV()[0]);
+}
+
+TEST(ConvertToWebRtcVideoFrameBufferTest,
+     ConvertsTextureFrameWithSharedResources) {
+  const gfx::Size kCodedSize(1280, 960);
+  const gfx::Rect kVisibleRect(0, 120, 1280, 720);
+  const gfx::Size kNaturalSize(640, 360);
+
+  scoped_refptr<MockSharedResources> resources =
+      base::MakeRefCounted<MockSharedResources>();
+
+  // The adapter should report width and height from the natural size for
+  // VideoFrame backed by owned memory.
+  auto owned_memory_frame = CreateTestFrame(
+      kCodedSize, kVisibleRect, kNaturalSize, media::VideoFrame::STORAGE_OPAQUE,
+      media::VideoPixelFormat::PIXEL_FORMAT_NV12);
+
+  scoped_refptr<media::VideoFrame> memory_frame =
+      CreateTestFrame(kCodedSize, kVisibleRect, kNaturalSize,
+                      media::VideoFrame::STORAGE_OWNED_MEMORY,
+                      media::VideoPixelFormat::PIXEL_FORMAT_ARGB);
+  // fill mock image with whilte color.
+  memset(memory_frame->data(media::VideoFrame::kARGBPlane), 0xFF,
+         kCodedSize.GetArea() * 4);
+
+  // Should call texture conversion.
+  resources->ExpectCreateFrameWithRealImplementation();
+  resources->ExpectCreateTemporaryFrameWithRealImplementation();
+  EXPECT_CALL(*resources, ConstructVideoFrameFromTexture(_))
+      .WillOnce(Return(memory_frame));
+
+  rtc::scoped_refptr<webrtc::VideoFrameBuffer> frame_buffer =
+      ConvertToWebRtcVideoFrameBuffer(std::move(owned_memory_frame), resources);
+  EXPECT_EQ(frame_buffer->width(), kNaturalSize.width());
+  EXPECT_EQ(frame_buffer->height(), kNaturalSize.height());
+
+  // The NV12 frame should have the same size as the natural size, but be black
+  // since we can't handle the texture with no shared resources.
+  auto i420_frame = frame_buffer->ToI420();
+  ASSERT_TRUE(i420_frame);
+  EXPECT_EQ(i420_frame->width(), kNaturalSize.width());
+  EXPECT_EQ(i420_frame->height(), kNaturalSize.height());
+  // Returned memory frame should not be replaced by a black frame.
+  EXPECT_NE(0x0, i420_frame->DataY()[0]);
+}
+}  // namespace blink
diff --git a/third_party/blink/renderer/platform/webrtc/legacy_webrtc_video_frame_adapter_test.cc b/third_party/blink/renderer/platform/webrtc/legacy_webrtc_video_frame_adapter_test.cc
index c747960..10ffd6a 100644
--- a/third_party/blink/renderer/platform/webrtc/legacy_webrtc_video_frame_adapter_test.cc
+++ b/third_party/blink/renderer/platform/webrtc/legacy_webrtc_video_frame_adapter_test.cc
@@ -331,8 +331,8 @@
   const gfx::Rect kVisibleRect(0, 120, 1280, 720);
   const gfx::Size kNaturalSize(640, 360);
 
-  scoped_refptr<MockSharedResources> resources =
-      base::MakeRefCounted<MockSharedResources>();
+  scoped_refptr<MockLegacySharedResources> resources =
+      base::MakeRefCounted<MockLegacySharedResources>();
 
   // The adapter should report width and height from the natural size for
   // VideoFrame backed by owned memory.
diff --git a/third_party/blink/renderer/platform/webrtc/testing/mock_webrtc_video_frame_adapter_shared_resources.h b/third_party/blink/renderer/platform/webrtc/testing/mock_webrtc_video_frame_adapter_shared_resources.h
index d637106..58652f1 100644
--- a/third_party/blink/renderer/platform/webrtc/testing/mock_webrtc_video_frame_adapter_shared_resources.h
+++ b/third_party/blink/renderer/platform/webrtc/testing/mock_webrtc_video_frame_adapter_shared_resources.h
@@ -6,15 +6,75 @@
 #define THIRD_PARTY_BLINK_RENDERER_PLATFORM_WEBRTC_TESTING_MOCK_WEBRTC_VIDEO_FRAME_ADAPTER_SHARED_RESOURCES_H_
 
 #include "third_party/blink/renderer/platform/webrtc/legacy_webrtc_video_frame_adapter.h"
+#include "third_party/blink/renderer/platform/webrtc/webrtc_video_frame_adapter.h"
 
 #include "testing/gmock/include/gmock/gmock.h"
 
 namespace blink {
 
-class MockSharedResources
+class MockSharedResources : public WebRtcVideoFrameAdapter::SharedResources {
+ public:
+  MockSharedResources() : WebRtcVideoFrameAdapter::SharedResources(nullptr) {}
+
+  MOCK_METHOD(scoped_refptr<media::VideoFrame>,
+              CreateFrame,
+              (media::VideoPixelFormat format,
+               const gfx::Size& coded_size,
+               const gfx::Rect& visible_rect,
+               const gfx::Size& natural_size,
+               base::TimeDelta timestamp));
+
+  MOCK_METHOD(scoped_refptr<media::VideoFrame>,
+              CreateTemporaryFrame,
+              (media::VideoPixelFormat format,
+               const gfx::Size& coded_size,
+               const gfx::Rect& visible_rect,
+               const gfx::Size& natural_size,
+               base::TimeDelta timestamp));
+
+  MOCK_METHOD(scoped_refptr<viz::RasterContextProvider>,
+              GetRasterContextProvider,
+              ());
+
+  MOCK_METHOD(scoped_refptr<media::VideoFrame>,
+              ConstructVideoFrameFromTexture,
+              (scoped_refptr<media::VideoFrame> source_frame));
+
+  MOCK_METHOD(scoped_refptr<media::VideoFrame>,
+              ConstructVideoFrameFromGpu,
+              (scoped_refptr<media::VideoFrame> source_frame));
+
+  void ExpectCreateFrameWithRealImplementation() {
+    EXPECT_CALL(*this, CreateFrame)
+        .WillOnce(testing::Invoke(
+            [this](media::VideoPixelFormat format, const gfx::Size& coded_size,
+                   const gfx::Rect& visible_rect, const gfx::Size& natural_size,
+                   base::TimeDelta timestamp) {
+              return WebRtcVideoFrameAdapter::SharedResources::CreateFrame(
+                  format, coded_size, visible_rect, natural_size, timestamp);
+            }));
+  }
+
+  void ExpectCreateTemporaryFrameWithRealImplementation() {
+    EXPECT_CALL(*this, CreateTemporaryFrame)
+        .WillOnce(testing::Invoke([this](media::VideoPixelFormat format,
+                                         const gfx::Size& coded_size,
+                                         const gfx::Rect& visible_rect,
+                                         const gfx::Size& natural_size,
+                                         base::TimeDelta timestamp) {
+          return WebRtcVideoFrameAdapter::SharedResources::CreateTemporaryFrame(
+              format, coded_size, visible_rect, natural_size, timestamp);
+        }));
+  }
+
+ private:
+  friend class base::RefCountedThreadSafe<MockSharedResources>;
+};
+
+class MockLegacySharedResources
     : public LegacyWebRtcVideoFrameAdapter::SharedResources {
  public:
-  MockSharedResources()
+  MockLegacySharedResources()
       : LegacyWebRtcVideoFrameAdapter::SharedResources(nullptr) {}
 
   MOCK_METHOD(scoped_refptr<media::VideoFrame>,
@@ -70,7 +130,7 @@
   }
 
  private:
-  friend class base::RefCountedThreadSafe<MockSharedResources>;
+  friend class base::RefCountedThreadSafe<MockLegacySharedResources>;
 };
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/platform/webrtc/webrtc_video_frame_adapter.cc b/third_party/blink/renderer/platform/webrtc/webrtc_video_frame_adapter.cc
new file mode 100644
index 0000000..4388412
--- /dev/null
+++ b/third_party/blink/renderer/platform/webrtc/webrtc_video_frame_adapter.cc
@@ -0,0 +1,356 @@
+// 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 "third_party/blink/renderer/platform/webrtc/webrtc_video_frame_adapter.h"
+
+#include <cmath>
+
+#include "base/dcheck_is_on.h"
+#include "base/synchronization/waitable_event.h"
+#include "base/threading/thread_restrictions.h"
+#include "gpu/command_buffer/client/raster_interface.h"
+#include "media/base/video_util.h"
+#include "third_party/blink/public/platform/platform.h"
+#include "third_party/blink/renderer/platform/scheduler/public/post_cross_thread_task.h"
+#include "third_party/blink/renderer/platform/scheduler/public/thread.h"
+#include "third_party/blink/renderer/platform/webrtc/convert_to_webrtc_video_frame_buffer.h"
+#include "third_party/blink/renderer/platform/wtf/cross_thread_functional.h"
+#include "third_party/webrtc/rtc_base/ref_counted_object.h"
+
+namespace blink {
+
+namespace {
+
+bool IsApproxEquals(int a, int b) {
+  return std::abs(a - b) <= 4;
+}
+
+bool IsApproxEquals(const gfx::Rect& a, const gfx::Rect& b) {
+  return IsApproxEquals(a.x(), b.x()) && IsApproxEquals(a.y(), b.y()) &&
+         IsApproxEquals(a.width(), b.width()) &&
+         IsApproxEquals(a.height(), b.height());
+}
+
+static void CreateContextProviderOnMainThread(
+    scoped_refptr<viz::RasterContextProvider>* result,
+    base::WaitableEvent* waitable_event) {
+  *result = blink::Platform::Current()->SharedCompositorWorkerContextProvider();
+  waitable_event->Signal();
+}
+
+}  // namespace
+
+scoped_refptr<media::VideoFrame>
+WebRtcVideoFrameAdapter::SharedResources::CreateFrame(
+    media::VideoPixelFormat format,
+    const gfx::Size& coded_size,
+    const gfx::Rect& visible_rect,
+    const gfx::Size& natural_size,
+    base::TimeDelta timestamp) {
+  return pool_.CreateFrame(format, coded_size, visible_rect, natural_size,
+                           timestamp);
+}
+
+scoped_refptr<media::VideoFrame>
+WebRtcVideoFrameAdapter::SharedResources::CreateTemporaryFrame(
+    media::VideoPixelFormat format,
+    const gfx::Size& coded_size,
+    const gfx::Rect& visible_rect,
+    const gfx::Size& natural_size,
+    base::TimeDelta timestamp) {
+  return pool_for_tmp_frames_.CreateFrame(format, coded_size, visible_rect,
+                                          natural_size, timestamp);
+}
+
+scoped_refptr<viz::RasterContextProvider>
+WebRtcVideoFrameAdapter::SharedResources::GetRasterContextProvider() {
+  base::AutoLock auto_lock(context_provider_lock_);
+  if (raster_context_provider_) {
+    // Reuse created context provider if it's alive.
+    viz::RasterContextProvider::ScopedRasterContextLock lock(
+        raster_context_provider_.get());
+    if (lock.RasterInterface()->GetGraphicsResetStatusKHR() == GL_NO_ERROR)
+      return raster_context_provider_;
+  }
+
+  // Recreate the context provider.
+  base::WaitableEvent waitable_event;
+  PostCrossThreadTask(
+      *Thread::MainThread()->GetTaskRunner(), FROM_HERE,
+      CrossThreadBindOnce(&CreateContextProviderOnMainThread,
+                          CrossThreadUnretained(&raster_context_provider_),
+                          CrossThreadUnretained(&waitable_event)));
+
+  // This wait is necessary because this task is completed via main thread
+  // asynchronously but WebRTC API is synchronous.
+  base::ScopedAllowBaseSyncPrimitivesOutsideBlockingScope allow_wait;
+  waitable_event.Wait();
+
+  return raster_context_provider_;
+}
+
+scoped_refptr<media::VideoFrame>
+WebRtcVideoFrameAdapter::SharedResources::ConstructVideoFrameFromTexture(
+    scoped_refptr<media::VideoFrame> source_frame) {
+  RTC_DCHECK(source_frame->HasTextures());
+
+  scoped_refptr<viz::RasterContextProvider> raster_context_provider =
+      GetRasterContextProvider();
+  if (!raster_context_provider) {
+    return nullptr;
+  }
+  viz::RasterContextProvider::ScopedRasterContextLock scoped_context(
+      raster_context_provider.get());
+
+  auto* ri = scoped_context.RasterInterface();
+  auto* gr_context = raster_context_provider->GrContext();
+
+  if (!ri) {
+    return nullptr;
+  }
+
+  return media::ReadbackTextureBackedFrameToMemorySync(
+      *source_frame, ri, gr_context, &pool_for_mapped_frames_);
+}
+
+scoped_refptr<media::VideoFrame>
+WebRtcVideoFrameAdapter::SharedResources::ConstructVideoFrameFromGpu(
+    scoped_refptr<media::VideoFrame> source_frame) {
+  CHECK(source_frame);
+  // NV12 is the only supported format.
+  DCHECK_EQ(source_frame->format(), media::PIXEL_FORMAT_NV12);
+  DCHECK_EQ(source_frame->storage_type(),
+            media::VideoFrame::STORAGE_GPU_MEMORY_BUFFER);
+
+  return media::ConvertToMemoryMappedFrame(std::move(source_frame));
+}
+
+void WebRtcVideoFrameAdapter::SharedResources::SetFeedback(
+    const media::VideoFrameFeedback& feedback) {
+  base::AutoLock auto_lock(feedback_lock_);
+  last_feedback_ = feedback;
+}
+
+media::VideoFrameFeedback
+WebRtcVideoFrameAdapter::SharedResources::GetFeedback() {
+  base::AutoLock auto_lock(feedback_lock_);
+  return last_feedback_;
+}
+
+WebRtcVideoFrameAdapter::SharedResources::SharedResources(
+    media::GpuVideoAcceleratorFactories* gpu_factories)
+    : gpu_factories_(gpu_factories) {}
+
+WebRtcVideoFrameAdapter::SharedResources::~SharedResources() = default;
+
+WebRtcVideoFrameAdapter::ScaledBufferSize::ScaledBufferSize(
+    gfx::Rect visible_rect,
+    gfx::Size natural_size)
+    : visible_rect(std::move(visible_rect)),
+      natural_size(std::move(natural_size)) {}
+
+bool WebRtcVideoFrameAdapter::ScaledBufferSize::operator==(
+    const ScaledBufferSize& rhs) const {
+  return visible_rect == rhs.visible_rect && natural_size == rhs.natural_size;
+}
+
+bool WebRtcVideoFrameAdapter::ScaledBufferSize::operator!=(
+    const ScaledBufferSize& rhs) const {
+  return !(*this == rhs);
+}
+
+WebRtcVideoFrameAdapter::ScaledBufferSize
+WebRtcVideoFrameAdapter::ScaledBufferSize::CropAndScale(
+    int offset_x,
+    int offset_y,
+    int crop_width,
+    int crop_height,
+    int scaled_width,
+    int scaled_height) const {
+  DCHECK_LT(offset_x, natural_size.width());
+  DCHECK_LT(offset_y, natural_size.height());
+  DCHECK_LE(offset_x + crop_width, natural_size.width());
+  DCHECK_LE(offset_y + crop_height, natural_size.height());
+  DCHECK_LE(scaled_width, crop_width);
+  DCHECK_LE(scaled_height, crop_height);
+  // Used to convert requested visible rect to the natural size, i.e. undo
+  // scaling.
+  double horizontal_scale =
+      static_cast<double>(visible_rect.width()) / natural_size.width();
+  double vertical_scale =
+      static_cast<double>(visible_rect.height()) / natural_size.height();
+  return ScaledBufferSize(
+      gfx::Rect(visible_rect.x() + offset_x * horizontal_scale,
+                visible_rect.y() + offset_y * vertical_scale,
+                crop_width * horizontal_scale, crop_height * vertical_scale),
+      gfx::Size(scaled_width, scaled_height));
+}
+
+WebRtcVideoFrameAdapter::ScaledBuffer::ScaledBuffer(
+    scoped_refptr<WebRtcVideoFrameAdapter> parent,
+    ScaledBufferSize size)
+    : parent_(std::move(parent)), size_(std::move(size)) {}
+
+rtc::scoped_refptr<webrtc::I420BufferInterface>
+WebRtcVideoFrameAdapter::ScaledBuffer::ToI420() {
+  return parent_->GetOrCreateFrameBufferForSize(size_)->ToI420();
+}
+
+rtc::scoped_refptr<webrtc::VideoFrameBuffer>
+WebRtcVideoFrameAdapter::ScaledBuffer::GetMappedFrameBuffer(
+    rtc::ArrayView<webrtc::VideoFrameBuffer::Type> types) {
+  auto frame_buffer = parent_->GetOrCreateFrameBufferForSize(size_);
+  return base::Contains(types, frame_buffer->type()) ? frame_buffer : nullptr;
+}
+
+rtc::scoped_refptr<webrtc::VideoFrameBuffer>
+WebRtcVideoFrameAdapter::ScaledBuffer::CropAndScale(int offset_x,
+                                                    int offset_y,
+                                                    int crop_width,
+                                                    int crop_height,
+                                                    int scaled_width,
+                                                    int scaled_height) {
+  return new rtc::RefCountedObject<ScaledBuffer>(
+      parent_, size_.CropAndScale(offset_x, offset_y, crop_width, crop_height,
+                                  scaled_width, scaled_height));
+}
+
+WebRtcVideoFrameAdapter::WebRtcVideoFrameAdapter(
+    scoped_refptr<media::VideoFrame> frame)
+    : WebRtcVideoFrameAdapter(std::move(frame), {}, nullptr) {}
+
+WebRtcVideoFrameAdapter::WebRtcVideoFrameAdapter(
+    scoped_refptr<media::VideoFrame> frame,
+    std::vector<scoped_refptr<media::VideoFrame>> scaled_frames,
+    scoped_refptr<SharedResources> shared_resources)
+    : frame_(std::move(frame)),
+      scaled_frames_(std::move(scaled_frames)),
+      shared_resources_(std::move(shared_resources)),
+      full_size_(frame_->visible_rect(), frame_->natural_size()) {
+#if DCHECK_IS_ON()
+  double frame_aspect_ratio =
+      static_cast<double>(frame_->coded_size().width()) /
+      frame_->coded_size().height();
+  for (const auto& scaled_frame : scaled_frames_) {
+    DCHECK_LT(scaled_frame->coded_size().width(), frame_->coded_size().width());
+    DCHECK_LT(scaled_frame->coded_size().height(),
+              frame_->coded_size().height());
+    double scaled_frame_aspect_ratio =
+        static_cast<double>(scaled_frame->coded_size().width()) /
+        scaled_frame->coded_size().height();
+    DCHECK_LE(std::abs(scaled_frame_aspect_ratio - frame_aspect_ratio), 0.05);
+  }
+#endif
+}
+
+WebRtcVideoFrameAdapter::~WebRtcVideoFrameAdapter() {
+  if (shared_resources_) {
+    shared_resources_->SetFeedback(
+        media::VideoFrameFeedback().RequireMapped(!adapted_frames_.empty()));
+  }
+}
+
+rtc::scoped_refptr<webrtc::I420BufferInterface>
+WebRtcVideoFrameAdapter::ToI420() {
+  return GetOrCreateFrameBufferForSize(full_size_)->ToI420();
+}
+
+rtc::scoped_refptr<webrtc::VideoFrameBuffer>
+WebRtcVideoFrameAdapter::GetMappedFrameBuffer(
+    rtc::ArrayView<webrtc::VideoFrameBuffer::Type> types) {
+  auto frame_buffer = GetOrCreateFrameBufferForSize(full_size_);
+  return base::Contains(types, frame_buffer->type()) ? frame_buffer : nullptr;
+}
+
+// Soft-applies cropping and scaling. The result is a ScaledBuffer.
+rtc::scoped_refptr<webrtc::VideoFrameBuffer>
+WebRtcVideoFrameAdapter::CropAndScale(int offset_x,
+                                      int offset_y,
+                                      int crop_width,
+                                      int crop_height,
+                                      int scaled_width,
+                                      int scaled_height) {
+  return new rtc::RefCountedObject<ScaledBuffer>(
+      this, full_size_.CropAndScale(offset_x, offset_y, crop_width, crop_height,
+                                    scaled_width, scaled_height));
+}
+
+rtc::scoped_refptr<webrtc::VideoFrameBuffer>
+WebRtcVideoFrameAdapter::GetOrCreateFrameBufferForSize(
+    const ScaledBufferSize& size) {
+  base::AutoLock auto_lock(adapted_frames_lock_);
+  // Does this buffer already exist?
+  for (const auto& adapted_frame : adapted_frames_) {
+    if (adapted_frame.size == size)
+      return adapted_frame.frame_buffer;
+  }
+  // Adapt the frame for this size.
+  scoped_refptr<media::VideoFrame> video_frame = GetOrWrapFrameForSize(size);
+  rtc::scoped_refptr<webrtc::VideoFrameBuffer> frame_buffer =
+      ConvertToWebRtcVideoFrameBuffer(video_frame, shared_resources_);
+  adapted_frames_.emplace_back(size, video_frame, frame_buffer);
+  return frame_buffer;
+}
+
+scoped_refptr<media::VideoFrame> WebRtcVideoFrameAdapter::GetOrWrapFrameForSize(
+    const ScaledBufferSize& size) const {
+  if (size == full_size_)
+    return frame_;
+  double requested_scale_factor =
+      static_cast<double>(size.natural_size.width()) /
+      size.visible_rect.width();
+  // Ideally we have a frame that is in the same scale as |size|. Otherwise, the
+  // best frame is the smallest frame that is greater than |size|.
+  scoped_refptr<media::VideoFrame> best_frame = frame_;
+  double best_frame_scale_factor = 1.0;
+  for (const auto& scaled_frame : scaled_frames_) {
+    double scale_factor =
+        static_cast<double>(scaled_frame->coded_size().width()) /
+        frame_->coded_size().width();
+    if (scale_factor >= requested_scale_factor &&
+        scale_factor < best_frame_scale_factor) {
+      best_frame = scaled_frame;
+      best_frame_scale_factor = scale_factor;
+      if (scale_factor == requested_scale_factor) {
+        break;
+      }
+    }
+  }
+  // Because |size| is expressed relative to the full size'd frame, we need to
+  // adjust the visible rect for the scale of the best frame.
+  gfx::Rect visible_rect(size.visible_rect.x() * best_frame_scale_factor,
+                         size.visible_rect.y() * best_frame_scale_factor,
+                         size.visible_rect.width() * best_frame_scale_factor,
+                         size.visible_rect.height() * best_frame_scale_factor);
+  if (IsApproxEquals(visible_rect, best_frame->visible_rect())) {
+    // Due to rounding errors it is possible for |visible_rect| to be slightly
+    // off, which could either cause unnecessary cropping/scaling or cause
+    // crashes if |visible_rect| is not contained within
+    // |best_frame->visible_rect()|, so we adjust it.
+    visible_rect = best_frame->visible_rect();
+  }
+  CHECK(best_frame->visible_rect().Contains(visible_rect))
+      << visible_rect.ToString() << " is not contained within "
+      << best_frame->visible_rect().ToString();
+  // Wrapping is only needed if we need to crop or scale the best frame.
+  if (best_frame->visible_rect() == visible_rect &&
+      best_frame->natural_size() == size.natural_size) {
+    return best_frame;
+  }
+  return media::VideoFrame::WrapVideoFrame(best_frame, best_frame->format(),
+                                           visible_rect, size.natural_size);
+}
+
+scoped_refptr<media::VideoFrame>
+WebRtcVideoFrameAdapter::GetAdaptedVideoBufferForTesting(
+    const ScaledBufferSize& size) {
+  base::AutoLock auto_lock(adapted_frames_lock_);
+  for (const auto& adapted_frame : adapted_frames_) {
+    if (adapted_frame.size == size)
+      return adapted_frame.video_frame;
+  }
+  return nullptr;
+}
+
+}  // namespace blink
diff --git a/third_party/blink/renderer/platform/webrtc/webrtc_video_frame_adapter.h b/third_party/blink/renderer/platform/webrtc/webrtc_video_frame_adapter.h
new file mode 100644
index 0000000..75c8213
--- /dev/null
+++ b/third_party/blink/renderer/platform/webrtc/webrtc_video_frame_adapter.h
@@ -0,0 +1,248 @@
+// 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 THIRD_PARTY_BLINK_RENDERER_PLATFORM_WEBRTC_WEBRTC_VIDEO_FRAME_ADAPTER_H_
+#define THIRD_PARTY_BLINK_RENDERER_PLATFORM_WEBRTC_WEBRTC_VIDEO_FRAME_ADAPTER_H_
+
+#include <vector>
+
+#include "base/logging.h"
+#include "base/memory/ref_counted.h"
+#include "base/synchronization/lock.h"
+#include "components/viz/common/gpu/raster_context_provider.h"
+#include "media/base/video_frame.h"
+#include "media/base/video_frame_pool.h"
+#include "media/base/video_types.h"
+#include "media/capture/video_frame_feedback.h"
+#include "media/video/gpu_video_accelerator_factories.h"
+#include "third_party/blink/renderer/platform/platform_export.h"
+#include "third_party/webrtc/api/scoped_refptr.h"
+#include "third_party/webrtc/api/video/video_frame_buffer.h"
+#include "ui/gfx/geometry/rect.h"
+#include "ui/gfx/geometry/size.h"
+
+namespace blink {
+
+// The WebRtcVideoFrameAdapter implements webrtc::VideoFrameBuffer and is backed
+// by one or more media::VideoFrames.
+// * Upon CropAndScale(), the crop and scale values are soft-applied.
+// * Upon GetMappedFrameBuffer(), any outstanding crop and scale is hard-applied
+//   before returning the resulting buffer. This also happens on ToI420().
+//
+// This eliminates any intermediary downscales by only hard-applying what
+// actually needs to be mapped.
+//
+// When crop and scale is hard-applied, the media::VideoFrame of closest size
+// that is greater than or equal the requested size is chosen, minimizing
+// scaling costs. If a media::VideoFrame exists in the desired size, no scaling
+// is needed.
+//
+// WebRtcVideoFrameAdapter keeps track of which crops and scales were
+// hard-applied during its lifetime.
+// TODO(https://crbug.com/webrtc/12469): Expose this information to the caller
+// or to the frame feeddback so that we may optionally use this information to
+// optimize future captured frames for these sizes.
+class PLATFORM_EXPORT WebRtcVideoFrameAdapter
+    : public webrtc::VideoFrameBuffer {
+ public:
+  class PLATFORM_EXPORT SharedResources
+      : public base::RefCountedThreadSafe<SharedResources> {
+   public:
+    explicit SharedResources(
+        media::GpuVideoAcceleratorFactories* gpu_factories);
+
+    // Create frames for requested output format and resolution.
+    virtual scoped_refptr<media::VideoFrame> CreateFrame(
+        media::VideoPixelFormat format,
+        const gfx::Size& coded_size,
+        const gfx::Rect& visible_rect,
+        const gfx::Size& natural_size,
+        base::TimeDelta timestamp);
+
+    // Temporary frames may have a different format or size than scaled frames.
+    // However, VideoFramePool doesn't work nicely if the requested frame size
+    // or format changes on the fly. Therefore a separate pool is used for
+    // temporary frames.
+    virtual scoped_refptr<media::VideoFrame> CreateTemporaryFrame(
+        media::VideoPixelFormat format,
+        const gfx::Size& coded_size,
+        const gfx::Rect& visible_rect,
+        const gfx::Size& natural_size,
+        base::TimeDelta timestamp);
+
+    virtual scoped_refptr<viz::RasterContextProvider>
+    GetRasterContextProvider();
+
+    // Constructs a VideoFrame from a texture by invoking RasterInterface,
+    // which would perform a blocking call to a GPU process.
+    // The pixel data is copied and may be in ARGB pixel format in some cases,
+    // So additional conversion to I420 would be needed.
+    virtual scoped_refptr<media::VideoFrame> ConstructVideoFrameFromTexture(
+        scoped_refptr<media::VideoFrame> source_frame);
+
+    // Constructs a VideoFrame from a GMB. Unless it's a Windows DXGI GMB,
+    // the buffer is mapped to the memory and wrapped by a VideoFrame with no
+    // copies. For DXGI buffers a blocking call to GPU process is made to
+    // copy pixel data.
+    virtual scoped_refptr<media::VideoFrame> ConstructVideoFrameFromGpu(
+        scoped_refptr<media::VideoFrame> source_frame);
+
+    // Used to report feedback from an adapter upon destruction.
+    void SetFeedback(const media::VideoFrameFeedback& feedback);
+
+    // Returns the most recently stored feedback.
+    media::VideoFrameFeedback GetFeedback();
+
+   protected:
+    friend class base::RefCountedThreadSafe<SharedResources>;
+    virtual ~SharedResources();
+
+   private:
+    media::VideoFramePool pool_;
+    media::VideoFramePool pool_for_mapped_frames_;
+    media::VideoFramePool pool_for_tmp_frames_;
+
+    base::Lock context_provider_lock_;
+    scoped_refptr<viz::RasterContextProvider> raster_context_provider_
+        GUARDED_BY(context_provider_lock_);
+
+    media::GpuVideoAcceleratorFactories* gpu_factories_;
+
+    base::Lock feedback_lock_;
+
+    // Contains feedback from the most recently destroyed Adapter.
+    media::VideoFrameFeedback last_feedback_ GUARDED_BY(feedback_lock_);
+  };
+
+  struct PLATFORM_EXPORT ScaledBufferSize {
+    ScaledBufferSize(gfx::Rect visible_rect, gfx::Size natural_size);
+
+    bool operator==(const ScaledBufferSize& rhs) const;
+    bool operator!=(const ScaledBufferSize& rhs) const;
+
+    // Applies crop-and-scale relative to the current natural size.
+    ScaledBufferSize CropAndScale(int offset_x,
+                                  int offset_y,
+                                  int crop_width,
+                                  int crop_height,
+                                  int scaled_width,
+                                  int scaled_height) const;
+
+    // Cropping relative to the original image.
+    gfx::Rect visible_rect;
+    // The size (after scaling) of the visible rect.
+    gfx::Size natural_size;
+  };
+
+  // Implements a soft-applied "view" of the parent WebRtcVideoFrameAdapter. Its
+  // size only gets hard-applied if GetMappedFrameBuffer() or ToI420() is
+  // called, in which case the result is cached inside the parent.
+  class ScaledBuffer : public webrtc::VideoFrameBuffer {
+   public:
+    ScaledBuffer(scoped_refptr<WebRtcVideoFrameAdapter> parent,
+                 ScaledBufferSize size);
+
+    // Regardless of the pixel format used internally, kNative is returned
+    // indicating that GetMappedFrameBuffer() or ToI420() is required to obtain
+    // the pixels.
+    webrtc::VideoFrameBuffer::Type type() const override {
+      return webrtc::VideoFrameBuffer::Type::kNative;
+    }
+    int width() const override { return size_.natural_size.width(); }
+    int height() const override { return size_.natural_size.height(); }
+
+    // Obtains a mapped I420 buffer with this ScaledBuffer's size hard-applied.
+    // If I420 is not used internally, a conversion happens.
+    rtc::scoped_refptr<webrtc::I420BufferInterface> ToI420() override;
+
+    // Obtains a mapped buffer of this ScaledBuffer's size hard-applied. The
+    // resulting buffer's type is the non-kNative type used internally.
+    rtc::scoped_refptr<webrtc::VideoFrameBuffer> GetMappedFrameBuffer(
+        rtc::ArrayView<webrtc::VideoFrameBuffer::Type> types) override;
+
+    // Soft-applies cropping and scaling. The result is another ScaledBuffer.
+    rtc::scoped_refptr<webrtc::VideoFrameBuffer> CropAndScale(
+        int offset_x,
+        int offset_y,
+        int crop_width,
+        int crop_height,
+        int scaled_width,
+        int scaled_height) override;
+
+    const ScaledBufferSize& size() const { return size_; }
+
+   private:
+    scoped_refptr<WebRtcVideoFrameAdapter> parent_;
+    ScaledBufferSize size_;
+  };
+
+  explicit WebRtcVideoFrameAdapter(scoped_refptr<media::VideoFrame> frame);
+  WebRtcVideoFrameAdapter(
+      scoped_refptr<media::VideoFrame> frame,
+      std::vector<scoped_refptr<media::VideoFrame>> scaled_frames,
+      scoped_refptr<SharedResources> shared_resources);
+
+  scoped_refptr<media::VideoFrame> getMediaVideoFrame() const { return frame_; }
+
+  // Regardless of the pixel format used internally, kNative is returned
+  // indicating that GetMappedFrameBuffer() or ToI420() is required to obtain
+  // the pixels.
+  webrtc::VideoFrameBuffer::Type type() const override {
+    return webrtc::VideoFrameBuffer::Type::kNative;
+  }
+  int width() const override { return frame_->natural_size().width(); }
+  int height() const override { return frame_->natural_size().height(); }
+
+  rtc::scoped_refptr<webrtc::I420BufferInterface> ToI420() override;
+  rtc::scoped_refptr<webrtc::VideoFrameBuffer> GetMappedFrameBuffer(
+      rtc::ArrayView<webrtc::VideoFrameBuffer::Type> types) override;
+
+  // Soft-applies cropping and scaling. The result is a ScaledBuffer.
+  rtc::scoped_refptr<webrtc::VideoFrameBuffer> CropAndScale(
+      int offset_x,
+      int offset_y,
+      int crop_width,
+      int crop_height,
+      int scaled_width,
+      int scaled_height) override;
+
+  // If this exact size has been hard-applied, gets the frame associated with
+  // that adaptation, otherwise null.
+  scoped_refptr<media::VideoFrame> GetAdaptedVideoBufferForTesting(
+      const ScaledBufferSize& size);
+
+ protected:
+  ~WebRtcVideoFrameAdapter() override;
+
+ private:
+  struct AdaptedFrame {
+    AdaptedFrame(ScaledBufferSize size,
+                 scoped_refptr<media::VideoFrame> video_frame,
+                 rtc::scoped_refptr<webrtc::VideoFrameBuffer> frame_buffer)
+        : size(std::move(size)),
+          video_frame(std::move(video_frame)),
+          frame_buffer(std::move(frame_buffer)) {}
+
+    ScaledBufferSize size;
+    scoped_refptr<media::VideoFrame> video_frame;
+    rtc::scoped_refptr<webrtc::VideoFrameBuffer> frame_buffer;
+  };
+
+  rtc::scoped_refptr<webrtc::VideoFrameBuffer> GetOrCreateFrameBufferForSize(
+      const ScaledBufferSize& size);
+  scoped_refptr<media::VideoFrame> GetOrWrapFrameForSize(
+      const ScaledBufferSize& size) const;
+
+  base::Lock adapted_frames_lock_;
+  const scoped_refptr<media::VideoFrame> frame_;
+  const std::vector<scoped_refptr<media::VideoFrame>> scaled_frames_;
+  const scoped_refptr<SharedResources> shared_resources_;
+  const ScaledBufferSize full_size_;
+  // Frames that have been adapted, i.e. that were "hard-applied" and mapped.
+  std::vector<AdaptedFrame> adapted_frames_ GUARDED_BY(adapted_frames_lock_);
+};
+
+}  // namespace blink
+
+#endif  // THIRD_PARTY_BLINK_RENDERER_PLATFORM_WEBRTC_WEBRTC_VIDEO_FRAME_ADAPTER_H_
diff --git a/third_party/blink/renderer/platform/webrtc/webrtc_video_frame_adapter_test.cc b/third_party/blink/renderer/platform/webrtc/webrtc_video_frame_adapter_test.cc
new file mode 100644
index 0000000..1130071
--- /dev/null
+++ b/third_party/blink/renderer/platform/webrtc/webrtc_video_frame_adapter_test.cc
@@ -0,0 +1,421 @@
+// 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 "third_party/blink/renderer/platform/webrtc/webrtc_video_frame_adapter.h"
+
+#include "base/memory/ref_counted.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/renderer/platform/testing/video_frame_utils.h"
+#include "third_party/blink/renderer/platform/webrtc/testing/mock_webrtc_video_frame_adapter_shared_resources.h"
+#include "third_party/webrtc/api/scoped_refptr.h"
+#include "third_party/webrtc/rtc_base/ref_counted_object.h"
+
+namespace blink {
+
+TEST(ScaledBufferSizeTest, CroppingIsRelative) {
+  const WebRtcVideoFrameAdapter::ScaledBufferSize k720p(
+      gfx::Rect(0, 0, 1280, 720), gfx::Size(1280, 720));
+
+  // Crop away a 100 pixel border.
+  const auto cropped_full_scale =
+      k720p.CropAndScale(100, 100, 1080, 520, 1080, 520);
+  EXPECT_EQ(cropped_full_scale.visible_rect.x(), 100);
+  EXPECT_EQ(cropped_full_scale.visible_rect.y(), 100);
+  EXPECT_EQ(cropped_full_scale.visible_rect.width(), 1080);
+  EXPECT_EQ(cropped_full_scale.visible_rect.height(), 520);
+  EXPECT_EQ(cropped_full_scale.natural_size.width(), 1080);
+  EXPECT_EQ(cropped_full_scale.natural_size.height(), 520);
+
+  // Applying the same size again should be a NO-OP.
+  const auto cropped_full_scale2 =
+      cropped_full_scale.CropAndScale(0, 0, 1080, 520, 1080, 520);
+  EXPECT_TRUE(cropped_full_scale2 == cropped_full_scale);
+
+  // Cropping again is relative to the current crop. Crop on crop.
+  const auto second_cropped_full_size =
+      cropped_full_scale.CropAndScale(100, 100, 880, 320, 880, 320);
+  EXPECT_EQ(second_cropped_full_size.visible_rect.x(), 200);
+  EXPECT_EQ(second_cropped_full_size.visible_rect.y(), 200);
+  EXPECT_EQ(second_cropped_full_size.visible_rect.width(), 880);
+  EXPECT_EQ(second_cropped_full_size.visible_rect.height(), 320);
+  EXPECT_EQ(second_cropped_full_size.natural_size.width(), 880);
+  EXPECT_EQ(second_cropped_full_size.natural_size.height(), 320);
+
+  // Applying the same size again should be a NO-OP.
+  const auto second_cropped_full_size2 =
+      second_cropped_full_size.CropAndScale(0, 0, 880, 320, 880, 320);
+  EXPECT_TRUE(second_cropped_full_size2 == second_cropped_full_size);
+
+  // Cropping again is relative to the current crop. Crop on crop on crop.
+  const auto third_cropped_full_size =
+      second_cropped_full_size.CropAndScale(100, 100, 680, 120, 680, 120);
+  EXPECT_EQ(third_cropped_full_size.visible_rect.x(), 300);
+  EXPECT_EQ(third_cropped_full_size.visible_rect.y(), 300);
+  EXPECT_EQ(third_cropped_full_size.visible_rect.width(), 680);
+  EXPECT_EQ(third_cropped_full_size.visible_rect.height(), 120);
+  EXPECT_EQ(third_cropped_full_size.natural_size.width(), 680);
+  EXPECT_EQ(third_cropped_full_size.natural_size.height(), 120);
+}
+
+TEST(ScaledBufferSizeTest, ScalingIsRelative) {
+  const WebRtcVideoFrameAdapter::ScaledBufferSize k720p(
+      gfx::Rect(0, 0, 1280, 720), gfx::Size(1280, 720));
+
+  // Scale down by 2x.
+  const auto no_crop_half_size = k720p.CropAndScale(0, 0, 1280, 720, 640, 360);
+  EXPECT_EQ(no_crop_half_size.visible_rect.x(), 0);
+  EXPECT_EQ(no_crop_half_size.visible_rect.y(), 0);
+  EXPECT_EQ(no_crop_half_size.visible_rect.width(), 1280);
+  EXPECT_EQ(no_crop_half_size.visible_rect.height(), 720);
+  EXPECT_EQ(no_crop_half_size.natural_size.width(), 640);
+  EXPECT_EQ(no_crop_half_size.natural_size.height(), 360);
+
+  // Applying the same size again should be a NO-OP.
+  const auto no_crop_half_size2 =
+      no_crop_half_size.CropAndScale(0, 0, 640, 360, 640, 360);
+  EXPECT_TRUE(no_crop_half_size2 == no_crop_half_size);
+
+  // Scaling again is relative to the current scale. Half-size on half-size.
+  const auto no_crop_quarter_size =
+      no_crop_half_size.CropAndScale(0, 0, 640, 360, 320, 180);
+  EXPECT_EQ(no_crop_quarter_size.visible_rect.x(), 0);
+  EXPECT_EQ(no_crop_quarter_size.visible_rect.y(), 0);
+  EXPECT_EQ(no_crop_quarter_size.visible_rect.width(), 1280);
+  EXPECT_EQ(no_crop_quarter_size.visible_rect.height(), 720);
+  EXPECT_EQ(no_crop_quarter_size.natural_size.width(), 320);
+  EXPECT_EQ(no_crop_quarter_size.natural_size.height(), 180);
+
+  // Applying the same size again should be a NO-OP.
+  const auto no_crop_quarter_size2 =
+      no_crop_quarter_size.CropAndScale(0, 0, 320, 180, 320, 180);
+  EXPECT_TRUE(no_crop_quarter_size2 == no_crop_quarter_size);
+
+  // Scaling again is relative to the current scale.
+  // Half-size on half-size on half-size.
+  const auto no_crop_eighths_size =
+      no_crop_quarter_size.CropAndScale(0, 0, 320, 180, 160, 90);
+  EXPECT_EQ(no_crop_eighths_size.visible_rect.x(), 0);
+  EXPECT_EQ(no_crop_eighths_size.visible_rect.y(), 0);
+  EXPECT_EQ(no_crop_eighths_size.visible_rect.width(), 1280);
+  EXPECT_EQ(no_crop_eighths_size.visible_rect.height(), 720);
+  EXPECT_EQ(no_crop_eighths_size.natural_size.width(), 160);
+  EXPECT_EQ(no_crop_eighths_size.natural_size.height(), 90);
+}
+
+TEST(ScaledBufferSizeTest, CroppingAndScalingIsRelative) {
+  const WebRtcVideoFrameAdapter::ScaledBufferSize k720p(
+      gfx::Rect(0, 0, 1280, 720), gfx::Size(1280, 720));
+
+  // Crop away a 100 pixel border and downscale by 2x.
+  const auto crop_and_scale1 =
+      k720p.CropAndScale(100, 100, 1080, 520, 540, 260);
+  EXPECT_EQ(crop_and_scale1.visible_rect.x(), 100);
+  EXPECT_EQ(crop_and_scale1.visible_rect.y(), 100);
+  EXPECT_EQ(crop_and_scale1.visible_rect.width(), 1080);
+  EXPECT_EQ(crop_and_scale1.visible_rect.height(), 520);
+  EXPECT_EQ(crop_and_scale1.natural_size.width(), 540);
+  EXPECT_EQ(crop_and_scale1.natural_size.height(), 260);
+
+  // Cropping some more at the new scale without further downscale.
+  const auto crop_and_scale2 =
+      crop_and_scale1.CropAndScale(50, 50, 440, 160, 440, 160);
+  // The delta offset is magnified due to scale. Offset = 100*1 + 50*2.
+  EXPECT_EQ(crop_and_scale2.visible_rect.x(), 200);
+  EXPECT_EQ(crop_and_scale2.visible_rect.y(), 200);
+  EXPECT_EQ(crop_and_scale2.visible_rect.width(), 880);
+  EXPECT_EQ(crop_and_scale2.visible_rect.height(), 320);
+  EXPECT_EQ(crop_and_scale2.natural_size.width(), 440);
+  EXPECT_EQ(crop_and_scale2.natural_size.height(), 160);
+
+  // Scaling some more without further cropping.
+  const auto crop_and_scale3 =
+      crop_and_scale2.CropAndScale(0, 0, 440, 160, 220, 80);
+  EXPECT_EQ(crop_and_scale3.visible_rect.x(), 200);
+  EXPECT_EQ(crop_and_scale3.visible_rect.y(), 200);
+  EXPECT_EQ(crop_and_scale3.visible_rect.width(), 880);
+  EXPECT_EQ(crop_and_scale3.visible_rect.height(), 320);
+  EXPECT_EQ(crop_and_scale3.natural_size.width(), 220);
+  EXPECT_EQ(crop_and_scale3.natural_size.height(), 80);
+}
+
+TEST(WebRtcVideoFrameAdapterTest, MapFullFrameIsZeroCopy) {
+  std::vector<webrtc::VideoFrameBuffer::Type> kNv12 = {
+      webrtc::VideoFrameBuffer::Type::kNV12};
+  const gfx::Size kSize720p(1280, 720);
+  const gfx::Rect kRect720p(0, 0, 1280, 720);
+
+  // The strictness of the mock ensures zero copy.
+  scoped_refptr<MockSharedResources> resources =
+      new testing::StrictMock<MockSharedResources>();
+
+  auto frame_720p = CreateTestFrame(kSize720p, kRect720p, kSize720p,
+                                    media::VideoFrame::STORAGE_OWNED_MEMORY,
+                                    media::VideoPixelFormat::PIXEL_FORMAT_NV12);
+
+  rtc::scoped_refptr<WebRtcVideoFrameAdapter> multi_buffer(
+      new rtc::RefCountedObject<WebRtcVideoFrameAdapter>(
+          frame_720p, std::vector<scoped_refptr<media::VideoFrame>>(),
+          resources));
+
+  // Mapping produces a frame of the correct size.
+  auto mapped_frame = multi_buffer->GetMappedFrameBuffer(kNv12);
+  EXPECT_EQ(mapped_frame->width(), kSize720p.width());
+  EXPECT_EQ(mapped_frame->height(), kSize720p.height());
+  // The mapping above should be backed by |frame_720p|.
+  auto adapted_frame = multi_buffer->GetAdaptedVideoBufferForTesting(
+      WebRtcVideoFrameAdapter::ScaledBufferSize(kRect720p, kSize720p));
+  EXPECT_EQ(adapted_frame, frame_720p);
+}
+
+TEST(WebRtcVideoFrameAdapterTest,
+     MapScaledFrameCreatesNewFrameWhenNotPreScaled) {
+  std::vector<webrtc::VideoFrameBuffer::Type> kNv12 = {
+      webrtc::VideoFrameBuffer::Type::kNV12};
+  const gfx::Size kSize720p(1280, 720);
+  const gfx::Rect kRect720p(0, 0, 1280, 720);
+  const gfx::Size kSize360p(640, 360);
+
+  // Because the size we are going to request does not the frame we expect one
+  // CreateFrame() to happen.
+  scoped_refptr<MockSharedResources> resources =
+      new testing::StrictMock<MockSharedResources>();
+  EXPECT_CALL(*resources, CreateFrame)
+      .WillOnce(testing::Invoke(
+          [](media::VideoPixelFormat format, const gfx::Size& coded_size,
+             const gfx::Rect& visible_rect, const gfx::Size& natural_size,
+             base::TimeDelta timestamp) {
+            return CreateTestFrame(coded_size, visible_rect, natural_size,
+                                   media::VideoFrame::STORAGE_OWNED_MEMORY,
+                                   format);
+          }));
+
+  auto frame_720p = CreateTestFrame(kSize720p, kRect720p, kSize720p,
+                                    media::VideoFrame::STORAGE_OWNED_MEMORY,
+                                    media::VideoPixelFormat::PIXEL_FORMAT_NV12);
+
+  rtc::scoped_refptr<WebRtcVideoFrameAdapter> multi_buffer(
+      new rtc::RefCountedObject<WebRtcVideoFrameAdapter>(
+          frame_720p, std::vector<scoped_refptr<media::VideoFrame>>(),
+          resources));
+
+  auto scaled_frame =
+      multi_buffer->Scale(kSize360p.width(), kSize360p.height());
+
+  // Mapping produces a frame of the correct size.
+  auto mapped_frame = scaled_frame->GetMappedFrameBuffer(kNv12);
+  EXPECT_EQ(mapped_frame->width(), kSize360p.width());
+  EXPECT_EQ(mapped_frame->height(), kSize360p.height());
+  // The mapping above should be backed by a frame that wraps |frame_720p|. We
+  // can tell by looking at the coded size.
+  auto adapted_frame = multi_buffer->GetAdaptedVideoBufferForTesting(
+      WebRtcVideoFrameAdapter::ScaledBufferSize(kRect720p, kSize360p));
+  ASSERT_TRUE(adapted_frame);
+  EXPECT_EQ(adapted_frame->coded_size(), frame_720p->coded_size());
+}
+
+TEST(WebRtcVideoFrameAdapterTest, MapScaledFrameUsesPreScaling) {
+  std::vector<webrtc::VideoFrameBuffer::Type> kNv12 = {
+      webrtc::VideoFrameBuffer::Type::kNV12};
+  const gfx::Size kSize720p(1280, 720);
+  const gfx::Rect kRect720p(0, 0, 1280, 720);
+  const gfx::Size kSize360p(640, 360);
+  const gfx::Rect kRect360p(0, 0, 640, 360);
+
+  // The strictness of the mock ensures no additional scaling.
+  scoped_refptr<MockSharedResources> resources =
+      new testing::StrictMock<MockSharedResources>();
+
+  auto frame_720p = CreateTestFrame(kSize720p, kRect720p, kSize720p,
+                                    media::VideoFrame::STORAGE_OWNED_MEMORY,
+                                    media::VideoPixelFormat::PIXEL_FORMAT_NV12);
+  auto frame_360p = CreateTestFrame(kSize360p, kRect360p, kSize360p,
+                                    media::VideoFrame::STORAGE_OWNED_MEMORY,
+                                    media::VideoPixelFormat::PIXEL_FORMAT_NV12);
+
+  rtc::scoped_refptr<WebRtcVideoFrameAdapter> multi_buffer(
+      new rtc::RefCountedObject<WebRtcVideoFrameAdapter>(
+          frame_720p,
+          std::vector<scoped_refptr<media::VideoFrame>>({frame_360p}),
+          resources));
+
+  auto scaled_frame =
+      multi_buffer->Scale(kSize360p.width(), kSize360p.height());
+
+  // Mapping produces a frame of the correct size.
+  auto mapped_frame = scaled_frame->GetMappedFrameBuffer(kNv12);
+  EXPECT_EQ(mapped_frame->width(), kSize360p.width());
+  EXPECT_EQ(mapped_frame->height(), kSize360p.height());
+  // The mapping above should be backed by |frame_360p|.
+  auto adapted_frame = multi_buffer->GetAdaptedVideoBufferForTesting(
+      WebRtcVideoFrameAdapter::ScaledBufferSize(kRect720p, kSize360p));
+  EXPECT_EQ(adapted_frame, frame_360p);
+}
+
+TEST(WebRtcVideoFrameAdapterTest, MapScaledFrameScalesFromClosestFrame) {
+  std::vector<webrtc::VideoFrameBuffer::Type> kNv12 = {
+      webrtc::VideoFrameBuffer::Type::kNV12};
+  const gfx::Size kSize720p(1280, 720);
+  const gfx::Rect kRect720p(0, 0, 1280, 720);
+  const gfx::Size kSize480p(853, 480);
+  const gfx::Rect kRect480p(0, 0, 853, 480);
+  const gfx::Size kSize360p(640, 360);
+  const gfx::Rect kRect360p(0, 0, 640, 360);
+
+  // A size in-between 480p and 360p.
+  const gfx::Size kSize432p(768, 432);
+
+  // Because the size we are going to request does not match any of the frames
+  // we expect one CreateFrame() to happen.
+  scoped_refptr<MockSharedResources> resources =
+      new testing::StrictMock<MockSharedResources>();
+  EXPECT_CALL(*resources, CreateFrame)
+      .WillOnce(testing::Invoke(
+          [](media::VideoPixelFormat format, const gfx::Size& coded_size,
+             const gfx::Rect& visible_rect, const gfx::Size& natural_size,
+             base::TimeDelta timestamp) {
+            return CreateTestFrame(coded_size, visible_rect, natural_size,
+                                   media::VideoFrame::STORAGE_OWNED_MEMORY,
+                                   format);
+          }));
+
+  auto frame_720p = CreateTestFrame(kSize720p, kRect720p, kSize720p,
+                                    media::VideoFrame::STORAGE_OWNED_MEMORY,
+                                    media::VideoPixelFormat::PIXEL_FORMAT_NV12);
+  auto frame_480p = CreateTestFrame(kSize480p, kRect480p, kSize480p,
+                                    media::VideoFrame::STORAGE_OWNED_MEMORY,
+                                    media::VideoPixelFormat::PIXEL_FORMAT_NV12);
+  auto frame_360p = CreateTestFrame(kSize360p, kRect360p, kSize360p,
+                                    media::VideoFrame::STORAGE_OWNED_MEMORY,
+                                    media::VideoPixelFormat::PIXEL_FORMAT_NV12);
+
+  rtc::scoped_refptr<WebRtcVideoFrameAdapter> multi_buffer(
+      new rtc::RefCountedObject<WebRtcVideoFrameAdapter>(
+          frame_720p,
+          std::vector<scoped_refptr<media::VideoFrame>>(
+              {frame_480p, frame_360p}),
+          resources));
+
+  auto scaled_frame =
+      multi_buffer->Scale(kSize432p.width(), kSize432p.height());
+
+  // Mapping produces a frame of the correct size.
+  auto mapped_frame = scaled_frame->GetMappedFrameBuffer(kNv12);
+  EXPECT_EQ(mapped_frame->width(), kSize432p.width());
+  EXPECT_EQ(mapped_frame->height(), kSize432p.height());
+  // The mapping above should be backed by a frame that wraps |frame_480p|. We
+  // can tell by looking at the coded size.
+  auto adapted_frame = multi_buffer->GetAdaptedVideoBufferForTesting(
+      WebRtcVideoFrameAdapter::ScaledBufferSize(kRect720p, kSize432p));
+  ASSERT_TRUE(adapted_frame);
+  EXPECT_EQ(adapted_frame->coded_size(), frame_480p->coded_size());
+}
+
+TEST(WebRtcVideoFrameAdapterTest, CanApplyCropAndScale) {
+  std::vector<webrtc::VideoFrameBuffer::Type> kNv12 = {
+      webrtc::VideoFrameBuffer::Type::kNV12};
+  const gfx::Size kSize720p(1280, 720);
+  const gfx::Rect kRect720p(0, 0, 1280, 720);
+  const gfx::Size kSize360p(640, 360);
+  const gfx::Rect kRect360p(0, 0, 640, 360);
+
+  const gfx::Rect kCroppedRect1(20, 20, 1240, 680);
+  const gfx::Rect kCroppedRect2(20, 20, 1200, 640);
+  const gfx::Size kScaledSize2(1200 / 2, 640 / 2);
+
+  // The strictness of the mock ensures zero copy.
+  scoped_refptr<MockSharedResources> resources =
+      new testing::StrictMock<MockSharedResources>();
+
+  auto frame_720p = CreateTestFrame(kSize720p, kRect720p, kSize720p,
+                                    media::VideoFrame::STORAGE_OWNED_MEMORY,
+                                    media::VideoPixelFormat::PIXEL_FORMAT_NV12);
+  auto frame_360p = CreateTestFrame(kSize360p, kRect360p, kSize360p,
+                                    media::VideoFrame::STORAGE_OWNED_MEMORY,
+                                    media::VideoPixelFormat::PIXEL_FORMAT_NV12);
+
+  rtc::scoped_refptr<WebRtcVideoFrameAdapter> multi_buffer(
+      new rtc::RefCountedObject<WebRtcVideoFrameAdapter>(
+          frame_720p,
+          std::vector<scoped_refptr<media::VideoFrame>>({frame_360p}),
+          resources));
+
+  // Apply initial cropping, keeping the same scale.
+  auto cropped_frame1 = multi_buffer->CropAndScale(
+      kCroppedRect1.x(), kCroppedRect1.y(), kCroppedRect1.width(),
+      kCroppedRect1.height(), kCroppedRect1.width(), kCroppedRect1.height());
+
+  // Mapping produces a frame of the correct size.
+  auto mapped_cropped_frame1 = cropped_frame1->GetMappedFrameBuffer(kNv12);
+  EXPECT_EQ(mapped_cropped_frame1->width(), kCroppedRect1.width());
+  EXPECT_EQ(mapped_cropped_frame1->height(), kCroppedRect1.height());
+  // The mapping above should be backed by a frame that wraps |frame_720p|. We
+  // can tell by looking at the coded size.
+  auto adapted_frame = multi_buffer->GetAdaptedVideoBufferForTesting(
+      WebRtcVideoFrameAdapter::ScaledBufferSize(
+          kCroppedRect1,
+          gfx::Size(kCroppedRect1.width(), kCroppedRect1.height())));
+  ASSERT_TRUE(adapted_frame);
+  EXPECT_EQ(adapted_frame->coded_size(), frame_720p->coded_size());
+
+  // Apply further cropping and scaling on the already cropped frame.
+  auto cropped_frame2 = cropped_frame1->CropAndScale(
+      kCroppedRect2.x(), kCroppedRect2.y(), kCroppedRect2.width(),
+      kCroppedRect2.height(), kScaledSize2.width(), kScaledSize2.height());
+
+  // Mapping produces a frame of the correct size.
+  auto mapped_cropped_frame2 = cropped_frame2->GetMappedFrameBuffer(kNv12);
+  EXPECT_EQ(mapped_cropped_frame2->width(), kScaledSize2.width());
+  EXPECT_EQ(mapped_cropped_frame2->height(), kScaledSize2.height());
+  // The mapping above should be backed by a frame that wraps |frame_360p|. We
+  // can tell by looking at the coded size.
+  adapted_frame = multi_buffer->GetAdaptedVideoBufferForTesting(
+      WebRtcVideoFrameAdapter::ScaledBufferSize(
+          // The second cropped rectangle is relative to the first one.
+          gfx::Rect(kCroppedRect1.x() + kCroppedRect2.x(),
+                    kCroppedRect1.y() + kCroppedRect2.y(),
+                    kCroppedRect2.width(), kCroppedRect2.height()),
+          kScaledSize2));
+  ASSERT_TRUE(adapted_frame);
+  EXPECT_EQ(adapted_frame->coded_size(), frame_360p->coded_size());
+}
+
+TEST(WebRtcVideoFrameAdapterTest, FrameFeedbackSetsRequireMappedFrame) {
+  std::vector<webrtc::VideoFrameBuffer::Type> kNv12 = {
+      webrtc::VideoFrameBuffer::Type::kNV12};
+  const gfx::Size kSize720p(1280, 720);
+  const gfx::Rect kRect720p(0, 0, 1280, 720);
+  const gfx::Size kSize360p(640, 360);
+
+  scoped_refptr<WebRtcVideoFrameAdapter::SharedResources> resources =
+      base::MakeRefCounted<WebRtcVideoFrameAdapter::SharedResources>(nullptr);
+
+  auto frame_720p = CreateTestFrame(kSize720p, kRect720p, kSize720p,
+                                    media::VideoFrame::STORAGE_OWNED_MEMORY,
+                                    media::VideoPixelFormat::PIXEL_FORMAT_NV12);
+
+  // By default, the feedback is not set to require mapping.
+  EXPECT_FALSE(resources->GetFeedback().require_mapped_frame);
+  {
+    // Do some scaling, but don't map it.
+    rtc::scoped_refptr<WebRtcVideoFrameAdapter> multi_buffer(
+        new rtc::RefCountedObject<WebRtcVideoFrameAdapter>(
+            frame_720p, std::vector<scoped_refptr<media::VideoFrame>>(),
+            resources));
+    multi_buffer->Scale(kSize360p.width(), kSize360p.height());
+  }
+  EXPECT_FALSE(resources->GetFeedback().require_mapped_frame);
+  {
+    // Do map the buffer.
+    rtc::scoped_refptr<WebRtcVideoFrameAdapter> multi_buffer(
+        new rtc::RefCountedObject<WebRtcVideoFrameAdapter>(
+            frame_720p, std::vector<scoped_refptr<media::VideoFrame>>(),
+            resources));
+    multi_buffer->Scale(kSize360p.width(), kSize360p.height())
+        ->GetMappedFrameBuffer(kNv12);
+  }
+  EXPECT_TRUE(resources->GetFeedback().require_mapped_frame);
+}
+
+}  // namespace blink
diff --git a/third_party/blink/web_tests/SlowTests b/third_party/blink/web_tests/SlowTests
index 5bd03802..747980556 100644
--- a/third_party/blink/web_tests/SlowTests
+++ b/third_party/blink/web_tests/SlowTests
@@ -734,6 +734,7 @@
 crbug.com/1182691 external/wpt/webcodecs/audio-encoder.any.html [ Slow ]
 crbug.com/1182691 external/wpt/webcodecs/video-encoder.any.html [ Slow ]
 crbug.com/1176474 wpt_internal/webcodecs/temporal_svc.any.html [ Slow ]
+crbug.com/1176474 wpt_internal/webcodecs/temporal_svc.any.worker.html [ Slow ]
 
 # Composited scroll snap is not accelerated for testing.
 crbug.com/1186753 virtual/threaded-prefer-compositing/fast/scroll-snap/snaps-after-scrollbar-scrolling-arrow.html [ Slow ]
diff --git a/third_party/blink/web_tests/external/wpt/paint-timing/fcp-only/svg-in-iframe.html b/third_party/blink/web_tests/external/wpt/paint-timing/fcp-only/svg-in-iframe.html
new file mode 100644
index 0000000..230c166c
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/paint-timing/fcp-only/svg-in-iframe.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<title>Performance Paint Timing Test: SVG in iframe does not trigger FCP</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/utils.js"></script>
+<iframe src="../resources/svg.html"></iframe>
+<script>
+  promise_test(async (t) => {
+    await new Promise(resolve => {
+      window.addEventListener("message", resolve);
+    });
+    return assertNoFirstContentfulPaint(t);
+  }, "SVG in an iframe does not trigger FCP in the main frame");
+</script>
diff --git a/third_party/blink/web_tests/external/wpt/paint-timing/resources/svg.html b/third_party/blink/web_tests/external/wpt/paint-timing/resources/svg.html
new file mode 100644
index 0000000..94185ea
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/paint-timing/resources/svg.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<div id="main">
+  <svg viewBox="0 0 10 10">
+    <defs>
+      <circle id="myCircle" cx="5" cy="5" r="4" stroke="blue" />
+    </defs>
+    <use id="circle" href="#myCircle" fill="green" />
+  </svg>
+</div>
+<script>
+  const observer = new PerformanceObserver(list => {
+    const fcp = list.getEntriesByName("first-contentful-paint");
+    if (!fcp.length)
+      return;
+
+    // Message the parent when FCP has been reached.
+    parent.postMessage("GotFCP", '*');
+  });
+  observer.observe({ type: "paint", buffered: true });
+</script>
diff --git a/third_party/blink/web_tests/external/wpt/webcodecs/audio-encoder.any.js b/third_party/blink/web_tests/external/wpt/webcodecs/audio-encoder.any.js
index 0813b79..a6f131b 100644
--- a/third_party/blink/web_tests/external/wpt/webcodecs/audio-encoder.any.js
+++ b/third_party/blink/web_tests/external/wpt/webcodecs/audio-encoder.any.js
@@ -1,29 +1,6 @@
 // META: global=window
 // META: script=/webcodecs/utils.js
 
-function make_audio_frame(timestamp, channels, sampleRate, length) {
-  let buffer = new AudioBuffer({
-    length: length,
-    numberOfChannels: channels,
-    sampleRate: sampleRate
-  });
-
-  for (var channel = 0; channel < buffer.numberOfChannels; channel++) {
-    // This gives us the actual array that contains the data
-    var array = buffer.getChannelData(channel);
-    let hz = 100 + channel * 50; // sound frequency
-    for (var i = 0; i < array.length; i++) {
-      let t = (i / sampleRate) * hz * (Math.PI * 2);
-      array[i] = Math.sin(t);
-    }
-  }
-
-  return new AudioFrame({
-    timestamp: timestamp,
-    buffer: buffer
-  });
-}
-
 // Merge all audio buffers into a new big one with all the data.
 function join_buffers(buffers) {
   assert_greater_than_equal(buffers.length, 0);
@@ -324,4 +301,4 @@
   assert_not_equals(decoder_config, null);
   assert_not_equals(decoder_config.description, null);
   encoder.close();
-}, "Emit decoder config and extra data.");
\ No newline at end of file
+}, "Emit decoder config and extra data.");
diff --git a/third_party/blink/web_tests/external/wpt/webcodecs/audio-frame-serialization.any.js b/third_party/blink/web_tests/external/wpt/webcodecs/audio-frame-serialization.any.js
new file mode 100644
index 0000000..a16207f
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/webcodecs/audio-frame-serialization.any.js
@@ -0,0 +1,72 @@
+// META: global=window
+// META: script=/common/media.js
+// META: script=/webcodecs/utils.js
+
+var defaultInit = {
+  timestamp: 1234,
+  channels: 2,
+  sampleRate: 8000,
+  frames: 100,
+}
+
+function createDefaultAudioFrame() {
+  return make_audio_frame(defaultInit.timestamp,
+                          defaultInit.channels,
+                          defaultInit.sampleRate,
+                          defaultInit.frames);
+}
+
+async_test(t => {
+  let localFrame = createDefaultAudioFrame();
+
+  let channel = new MessageChannel();
+  let localPort = channel.port1;
+  let externalPort = channel.port2;
+
+  externalPort.onmessage = t.step_func((e) => {
+    let externalFrame = e.data;
+    let buffer = externalFrame.buffer;
+    // We should have a valid deserialized buffer.
+    assert_true(buffer != undefined || buffer != null);
+    assert_equals(buffer.numberOfChannels,
+                  localFrame.buffer.numberOfChannels, "numberOfChannels");
+
+    for (var channel = 0; channel < buffer.numberOfChannels; channel++) {
+      // This gives us the actual array that contains the data
+      var dest_array = buffer.getChannelData(channel);
+      var source_array = localFrame.buffer.getChannelData(channel);
+      for (var i = 0; i < dest_array.length; i+=10) {
+        assert_equals(dest_array[i], source_array[i],
+          "data (ch=" + channel + ", i=" + i + ")");
+      }
+    }
+
+    externalFrame.close();
+    externalPort.postMessage("Done");
+  })
+
+  localPort.onmessage = t.step_func_done((e) => {
+    assert_true(localFrame.buffer != null);
+    localFrame.close();
+  })
+
+  localPort.postMessage(localFrame);
+
+}, 'Verify closing frames does not propagate accross contexts.');
+
+async_test(t => {
+  let localFrame = createDefaultAudioFrame();
+
+  let channel = new MessageChannel();
+  let localPort = channel.port1;
+
+  localPort.onmessage = t.unreached_func();
+
+  localFrame.close();
+
+  assert_throws_dom("DataCloneError", () => {
+    localPort.postMessage(localFrame);
+  });
+
+  t.done();
+}, 'Verify posting closed frames throws.');
diff --git a/third_party/blink/web_tests/external/wpt/webcodecs/utils.js b/third_party/blink/web_tests/external/wpt/webcodecs/utils.js
index 854efc3..12cb715 100644
--- a/third_party/blink/web_tests/external/wpt/webcodecs/utils.js
+++ b/third_party/blink/web_tests/external/wpt/webcodecs/utils.js
@@ -1,3 +1,26 @@
+function make_audio_frame(timestamp, channels, sampleRate, length) {
+  let buffer = new AudioBuffer({
+    length: length,
+    numberOfChannels: channels,
+    sampleRate: sampleRate
+  });
+
+  for (var channel = 0; channel < buffer.numberOfChannels; channel++) {
+    // This gives us the actual array that contains the data
+    var array = buffer.getChannelData(channel);
+    let hz = 100 + channel * 50; // sound frequency
+    for (var i = 0; i < array.length; i++) {
+      let t = (i / sampleRate) * hz * (Math.PI * 2);
+      array[i] = Math.sin(t);
+    }
+  }
+
+  return new AudioFrame({
+    timestamp: timestamp,
+    buffer: buffer
+  });
+}
+
 function makeOffscreenCanvas(width, height) {
   let canvas = new OffscreenCanvas(width, height);
   let ctx = canvas.getContext('2d');
diff --git a/tools/ipc_fuzzer/fuzzer/fuzzer.cc b/tools/ipc_fuzzer/fuzzer/fuzzer.cc
index 3f4bb9c5..ef1a2c0 100644
--- a/tools/ipc_fuzzer/fuzzer/fuzzer.cc
+++ b/tools/ipc_fuzzer/fuzzer/fuzzer.cc
@@ -13,7 +13,6 @@
 #include "base/containers/span.h"
 #include "base/memory/ptr_util.h"
 #include "base/stl_util.h"
-#include "base/strings/nullable_string16.h"
 #include "base/strings/string_util.h"
 #include "base/unguessable_token.h"
 #include "base/util/type_safety/id_type.h"
@@ -444,20 +443,6 @@
 };
 
 template <>
-struct FuzzTraits<base::NullableString16> {
-  static bool Fuzz(base::NullableString16* p, Fuzzer* fuzzer) {
-    std::u16string string = p->string();
-    bool is_null = p->is_null();
-    if (!FuzzParam(&string, fuzzer))
-      return false;
-    if (!FuzzParam(&is_null, fuzzer))
-      return false;
-    *p = base::NullableString16(string, is_null);
-    return true;
-  }
-};
-
-template <>
 struct FuzzTraits<base::Time> {
   static bool Fuzz(base::Time* p, Fuzzer* fuzzer) {
     int64_t internal_value = p->ToInternalValue();
diff --git a/tools/mb/mb_config.pyl b/tools/mb/mb_config.pyl
index 72ce3d98..72c34ba 100644
--- a/tools/mb/mb_config.pyl
+++ b/tools/mb/mb_config.pyl
@@ -2003,11 +2003,11 @@
     ],
 
     'fuchsia_clang_tot_official_arm64': [
-      'official_optimize_goma', 'fuchsia', 'arm64', 'clang_tot',
+      'official_optimize', 'fuchsia', 'arm64', 'clang_tot', 'static',
     ],
 
     'fuchsia_clang_tot_release_x64': [
-      'fuchsia', 'release_bot', 'clang_tot',
+      'fuchsia', 'release', 'clang_tot', 'static',
     ],
 
     'fuchsia_official_arm64_size': [
diff --git a/tools/mb/mb_config_expectations/chromium.clang.json b/tools/mb/mb_config_expectations/chromium.clang.json
index 95a1bf6..2d00e25 100644
--- a/tools/mb/mb_config_expectations/chromium.clang.json
+++ b/tools/mb/mb_config_expectations/chromium.clang.json
@@ -137,18 +137,17 @@
       "is_component_build": false,
       "is_debug": false,
       "llvm_force_head_revision": true,
-      "target_os": "fuchsia",
-      "use_goma": true
+      "target_os": "fuchsia"
     }
   },
   "ToTFuchsiaOfficial": {
     "gn_args": {
       "is_clang": true,
+      "is_component_build": false,
       "is_official_build": true,
       "llvm_force_head_revision": true,
       "target_cpu": "arm64",
-      "target_os": "fuchsia",
-      "use_goma": true
+      "target_os": "fuchsia"
     }
   },
   "ToTLinux": {
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index cc6de42..6561bdef 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -3313,6 +3313,9 @@
 </enum>
 
 <enum name="AppMenuSimilarSelectionType">
+  <obsolete>
+    Deprecated as of 03/2021.
+  </obsolete>
   <int value="0"
       label="selected adding to bookmarks, and then selected all bookmarks"/>
   <int value="1"
@@ -4553,6 +4556,12 @@
   <int value="6" label="Document destroyed during stream creation"/>
 </enum>
 
+<enum name="AudioPermissionState">
+  <int value="0" label="Granted"/>
+  <int value="1" label="Denied but can ask again"/>
+  <int value="2" label="Denied and can't ask again"/>
+</enum>
+
 <enum name="AudioRenderDeviceError">
   <int value="0" label="No error"/>
   <int value="1" label="Error during stream creation"/>
@@ -7653,6 +7662,12 @@
   <int value="11" label="SDP browse failed"/>
   <int value="12" label="GATT browse failed"/>
   <int value="13" label="Unknown"/>
+  <int value="14" label="BT IO connection failed"/>
+  <int value="15" label="Not connected"/>
+  <int value="16" label="Operation not permitted"/>
+  <int value="17" label="Invalid parameters"/>
+  <int value="18" label="Connection refused"/>
+  <int value="19" label="Connection canceled"/>
 </enum>
 
 <enum name="BlueZResultOfPairing">
@@ -31860,6 +31875,8 @@
   <int value="3850" label="PermissionsPolicyHeader"/>
   <int value="3851" label="WebAppManifestUrlHandlers"/>
   <int value="3852" label="LaxAllowingUnsafeCookies"/>
+  <int value="3853" label="V8MediaSession_SetMicrophoneActive_Method"/>
+  <int value="3854" label="V8MediaSession_SetCameraActive_Method"/>
 </enum>
 
 <enum name="FeaturePolicyAllowlistType">
@@ -71490,6 +71507,19 @@
   </int>
 </enum>
 
+<enum name="SimPinOperationResult">
+  <int value="0" label="Success"/>
+  <int value="1" label="Error Device Missing"/>
+  <int value="2" label="Error Failure"/>
+  <int value="3" label="Error Incorrect Pin"/>
+  <int value="4" label="Error Not Found"/>
+  <int value="5" label="Error Not Supported"/>
+  <int value="6" label="Error Pin Blocked"/>
+  <int value="7" label="Error Pin Required"/>
+  <int value="8" label="Error Timeout"/>
+  <int value="9" label="Error Unknown"/>
+</enum>
+
 <enum name="SimpleCache.EntryCreatedAndStream2Omitted">
   <int value="0" label="Stream 2 file was present"/>
   <int value="1" label="Empty stream 2 file was omitted"/>
diff --git a/tools/metrics/histograms/histograms_xml/histogram_suffixes_list.xml b/tools/metrics/histograms/histograms_xml/histogram_suffixes_list.xml
index 39688d7..24edc4c 100644
--- a/tools/metrics/histograms/histograms_xml/histogram_suffixes_list.xml
+++ b/tools/metrics/histograms/histograms_xml/histogram_suffixes_list.xml
@@ -12404,6 +12404,9 @@
   <suffix name="LoadingPredictor"
       label="Provides information about subresources predicted to be on the
              page"/>
+  <suffix name="LoginDetection"
+      label="Provides information about hosts that are identified as commonly
+             logged-in"/>
   <suffix name="None" label="No optimization type"/>
   <suffix name="NoScript"
       label="Disables the fetching and execution of JavaScript">
diff --git a/tools/metrics/histograms/histograms_xml/mobile/histograms.xml b/tools/metrics/histograms/histograms_xml/mobile/histograms.xml
index 33b709f..c8dd806 100644
--- a/tools/metrics/histograms/histograms_xml/mobile/histograms.xml
+++ b/tools/metrics/histograms/histograms_xml/mobile/histograms.xml
@@ -32,6 +32,9 @@
 
 <histogram name="Mobile.AppMenu.SimilarSelection"
     enum="AppMenuSimilarSelectionType" expires_after="M95">
+  <obsolete>
+    Deprecated as of 03/2021.
+  </obsolete>
   <owner>gangwu@chromium.org</owner>
   <owner>twellington@chromium.org</owner>
   <summary>
diff --git a/tools/metrics/histograms/histograms_xml/network/histograms.xml b/tools/metrics/histograms/histograms_xml/network/histograms.xml
index 6370425..f55a3898 100644
--- a/tools/metrics/histograms/histograms_xml/network/histograms.xml
+++ b/tools/metrics/histograms/histograms_xml/network/histograms.xml
@@ -122,6 +122,17 @@
   </summary>
 </histogram>
 
+<histogram name="Network.Cellular.ESim.ProfileDownload.ActivationCode.Latency"
+    units="ms" expires_after="2022-03-01">
+  <owner>azeemarshad@chromium.org</owner>
+  <owner>cros-connectivity@google.com</owner>
+  <owner>hsuregan@chromium.org</owner>
+  <summary>
+    Tracks the time for a profile to be fully downloaded from a provided
+    activation code.
+  </summary>
+</histogram>
+
 <histogram name="Network.Cellular.ESim.ProfileRenameResult"
     enum="BooleanSuccess" expires_after="2022-03-01">
   <owner>azeemarshad@chromium.org</owner>
@@ -179,6 +190,20 @@
   </summary>
 </histogram>
 
+<histogram name="Network.Cellular.Pin.{SimPinOperation}"
+    enum="SimPinOperationResult" expires_after="2022-03-01">
+  <owner>azeemarshad@chromium.org</owner>
+  <owner>cros-system-services-networking@google.com</owner>
+  <owner>hsuregan@chromium.org</owner>
+  <summary>Tracks the rate of success of various pin operations.</summary>
+  <token key="SimPinOperation">
+    <variant name="ChangeSuccess"/>
+    <variant name="LockSuccess"/>
+    <variant name="UnblockSuccess"/>
+    <variant name="UnlockSuccess"/>
+  </token>
+</histogram>
+
 <histogram name="Network.Cellular.PSim.ServiceAtLogin.Count" units="units"
     expires_after="2022-03-01">
   <owner>azeemarshad@chromium.org</owner>
diff --git a/tools/metrics/histograms/histograms_xml/others/histograms.xml b/tools/metrics/histograms/histograms_xml/others/histograms.xml
index a7c33687..7629dfb 100644
--- a/tools/metrics/histograms/histograms_xml/others/histograms.xml
+++ b/tools/metrics/histograms/histograms_xml/others/histograms.xml
@@ -17178,6 +17178,17 @@
   </summary>
 </histogram>
 
+<histogram name="VoiceInteraction.AudioPermissionEvent"
+    enum="AudioPermissionState" expires_after="2021-08-22">
+  <owner>basiaz@google.com</owner>
+  <owner>chrome-language@google.com</owner>
+  <summary>
+    Android: The events related to granting or denying audio permission for
+    system level transcription (as opposite to Assistant). Will be logged each
+    time non-Assistant voice recognition is triggered with a mic button click.
+  </summary>
+</histogram>
+
 <histogram name="VoiceInteraction.DismissedEventSource"
     enum="VoiceInteractionEventSource" expires_after="2021-08-22">
   <owner>wylieb@chromium.org</owner>
diff --git a/tools/metrics/histograms/histograms_xml/uma/histograms.xml b/tools/metrics/histograms/histograms_xml/uma/histograms.xml
index 4bb8e02..1869d1b39 100644
--- a/tools/metrics/histograms/histograms_xml/uma/histograms.xml
+++ b/tools/metrics/histograms/histograms_xml/uma/histograms.xml
@@ -183,7 +183,7 @@
 </histogram>
 
 <histogram name="UMA.JavaCachingRecorder.DroppedHistogramSampleCount"
-    units="samples" expires_after="2021-03-21">
+    units="samples" expires_after="2022-03-21">
   <owner>bttk@chromium.org</owner>
   <owner>src/base/metrics/OWNERS</owner>
   <summary>
@@ -194,7 +194,7 @@
 </histogram>
 
 <histogram name="UMA.JavaCachingRecorder.DroppedUserActionCount"
-    units="samples" expires_after="2021-03-21">
+    units="samples" expires_after="2022-03-21">
   <owner>bttk@chromium.org</owner>
   <owner>src/base/metrics/OWNERS</owner>
   <summary>
@@ -204,7 +204,7 @@
 </histogram>
 
 <histogram name="UMA.JavaCachingRecorder.FlushedHistogramCount"
-    units="histograms" expires_after="2021-03-21">
+    units="histograms" expires_after="2022-03-21">
   <owner>bttk@chromium.org</owner>
   <owner>src/base/metrics/OWNERS</owner>
   <summary>
@@ -213,7 +213,7 @@
 </histogram>
 
 <histogram name="UMA.JavaCachingRecorder.InputHistogramSampleCount"
-    units="samples" expires_after="2021-03-21">
+    units="samples" expires_after="2022-03-21">
   <owner>bttk@chromium.org</owner>
   <owner>src/base/metrics/OWNERS</owner>
   <summary>
@@ -223,7 +223,7 @@
 </histogram>
 
 <histogram name="UMA.JavaCachingRecorder.InputUserActionCount" units="samples"
-    expires_after="2021-03-21">
+    expires_after="2022-03-21">
   <owner>bttk@chromium.org</owner>
   <owner>src/base/metrics/OWNERS</owner>
   <summary>
diff --git a/tools/perf/core/perf_data_generator.py b/tools/perf/core/perf_data_generator.py
index fa8df83..eefb6bd 100755
--- a/tools/perf/core/perf_data_generator.py
+++ b/tools/perf/core/perf_data_generator.py
@@ -101,7 +101,7 @@
 ]
 
 # This is an opt-in list for builders which uses dynamic sharding.
-DYNAMIC_SHARDING_TESTERS = ['android-pixel2-perf-fyi']
+DYNAMIC_SHARDING_TESTERS = ['android-pixel2-perf-fyi', 'linux-perf-calibration']
 
 CALIBRATION_BUILDERS = {
     'linux-perf-calibration': {
@@ -221,20 +221,20 @@
         },
     },
     'fuchsia-perf-fyi': {
-        # TODO(rohpavone): Temporarily using telemetry_gpu_tests until custom
-        # test is created to run telemetry benchmarks, as this gets infra up.
         'tests': [{
             'isolate':
-            'fuchsia_telemetry_gpu_integration_test',
+            'performance_web_engine_test_suite',
             'extra_args': [
-                'hardware_accelerated_feature', '--show-stdout',
-                '--browser=web-engine-shell', '--passthrough', '-v',
-                '--extra-browser-args=--enable-logging=stderr --js-flags=--expose-gc',
+                '--output-format=histograms',
+                '--experimental-tbmv3-metrics',
                 '--device=custom',
-                '--custom-device-target=internal.astro_target'
+                '--custom-device-target=internal.astro_target',
+                # TODO(rohpavone): Remove this to enable other stories once
+                # script has stabilized.
+                '--story-filter=load:chrome:blank',
             ],
             'type':
-            TEST_TYPES.GENERIC,
+            TEST_TYPES.TELEMETRY,
         }],
         'platform':
         'fuchsia',
@@ -935,6 +935,7 @@
             # chromium_swarming recipe_module ignore this dimension.
             'gpu': None,
             'os': 'ChromeOS',
+            'device_status': 'available',
             'device_type': 'eve',
         },
     },
@@ -1339,6 +1340,8 @@
   elif (tester_config['platform'] == 'win'
     and tester_config['target_bits'] == 64):
     browser_name = 'release_x64'
+  elif tester_config['platform'] == 'fuchsia':
+    browser_name = 'web-engine-shell'
   else:
     browser_name ='release'
   test_args = [
diff --git a/tools/perf/core/perf_json_config_validator.py b/tools/perf/core/perf_json_config_validator.py
index 7c9830f..448e902 100644
--- a/tools/perf/core/perf_json_config_validator.py
+++ b/tools/perf/core/perf_json_config_validator.py
@@ -11,8 +11,8 @@
 
 _VALID_SWARMING_DIMENSIONS = {
     'gpu', 'device_ids', 'os', 'pool', 'perf_tests', 'perf_tests_with_args',
-    'cpu', 'device_os', 'device_type', 'device_os_flavor', 'id', 'mac_model',
-    'synthetic_product_name'
+    'cpu', 'device_os', 'device_status', 'device_type', 'device_os_flavor',
+    'id', 'mac_model', 'synthetic_product_name'
 }
 _DEFAULT_VALID_PERF_POOLS = {
     'chrome.tests.perf',
diff --git a/tools/perf/core/perfetto_binary_roller/binary_deps.json b/tools/perf/core/perfetto_binary_roller/binary_deps.json
index 5aacacc..f3a89354 100644
--- a/tools/perf/core/perfetto_binary_roller/binary_deps.json
+++ b/tools/perf/core/perfetto_binary_roller/binary_deps.json
@@ -2,15 +2,15 @@
     "trace_processor_shell": {
         "win": {
             "hash": "c490b300106dbf909633a65af57bb3ab0e41befb",
-            "remote_path": "perfetto_binaries/trace_processor_shell/win/b0d9de4c263359c154e3ca31af9d40055f9840ad/trace_processor_shell.exe"
+            "remote_path": "perfetto_binaries/trace_processor_shell/win/6ecb51ac72ea4de3663b4442ad3e72b64f660af1/trace_processor_shell.exe"
         },
         "mac": {
             "hash": "682dc60bd132e8a5f26c37231cb3f221caf20e57",
-            "remote_path": "perfetto_binaries/trace_processor_shell/mac/b0d9de4c263359c154e3ca31af9d40055f9840ad/trace_processor_shell"
+            "remote_path": "perfetto_binaries/trace_processor_shell/mac/6ecb51ac72ea4de3663b4442ad3e72b64f660af1/trace_processor_shell"
         },
         "linux": {
             "hash": "36eed8cb39578770709ab41a3a76444f5c1ec727",
-            "remote_path": "perfetto_binaries/trace_processor_shell/linux/3243c529bd7e2e1f3a0163ccedb680f4e3cdf335/trace_processor_shell"
+            "remote_path": "perfetto_binaries/trace_processor_shell/linux/6ecb51ac72ea4de3663b4442ad3e72b64f660af1/trace_processor_shell"
         }
     },
     "power_profile.sql": {
diff --git a/tools/perf/core/results_dashboard.py b/tools/perf/core/results_dashboard.py
index cea7ec24..20dd157 100755
--- a/tools/perf/core/results_dashboard.py
+++ b/tools/perf/core/results_dashboard.py
@@ -177,7 +177,7 @@
   subprocess.check_call(cmd)
 
 
-def MakeListOfPoints(charts, bot, test_name, buildername,
+def MakeListOfPoints(charts, bot, test_name, project, buildbucket, buildername,
                      buildnumber, supplemental_columns,
                      perf_dashboard_machine_group,
                      revisions_dict=None):
@@ -223,6 +223,9 @@
       result['supplemental_columns'].update(revision_columns)
       result['supplemental_columns'].update(
           _GetStdioUriColumn(test_name, buildername, buildnumber))
+      result['supplemental_columns'].update(
+          _GetBuildStatusUriColumn(project, buildbucket, buildername,
+                                   buildnumber))
       result['supplemental_columns'].update(supplemental_columns)
 
       result['value'] = trace_values[0]
@@ -239,7 +242,8 @@
   return results
 
 
-def MakeDashboardJsonV1(chart_json, revision_dict, test_name, bot, buildername,
+def MakeDashboardJsonV1(chart_json, revision_dict, test_name, bot, project,
+                        buildbucket, buildername,
                         buildnumber, supplemental_dict, is_ref,
                         perf_dashboard_machine_group):
   """Generates Dashboard JSON in the new Telemetry format.
@@ -277,6 +281,8 @@
 
   supplemental.update(
       _GetStdioUriColumn(test_name, buildername, buildnumber))
+  supplemental.update(
+      _GetBuildStatusUriColumn(project, buildbucket, buildername, buildnumber))
 
   # TODO(sullivan): The android recipe sends "test_name.reference"
   # while the desktop one just sends "test_name" for ref builds. Need
@@ -315,7 +321,7 @@
   # converting all perf bots to LUCI (crbug.com/803137).
   if not (project and buildbucket and buildnumber and buildnumber):
     return None
-  return 'https://ci.chromium.org/p/%s/builders/%s/%s/%s' % (
+  return 'https://ci.chromium.org/ui/p/%s/builders/%s/%s/%s' % (
       urllib.quote(project),
       urllib.quote(buildbucket),
       urllib.quote(buildername),
@@ -330,6 +336,14 @@
   return _CreateLinkColumn('stdio_uri', 'Buildbot stdio', url)
 
 
+def _GetBuildStatusUriColumn(project, buildbucket, buildername, buildnumber):
+  """Gets a supplemental column containing buildbot status link."""
+  url = _MakeBuildStatusUrl(project, buildbucket, buildername, buildnumber)
+  if not url:
+    return {}
+  return _CreateLinkColumn('build_uri', 'Buildbot status page', url)
+
+
 def _CreateLinkColumn(name, label, url):
   """Returns a column containing markdown link to show on dashboard."""
   return {'a_' + name: '[%s](%s)' % (label, url)}
diff --git a/tools/perf/core/upload_results_to_perf_dashboard.py b/tools/perf/core/upload_results_to_perf_dashboard.py
index b981816..f04ae952 100755
--- a/tools/perf/core/upload_results_to_perf_dashboard.py
+++ b/tools/perf/core/upload_results_to_perf_dashboard.py
@@ -55,6 +55,7 @@
     # pylint: disable=redefined-variable-type
     dashboard_json = results_dashboard.MakeListOfPoints(
       results, options.configuration_name, stripped_test_name,
+      options.project, options.buildbucket,
       options.buildername, options.buildnumber, {},
       options.perf_dashboard_machine_group,
       revisions_dict=revisions)
@@ -62,6 +63,7 @@
     dashboard_json = results_dashboard.MakeDashboardJsonV1(
       results,
       revisions, stripped_test_name, options.configuration_name,
+      options.project, options.buildbucket,
       options.buildername, options.buildnumber,
       {}, reference_build,
       perf_dashboard_machine_group=options.perf_dashboard_machine_group)
diff --git a/ui/color/core_default_color_mixer.cc b/ui/color/core_default_color_mixer.cc
index 36ae809..0e443fe3 100644
--- a/ui/color/core_default_color_mixer.cc
+++ b/ui/color/core_default_color_mixer.cc
@@ -22,42 +22,39 @@
 
 ColorMixer& AddMixerForDarkMode(ColorProvider* provider, bool high_contrast) {
   ColorMixer& mixer = provider->AddMixer();
-  if (!high_contrast) {
-    mixer.AddSet(
-        {kColorSetCoreDefaults,
-         {
-             {kColorAccent, gfx::kGoogleBlue300},
-             {kColorAlertHighSeverity, gfx::kGoogleRed300},
-             {kColorAlertLowSeverity, gfx::kGoogleGreen300},
-             {kColorAlertMediumSeverity, gfx::kGoogleYellow300},
-             {kColorMidground, gfx::kGoogleGrey800},
-             {kColorPrimaryBackground, SkColorSetRGB(0x29, 0x2A, 0x2D)},
-             {kColorPrimaryForeground, gfx::kGoogleGrey200},
-             {kColorSecondaryForeground, gfx::kGoogleGrey500},
-             {kColorSubtleEmphasisBackground, SkColorSetRGB(0x32, 0x36, 0x39)},
-             {kColorTextSelectionBackground, gfx::kGoogleBlue800},
-         }});
-  }
+  mixer.AddSet(
+      {kColorSetCoreDefaults,
+       {
+           {kColorAccent, gfx::kGoogleBlue300},
+           {kColorAlertHighSeverity, gfx::kGoogleRed300},
+           {kColorAlertLowSeverity, gfx::kGoogleGreen300},
+           {kColorAlertMediumSeverity, gfx::kGoogleYellow300},
+           {kColorMidground, gfx::kGoogleGrey800},
+           {kColorPrimaryBackground, SkColorSetRGB(0x29, 0x2A, 0x2D)},
+           {kColorPrimaryForeground, gfx::kGoogleGrey200},
+           {kColorSecondaryForeground, gfx::kGoogleGrey500},
+           {kColorSubtleEmphasisBackground, SkColorSetRGB(0x32, 0x36, 0x39)},
+           {kColorTextSelectionBackground, gfx::kGoogleBlue800},
+       }});
   return mixer;
 }
 
 ColorMixer& AddMixerForLightMode(ColorProvider* provider, bool high_contrast) {
   ColorMixer& mixer = provider->AddMixer();
-  if (!high_contrast) {
-    mixer.AddSet({kColorSetCoreDefaults,
-                  {
-                      {kColorAccent, gfx::kGoogleBlue600},
-                      {kColorAlertHighSeverity, gfx::kGoogleRed600},
-                      {kColorAlertLowSeverity, gfx::kGoogleGreen700},
-                      {kColorAlertMediumSeverity, gfx::kGoogleYellow700},
-                      {kColorMidground, gfx::kGoogleGrey300},
-                      {kColorPrimaryBackground, SK_ColorWHITE},
-                      {kColorPrimaryForeground, gfx::kGoogleGrey900},
-                      {kColorSecondaryForeground, gfx::kGoogleGrey700},
-                      {kColorSubtleEmphasisBackground, gfx::kGoogleGrey050},
-                      {kColorTextSelectionBackground, gfx::kGoogleBlue200},
-                  }});
-  }
+  mixer.AddSet({kColorSetCoreDefaults,
+                {
+                    {kColorAccent, gfx::kGoogleBlue600},
+                    {kColorAlertHighSeverity, gfx::kGoogleRed600},
+                    {kColorAlertLowSeverity, gfx::kGoogleGreen700},
+                    {kColorAlertMediumSeverity, gfx::kGoogleYellow700},
+                    {kColorMidground, gfx::kGoogleGrey300},
+                    {kColorPrimaryBackground, SK_ColorWHITE},
+                    {kColorPrimaryForeground, gfx::kGoogleGrey900},
+                    {kColorSecondaryForeground, gfx::kGoogleGrey700},
+                    {kColorSubtleEmphasisBackground, gfx::kGoogleGrey050},
+                    {kColorTextSelectionBackground, gfx::kGoogleBlue200},
+                }});
+
   return mixer;
 }
 
diff --git a/ui/color/mac/native_color_mixers.mm b/ui/color/mac/native_color_mixers.mm
index 53cd58c..4cc08cd 100644
--- a/ui/color/mac/native_color_mixers.mm
+++ b/ui/color/mac/native_color_mixers.mm
@@ -37,7 +37,7 @@
 void AddNativeCoreColorMixer(ColorProvider* provider,
                              bool dark_window,
                              bool high_contrast) {
-  ScopedCurrentNSAppearance scoped_nsappearance(dark_window);
+  ScopedCurrentNSAppearance scoped_nsappearance(dark_window, high_contrast);
   ColorMixer& mixer = provider->AddMixer();
   mixer.AddSet({kColorSetNative,
                 {
@@ -50,7 +50,7 @@
 void AddNativeUiColorMixer(ColorProvider* provider,
                            bool dark_window,
                            bool high_contrast) {
-  ScopedCurrentNSAppearance scoped_nsappearance(dark_window);
+  ScopedCurrentNSAppearance scoped_nsappearance(dark_window, high_contrast);
   ColorMixer& mixer = provider->AddMixer();
   mixer.AddSet(
       {kColorSetNative,
@@ -84,12 +84,23 @@
                                      ? SkColorSetA(gfx::kGoogleGrey800, 0xCC)
                                      : SkColorSetA(SK_ColorBLACK, 0x26);
   mixer[kColorMenuSeparator] = {menu_separator_color};
+
+  if (!high_contrast)
+    return;
+
+  if (dark_window) {
+    mixer[kColorMenuItemForegroundSelected] = {SK_ColorBLACK};
+    mixer[kColorMenuItemBackgroundSelected] = {SK_ColorLTGRAY};
+  } else {
+    mixer[kColorMenuItemForegroundSelected] = {SK_ColorWHITE};
+    mixer[kColorMenuItemBackgroundSelected] = {SK_ColorDKGRAY};
+  }
 }
 
 void AddSystemTintMixer(ColorProvider* provider) {
   ColorMixer& mixer = provider->AddMixer();
 
-  for (ui::ColorId id = ui::kUiColorsStart; id < kUiColorsLast; ++id) {
+  for (ColorId id = kUiColorsStart; id < kUiColorsEnd; ++id) {
     // Apply system tint to non-OS colors.
     if (!kNativeOSColorIds.contains(id))
       mixer[id] += ApplySystemControlTintIfNeeded();
diff --git a/ui/color/mac/scoped_current_nsappearance.h b/ui/color/mac/scoped_current_nsappearance.h
index 7b134c1..73e8901c 100644
--- a/ui/color/mac/scoped_current_nsappearance.h
+++ b/ui/color/mac/scoped_current_nsappearance.h
@@ -13,7 +13,7 @@
 // based on the desired light/dark colors scheme.
 class COMPONENT_EXPORT(COLOR) ScopedCurrentNSAppearance {
  public:
-  explicit ScopedCurrentNSAppearance(bool dark);
+  explicit ScopedCurrentNSAppearance(bool dark, bool high_contrast);
 
   // There should be no reason to copy or move a ScopedCurrentNSAppearance.
   ScopedCurrentNSAppearance(const ScopedCurrentNSAppearance&) = delete;
diff --git a/ui/color/mac/scoped_current_nsappearance.mm b/ui/color/mac/scoped_current_nsappearance.mm
index 62bfad44..35040b0 100644
--- a/ui/color/mac/scoped_current_nsappearance.mm
+++ b/ui/color/mac/scoped_current_nsappearance.mm
@@ -7,10 +7,20 @@
 #import <Cocoa/Cocoa.h>
 
 namespace ui {
-ScopedCurrentNSAppearance::ScopedCurrentNSAppearance(bool dark) {
+ScopedCurrentNSAppearance::ScopedCurrentNSAppearance(bool dark,
+                                                     bool high_contrast) {
   if (@available(macOS 10.14, *)) {
-    NSAppearanceName appearance =
-        dark ? NSAppearanceNameDarkAqua : NSAppearanceNameAqua;
+    NSAppearanceName appearance;
+
+    if (dark) {
+      appearance = high_contrast
+                       ? NSAppearanceNameAccessibilityHighContrastDarkAqua
+                       : NSAppearanceNameDarkAqua;
+    } else {
+      appearance = high_contrast ? NSAppearanceNameAccessibilityHighContrastAqua
+                                 : NSAppearanceNameAqua;
+    }
+
     [NSAppearance
         setCurrentAppearance:[NSAppearance appearanceNamed:appearance]];
   }
diff --git a/ui/color/win/native_color_mixers.cc b/ui/color/win/native_color_mixers.cc
index 0b909ba8..d86b5f8 100644
--- a/ui/color/win/native_color_mixers.cc
+++ b/ui/color/win/native_color_mixers.cc
@@ -27,6 +27,9 @@
 #define MAP(chrome, native) {chrome, color_utils::GetSysSkColor(native)}
   ColorMixer& mixer = provider->AddMixer();
 
+  if (!high_contrast)
+    return;
+
   mixer.AddSet(
       {kColorSetNative,
        {
@@ -63,9 +66,6 @@
            MAP(kColorNativeWindowText, COLOR_WINDOWTEXT),
        }});
 
-  if (!high_contrast)
-    return;
-
   // Window Background
   mixer[kColorPrimaryBackground] = {kColorNativeWindow};
   mixer[kColorWindowBackground] = {kColorNativeWindow};
diff --git a/ui/compositor/total_animation_throughput_reporter.cc b/ui/compositor/total_animation_throughput_reporter.cc
index 7f9e8012..90fbdce 100644
--- a/ui/compositor/total_animation_throughput_reporter.cc
+++ b/ui/compositor/total_animation_throughput_reporter.cc
@@ -44,6 +44,9 @@
     ui::Compositor* compositor) {
   throughput_tracker_->Stop();
   throughput_tracker_.reset();
+  // Stop observing if no need to report multiple times.
+  if (report_repeating_callback_.is_null())
+    compositor_->RemoveObserver(this);
 }
 
 void TotalAnimationThroughputReporter::OnCompositingShuttingDown(
diff --git a/ui/compositor/total_animation_throughput_reporter_unittest.cc b/ui/compositor/total_animation_throughput_reporter_unittest.cc
index e62fba3..f801150 100644
--- a/ui/compositor/total_animation_throughput_reporter_unittest.cc
+++ b/ui/compositor/total_animation_throughput_reporter_unittest.cc
@@ -11,6 +11,7 @@
 #include "base/time/time.h"
 #include "build/build_config.h"
 #include "cc/metrics/frame_sequence_metrics.h"
+#include "ui/compositor/compositor_observer.h"
 #include "ui/compositor/layer.h"
 #include "ui/compositor/layer_animation_sequence.h"
 #include "ui/compositor/layer_animator.h"
@@ -252,6 +253,32 @@
   EXPECT_TRUE(checker.WaitUntilReported());
 }
 
+namespace {
+
+class ObserverChecker : public ui::CompositorObserver {
+ public:
+  ObserverChecker(ui::Compositor* compositor,
+                  ui::CompositorObserver* reporter_observer)
+      : reporter_observer_(reporter_observer) {
+    EXPECT_TRUE(compositor->HasObserver(reporter_observer_));
+    compositor->AddObserver(this);
+  }
+  ObserverChecker(const ObserverChecker&) = delete;
+  ObserverChecker& operator=(const ObserverChecker&) = delete;
+  ~ObserverChecker() override = default;
+
+  // ui::CompositorObserver:
+  void OnLastAnimationEnded(ui::Compositor* compositor) override {
+    EXPECT_FALSE(compositor->HasObserver(reporter_observer_));
+    compositor->RemoveObserver(this);
+  }
+
+ private:
+  ui::CompositorObserver* const reporter_observer_;
+};
+
+}  // namespace
+
 // Make sure the once reporter is called only once.
 TEST_F(TotalAnimationThroughputReporterTest, OnceReporter) {
   Layer layer;
@@ -267,6 +294,10 @@
   TotalAnimationThroughputReporter reporter(
       compositor(), checker.once_callback(), /*should_delete=*/false);
 
+  // Make sure the TotalAnimationThroughputReporter removes itself
+  // from compositor as observer.
+  ObserverChecker observer_checker(compositor(), &reporter);
+
   // Report data for animation of opacity goes to 1.
   layer.SetOpacity(1.0f);
   EXPECT_TRUE(checker.WaitUntilReported());
diff --git a/ui/gl/init/gl_initializer_linux_x11.cc b/ui/gl/init/gl_initializer_linux_x11.cc
index b1f21c4..d4f6b70 100644
--- a/ui/gl/init/gl_initializer_linux_x11.cc
+++ b/ui/gl/init/gl_initializer_linux_x11.cc
@@ -149,11 +149,7 @@
   }
 #endif  // !BUILDFLAG(USE_STATIC_ANGLE)
 
-  if (implementation == kGLImplementationEGLANGLE) {
-    SetGLImplementation(kGLImplementationEGLANGLE);
-  } else {
-    SetGLImplementation(kGLImplementationEGLGLES2);
-  }
+  SetGLImplementation(implementation);
 
   InitializeStaticGLBindingsGL();
   InitializeStaticGLBindingsEGL();
diff --git a/ui/message_center/public/cpp/message_center_constants.h b/ui/message_center/public/cpp/message_center_constants.h
index 8a265b5..d3cf0a1 100644
--- a/ui/message_center/public/cpp/message_center_constants.h
+++ b/ui/message_center/public/cpp/message_center_constants.h
@@ -58,17 +58,6 @@
 const int kMessageFontSize = 12;      // For everything but title.
 const int kMessageLineHeight = 18;    // In DIPs.
 
-// Colors.
-// Background of the card.
-constexpr SkColor kNotificationBackgroundColor = SK_ColorWHITE;
-// The focus border.
-constexpr SkColor kFocusBorderColor = SkColorSetRGB(64, 128, 250);
-// Foreground of small icon image.
-constexpr SkColor kSmallImageMaskForegroundColor = SK_ColorWHITE;
-// Background of small icon image.
-constexpr SkColor kSmallImageMaskBackgroundColor =
-    SkColorSetRGB(0xa3, 0xa3, 0xa3);
-
 // For list notifications.
 // Not used when --enabled-new-style-notification is set.
 const size_t kNotificationMaximumItems = 5;
diff --git a/ui/message_center/public/cpp/notification.cc b/ui/message_center/public/cpp/notification.cc
index b900fde..416feb9 100644
--- a/ui/message_center/public/cpp/notification.cc
+++ b/ui/message_center/public/cpp/notification.cc
@@ -134,18 +134,28 @@
          origin_url_.SchemeIsHTTPOrHTTPS();
 }
 
-gfx::Image Notification::GenerateMaskedSmallIcon(int dip_size,
-                                                 SkColor color) const {
+gfx::Image Notification::GenerateMaskedSmallIcon(
+    int dip_size,
+    SkColor mask_color,
+    SkColor background_color,
+    SkColor foreground_color) const {
   if (!vector_small_image().is_empty())
     return gfx::Image(
-        gfx::CreateVectorIcon(vector_small_image(), dip_size, color));
+        gfx::CreateVectorIcon(vector_small_image(), dip_size, mask_color));
 
   if (small_image().IsEmpty())
     return gfx::Image();
 
   // If |vector_small_image| is not available, fallback to raster based
   // masking and resizing.
-  gfx::ImageSkia image = small_image().AsImageSkia();
+  gfx::ImageSkia image;
+  if (small_image_needs_additional_masking()) {
+    image = GetMaskedSmallImage(small_image().AsImageSkia(), background_color,
+                                foreground_color)
+                .AsImageSkia();
+  } else {
+    image = small_image().AsImageSkia();
+  }
 
 #if BUILDFLAG(IS_CHROMEOS_ASH)
   bool create_masked_image =
@@ -156,7 +166,8 @@
 
   if (create_masked_image) {
     image = gfx::ImageSkiaOperations::CreateMaskedImage(
-        CreateSolidColorImage(image.width(), image.height(), color), image);
+        CreateSolidColorImage(image.width(), image.height(), mask_color),
+        image);
   }
   gfx::ImageSkia resized = gfx::ImageSkiaOperations::CreateResizedImage(
       image, skia::ImageOperations::ResizeMethod::RESIZE_BEST,
@@ -164,4 +175,22 @@
   return gfx::Image(resized);
 }
 
+// Take the alpha channel of small_image, mask it with the foreground,
+// then add the masked foreground on top of the background
+gfx::Image Notification::GetMaskedSmallImage(const gfx::ImageSkia& small_image,
+                                             SkColor background_color,
+                                             SkColor foreground_color) const {
+  int width = small_image.width();
+  int height = small_image.height();
+
+  const gfx::ImageSkia background =
+      CreateSolidColorImage(width, height, background_color);
+  const gfx::ImageSkia foreground =
+      CreateSolidColorImage(width, height, foreground_color);
+  const gfx::ImageSkia masked_small_image =
+      gfx::ImageSkiaOperations::CreateMaskedImage(foreground, small_image);
+  return gfx::Image(gfx::ImageSkiaOperations::CreateSuperimposedImage(
+      background, masked_small_image));
+}
+
 }  // namespace message_center
diff --git a/ui/message_center/public/cpp/notification.h b/ui/message_center/public/cpp/notification.h
index cbd8c0c60..9aa29701 100644
--- a/ui/message_center/public/cpp/notification.h
+++ b/ui/message_center/public/cpp/notification.h
@@ -106,6 +106,11 @@
   // notification. Optional.
   gfx::Image small_image;
 
+  // If true, the small image should be masked with the foreground and then
+  // added on top of the background. Masking is delayed until the notification
+  // is in the views hierarchy or about to be passed to the OS.
+  bool small_image_needs_additional_masking = false;
+
 #if BUILDFLAG(IS_CHROMEOS_ASH)
   // If true, we simply use the raw |small_image| icon, ignoring accent color
   // styling. For example, this is used with raw icons received from Android.
@@ -347,6 +352,14 @@
     optional_fields_.small_image = image;
   }
 
+  bool small_image_needs_additional_masking() const {
+    return optional_fields_.small_image_needs_additional_masking;
+  }
+  void set_small_image_needs_additional_masking(bool needs_additional_masking) {
+    optional_fields_.small_image_needs_additional_masking =
+        needs_additional_masking;
+  }
+
   const gfx::VectorIcon& vector_small_image() const {
     return *optional_fields_.vector_small_image;
   }
@@ -362,7 +375,14 @@
   // filled by the |color|.
   // Otherwise, it uses alpha channel of the rasterized |small_image| for
   // masking.
-  gfx::Image GenerateMaskedSmallIcon(int dip_size, SkColor color) const;
+  gfx::Image GenerateMaskedSmallIcon(int dip_size,
+                                     SkColor mask_color,
+                                     SkColor background_color,
+                                     SkColor foreground_color) const;
+
+  gfx::Image GetMaskedSmallImage(const gfx::ImageSkia& small_image,
+                                 SkColor background_color,
+                                 SkColor foreground_color) const;
 
   // Buttons, with icons fetched asynchronously.
   const std::vector<ButtonInfo>& buttons() const {
diff --git a/ui/message_center/views/notification_view_md.cc b/ui/message_center/views/notification_view_md.cc
index 1a47b7e..cb5efde 100644
--- a/ui/message_center/views/notification_view_md.cc
+++ b/ui/message_center/views/notification_view_md.cc
@@ -1022,10 +1022,15 @@
           accent_color, GetNotificationHeaderViewBackgroundColor())
           .color;
 
+  auto* theme = GetNativeTheme();
   // TODO(knollr): figure out if this has a performance impact and
   // cache images if so. (crbug.com/768748)
-  gfx::Image masked_small_icon =
-      notification.GenerateMaskedSmallIcon(kSmallImageSizeMD, icon_color);
+  gfx::Image masked_small_icon = notification.GenerateMaskedSmallIcon(
+      kSmallImageSizeMD, icon_color,
+      theme->GetSystemColor(
+          ui::NativeTheme::kColorId_MessageCenterSmallImageMaskBackground),
+      theme->GetSystemColor(
+          ui::NativeTheme::kColorId_MessageCenterSmallImageMaskForeground));
 
   if (masked_small_icon.IsEmpty()) {
     header_row_->ClearAppIcon();
diff --git a/ui/message_center/views/notification_view_md_unittest.cc b/ui/message_center/views/notification_view_md_unittest.cc
index f06c339..cd00246 100644
--- a/ui/message_center/views/notification_view_md_unittest.cc
+++ b/ui/message_center/views/notification_view_md_unittest.cc
@@ -1012,9 +1012,18 @@
     notification_view()->ToggleExpanded();
   EXPECT_TRUE(notification_view()->actions_row_->GetVisible());
 
+  auto* theme = notification_view()->GetNativeTheme();
   auto app_icon_color_matches = [&](SkColor color) {
     SkBitmap expected =
-        notification->GenerateMaskedSmallIcon(kSmallImageSizeMD, color)
+        notification
+            ->GenerateMaskedSmallIcon(
+                kSmallImageSizeMD, color,
+                theme->GetSystemColor(
+                    ui::NativeTheme::
+                        kColorId_MessageCenterSmallImageMaskBackground),
+                theme->GetSystemColor(
+                    ui::NativeTheme::
+                        kColorId_MessageCenterSmallImageMaskForeground))
             .AsBitmap();
     SkBitmap actual = *notification_view()
                            ->header_row_->app_icon_view_for_testing()
diff --git a/ui/native_theme/common_theme.cc b/ui/native_theme/common_theme.cc
index 082e12f..ec16386 100644
--- a/ui/native_theme/common_theme.cc
+++ b/ui/native_theme/common_theme.cc
@@ -316,6 +316,12 @@
           accent, (color_id == kInitial) ? 0x4D : gfx::kGoogleGreyAlpha200);
     }
 
+    // Message Center
+    case NativeTheme::kColorId_MessageCenterSmallImageMaskForeground:
+      return SK_ColorWHITE;
+    case NativeTheme::kColorId_MessageCenterSmallImageMaskBackground:
+      return SkColorSetRGB(0xa3, 0xa3, 0xa3);
+
     // Notification
     case NativeTheme::kColorId_NotificationBackground:
     case NativeTheme::kColorId_NotificationColor:
diff --git a/ui/native_theme/native_theme.cc b/ui/native_theme/native_theme.cc
index 3ee6518..3396f06 100644
--- a/ui/native_theme/native_theme.cc
+++ b/ui/native_theme/native_theme.cc
@@ -300,7 +300,12 @@
 
 bool NativeTheme::AllowColorPipelineRedirection(
     ColorScheme color_scheme) const {
-  return color_scheme != ColorScheme::kPlatformHighContrast;
+  // TODO(kerenzhu): Don't use UserHasContrastPreference().
+  // ColorScheme should encode high contrast info but currently on mac it does
+  // not. ColorScheme should also allow combination of light/dark mode with high
+  // contrast.
+  return color_scheme != ColorScheme::kPlatformHighContrast &&
+         !UserHasContrastPreference();
 }
 
 SkColor NativeTheme::GetSystemColorDeprecated(ColorId color_id,
diff --git a/ui/native_theme/native_theme_color_id.h b/ui/native_theme/native_theme_color_id.h
index efb0f865..3117e88 100644
--- a/ui/native_theme/native_theme_color_id.h
+++ b/ui/native_theme/native_theme_color_id.h
@@ -86,6 +86,9 @@
   OP(kColorId_OverlayScrollbarThumbHoveredFill),                               \
   OP(kColorId_OverlayScrollbarThumbHoveredStroke),                             \
   OP(kColorId_OverlayScrollbarThumbStroke),                                    \
+  /* Message Center */ \
+  OP(kColorId_MessageCenterSmallImageMaskBackground),                          \
+  OP(kColorId_MessageCenterSmallImageMaskForeground),                          \
   /* Notification view */                                                      \
   OP(kColorId_NotificationBackground),                                         \
   OP(kColorId_NotificationBackgroundActive),                                   \
diff --git a/ui/native_theme/native_theme_mac.mm b/ui/native_theme/native_theme_mac.mm
index dab062c..3ac43db 100644
--- a/ui/native_theme/native_theme_mac.mm
+++ b/ui/native_theme/native_theme_mac.mm
@@ -145,7 +145,7 @@
 SkColor NativeThemeMac::GetSystemColorDeprecated(ColorId color_id,
                                                  ColorScheme color_scheme,
                                                  bool apply_processing) const {
-  if (UserHasContrastPreference()) {
+  if (GetPreferredContrast() == PreferredContrast::kMore) {
     switch (color_id) {
       case kColorId_SelectedMenuItemForegroundColor:
         return color_scheme == ColorScheme::kDark ? SK_ColorBLACK
@@ -170,8 +170,9 @@
 base::Optional<SkColor> NativeThemeMac::GetOSColor(
     ColorId color_id,
     ColorScheme color_scheme) const {
-  ScopedCurrentNSAppearance scoped_nsappearance(color_scheme ==
-                                                ColorScheme::kDark);
+  ScopedCurrentNSAppearance scoped_nsappearance(
+      color_scheme == ColorScheme::kDark,
+      GetPreferredContrast() == PreferredContrast::kMore);
 
   // Even with --secondary-ui-md, menus use the platform colors and styling, and
   // Mac has a couple of specific color overrides, documented below.
diff --git a/ui/native_theme/native_theme_unittest.cc b/ui/native_theme/native_theme_unittest.cc
index b96b655..1ea1f3e 100644
--- a/ui/native_theme/native_theme_unittest.cc
+++ b/ui/native_theme/native_theme_unittest.cc
@@ -7,6 +7,7 @@
 #include <ostream>
 #include <tuple>
 
+#include "base/notreached.h"
 #include "base/strings/stringprintf.h"
 #include "base/test/scoped_feature_list.h"
 #include "build/build_config.h"
@@ -18,6 +19,12 @@
 #include "ui/color/mac/system_color_utils.h"
 #endif
 
+namespace {
+
+enum class ContrastMode { kNonHighContrast, kHighContrast };
+
+}  // namespace
+
 namespace ui {
 namespace {
 
@@ -47,17 +54,22 @@
 }
 
 class NativeThemeRedirectedEquivalenceTest
-    : public testing::TestWithParam<
-          std::tuple<NativeTheme::ColorScheme, NativeTheme::ColorId>> {
+    : public testing::TestWithParam<std::tuple<NativeTheme::ColorScheme,
+                                               ContrastMode,
+                                               NativeTheme::ColorId>> {
  public:
   NativeThemeRedirectedEquivalenceTest() = default;
 
   static std::string ParamInfoToString(
       ::testing::TestParamInfo<std::tuple<NativeTheme::ColorScheme,
+                                          ContrastMode,
                                           NativeTheme::ColorId>> param_info) {
     auto param_tuple = param_info.param;
-    return ColorSchemeToString(std::get<0>(param_tuple)) + "_With_" +
-           ColorIdToString(std::get<1>(param_tuple));
+    return ColorSchemeToString(
+               std::get<NativeTheme::ColorScheme>(param_tuple)) +
+           ContrastModeToString(std::get<ContrastMode>(param_tuple)) +
+           "_With_" +
+           ColorIdToString(std::get<NativeTheme::ColorId>(param_tuple));
   }
 
  private:
@@ -76,6 +88,18 @@
     }
   }
 
+  static std::string ContrastModeToString(ContrastMode contrast_mode) {
+    switch (contrast_mode) {
+      case ContrastMode::kNonHighContrast:
+        return "";
+      case ContrastMode::kHighContrast:
+        return "HighContrast";
+      default:
+        NOTREACHED();
+        return "InvalidContrastMode";
+    }
+  }
+
   static std::string ColorIdToString(NativeTheme::ColorId id) {
     if (id >= NativeTheme::ColorId::kColorId_NumColors) {
       NOTREACHED() << "Invalid color value " << id;
@@ -87,8 +111,17 @@
 
 std::pair<PrintableSkColor, PrintableSkColor> GetOriginalAndRedirected(
     NativeTheme::ColorId color_id,
-    NativeTheme::ColorScheme color_scheme) {
+    NativeTheme::ColorScheme color_scheme,
+    ContrastMode contrast_mode) {
   NativeTheme* native_theme = NativeTheme::GetInstanceForNativeUi();
+
+  if (contrast_mode == ContrastMode::kHighContrast) {
+#if defined(OS_WIN)
+    color_scheme = NativeTheme::ColorScheme::kPlatformHighContrast;
+#endif
+    native_theme->set_preferred_contrast(NativeTheme::PreferredContrast::kMore);
+  }
+
   PrintableSkColor original{
       native_theme->GetSystemColor(color_id, color_scheme)};
 
@@ -96,6 +129,8 @@
   scoped_feature_list.InitAndEnableFeature(features::kColorProviderRedirection);
   PrintableSkColor redirected{
       native_theme->GetSystemColor(color_id, color_scheme)};
+  native_theme->set_preferred_contrast(
+      NativeTheme::PreferredContrast::kNoPreference);
 
   return std::make_pair(original, redirected);
 }
@@ -105,10 +140,11 @@
 TEST_P(NativeThemeRedirectedEquivalenceTest, NativeUiGetSystemColor) {
   auto param_tuple = GetParam();
   auto color_scheme = std::get<NativeTheme::ColorScheme>(param_tuple);
+  auto contrast_mode = std::get<ContrastMode>(param_tuple);
   auto color_id = std::get<NativeTheme::ColorId>(param_tuple);
 
   // Verifies that colors with and without the Color Provider are the same.
-  auto pair = GetOriginalAndRedirected(color_id, color_scheme);
+  auto pair = GetOriginalAndRedirected(color_id, color_scheme, contrast_mode);
   auto original = pair.first;
   auto redirected = pair.second;
   EXPECT_EQ(original, redirected);
@@ -118,11 +154,12 @@
 TEST_P(NativeThemeRedirectedEquivalenceTest, NativeUiGetSystemColorWithTint) {
   auto param_tuple = GetParam();
   auto color_scheme = std::get<NativeTheme::ColorScheme>(param_tuple);
+  auto contrast_mode = std::get<ContrastMode>(param_tuple);
   auto color_id = std::get<NativeTheme::ColorId>(param_tuple);
 
   ScopedEnableGraphiteTint enable_graphite_tint;
   // Verifies that colors with and without the Color Provider are the same.
-  auto pair = GetOriginalAndRedirected(color_id, color_scheme);
+  auto pair = GetOriginalAndRedirected(color_id, color_scheme, contrast_mode);
   auto original = pair.first;
   auto redirected = pair.second;
   EXPECT_EQ(original, redirected);
@@ -133,11 +170,11 @@
 INSTANTIATE_TEST_SUITE_P(
     ,
     NativeThemeRedirectedEquivalenceTest,
-    ::testing::Combine(
-        ::testing::Values(NativeTheme::ColorScheme::kLight,
-                          NativeTheme::ColorScheme::kDark,
-                          NativeTheme::ColorScheme::kPlatformHighContrast),
-        ::testing::Values(NATIVE_THEME_COLOR_IDS)),
+    ::testing::Combine(::testing::Values(NativeTheme::ColorScheme::kLight,
+                                         NativeTheme::ColorScheme::kDark),
+                       ::testing::Values(ContrastMode::kNonHighContrast,
+                                         ContrastMode::kHighContrast),
+                       ::testing::Values(NATIVE_THEME_COLOR_IDS)),
     NativeThemeRedirectedEquivalenceTest::ParamInfoToString);
 #undef OP
 
diff --git a/ui/ozone/platform/wayland/host/wayland_auxiliary_window.cc b/ui/ozone/platform/wayland/host/wayland_auxiliary_window.cc
index 7d8cb10..c22d8be 100644
--- a/ui/ozone/platform/wayland/host/wayland_auxiliary_window.cc
+++ b/ui/ozone/platform/wayland/host/wayland_auxiliary_window.cc
@@ -30,6 +30,7 @@
 void WaylandAuxiliaryWindow::Hide() {
   if (!subsurface_)
     return;
+  WaylandWindow::Hide();
 
   subsurface_.reset();
 
diff --git a/ui/ozone/platform/wayland/host/wayland_popup.cc b/ui/ozone/platform/wayland/host/wayland_popup.cc
index 73ac973..22b64d9 100644
--- a/ui/ozone/platform/wayland/host/wayland_popup.cc
+++ b/ui/ozone/platform/wayland/host/wayland_popup.cc
@@ -78,6 +78,7 @@
 
   if (child_window())
     child_window()->Hide();
+  WaylandWindow::Hide();
 
   if (shell_popup_) {
     parent_window()->set_child_window(nullptr);
diff --git a/ui/ozone/platform/wayland/host/wayland_toplevel_window.cc b/ui/ozone/platform/wayland/host/wayland_toplevel_window.cc
index 8d86fef3..483ad68 100644
--- a/ui/ozone/platform/wayland/host/wayland_toplevel_window.cc
+++ b/ui/ozone/platform/wayland/host/wayland_toplevel_window.cc
@@ -115,6 +115,7 @@
     child_window()->Hide();
     set_child_window(nullptr);
   }
+  WaylandWindow::Hide();
 
   shell_toplevel_.reset();
   connection()->ScheduleFlush();
diff --git a/ui/ozone/platform/wayland/host/wayland_window.cc b/ui/ozone/platform/wayland/host/wayland_window.cc
index 8926ee3..3089a822 100644
--- a/ui/ozone/platform/wayland/host/wayland_window.cc
+++ b/ui/ozone/platform/wayland/host/wayland_window.cc
@@ -175,7 +175,11 @@
 }
 
 void WaylandWindow::Hide() {
-  NOTREACHED();
+  // Mutter compositor crashes if we don't remove subsurface roles when hiding.
+  if (primary_subsurface_)
+    primary_subsurface()->Hide();
+  for (auto& subsurface : wayland_subsurfaces_)
+    subsurface->Hide();
 }
 
 void WaylandWindow::Close() {
diff --git a/ui/webui/resources/js/icon.js b/ui/webui/resources/js/icon.js
index d0af7fd..5c11999 100644
--- a/ui/webui/resources/js/icon.js
+++ b/ui/webui/resources/js/icon.js
@@ -131,16 +131,18 @@
    * @param {boolean} isSyncedUrlForHistoryUi Should be set to true only if the
    *     caller is an UI aimed at displaying user history, and the requested url
    *     is known to be present in Chrome sync data.
-   * @param {string} remoteIconUrlForUma In case the entry is contained in
-   * sync data, we can pass the associated icon url.
+   * @param {string} remoteIconUrlForUma In case the entry is contained in sync
+   *     data, we can pass the associated icon url.
+   * @param {number} size The favicon size.
    *
    * @return {string} -webkit-image-set for the favicon.
    */
   /* #export */ function getFaviconForPageURL(
-      url, isSyncedUrlForHistoryUi, remoteIconUrlForUma = '') {
+      url, isSyncedUrlForHistoryUi, remoteIconUrlForUma = '', size = 16) {
     // Note: URL param keys used below must match those in the description of
     // chrome://favicon2 format in components/favicon_base/favicon_url_parser.h.
     const faviconUrl = getBaseFaviconUrl();
+    faviconUrl.searchParams.set('size', size);
     faviconUrl.searchParams.set('page_url', url);
     // TODO(dbeam): use the presence of 'allow_google_server_fallback' to
     // indicate true, otherwise false.
diff --git a/weblayer/browser/android/javatests/src/org/chromium/weblayer/test/PageInfoTest.java b/weblayer/browser/android/javatests/src/org/chromium/weblayer/test/PageInfoTest.java
index 65920f6..946a709 100644
--- a/weblayer/browser/android/javatests/src/org/chromium/weblayer/test/PageInfoTest.java
+++ b/weblayer/browser/android/javatests/src/org/chromium/weblayer/test/PageInfoTest.java
@@ -11,6 +11,7 @@
 import static androidx.test.espresso.matcher.ViewMatchers.withText;
 
 import android.content.Context;
+import android.os.Build.VERSION_CODES;
 import android.os.Bundle;
 import android.view.View;
 
@@ -25,6 +26,7 @@
 import org.chromium.base.StrictModeContext;
 import org.chromium.base.test.util.CriteriaHelper;
 import org.chromium.base.test.util.CriteriaNotSatisfiedException;
+import org.chromium.base.test.util.DisableIf;
 import org.chromium.content_public.browser.test.util.TestThreadUtils;
 import org.chromium.weblayer.TestWebLayer;
 import org.chromium.weblayer.shell.InstrumentationActivity;
@@ -104,7 +106,10 @@
 
     @Test
     @SmallTest
-    public void testPageInfoCookiesSubPage() {
+    @DisableIf.Build(message = "Flaky on Android Marshmallow x86, see crbug.com/1188735",
+            sdk_is_greater_than = VERSION_CODES.LOLLIPOP_MR1, sdk_is_less_than = VERSION_CODES.N)
+    public void
+    testPageInfoCookiesSubPage() {
         Bundle extras = new Bundle();
         extras.putBoolean(InstrumentationActivity.EXTRA_URLBAR_TEXT_CLICKABLE, true);
         InstrumentationActivity activity = mActivityTestRule.launchShellWithUrl(
diff --git a/weblayer/browser/java/org/chromium/weblayer_private/WebLayerImpl.java b/weblayer/browser/java/org/chromium/weblayer_private/WebLayerImpl.java
index 4242b19c..515e3bcc 100644
--- a/weblayer/browser/java/org/chromium/weblayer_private/WebLayerImpl.java
+++ b/weblayer/browser/java/org/chromium/weblayer_private/WebLayerImpl.java
@@ -48,6 +48,8 @@
 import org.chromium.base.library_loader.LibraryLoader;
 import org.chromium.base.library_loader.LibraryProcessType;
 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.base.task.TaskTraits;
 import org.chromium.components.browser_ui.contacts_picker.ContactsPickerDialog;
@@ -233,7 +235,13 @@
         // Load library in the background since it may be expensive.
         // TODO(crbug.com/1146438): Look into enabling relro sharing in browser process. It seems to
         // crash when WebView is loaded in the same process.
-        new Thread(() -> LibraryLoader.getInstance().loadNow()).start();
+        new BackgroundOnlyAsyncTask<Void>() {
+            @Override
+            protected Void doInBackground() {
+                LibraryLoader.getInstance().loadNow();
+                return null;
+            }
+        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
 
         PackageInfo packageInfo = WebViewFactory.getLoadedPackageInfo();