diff --git a/DEPS b/DEPS
index 4d29fec..df17d94 100644
--- a/DEPS
+++ b/DEPS
@@ -217,11 +217,11 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling Skia
   # and whatever else without interference from each other.
-  'skia_revision': 'e5894fce585de839c3abe382ad8938e52925f8a9',
+  'skia_revision': '8f9f763ec510c3c510b1de8b63cf89089953c5e7',
   # 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': '8c956ecc96fd13e8f24017d41591d3f5c9a6db31',
+  'v8_revision': '676b039f932a490af484d2dfb018c460e4c4fde8',
   # 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.
@@ -229,11 +229,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': 'a76f224f4a17b53c2bdd9629082bb2cfc8e1c0f5',
+  'angle_revision': '9c5009519cbd61ebca30912818e3a13769fb6274',
   # 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': '72e6254a135d50913e3955113c1a31561e22fb86',
+  'swiftshader_revision': '84bc198202e5be2cb1c96f8c0cb21047c557a82a',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling PDFium
   # and whatever else without interference from each other.
@@ -296,7 +296,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': '2c906fa4f47e62f4ca5d391b55d12b47be23ce6c',
+  'devtools_frontend_revision': '750a2d16274378c3766fc7d6c5cbc0a380de7de4',
   # 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.
@@ -336,11 +336,11 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling feed
   # and whatever else without interference from each other.
-  'dawn_revision': '46ed96336a62398190830588916012566fb4388d',
+  'dawn_revision': 'ff7eb1641e92a39357e2895361d18e73d011de24',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling feed
   # and whatever else without interference from each other.
-  'quiche_revision': 'ca8e83b3ce94987ee30834f4ed2a3bbebd8e56ec',
+  'quiche_revision': '3196159f28b0ef380e4c0ef305833541ac12975e',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling ios_webkit
   # and whatever else without interference from each other.
@@ -403,7 +403,7 @@
   'libcxx_revision':       '8fa87946779682841e21e2da977eccfb6cb3bded',
 
   # GN CIPD package version.
-  'gn_version': 'git_revision:72d5a6e15d868abc8451fe0a3b6596e86a2ffc40',
+  'gn_version': 'git_revision:d2dce7523036ed7c55fbb8d2f272ab3720d5cf34',
 }
 
 # Only these hosts are allowed for dependencies in this DEPS file.
@@ -1355,7 +1355,7 @@
   },
 
   'src/third_party/perfetto':
-    Var('android_git') + '/platform/external/perfetto.git' + '@' + '8b07d9bbd00bbc938dc5e81bea6f4e99087b54fe',
+    Var('android_git') + '/platform/external/perfetto.git' + '@' + 'd57b60b2a95fa369db906eaaeadc4623dab6eb90',
 
   'src/third_party/perl': {
       'url': Var('chromium_git') + '/chromium/deps/perl.git' + '@' + '6f3e5028eb65d0b4c5fdd792106ac4c84eee1eb3',
@@ -1469,7 +1469,7 @@
       'packages': [
           {
               'package': 'chromium/third_party/r8',
-              'version': 'o1uegxayAMktc600LZ1gX5ZzkC_qhU-frNcWJfmBg98C',
+              'version': 'gXyBDv_fM87KnLcxvF5AGV5lwnm-JXIALYH8zrzdoaMC',
           },
       ],
       'condition': 'checkout_android',
@@ -1648,7 +1648,7 @@
     Var('chromium_git') + '/v8/v8.git' + '@' +  Var('v8_revision'),
 
   'src-internal': {
-    'url': 'https://chrome-internal.googlesource.com/chrome/src-internal.git@5ea44b00385c5c242647299d55d7842c3ca1691c',
+    'url': 'https://chrome-internal.googlesource.com/chrome/src-internal.git@12043255d16ef30071b793bc2c6cfeae8fd08056',
     'condition': 'checkout_src_internal',
   },
 
@@ -1656,7 +1656,7 @@
     'packages': [
       {
         'package': 'chromeos_internal/apps/eche_app/app',
-        'version': 'xOL6NPitN97NnC6-mtC9UiA6_mh3TTXh7NGMHD5f3SUC',
+        'version': 'QyJ6_BCrjN8MSr_PRJCv8HE2o1svd-19JfamchWLMvoC',
       },
     ],
     'condition': 'checkout_chromeos and checkout_src_internal',
@@ -1667,7 +1667,7 @@
     'packages': [
       {
         'package': 'chromeos_internal/apps/help_app/app',
-        'version': 'LqRTHHpa3W3eZECPY4mSCd84EgL_cYnYTX5dti03nGoC',
+        'version': 'Fc2S-_2zLIDjPBQg_UmRbmZ0mEDvLATRtX0NtmSHw3sC',
       },
     ],
     'condition': 'checkout_chromeos and checkout_src_internal',
@@ -1678,7 +1678,7 @@
     'packages': [
       {
         'package': 'chromeos_internal/apps/media_app/app',
-        'version': 'qEZ7iEjgu2Qmj28UFxDazVqbT3phzqxgf8EPJu8Y1xQC',
+        'version': 'KOQqyNuIwG_RWkMxsF6rSuJ7-eVK7TNiOJBiDN9yiawC',
       },
     ],
     'condition': 'checkout_chromeos and checkout_src_internal',
diff --git a/android_webview/browser/tracing/background_tracing_field_trial.cc b/android_webview/browser/tracing/background_tracing_field_trial.cc
index 2d38e85..7246bb23 100644
--- a/android_webview/browser/tracing/background_tracing_field_trial.cc
+++ b/android_webview/browser/tracing/background_tracing_field_trial.cc
@@ -14,23 +14,20 @@
 const char kBackgroundTracingFieldTrial[] = "BackgroundWebviewTracing";
 
 void SetupBackgroundTracingFieldTrial() {
+  auto* manager = content::BackgroundTracingManager::GetInstance();
+  DCHECK(manager);
   std::unique_ptr<content::BackgroundTracingConfig> config =
-      content::BackgroundTracingManager::GetInstance()
-          ->GetBackgroundTracingConfig(kBackgroundTracingFieldTrial);
+      manager->GetBackgroundTracingConfig(kBackgroundTracingFieldTrial);
 
   if (config &&
       config->tracing_mode() == content::BackgroundTracingConfig::SYSTEM &&
-      tracing::ShouldSetupSystemTracing()) {
+      tracing::ShouldSetupSystemTracing() &&
+      base::FeatureList::IsEnabled(features::kBackgroundTracingProtoOutput)) {
     // Only enable background tracing for system tracing if the system producer
-    // is enabled.
-    content::BackgroundTracingManager::GetInstance()->SetActiveScenario(
-        std::move(config),
-        base::BindRepeating(
-            base::DoNothing::Repeatedly<std::unique_ptr<std::string>,
-                                        content::BackgroundTracingManager::
-                                            FinishedProcessingCallback>()),
-        content::BackgroundTracingManager::ANONYMIZE_DATA);
+    // is enabled. Legacy JSON traces are not supported.
+    manager->SetActiveScenario(
+        std::move(config), content::BackgroundTracingManager::ANONYMIZE_DATA);
   }
 }
 
-}  // namespace android_webview
\ No newline at end of file
+}  // namespace android_webview
diff --git a/android_webview/java/src/org/chromium/android_webview/AwContents.java b/android_webview/java/src/org/chromium/android_webview/AwContents.java
index 00c18a9..625870e 100644
--- a/android_webview/java/src/org/chromium/android_webview/AwContents.java
+++ b/android_webview/java/src/org/chromium/android_webview/AwContents.java
@@ -932,7 +932,7 @@
                     (float) openWebPixelCoverage / RectUtils.getRectArea(rootVisibleRect);
 
             AwContentsJni.get().updateOpenWebScreenArea(
-                    openWebPixelCoverage, (int) openWebVisiblePercentage);
+                    openWebPixelCoverage, (int) (openWebVisiblePercentage * 100));
         }
     }
 
diff --git a/android_webview/javatests/src/org/chromium/android_webview/test/AwMetricsIntegrationTest.java b/android_webview/javatests/src/org/chromium/android_webview/test/AwMetricsIntegrationTest.java
index f3189b3d..eb81376 100644
--- a/android_webview/javatests/src/org/chromium/android_webview/test/AwMetricsIntegrationTest.java
+++ b/android_webview/javatests/src/org/chromium/android_webview/test/AwMetricsIntegrationTest.java
@@ -19,6 +19,7 @@
 
 import org.chromium.android_webview.AwBrowserProcess;
 import org.chromium.android_webview.AwContents;
+import org.chromium.android_webview.common.AwFeatures;
 import org.chromium.android_webview.common.PlatformServiceBridge;
 import org.chromium.android_webview.metrics.AwMetricsServiceClient;
 import org.chromium.base.Callback;
@@ -401,4 +402,32 @@
             embeddedTestServer.stopAndDestroyServer();
         }
     }
+
+    @Test
+    @MediumTest
+    @Feature({"AndroidWebView"})
+    @CommandLineFlags.Add({"enable-features=" + AwFeatures.WEBVIEW_MEASURE_SCREEN_COVERAGE})
+    public void testScreenCoverageReporting() throws Throwable {
+        EmbeddedTestServer embeddedTestServer = EmbeddedTestServer.createAndStartServer(
+                InstrumentationRegistry.getInstrumentation().getContext());
+        try {
+            mRule.loadUrlAsync(mAwContents,
+                    embeddedTestServer.getURL("/android_webview/test/data/hello_world.html"));
+
+            // We need to wait for log collection because the histogram is recorded during
+            // MetricsProvider::ProvideCurrentSessionData().
+            mPlatformServiceBridge.waitForNextMetricsLog();
+
+            final String histogramName = "Android.WebView.WebViewOpenWebVisible.ScreenPortion";
+            int totalSamples = RecordHistogram.getHistogramTotalCountForTesting(histogramName);
+            Assert.assertNotEquals("There should be at least one sample recorded", 0, totalSamples);
+
+            int zeroBucketSamples =
+                    RecordHistogram.getHistogramValueCountForTesting(histogramName, 0);
+            Assert.assertNotEquals("There should be at least one sample in a non-zero bucket",
+                    zeroBucketSamples, totalSamples);
+        } finally {
+            embeddedTestServer.stopAndDestroyServer();
+        }
+    }
 }
diff --git a/android_webview/nonembedded/component_updater/aw_component_update_service.cc b/android_webview/nonembedded/component_updater/aw_component_update_service.cc
index 0788d1b..bdc6e8a 100644
--- a/android_webview/nonembedded/component_updater/aw_component_update_service.cc
+++ b/android_webview/nonembedded/component_updater/aw_component_update_service.cc
@@ -18,7 +18,6 @@
 #include "base/command_line.h"
 #include "base/threading/thread_task_runner_handle.h"
 #include "components/component_updater/component_installer.h"
-#include "components/component_updater/component_updater_paths.h"
 #include "components/component_updater/component_updater_service.h"
 #include "components/component_updater/component_updater_utils.h"
 #include "components/update_client/crx_update_item.h"
@@ -36,13 +35,6 @@
 void JNI_AwComponentUpdateService_StartComponentUpdateService(
     JNIEnv* env,
     const base::android::JavaParamRef<jobject>& j_finished_callback) {
-  // Has to be called after init WebViewApkProcess, should only happen once
-  // during the lifetime of webview_apk process.
-  component_updater::RegisterPathProvider(
-      /*components_system_root_key=*/android_webview::DIR_COMPONENTS_ROOT,
-      /*components_system_root_key_alt=*/android_webview::DIR_COMPONENTS_ROOT,
-      /*components_user_root_key=*/android_webview::DIR_COMPONENTS_ROOT);
-
   AwComponentUpdateService::GetInstance()->StartComponentUpdateService(
       base::BindOnce(
           &base::android::RunIntCallbackAndroid,
diff --git a/android_webview/nonembedded/webview_apk_process.cc b/android_webview/nonembedded/webview_apk_process.cc
index 3acc894..84b4826 100644
--- a/android_webview/nonembedded/webview_apk_process.cc
+++ b/android_webview/nonembedded/webview_apk_process.cc
@@ -11,6 +11,7 @@
 #include "base/path_service.h"
 #include "base/task/single_thread_task_executor.h"
 #include "base/task/thread_pool/thread_pool_instance.h"
+#include "components/component_updater/component_updater_paths.h"
 #include "components/prefs/json_pref_store.h"
 #include "components/prefs/pref_registry_simple.h"
 #include "components/prefs/pref_service.h"
@@ -54,6 +55,11 @@
       base::MessagePumpType::JAVA);
 
   RegisterPathProvider();
+  component_updater::RegisterPathProvider(
+      /*components_system_root_key=*/android_webview::DIR_COMPONENTS_ROOT,
+      /*components_system_root_key_alt=*/android_webview::DIR_COMPONENTS_ROOT,
+      /*components_user_root_key=*/android_webview::DIR_COMPONENTS_ROOT);
+
   CreatePrefService();
 }
 
diff --git a/ash/BUILD.gn b/ash/BUILD.gn
index 1c405d7d..ff05f65 100644
--- a/ash/BUILD.gn
+++ b/ash/BUILD.gn
@@ -1021,6 +1021,8 @@
     "system/holding_space/holding_space_item_view.h",
     "system/holding_space/holding_space_item_views_section.cc",
     "system/holding_space/holding_space_item_views_section.h",
+    "system/holding_space/holding_space_progress_ring.cc",
+    "system/holding_space/holding_space_progress_ring.h",
     "system/holding_space/holding_space_tray.cc",
     "system/holding_space/holding_space_tray.h",
     "system/holding_space/holding_space_tray_bubble.cc",
diff --git a/ash/accelerators/accelerator_controller_unittest.cc b/ash/accelerators/accelerator_controller_unittest.cc
index 2dcb4cf..be2abd2 100644
--- a/ash/accelerators/accelerator_controller_unittest.cc
+++ b/ash/accelerators/accelerator_controller_unittest.cc
@@ -143,13 +143,27 @@
   ime1.id = "id1";
   ImeInfo ime2;
   ime2.id = "id2";
-  std::vector<ImeInfo> available_imes;
-  available_imes.push_back(std::move(ime1));
-  available_imes.push_back(std::move(ime2));
-  Shell::Get()->ime_controller()->RefreshIme("id1", std::move(available_imes),
+  std::vector<ImeInfo> visible_imes;
+  visible_imes.push_back(std::move(ime1));
+  visible_imes.push_back(std::move(ime2));
+  Shell::Get()->ime_controller()->RefreshIme("id1", std::move(visible_imes),
                                              std::vector<ImeMenuItem>());
 }
 
+void AddNotVisibleTestIme() {
+  ImeInfo dictation;
+  dictation.id = "_ext_ime_egfdjlfmgnehecnclamagfafdccgfndpdictation";
+  const std::vector<ImeInfo> visible_imes =
+      Shell::Get()->ime_controller()->GetVisibleImes();
+  std::vector<ImeInfo> available_imes;
+  for (auto ime : visible_imes) {
+    available_imes.push_back(ime);
+  }
+  available_imes.push_back(dictation);
+  Shell::Get()->ime_controller()->RefreshIme(
+      dictation.id, std::move(available_imes), std::vector<ImeMenuItem>());
+}
+
 ui::Accelerator CreateReleaseAccelerator(ui::KeyboardCode key_code,
                                          int modifiers) {
   ui::Accelerator accelerator(key_code, modifiers);
@@ -1426,7 +1440,7 @@
 }
 
 TEST_F(AcceleratorControllerTest, ImeGlobalAccelerators) {
-  ASSERT_EQ(0u, Shell::Get()->ime_controller()->available_imes().size());
+  ASSERT_EQ(0u, Shell::Get()->ime_controller()->GetVisibleImes().size());
 
   // Cycling IME is blocked because there is nothing to switch to.
   ui::Accelerator control_space_down(ui::VKEY_SPACE, ui::EF_CONTROL_DOWN);
@@ -1438,11 +1452,24 @@
   EXPECT_FALSE(ProcessInController(control_space_up));
   EXPECT_FALSE(ProcessInController(control_shift_space));
 
+  // Adding only a not visible IME doesn't make IME accelerators available.
+  AddNotVisibleTestIme();
+  ASSERT_EQ(0u, Shell::Get()->ime_controller()->GetVisibleImes().size());
+  EXPECT_FALSE(ProcessInController(control_space_down));
+  EXPECT_FALSE(ProcessInController(control_space_up));
+  EXPECT_FALSE(ProcessInController(control_shift_space));
+
   // Cycling IME works when there are IMEs available.
   AddTestImes();
   EXPECT_TRUE(ProcessInController(control_space_down));
   EXPECT_TRUE(ProcessInController(control_space_up));
   EXPECT_TRUE(ProcessInController(control_shift_space));
+
+  // Adding the not visible IME back doesn't block cycling.
+  AddNotVisibleTestIme();
+  EXPECT_TRUE(ProcessInController(control_space_down));
+  EXPECT_TRUE(ProcessInController(control_space_up));
+  EXPECT_TRUE(ProcessInController(control_shift_space));
 }
 
 // TODO(nona|mazda): Remove this when crbug.com/139556 in a better way.
diff --git a/ash/app_list/app_list_bubble_presenter.cc b/ash/app_list/app_list_bubble_presenter.cc
index ddec5ba..89b694a 100644
--- a/ash/app_list/app_list_bubble_presenter.cc
+++ b/ash/app_list/app_list_bubble_presenter.cc
@@ -60,13 +60,14 @@
                           base::Unretained(this)));
 }
 
-void AppListBubblePresenter::Toggle(int64_t display_id) {
+ShelfAction AppListBubblePresenter::Toggle(int64_t display_id) {
   DVLOG(1) << __PRETTY_FUNCTION__;
   if (bubble_widget_) {
     Dismiss();
-    return;
+    return SHELF_ACTION_APP_LIST_DISMISSED;
   }
   Show(display_id);
+  return SHELF_ACTION_APP_LIST_SHOWN;
 }
 
 void AppListBubblePresenter::Dismiss() {
diff --git a/ash/app_list/app_list_bubble_presenter.h b/ash/app_list/app_list_bubble_presenter.h
index 130558b..5762f26 100644
--- a/ash/app_list/app_list_bubble_presenter.h
+++ b/ash/app_list/app_list_bubble_presenter.h
@@ -10,6 +10,7 @@
 #include <memory>
 
 #include "ash/ash_export.h"
+#include "ash/public/cpp/shelf_types.h"
 #include "ui/views/widget/widget_observer.h"
 
 namespace ash {
@@ -31,8 +32,9 @@
   // Shows the bubble on the display with `display_id`.
   void Show(int64_t display_id);
 
-  // Shows or hides the bubble on the display with `display_id`.
-  void Toggle(int64_t display_id);
+  // Shows or hides the bubble on the display with `display_id`. Returns the
+  // appropriate ShelfAction to indicate whether the bubble was shown or hidden.
+  ShelfAction Toggle(int64_t display_id);
 
   // Closes and destroys the bubble.
   void Dismiss();
diff --git a/ash/app_list/app_list_controller_impl.cc b/ash/app_list/app_list_controller_impl.cc
index c05c3b2..6b3412a 100644
--- a/ash/app_list/app_list_controller_impl.cc
+++ b/ash/app_list/app_list_controller_impl.cc
@@ -239,7 +239,11 @@
   return nullptr;
 }
 
-void LogAppListShowSource(AppListShowSource show_source) {
+void LogAppListShowSource(AppListShowSource show_source, bool app_list_bubble) {
+  if (app_list_bubble) {
+    UMA_HISTOGRAM_ENUMERATION("Apps.AppListBubbleShowSource", show_source);
+    return;
+  }
   UMA_HISTOGRAM_ENUMERATION("Apps.AppListShowSource", show_source);
 }
 
@@ -712,10 +716,12 @@
 void AppListControllerImpl::Show(int64_t display_id,
                                  absl::optional<AppListShowSource> show_source,
                                  base::TimeTicks event_time_stamp) {
+  const bool show_app_list_bubble =
+      features::IsAppListBubbleEnabled() && !IsTabletMode();
   if (show_source.has_value())
-    LogAppListShowSource(show_source.value());
+    LogAppListShowSource(show_source.value(), show_app_list_bubble);
 
-  if (features::IsAppListBubbleEnabled() && !IsTabletMode()) {
+  if (show_app_list_bubble) {
     bubble_presenter_->Show(display_id);
     return;
   }
@@ -763,15 +769,15 @@
       Back();
       return SHELF_ACTION_APP_LIST_BACK;
     }
-
-    LogAppListShowSource(show_source);
+    LogAppListShowSource(show_source, /*app_list_bubble=*/false);
     return SHELF_ACTION_APP_LIST_SHOWN;
   }
 
   if (features::IsAppListBubbleEnabled()) {
-    bubble_presenter_->Toggle(display_id);
-    return bubble_presenter_->IsShowing() ? SHELF_ACTION_APP_LIST_SHOWN
-                                          : SHELF_ACTION_APP_LIST_DISMISSED;
+    ShelfAction action = bubble_presenter_->Toggle(display_id);
+    if (action == SHELF_ACTION_APP_LIST_SHOWN)
+      LogAppListShowSource(show_source, /*app_list_bubble=*/true);
+    return action;
   }
 
   base::AutoReset<bool> auto_reset(&should_dismiss_immediately_,
@@ -779,7 +785,7 @@
   ShelfAction action = fullscreen_presenter_->ToggleAppList(
       display_id, show_source, event_time_stamp);
   if (action == SHELF_ACTION_APP_LIST_SHOWN)
-    LogAppListShowSource(show_source);
+    LogAppListShowSource(show_source, /*app_list_bubble=*/false);
   return action;
 }
 
@@ -1256,6 +1262,7 @@
 }
 
 void AppListControllerImpl::Back() {
+  // TODO(https://crbug.com/1220808): Handle back action for AppListBubble
   fullscreen_presenter_->GetView()->Back();
 }
 
diff --git a/ash/app_list/app_list_metrics_unittest.cc b/ash/app_list/app_list_metrics_unittest.cc
index df98650..547c132 100644
--- a/ash/app_list/app_list_metrics_unittest.cc
+++ b/ash/app_list/app_list_metrics_unittest.cc
@@ -7,6 +7,7 @@
 #include <vector>
 
 #include "ash/accessibility/accessibility_controller_impl.h"
+#include "ash/app_list/app_list_bubble_presenter.h"
 #include "ash/app_list/app_list_controller_impl.h"
 #include "ash/app_list/app_list_metrics.h"
 #include "ash/app_list/app_list_presenter_impl.h"
@@ -25,6 +26,7 @@
 #include "ash/app_list/views/search_result_tile_item_list_view.h"
 #include "ash/app_list/views/search_result_tile_item_view.h"
 #include "ash/app_list/views/suggestion_chip_container_view.h"
+#include "ash/constants/ash_features.h"
 #include "ash/public/cpp/app_list/app_list_types.h"
 #include "ash/public/cpp/shelf_item_delegate.h"
 #include "ash/public/cpp/shelf_model.h"
@@ -38,6 +40,7 @@
 #include "ash/test/ash_test_base.h"
 #include "ash/wm/tablet_mode/tablet_mode_controller.h"
 #include "base/test/metrics/histogram_tester.h"
+#include "base/test/scoped_feature_list.h"
 #include "ui/display/screen.h"
 #include "ui/events/test/event_generator.h"
 
@@ -535,11 +538,11 @@
 TEST_F(AppListShowSourceMetricTest, TabletInAppToHome) {
   base::HistogramTester histogram_tester;
 
-  // Enable accessibility feature that forces home button to be shown even with
-  // kHideShelfControlsInTabletMode enabled.
-  // TODO(https://crbug.com/1050544) Use the a11y feature specific to showing
-  // navigation buttons in tablet mode once it lands.
-  Shell::Get()->accessibility_controller()->autoclick().SetEnabled(true);
+  // Enable accessibility feature that forces home button to be shown in tablet
+  // mode.
+  Shell::Get()
+      ->accessibility_controller()
+      ->SetTabletModeShelfNavigationButtonsEnabled(true);
 
   std::unique_ptr<views::Widget> widget = CreateTestWidget();
   Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
@@ -572,7 +575,7 @@
   Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
   GetAppListTestHelper()->CheckVisibility(false);
 
-  // Ensure that no AppListShowSource metric was recoreded.
+  // Ensure that no AppListShowSource metric was recorded.
   histogram_tester.ExpectTotalCount("Apps.AppListShowSource", 0);
 }
 
@@ -589,4 +592,99 @@
       1 /* Number of times app list shown after entering tablet mode */);
 }
 
+class AppListBubbleShowSourceMetricTest : public AppListShowSourceMetricTest {
+ public:
+  AppListBubbleShowSourceMetricTest() {
+    scoped_feature_list_.InitWithFeatures({features::kAppListBubble}, {});
+  }
+  ~AppListBubbleShowSourceMetricTest() override = default;
+
+  base::test::ScopedFeatureList scoped_feature_list_;
+};
+
+// Tests that showing the AppListBubble in clamshell mode records the proper
+// metrics for Apps.AppListBubbleShowSource.
+TEST_F(AppListBubbleShowSourceMetricTest, ClamshellModeHomeButton) {
+  base::HistogramTester histogram_tester;
+  auto* app_list_bubble_presenter =
+      Shell::Get()->app_list_controller()->bubble_presenter_for_test();
+  // Show the Bubble AppList.
+  GetAppListTestHelper()->ShowAndRunLoop(GetPrimaryDisplayId());
+  EXPECT_TRUE(app_list_bubble_presenter->IsShowing());
+
+  // Test that the proper histogram is logged.
+  histogram_tester.ExpectTotalCount("Apps.AppListBubbleShowSource", 1);
+
+  // Hide the Bubble AppList.
+  GetAppListTestHelper()->Dismiss();
+  EXPECT_FALSE(app_list_bubble_presenter->IsShowing());
+
+  // Test that no histograms were logged.
+  histogram_tester.ExpectTotalCount("Apps.AppListBubbleShowSource", 1);
+
+  // Show the Bubble AppList one more time.
+  GetAppListTestHelper()->ShowAndRunLoop(GetPrimaryDisplayId());
+  EXPECT_TRUE(app_list_bubble_presenter->IsShowing());
+
+  // Test that the histogram records 2 total shows.
+  histogram_tester.ExpectTotalCount("Apps.AppListBubbleShowSource", 2);
+
+  // Test that no fullscreen app list metrics were recorded.
+  histogram_tester.ExpectTotalCount("Apps.AppListShowSource", 0);
+}
+
+// Test that tablet mode launcher operations do not record AppListBubble
+// metrics.
+TEST_F(AppListBubbleShowSourceMetricTest,
+       TabletModeDoesNotRecordAppListBubbleShow) {
+  base::HistogramTester histogram_tester;
+  // Enable accessibility feature that forces home button to be shown in tablet
+  // mode.
+  Shell::Get()
+      ->accessibility_controller()
+      ->SetTabletModeShelfNavigationButtonsEnabled(true);
+
+  // Go to tablet mode, the tablet mode (non bubble) launcher will show. Create
+  // a test widget so the launcher will show in the background.
+  std::unique_ptr<views::Widget> widget = CreateTestWidget();
+  Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
+  auto* app_list_bubble_presenter =
+      Shell::Get()->app_list_controller()->bubble_presenter_for_test();
+  EXPECT_FALSE(app_list_bubble_presenter->IsShowing());
+
+  // Ensure that no AppListBubbleShowSource metric was recorded.
+  histogram_tester.ExpectTotalCount("Apps.AppListBubbleShowSource", 0);
+
+  // Press the Home Button, which hides `widget` and shows the tablet mode
+  // launcher.
+  ClickHomeButton();
+  EXPECT_FALSE(app_list_bubble_presenter->IsShowing());
+
+  // Test that no AppListBubble metrics were recorded.
+  histogram_tester.ExpectTotalCount("Apps.AppListBubbleShowSource", 0);
+}
+
+// Tests that Toggling the AppListBubble does not record metrics when the
+// result of the toggle is that the AppListBubble is hidden.
+TEST_F(AppListBubbleShowSourceMetricTest, ToggleDoesNotRecordOnHide) {
+  base::HistogramTester histogram_tester;
+  auto* app_list_controller = Shell::Get()->app_list_controller();
+
+  // Toggle the AppListBubble to show it.
+  app_list_controller->ToggleAppList(GetPrimaryDisplayId(),
+                                     AppListShowSource::kSearchKey,
+                                     base::TimeTicks::Now());
+  auto* app_list_bubble_presenter =
+      Shell::Get()->app_list_controller()->bubble_presenter_for_test();
+  ASSERT_TRUE(app_list_bubble_presenter->IsShowing());
+
+  // Toggle the AppListBubble once more, to hide it.
+  app_list_controller->ToggleAppList(GetPrimaryDisplayId(),
+                                     AppListShowSource::kSearchKey,
+                                     base::TimeTicks::Now());
+  ASSERT_FALSE(app_list_bubble_presenter->IsShowing());
+  // Test that only one show was recorded.
+  histogram_tester.ExpectTotalCount("Apps.AppListBubbleShowSource", 1);
+}
+
 }  // namespace ash
diff --git a/ash/content/BUILD.gn b/ash/content/BUILD.gn
index 25d747b..4e29197 100644
--- a/ash/content/BUILD.gn
+++ b/ash/content/BUILD.gn
@@ -32,6 +32,8 @@
   ]
 }
 
+# When adding new WebUI, add your closure_compile target here. Otherwise you
+# won't get type checking.
 group("closure_compile") {
   testonly = true
   deps = [
diff --git a/ash/content/DEPS b/ash/content/DEPS
new file mode 100644
index 0000000..d1ee440
--- /dev/null
+++ b/ash/content/DEPS
@@ -0,0 +1,7 @@
+include_rules = [
+  # Code in //ash sits below chrome in the dependency graph.
+  "-chrome",
+  # Code in //ash runs in the browser process.
+  "+content/public/browser",
+  "+content/public/test",
+]
diff --git a/ash/content/README.md b/ash/content/README.md
new file mode 100644
index 0000000..bdb2656
--- /dev/null
+++ b/ash/content/README.md
@@ -0,0 +1,22 @@
+# //ash/content
+
+//ash/content contains code that has dependencies on //content. Most code
+here is Chrome OS-specific WebUI for system web apps.
+
+General purpose window manager or system UI code should not have content
+dependencies, and should not live in this directory. Prefer a different
+top-level ash directory, like //ash/system, //ash/wm, or add
+//ash/your_feature. Low-level components go in //ash/components/your_feature.
+
+Each subdirectory should be its own separate "module", and have its own
+BUILD.gn file. See this directory's [BUILD.gn file][1] for tips on adding
+your own subdirectory.
+
+This directory is in //ash because it runs in the "ash-chrome" binary when
+Lacros is running. Most of its subdirectories used to live in
+//chromeos/components. See the [Lacros documentation][2] or the Lacros
+[directory migration design][3].
+
+[1]: /ash/content/BUILD.gn
+[2]: /docs/lacros.md
+[3]: https://docs.google.com/document/d/1g-98HpzA8XcoGBWUv1gQNr4rbnD5yfvbtYZyPDDbkaE/edit#heading=h.5aq0kntd3afh
diff --git a/ash/content/file_manager/DEPS b/ash/content/file_manager/DEPS
index 7441df1..96c8cf6 100644
--- a/ash/content/file_manager/DEPS
+++ b/ash/content/file_manager/DEPS
@@ -1,7 +1,6 @@
 include_rules = [
   # Do not add chrome here (use a delegate instead).
   "+ash/grit/ash_file_manager_resources.h",
-  "+content/public/browser",
   "+ui/file_manager/grit",
   "+ui/webui",
 ]
diff --git a/ash/content/os_feedback_ui/DEPS b/ash/content/os_feedback_ui/DEPS
index ddfe6f2..93138d4 100644
--- a/ash/content/os_feedback_ui/DEPS
+++ b/ash/content/os_feedback_ui/DEPS
@@ -1,5 +1,4 @@
 include_rules = [
-  "+content/public/browser",
   "+ui/resources",
   "+ui/webui",
 ]
diff --git a/ash/content/scanning/DEPS b/ash/content/scanning/DEPS
index b9cdc97..cede222 100644
--- a/ash/content/scanning/DEPS
+++ b/ash/content/scanning/DEPS
@@ -2,10 +2,7 @@
   "+ash/constants",
   "+ash/grit/ash_scanning_app_resources.h",
   "+ash/grit/ash_scanning_app_resources_map.h",
-  "-chrome",
   "+chromeos/strings/grit/chromeos_strings.h",
-  "+content/public/browser",
-  "+content/public/test",
   "+ui/base",
   "+ui/gfx",
   "+ui/resources",
diff --git a/ash/content/shimless_rma/DEPS b/ash/content/shimless_rma/DEPS
index 70b215bc..74f7342 100644
--- a/ash/content/shimless_rma/DEPS
+++ b/ash/content/shimless_rma/DEPS
@@ -1,8 +1,6 @@
 include_rules = [
-  "-chrome",
   "+chromeos/dbus/rmad",
   "+chromeos/grit/chromeos_shimless_rma_resources.h",
-  "+content/public/browser",
   "+ui/resources",
   "+ui/web_dialogs",
   "+ui/chromeos/strings",
diff --git a/ash/content/shortcut_customization_ui/DEPS b/ash/content/shortcut_customization_ui/DEPS
index dec6a667..4ff4187 100644
--- a/ash/content/shortcut_customization_ui/DEPS
+++ b/ash/content/shortcut_customization_ui/DEPS
@@ -1,8 +1,6 @@
 include_rules = [
   "+ash/grit/ash_shortcut_customization_app_resources.h",
   "+ash/grit/ash_shortcut_customization_app_resources_map.h",
-  "-chrome",
-  "+content/public/browser",
   "+ui/resources",
   "+ui/webui",
-]
\ No newline at end of file
+]
diff --git a/ash/ime/ime_controller_impl.cc b/ash/ime/ime_controller_impl.cc
index 1cfb421..95038fb 100644
--- a/ash/ime/ime_controller_impl.cc
+++ b/ash/ime/ime_controller_impl.cc
@@ -31,6 +31,10 @@
   kMaxValue = kSwitchIme
 };
 
+// The ID for the Accessibility Common IME (used for Dictation).
+const char* kAccessibilityCommonIMEId =
+    "_ext_ime_egfdjlfmgnehecnclamagfafdccgfndpdictation";
+
 }  // namespace
 
 ImeControllerImpl::ImeControllerImpl()
@@ -48,6 +52,14 @@
   observers_.RemoveObserver(observer);
 }
 
+const std::vector<ImeInfo>& ImeControllerImpl::GetVisibleImes() const {
+  return visible_imes_;
+}
+
+bool ImeControllerImpl::IsCurrentImeVisible() const {
+  return current_ime_.id != kAccessibilityCommonIMEId;
+}
+
 void ImeControllerImpl::SetClient(ImeControllerClient* client) {
   if (client_) {
     if (CastConfigController::Get())
@@ -71,7 +83,7 @@
 
   // Do not consume key event if there is only one input method is enabled.
   // Ctrl+Space or Alt+Shift may be used by other application.
-  return available_imes_.size() > 1;
+  return GetVisibleImes().size() > 1;
 }
 
 void ImeControllerImpl::SwitchToNextIme() {
@@ -131,12 +143,17 @@
 
   available_imes_.clear();
   available_imes_.reserve(available_imes.size());
+  visible_imes_.clear();
+  visible_imes_.reserve(visible_imes_.size());
   for (const auto& ime : available_imes) {
     if (ime.id.empty()) {
       DLOG(ERROR) << "Received IME with invalid ID.";
       continue;
     }
     available_imes_.push_back(ime);
+    if (ime.id != kAccessibilityCommonIMEId) {
+      visible_imes_.push_back(ime);
+    }
     if (ime.id == current_ime_id)
       current_ime_ = ime;
   }
diff --git a/ash/ime/ime_controller_impl.h b/ash/ime/ime_controller_impl.h
index 5224742..98ac574 100644
--- a/ash/ime/ime_controller_impl.h
+++ b/ash/ime/ime_controller_impl.h
@@ -52,9 +52,10 @@
   void AddObserver(Observer* observer);
   void RemoveObserver(Observer* observer);
 
-  const ImeInfo& current_ime() const { return current_ime_; }
+  const std::vector<ImeInfo>& GetVisibleImes() const;
+  bool IsCurrentImeVisible() const;
 
-  const std::vector<ImeInfo>& available_imes() const { return available_imes_; }
+  const ImeInfo& current_ime() const { return current_ime_; }
 
   bool is_extra_input_options_enabled() const {
     return is_extra_input_options_enabled_;
@@ -147,6 +148,10 @@
   // "Available" IMEs are both installed and enabled by the user in settings.
   std::vector<ImeInfo> available_imes_;
 
+  // "Visible" IMEs are installed, enabled, and don't include built-in IMEs that
+  // shouldn't be shown to the user, like Dictation.
+  std::vector<ImeInfo> visible_imes_;
+
   // True if the available IMEs are currently managed by enterprise policy.
   // For example, can occur at the login screen with device-level policy.
   bool managed_by_policy_ = false;
diff --git a/ash/ime/ime_controller_impl_unittest.cc b/ash/ime/ime_controller_impl_unittest.cc
index d3feb2c..5b5f615 100644
--- a/ash/ime/ime_controller_impl_unittest.cc
+++ b/ash/ime/ime_controller_impl_unittest.cc
@@ -102,9 +102,9 @@
 
   // Cached data was updated.
   EXPECT_EQ("ime1", controller->current_ime().id);
-  ASSERT_EQ(2u, controller->available_imes().size());
-  EXPECT_EQ("ime1", controller->available_imes()[0].id);
-  EXPECT_EQ("ime2", controller->available_imes()[1].id);
+  ASSERT_EQ(2u, controller->GetVisibleImes().size());
+  EXPECT_EQ("ime1", controller->GetVisibleImes()[0].id);
+  EXPECT_EQ("ime2", controller->GetVisibleImes()[1].id);
   ASSERT_EQ(1u, controller->current_ime_menu_items().size());
   EXPECT_EQ("menu1", controller->current_ime_menu_items()[0].key);
 
@@ -118,6 +118,7 @@
   // Set up a single IME.
   RefreshImes("ime1", {"ime1"});
   EXPECT_EQ("ime1", controller->current_ime().id);
+  EXPECT_TRUE(controller->IsCurrentImeVisible());
 
   // When there is no current IME the cached current IME is empty.
   const std::string empty_ime_id;
@@ -125,6 +126,30 @@
   EXPECT_TRUE(controller->current_ime().id.empty());
 }
 
+TEST_F(ImeControllerImplTest, CurrentImeNotVisible) {
+  ImeControllerImpl* controller = Shell::Get()->ime_controller();
+
+  // Add only Dictation.
+  std::string dictation_id =
+      "_ext_ime_egfdjlfmgnehecnclamagfafdccgfndpdictation";
+  RefreshImes(dictation_id, {dictation_id});
+  EXPECT_EQ(dictation_id, controller->current_ime().id);
+  EXPECT_FALSE(controller->IsCurrentImeVisible());
+  EXPECT_EQ(0u, controller->GetVisibleImes().size());
+
+  // Add something else too, but Dictation is active.
+  RefreshImes(dictation_id, {dictation_id, "ime1"});
+  EXPECT_EQ(dictation_id, controller->current_ime().id);
+  EXPECT_FALSE(controller->IsCurrentImeVisible());
+  EXPECT_EQ(1u, controller->GetVisibleImes().size());
+
+  // Inactivate the other IME, leave Dictation in the list.
+  RefreshImes("ime1", {dictation_id, "ime1"});
+  EXPECT_EQ("ime1", controller->current_ime().id);
+  EXPECT_TRUE(controller->IsCurrentImeVisible());
+  EXPECT_EQ(1u, controller->GetVisibleImes().size());
+}
+
 TEST_F(ImeControllerImplTest, SetImesManagedByPolicy) {
   ImeControllerImpl* controller = Shell::Get()->ime_controller();
   TestImeObserver observer;
@@ -152,7 +177,7 @@
   ImeControllerImpl* controller = Shell::Get()->ime_controller();
 
   // Can't switch IMEs when none are available.
-  ASSERT_EQ(0u, controller->available_imes().size());
+  ASSERT_EQ(0u, controller->GetVisibleImes().size());
   EXPECT_FALSE(controller->CanSwitchIme());
 
   // Can't switch with only 1 IME.
@@ -208,7 +233,7 @@
   const ui::Accelerator wide_half_2(ui::VKEY_DBE_DBCSCHAR, ui::EF_NONE);
 
   // When there are no IMEs available switching by accelerator does not work.
-  ASSERT_EQ(0u, controller->available_imes().size());
+  ASSERT_EQ(0u, controller->GetVisibleImes().size());
   EXPECT_FALSE(controller->CanSwitchImeWithAccelerator(convert));
   EXPECT_FALSE(controller->CanSwitchImeWithAccelerator(non_convert));
   EXPECT_FALSE(controller->CanSwitchImeWithAccelerator(wide_half_1));
diff --git a/ash/login/ui/lock_contents_view.cc b/ash/login/ui/lock_contents_view.cc
index e728741..01419c5 100644
--- a/ash/login/ui/lock_contents_view.cc
+++ b/ash/login/ui/lock_contents_view.cc
@@ -2131,7 +2131,7 @@
   int bold_length = 0;
   // Display a hint to switch keyboards if there are other active input
   // methods in clamshell mode.
-  if (ime_controller->available_imes().size() > 1 && !IsTabletMode()) {
+  if (ime_controller->GetVisibleImes().size() > 1 && !IsTabletMode()) {
     error_text += u" ";
     bold_start = error_text.length();
     std::u16string shortcut =
diff --git a/ash/public/cpp/holding_space/holding_space_item.cc b/ash/public/cpp/holding_space/holding_space_item.cc
index 7ee0701..d0180fe 100644
--- a/ash/public/cpp/holding_space/holding_space_item.cc
+++ b/ash/public/cpp/holding_space/holding_space_item.cc
@@ -37,7 +37,8 @@
 bool HoldingSpaceItem::operator==(const HoldingSpaceItem& rhs) const {
   return type_ == rhs.type_ && id_ == rhs.id_ && file_path_ == rhs.file_path_ &&
          file_system_url_ == rhs.file_system_url_ && text_ == rhs.text_ &&
-         *image_ == *rhs.image_ && progress_ == rhs.progress_;
+         secondary_text_ == rhs.secondary_text_ && *image_ == *rhs.image_ &&
+         progress_ == rhs.progress_ && paused_ == rhs.paused_;
 }
 
 // static
@@ -62,8 +63,8 @@
   // Note: std::make_unique does not work with private constructors.
   return base::WrapUnique(new HoldingSpaceItem(
       type, /*id=*/base::UnguessableToken::Create().ToString(), file_path,
-      file_system_url, file_path.BaseName().LossyDisplayName(),
-      std::move(image_resolver).Run(type, file_path), progress));
+      file_system_url, std::move(image_resolver).Run(type, file_path),
+      progress));
 }
 
 // static
@@ -99,7 +100,7 @@
   // NOTE: `std::make_unique` does not work with private constructors.
   return base::WrapUnique(new HoldingSpaceItem(
       type, DeserializeId(dict), file_path,
-      /*file_system_url=*/GURL(), file_path.BaseName().LossyDisplayName(),
+      /*file_system_url=*/GURL(),
       std::move(image_resolver).Run(type, file_path), /*progress=*/1.f));
 }
 
@@ -166,12 +167,32 @@
 
   file_path_ = file_path;
   file_system_url_ = file_system_url;
-  text_ = file_path.BaseName().LossyDisplayName();
   image_->UpdateBackingFilePath(file_path);
 
   return true;
 }
 
+std::u16string HoldingSpaceItem::GetText() const {
+  return text_.value_or(file_path_.BaseName().LossyDisplayName());
+}
+
+bool HoldingSpaceItem::SetText(const absl::optional<std::u16string>& text) {
+  if (text_ == text)
+    return false;
+
+  text_ = text;
+  return true;
+}
+
+bool HoldingSpaceItem::SetSecondaryText(
+    const absl::optional<std::u16string>& secondary_text) {
+  if (secondary_text_ == secondary_text)
+    return false;
+
+  secondary_text_ = secondary_text;
+  return true;
+}
+
 bool HoldingSpaceItem::IsInProgress() const {
   return progress_ != 1.f;
 }
@@ -228,28 +249,16 @@
   return true;
 }
 
-bool HoldingSpaceItem::SetCurrentSizeInBytes(
-    const absl::optional<int64_t>& current_size_in_bytes) {
-  if (current_size_in_bytes_ == current_size_in_bytes)
-    return false;
-
-  DCHECK(!current_size_in_bytes || current_size_in_bytes >= 0);
-  current_size_in_bytes_ = current_size_in_bytes;
-  return true;
-}
-
 HoldingSpaceItem::HoldingSpaceItem(Type type,
                                    const std::string& id,
                                    const base::FilePath& file_path,
                                    const GURL& file_system_url,
-                                   const std::u16string& text,
                                    std::unique_ptr<HoldingSpaceImage> image,
                                    const absl::optional<float>& progress)
     : type_(type),
       id_(id),
       file_path_(file_path),
       file_system_url_(file_system_url),
-      text_(text),
       image_(std::move(image)),
       progress_(progress) {
   if (progress_.has_value()) {
diff --git a/ash/public/cpp/holding_space/holding_space_item.h b/ash/public/cpp/holding_space/holding_space_item.h
index 62e3ebf..f2bd098 100644
--- a/ash/public/cpp/holding_space/holding_space_item.h
+++ b/ash/public/cpp/holding_space/holding_space_item.h
@@ -109,6 +109,19 @@
   bool SetBackingFile(const base::FilePath& file_path,
                       const GURL& file_system_url);
 
+  // Returns `text_`, falling back to the lossy display name of the item's
+  // backing file if absent.
+  std::u16string GetText() const;
+
+  // Sets the text that should be shown for the item, returning `true` if a
+  // change occurred or `false` to indicate no-op. If absent, the lossy display
+  // name of the item's backing file will be used.
+  bool SetText(const absl::optional<std::u16string>& text);
+
+  // Sets the secondary text that should be shown for the item, returning `true`
+  // if a change occurred or `false` to indicate no-op.
+  bool SetSecondaryText(const absl::optional<std::u16string>& text);
+
   // Returns whether the item is in progress.
   bool IsInProgress() const;
 
@@ -134,16 +147,13 @@
   // NOTE: Only in-progress items can be paused.
   bool SetPaused(bool paused);
 
-  // Sets the current size (in bytes) of the file backing this item.
-  // NOTE: If present, the `current_size_in_bytes` must be >= `0`.
-  bool SetCurrentSizeInBytes(
-      const absl::optional<int64_t>& current_size_in_bytes);
-
   const std::string& id() const { return id_; }
 
   Type type() const { return type_; }
 
-  const std::u16string& text() const { return text_; }
+  const absl::optional<std::u16string>& secondary_text() const {
+    return secondary_text_;
+  }
 
   const HoldingSpaceImage& image() const { return *image_; }
 
@@ -153,10 +163,6 @@
 
   const absl::optional<float>& progress() const { return progress_; }
 
-  const absl::optional<int64_t>& current_size_in_bytes() const {
-    return current_size_in_bytes_;
-  }
-
   HoldingSpaceImage& image_for_testing() { return *image_; }
 
  private:
@@ -165,7 +171,6 @@
                    const std::string& id,
                    const base::FilePath& file_path,
                    const GURL& file_system_url,
-                   const std::u16string& text,
                    std::unique_ptr<HoldingSpaceImage> image,
                    const absl::optional<float>& progress);
 
@@ -181,7 +186,10 @@
   GURL file_system_url_;
 
   // If set, the text that should be shown for the item.
-  std::u16string text_;
+  absl::optional<std::u16string> text_;
+
+  // If set, the secondary text that should be shown for the item.
+  absl::optional<std::u16string> secondary_text_;
 
   // The image representation of the item.
   std::unique_ptr<HoldingSpaceImage> image_;
@@ -195,12 +203,6 @@
   // NOTE: Only in-progress items can be paused.
   bool paused_ = false;
 
-  // The current size (in bytes) of the file backing this item.
-  // NOTE: If present, the `current_size_in_bytes` is >= `0`.
-  // NOTE: This is currently set only in response to updates of in-progress
-  // downloads and only used when `progress_` != `1.f`.
-  absl::optional<int64_t> current_size_in_bytes_;
-
   // Mutable to allow const access from `AddDeletionCallback()`.
   mutable base::RepeatingClosureList deletion_callback_list_;
 };
diff --git a/ash/public/cpp/holding_space/holding_space_item_unittest.cc b/ash/public/cpp/holding_space/holding_space_item_unittest.cc
index fcec49f..24d07da 100644
--- a/ash/public/cpp/holding_space/holding_space_item_unittest.cc
+++ b/ash/public/cpp/holding_space/holding_space_item_unittest.cc
@@ -153,29 +153,53 @@
   EXPECT_EQ(holding_space_item->progress(), 1.f);
 }
 
-// Tests setting the current size (in bytes) for each holding space item type.
-TEST_P(HoldingSpaceItemTest, CurrentSizeInBytes) {
+// Tests setting the secondary text for each holding space item type.
+TEST_P(HoldingSpaceItemTest, SecondaryText) {
   // Create a `holding_space_item`.
   auto holding_space_item = HoldingSpaceItem::CreateFileBackedItem(
       /*type=*/GetParam(), base::FilePath("file_path"),
       GURL("filesystem::file_system_url"),
       /*image_resolver=*/base::BindOnce(&CreateFakeHoldingSpaceImage));
 
-  // Initially the current size should be absent.
-  EXPECT_FALSE(holding_space_item->current_size_in_bytes());
-  EXPECT_FALSE(holding_space_item->current_size_in_bytes());
+  // Initially the secondary text should be absent.
+  EXPECT_FALSE(holding_space_item->secondary_text());
 
-  // It should be possible to update current size to a new value.
-  EXPECT_TRUE(holding_space_item->SetCurrentSizeInBytes(100));
-  EXPECT_EQ(holding_space_item->current_size_in_bytes(), 100);
+  // It should be possible to update secondary text to a new value.
+  EXPECT_TRUE(holding_space_item->SetSecondaryText(u"secondary_text"));
+  EXPECT_EQ(holding_space_item->secondary_text().value(), u"secondary_text");
 
-  // It should no-op to try to update current size to its existing value.
-  EXPECT_FALSE(holding_space_item->SetCurrentSizeInBytes(100));
-  EXPECT_EQ(holding_space_item->current_size_in_bytes(), 100);
+  // It should no-op to try to update secondary text to its existing value.
+  EXPECT_FALSE(holding_space_item->SetSecondaryText(u"secondary_text"));
+  EXPECT_EQ(holding_space_item->secondary_text().value(), u"secondary_text");
 
-  // It should be possible to unset current size.
-  EXPECT_TRUE(holding_space_item->SetCurrentSizeInBytes(absl::nullopt));
-  EXPECT_FALSE(holding_space_item->current_size_in_bytes());
+  // It should be possible to unset secondary text.
+  EXPECT_TRUE(holding_space_item->SetSecondaryText(absl::nullopt));
+  EXPECT_FALSE(holding_space_item->secondary_text());
+}
+
+// Tests setting the text for each holding space item type.
+TEST_P(HoldingSpaceItemTest, Text) {
+  // Create a `holding_space_item`.
+  auto holding_space_item = HoldingSpaceItem::CreateFileBackedItem(
+      /*type=*/GetParam(), base::FilePath("file_path"),
+      GURL("filesystem::file_system_url"),
+      /*image_resolver=*/base::BindOnce(&CreateFakeHoldingSpaceImage));
+
+  // Initially the text should reflect the backing file.
+  EXPECT_EQ(holding_space_item->GetText(), u"file_path");
+
+  // It should be possible to update text to a new value.
+  EXPECT_TRUE(holding_space_item->SetText(u"text"));
+  EXPECT_EQ(holding_space_item->GetText(), u"text");
+
+  // It should no-op to try to update text to its existing value.
+  EXPECT_FALSE(holding_space_item->SetText(u"text"));
+  EXPECT_EQ(holding_space_item->GetText(), u"text");
+
+  // It should be possible to unset text which will once again cause text to
+  // reflect the backing file.
+  EXPECT_TRUE(holding_space_item->SetText(absl::nullopt));
+  EXPECT_EQ(holding_space_item->GetText(), u"file_path");
 }
 
 INSTANTIATE_TEST_SUITE_P(All,
diff --git a/ash/public/cpp/holding_space/holding_space_model.cc b/ash/public/cpp/holding_space/holding_space_model.cc
index 9fe48aa..549c585 100644
--- a/ash/public/cpp/holding_space/holding_space_model.cc
+++ b/ash/public/cpp/holding_space/holding_space_model.cc
@@ -33,9 +33,13 @@
   if (progress_)
     did_update |= item_->SetProgress(progress_.value());
 
-  // Update current size.
-  if (current_size_in_bytes_)
-    did_update |= item_->SetCurrentSizeInBytes(current_size_in_bytes_.value());
+  // Update secondary text.
+  if (secondary_text_)
+    did_update |= item_->SetSecondaryText(secondary_text_.value());
+
+  // Update text.
+  if (text_)
+    did_update |= item_->SetText(text_.value());
 
   // Notify observers if and only if an update occurred.
   if (did_update) {
@@ -67,10 +71,16 @@
 }
 
 HoldingSpaceModel::ScopedItemUpdate&
-HoldingSpaceModel::ScopedItemUpdate::SetCurrentSizeInBytes(
-    const absl::optional<int64_t>& current_size_in_bytes) {
-  DCHECK(!current_size_in_bytes || current_size_in_bytes >= 0);
-  current_size_in_bytes_ = current_size_in_bytes;
+HoldingSpaceModel::ScopedItemUpdate::SetSecondaryText(
+    const absl::optional<std::u16string>& secondary_text) {
+  secondary_text_ = secondary_text;
+  return *this;
+}
+
+HoldingSpaceModel::ScopedItemUpdate&
+HoldingSpaceModel::ScopedItemUpdate::SetText(
+    const absl::optional<std::u16string>& text) {
+  text_ = text;
   return *this;
 }
 
diff --git a/ash/public/cpp/holding_space/holding_space_model.h b/ash/public/cpp/holding_space/holding_space_model.h
index 9333af0..af90133 100644
--- a/ash/public/cpp/holding_space/holding_space_model.h
+++ b/ash/public/cpp/holding_space/holding_space_model.h
@@ -59,11 +59,15 @@
     // NOTE: Once set to `1.f`, holding space item progress becomes read-only.
     ScopedItemUpdate& SetProgress(const absl::optional<float>& progress);
 
-    // Sets the current size (in bytes) of the file backing the item and returns
-    // a reference to `this`.
-    // NOTE: If present, the `current_size_in_bytes` must be >= `0`.
-    ScopedItemUpdate& SetCurrentSizeInBytes(
-        const absl::optional<int64_t>& current_size_in_bytes);
+    // Sets the secondary text that should be shown for the item and returns a
+    // reference to `this`.
+    ScopedItemUpdate& SetSecondaryText(
+        const absl::optional<std::u16string>& secondary_text);
+
+    // Sets the text that should be shown for the item and returns a reference
+    // to `this`. If absent, the lossy display name of the backing file will be
+    // used.
+    ScopedItemUpdate& SetText(const absl::optional<std::u16string>& text);
 
    private:
     friend class HoldingSpaceModel;
@@ -76,7 +80,8 @@
     absl::optional<GURL> file_system_url_;
     absl::optional<bool> paused_;
     absl::optional<absl::optional<float>> progress_;
-    absl::optional<absl::optional<int64_t>> current_size_in_bytes_;
+    absl::optional<absl::optional<std::u16string>> secondary_text_;
+    absl::optional<absl::optional<std::u16string>> text_;
   };
 
   HoldingSpaceModel();
diff --git a/ash/public/cpp/holding_space/holding_space_model_unittest.cc b/ash/public/cpp/holding_space/holding_space_model_unittest.cc
index 472c45d..9297e97 100644
--- a/ash/public/cpp/holding_space/holding_space_model_unittest.cc
+++ b/ash/public/cpp/holding_space/holding_space_model_unittest.cc
@@ -150,11 +150,17 @@
   EXPECT_EQ(observation.TakeUpdatedItemCount(), 1);
   EXPECT_EQ(item_ptr->progress(), 0.5f);
 
-  // Update current size
-  model().UpdateItem(item_ptr->id())->SetCurrentSizeInBytes(100);
+  // Update text.
+  model().UpdateItem(item_ptr->id())->SetText(u"text");
   EXPECT_EQ(observation.TakeLastUpdatedItem(), item_ptr);
   EXPECT_EQ(observation.TakeUpdatedItemCount(), 1);
-  EXPECT_EQ(item_ptr->current_size_in_bytes(), 100);
+  EXPECT_EQ(item_ptr->GetText(), u"text");
+
+  // Update secondary text.
+  model().UpdateItem(item_ptr->id())->SetSecondaryText(u"secondary_text");
+  EXPECT_EQ(observation.TakeLastUpdatedItem(), item_ptr);
+  EXPECT_EQ(observation.TakeUpdatedItemCount(), 1);
+  EXPECT_EQ(item_ptr->secondary_text(), u"secondary_text");
 
   // Update all attributes.
   updated_file_path = base::FilePath("again_updated_file_path");
@@ -162,7 +168,8 @@
   model()
       .UpdateItem(item_ptr->id())
       ->SetBackingFile(updated_file_path, updated_file_system_url)
-      .SetCurrentSizeInBytes(200)
+      .SetText(u"updated_text")
+      .SetSecondaryText(u"updated_secondary_text")
       .SetPaused(false)
       .SetProgress(0.75f);
   EXPECT_EQ(observation.TakeLastUpdatedItem(), item_ptr);
@@ -171,7 +178,8 @@
   EXPECT_EQ(item_ptr->file_system_url(), updated_file_system_url);
   EXPECT_FALSE(item_ptr->IsPaused());
   EXPECT_EQ(item_ptr->progress(), 0.75f);
-  EXPECT_EQ(item_ptr->current_size_in_bytes(), 200);
+  EXPECT_EQ(item_ptr->GetText(), u"updated_text");
+  EXPECT_EQ(item_ptr->secondary_text(), u"updated_secondary_text");
 }
 
 // Verifies that updating items will no-op appropriately.
@@ -202,7 +210,8 @@
   model()
       .UpdateItem(item_ptr->id())
       ->SetBackingFile(item_ptr->file_path(), item_ptr->file_system_url())
-      .SetCurrentSizeInBytes(absl::nullopt)
+      .SetText(absl::nullopt)
+      .SetSecondaryText(absl::nullopt)
       .SetPaused(item_ptr->IsPaused())
       .SetProgress(item_ptr->progress());
   EXPECT_EQ(observation.TakeUpdatedItemCount(), 0);
diff --git a/ash/system/holding_space/holding_space_drag_util.cc b/ash/system/holding_space/holding_space_drag_util.cc
index 1625149..8db66b1 100644
--- a/ash/system/holding_space/holding_space_drag_util.cc
+++ b/ash/system/holding_space/holding_space_drag_util.cc
@@ -224,7 +224,7 @@
     // Label.
     ScopedLightModeAsDefault scoped_light_mode;
     auto* label = AddChildView(bubble_utils::CreateLabel(
-        bubble_utils::LabelStyle::kChipTitle, item->text()));
+        bubble_utils::LabelStyle::kChipTitle, item->GetText()));
     label->SetElideBehavior(gfx::ElideBehavior::ELIDE_MIDDLE);
     label->SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_LEFT);
     layout->SetFlexForView(label, 1);
diff --git a/ash/system/holding_space/holding_space_item_chip_view.cc b/ash/system/holding_space/holding_space_item_chip_view.cc
index a4d4eef..bad2892 100644
--- a/ash/system/holding_space/holding_space_item_chip_view.cc
+++ b/ash/system/holding_space/holding_space_item_chip_view.cc
@@ -15,6 +15,7 @@
 #include "ash/resources/vector_icons/vector_icons.h"
 #include "ash/style/ash_color_provider.h"
 #include "ash/system/holding_space/holding_space_item_view.h"
+#include "ash/system/holding_space/holding_space_progress_ring.h"
 #include "ash/system/holding_space/holding_space_view_builder.h"
 #include "ash/system/holding_space/holding_space_view_delegate.h"
 #include "base/bind.h"
@@ -25,6 +26,7 @@
 #include "ui/gfx/paint_vector_icon.h"
 #include "ui/gfx/scoped_canvas.h"
 #include "ui/gfx/skia_paint_util.h"
+#include "ui/gfx/transform_util.h"
 #include "ui/views/accessibility/view_accessibility.h"
 #include "ui/views/border.h"
 #include "ui/views/controls/button/image_button.h"
@@ -41,6 +43,7 @@
 
 // Appearance.
 constexpr int kChildSpacing = 8;
+constexpr float kInProgressImageScaleFactor = 0.7f;
 constexpr int kLabelMaskGradientWidth = 16;
 constexpr gfx::Insets kLabelMargins(/*top=*/4, 0, /*bottom=*/4, /*right=*/2);
 constexpr gfx::Insets kPadding(0, /*left=*/8, 0, /*right=*/10);
@@ -75,10 +78,51 @@
 VIEW_BUILDER_PROPERTY(PaintCallbackLabel::Callback, Callback)
 END_VIEW_BUILDER
 
+// ProgressRingView ------------------------------------------------------------
+
+class ProgressRingView : public views::View {
+ public:
+  ProgressRingView() = default;
+  ProgressRingView(const ProgressRingView&) = delete;
+  ProgressRingView& operator=(const ProgressRingView&) = delete;
+  ~ProgressRingView() override = default;
+
+  // Sets the underlying `item` for which to indicate progress.
+  // NOTE: This method should be invoked only once.
+  void SetHoldingSpaceItem(const HoldingSpaceItem* item) {
+    DCHECK(!progress_ring_);
+    progress_ring_ = std::make_unique<HoldingSpaceProgressRing>(item);
+
+    SetPaintToLayer();
+    layer()->SetFillsBoundsOpaquely(false);
+    layer()->Add(progress_ring_->layer());
+  }
+
+ private:
+  // views::View:
+  void OnBoundsChanged(const gfx::Rect& previous_bounds) override {
+    if (progress_ring_)
+      progress_ring_->layer()->SetBounds(GetLocalBounds());
+  }
+
+  void OnThemeChanged() override {
+    views::View::OnThemeChanged();
+    if (progress_ring_)
+      progress_ring_->InvalidateLayer();
+  }
+
+  std::unique_ptr<HoldingSpaceProgressRing> progress_ring_;
+};
+
+BEGIN_VIEW_BUILDER(/*no export*/, ProgressRingView, views::View)
+VIEW_BUILDER_PROPERTY(const HoldingSpaceItem*, HoldingSpaceItem)
+END_VIEW_BUILDER
+
 }  // namespace
 }  // namespace ash
 
 DEFINE_VIEW_BUILDER(/*no export*/, ash::PaintCallbackLabel)
+DEFINE_VIEW_BUILDER(/*no export*/, ash::ProgressRingView)
 
 namespace ash {
 namespace {
@@ -157,7 +201,9 @@
       .SetLayoutManager(std::move(layout_manager))
       .AddChild(
           HoldingSpaceViewBuilder<views::View>(
-              views::Builder<views::View>().SetUseDefaultFillLayout(true))
+              views::Builder<ProgressRingView>()
+                  .SetHoldingSpaceItem(item)
+                  .SetUseDefaultFillLayout(true))
               .AddChild(views::Builder<RoundedImageView>()
                             .CopyAddressTo(&image_)
                             .SetID(kHoldingSpaceItemImageId)
@@ -233,6 +279,7 @@
     const HoldingSpaceItem* item) {
   HoldingSpaceItemView::OnHoldingSpaceItemUpdated(item);
   if (this->item() == item) {
+    UpdateImage();
     UpdateLabels();
     UpdateSecondaryAction();
   }
@@ -322,38 +369,48 @@
 }
 
 void HoldingSpaceItemChipView::UpdateImage() {
+  // Image.
   image_->SetImage(item()->image().GetImageSkia(
       gfx::Size(kHoldingSpaceChipIconSize, kHoldingSpaceChipIconSize),
       /*dark_background=*/AshColorProvider::Get()->IsDarkModeEnabled()));
   SchedulePaint();
+
+  // Transform.
+  gfx::Transform transform;
+  if (item()->IsInProgress() && !image_->bounds().IsEmpty()) {
+    transform = gfx::GetScaleTransform(image_->bounds().CenterPoint(),
+                                       kInProgressImageScaleFactor);
+  }
+
+  if (!image_->layer()) {
+    image_->SetPaintToLayer();
+    image_->layer()->SetFillsBoundsOpaquely(false);
+  }
+
+  image_->layer()->SetTransform(transform);
 }
 
-// TODO(dmblack): Implement secondary label text.
 void HoldingSpaceItemChipView::UpdateLabels() {
   const bool multiselect = delegate()->selection_ui() ==
                            HoldingSpaceViewDelegate::SelectionUi::kMultiSelect;
 
   // Primary.
-  primary_label_->SetText(item()->text());
+  primary_label_->SetText(item()->GetText());
   primary_label_->SetEnabledColor(
       selected() && multiselect
           ? GetMultiSelectTextColor()
           : AshColorProvider::Get()->GetContentLayerColor(
                 AshColorProvider::ContentLayerType::kTextColorPrimary));
 
-  if (!item()->IsInProgress()) {
-    secondary_label_->SetVisible(false);
-    return;
-  }
-
   // Secondary.
-  secondary_label_->SetText(item()->text());
+  secondary_label_->SetText(
+      item()->secondary_text().value_or(base::EmptyString16()));
   secondary_label_->SetEnabledColor(
       selected() && multiselect
           ? GetMultiSelectTextColor()
           : AshColorProvider::Get()->GetContentLayerColor(
                 AshColorProvider::ContentLayerType::kTextColorSecondary));
-  secondary_label_->SetVisible(true);
+  secondary_label_->SetVisible(!secondary_label_->GetText().empty());
 }
 
 void HoldingSpaceItemChipView::UpdateSecondaryAction() {
diff --git a/ash/system/holding_space/holding_space_item_screen_capture_view.cc b/ash/system/holding_space/holding_space_item_screen_capture_view.cc
index 33f2118..ea6ea76 100644
--- a/ash/system/holding_space/holding_space_item_screen_capture_view.cc
+++ b/ash/system/holding_space/holding_space_item_screen_capture_view.cc
@@ -97,7 +97,7 @@
 
 std::u16string HoldingSpaceItemScreenCaptureView::GetTooltipText(
     const gfx::Point& point) const {
-  return item()->text();
+  return item()->GetText();
 }
 
 void HoldingSpaceItemScreenCaptureView::OnHoldingSpaceItemUpdated(
diff --git a/ash/system/holding_space/holding_space_item_view.cc b/ash/system/holding_space/holding_space_item_view.cc
index 7f75d1f..ec15ed3 100644
--- a/ash/system/holding_space/holding_space_item_view.cc
+++ b/ash/system/holding_space/holding_space_item_view.cc
@@ -126,7 +126,7 @@
   SetNotifyEnterExitOnChild(true);
 
   // Accessibility.
-  GetViewAccessibility().OverrideName(item->text());
+  GetViewAccessibility().OverrideName(item->GetText());
   GetViewAccessibility().OverrideRole(ax::mojom::Role::kListItem);
 
   // Layer.
@@ -289,7 +289,7 @@
 void HoldingSpaceItemView::OnHoldingSpaceItemUpdated(
     const HoldingSpaceItem* item) {
   if (item_ == item) {
-    GetViewAccessibility().OverrideName(item->text());
+    GetViewAccessibility().OverrideName(item->GetText());
     UpdatePrimaryAction();
   }
 }
diff --git a/ash/system/holding_space/holding_space_progress_ring.cc b/ash/system/holding_space/holding_space_progress_ring.cc
new file mode 100644
index 0000000..87c28de
--- /dev/null
+++ b/ash/system/holding_space/holding_space_progress_ring.cc
@@ -0,0 +1,113 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ash/system/holding_space/holding_space_progress_ring.h"
+
+#include <memory>
+
+#include "ash/public/cpp/holding_space/holding_space_controller.h"
+#include "ash/public/cpp/holding_space/holding_space_item.h"
+#include "ash/style/ash_color_provider.h"
+#include "ui/compositor/layer.h"
+#include "ui/compositor/paint_recorder.h"
+#include "ui/gfx/canvas.h"
+#include "ui/gfx/geometry/insets.h"
+#include "ui/gfx/scoped_canvas.h"
+#include "ui/gfx/skia_util.h"
+
+namespace ash {
+namespace {
+
+// Appearance.
+constexpr float kStrokeWidth = 2.f;
+constexpr float kTrackOpacity = 0.3f;
+
+// Helpers ---------------------------------------------------------------------
+
+// Returns the sweep angle to represent progress of the specified `item`.
+// NOTE: This method may only be called if `item` has determinate progress.
+float CalculateSweepAngle(const HoldingSpaceItem* item) {
+  DCHECK(item->IsInProgress());
+  DCHECK(item->progress().has_value());
+  return 360.f * item->progress().value();
+}
+
+}  // namespace
+
+// HoldingSpaceProgressRing ----------------------------------------------------
+
+HoldingSpaceProgressRing::HoldingSpaceProgressRing(const HoldingSpaceItem* item)
+    : ui::LayerOwner(std::make_unique<ui::Layer>(ui::LAYER_TEXTURED)),
+      item_(item) {
+  layer()->set_delegate(this);
+  layer()->SetFillsBoundsOpaquely(false);
+  model_observation_.Observe(HoldingSpaceController::Get()->model());
+}
+
+HoldingSpaceProgressRing::~HoldingSpaceProgressRing() = default;
+
+void HoldingSpaceProgressRing::InvalidateLayer() {
+  layer()->SchedulePaint(gfx::Rect(layer()->size()));
+}
+
+void HoldingSpaceProgressRing::OnDeviceScaleFactorChanged(float old_scale,
+                                                          float new_scale) {
+  InvalidateLayer();
+}
+
+// TODO(crbug.com/1184438): Handle indeterminate progress.
+void HoldingSpaceProgressRing::OnPaintLayer(const ui::PaintContext& context) {
+  if (!item_ || !item_->IsInProgress() || !item_->progress().has_value())
+    return;
+
+  ui::PaintRecorder recorder(context, layer()->size());
+  gfx::Canvas* canvas = recorder.canvas();
+
+  // The `canvas` should be flipped for RTL.
+  gfx::ScopedCanvas scoped_canvas(recorder.canvas());
+  scoped_canvas.FlipIfRTL(layer()->size().width());
+
+  gfx::Rect bounds(layer()->size());
+  bounds.Inset(gfx::Insets(std::ceil(kStrokeWidth / 2.f)));
+
+  cc::PaintFlags flags;
+  flags.setAntiAlias(true);
+  flags.setStrokeCap(cc::PaintFlags::Cap::kRound_Cap);
+  flags.setStrokeWidth(kStrokeWidth);
+  flags.setStyle(cc::PaintFlags::Style::kStroke_Style);
+
+  const SkColor color = AshColorProvider::Get()->GetControlsLayerColor(
+      AshColorProvider::ControlsLayerType::kFocusRingColor);
+
+  // Track.
+  flags.setColor(SkColorSetA(color, 0xFF * kTrackOpacity));
+  canvas->DrawCircle(gfx::PointF(bounds.CenterPoint()),
+                     std::min(bounds.height(), bounds.width()) / 2.f, flags);
+
+  // Ring.
+  flags.setColor(color);
+  canvas->DrawPath(
+      SkPath().arcTo(/*oval=*/gfx::RectToSkRect(bounds), /*start_angle=*/-90,
+                     /*sweep_angle=*/CalculateSweepAngle(item_),
+                     /*forceMoveTo=*/false),
+      flags);
+}
+
+void HoldingSpaceProgressRing::OnHoldingSpaceItemUpdated(
+    const HoldingSpaceItem* item) {
+  if (item_ == item)
+    InvalidateLayer();
+}
+
+void HoldingSpaceProgressRing::OnHoldingSpaceItemsRemoved(
+    const std::vector<const HoldingSpaceItem*>& items) {
+  for (const HoldingSpaceItem* item : items) {
+    if (item_ == item) {
+      item_ = nullptr;
+      return;
+    }
+  }
+}
+
+}  // namespace ash
\ No newline at end of file
diff --git a/ash/system/holding_space/holding_space_progress_ring.h b/ash/system/holding_space/holding_space_progress_ring.h
new file mode 100644
index 0000000..ced85e9
--- /dev/null
+++ b/ash/system/holding_space/holding_space_progress_ring.h
@@ -0,0 +1,55 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef ASH_SYSTEM_HOLDING_SPACE_HOLDING_SPACE_PROGRESS_RING_H_
+#define ASH_SYSTEM_HOLDING_SPACE_HOLDING_SPACE_PROGRESS_RING_H_
+
+#include <vector>
+
+#include "ash/public/cpp/holding_space/holding_space_model.h"
+#include "ash/public/cpp/holding_space/holding_space_model_observer.h"
+#include "base/scoped_observation.h"
+#include "ui/compositor/layer_delegate.h"
+#include "ui/compositor/layer_owner.h"
+
+namespace ash {
+
+class HoldingSpaceItem;
+
+// A class owning a `ui::Layer` which paints a ring to indicate progress of its
+// associated holding space `item`. NOTE: The `ui::Layer` is not painted if the
+// holding space `item` is not in-progress.
+class HoldingSpaceProgressRing : public ui::LayerOwner,
+                                 public ui::LayerDelegate,
+                                 public HoldingSpaceModelObserver {
+ public:
+  explicit HoldingSpaceProgressRing(const HoldingSpaceItem* item);
+  HoldingSpaceProgressRing(const HoldingSpaceProgressRing&) = delete;
+  HoldingSpaceProgressRing& operator=(const HoldingSpaceProgressRing&) = delete;
+  ~HoldingSpaceProgressRing() override;
+
+  // Invoke to schedule repaint of the entire `layer()`.
+  void InvalidateLayer();
+
+ private:
+  // ui::LayerDelegate:
+  void OnDeviceScaleFactorChanged(float old_scale, float new_scale) override;
+  void OnPaintLayer(const ui::PaintContext& context) override;
+
+  // HoldingSpaceModelObserver:
+  void OnHoldingSpaceItemUpdated(const HoldingSpaceItem* item) override;
+  void OnHoldingSpaceItemsRemoved(
+      const std::vector<const HoldingSpaceItem*>& items) override;
+
+  // The associated holding space `item` for which to indicate progress.
+  // NOTE: May temporarily be `nullptr` during the `item`s destruction sequence.
+  const HoldingSpaceItem* item_ = nullptr;
+
+  base::ScopedObservation<HoldingSpaceModel, HoldingSpaceModelObserver>
+      model_observation_{this};
+};
+
+}  // namespace ash
+
+#endif  // ASH_SYSTEM_HOLDING_SPACE_HOLDING_SPACE_PROGRESS_RING_H_
diff --git a/ash/system/holding_space/holding_space_tray_icon_preview.cc b/ash/system/holding_space/holding_space_tray_icon_preview.cc
index 0ec2157..3d12bfd 100644
--- a/ash/system/holding_space/holding_space_tray_icon_preview.cc
+++ b/ash/system/holding_space/holding_space_tray_icon_preview.cc
@@ -75,6 +75,28 @@
   return gfx::ShadowDetails::Get(kElevation, radius);
 }
 
+// Adjust the specified `origin` for shadow margins.
+void AdjustOriginForShadowMargins(gfx::Point& origin, const Shelf* shelf) {
+  const gfx::ShadowValues& values(GetShadowDetails().values);
+  const gfx::Insets margins(gfx::ShadowValue::GetMargin(values));
+  if (shelf->IsHorizontalAlignment()) {
+    // When the `shelf` is horizontally aligned the `origin` will already have
+    // been offset to center the preview `layer()` vertically within its parent
+    // container so no further vertical offset  is needed.
+    const int offset = margins.width() / 2;
+    origin.Offset(base::i18n::IsRTL() ? -offset : offset, 0);
+  } else {
+    origin.Offset(margins.width() / 2, margins.height() / 2);
+  }
+}
+
+// Enlarges the specified `size` for shadow margins.
+void EnlargeForShadowMargins(gfx::Size& size) {
+  const gfx::ShadowValues& values(GetShadowDetails().values);
+  const gfx::Insets margins(gfx::ShadowValue::GetMargin(values));
+  size.Enlarge(-margins.width(), -margins.height());
+}
+
 // Returns whether the specified `shelf_alignment` is horizontal.
 bool IsHorizontal(ShelfAlignment shelf_alignment) {
   switch (shelf_alignment) {
@@ -398,11 +420,14 @@
 
 void HoldingSpaceTrayIconPreview::OnPaintLayer(
     const ui::PaintContext& context) {
-  const gfx::Rect contents_bounds = gfx::Rect(GetPreviewSize());
-
-  ui::PaintRecorder recorder(context, contents_bounds.size());
+  ui::PaintRecorder recorder(context, layer()->size());
   gfx::Canvas* canvas = recorder.canvas();
 
+  // The `layer()` was enlarged so that the shadow would be painted outside of
+  // desired preview bounds. Content bounds should be clamped to preview size.
+  gfx::Rect contents_bounds = gfx::Rect(layer()->size());
+  contents_bounds.ClampToCenteredSize(GetPreviewSize());
+
   // Background.
   // NOTE: The background radius is shrunk by a single pixel to avoid being
   // painted outside `contents_image_` bounds as might otherwise occur due to
@@ -417,7 +442,7 @@
   flags.setLooper(gfx::CreateShadowDrawLooper(GetShadowDetails().values));
   canvas->DrawCircle(
       gfx::PointF(contents_bounds.CenterPoint()),
-      std::min(contents_bounds.width(), contents_bounds.height()) / 2 - 0.5,
+      std::min(contents_bounds.width(), contents_bounds.height()) / 2.f - 0.5f,
       flags);
 
   // Contents.
@@ -519,11 +544,15 @@
 
 void HoldingSpaceTrayIconPreview::UpdateLayerBounds() {
   DCHECK(layer());
+
+  // The shadow for `layer()` should be painted outside desired preview bounds.
+  gfx::Size size = GetPreviewSize();
+  EnlargeForShadowMargins(size);
+
   // With a horizontal shelf in RTL, `layer()` is aligned with its parent
   // layer's right bound and translated with a negative offset. In all other
   // cases, `layer()` is aligned with its parent layer's left/top bound and
   // translated with a positive offset.
-  const gfx::Size size = GetPreviewSize();
   gfx::Point origin;
   if (shelf_->IsHorizontalAlignment()) {
     gfx::Rect container_bounds = container_->GetLocalBounds();
@@ -531,6 +560,7 @@
       origin = container_bounds.top_right() - gfx::Vector2d(size.width(), 0);
     origin.Offset(0, (container_bounds.height() - size.height()) / 2);
   }
+  AdjustOriginForShadowMargins(origin, shelf_);
   gfx::Rect bounds(origin, size);
   if (bounds != layer()->bounds())
     layer()->SetBounds(bounds);
diff --git a/ash/system/ime/ime_feature_pod_controller.cc b/ash/system/ime/ime_feature_pod_controller.cc
index 2ff170c..719f460 100644
--- a/ash/system/ime/ime_feature_pod_controller.cc
+++ b/ash/system/ime/ime_feature_pod_controller.cc
@@ -21,7 +21,7 @@
 bool IsButtonVisible() {
   DCHECK(Shell::Get());
   ImeControllerImpl* ime_controller = Shell::Get()->ime_controller();
-  size_t ime_count = ime_controller->available_imes().size();
+  size_t ime_count = ime_controller->GetVisibleImes().size();
   return !ime_controller->is_menu_active() &&
          (ime_count > 1 || ime_controller->managed_by_policy());
 }
@@ -29,7 +29,7 @@
 std::u16string GetLabelString() {
   DCHECK(Shell::Get());
   ImeControllerImpl* ime_controller = Shell::Get()->ime_controller();
-  size_t ime_count = ime_controller->available_imes().size();
+  size_t ime_count = ime_controller->GetVisibleImes().size();
   if (ime_count > 1) {
     return ime_controller->current_ime().short_name;
   } else {
@@ -42,7 +42,7 @@
 std::u16string GetTooltipString() {
   DCHECK(Shell::Get());
   ImeControllerImpl* ime_controller = Shell::Get()->ime_controller();
-  size_t ime_count = ime_controller->available_imes().size();
+  size_t ime_count = ime_controller->GetVisibleImes().size();
   if (ime_count > 1) {
     return l10n_util::GetStringFUTF16(IDS_ASH_STATUS_TRAY_IME_TOOLTIP_WITH_NAME,
                                       ime_controller->current_ime().name);
diff --git a/ash/system/ime/unified_ime_detailed_view_controller.cc b/ash/system/ime/unified_ime_detailed_view_controller.cc
index 5d580ef..dbe0b97f 100644
--- a/ash/system/ime/unified_ime_detailed_view_controller.cc
+++ b/ash/system/ime/unified_ime_detailed_view_controller.cc
@@ -75,7 +75,7 @@
 void UnifiedIMEDetailedViewController::Update() {
   ImeControllerImpl* ime_controller = Shell::Get()->ime_controller();
   view_->Update(ime_controller->current_ime().id,
-                ime_controller->available_imes(),
+                ime_controller->GetVisibleImes(),
                 ime_controller->current_ime_menu_items(),
                 ShouldShowKeyboardToggle(), GetSingleImeBehavior());
 }
diff --git a/ash/system/ime_menu/ime_list_view.cc b/ash/system/ime_menu/ime_list_view.cc
index b1f6809..5d69213 100644
--- a/ash/system/ime_menu/ime_list_view.cc
+++ b/ash/system/ime_menu/ime_list_view.cc
@@ -209,7 +209,7 @@
 void ImeListView::Init(bool show_keyboard_toggle,
                        SingleImeBehavior single_ime_behavior) {
   ImeControllerImpl* ime_controller = Shell::Get()->ime_controller();
-  Update(ime_controller->current_ime().id, ime_controller->available_imes(),
+  Update(ime_controller->current_ime().id, ime_controller->GetVisibleImes(),
          ime_controller->current_ime_menu_items(), show_keyboard_toggle,
          single_ime_behavior);
 }
diff --git a/ash/system/ime_menu/ime_menu_tray.cc b/ash/system/ime_menu/ime_menu_tray.cc
index e79cdb6..54af1a6 100644
--- a/ash/system/ime_menu/ime_menu_tray.cc
+++ b/ash/system/ime_menu/ime_menu_tray.cc
@@ -486,7 +486,7 @@
   UpdateTrayLabel();
   if (bubble_ && ime_list_view_) {
     ime_list_view_->Update(
-        ime_controller_->current_ime().id, ime_controller_->available_imes(),
+        ime_controller_->current_ime().id, ime_controller_->GetVisibleImes(),
         ime_controller_->current_ime_menu_items(), ShouldShowKeyboardToggle(),
         ImeListView::SHOW_SINGLE_IME);
   }
diff --git a/ash/system/ime_menu/ime_menu_tray_unittest.cc b/ash/system/ime_menu/ime_menu_tray_unittest.cc
index c253477..8547dc0 100644
--- a/ash/system/ime_menu/ime_menu_tray_unittest.cc
+++ b/ash/system/ime_menu/ime_menu_tray_unittest.cc
@@ -164,6 +164,39 @@
   EXPECT_EQ(u"UK*", GetTrayText());
 }
 
+TEST_F(ImeMenuTrayTest, TrayLabelExludesDictation) {
+  Shell::Get()->ime_controller()->ShowImeMenuOnShelf(true);
+  ASSERT_TRUE(IsVisible());
+
+  ImeInfo info1;
+  info1.id = "ime1";
+  info1.name = u"English";
+  info1.short_name = u"US";
+  info1.third_party = false;
+
+  ImeInfo info2;
+  info2.id = "ime2";
+  info2.name = u"English UK";
+  info2.short_name = u"UK";
+  info2.third_party = true;
+
+  ImeInfo dictation;
+  dictation.id = "_ext_ime_egfdjlfmgnehecnclamagfafdccgfndpdictation";
+  dictation.name = u"Dictation";
+
+  // Changes the input method to "ime1".
+  SetCurrentIme("ime1", {info1, dictation, info2});
+  EXPECT_EQ(u"US", GetTrayText());
+
+  // Changes the input method to a third-party IME extension.
+  SetCurrentIme("ime2", {info1, dictation, info2});
+  EXPECT_EQ(u"UK*", GetTrayText());
+
+  // Sets to "dictation", which shouldn't be shown.
+  SetCurrentIme(dictation.id, {info1, dictation, info2});
+  EXPECT_EQ(u"", GetTrayText());
+}
+
 // Tests that IME menu tray changes background color when tapped/clicked. And
 // tests that the background color becomes 'inactive' when disabling the IME
 // menu feature. Also makes sure that the shelf won't autohide as long as the
diff --git a/ash/system/unified/ime_mode_view.cc b/ash/system/unified/ime_mode_view.cc
index 74f981d..9f2f509 100644
--- a/ash/system/unified/ime_mode_view.cc
+++ b/ash/system/unified/ime_mode_view.cc
@@ -92,7 +92,12 @@
 
   ImeControllerImpl* ime_controller = Shell::Get()->ime_controller();
 
-  size_t ime_count = ime_controller->available_imes().size();
+  if (!ime_controller->IsCurrentImeVisible()) {
+    SetVisible(false);
+    return;
+  }
+
+  size_t ime_count = ime_controller->GetVisibleImes().size();
   SetVisible(!ime_menu_on_shelf_activated_ &&
              (ime_count > 1 || ime_controller->managed_by_policy()));
 
diff --git a/ash/wm/full_restore/full_restore_controller.cc b/ash/wm/full_restore/full_restore_controller.cc
index b8bf886..c823aab8 100644
--- a/ash/wm/full_restore/full_restore_controller.cc
+++ b/ash/wm/full_restore/full_restore_controller.cc
@@ -283,6 +283,12 @@
   UpdateAndObserveWindow(window);
 }
 
+void FullRestoreController::OnRestorePrefChanged(const AccountId& account_id,
+                                                 bool could_restore) {
+  if (could_restore)
+    SaveAllWindows();
+}
+
 void FullRestoreController::OnWindowPropertyChanged(aura::Window* window,
                                                     const void* key,
                                                     intptr_t old) {
@@ -421,7 +427,7 @@
   }
 
   // Do not save window data if the setting is turned off by active user.
-  if (!full_restore::FullRestoreInfo::GetInstance()->ShouldRestore(
+  if (!full_restore::FullRestoreInfo::GetInstance()->CanPerformRestore(
           Shell::Get()->session_controller()->GetActiveAccountId())) {
     return;
   }
diff --git a/ash/wm/full_restore/full_restore_controller.h b/ash/wm/full_restore/full_restore_controller.h
index 26e04c7..bac9f08 100644
--- a/ash/wm/full_restore/full_restore_controller.h
+++ b/ash/wm/full_restore/full_restore_controller.h
@@ -13,6 +13,7 @@
 #include "base/memory/weak_ptr.h"
 #include "base/scoped_multi_source_observation.h"
 #include "base/scoped_observation.h"
+#include "components/account_id/account_id.h"
 #include "components/full_restore/full_restore_info.h"
 #include "components/full_restore/window_info.h"
 #include "ui/aura/window_observer.h"
@@ -75,6 +76,8 @@
   // full_restore::FullRestoreInfo::Observer:
   void OnWidgetInitialized(views::Widget* widget) override;
   void OnARCTaskReadyForUnparentedWindow(aura::Window* window) override;
+  void OnRestorePrefChanged(const AccountId& account_id,
+                            bool could_restore) override;
 
   // aura::WindowObserver:
   void OnWindowPropertyChanged(aura::Window* window,
diff --git a/ash/wm/full_restore/full_restore_controller_unittest.cc b/ash/wm/full_restore/full_restore_controller_unittest.cc
index 2230fd4..0011acf 100644
--- a/ash/wm/full_restore/full_restore_controller_unittest.cc
+++ b/ash/wm/full_restore/full_restore_controller_unittest.cc
@@ -257,9 +257,9 @@
                             base::Unretained(this)));
     env_observation_.Observe(aura::Env::GetInstance());
 
-    // Turn on should restore flag by default, so do not need to set the flag
+    // Turn on the user preference by default, so do not need to set
     // for all test cases all the time.
-    full_restore::FullRestoreInfo::GetInstance()->SetRestoreFlag(
+    full_restore::FullRestoreInfo::GetInstance()->SetRestorePref(
         Shell::Get()->session_controller()->GetActiveAccountId(), true);
   }
 
@@ -354,23 +354,33 @@
 
 // Tests window save with setting on or off.
 TEST_F(FullRestoreControllerTest, WindowSaveDisabled) {
-  auto window = CreateAppWindow(gfx::Rect(600, 600), AppType::BROWSER);
+  auto account_id = Shell::Get()->session_controller()->GetActiveAccountId();
+  auto window1 = CreateAppWindow(gfx::Rect(600, 600), AppType::BROWSER);
+  auto window2 = CreateAppWindow(gfx::Rect(600, 600), AppType::BROWSER);
   ResetSaveWindowsCount();
 
   // Disable full restore.
-  full_restore::FullRestoreInfo::GetInstance()->SetRestoreFlag(
-      Shell::Get()->session_controller()->GetActiveAccountId(), false);
+  full_restore::FullRestoreInfo::GetInstance()->SetRestorePref(account_id,
+                                                               false);
 
-  auto* window_state = WindowState::Get(window.get());
-  window_state->Minimize();
-  EXPECT_EQ(0, GetSaveWindowsCount(window.get()));
+  auto* window1_state = WindowState::Get(window1.get());
+  auto* window2_state = WindowState::Get(window2.get());
+
+  // Window minimization should not trigger window save with the
+  // user preference off.
+  window1_state->Minimize();
+  window2_state->Minimize();
+  EXPECT_EQ(0, GetSaveWindowsCount(window1.get()));
+  EXPECT_EQ(0, GetSaveWindowsCount(window2.get()));
 
   // Enable full restore.
-  full_restore::FullRestoreInfo::GetInstance()->SetRestoreFlag(
-      Shell::Get()->session_controller()->GetActiveAccountId(), true);
+  full_restore::FullRestoreInfo::GetInstance()->SetRestorePref(account_id,
+                                                               true);
 
-  window_state->Unminimize();
-  EXPECT_EQ(1, GetSaveWindowsCount(window.get()));
+  // Setting the user preference to true should trigger window save
+  // immediately.
+  EXPECT_EQ(1, GetSaveWindowsCount(window1.get()));
+  EXPECT_EQ(1, GetSaveWindowsCount(window2.get()));
 }
 
 // Tests that data gets saved when changing a window's window state.
diff --git a/base/android/java/src/org/chromium/base/jank_tracker/JankReportingRunnable.java b/base/android/java/src/org/chromium/base/jank_tracker/JankReportingRunnable.java
index f108237..9efccc2 100644
--- a/base/android/java/src/org/chromium/base/jank_tracker/JankReportingRunnable.java
+++ b/base/android/java/src/org/chromium/base/jank_tracker/JankReportingRunnable.java
@@ -29,7 +29,7 @@
             mMetricsStore.startTrackingScenario(mScenario);
         } else {
             FrameMetrics frames = mMetricsStore.stopTrackingScenario(mScenario);
-            if (frames == null) {
+            if (frames == null || frames.timestampsNs.length == 0) {
                 return;
             }
 
diff --git a/base/android/java/src/org/chromium/base/jank_tracker/JankTracker.java b/base/android/java/src/org/chromium/base/jank_tracker/JankTracker.java
index 9ce17ce..d41e812 100644
--- a/base/android/java/src/org/chromium/base/jank_tracker/JankTracker.java
+++ b/base/android/java/src/org/chromium/base/jank_tracker/JankTracker.java
@@ -5,9 +5,7 @@
 package org.chromium.base.jank_tracker;
 
 import android.app.Activity;
-import android.os.Build.VERSION_CODES;
-
-import androidx.annotation.RequiresApi;
+import android.os.Build;
 
 /**
  * Class for recording janky frame metrics for a specific Activity.
@@ -16,8 +14,10 @@
  * based on activity state. When the activity is being destroyed {@link #destroy()} should be called
  * to clear the activity state observer. All methods should be called from the UI thread.
  */
-@RequiresApi(api = VERSION_CODES.N)
 public final class JankTracker {
+    private static final boolean IS_TRACKING_ENABLED =
+            Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
+
     private final JankActivityTracker mActivityTracker;
     private final JankReportingScheduler mReportingScheduler;
 
@@ -26,6 +26,12 @@
      * starts when the activity starts, and it's paused when the activity stops.
      */
     public JankTracker(Activity activity) {
+        if (!IS_TRACKING_ENABLED) {
+            mActivityTracker = null;
+            mReportingScheduler = null;
+            return;
+        }
+
         FrameMetricsStore metricsStore = new FrameMetricsStore();
         FrameMetricsListener metricsListener = new FrameMetricsListener(metricsStore);
         mReportingScheduler = new JankReportingScheduler(metricsStore);
@@ -34,10 +40,14 @@
     }
 
     public void startTrackingScenario(@JankScenario int scenario) {
+        if (!IS_TRACKING_ENABLED) return;
+
         mReportingScheduler.startTrackingScenario(scenario);
     }
 
     public void finishTrackingScenario(@JankScenario int scenario) {
+        if (!IS_TRACKING_ENABLED) return;
+
         mReportingScheduler.finishTrackingScenario(scenario);
     }
 
@@ -45,6 +55,8 @@
      * Stops listening for Activity state changes.
      */
     public void destroy() {
+        if (!IS_TRACKING_ENABLED) return;
+
         mActivityTracker.destroy();
     }
 }
diff --git a/base/android/junit/src/org/chromium/base/jank_tracker/JankReportingRunnableTest.java b/base/android/junit/src/org/chromium/base/jank_tracker/JankReportingRunnableTest.java
index 05a24aa..30be201 100644
--- a/base/android/junit/src/org/chromium/base/jank_tracker/JankReportingRunnableTest.java
+++ b/base/android/junit/src/org/chromium/base/jank_tracker/JankReportingRunnableTest.java
@@ -6,6 +6,7 @@
 
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -68,4 +69,26 @@
 
         verify(mNativeMock).recordJankMetrics("TabSwitcher", new long[] {1_000L}, new long[0], 1);
     }
+
+    @Test
+    public void testStopTracking_emptyStoreShouldntRecordAnything() {
+        // Create a store but don't add any measurements.
+        FrameMetricsStore metricsStore = Mockito.spy(new FrameMetricsStore());
+
+        JankReportingRunnable startReportingRunnable = new JankReportingRunnable(
+                metricsStore, JankScenario.TAB_SWITCHER, /* isStartingTracking= */ true);
+        startReportingRunnable.run();
+
+        LibraryLoader.getInstance().setLibrariesLoadedForNativeTests();
+
+        JankReportingRunnable stopReportingRunnable = new JankReportingRunnable(
+                metricsStore, JankScenario.TAB_SWITCHER, /* isStartingTracking= */ false);
+        stopReportingRunnable.run();
+
+        verify(metricsStore).startTrackingScenario(JankScenario.TAB_SWITCHER);
+        verify(metricsStore).stopTrackingScenario(JankScenario.TAB_SWITCHER);
+
+        // Native shouldn't be called when there are no measurements.
+        verifyZeroInteractions(mNativeMock);
+    }
 }
diff --git a/build/OWNERS b/build/OWNERS
index 405ccdc..bd907811 100644
--- a/build/OWNERS
+++ b/build/OWNERS
@@ -9,7 +9,7 @@
 tikuta@chromium.org
 
 # Clang build config changes:
-hans@chromium.org
+file://tools/clang/scripts/OWNERS
 
 # For java build changes:
 wnwen@chromium.org
diff --git a/build/fuchsia/linux.sdk.sha1 b/build/fuchsia/linux.sdk.sha1
index cc970f0..9673438 100644
--- a/build/fuchsia/linux.sdk.sha1
+++ b/build/fuchsia/linux.sdk.sha1
@@ -1 +1 @@
-5.20210615.2.1
+5.20210616.2.1
diff --git a/build/fuchsia/mac.sdk.sha1 b/build/fuchsia/mac.sdk.sha1
index cc970f0..9673438 100644
--- a/build/fuchsia/mac.sdk.sha1
+++ b/build/fuchsia/mac.sdk.sha1
@@ -1 +1 @@
-5.20210615.2.1
+5.20210616.2.1
diff --git a/build/sanitizers/tsan_suppressions.cc b/build/sanitizers/tsan_suppressions.cc
index 6704a34..06d32f3 100644
--- a/build/sanitizers/tsan_suppressions.cc
+++ b/build/sanitizers/tsan_suppressions.cc
@@ -121,9 +121,6 @@
     // http://crbug.com/927330
     "race:net::(anonymous namespace)::g_network_change_notifier\n"
 
-    // https://crbug.com/965722
-    "race:content::(anonymous namespace)::CorruptDBRequestHandler\n"
-
     // https://crbug.com/977085
     "race:vp3_update_thread_context\n"
 
diff --git a/build/util/BUILD.gn b/build/util/BUILD.gn
index 91d768a..cce503a 100644
--- a/build/util/BUILD.gn
+++ b/build/util/BUILD.gn
@@ -5,18 +5,26 @@
 import("//build/util/lastchange.gni")
 
 action("chromium_git_revision") {
-  script = "lastchange.py"
+  script = "version.py"
+
+  template_file = "chromium_git_revision.h.in"
+  inputs = [
+    lastchange_file,
+    template_file,
+  ]
 
   output_file = "$target_gen_dir/chromium_git_revision.h"
   outputs = [ output_file ]
 
   args = [
-    "--header",
+    # LASTCHANGE contains "<build hash>-<ref>".  The user agent only wants the
+    # "<build hash>" bit, so chop off everything after it.
+    "-e",
+    "LASTCHANGE=LASTCHANGE[:LASTCHANGE.find('-')]",
+    "-f",
+    rebase_path(lastchange_file, root_build_dir),
+    rebase_path(template_file, root_build_dir),
     rebase_path(output_file, root_build_dir),
-    "--revision-id-only",
-    "--revision-id-prefix",
-    "@",
-    "-m CHROMIUM_GIT_REVISION",
   ]
 }
 
diff --git a/build/util/chromium_git_revision.h.in b/build/util/chromium_git_revision.h.in
new file mode 100644
index 0000000..41c1159
--- /dev/null
+++ b/build/util/chromium_git_revision.h.in
@@ -0,0 +1,8 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// chromium_git_revision.h is generated from chromium_git_revision.h.in.  Edit
+// the source!
+
+#define CHROMIUM_GIT_REVISION "@@LASTCHANGE@"
diff --git a/cc/input/threaded_input_handler.cc b/cc/input/threaded_input_handler.cc
index cebc21a..6ed39c4 100644
--- a/cc/input/threaded_input_handler.cc
+++ b/cc/input/threaded_input_handler.cc
@@ -1294,7 +1294,7 @@
                  "LayerImpl::tryScroll: Failed due to no scrolling layer");
     scroll_status.thread = InputHandler::ScrollThread::SCROLL_ON_MAIN_THREAD;
     scroll_status.main_thread_scrolling_reasons =
-        MainThreadScrollingReason::kNonFastScrollableRegion;
+        MainThreadScrollingReason::kNoScrollingLayer;
     return scroll_status;
   }
 
diff --git a/cc/mojo_embedder/async_layer_tree_frame_sink.cc b/cc/mojo_embedder/async_layer_tree_frame_sink.cc
index 3ac47e4..f3f2fc5 100644
--- a/cc/mojo_embedder/async_layer_tree_frame_sink.cc
+++ b/cc/mojo_embedder/async_layer_tree_frame_sink.cc
@@ -199,7 +199,9 @@
                          "Event.Pipeline", TRACE_ID_GLOBAL(trace_id),
                          TRACE_EVENT_FLAG_FLOW_OUT, "step",
                          "SubmitHitTestData");
-  power_mode_voter_.OnFrameProduced();
+
+  power_mode_voter_.OnFrameProduced(frame.render_pass_list.back()->damage_rect,
+                                    frame.device_scale_factor());
 
   compositor_frame_sink_ptr_->SubmitCompositorFrame(
       local_surface_id_, std::move(frame), std::move(hit_test_region_list), 0);
diff --git a/cc/trees/layer_tree_host_impl_unittest.cc b/cc/trees/layer_tree_host_impl_unittest.cc
index 4e30e95..44f0cb5 100644
--- a/cc/trees/layer_tree_host_impl_unittest.cc
+++ b/cc/trees/layer_tree_host_impl_unittest.cc
@@ -3252,7 +3252,7 @@
     EXPECT_FALSE(status.needs_main_thread_hit_test);
   } else {
     EXPECT_EQ(ScrollThread::SCROLL_ON_MAIN_THREAD, status.thread);
-    EXPECT_EQ(MainThreadScrollingReason::kNonFastScrollableRegion,
+    EXPECT_EQ(MainThreadScrollingReason::kNoScrollingLayer,
               status.main_thread_scrolling_reasons);
   }
 }
diff --git a/chrome/android/java/res/layout/incognito_history_placeholder.xml b/chrome/android/java/res/layout/incognito_history_placeholder.xml
index a936f44..05446e4 100644
--- a/chrome/android/java/res/layout/incognito_history_placeholder.xml
+++ b/chrome/android/java/res/layout/incognito_history_placeholder.xml
@@ -8,7 +8,8 @@
     xmlns:tools="http://schemas.android.com/tools"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
-    android:layout_height="match_parent">
+    android:layout_height="match_parent"
+    android:background="@color/default_bg_color">
 
     <LinearLayout
         android:layout_width="match_parent"
@@ -74,7 +75,7 @@
                 android:gravity="center_horizontal"
                 android:textAppearance="@style/TextAppearance.TextMedium"
                 android:text="@string/incognito_history_placeholder_description"
-                app:leading="@dimen/text_size_small_leading" />
+                app:leading="@dimen/text_size_medium_leading" />
 
         </LinearLayout>
     </ScrollView>
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ChromeLocalizationUtils.java b/chrome/android/java/src/org/chromium/chrome/browser/ChromeLocalizationUtils.java
index 2baadefe..f80e7c6 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ChromeLocalizationUtils.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ChromeLocalizationUtils.java
@@ -15,8 +15,8 @@
 import org.chromium.base.LocaleUtils;
 import org.chromium.base.metrics.RecordHistogram;
 import org.chromium.chrome.R;
+import org.chromium.chrome.browser.language.AppLocaleUtils;
 import org.chromium.chrome.browser.language.GlobalAppLocaleController;
-import org.chromium.chrome.browser.language.settings.LanguageItem;
 import org.chromium.ui.base.LocalizationUtils;
 
 import java.lang.annotation.Retention;
@@ -94,11 +94,11 @@
             topAndroidLanguage =
                     LocaleUtils.toLanguage(LocaleList.getDefault().get(0).toLanguageTag());
         }
-        boolean isDefaultLanguageAvailable = LanguageItem.isAvailableUiLanguage(defaultLanguage);
+        boolean isDefaultLanguageAvailable = AppLocaleUtils.isSupportedUiLanguage(defaultLanguage);
         boolean isTopAndroidLanguageAvailable =
-                LanguageItem.isAvailableUiLanguage(topAndroidLanguage);
+                AppLocaleUtils.isSupportedUiLanguage(topAndroidLanguage);
 
-        // The java and native UI languages can be different if the native language plack is not
+        // The java and native UI languages can be different if the native language pack is not
         // correctly installed through the Play Store.
         String javaUiLanguage = LocaleUtils.toLanguage(getJavaUiLocale());
         String nativeUiLanguage = LocaleUtils.toLanguage(LocalizationUtils.getNativeUiLocale());
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 510c842..ef93f93 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ChromeTabbedActivity.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ChromeTabbedActivity.java
@@ -1548,9 +1548,7 @@
             setTrackColdStartupMetrics(true);
         }
 
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
-            mJankTracker = new JankTracker(this);
-        }
+        mJankTracker = new JankTracker(this);
 
         supportRequestWindowFeature(Window.FEATURE_ACTION_MODE_OVERLAY);
 
@@ -2331,7 +2329,7 @@
             mStartupPaintPreviewHelperSupplier.destroy();
         }
 
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && mJankTracker != null) {
+        if (mJankTracker != null) {
             mJankTracker.destroy();
             mJankTracker = null;
         }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/history/HistoryManager.java b/chrome/android/java/src/org/chromium/chrome/browser/history/HistoryManager.java
index 942ece5..037f099 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/history/HistoryManager.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/history/HistoryManager.java
@@ -13,6 +13,7 @@
 import android.provider.Browser;
 import android.view.LayoutInflater;
 import android.view.MenuItem;
+import android.view.View;
 import android.view.ViewGroup;
 import android.widget.ImageButton;
 import android.widget.TextView;
@@ -318,7 +319,13 @@
                 R.layout.incognito_history_placeholder, null);
         ImageButton dismissButton =
                 placeholderView.findViewById(R.id.close_history_placeholder_button);
-        dismissButton.setOnClickListener(v -> mActivity.finish());
+        if (mIsSeparateActivity) {
+            dismissButton.setOnClickListener(v -> mActivity.finish());
+        } else {
+            dismissButton.setVisibility(View.GONE);
+        }
+        placeholderView.setFocusable(true);
+        placeholderView.setFocusableInTouchMode(true);
         return placeholderView;
     }
 
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ntp/search/SearchBoxMediator.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/search/SearchBoxMediator.java
index d539363..7dd92dc 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ntp/search/SearchBoxMediator.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/search/SearchBoxMediator.java
@@ -109,7 +109,7 @@
         final @ColorInt int primaryColor = ChromeColors.getDefaultThemeColor(
                 mContext.getResources(), false /* forceDarkBgColor= */);
         ColorStateList colorStateList =
-                mAssistantVoiceSearchService.getMicButtonColorStateList(primaryColor, mContext);
+                mAssistantVoiceSearchService.getButtonColorStateList(primaryColor, mContext);
         mModel.set(SearchBoxProperties.VOICE_SEARCH_COLOR_STATE_LIST, colorStateList);
     }
 
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ntp/search/SearchBoxViewBinder.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/search/SearchBoxViewBinder.java
index 273451f..0f0fbd9 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ntp/search/SearchBoxViewBinder.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/search/SearchBoxViewBinder.java
@@ -47,6 +47,8 @@
         } else if (SearchBoxProperties.VOICE_SEARCH_COLOR_STATE_LIST == propertyKey) {
             ApiCompatibilityUtils.setImageTintList(voiceSearchButton,
                     model.get(SearchBoxProperties.VOICE_SEARCH_COLOR_STATE_LIST));
+            ApiCompatibilityUtils.setImageTintList(
+                    lensButton, model.get(SearchBoxProperties.VOICE_SEARCH_COLOR_STATE_LIST));
         } else if (SearchBoxProperties.VOICE_SEARCH_DRAWABLE == propertyKey) {
             voiceSearchButton.setImageDrawable(
                     model.get(SearchBoxProperties.VOICE_SEARCH_DRAWABLE));
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/omnibox/LocationBarLayout.java b/chrome/android/java/src/org/chromium/chrome/browser/omnibox/LocationBarLayout.java
index 8bf7584..57312a9 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/omnibox/LocationBarLayout.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/omnibox/LocationBarLayout.java
@@ -153,6 +153,10 @@
         ApiCompatibilityUtils.setImageTintList(mDeleteButton, colorStateList);
     }
 
+    /* package */ void setLensButtonTint(ColorStateList colorStateList) {
+        ApiCompatibilityUtils.setImageTintList(mLensButton, colorStateList);
+    }
+
     /**
      * Override the default LocationBarDataProvider in tests. Production code should use the
      * {@link #initialize} method instead.
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/omnibox/LocationBarMediator.java b/chrome/android/java/src/org/chromium/chrome/browser/omnibox/LocationBarMediator.java
index 1fade89..1f09a396 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/omnibox/LocationBarMediator.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/omnibox/LocationBarMediator.java
@@ -884,12 +884,22 @@
                 mAssistantVoiceSearchServiceSupplier.get();
         if (assistantVoiceSearchService == null) return;
 
-        mLocationBarLayout.setMicButtonTint(assistantVoiceSearchService.getMicButtonColorStateList(
+        mLocationBarLayout.setMicButtonTint(assistantVoiceSearchService.getButtonColorStateList(
                 getPrimaryBackgroundColor(), mContext));
         mLocationBarLayout.setMicButtonDrawable(
                 assistantVoiceSearchService.getCurrentMicDrawable());
     }
 
+    @VisibleForTesting
+    /* package */ void updateLensButtonColors() {
+        AssistantVoiceSearchService assistantVoiceSearchService =
+                mAssistantVoiceSearchServiceSupplier.get();
+        if (assistantVoiceSearchService == null) return;
+
+        mLocationBarLayout.setLensButtonTint(assistantVoiceSearchService.getButtonColorStateList(
+                getPrimaryBackgroundColor(), mContext));
+    }
+
     /**
      * Update visuals to use a correct light or dark color scheme depending on the primary color.
      */
@@ -1108,6 +1118,7 @@
     @Override
     public void onPrimaryColorChanged() {
         updateAssistantVoiceSearchDrawableAndColors();
+        updateLensButtonColors();
         updateUseDarkColors();
     }
 
@@ -1214,6 +1225,7 @@
     @Override
     public void onAssistantVoiceSearchServiceChanged() {
         updateAssistantVoiceSearchDrawableAndColors();
+        updateLensButtonColors();
     }
 
     // VoiceRecognitionHandler.Delegate implementation.
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/omnibox/voice/AssistantVoiceSearchService.java b/chrome/android/java/src/org/chromium/chrome/browser/omnibox/voice/AssistantVoiceSearchService.java
index 213cfe64..2bf67b5 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/omnibox/voice/AssistantVoiceSearchService.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/omnibox/voice/AssistantVoiceSearchService.java
@@ -124,7 +124,7 @@
     private boolean mIsDefaultSearchEngineGoogle;
     private boolean mIsAssistantVoiceSearchEnabled;
     private boolean mIsColorfulMicEnabled;
-    private boolean mShouldShowColorfulMic;
+    private boolean mShouldShowColorfulButtons;
     private boolean mIsMultiAccountCheckEnabled;
     private String mMinAgsaVersion;
 
@@ -179,7 +179,7 @@
 
         mIsDefaultSearchEngineGoogle = mTemplateUrlService.isDefaultSearchEngineGoogle();
 
-        mShouldShowColorfulMic = isColorfulMicEnabled();
+        mShouldShowColorfulButtons = isColorfulMicEnabled();
     }
 
     /** @return Whether the user has had a chance to enable the feature. */
@@ -213,14 +213,14 @@
      *         exist or reuse the existing Drawable's ConstantState if it does already exist.
      **/
     public Drawable getCurrentMicDrawable() {
-        return AppCompatResources.getDrawable(
-                mContext, mShouldShowColorfulMic ? R.drawable.ic_colorful_mic : R.drawable.btn_mic);
+        return AppCompatResources.getDrawable(mContext,
+                mShouldShowColorfulButtons ? R.drawable.ic_colorful_mic : R.drawable.btn_mic);
     }
 
     /** @return The correct ColorStateList for the current theme. */
-    public @Nullable ColorStateList getMicButtonColorStateList(
+    public @Nullable ColorStateList getButtonColorStateList(
             @ColorInt int primaryColor, Context context) {
-        if (mShouldShowColorfulMic) return null;
+        if (mShouldShowColorfulButtons) return null;
 
         final boolean useLightColors =
                 ColorUtils.shouldUseLightForegroundOnBackground(primaryColor);
@@ -403,11 +403,11 @@
         new AsyncTask<Boolean>() {
             @Override
             protected Boolean doInBackground() {
-                return mShouldShowColorfulMic != shouldShowColorfulMic;
+                return mShouldShowColorfulButtons != shouldShowColorfulMic;
             }
             @Override
             protected void onPostExecute(Boolean notify) {
-                mShouldShowColorfulMic = shouldShowColorfulMic;
+                mShouldShowColorfulButtons = shouldShowColorfulMic;
                 if (notify) notifyObserver();
             }
         }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
@@ -430,7 +430,7 @@
         if (mIsDefaultSearchEngineGoogle == searchEngineGoogle) return;
 
         mIsDefaultSearchEngineGoogle = searchEngineGoogle;
-        mShouldShowColorfulMic = isColorfulMicEnabled();
+        mShouldShowColorfulButtons = isColorfulMicEnabled();
         notifyObserver();
     }
 
@@ -453,7 +453,7 @@
     /** Enable the colorful mic for testing purposes. */
     void setColorfulMicEnabledForTesting(boolean enabled) {
         mIsColorfulMicEnabled = enabled;
-        mShouldShowColorfulMic = enabled;
+        mShouldShowColorfulButtons = enabled;
     }
 
     void setMultiAccountCheckEnabledForTesting(boolean enabled) {
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/CustomTabActivityTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/CustomTabActivityTest.java
index 2223bcd9..06e84b8 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/CustomTabActivityTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/CustomTabActivityTest.java
@@ -10,6 +10,8 @@
 import static org.hamcrest.Matchers.not;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
 import static org.chromium.base.test.util.Restriction.RESTRICTION_TYPE_NON_LOW_END_DEVICE;
@@ -193,6 +195,20 @@
     private TestWebServer mWebServer;
     private CustomTabsConnection mConnectionToCleanup;
 
+    private class CustomTabsExtraCallbackHelper<T> extends CallbackHelper {
+        private T mValue;
+
+        public T getValue() {
+            assert getCallCount() > 0;
+            return mValue;
+        }
+
+        public void notifyCalled(T value) {
+            mValue = value;
+            notifyCalled();
+        }
+    }
+
     @Rule
     public final ScreenShooter mScreenShooter = new ScreenShooter();
 
@@ -468,8 +484,7 @@
         // The context menu for images should not be built when the first run is not completed.
         ContextMenuCoordinator imageMenu = ContextMenuUtils.openContextMenu(
                 mCustomTabActivityTestRule.getActivity().getActivityTab(), "logo");
-        Assert.assertNull(
-                "Context menu for images should not be built when first run is not finished.",
+        assertNull("Context menu for images should not be built when first run is not finished.",
                 imageMenu);
 
         // Options on the context menu for links should be limited when the first run is not
@@ -696,7 +711,7 @@
 
         onFinished1.waitForCallback("Pending Intent was not sent.");
         Assert.assertThat(onFinished1.getCallbackIntent().getDataString(), equalTo(mTestPage));
-        Assert.assertNull(onFinished2.getCallbackIntent());
+        assertNull(onFinished2.getCallbackIntent());
 
         CustomTabsConnection connection = CustomTabsConnection.getInstance();
         int id = toolbarItems.get(0).getInt(CustomTabsIntent.KEY_ID);
@@ -731,7 +746,7 @@
         CustomTabToolbar toolbar = (CustomTabToolbar) toolbarView;
         final ImageButton actionButton = toolbar.getCustomActionButtonForTest(0);
 
-        Assert.assertNull("Action button should not be shown", actionButton);
+        assertNull("Action button should not be shown", actionButton);
 
         BrowserServicesIntentDataProvider dataProvider = getActivity().getIntentDataProvider();
         Assert.assertThat(dataProvider.getCustomButtonsOnToolbar(), is(empty()));
@@ -991,7 +1006,7 @@
      * Tests that page load metrics are sent.
      */
     @Test
-    @SmallTest
+    @MediumTest
     public void testPageLoadMetricsAreSent() throws Exception {
         checkPageLoadMetrics(true);
     }
@@ -1000,7 +1015,7 @@
      * Tests that page load metrics are not sent when the client is not allowlisted.
      */
     @Test
-    @SmallTest
+    @MediumTest
     public void testPageLoadMetricsAreNotSentByDefault() throws Exception {
         checkPageLoadMetrics(false);
     }
@@ -1197,7 +1212,7 @@
     public void testWarmupAndLaunchRightToolbarLayout() throws Exception {
         CustomTabsTestUtils.warmUpAndWait();
         mCustomTabActivityTestRule.startActivityCompletely(createMinimalCustomTabIntent());
-        Assert.assertNull("Should not have a tab switcher button.",
+        assertNull("Should not have a tab switcher button.",
                 getActivity().findViewById(R.id.tab_switcher_button));
     }
 
@@ -1863,7 +1878,7 @@
         PostTask.postTask(UiThreadTaskTraits.DEFAULT, () -> {
             getActivity().getComponent().resolveNavigationController()
                     .openCurrentUrlInBrowser(true);
-            Assert.assertNull(getActivity().getActivityTab());
+            assertNull(getActivity().getActivityTab());
         });
         // Use the extended CriteriaHelper timeout to make sure we get an activity
         final Activity lastActivity =
@@ -1895,12 +1910,18 @@
     }
 
     private void checkPageLoadMetrics(boolean allowMetrics) throws TimeoutException {
-        final AtomicReference<Long> firstContentfulPaintMs = new AtomicReference<>(-1L);
-        final AtomicReference<Long> largestContentfulPaintMs = new AtomicReference<>(-1L);
+        CustomTabsExtraCallbackHelper<Long> firstContentfulPaintCallback =
+                new CustomTabsExtraCallbackHelper<>();
+        CustomTabsExtraCallbackHelper<Long> largestContentfulPaintCallback =
+                new CustomTabsExtraCallbackHelper<>();
+        CustomTabsExtraCallbackHelper<Long> loadEventStartCallback =
+                new CustomTabsExtraCallbackHelper<>();
+        CustomTabsExtraCallbackHelper<Float> layoutShiftScoreCallback =
+                new CustomTabsExtraCallbackHelper<>();
+        CustomTabsExtraCallbackHelper<Boolean> sawNetworkQualityEstimatesCallback =
+                new CustomTabsExtraCallbackHelper<>();
+
         final AtomicReference<Long> activityStartTimeMs = new AtomicReference<>(-1L);
-        final AtomicReference<Long> loadEventStartMs = new AtomicReference<>(-1L);
-        final AtomicReference<Float> layoutShiftScore = new AtomicReference<>(-1f);
-        final AtomicReference<Boolean> sawNetworkQualityEstimates = new AtomicReference<>(false);
 
         CustomTabsCallback cb = new CustomTabsCallback() {
             @Override
@@ -1913,13 +1934,13 @@
                     assertEquals(CustomTabsConnection.PAGE_LOAD_METRICS_CALLBACK, callbackName);
                 }
                 if (-1 != args.getLong(PageLoadMetrics.EFFECTIVE_CONNECTION_TYPE, -1)) {
-                    sawNetworkQualityEstimates.set(true);
+                    sawNetworkQualityEstimatesCallback.notifyCalled(true);
                 }
 
                 float layoutShiftScoreValue =
                         args.getFloat(PageLoadMetrics.LAYOUT_SHIFT_SCORE, -1f);
                 if (layoutShiftScoreValue >= 0f) {
-                    layoutShiftScore.set(layoutShiftScoreValue);
+                    layoutShiftScoreCallback.notifyCalled(layoutShiftScoreValue);
                 }
 
                 long navigationStart = args.getLong(PageLoadMetrics.NAVIGATION_START, -1);
@@ -1935,20 +1956,20 @@
                         args.getLong(PageLoadMetrics.FIRST_CONTENTFUL_PAINT, -1);
                 if (firstContentfulPaint > 0) {
                     Assert.assertTrue(firstContentfulPaint <= (current - navigationStart));
-                    firstContentfulPaintMs.set(firstContentfulPaint);
+                    firstContentfulPaintCallback.notifyCalled(firstContentfulPaint);
                 }
 
                 long largestContentfulPaint =
                         args.getLong(PageLoadMetrics.LARGEST_CONTENTFUL_PAINT, -1);
                 if (largestContentfulPaint > 0) {
                     Assert.assertTrue(largestContentfulPaint <= (current - navigationStart));
-                    largestContentfulPaintMs.set(largestContentfulPaint);
+                    largestContentfulPaintCallback.notifyCalled(largestContentfulPaint);
                 }
 
                 long loadEventStart = args.getLong(PageLoadMetrics.LOAD_EVENT_START, -1);
                 if (loadEventStart > 0) {
                     Assert.assertTrue(loadEventStart <= (current - navigationStart));
-                    loadEventStartMs.set(loadEventStart);
+                    loadEventStartCallback.notifyCalled(loadEventStart);
                 }
             }
         };
@@ -1970,23 +1991,30 @@
         activityStartTimeMs.set(SystemClock.uptimeMillis());
         mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);
         if (allowMetrics) {
-            CriteriaHelper.pollInstrumentationThread(() -> firstContentfulPaintMs.get() > 0);
-            CriteriaHelper.pollInstrumentationThread(() -> loadEventStartMs.get() > 0);
-            CriteriaHelper.pollInstrumentationThread(() -> sawNetworkQualityEstimates.get());
+            firstContentfulPaintCallback.waitForCallback(0);
+            loadEventStartCallback.waitForCallback(0);
+            sawNetworkQualityEstimatesCallback.waitForCallback(0);
+
+            assertTrue(firstContentfulPaintCallback.getValue() > 0);
+            assertTrue(firstContentfulPaintCallback.getValue() > 0);
+            assertTrue(sawNetworkQualityEstimatesCallback.getValue());
         } else {
             try {
-                CriteriaHelper.pollInstrumentationThread(() -> firstContentfulPaintMs.get() > 0);
-            } catch (AssertionError e) {
+                firstContentfulPaintCallback.waitForCallback(0);
+                assertTrue(firstContentfulPaintCallback.getValue() > 0);
+            } catch (TimeoutException e) {
                 // Expected.
             }
-            assertEquals(-1L, (long) firstContentfulPaintMs.get());
+            assertEquals(0, firstContentfulPaintCallback.getCallCount());
 
             try {
-                CriteriaHelper.pollInstrumentationThread(() -> largestContentfulPaintMs.get() > 0);
-            } catch (AssertionError e) {
+                largestContentfulPaintCallback.waitForCallback(0);
+                assertTrue(largestContentfulPaintCallback.getValue() > 0);
+            } catch (TimeoutException e) {
                 // Expected.
             }
-            assertEquals(-1L, (long) largestContentfulPaintMs.get());
+
+            assertEquals(0, largestContentfulPaintCallback.getCallCount());
         }
 
         // Navigate to a new page, as metrics like LCP are only reported at the end of the page load
@@ -1997,8 +2025,11 @@
         });
 
         if (allowMetrics) {
-            CriteriaHelper.pollInstrumentationThread(() -> largestContentfulPaintMs.get() > 0);
-            CriteriaHelper.pollInstrumentationThread(() -> layoutShiftScore.get() != -1f);
+            largestContentfulPaintCallback.waitForCallback(0);
+            assertTrue((long) largestContentfulPaintCallback.getValue() > 0);
+
+            layoutShiftScoreCallback.waitForCallback(0);
+            assertNotNull(layoutShiftScoreCallback.getValue());
         }
     }
 
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/site_settings/WebsitePermissionsFetcherTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/site_settings/WebsitePermissionsFetcherTest.java
index 81dc8ff..ddb3960 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/site_settings/WebsitePermissionsFetcherTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/site_settings/WebsitePermissionsFetcherTest.java
@@ -493,7 +493,7 @@
         // If the ContentSettingsType.NUM_TYPES value changes *and* a new value has been exposed on
         // Android, then please update this code block to include a test for your new type.
         // Otherwise, just update count in the assert.
-        Assert.assertEquals(70, ContentSettingsType.NUM_TYPES);
+        Assert.assertEquals(71, ContentSettingsType.NUM_TYPES);
         websitePreferenceBridge.addContentSettingException(
                 new ContentSettingException(ContentSettingsType.COOKIES, googleOrigin,
                         ContentSettingValues.DEFAULT, preferenceSource));
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/vr/WebXrVrInputTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/vr/WebXrVrInputTest.java
index 156bcc6b..8b8701d 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/vr/WebXrVrInputTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/vr/WebXrVrInputTest.java
@@ -277,7 +277,8 @@
     }
 
     /**
-     * Tests that screen touches are registered as XR input when the viewer is Cardboard.
+     * Tests that screen touches are registered as XR input in immersive sessions,
+     *  when the viewer is Cardboard.
      */
     @Test
     @MediumTest
@@ -288,31 +289,20 @@
         mWebXrVrTestFramework.loadFileAndAwaitInitialization(
                 "test_webxr_input", PAGE_LOAD_TIMEOUT_S);
         mWebXrVrTestFramework.enterSessionWithUserGestureOrFail();
-        // Make it so that the webpage doesn't try to finish the JavaScript step after each input
-        // since we don't need to ack each one like with the Daydream controller.
-        mWebXrVrTestFramework.runJavaScriptOrFail(
-                "finishAfterEachInput = false", POLL_TIMEOUT_SHORT_MS);
-        int numIterations = 10;
-        mWebXrVrTestFramework.runJavaScriptOrFail(
-                "stepSetupListeners(" + String.valueOf(numIterations) + ")", POLL_TIMEOUT_SHORT_MS);
 
-        int x = mWebXrVrTestFramework.getCurrentContentView().getWidth() / 2;
-        int y = mWebXrVrTestFramework.getCurrentContentView().getHeight() / 2;
         // TODO(mthiesse, https://crbug.com/758374): Injecting touch events into the root GvrLayout
         // (VrShell) is flaky. Sometimes the events just don't get routed to the presentation
         // view for no apparent reason. We should figure out why this is and see if it's fixable.
-        final View presentationView =
-                TestVrShellDelegate.getVrShellForTesting().getPresentationViewForTesting();
-
-        // Tap the screen a bunch of times and make sure that they're all registered.
-        spamScreenTaps(presentationView, x, y, numIterations);
-
-        mWebXrVrTestFramework.waitOnJavaScriptStep();
-        mWebXrVrTestFramework.endTest();
+        // Note that we only use 5 iterations, vs the 10 used in the inline test, because it seems
+        // that on M, we get enough memory pressure that the yet-to-be-processed taps get GC'd.
+        // See https://crbug.com/1194906 for more details.
+        testScreenTapsRegisteredOnCardboardImpl(
+                TestVrShellDelegate.getVrShellForTesting().getPresentationViewForTesting(), 5);
     }
 
     /**
-     * Tests that screen touches are registered as transient XR input when the viewer is Cardboard.
+     * Tests that screen touches are registered as transient XR input in inline sessions,
+     * when the viewer is Cardboard.
      */
     @Test
     @MediumTest
@@ -322,17 +312,22 @@
     public void testTransientScreenTapsRegisteredOnCardboard_WebXr() {
         mWebXrVrTestFramework.loadFileAndAwaitInitialization(
                 "test_webxr_transient_input", PAGE_LOAD_TIMEOUT_S);
+
+        testScreenTapsRegisteredOnCardboardImpl(mWebXrVrTestFramework.getCurrentContentView(), 10);
+    }
+
+    // Note that the page should load the appropriate test page and enter any relevant session
+    // before calling this.
+    private void testScreenTapsRegisteredOnCardboardImpl(View presentationView, int numIterations) {
         // Make it so that the webpage doesn't try to finish the JavaScript step after each input
         // since we don't need to ack each one like with the Daydream controller.
         mWebXrVrTestFramework.runJavaScriptOrFail(
                 "finishAfterEachInput = false", POLL_TIMEOUT_SHORT_MS);
-        int numIterations = 10;
         mWebXrVrTestFramework.runJavaScriptOrFail(
                 "stepSetupListeners(" + String.valueOf(numIterations) + ")", POLL_TIMEOUT_SHORT_MS);
 
-        int x = mWebXrVrTestFramework.getCurrentContentView().getWidth() / 2;
-        int y = mWebXrVrTestFramework.getCurrentContentView().getHeight() / 2;
-        final View presentationView = mWebXrVrTestFramework.getCurrentContentView();
+        int x = presentationView.getWidth() / 2;
+        int y = presentationView.getHeight() / 2;
 
         // Tap the screen a bunch of times and make sure that they're all registered.
         spamScreenTaps(presentationView, x, y, numIterations);
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/omnibox/LocationBarMediatorTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/omnibox/LocationBarMediatorTest.java
index 8e324fe..108a9f2 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/omnibox/LocationBarMediatorTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/omnibox/LocationBarMediatorTest.java
@@ -620,13 +620,23 @@
     public void testUpdateAssistantVoiceSearchDrawablesAndColors() {
         AssistantVoiceSearchService avs = Mockito.mock(AssistantVoiceSearchService.class);
         ColorStateList csl = Mockito.mock(ColorStateList.class);
-        doReturn(csl).when(avs).getMicButtonColorStateList(anyInt(), anyObject());
+        doReturn(csl).when(avs).getButtonColorStateList(anyInt(), anyObject());
         mMediator.setAssistantVoiceSearchServiceForTesting(avs);
 
         verify(mLocationBarLayout).setMicButtonTint(csl);
     }
 
     @Test
+    public void testUpdateLensButtonColors() {
+        AssistantVoiceSearchService avs = Mockito.mock(AssistantVoiceSearchService.class);
+        ColorStateList csl = Mockito.mock(ColorStateList.class);
+        doReturn(csl).when(avs).getButtonColorStateList(anyInt(), anyObject());
+        mMediator.setAssistantVoiceSearchServiceForTesting(avs);
+
+        verify(mLocationBarLayout).setLensButtonTint(csl);
+    }
+
+    @Test
     public void testUpdateAssistantVoiceSearchDrawablesAndColors_serviceNull() {
         mMediator.updateAssistantVoiceSearchDrawableAndColors();
         // If the service is null, the update method bails out.
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/omnibox/voice/AssistantVoiceSearchServiceUnitTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/omnibox/voice/AssistantVoiceSearchServiceUnitTest.java
index 6767eb7..fd5c504e 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/omnibox/voice/AssistantVoiceSearchServiceUnitTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/omnibox/voice/AssistantVoiceSearchServiceUnitTest.java
@@ -284,13 +284,13 @@
         mAssistantVoiceSearchService.onAccountsChanged();
 
         // Colorful mic should be returned when only 1 account is present.
-        Assert.assertNull(mAssistantVoiceSearchService.getMicButtonColorStateList(0, mContext));
+        Assert.assertNull(mAssistantVoiceSearchService.getButtonColorStateList(0, mContext));
 
         // Adding new account would trigger onAccountsChanged() automatically
         mAccountManagerTestRule.addAccount(TEST_ACCOUNT_EMAIL2);
 
         // Colorful mic should be returned when only 1 account is present.
-        Assert.assertNotNull(mAssistantVoiceSearchService.getMicButtonColorStateList(0, mContext));
+        Assert.assertNotNull(mAssistantVoiceSearchService.getButtonColorStateList(0, mContext));
     }
 
     @Test
@@ -308,9 +308,9 @@
 
     @Test
     @Feature("OmniboxAssistantVoiceSearch")
-    public void getMicButtonColorStateList_ColorfulMicEnabled() {
+    public void getButtonColorStateList_ColorfulMicEnabled() {
         mAssistantVoiceSearchService.setColorfulMicEnabledForTesting(true);
-        Assert.assertNull(mAssistantVoiceSearchService.getMicButtonColorStateList(0, mContext));
+        Assert.assertNull(mAssistantVoiceSearchService.getButtonColorStateList(0, mContext));
     }
 
     @Test
diff --git a/chrome/app/generated_resources.grd b/chrome/app/generated_resources.grd
index 02bf7af..287316c 100644
--- a/chrome/app/generated_resources.grd
+++ b/chrome/app/generated_resources.grd
@@ -9997,10 +9997,10 @@
         Share an application window
       </message>
       <message name="IDS_DESKTOP_MEDIA_PICKER_TEXT" desc="Text for the window picker dialog shown when desktop capture is requested by an app to be used by the app itself.">
-        The extension "<ph name="APP_NAME">$1<ex>Google Hangouts</ex></ph>" wants to share the contents of your screen.
+        <ph name="APP_NAME">$1<ex>Google Hangouts</ex></ph> wants to share the contents of your screen.
       </message>
       <message name="IDS_DESKTOP_MEDIA_PICKER_TEXT_DELEGATED" desc="Text for the window picker dialog shown when desktop capture is requested by an app to be used by a tab.">
-        The extension "<ph name="APP_NAME">$1<ex>Google Hangouts</ex></ph>" wants to share the contents of your screen with <ph name="TARGET_NAME">$2<ex>https://google.com</ex></ph>.
+        <ph name="APP_NAME">$1<ex>Google Hangouts</ex></ph> wants to share the contents of your screen with <ph name="TARGET_NAME">$2<ex>https://google.com</ex></ph>.
       </message>
       <message name="IDS_DESKTOP_MEDIA_PICKER_AUDIO_SHARE" desc="Text for the checkbox on the window picker dialog, when checked, audio will be shared, otherwise just video sharing">
         Share audio
@@ -11427,6 +11427,13 @@
       Enabled – <ph name="VARIATION_NAME">$1<ex>tabs shrink to pinned tab width</ex></ph>
     </message>
 
+    <!-- ChromeLabs Side Panel-->
+    <message name="IDS_SIDE_PANEL_EXPERIMENT_NAME" desc="Name for Side Panel experiment">
+      Side Panel
+    </message>
+    <message name="IDS_SIDE_PANEL_EXPERIMENT_DESCRIPTION" desc="Description for Side Panel experiment">
+      Enables a browser-level side panel for a useful and persistent way to access your Reading List and Bookmarks.
+    </message>
     <!-- ChromeLabs Read Later-->
     <message name="IDS_READ_LATER_EXPERIMENT_NAME" desc="Name for Read Later experiment">
       Reading List
diff --git a/chrome/app/generated_resources_grd/IDS_DESKTOP_MEDIA_PICKER_TEXT.png.sha1 b/chrome/app/generated_resources_grd/IDS_DESKTOP_MEDIA_PICKER_TEXT.png.sha1
index b8b00cc..c0903f2 100644
--- a/chrome/app/generated_resources_grd/IDS_DESKTOP_MEDIA_PICKER_TEXT.png.sha1
+++ b/chrome/app/generated_resources_grd/IDS_DESKTOP_MEDIA_PICKER_TEXT.png.sha1
@@ -1 +1 @@
-565f7a699ad9089bf0d14c933920dbc07006c538
\ No newline at end of file
+bee55c467a7ed6721049da2d5170b28594914ec3
\ No newline at end of file
diff --git a/chrome/app/generated_resources_grd/IDS_DESKTOP_MEDIA_PICKER_TEXT_DELEGATED.png.sha1 b/chrome/app/generated_resources_grd/IDS_DESKTOP_MEDIA_PICKER_TEXT_DELEGATED.png.sha1
index b8b00cc..c0903f2 100644
--- a/chrome/app/generated_resources_grd/IDS_DESKTOP_MEDIA_PICKER_TEXT_DELEGATED.png.sha1
+++ b/chrome/app/generated_resources_grd/IDS_DESKTOP_MEDIA_PICKER_TEXT_DELEGATED.png.sha1
@@ -1 +1 @@
-565f7a699ad9089bf0d14c933920dbc07006c538
\ No newline at end of file
+bee55c467a7ed6721049da2d5170b28594914ec3
\ No newline at end of file
diff --git a/chrome/app/generated_resources_grd/IDS_SIDE_PANEL_EXPERIMENT_DESCRIPTION.png.sha1 b/chrome/app/generated_resources_grd/IDS_SIDE_PANEL_EXPERIMENT_DESCRIPTION.png.sha1
new file mode 100644
index 0000000..8d89aa71
--- /dev/null
+++ b/chrome/app/generated_resources_grd/IDS_SIDE_PANEL_EXPERIMENT_DESCRIPTION.png.sha1
@@ -0,0 +1 @@
+f3bf7d631ab665453b4b1a0ab1847e7c3b586a8a
\ No newline at end of file
diff --git a/chrome/app/generated_resources_grd/IDS_SIDE_PANEL_EXPERIMENT_NAME.png.sha1 b/chrome/app/generated_resources_grd/IDS_SIDE_PANEL_EXPERIMENT_NAME.png.sha1
new file mode 100644
index 0000000..8d89aa71
--- /dev/null
+++ b/chrome/app/generated_resources_grd/IDS_SIDE_PANEL_EXPERIMENT_NAME.png.sha1
@@ -0,0 +1 @@
+f3bf7d631ab665453b4b1a0ab1847e7c3b586a8a
\ No newline at end of file
diff --git a/chrome/app/os_settings_strings.grdp b/chrome/app/os_settings_strings.grdp
index c7680f7b..5fbea96 100644
--- a/chrome/app/os_settings_strings.grdp
+++ b/chrome/app/os_settings_strings.grdp
@@ -3654,6 +3654,9 @@
   <message name="IDS_SETTINGS_APPS_LINK_TEXT" desc="The label for the button which links to the App Management page.">
     Manage your apps
   </message>
+  <message name="IDS_SETTINGS_APP_NOTIFICATIONS_LINK_TEXT" desc="The label for the settings row button in the App section of OS Settings which links to the App Notifications page.">
+    Notifications
+  </message>
   <message name="IDS_SETTINGS_ANDROID_APPS_TITLE" desc="The title of Google Play Store (Arc++ / Android Apps) section.">
     Google Play Store
   </message>
diff --git a/chrome/app/os_settings_strings_grdp/IDS_SETTINGS_APP_NOTIFICATIONS_LINK_TEXT.png.sha1 b/chrome/app/os_settings_strings_grdp/IDS_SETTINGS_APP_NOTIFICATIONS_LINK_TEXT.png.sha1
new file mode 100644
index 0000000..7b0ee14
--- /dev/null
+++ b/chrome/app/os_settings_strings_grdp/IDS_SETTINGS_APP_NOTIFICATIONS_LINK_TEXT.png.sha1
@@ -0,0 +1 @@
+d164d32a3b67c116a6da61ae886a9c436d6f4b63
\ No newline at end of file
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
index 07c27d2..791e073 100644
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -3483,6 +3483,8 @@
       "cart/cart_service_factory.h",
       "cart/commerce_hint_service.cc",
       "cart/commerce_hint_service.h",
+      "cart/discount_url_loader.cc",
+      "cart/discount_url_loader.h",
       "cart/fetch_discount_worker.cc",
       "cart/fetch_discount_worker.h",
       "certificate_viewer.h",
diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
index 3811eaf..946e1dc 100644
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -4566,7 +4566,7 @@
      flag_descriptions::kScrollableTabStripButtonsDescription, kOsDesktop,
      FEATURE_VALUE_TYPE(features::kScrollableTabStripButtons)},
 
-    {"side-panel", flag_descriptions::kSidePanelName,
+    {flag_descriptions::kSidePanelFlagId, flag_descriptions::kSidePanelName,
      flag_descriptions::kSidePanelDescription, kOsDesktop,
      FEATURE_VALUE_TYPE(features::kSidePanel)},
 
@@ -7258,6 +7258,12 @@
      flag_descriptions::kSyncTrustedVaultPassphraseRecoveryDescription, kOsAll,
      FEATURE_VALUE_TYPE(::switches::kSyncTrustedVaultPassphraseRecovery)},
 
+    {"debug-history-intervention-no-user-activation",
+     flag_descriptions::kDebugHistoryInterventionNoUserActivationName,
+     flag_descriptions::kDebugHistoryInterventionNoUserActivationDescription,
+     kOsAll,
+     FEATURE_VALUE_TYPE(features::kDebugHistoryInterventionNoUserActivation)},
+
     // NOTE: Adding a new flag requires adding a corresponding entry to enum
     // "LoginCustomFlags" in tools/metrics/histograms/enums.xml. See "Flag
     // Histograms" in tools/metrics/histograms/README.md (run the
diff --git a/chrome/browser/apps/app_service/app_launch_params.h b/chrome/browser/apps/app_service/app_launch_params.h
index b2bd3cc..9b0052c 100644
--- a/chrome/browser/apps/app_service/app_launch_params.h
+++ b/chrome/browser/apps/app_service/app_launch_params.h
@@ -102,6 +102,10 @@
   // should translate and then launch the PWA to. Null when it's not a protocol
   // handler launch.
   absl::optional<GURL> protocol_handler_launch_url;
+
+  // Whether or not to have the resulting Browser be omitted from session
+  // restore.
+  bool omit_from_session_restore = false;
 };
 
 }  // namespace apps
diff --git a/chrome/browser/ash/crostini/crostini_terminal.cc b/chrome/browser/ash/crostini/crostini_terminal.cc
index 2d97b3d..bb5d518 100644
--- a/chrome/browser/ash/crostini/crostini_terminal.cc
+++ b/chrome/browser/ash/crostini/crostini_terminal.cc
@@ -96,6 +96,11 @@
     return;
   }
 
+  // Do not track Crostini apps or terminal in session restore. Apps will fail
+  // since VMs are not restarted on restore, and we don't want terminal to
+  // force the VM to start.
+  params->omit_from_session_restore = true;
+
   // This LaunchSystemWebAppImpl call is necessary. Terminal App uses its own
   // CrostiniApps publisher for launching. Calling LaunchSystemWebAppAsync
   // would ask AppService to launch the App, which routes the launch request to
diff --git a/chrome/browser/autofill/credit_card_accessory_controller_impl.cc b/chrome/browser/autofill/credit_card_accessory_controller_impl.cc
index 8ede903..d8e0d89 100644
--- a/chrome/browser/autofill/credit_card_accessory_controller_impl.cc
+++ b/chrome/browser/autofill/credit_card_accessory_controller_impl.cc
@@ -254,10 +254,10 @@
 }
 
 void CreditCardAccessoryControllerImpl::OnCreditCardFetched(
-    bool did_succeed,
+    CreditCardFetchResult result,
     const CreditCard* credit_card,
     const std::u16string& cvc) {
-  if (!did_succeed)
+  if (result != CreditCardFetchResult::kSuccess)
     return;
   content::RenderFrameHost* rfh = web_contents_->GetFocusedFrame();
   if (!rfh || !last_focused_field_id_ ||
diff --git a/chrome/browser/autofill/credit_card_accessory_controller_impl.h b/chrome/browser/autofill/credit_card_accessory_controller_impl.h
index 54deff9..b72b058 100644
--- a/chrome/browser/autofill/credit_card_accessory_controller_impl.h
+++ b/chrome/browser/autofill/credit_card_accessory_controller_impl.h
@@ -39,7 +39,7 @@
   void OnPersonalDataChanged() override;
 
   // CreditCardAccessManager::Accessor:
-  void OnCreditCardFetched(bool did_succeed,
+  void OnCreditCardFetched(CreditCardFetchResult result,
                            const CreditCard* credit_card,
                            const std::u16string& cvc) override;
 
diff --git a/chrome/browser/autofill/credit_card_accessory_controller_impl_unittest.cc b/chrome/browser/autofill/credit_card_accessory_controller_impl_unittest.cc
index e3d5210..12a8217 100644
--- a/chrome/browser/autofill/credit_card_accessory_controller_impl_unittest.cc
+++ b/chrome/browser/autofill/credit_card_accessory_controller_impl_unittest.cc
@@ -97,7 +97,7 @@
       const CreditCard* card,
       base::WeakPtr<Accessor> accessor,
       const base::TimeTicks& timestamp = base::TimeTicks()) override {
-    accessor->OnCreditCardFetched(/*did_succeed=*/true, &card_);
+    accessor->OnCreditCardFetched(CreditCardFetchResult::kSuccess, &card_);
   }
 
   CreditCard card_;
diff --git a/chrome/browser/autofill/mock_credit_card_accessory_controller.h b/chrome/browser/autofill/mock_credit_card_accessory_controller.h
index 7f61a69..3f5dfd5 100644
--- a/chrome/browser/autofill/mock_credit_card_accessory_controller.h
+++ b/chrome/browser/autofill/mock_credit_card_accessory_controller.h
@@ -42,7 +42,9 @@
   MOCK_METHOD(void, OnPersonalDataChanged, (), (override));
   MOCK_METHOD(void,
               OnCreditCardFetched,
-              (bool, const autofill::CreditCard*, const std::u16string&),
+              (autofill::CreditCardFetchResult,
+               const autofill::CreditCard*,
+               const std::u16string&),
               (override));
 };
 
diff --git a/chrome/browser/browsing_data/README.md b/chrome/browser/browsing_data/README.md
index 3493a2a..121c59d 100644
--- a/chrome/browser/browsing_data/README.md
+++ b/chrome/browser/browsing_data/README.md
@@ -5,32 +5,3 @@
 * "All cookies and site data" (chrome://settings/siteData)
 * "All sites" (chrome://settings/content/all)
 * "Cookies in use" display off the origin chip in the infobar
-
-## BrowsingDataXYZHelper
-
-Instances of this type are used to fully populate a CookiesTreeModel
-with full details (e.g. origin/size/modified) for different storage
-types, e.g. to report storage used by all origins.
-
-When StartFetching is called, a call is made into the relevant storage
-context to enumerate usage info - usually, a set of tuples of (origin,
-size, last modified). The CookiesTreeModel assembles this into the
-tree of nodes used to populate UI.
-
-Some UI also uses this to delete origin data, which again calls into
-the storage context.
-
-## CannedBrowsingDataXYZHelper
-
-Subclass of the above. These are created to sparsely populate a
-CookiesTreeModel on demand by LocalSharedObjectContainer, with only
-some details (e.g. full details for cookies, but only the usage of
-other storage typess).
-
-* PageSpecificContentSettings is notified on storage access/blocked.
-* It calls into the "canned" helper instance for the storage type.
-* The "canned" instance records necessary "pending" info about the access.
-* On demand, the "pending" info is used to populate a CookiesTreeModel.
-
-This "pending" info only needs to record the origin for most storage
-types.
diff --git a/chrome/browser/cart/cart_handler.cc b/chrome/browser/cart/cart_handler.cc
index 1b062802..0bb326f 100644
--- a/chrome/browser/cart/cart_handler.cc
+++ b/chrome/browser/cart/cart_handler.cc
@@ -116,6 +116,7 @@
   cart_service_->SetCartDiscountEnabled(enabled);
 }
 
-void CartHandler::PrepareForNavigation(const GURL& cart_url) {
-  cart_service_->PrepareForNavigation(cart_url);
+void CartHandler::PrepareForNavigation(const GURL& cart_url,
+                                       bool is_navigating) {
+  cart_service_->PrepareForNavigation(cart_url, is_navigating);
 }
diff --git a/chrome/browser/cart/cart_handler.h b/chrome/browser/cart/cart_handler.h
index 446a25d..5bf5557 100644
--- a/chrome/browser/cart/cart_handler.h
+++ b/chrome/browser/cart/cart_handler.h
@@ -37,7 +37,7 @@
   void OnDiscountConsentAcknowledged(bool accept) override;
   void GetDiscountEnabled(GetDiscountEnabledCallback callback) override;
   void SetDiscountEnabled(bool enabled) override;
-  void PrepareForNavigation(const GURL& cart_url) override;
+  void PrepareForNavigation(const GURL& cart_url, bool is_navigating) override;
 
  private:
   void GetCartDataCallback(GetMerchantCartsCallback callback,
diff --git a/chrome/browser/cart/cart_service.cc b/chrome/browser/cart/cart_service.cc
index 8c16407f..45493d7 100644
--- a/chrome/browser/cart/cart_service.cc
+++ b/chrome/browser/cart/cart_service.cc
@@ -371,8 +371,18 @@
   }
 }
 
-void CartService::PrepareForNavigation(const GURL& cart_url) {
+void CartService::PrepareForNavigation(const GURL& cart_url,
+                                       bool is_navigating) {
   metrics_tracker_->PrepareToRecordUKM(cart_url);
+  if (is_navigating || !IsPartnerMerchant(cart_url) ||
+      !IsCartDiscountEnabled()) {
+    return;
+  }
+  if (!discount_url_loader_) {
+    discount_url_loader_ = std::make_unique<DiscountURLLoader>(
+        chrome::FindTabbedBrowser(profile_, false), profile_);
+  }
+  discount_url_loader_->PrepareURLForDiscountLoad(cart_url);
 }
 
 void CartService::LoadCartsWithFakeData(CartDB::LoadCallback callback) {
@@ -402,6 +412,9 @@
   cart_db_->LoadAllCarts(base::BindOnce(&CartService::DeleteRemovedCartsContent,
                                         weak_ptr_factory_.GetWeakPtr()));
   metrics_tracker_->ShutDown();
+  if (discount_url_loader_) {
+    discount_url_loader_->ShutDown();
+  }
 }
 
 void CartService::OnURLsDeleted(history::HistoryService* history_service,
diff --git a/chrome/browser/cart/cart_service.h b/chrome/browser/cart/cart_service.h
index b1256518..25a068f 100644
--- a/chrome/browser/cart/cart_service.h
+++ b/chrome/browser/cart/cart_service.h
@@ -13,6 +13,7 @@
 #include "chrome/browser/cart/cart_discount_link_fetcher.h"
 #include "chrome/browser/cart/cart_metrics_tracker.h"
 #include "chrome/browser/cart/cart_service_factory.h"
+#include "chrome/browser/cart/discount_url_loader.h"
 #include "chrome/browser/profiles/profile.h"
 #include "components/history/core/browser/history_service.h"
 #include "components/history/core/browser/history_service_observer.h"
@@ -21,6 +22,7 @@
 #include "components/prefs/pref_registry_simple.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
 
+class DiscountURLLoader;
 class FetchDiscountWorker;
 
 // Service to maintain and read/write data for chrome cart module.
@@ -94,11 +96,12 @@
   void GetDiscountURL(const GURL& cart_url,
                       base::OnceCallback<void(const GURL&)> callback);
   // Gets called when a navigation to |cart_url| is happening or might happen.
-  // This can be triggered by either left click on a cart in cart module or
-  // right click to open context menu which could lead to open the cart later.
-  // This method is used to record the latest interacted cart, and then use that
-  // to identify whether a navigation originated from cart module has happened.
-  void PrepareForNavigation(const GURL& cart_url);
+  // |is_navigating| indicates whether the navigation is happening (e.g. left
+  // click on the cart item) or might happen later (e.g. right click to open
+  // context menu). This method 1) Record the latest interacted cart,
+  // and then use that to identify whether a navigation originated from cart
+  // module has happened. 2) Help identify whether to load discount URL.
+  void PrepareForNavigation(const GURL& cart_url, bool is_navigating);
   // history::HistoryServiceObserver:
   void OnURLsDeleted(history::HistoryService* history_service,
                      const history::DeletionInfo& deletion_info) override;
@@ -111,6 +114,7 @@
   friend class CartServiceFactory;
   friend class CartServiceTest;
   friend class CartServiceDiscountTest;
+  friend class CartServiceBrowserDiscountTest;
   FRIEND_TEST_ALL_PREFIXES(CartHandlerNtpModuleFakeDataTest,
                            TestEnableFakeData);
 
@@ -187,6 +191,7 @@
   std::unique_ptr<CartDiscountLinkFetcher> discount_link_fetcher_;
   optimization_guide::OptimizationGuideDecider* optimization_guide_decider_;
   std::unique_ptr<CartMetricsTracker> metrics_tracker_;
+  std::unique_ptr<DiscountURLLoader> discount_url_loader_;
   base::WeakPtrFactory<CartService> weak_ptr_factory_{this};
 };
 
diff --git a/chrome/browser/cart/cart_service_browsertest.cc b/chrome/browser/cart/cart_service_browsertest.cc
index d70a639..6f7a3ee 100644
--- a/chrome/browser/cart/cart_service_browsertest.cc
+++ b/chrome/browser/cart/cart_service_browsertest.cc
@@ -9,11 +9,14 @@
 #include "chrome/browser/persisted_state_db/profile_proto_db.h"
 #include "chrome/browser/ui/browser.h"
 #include "chrome/browser/ui/browser_list.h"
+#include "chrome/browser/ui/tabs/tab_strip_model.h"
+#include "chrome/common/pref_names.h"
 #include "chrome/test/base/chrome_test_utils.h"
 #include "chrome/test/base/in_process_browser_test.h"
 #include "chrome/test/base/ui_test_utils.h"
 #include "components/network_session_configurator/common/network_switches.h"
 #include "components/optimization_guide/core/optimization_guide_features.h"
+#include "components/prefs/pref_service.h"
 #include "components/search/ntp_features.h"
 #include "components/ukm/test_ukm_recorder.h"
 #include "content/public/test/browser_test.h"
@@ -34,6 +37,9 @@
   return proto;
 }
 
+const char kFakeMerchantA[] = "foo.com";
+const char kFakeMerchantURLA[] = "https://www.foo.com/cart.html";
+const char kFakeMerchantURLB[] = "https://www.bar.com/cart.html";
 const char kMockMerchant[] = "walmart.com";
 const char kMockMerchantURL[] = "https://www.walmart.com";
 using ShoppingCarts =
@@ -170,8 +176,8 @@
 
 IN_PROC_BROWSER_TEST_F(CartServiceBrowserTest, TestNavigationUKMCollection) {
   ukm::TestAutoSetUkmRecorder ukm_recorder;
-  GURL foo_url("https://www.foo.com/cart.html");
-  GURL bar_url("https://www.bar.com/cart.html");
+  GURL foo_url(kFakeMerchantURLA);
+  GURL bar_url(kFakeMerchantURLB);
   foo_url = https_server_.GetURL(foo_url.host(), foo_url.path());
   bar_url = https_server_.GetURL(bar_url.host(), bar_url.path());
 
@@ -182,7 +188,7 @@
   EXPECT_EQ(0u, entries.size());
 
   // Not record UKM when prepared URL is different from navigation URL.
-  service_->PrepareForNavigation(foo_url);
+  service_->PrepareForNavigation(foo_url, true);
   NavigateToURL(bar_url);
   entries = ukm_recorder.GetEntriesByName(
       ukm::builders::Shopping_ChromeCart::kEntryName);
@@ -193,7 +199,7 @@
   EXPECT_EQ(0u, entries.size());
 
   // Record UKM when prepared URL matches with navigation URL.
-  service_->PrepareForNavigation(foo_url);
+  service_->PrepareForNavigation(foo_url, true);
   NavigateToURL(foo_url);
   entries = ukm_recorder.GetEntriesByName(
       ukm::builders::Shopping_ChromeCart::kEntryName);
@@ -201,7 +207,7 @@
   ukm_recorder.ExpectEntrySourceHasUrl(entries.back(), foo_url);
 
   EXPECT_EQ(1, browser()->tab_strip_model()->count());
-  service_->PrepareForNavigation(bar_url);
+  service_->PrepareForNavigation(bar_url, true);
   NavigateToURL(bar_url, WindowOpenDisposition::NEW_BACKGROUND_TAB);
   EXPECT_EQ(2, browser()->tab_strip_model()->count());
   entries = ukm_recorder.GetEntriesByName(
@@ -211,7 +217,7 @@
 
   BrowserList* active_browser_list = BrowserList::GetInstance();
   EXPECT_EQ(1u, active_browser_list->size());
-  service_->PrepareForNavigation(foo_url);
+  service_->PrepareForNavigation(foo_url, true);
   NavigateToURL(foo_url, WindowOpenDisposition::NEW_WINDOW);
   EXPECT_EQ(2u, active_browser_list->size());
   entries = ukm_recorder.GetEntriesByName(
@@ -219,3 +225,72 @@
   EXPECT_EQ(3u, entries.size());
   ukm_recorder.ExpectEntrySourceHasUrl(entries.back(), foo_url);
 }
+
+class FakeCartDiscountLinkFetcher : public CartDiscountLinkFetcher {
+ public:
+  using CartDiscountLinkFetcherCallback = base::OnceCallback<void(const GURL&)>;
+
+  explicit FakeCartDiscountLinkFetcher(const GURL& discount_url)
+      : discount_url_(discount_url) {}
+
+  void Fetch(
+      std::unique_ptr<network::PendingSharedURLLoaderFactory> pending_factory,
+      cart_db::ChromeCartContentProto cart_content_proto,
+      CartDiscountLinkFetcherCallback callback) override {
+    std::move(callback).Run(discount_url_);
+  }
+
+ private:
+  const GURL& discount_url_;
+};
+
+class CartServiceBrowserDiscountTest : public CartServiceBrowserTest {
+ public:
+  void SetUpInProcessBrowserTestFixture() override {
+    scoped_feature_list_.InitAndEnableFeatureWithParameters(
+        ntp_features::kNtpChromeCartModule,
+        {{ntp_features::kNtpChromeCartModuleAbandonedCartDiscountParam, "true"},
+         {"partner-merchant-pattern", "(foo.com)"}});
+  }
+
+  void SetUpOnMainThread() override {
+    CartServiceBrowserTest::SetUpOnMainThread();
+    // Enable the discount feature by default.
+    profile_->GetPrefs()->SetBoolean(prefs::kCartDiscountEnabled, true);
+  }
+
+  void SetCartDiscountURLForTesting(const GURL& discount_url) {
+    std::unique_ptr<FakeCartDiscountLinkFetcher> fake_fetcher =
+        std::make_unique<FakeCartDiscountLinkFetcher>(discount_url);
+    service_->SetCartDiscountLinkFetcherForTesting(std::move(fake_fetcher));
+  }
+
+ private:
+  base::test::ScopedFeatureList scoped_feature_list_;
+};
+
+// TODO(crbug.com/1218979): Similar to TestNavigationUKMCollection, add tests
+// that open tab with different WindowOpenDisposition for this test. Figure out
+// a proper way to wait for the second load of the discount URL.
+IN_PROC_BROWSER_TEST_F(CartServiceBrowserDiscountTest,
+                       LoadDiscountInCurrentTab) {
+  cart_db::ChromeCartContentProto merchant_proto =
+      BuildProto(kFakeMerchantA, kFakeMerchantURLA);
+  cart_db::DiscountInfoProto* added_discount =
+      merchant_proto.mutable_discount_info()->add_discount_info();
+  added_discount->set_rule_id("fake_id");
+  added_discount->set_percent_off(5);
+  added_discount->set_raw_merchant_offer_id("fake_offer_id");
+  service_->AddCart(kFakeMerchantA, absl::nullopt, merchant_proto);
+
+  GURL foo_url("https://www.foo.com/cart.html");
+  GURL bar_url("https://www.bar.com/cart.html");
+  foo_url = https_server_.GetURL(foo_url.host(), foo_url.path());
+  bar_url = https_server_.GetURL(bar_url.host(), bar_url.path());
+  CartServiceBrowserDiscountTest::SetCartDiscountURLForTesting(bar_url);
+
+  service_->PrepareForNavigation(foo_url, false);
+  TabStripModel* model = browser()->tab_strip_model();
+  NavigateToURL(foo_url);
+  ASSERT_EQ(bar_url, model->GetActiveWebContents()->GetVisibleURL());
+}
diff --git a/chrome/browser/cart/chrome_cart.mojom b/chrome/browser/cart/chrome_cart.mojom
index 8e14560..58b57cd4 100644
--- a/chrome/browser/cart/chrome_cart.mojom
+++ b/chrome/browser/cart/chrome_cart.mojom
@@ -54,6 +54,8 @@
   SetDiscountEnabled(bool enabled);
   // Passes the |cart_url| that user is navigating or about to navigate
   // towards to browser process, in order to help identify navigations
-  // originated from cart module.
-  PrepareForNavigation(url.mojom.Url cart_url);
+  // originated from cart module. |is_navigating| indicates whether
+  // the navigation is happening now (e.g. left click on cart item)
+  // or not.
+  PrepareForNavigation(url.mojom.Url cart_url, bool is_navigating);
 };
diff --git a/chrome/browser/cart/discount_url_loader.cc b/chrome/browser/cart/discount_url_loader.cc
new file mode 100644
index 0000000..e331331a
--- /dev/null
+++ b/chrome/browser/cart/discount_url_loader.cc
@@ -0,0 +1,59 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/cart/discount_url_loader.h"
+
+#include "chrome/browser/cart/cart_service_factory.h"
+#include "chrome/browser/ui/browser_finder.h"
+#include "chrome/browser/ui/browser_list.h"
+
+DiscountURLLoader::DiscountURLLoader(Browser* browser, Profile* profile) {
+  if (!browser) {
+    return;
+  }
+  browser->tab_strip_model()->AddObserver(this);
+  BrowserList::GetInstance()->AddObserver(this);
+  cart_service_ = CartServiceFactory::GetForProfile(profile);
+}
+
+DiscountURLLoader::~DiscountURLLoader() = default;
+
+void DiscountURLLoader::ShutDown() {
+  BrowserList::GetInstance()->RemoveObserver(this);
+}
+
+void DiscountURLLoader::PrepareURLForDiscountLoad(const GURL& url) {
+  last_interacted_url_ = url;
+}
+
+void DiscountURLLoader::TabChangedAt(content::WebContents* contents,
+                                     int index,
+                                     TabChangeType change_type) {
+  if (change_type != TabChangeType::kAll) {
+    return;
+  }
+  if (last_interacted_url_) {
+    if (last_interacted_url_ == contents->GetVisibleURL()) {
+      cart_service_->GetDiscountURL(
+          contents->GetVisibleURL(),
+          base::BindOnce(&DiscountURLLoader::NavigateToDiscountURL,
+                         weak_ptr_factory_.GetWeakPtr(), contents));
+    }
+    last_interacted_url_.reset();
+  }
+}
+
+void DiscountURLLoader::OnBrowserAdded(Browser* browser) {
+  browser->tab_strip_model()->AddObserver(this);
+}
+
+void DiscountURLLoader::OnBrowserRemoved(Browser* browser) {
+  browser->tab_strip_model()->RemoveObserver(this);
+}
+
+void DiscountURLLoader::NavigateToDiscountURL(content::WebContents* contents,
+                                              const GURL& discount_url) {
+  contents->GetController().LoadURL(discount_url, content::Referrer(),
+                                    ui::PAGE_TRANSITION_FIRST, std::string());
+}
diff --git a/chrome/browser/cart/discount_url_loader.h b/chrome/browser/cart/discount_url_loader.h
new file mode 100644
index 0000000..257a790
--- /dev/null
+++ b/chrome/browser/cart/discount_url_loader.h
@@ -0,0 +1,48 @@
+// 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_CART_DISCOUNT_URL_LOADER_H_
+#define CHROME_BROWSER_CART_DISCOUNT_URL_LOADER_H_
+
+#include "base/memory/weak_ptr.h"
+#include "chrome/browser/cart/cart_service.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_list_observer.h"
+#include "chrome/browser/ui/tabs/tab_strip_model_observer.h"
+#include "content/public/browser/web_contents.h"
+
+// TODO(crbug.com/1218979): This is a workaround to try to override navigation
+// from context menu. Investigate if there are better ways to handle the second
+// navigation.
+class DiscountURLLoader : public BrowserListObserver,
+                          public TabStripModelObserver {
+ public:
+  explicit DiscountURLLoader(Browser* browser, Profile* profile);
+  ~DiscountURLLoader() override;
+  // Called to destroy any observers.
+  void ShutDown();
+
+  // Gets called when partner merchant cart with |url| is right clicked. Cache
+  // the |url| so that it can later be used to decide if a navigation originates
+  // from cart module interaction, and reload page with discount URL if needed.
+  void PrepareURLForDiscountLoad(const GURL& url);
+
+  // TabStripModelObserver:
+  void TabChangedAt(content::WebContents* contents,
+                    int index,
+                    TabChangeType change_type) override;
+  // BrowserListObserver:
+  void OnBrowserAdded(Browser* browser) override;
+  void OnBrowserRemoved(Browser* browser) override;
+
+ private:
+  void NavigateToDiscountURL(content::WebContents* contents,
+                             const GURL& discount_url);
+  absl::optional<GURL> last_interacted_url_;
+  CartService* cart_service_;
+  base::WeakPtrFactory<DiscountURLLoader> weak_ptr_factory_{this};
+};
+
+#endif  // CHROME_BROWSER_CART_DISCOUNT_URL_LOADER_H_
diff --git a/chrome/browser/chrome_content_browser_client.cc b/chrome/browser/chrome_content_browser_client.cc
index f655f3b0..9844c95 100644
--- a/chrome/browser/chrome_content_browser_client.cc
+++ b/chrome/browser/chrome_content_browser_client.cc
@@ -5922,6 +5922,27 @@
       HostContentSettingsMapFactory::GetForProfile(browser_context), origin);
 }
 
+bool ChromeContentBrowserClient::IsJitDisabledForSite(
+    content::BrowserContext* browser_context,
+    const GURL& site_url) {
+  Profile* profile = Profile::FromBrowserContext(browser_context);
+  auto* map = HostContentSettingsMapFactory::GetForProfile(profile);
+
+  // Special case to determine if any policy is set.
+  if (site_url.is_empty()) {
+    return map->GetDefaultContentSetting(ContentSettingsType::JAVASCRIPT_JIT,
+                                         nullptr) == CONTENT_SETTING_BLOCK;
+  }
+
+  // Only disable JIT for web schemes.
+  if (!site_url.SchemeIsHTTPOrHTTPS())
+    return false;
+
+  return (map->GetContentSetting(site_url, site_url,
+                                 ContentSettingsType::JAVASCRIPT_JIT) ==
+          CONTENT_SETTING_BLOCK);
+}
+
 ukm::UkmService* ChromeContentBrowserClient::GetUkmService() {
   return g_browser_process->GetMetricsServicesManager()->GetUkmService();
 }
diff --git a/chrome/browser/chrome_content_browser_client.h b/chrome/browser/chrome_content_browser_client.h
index 54a66b7..c5b0f7c 100644
--- a/chrome/browser/chrome_content_browser_client.h
+++ b/chrome/browser/chrome_content_browser_client.h
@@ -707,6 +707,8 @@
   bool ShouldAllowInsecurePrivateNetworkRequests(
       content::BrowserContext* browser_context,
       const url::Origin& origin) override;
+  bool IsJitDisabledForSite(content::BrowserContext* browser_context,
+                            const GURL& site_url) override;
   ukm::UkmService* GetUkmService() override;
 
   void OnKeepaliveRequestStarted(
diff --git a/chrome/browser/chromeos/extensions/default_app_order.cc b/chrome/browser/chromeos/extensions/default_app_order.cc
index f4f8a16..9e0a6ea 100644
--- a/chrome/browser/chromeos/extensions/default_app_order.cc
+++ b/chrome/browser/chromeos/extensions/default_app_order.cc
@@ -86,8 +86,6 @@
     arc::kPlayMusicAppId,
     extension_misc::kGooglePlayMusicAppId,
 
-    arc::kPlayGamesAppId,
-
     arc::kPlayBooksAppId,
     extension_misc::kGooglePlayBooksAppId,
     web_app::kPlayBooksAppId,
diff --git a/chrome/browser/component_updater/pki_metadata_component_installer.cc b/chrome/browser/component_updater/pki_metadata_component_installer.cc
index 660d1f6..ce7287c 100644
--- a/chrome/browser/component_updater/pki_metadata_component_installer.cc
+++ b/chrome/browser/component_updater/pki_metadata_component_installer.cc
@@ -193,7 +193,11 @@
     log_list_mojo.push_back(std::move(log_ptr));
   }
 
-  network_service->UpdateCtLogList(std::move(log_list_mojo));
+  base::Time update_time =
+      base::Time::UnixEpoch() +
+      base::TimeDelta::FromSeconds(proto->log_list().timestamp().seconds()) +
+      base::TimeDelta::FromNanoseconds(proto->log_list().timestamp().nanos());
+  network_service->UpdateCtLogList(std::move(log_list_mojo), update_time);
 #endif  // BUILDFLAG(IS_CT_SUPPORTED)
 }
 
diff --git a/chrome/browser/content_creation/notes/internal/android/java/res/layout/carousel_item.xml b/chrome/browser/content_creation/notes/internal/android/java/res/layout/carousel_item.xml
index afd1b6b..a48737e 100644
--- a/chrome/browser/content_creation/notes/internal/android/java/res/layout/carousel_item.xml
+++ b/chrome/browser/content_creation/notes/internal/android/java/res/layout/carousel_item.xml
@@ -11,6 +11,7 @@
     android:background="@color/modern_grey_200"
     android:paddingEnd="@dimen/note_side_padding"
     android:paddingStart="@dimen/note_side_padding"
+    android:importantForAccessibility="no"
     android:id="@+id/item">
 
         <!-- Note template -->
@@ -20,7 +21,8 @@
             android:layout_height="280dp"
             android:gravity="center_horizontal"
             android:background="@drawable/note_background_outline"
-            android:orientation="vertical">
+            android:orientation="vertical"
+            android:importantForAccessibility="yes">
 
             <!-- Selected text -->
             <TextView
@@ -33,7 +35,8 @@
                 android:layout_marginEnd="20dp"
                 android:layout_marginBottom="20dp"
                 android:background="@drawable/note_background_outline"
-                android:textAppearance="@style/TextAppearance.Headline" />
+                android:textAppearance="@style/TextAppearance.Headline"
+                android:importantForAccessibility="no"/>
 
             <!-- Footer -->
             <RelativeLayout
@@ -52,7 +55,8 @@
                     android:ellipsize="end"
                     android:layout_marginEnd="20dp"
                     android:textAppearance="@style/TextAppearance.TextSmallThick.Secondary.Light"
-                    android:layout_alignParentStart="true"/>
+                    android:layout_alignParentStart="true"
+                    android:importantForAccessibility="no"/>
 
                 <TextView
                     android:id="@+id/footer_title"
@@ -63,7 +67,8 @@
                     android:layout_marginEnd="20dp"
                     android:textAppearance="@style/TextAppearance.TextSmallItalic.Secondary.Light"
                     android:layout_alignTop="@id/footer_icon"
-                    android:layout_alignParentStart="false"/>
+                    android:layout_alignParentStart="false"
+                    android:importantForAccessibility="no"/>
 
                 <ImageView
                     android:id="@+id/footer_icon"
diff --git a/chrome/browser/content_creation/notes/internal/android/java/res/layout/creation_dialog.xml b/chrome/browser/content_creation/notes/internal/android/java/res/layout/creation_dialog.xml
index 9646b97..f6ef550 100644
--- a/chrome/browser/content_creation/notes/internal/android/java/res/layout/creation_dialog.xml
+++ b/chrome/browser/content_creation/notes/internal/android/java/res/layout/creation_dialog.xml
@@ -9,7 +9,9 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:background="@color/default_bg_color"
-    android:orientation="vertical">
+    android:orientation="vertical"
+    android:contentDescription="@string/content_creation_note_dialog_description"
+    android:importantForAccessibility="yes">
 
     <include layout="@layout/top_bar"/>
 
@@ -31,7 +33,7 @@
             android:layout_marginBottom="17dp"
             android:layout_gravity="center_horizontal"
             android:background="@drawable/note_title_outline"
-            android:textAppearance="@style/TextAppearance.TextSmall" />
+            android:textAppearance="@style/TextAppearance.TextSmall"/>
 
 	    <androidx.recyclerview.widget.RecyclerView
 	      android:id="@+id/note_carousel"
diff --git a/chrome/browser/content_creation/notes/internal/android/java/src/org/chromium/chrome/browser/content_creation/notes/NoteCreationDialog.java b/chrome/browser/content_creation/notes/internal/android/java/src/org/chromium/chrome/browser/content_creation/notes/NoteCreationDialog.java
index 4318ae22a..c12cc20 100644
--- a/chrome/browser/content_creation/notes/internal/android/java/src/org/chromium/chrome/browser/content_creation/notes/NoteCreationDialog.java
+++ b/chrome/browser/content_creation/notes/internal/android/java/src/org/chromium/chrome/browser/content_creation/notes/NoteCreationDialog.java
@@ -127,7 +127,9 @@
         View background = parent.findViewById(R.id.background);
         template.mainBackground.apply(background);
         background.setClipToOutline(true);
-
+        background.setContentDescription(
+                getActivity().getString(R.string.content_creation_note_template_selected,
+                        model.get(NoteProperties.TEMPLATE).localizedName));
         Typeface typeface = model.get(NoteProperties.TYPEFACE);
         TextView noteText = (TextView) parent.findViewById(R.id.text);
         noteText.setTypeface(typeface);
diff --git a/chrome/browser/download/download_browsertest.cc b/chrome/browser/download/download_browsertest.cc
index 968fcb3..106629e 100644
--- a/chrome/browser/download/download_browsertest.cc
+++ b/chrome/browser/download/download_browsertest.cc
@@ -2811,12 +2811,9 @@
   // Simulate saving the PDF from the UI.
   pdf::PDFWebContentsHelper* pdf_helper =
       pdf::PDFWebContentsHelper::FromWebContents(inner_web_contents);
-  blink::mojom::ReferrerPtr referrer = blink::mojom::Referrer::New();
-  referrer->url = subframe_url;
-  referrer->policy =
-      network::mojom::ReferrerPolicy::kStrictOriginWhenCrossOrigin;
   static_cast<pdf::mojom::PdfService*>(pdf_helper)
-      ->SaveUrlAs(subframe_url, std::move(referrer));
+      ->SaveUrlAs(subframe_url,
+                  network::mojom::ReferrerPolicy::kStrictOriginWhenCrossOrigin);
 
   request_waiter.Run();
 
diff --git a/chrome/browser/extensions/api/sessions/sessions_api.cc b/chrome/browser/extensions/api/sessions/sessions_api.cc
index fd35f63..e904ad0 100644
--- a/chrome/browser/extensions/api/sessions/sessions_api.cc
+++ b/chrome/browser/extensions/api/sessions/sessions_api.cc
@@ -16,6 +16,7 @@
 #include "base/strings/string_number_conversions.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/time/time.h"
+#include "build/chromeos_buildflags.h"
 #include "chrome/browser/extensions/api/sessions/session_id.h"
 #include "chrome/browser/extensions/api/tab_groups/tab_groups_util.h"
 #include "chrome/browser/extensions/api/tabs/windows_util.h"
@@ -316,6 +317,9 @@
       type = api::windows::WINDOW_TYPE_DEVTOOLS;
       break;
     case sessions::SessionWindow::TYPE_APP_POPUP:
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+    case sessions::SessionWindow::TYPE_CUSTOM_TAB:
+#endif
       NOTREACHED();
   }
 
diff --git a/chrome/browser/flag-metadata.json b/chrome/browser/flag-metadata.json
index 4df2348..fdca391 100644
--- a/chrome/browser/flag-metadata.json
+++ b/chrome/browser/flag-metadata.json
@@ -868,6 +868,11 @@
     "expiry_milestone": 95
   },
   {
+    "name": "debug-history-intervention-no-user-activation",
+    "owners": [ "shivanisha" ],
+    "expiry_milestone": 95
+  },
+  {
     "name": "debug-packed-apps",
     "owners": [ "benwells", "raymes" ],
     "expiry_milestone": 100
@@ -2882,7 +2887,7 @@
   {
     "name": "file-handling-api",
     "owners": [ "huangdarwin", "mgiuca", "cmp" ],
-    "expiry_milestone": 94
+    "expiry_milestone": 95
   },
   {
     "name": "files-app-copy-image",
diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc
index d05445b..104cd244b 100644
--- a/chrome/browser/flag_descriptions.cc
+++ b/chrome/browser/flag_descriptions.cc
@@ -110,6 +110,14 @@
     "If enabled, the legacy Menagerie API for profile data will be replaced by "
     "the new profile data source";
 
+const char kDebugHistoryInterventionNoUserActivationName[] =
+    "Debug flag for history intervention on no user activation";
+const char kDebugHistoryInterventionNoUserActivationDescription[] =
+    "This flag when enabled, will be used to debug an issue where a page that "
+    "did not get user activation "
+    "is able to work around the history intervention which is not the expected "
+    "behavior";
+
 const char kDetectedSourceLanguageOptionName[] =
     "Use Detected Language string on Desktop and Android";
 const char kDetectedSourceLanguageOptionDescription[] =
@@ -2181,8 +2189,11 @@
     "Enables new received tab "
     "UI shown next to the profile icon instead of using system notifications.";
 
+const char kSidePanelFlagId[] = "side-panel";
 const char kSidePanelName[] = "Side panel";
-const char kSidePanelDescription[] = "Host some content in a side panel.";
+const char kSidePanelDescription[] =
+    "Enables a browser-level side panel for a useful and persistent way to "
+    "access your Reading List and Bookmarks.";
 
 const char kServiceWorkerSubresourceFilterName[] =
     "ServiceWorker subresource filter";
diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h
index dea7645..b6c2c69 100644
--- a/chrome/browser/flag_descriptions.h
+++ b/chrome/browser/flag_descriptions.h
@@ -96,6 +96,9 @@
 extern const char kConversionMeasurementDebugModeName[];
 extern const char kConversionMeasurementDebugModeDescription[];
 
+extern const char kDebugHistoryInterventionNoUserActivationName[];
+extern const char kDebugHistoryInterventionNoUserActivationDescription[];
+
 extern const char kDeprecateMenagerieAPIName[];
 extern const char kDeprecateMenagerieAPIDescription[];
 
@@ -1252,6 +1255,7 @@
 extern const char kSendTabToSelfV2Name[];
 extern const char kSendTabToSelfV2Description[];
 
+extern const char kSidePanelFlagId[];
 extern const char kSidePanelName[];
 extern const char kSidePanelDescription[];
 
diff --git a/chrome/browser/language/android/BUILD.gn b/chrome/browser/language/android/BUILD.gn
index e758e68..a05a733 100644
--- a/chrome/browser/language/android/BUILD.gn
+++ b/chrome/browser/language/android/BUILD.gn
@@ -16,6 +16,7 @@
     "//chrome/browser/preferences:java",
     "//components/language/android:language_bridge_java",
     "//third_party/google_android_play_core:com_google_android_play_core_java",
+    "//ui/android:ui_no_recycler_view_java",
   ]
 }
 
diff --git a/chrome/browser/language/android/java/src/org/chromium/chrome/browser/language/AppLocaleUtils.java b/chrome/browser/language/android/java/src/org/chromium/chrome/browser/language/AppLocaleUtils.java
index e7a0a75..b067d7c 100644
--- a/chrome/browser/language/android/java/src/org/chromium/chrome/browser/language/AppLocaleUtils.java
+++ b/chrome/browser/language/android/java/src/org/chromium/chrome/browser/language/AppLocaleUtils.java
@@ -9,8 +9,14 @@
 import android.text.TextUtils;
 
 import org.chromium.base.BundleUtils;
+import org.chromium.base.LocaleUtils;
 import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
 import org.chromium.chrome.browser.preferences.SharedPreferencesManager;
+import org.chromium.ui.base.ResourceBundle;
+
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Objects;
 
 /**
  * Provides utility functions to assist with overriding the application language.
@@ -80,4 +86,44 @@
             listener.onComplete(true);
         }
     }
+
+    /**
+     * Return true if the locale is an exact match for an available UI language.
+     * Note: "en" and "en-AU" will return false since the available locales are "en-GB" and "en-US".
+     * @param locale BCP-47 language tag representing a locale (e.g. "en-US")
+     */
+    public static boolean isAvailableExactUiLanguage(String locale) {
+        return isAvailableUiLanguage(locale, null);
+    }
+
+    /**
+     * Return true if this locale is available or has a reasonable fallback language that can be
+     * used for UI. For example we do not have language packs for "en" or "pt" but fallback to the
+     * reasonable alternatives "en-US" and "pt-BR". Similarly, we have no language pack for "es-MX"
+     * or "es-AR" but will use "es-419" for both. However, for languages with no translations
+     * (e.g. "yo", "cy", ect.) the fallback is "en-US" which is not reasonable.
+     * @param locale BCP-47 language tag representing a locale (e.g. "en-US")
+     */
+    public static boolean isSupportedUiLanguage(String locale) {
+        return isAvailableUiLanguage(locale, BASE_LANGUAGE_COMPARATOR);
+    }
+
+    private static boolean isAvailableUiLanguage(String locale, Comparator<String> comparator) {
+        if (Objects.equals(locale, AppLocaleUtils.SYSTEM_LANGUAGE_VALUE)) return true;
+        return Arrays.binarySearch(ResourceBundle.getAvailableLocales(), locale, comparator) >= 0;
+    }
+
+    /**
+     * Comparator that removes any country or script information from either language tag
+     * since they are not needed for locale availability checks.
+     * Example: "es-MX" and "es-ES" will evaluate as equal.
+     */
+    private static final Comparator<String> BASE_LANGUAGE_COMPARATOR = new Comparator<String>() {
+        @Override
+        public int compare(String a, String b) {
+            String langA = LocaleUtils.toLanguage(a);
+            String langB = LocaleUtils.toLanguage(b);
+            return langA.compareTo(langB);
+        }
+    };
 }
diff --git a/chrome/browser/language/android/java/src/org/chromium/chrome/browser/language/AppLocaleUtilsTest.java b/chrome/browser/language/android/java/src/org/chromium/chrome/browser/language/AppLocaleUtilsTest.java
index 70bb7a6..bf74b6f 100644
--- a/chrome/browser/language/android/java/src/org/chromium/chrome/browser/language/AppLocaleUtilsTest.java
+++ b/chrome/browser/language/android/java/src/org/chromium/chrome/browser/language/AppLocaleUtilsTest.java
@@ -16,6 +16,9 @@
 import org.chromium.chrome.browser.preferences.SharedPreferencesManager;
 import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
 
+import java.util.Arrays;
+import java.util.List;
+
 /**
  * Tests for the AppLocalUtils class.
  */
@@ -59,6 +62,47 @@
         Assert.assertFalse(AppLocaleUtils.isAppLanguagePref("en"));
     }
 
+    @Test
+    @SmallTest
+    public void testIsAvailableBaseUiLanguage() {
+        // Base languages that there are no UI translations for.
+        List<String> notAvailableBaseLanguages =
+                Arrays.asList("cy", "ga", "ia", "yo", "aa-not-a-language");
+        for (String language : notAvailableBaseLanguages) {
+            Assert.assertFalse(String.format("Language %s", language),
+                    AppLocaleUtils.isSupportedUiLanguage(language));
+        }
+
+        // Base languages that have UI translations.
+        List<String> avaliableBaseLanguages =
+                Arrays.asList("af-ZA", "en", "en-US", "en-non-a-language", "fr-CA", "fr-FR",
+                        "zu-ZA", AppLocaleUtils.SYSTEM_LANGUAGE_VALUE);
+        for (String language : avaliableBaseLanguages) {
+            Assert.assertTrue(String.format("Language %s", language),
+                    AppLocaleUtils.isSupportedUiLanguage(language));
+        }
+    }
+
+    @Test
+    @SmallTest
+    public void testIsAvailableExactUiLanguage() {
+        // Languages for which there is no exact matching UI language.
+        List<String> notAvailableExactLanguages = Arrays.asList(
+                "en", "pt", "zh", "cy", "ga", "ia", "en-AU", "en-ZA", "fr-CM", "en-not-a-language");
+        for (String language : notAvailableExactLanguages) {
+            Assert.assertFalse(String.format("Language %s", language),
+                    AppLocaleUtils.isAvailableExactUiLanguage(language));
+        }
+
+        // Languages that have an exact matching UI language.
+        List<String> avaliableExactLanguages = Arrays.asList(
+                "en-US", "pt-BR", "fr", "fr-CA", "zh-CN", AppLocaleUtils.SYSTEM_LANGUAGE_VALUE);
+        for (String language : avaliableExactLanguages) {
+            Assert.assertTrue(String.format("Language %s", language),
+                    AppLocaleUtils.isAvailableExactUiLanguage(language));
+        }
+    }
+
     // Helper function to manually get and check AppLanguagePref.
     private void assertLanguagePrefEquals(String language) {
         Assert.assertEquals(language,
diff --git a/chrome/browser/language/android/java/src/org/chromium/chrome/browser/language/settings/LanguageItem.java b/chrome/browser/language/android/java/src/org/chromium/chrome/browser/language/settings/LanguageItem.java
index 0efbbfc..4171977 100644
--- a/chrome/browser/language/android/java/src/org/chromium/chrome/browser/language/settings/LanguageItem.java
+++ b/chrome/browser/language/android/java/src/org/chromium/chrome/browser/language/settings/LanguageItem.java
@@ -10,9 +10,7 @@
 import org.chromium.chrome.browser.language.AppLocaleUtils;
 import org.chromium.chrome.browser.language.GlobalAppLocaleController;
 import org.chromium.chrome.browser.language.R;
-import org.chromium.ui.base.ResourceBundle;
 
-import java.util.Arrays;
 import java.util.Comparator;
 import java.util.Locale;
 import java.util.Objects;
@@ -51,7 +49,7 @@
         mDisplayName = displayName;
         mNativeDisplayName = nativeDisplayName;
         mSupportTranslate = supportTranslate;
-        mSupportAppUI = isAvailableUiLanguage(code);
+        mSupportAppUI = AppLocaleUtils.isAvailableExactUiLanguage(code);
     }
 
     /**
@@ -84,12 +82,12 @@
 
     /**
      * Return true if this LanguageItem is a base language that supports translate.
-     * This filters out country variants that are not supported by Translate even if their base
-     * language is (e.g. en-US, en-IN, or es-MX).
+     * This filters out country variants that are not differentiated by Translate even if their base
+     * language is (e.g. en-GB, en-IN, or es-MX).
      * Todo(crbug.com/1180262): Make mSupportTranslate equivalent to this flag.
      * @return Whether or not this Language item is a base translatable language.
      */
-    public boolean isSupportedBaseLanguage() {
+    public boolean isSupportedBaseTranslateLanguage() {
         if (!mSupportTranslate) {
             return false;
         }
@@ -153,12 +151,4 @@
                 true /*supportTranslate*/);
     }
 
-    /**
-     * Return true if the language is available as a UI language.
-     * @param language BCP-47 language tag representing a locale (e.g. "en-US")
-     */
-    public static boolean isAvailableUiLanguage(String language) {
-        if (Objects.equals(language, AppLocaleUtils.SYSTEM_LANGUAGE_VALUE)) return true;
-        return Arrays.binarySearch(ResourceBundle.getAvailableLocales(), language) >= 0;
-    }
 }
diff --git a/chrome/browser/language/android/java/src/org/chromium/chrome/browser/language/settings/LanguagesManager.java b/chrome/browser/language/android/java/src/org/chromium/chrome/browser/language/settings/LanguagesManager.java
index 2274c81..3fdb0da 100644
--- a/chrome/browser/language/android/java/src/org/chromium/chrome/browser/language/settings/LanguagesManager.java
+++ b/chrome/browser/language/android/java/src/org/chromium/chrome/browser/language/settings/LanguagesManager.java
@@ -202,7 +202,8 @@
         LinkedHashSet<LanguageItem> results = new LinkedHashSet<>();
         // Filter for translatable languages not in |codesToSkipSet|.
         Predicate<LanguageItem> filter = (item) -> {
-            return item.isSupportedBaseLanguage() && !codesToSkipSet.contains(item.getCode());
+            return item.isSupportedBaseTranslateLanguage()
+                    && !codesToSkipSet.contains(item.getCode());
         };
         addItemsToResult(results, getUserAcceptLanguageItems(), filter);
         addItemsToResult(results, mLanguagesMap.values(), filter);
diff --git a/chrome/browser/mac/auth_session_request.mm b/chrome/browser/mac/auth_session_request.mm
index 2d46452a..df9b257 100644
--- a/chrome/browser/mac/auth_session_request.mm
+++ b/chrome/browser/mac/auth_session_request.mm
@@ -203,9 +203,14 @@
   //
   // Having a location indicator that is present but read-only is satisfied with
   // a popup window. That must not be changed.
+  //
+  // Omit it from session restore as well. This is a special window for use by
+  // this code; if it were restored it would not have the AuthSessionRequest and
+  // would not behave correctly.
 
-  Browser* browser = Browser::Create(
-      Browser::CreateParams(Browser::TYPE_POPUP, profile, true));
+  Browser::CreateParams params(Browser::TYPE_POPUP, profile, true);
+  params.omit_from_session_restore = true;
+  Browser* browser = Browser::Create(params);
   chrome::AddTabAt(browser, GURL("about:blank"), -1, true);
   browser->window()->Show();
 
diff --git a/chrome/browser/media/extension_media_access_handler.cc b/chrome/browser/media/extension_media_access_handler.cc
index 9b48d83..7690b6d 100644
--- a/chrome/browser/media/extension_media_access_handler.cc
+++ b/chrome/browser/media/extension_media_access_handler.cc
@@ -26,7 +26,8 @@
 // 5. Hotwording component extension.
 // 6. XKB input method component extension.
 // 7. M17n/T13n/CJK input method component extension.
-// Once http://crbug.com/292856 is fixed, remove this whitelist.
+// 8. Accessibility Common extension (used for Dictation)
+// Once http://crbug.com/292856 is fixed, remove this allowlist.
 bool IsMediaRequestAllowedForExtension(const extensions::Extension* extension) {
   return extension->id() == "mppnpdlheglhdfmldimlhpnegondlapf" ||
          extension->id() == "jokbpnebhdcladagohdnfgjcpejggllo" ||
@@ -34,7 +35,8 @@
          extension->id() == "nnckehldicaciogcbchegobnafnjkcne" ||
          extension->id() == "nbpagnldghgfoolbancepceaanlmhfmd" ||
          extension->id() == "jkghodnilhceideoidjikpgommlajknk" ||
-         extension->id() == "gjaehgfemfahhmlgpdfknkhdnemmolop";
+         extension->id() == "gjaehgfemfahhmlgpdfknkhdnemmolop" ||
+         extension->id() == "egfdjlfmgnehecnclamagfafdccgfndp";
 }
 
 }  // namespace
diff --git a/chrome/browser/media/router/providers/dial/dial_media_route_provider_unittest.cc b/chrome/browser/media/router/providers/dial/dial_media_route_provider_unittest.cc
index bedeb70..bcb4273 100644
--- a/chrome/browser/media/router/providers/dial/dial_media_route_provider_unittest.cc
+++ b/chrome/browser/media/router/providers/dial/dial_media_route_provider_unittest.cc
@@ -4,6 +4,9 @@
 
 #include "chrome/browser/media/router/providers/dial/dial_media_route_provider.h"
 
+#include <map>
+#include <utility>
+
 #include "base/bind.h"
 #include "base/callback_helpers.h"
 #include "base/run_loop.h"
@@ -27,6 +30,7 @@
 using media_router::mojom::RouteMessagePtr;
 using ::testing::_;
 using ::testing::IsEmpty;
+using ::testing::NiceMock;
 using ::testing::SaveArg;
 
 namespace media_router {
@@ -411,7 +415,7 @@
   network::TestURLLoaderFactory loader_factory_;
 
   mojo::Remote<mojom::MediaRouteProvider> provider_remote_;
-  MockMojoMediaRouter mock_router_;
+  NiceMock<MockMojoMediaRouter> mock_router_;
   std::unique_ptr<mojo::Receiver<mojom::MediaRouter>> router_receiver_;
 
   TestDialMediaSinkServiceImpl mock_sink_service_;
diff --git a/chrome/browser/media/router/providers/wired_display/wired_display_media_route_provider_unittest.cc b/chrome/browser/media/router/providers/wired_display/wired_display_media_route_provider_unittest.cc
index 4a6094e6..5723549 100644
--- a/chrome/browser/media/router/providers/wired_display/wired_display_media_route_provider_unittest.cc
+++ b/chrome/browser/media/router/providers/wired_display/wired_display_media_route_provider_unittest.cc
@@ -26,6 +26,7 @@
 using testing::_;
 using testing::Invoke;
 using testing::IsEmpty;
+using testing::NiceMock;
 using testing::WithArg;
 
 namespace media_router {
@@ -78,7 +79,8 @@
 class MockReceiverCreator {
  public:
   MockReceiverCreator()
-      : unique_receiver_(std::make_unique<MockPresentationReceiver>()),
+      : unique_receiver_(
+            std::make_unique<NiceMock<MockPresentationReceiver>>()),
         receiver_(unique_receiver_.get()) {}
   ~MockReceiverCreator() = default;
 
@@ -174,7 +176,7 @@
     mojo::PendingRemote<mojom::MediaRouter> router_pointer;
     router_receiver_ = std::make_unique<mojo::Receiver<mojom::MediaRouter>>(
         &router_, router_pointer.InitWithNewPipeAndPassReceiver());
-    provider_ = std::make_unique<TestWiredDisplayMediaRouteProvider>(
+    provider_ = std::make_unique<NiceMock<TestWiredDisplayMediaRouteProvider>>(
         provider_remote_.BindNewPipeAndPassReceiver(),
         std::move(router_pointer), &profile_);
     provider_->set_primary_display(primary_display_);
@@ -193,7 +195,7 @@
   content::BrowserTaskEnvironment task_environment_;
   mojo::Remote<mojom::MediaRouteProvider> provider_remote_;
   std::unique_ptr<TestWiredDisplayMediaRouteProvider> provider_;
-  MockMojoMediaRouter router_;
+  NiceMock<MockMojoMediaRouter> router_;
   std::unique_ptr<mojo::Receiver<mojom::MediaRouter>> router_receiver_;
 
   gfx::Rect primary_display_bounds_;
@@ -363,7 +365,7 @@
 TEST_F(WiredDisplayMediaRouteProviderTest, SendMediaStatusUpdate) {
   const std::string presentation_id = "presentationId";
   const std::string page_title = "Presentation Page Title";
-  MockCallback callback;
+  NiceMock<MockCallback> callback;
 
   provider_->set_all_displays({secondary_display1_, primary_display_});
   provider_remote_->StartObservingMediaRoutes(kPresentationSource);
@@ -394,7 +396,7 @@
 }
 
 TEST_F(WiredDisplayMediaRouteProviderTest, ExitFullscreenOnDisplayRemoved) {
-  MockCallback callback;
+  NiceMock<MockCallback> callback;
   provider_->set_all_displays({secondary_display1_, primary_display_});
   provider_remote_->StartObservingMediaRoutes(kPresentationSource);
   base::RunLoop().RunUntilIdle();
diff --git a/chrome/browser/net/system_network_context_manager.cc b/chrome/browser/net/system_network_context_manager.cc
index 7910033..b4f3333 100644
--- a/chrome/browser/net/system_network_context_manager.cc
+++ b/chrome/browser/net/system_network_context_manager.cc
@@ -526,7 +526,8 @@
       }
       log_list_mojo.push_back(std::move(log_info));
     }
-    network_service->UpdateCtLogList(std::move(log_list_mojo));
+    network_service->UpdateCtLogList(std::move(log_list_mojo),
+                                     base::GetBuildTime());
   }
 #endif
 
@@ -680,7 +681,6 @@
 
   if (g_enable_certificate_transparency) {
     network_context_params->enforce_chrome_ct_policy = true;
-    network_context_params->ct_log_update_time = base::GetBuildTime();
   }
 
 #endif
diff --git a/chrome/browser/page_load_metrics/integration_tests/data/full_size_image.html b/chrome/browser/page_load_metrics/integration_tests/data/full_size_image.html
new file mode 100644
index 0000000..71db767
--- /dev/null
+++ b/chrome/browser/page_load_metrics/integration_tests/data/full_size_image.html
@@ -0,0 +1,14 @@
+<iframe width="96" height="96" src="iframe_with_image.html"></iframe>
+<script>
+  const lcpPromise = new Promise(resolve => {
+    window.addEventListener("message", event => {
+      iframeLCP = event.data.startTime;
+      timeOriginDelta = event.data.timeOrigin - performance.timeOrigin;
+      lcpTime = iframeLCP + timeOriginDelta;
+      resolve();
+    });
+  });
+  async function waitForLCP() {
+    await lcpPromise;
+  }
+</script>
diff --git a/chrome/browser/page_load_metrics/integration_tests/data/iframe_with_image.html b/chrome/browser/page_load_metrics/integration_tests/data/iframe_with_image.html
new file mode 100644
index 0000000..fa995b323
--- /dev/null
+++ b/chrome/browser/page_load_metrics/integration_tests/data/iframe_with_image.html
@@ -0,0 +1,15 @@
+<style>
+  body {
+    margin: 0;
+  }
+</style>
+<img src="images/blue96x96.png" />
+<script>
+  new PerformanceObserver(e => {
+    const entries = e.getEntries();
+    const entry = entries[entries.length - 1];
+    window.parent.postMessage({
+      startTime: entry.startTime, timeOrigin: performance.timeOrigin
+    }, '*');
+  }).observe({ type: 'largest-contentful-paint', buffered: true });
+</script>
diff --git a/chrome/browser/page_load_metrics/integration_tests/largest_contentful_paint_browsertest.cc b/chrome/browser/page_load_metrics/integration_tests/largest_contentful_paint_browsertest.cc
index 3b308b1..78af35f 100644
--- a/chrome/browser/page_load_metrics/integration_tests/largest_contentful_paint_browsertest.cc
+++ b/chrome/browser/page_load_metrics/integration_tests/largest_contentful_paint_browsertest.cc
@@ -2,15 +2,19 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include "base/command_line.h"
 #include "chrome/browser/page_load_metrics/integration_tests/metric_integration_test.h"
 
+#include "base/feature_list.h"
 #include "base/json/json_string_value_serializer.h"
 #include "base/strings/strcat.h"
+#include "base/test/scoped_feature_list.h"
 #include "base/test/trace_event_analyzer.h"
 #include "build/build_config.h"
 #include "chrome/test/base/ui_test_utils.h"
 #include "content/public/test/browser_test.h"
 #include "services/metrics/public/cpp/ukm_builders.h"
+#include "third_party/blink/public/common/features.h"
 
 using trace_analyzer::Query;
 using trace_analyzer::TraceAnalyzer;
@@ -145,3 +149,32 @@
 
   EXPECT_EQ(EvalJs(sub, "test_step_2()").value.GetString(), "green-16x16.png");
 }
+
+class PageViewportInLCPTest : public MetricIntegrationTest {
+ public:
+  void SetUpCommandLine(base::CommandLine* command_line) override {
+    feature_list_.InitWithFeatures(
+        {blink::features::kUsePageViewportInLCP} /*enabled*/, {} /*disabled*/);
+  }
+
+  base::test::ScopedFeatureList feature_list_;
+};
+
+IN_PROC_BROWSER_TEST_F(PageViewportInLCPTest, FullSizeImageInIframe) {
+  Start();
+  StartTracing({"loading"});
+  Load("/full_size_image.html");
+  content::EvalJsResult result = EvalJs(web_contents(), "waitForLCP()");
+  double lcpTime = EvalJs(web_contents(), "lcpTime").ExtractDouble();
+
+  // Navigate away to force metrics recording.
+  ui_test_utils::NavigateToURL(browser(), GURL("about:blank"));
+
+  // |lcpTime| is computed from 3 different JS timestamps, so use an epsilon of
+  // 2 to account for coarsening and UKM integer rounding.
+  ExpectUKMPageLoadMetricNear(
+      PageLoad::kPaintTiming_NavigationToLargestContentfulPaint2Name, lcpTime,
+      2.0);
+  ExpectUniqueUMAPageLoadMetricNear(
+      "PageLoad.PaintTiming.NavigationToLargestContentfulPaint2", lcpTime);
+}
diff --git a/chrome/browser/page_load_metrics/integration_tests/metric_integration_test.h b/chrome/browser/page_load_metrics/integration_tests/metric_integration_test.h
index dca7f73..551816e 100644
--- a/chrome/browser/page_load_metrics/integration_tests/metric_integration_test.h
+++ b/chrome/browser/page_load_metrics/integration_tests/metric_integration_test.h
@@ -5,6 +5,7 @@
 #ifndef CHROME_BROWSER_PAGE_LOAD_METRICS_INTEGRATION_TESTS_METRIC_INTEGRATION_TEST_H_
 #define CHROME_BROWSER_PAGE_LOAD_METRICS_INTEGRATION_TESTS_METRIC_INTEGRATION_TEST_H_
 
+#include "base/strings/string_piece_forward.h"
 #include "chrome/test/base/in_process_browser_test.h"
 
 #include "base/test/metrics/histogram_tester.h"
diff --git a/chrome/browser/page_load_metrics/observers/core/amp_page_load_metrics_observer_unittest.cc b/chrome/browser/page_load_metrics/observers/core/amp_page_load_metrics_observer_unittest.cc
index b52ee664..d03701a5 100644
--- a/chrome/browser/page_load_metrics/observers/core/amp_page_load_metrics_observer_unittest.cc
+++ b/chrome/browser/page_load_metrics/observers/core/amp_page_load_metrics_observer_unittest.cc
@@ -405,7 +405,7 @@
   tester()->SimulateMetadataUpdate(metadata, subframe);
 
   page_load_metrics::mojom::FrameRenderDataUpdate render_data(1.0, 0.5, 0, 0, 0,
-                                                              0, 0, 0, {}, {});
+                                                              0, 0, 0, {});
   tester()->SimulateRenderDataUpdate(render_data, subframe);
 
   // Navigate the main frame to trigger metrics recording.
@@ -452,9 +452,8 @@
   tester()->SimulateMetadataUpdate(metadata, subframe);
 
   base::TimeTicks current_time = base::TimeTicks::Now();
-  page_load_metrics::mojom::FrameRenderDataUpdate render_data(
-      0.65, 0.65, 0, 0, 0, 0, 0, 0, {},
-      {current_time - base::TimeDelta::FromMilliseconds(2500)});
+  page_load_metrics::mojom::FrameRenderDataUpdate render_data(0.65, 0.65, 0, 0,
+                                                              0, 0, 0, 0, {});
 
   render_data.new_layout_shifts.emplace_back(
       page_load_metrics::mojom::LayoutShift::New(
diff --git a/chrome/browser/page_load_metrics/observers/core/ukm_page_load_metrics_observer_unittest.cc b/chrome/browser/page_load_metrics/observers/core/ukm_page_load_metrics_observer_unittest.cc
index 9dbbf67..ef4fc48 100644
--- a/chrome/browser/page_load_metrics/observers/core/ukm_page_load_metrics_observer_unittest.cc
+++ b/chrome/browser/page_load_metrics/observers/core/ukm_page_load_metrics_observer_unittest.cc
@@ -1493,9 +1493,8 @@
 TEST_F(UkmPageLoadMetricsObserverTest, LayoutInstability) {
   NavigateAndCommit(GURL(kTestUrl1));
   base::TimeTicks time_origin = base::TimeTicks::Now();
-  page_load_metrics::mojom::FrameRenderDataUpdate render_data(
-      1.0, 1.0, 0, 0, 0, 0, 0, 0, {},
-      {time_origin - base::TimeDelta::FromMilliseconds(3000)});
+  page_load_metrics::mojom::FrameRenderDataUpdate render_data(1.0, 1.0, 0, 0, 0,
+                                                              0, 0, 0, {});
   render_data.new_layout_shifts.emplace_back(
       page_load_metrics::mojom::LayoutShift::New(
           time_origin - base::TimeDelta::FromMilliseconds(4000), 0.5));
@@ -1508,8 +1507,8 @@
   // Simulate hiding the tab (the report should include shifts after hide).
   web_contents()->WasHidden();
 
-  page_load_metrics::mojom::FrameRenderDataUpdate render_data_2(
-      1.5, 0.0, 0, 0, 0, 0, 0, 0, {}, {});
+  page_load_metrics::mojom::FrameRenderDataUpdate render_data_2(1.5, 0.0, 0, 0,
+                                                                0, 0, 0, 0, {});
   render_data_2.new_layout_shifts.emplace_back(
       page_load_metrics::mojom::LayoutShift::New(
           time_origin - base::TimeDelta::FromMilliseconds(2500), 1.5));
@@ -1654,7 +1653,7 @@
 
   // Simulate layout instability in the main frame.
   page_load_metrics::mojom::FrameRenderDataUpdate render_data(1.0, 1.0, 0, 0, 0,
-                                                              0, 0, 0, {}, {});
+                                                              0, 0, 0, {});
   tester()->SimulateRenderDataUpdate(render_data);
 
   RenderFrameHost* subframe =
@@ -2192,7 +2191,7 @@
   NavigateAndCommit(GURL(kTestUrl1));
 
   page_load_metrics::mojom::FrameRenderDataUpdate render_data(1.0, 1.0, 0, 0, 0,
-                                                              0, 0, 0, {}, {});
+                                                              0, 0, 0, {});
   tester()->SimulateRenderDataUpdate(render_data);
 
   // Simulate closing the tab.
@@ -2227,7 +2226,7 @@
     float delta,
     content::RenderFrameHost* frame) {
   page_load_metrics::mojom::FrameRenderDataUpdate render_data(
-      delta, delta, 0, 0, 0, 0, 0, 0, {}, {});
+      delta, delta, 0, 0, 0, 0, 0, 0, {});
   tester()->SimulateRenderDataUpdate(render_data, frame);
 }
 
diff --git a/chrome/browser/page_load_metrics/observers/from_gws_page_load_metrics_observer_unittest.cc b/chrome/browser/page_load_metrics/observers/from_gws_page_load_metrics_observer_unittest.cc
index 19153e8..4f83f26 100644
--- a/chrome/browser/page_load_metrics/observers/from_gws_page_load_metrics_observer_unittest.cc
+++ b/chrome/browser/page_load_metrics/observers/from_gws_page_load_metrics_observer_unittest.cc
@@ -192,7 +192,7 @@
 
   tester()->SimulateTimingUpdate(timing);
   page_load_metrics::mojom::FrameRenderDataUpdate render_data(1.0, 1.0, 0, 0, 0,
-                                                              0, 0, 0, {}, {});
+                                                              0, 0, 0, {});
   tester()->SimulateRenderDataUpdate(render_data);
   render_data.layout_shift_delta = 1.5;
   render_data.layout_shift_delta_before_input_or_scroll = 0.0;
@@ -613,7 +613,7 @@
   NavigateAndCommit(GURL("https://www.google.com/search#q=test"));
   NavigateAndCommit(GURL(kExampleUrl));
   page_load_metrics::mojom::FrameRenderDataUpdate render_data(1.0, 1.0, 0, 0, 0,
-                                                              0, 0, 0, {}, {});
+                                                              0, 0, 0, {});
   tester()->SimulateRenderDataUpdate(render_data);
 
   web_contents()->WasHidden();
diff --git a/chrome/browser/policy/configuration_policy_handler_list_factory.cc b/chrome/browser/policy/configuration_policy_handler_list_factory.cc
index 768105b..c8e816c 100644
--- a/chrome/browser/policy/configuration_policy_handler_list_factory.cc
+++ b/chrome/browser/policy/configuration_policy_handler_list_factory.cc
@@ -696,6 +696,15 @@
   { key::kInsecureContentBlockedForUrls,
     prefs::kManagedInsecureContentBlockedForUrls,
     base::Value::Type::LIST },
+  { key::kDefaultJavaScriptJitSetting,
+    prefs::kManagedDefaultJavaScriptJitSetting,
+    base::Value::Type::INTEGER },
+  { key::kJavaScriptJitAllowedForSites,
+    prefs::kManagedJavaScriptJitAllowedForSites,
+    base::Value::Type::LIST },
+  { key::kJavaScriptJitBlockedForSites,
+    prefs::kManagedJavaScriptJitBlockedForSites,
+    base::Value::Type::LIST },
   { key::kShowCastIconInToolbar,
     prefs::kShowCastIconInToolbar,
     base::Value::Type::BOOLEAN },
diff --git a/chrome/browser/policy/policy_prefs_browsertest.cc b/chrome/browser/policy/policy_prefs_browsertest.cc
index ff75c59..0c1bea6 100644
--- a/chrome/browser/policy/policy_prefs_browsertest.cc
+++ b/chrome/browser/policy/policy_prefs_browsertest.cc
@@ -17,13 +17,15 @@
 #include "base/command_line.h"
 #include "base/files/file_path.h"
 #include "base/macros.h"
+#include "base/no_destructor.h"
+#include "base/path_service.h"
 #include "base/run_loop.h"
 #include "build/chromeos_buildflags.h"
 #include "chrome/browser/browser_process.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 "chrome/browser/profiles/profile_manager.h"
+#include "chrome/common/chrome_paths.h"
+#include "chrome/test/base/chrome_test_utils.h"
 #include "components/policy/core/browser/browser_policy_connector.h"
 #include "components/policy/core/browser/policy_pref_mapping_test.h"
 #include "components/policy/core/common/mock_configuration_policy_provider.h"
@@ -45,21 +47,23 @@
 namespace {
 
 base::FilePath GetTestCasePath() {
-  return ui_test_utils::GetTestFilePath(
-      base::FilePath(FILE_PATH_LITERAL("policy")),
-      base::FilePath(FILE_PATH_LITERAL("policy_test_cases.json")));
+  base::ScopedAllowBlockingForTesting allow_blocking;
+  base::FilePath path;
+  base::PathService::Get(chrome::DIR_TEST_DATA, &path);
+  return path.Append(FILE_PATH_LITERAL("policy"))
+      .Append(FILE_PATH_LITERAL("policy_test_cases.json"));
 }
 
 }  // namespace
 
-typedef InProcessBrowserTest PolicyPrefsTestCoverageTest;
+typedef PlatformBrowserTest PolicyPrefsTestCoverageTest;
 
 IN_PROC_BROWSER_TEST_F(PolicyPrefsTestCoverageTest, AllPoliciesHaveATestCase) {
   VerifyAllPoliciesHaveATestCase(GetTestCasePath());
 }
 
 // Base class for tests that change policy.
-class PolicyPrefsTest : public InProcessBrowserTest {
+class PolicyPrefsTest : public PlatformBrowserTest {
  public:
   PolicyPrefsTest() = default;
   PolicyPrefsTest(const PolicyPrefsTest&) = delete;
@@ -68,21 +72,43 @@
 
  protected:
   void SetUpInProcessBrowserTestFixture() override {
-    EXPECT_CALL(provider_, IsInitializationComplete(testing::_))
+    EXPECT_CALL(*GetMockPolicyProvider(), IsInitializationComplete(testing::_))
         .WillRepeatedly(testing::Return(true));
-    EXPECT_CALL(provider_, IsFirstPolicyLoadComplete(testing::_))
+    EXPECT_CALL(*GetMockPolicyProvider(), IsFirstPolicyLoadComplete(testing::_))
         .WillRepeatedly(testing::Return(true));
-    BrowserPolicyConnector::SetPolicyProviderForTesting(&provider_);
+    BrowserPolicyConnector::SetPolicyProviderForTesting(
+        GetMockPolicyProvider());
   }
 
   void TearDownOnMainThread() override { ClearProviderPolicy(); }
 
   void ClearProviderPolicy() {
-    provider_.UpdateChromePolicy(PolicyMap());
+    GetMockPolicyProvider()->UpdateChromePolicy(PolicyMap());
     base::RunLoop().RunUntilIdle();
   }
 
+  MockConfigurationPolicyProvider* GetMockPolicyProvider() {
+#if defined(OS_ANDROID)
+    // Trying to delete the mock provider on Android leads to a cascade of
+    // crashes due to ChromeBrowserPolicyConnector and ProfileImpl not being
+    // deleted. Those crashes are caused by checks that ensure that observer
+    // lists of ConfigurationPolicyProvider are always empty on destruction.
+    // On Desktop, removal of observers from those lists is triggered by the
+    // destructors of the classes above, but those same destructors are never
+    // invoked on Android.
+    static base::NoDestructor<MockConfigurationPolicyProvider> provider;
+    return provider.get();
+#else
+    // On non-Android platforms, the mock provider cleanup will be triggered
+    // by ChromeBrowserPolicyConnector and ProfileImpl destructors. Thus it's
+    // safe to define a provider object that is deleted on scope destruction.
+    return &provider_;
+#endif  // defined(OS_ANDROID)
+  }
+
+#if !defined(OS_ANDROID)
   MockConfigurationPolicyProvider provider_;
+#endif  // !defined(OS_ANDROID)
 };
 
 // Verifies that policies make their corresponding preferences become managed,
@@ -92,11 +118,14 @@
   policy::FakeBrowserDMTokenStorage storage;
   policy::BrowserDMTokenStorage::SetForTesting(&storage);
 #endif  // !BUILDFLAG(IS_CHROMEOS_ASH)
+
   PrefService* local_state = g_browser_process->local_state();
-  PrefService* user_prefs = browser()->profile()->GetPrefs();
+  PrefService* user_prefs =
+      ProfileManager::GetActiveUserProfile()->GetOriginalProfile()->GetPrefs();
 
   VerifyPolicyToPrefMappings(GetTestCasePath(), local_state, user_prefs,
-                             /* signin_profile_prefs= */ nullptr, &provider_);
+                             /* signin_profile_prefs= */ nullptr,
+                             GetMockPolicyProvider());
 }
 
 #if BUILDFLAG(IS_CHROMEOS_ASH)
@@ -127,7 +156,7 @@
   // checked by PolicyPrefsTest.PolicyToPrefsMapping test.
   VerifyPolicyToPrefMappings(GetTestCasePath(), /* local_state= */ nullptr,
                              /* user_prefs= */ nullptr, signin_profile_prefs,
-                             &provider_);
+                             GetMockPolicyProvider());
 }
 
 #endif  // BUILDFLAG(IS_CHROMEOS_ASH)
diff --git a/chrome/browser/policy/test/jit_policy_browsertest.cc b/chrome/browser/policy/test/jit_policy_browsertest.cc
new file mode 100644
index 0000000..2afd9c9
--- /dev/null
+++ b/chrome/browser/policy/test/jit_policy_browsertest.cc
@@ -0,0 +1,183 @@
+// 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 <vector>
+
+#include "base/values.h"
+#include "chrome/browser/policy/policy_test_utils.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/tabs/tab_strip_model.h"
+#include "chrome/test/base/ui_test_utils.h"
+#include "components/policy/core/common/policy_map.h"
+#include "components/policy/policy_constants.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/test/browser_test.h"
+#include "content/public/test/test_utils.h"
+#include "net/test/embedded_test_server/embedded_test_server.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+
+namespace policy {
+
+namespace {
+
+enum DefaultJitPolicyVariants {
+  DISABLED_BY_DEFAULT,
+  ENABLED_BY_DEFAULT,
+  NOT_SET
+};
+
+}  // namespace
+
+class JITPolicyTest
+    : public PolicyTest,
+      public testing::WithParamInterface<DefaultJitPolicyVariants> {
+ public:
+  JITPolicyTest() = default;
+  ~JITPolicyTest() override = default;
+
+  void SetUpCommandLine(base::CommandLine* command_line) override {
+    PolicyTest::SetUpCommandLine(command_line);
+    // This is needed for this test to run properly on platforms where
+    //  --site-per-process isn't the default, such as Android.
+    content::IsolateAllSitesForTesting(command_line);
+  }
+
+  void SetUpInProcessBrowserTestFixture() override {
+    PolicyTest::SetUpInProcessBrowserTestFixture();
+    PolicyMap policies;
+
+    AddDefaultPolicy(&policies);
+
+    base::Value block_list(base::Value::Type::LIST);
+    block_list.Append("jit-disabled.com");
+    SetPolicy(&policies, key::kJavaScriptJitBlockedForSites,
+              std::move(block_list));
+
+    base::Value allow_list(base::Value::Type::LIST);
+    allow_list.Append("jit-enabled.com");
+    SetPolicy(&policies, key::kJavaScriptJitAllowedForSites,
+              std::move(allow_list));
+
+    provider_.UpdateChromePolicy(policies);
+  }
+
+ protected:
+  void AddDefaultPolicy(PolicyMap* policies);
+  void ExpectThatPolicyDisablesJitOnUrl(const char* policy_value,
+                                        const char* url_value,
+                                        bool expect_jit_disabled);
+  bool DetermineExpectedResultForDefault();
+};
+
+void JITPolicyTest::AddDefaultPolicy(PolicyMap* policies) {
+  switch (GetParam()) {
+    case DISABLED_BY_DEFAULT:
+      SetPolicy(policies, key::kDefaultJavaScriptJitSetting,
+                base::Value(CONTENT_SETTING_BLOCK));
+      break;
+    case ENABLED_BY_DEFAULT:
+      SetPolicy(policies, key::kDefaultJavaScriptJitSetting,
+                base::Value(CONTENT_SETTING_ALLOW));
+      break;
+    case NOT_SET:
+      break;
+  }
+}
+
+bool JITPolicyTest::DetermineExpectedResultForDefault() {
+  switch (GetParam()) {
+    case DISABLED_BY_DEFAULT:
+      return true;
+      break;
+    case ENABLED_BY_DEFAULT:
+    case NOT_SET:
+      return false;
+      break;
+  }
+}
+
+void JITPolicyTest::ExpectThatPolicyDisablesJitOnUrl(const char* policy_value,
+                                                     const char* url_value,
+                                                     bool expect_jit_disabled) {
+  // This clears and resets the policies set-up in
+  // SetUpInProcessBrowserTestFixture.
+  PolicyMap policies;
+  AddDefaultPolicy(&policies);
+
+  base::Value block_list(base::Value::Type::LIST);
+  block_list.Append(policy_value);
+  SetPolicy(&policies, key::kJavaScriptJitBlockedForSites,
+            std::move(block_list));
+
+  UpdateProviderPolicy(policies);
+
+  GURL blocked_url = embedded_test_server()->GetURL(url_value, "/title1.html");
+  auto* render_frame_host =
+      ui_test_utils::NavigateToURL(browser(), blocked_url);
+  EXPECT_EQ(expect_jit_disabled,
+            render_frame_host->GetProcess()->IsJitDisabled());
+}
+
+IN_PROC_BROWSER_TEST_P(JITPolicyTest, JitPolicyTest) {
+  ASSERT_TRUE(embedded_test_server()->Start());
+
+  GURL disabled_url =
+      embedded_test_server()->GetURL("jit-disabled.com", "/title1.html");
+  auto* render_frame_host =
+      ui_test_utils::NavigateToURL(browser(), disabled_url);
+  EXPECT_TRUE(render_frame_host->GetProcess()->IsJitDisabled());
+
+  GURL enabled_url =
+      embedded_test_server()->GetURL("jit-enabled.com", "/title1.html");
+  render_frame_host = ui_test_utils::NavigateToURL(browser(), enabled_url);
+  EXPECT_FALSE(render_frame_host->GetProcess()->IsJitDisabled());
+
+  GURL default_url = embedded_test_server()->GetURL("foo.com", "/title1.html");
+  render_frame_host = ui_test_utils::NavigateToURL(browser(), default_url);
+
+  EXPECT_EQ(DetermineExpectedResultForDefault(),
+            render_frame_host->GetProcess()->IsJitDisabled());
+}
+
+IN_PROC_BROWSER_TEST_P(JITPolicyTest, JitDomainTest) {
+  // For brevity, this test only tests Deny rules, because Allow rules are
+  // tested above.
+  ASSERT_TRUE(embedded_test_server()->Start());
+  // Check subdomains work.
+  ExpectThatPolicyDisablesJitOnUrl("foo.com", "foo.com",
+                                   /*expect_jit_disabled=*/true);
+  ExpectThatPolicyDisablesJitOnUrl("foo.com", "subdomain.foo.com",
+                                   /*expect_jit_disabled=*/true);
+  ExpectThatPolicyDisablesJitOnUrl("[*.]foo.com", "subdomain.foo.com",
+                                   /*expect_jit_disabled=*/true);
+
+  bool expected_result_for_default = DetermineExpectedResultForDefault();
+
+  // Policy applies to different domain.
+  ExpectThatPolicyDisablesJitOnUrl(
+      "foo.com", "bar.com",
+      /*expect_jit_disabled=*/expected_result_for_default);
+
+  // Here there is an invalid policy as the JavaScript JIT policies only support
+  // eTLD+1 as origin.
+  ExpectThatPolicyDisablesJitOnUrl(
+      "subdomain.foo.com", "foo.com",
+      /*expect_jit_disabled=*/expected_result_for_default);
+  ExpectThatPolicyDisablesJitOnUrl(
+      "subdomain.foo.com", "subdomain.foo.com",
+      /*expect_jit_disabled=*/expected_result_for_default);
+}
+
+INSTANTIATE_TEST_SUITE_P(DefaultDisabled,
+                         JITPolicyTest,
+                         testing::Values(DISABLED_BY_DEFAULT));
+INSTANTIATE_TEST_SUITE_P(DefaultEnabled,
+                         JITPolicyTest,
+                         testing::Values(ENABLED_BY_DEFAULT));
+INSTANTIATE_TEST_SUITE_P(DefaultNotSet,
+                         JITPolicyTest,
+                         testing::Values(NOT_SET));
+
+}  // namespace policy
diff --git a/chrome/browser/resources/chromeos/accessibility/accessibility_common/BUILD.gn b/chrome/browser/resources/chromeos/accessibility/accessibility_common/BUILD.gn
index 9a1495d..a9f7ccb 100644
--- a/chrome/browser/resources/chromeos/accessibility/accessibility_common/BUILD.gn
+++ b/chrome/browser/resources/chromeos/accessibility/accessibility_common/BUILD.gn
@@ -71,6 +71,9 @@
     "../common/testing/callback_helper.js",
     "../common/testing/e2e_test_base.js",
     "../common/testing/mock_accessibility_private.js",
+    "../common/testing/mock_input_ime.js",
+    "../common/testing/mock_input_method_private.js",
+    "../common/testing/mock_language_settings_private.js",
   ]
 
   # The test base classes generate C++ code with these deps.
@@ -103,6 +106,8 @@
     "$externs_path/accessibility_private.js",
     "$externs_path/automation.js",
     "$externs_path/command_line_private.js",
+    "$externs_path/input_method_private.js",
+    "$externs_path/language_settings_private.js",
   ]
 }
 
@@ -122,4 +127,9 @@
 
 js_library("dictation") {
   sources = [ "dictation/dictation.js" ]
+  externs_list = [
+    "$externs_path/accessibility_private.js",
+    "$externs_path/input_method_private.js",
+    "$externs_path/language_settings_private.js",
+  ]
 }
diff --git a/chrome/browser/resources/chromeos/accessibility/accessibility_common/accessibility_common_loader.js b/chrome/browser/resources/chromeos/accessibility/accessibility_common/accessibility_common_loader.js
index e9e1661..f180c3e 100644
--- a/chrome/browser/resources/chromeos/accessibility/accessibility_common/accessibility_common_loader.js
+++ b/chrome/browser/resources/chromeos/accessibility/accessibility_common/accessibility_common_loader.js
@@ -60,6 +60,11 @@
         {}, this.onDictationUpdated_.bind(this));
     chrome.accessibilityFeatures.dictation.onChange.addListener(
         this.onDictationUpdated_.bind(this));
+
+    // AccessibilityCommon is an IME so it shows in the input methods list
+    // when it starts up. Remove from this list, Dictation will add it back
+    // whenever needed.
+    Dictation.removeAsInputMethod();
   }
 
   /**
diff --git a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/dictation.js b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/dictation.js
index 71ceec1..98fd3ad 100644
--- a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/dictation.js
+++ b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/dictation.js
@@ -3,6 +3,24 @@
 // found in the LICENSE file.
 
 /**
+ * Dictation states.
+ * @enum {!number}
+ */
+const DictationState = {
+  OFF: 1,
+  STARTING: 2,
+  LISTENING: 3,
+  STOPPING: 4,
+};
+
+/**
+ * The IME engine ID for AccessibilityCommon.
+ * @private {string}
+ * @const
+ */
+const IME_ENGINE_ID = '_ext_ime_egfdjlfmgnehecnclamagfafdccgfndpdictation';
+
+/**
  * Main class for the Chrome OS dictation feature.
  * Please note: this is being developed behind the flag
  * --enable-experimental-accessibility-dictation-extension
@@ -11,6 +29,24 @@
   constructor() {
     chrome.accessibilityPrivate.onToggleDictation.addListener(
         this.onToggleDictation_.bind(this));
+    chrome.input.ime.onFocus.addListener(this.onImeFocus_.bind(this));
+    chrome.input.ime.onBlur.addListener(this.onImeBlur_.bind(this));
+
+    /** @private {number} */
+    this.activeImeContextId_ = -1;
+
+    /**
+     * The engine ID of the previously active IME input method. Used to
+     * restore the previous IME after Dictation is deactivated.
+     * @private {string}
+     */
+    this.previousImeEngineId_ = '';
+
+    /**
+     * The state of Dictation.
+     * @private {!DictationState}
+     */
+    this.state_ = DictationState.OFF;
   }
 
   /**
@@ -19,10 +55,86 @@
    * @private
    */
   onToggleDictation_(activated) {
-    if (activated) {
-      // Dictation as a JS extension isn't actually implemented yet, so just
-      // turn off again.
-      chrome.accessibilityPrivate.toggleDictation();
+    if (activated && this.state_ === DictationState.OFF) {
+      this.state_ = DictationState.STARTING;
+      chrome.inputMethodPrivate.getCurrentInputMethod((method) => {
+        if (this.state_ !== DictationState.STARTING) {
+          return;
+        }
+        this.previousImeEngineId_ = method;
+        // Add AccessibilityCommon as an input method and active it.
+        chrome.languageSettingsPrivate.addInputMethod(IME_ENGINE_ID);
+        chrome.inputMethodPrivate.setCurrentInputMethod(IME_ENGINE_ID, () => {
+          if (this.state_ === DictationState.STARTING) {
+            // TODO(crbug.com/1216111): Start speech recognition and
+            // change state to LISTENING after SR starts.
+          } else {
+            // We are no longer starting up - perhaps a stop came
+            // through during the async callbacks. Ensure cleanup
+            // by calling onDictationStopped_.
+            this.onDictationStopped_();
+          }
+        });
+      });
+    } else {
+      this.onDictationStopped_();
     }
   }
+
+  /**
+   * Stops Dictation in the browser / ash if it wasn't already stopped.
+   * @private
+   */
+  stopDictation_() {
+    // Stop Dictation if the state isn't already off.
+    if (this.state_ !== DictationState.OFF) {
+      chrome.accessibilityPrivate.toggleDictation();
+      this.state_ = DictationState.STOPPING;
+    }
+  }
+
+  /**
+   * Called when Dictation has been toggled off. Cleans up IME and local state.
+   * @private
+   */
+  onDictationStopped_() {
+    if (this.state_ === DictationState.OFF) {
+      return;
+    }
+    this.state_ = DictationState.OFF;
+    // Clean up IME state and reset to the previous IME method.
+    this.activeImeContextId_ = -1;
+    chrome.inputMethodPrivate.setCurrentInputMethod(this.previousImeEngineId_);
+    this.previousImeEngineId_ = '';
+    Dictation.removeAsInputMethod();
+  }
+
+  /**
+   * chrome.input.ime.onFocus callback. Save the active context ID.
+   * @param {chrome.input.ime.InputContext} context Input field context.
+   * @private
+   */
+  onImeFocus_(context) {
+    this.activeImeContextId_ = context.contextID;
+  }
+
+  /**
+   * chrome.input.ime.onFocus callback. Stops Dictation if the active
+   * context ID lost focus.
+   * @param {number} contextId
+   * @private
+   */
+  onImeBlur_(contextId) {
+    if (contextId === this.activeImeContextId_) {
+      this.stopDictation_();
+    }
+  }
+
+  /**
+   * Removes AccessibilityCommon as an input method so it doesn't show up in
+   * the shelf input method picker UI.
+   */
+  static removeAsInputMethod() {
+    chrome.languageSettingsPrivate.removeInputMethod(IME_ENGINE_ID);
+  }
 }
diff --git a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/dictation_test.js b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/dictation_test.js
index dd5e3d4a..b6120f3 100644
--- a/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/dictation_test.js
+++ b/chrome/browser/resources/chromeos/accessibility/accessibility_common/dictation/dictation_test.js
@@ -4,11 +4,38 @@
 
 GEN_INCLUDE(['../../common/testing/e2e_test_base.js']);
 GEN_INCLUDE(['../../common/testing/mock_accessibility_private.js']);
+GEN_INCLUDE(['../../common/testing/mock_input_ime.js']);
+GEN_INCLUDE(['../../common/testing/mock_input_method_private.js']);
+GEN_INCLUDE(['../../common/testing/mock_language_settings_private.js']);
 
 /**
  * Dictation feature using accessibility common extension browser tests.
  */
 DictationE2ETest = class extends E2ETestBase {
+  constructor() {
+    super();
+    this.mockAccessibilityPrivate = MockAccessibilityPrivate;
+    chrome.accessibilityPrivate = this.mockAccessibilityPrivate;
+
+    this.mockInputIme = MockInputIme;
+    chrome.input.ime = this.mockInputIme;
+
+    this.mockInputMethodPrivate = MockInputMethodPrivate;
+    chrome.inputMethodPrivate = this.mockInputMethodPrivate;
+
+    this.mockLanguageSettingsPrivate = MockLanguageSettingsPrivate;
+    chrome.languageSettingsPrivate = this.mockLanguageSettingsPrivate;
+
+    this.dictationEngineId =
+        '_ext_ime_egfdjlfmgnehecnclamagfafdccgfndpdictation';
+
+    // Re-initialize AccessibilityCommon with mock APIs.
+    const reinit = module => {
+      accessibilityCommon = new module.AccessibilityCommon();
+    };
+import('/accessibility_common/accessibility_common_loader.js').then(reinit);
+  }
+
   /** @override */
   testGenCppIncludes() {
     super.testGenCppIncludes();
@@ -39,12 +66,125 @@
     `);
     super.testGenPreambleCommon('kAccessibilityCommonExtensionId');
   }
-};
 
-TEST_F('DictationE2ETest', 'SanityCheck', function() {
-  this.newCallback(async () => {
+  /**
+   * Waits for Dictation module to be loaded.
+   */
+  async waitForDictationModule() {
     await importModule(
         'Dictation', '/accessibility_common/dictation/dictation.js');
     assertNotNullNorUndefined(Dictation);
+    // Enable Dictation.
+    await new Promise(resolve => {
+      chrome.accessibilityFeatures.dictation.set({value: true}, resolve);
+    });
+    return new Promise(resolve => {
+      resolve();
+    });
+  }
+
+  /**
+   * Generates a function that runs a callback after the Dictation module has
+   * loaded.
+   * @param {function<>} callback
+   * @returns {function<>}
+   */
+  runAfterDictationLoad(callback) {
+    return this.newCallback(async () => {
+      await this.waitForDictationModule();
+      callback();
+    });
+  }
+
+  /**
+   * Checks that Dictation is the active IME.
+   */
+  checkDictationImeActive() {
+    assertEquals(
+        this.dictationEngineId,
+        this.mockInputMethodPrivate.getCurrentInputMethodForTest());
+    assertTrue(this.mockLanguageSettingsPrivate.hasInputMethod(
+        this.dictationEngineId));
+  }
+
+  /*
+   * Checks that Dictation is not the active IME.
+   * @param {*} opt_activeImeId If we do not expect Dictation IME to be
+   *     activated, an optional IME ID that we do expect to be activated.
+   */
+  checkDictationImeInactive(opt_activeImeId) {
+    assertNotEquals(
+        this.dictationEngineId,
+        this.mockInputMethodPrivate.getCurrentInputMethodForTest());
+    assertFalse(this.mockLanguageSettingsPrivate.hasInputMethod(
+        this.dictationEngineId));
+    if (opt_activeImeId) {
+      assertEquals(
+          opt_activeImeId,
+          this.mockInputMethodPrivate.getCurrentInputMethodForTest());
+    }
+  }
+};
+
+TEST_F('DictationE2ETest', 'SanityCheck', function() {
+  this.runAfterDictationLoad(() => {
+    assertFalse(this.mockAccessibilityPrivate.getDictationActive());
+  })();
+});
+
+TEST_F('DictationE2ETest', 'LoadsIMEWhenEnabled', function() {
+  this.runAfterDictationLoad(() => {
+    this.checkDictationImeInactive();
+
+    this.mockAccessibilityPrivate.callOnToggleDictation(true);
+    assertTrue(this.mockAccessibilityPrivate.getDictationActive());
+    this.checkDictationImeActive();
+
+    // Turn off Dictation and make sure it removes as IME
+    this.mockAccessibilityPrivate.callOnToggleDictation(false);
+    assertFalse(this.mockAccessibilityPrivate.getDictationActive());
+    this.checkDictationImeInactive();
+  })();
+});
+
+TEST_F('DictationE2ETest', 'TogglesDictationOffWhenIMEBlur', function() {
+  this.runAfterDictationLoad(() => {
+    this.checkDictationImeInactive();
+
+    this.mockAccessibilityPrivate.callOnToggleDictation(true);
+    assertTrue(this.mockAccessibilityPrivate.getDictationActive());
+    this.checkDictationImeActive();
+
+    // Focus an input context.
+    this.mockInputIme.callOnFocus(1);
+    // Blur the input context. Dictation should get toggled off.
+    this.mockInputIme.callOnBlur(1);
+
+    assertFalse(this.mockAccessibilityPrivate.getDictationActive());
+
+    // Now that we've confirmed that Dictation JS tried to toggle Dictation,
+    // via AccessibilityPrivate, we can call the onToggleDictation
+    // callback as AccessibilityManager would do, to allow Dictation JS to clean
+    // up state.
+    this.mockAccessibilityPrivate.callOnToggleDictation(false);
+
+    this.checkDictationImeInactive();
+  })();
+});
+
+TEST_F('DictationE2ETest', 'ResetsPreviousIMEAfterDeactivate', function() {
+  this.runAfterDictationLoad(() => {
+    // Set something as the active IME.
+    this.mockInputMethodPrivate.setCurrentInputMethod('keyboard_cat');
+    this.mockLanguageSettingsPrivate.addInputMethod('keyboard_cat');
+
+    // Activate Dictation.
+    this.mockAccessibilityPrivate.callOnToggleDictation(true);
+    assertTrue(this.mockAccessibilityPrivate.getDictationActive());
+    this.checkDictationImeActive();
+
+    // Deactivate Dictation.
+    this.mockAccessibilityPrivate.callOnToggleDictation(false);
+    this.checkDictationImeInactive('keyboard_cat');
   })();
 });
diff --git a/chrome/browser/resources/chromeos/accessibility/accessibility_common_manifest.json.jinja2 b/chrome/browser/resources/chromeos/accessibility/accessibility_common_manifest.json.jinja2
index 634febd..6cde5c8 100644
--- a/chrome/browser/resources/chromeos/accessibility/accessibility_common_manifest.json.jinja2
+++ b/chrome/browser/resources/chromeos/accessibility/accessibility_common_manifest.json.jinja2
@@ -17,10 +17,22 @@
     "accessibilityFeatures.read",
     "accessibilityFeatures.modify",
     "commandLinePrivate",
-    "settingsPrivate"
+    "input",
+    "inputMethodPrivate",
+    "settingsPrivate",
+    "languageSettingsPrivate"
   ],
   "automation": {
     "desktop": true
   },
-  "default_locale": "en"
+  "default_locale": "en",
+  "input_components": [
+    {
+      "name": "Dictation",
+      "type": "ime",
+      "id": "dictation",
+      "description": "Dictation",
+      "language": ["none"]
+    }
+  ]
 }
diff --git a/chrome/browser/resources/chromeos/accessibility/common/testing/mock_accessibility_private.js b/chrome/browser/resources/chromeos/accessibility/common/testing/mock_accessibility_private.js
index 09b1b78..4266927 100644
--- a/chrome/browser/resources/chromeos/accessibility/common/testing/mock_accessibility_private.js
+++ b/chrome/browser/resources/chromeos/accessibility/common/testing/mock_accessibility_private.js
@@ -49,6 +49,12 @@
   /** @private {?string} */
   highlightColor_: null,
 
+  /** @private {function<boolean>} */
+  dictationToggleListener_: null,
+
+  /** @private {boolean} */
+  dictationActivated_: false,
+
   // Methods from AccessibilityPrivate API. //
 
   onScrollableBoundsForPointRequested: {
@@ -62,6 +68,7 @@
 
     /**
      * Removes the listener.
+     * @param {function<number, number>} listener
      */
     removeListener: (listener) => {
       if (MockAccessibilityPrivate.boundsListener_ === listener) {
@@ -70,10 +77,8 @@
     }
   },
 
-  onMagnifierBoundsChanged: {
-    addListener: (listener) => {},
-    removeListener: (listener) => {}
-  },
+  onMagnifierBoundsChanged:
+      {addListener: (listener) => {}, removeListener: (listener) => {}},
 
   onSelectToSpeakPanelAction: {
     /**
@@ -86,6 +91,26 @@
     },
   },
 
+  onToggleDictation: {
+    /**
+     * Adds a listener to onToggleDictation.
+     * @param {function<boolean>} listener
+     */
+    addListener: (listener) => {
+      MockAccessibilityPrivate.dictationToggleListener_ = listener;
+    },
+
+    /**
+     * Removes the listener.
+     * @param {function<boolean>} listener
+     */
+    removeListener: (listener) => {
+      if (MockAccessibilityPrivate.dictationToggleListener_ === listener) {
+        MockAccessibilityPrivate.dictationToggleListener_ = null;
+      }
+    }
+  },
+
   onSelectToSpeakStateChangeRequested: {
     /**
      * Adds a listener to onSelectToSpeakStateChangeRequested.
@@ -148,6 +173,14 @@
         .selectToSpeakPanelState_ = {show, anchor, isPaused, speed};
   },
 
+  /**
+   * Called in order to toggle Dictation listening.
+   */
+  toggleDictation: () => {
+    MockAccessibilityPrivate.dictationActivated_ =
+        !MockAccessibilityPrivate.dictationActivated_;
+  },
+
   // Methods for testing. //
 
   /**
@@ -239,4 +272,26 @@
       MockAccessibilityPrivate.selectToSpeakStateChangeListener_();
     }
   },
+
+  /**
+   * Simulates Dictation activation change from AccessibilityManager, which may
+   * occur when the user or a chrome extension toggles Dictation active state.
+   * @param {boolean} activated
+   */
+  callOnToggleDictation: (activated) => {
+    MockAccessibilityPrivate.dictationActivated_ = activated;
+    if (MockAccessibilityPrivate.dictationToggleListener_) {
+      MockAccessibilityPrivate.dictationToggleListener_(activated);
+    }
+  },
+
+  /**
+   * Gets the current Dictation active state. This can be flipped when
+   * MockAccessibilityPrivate.toggleDictation is called, and set when
+   * MocakAccessibilityPrivate.callOnToggleDictation is called.
+   * @returns {boolean} The current Dictation active state.
+   */
+  getDictationActive() {
+    return MockAccessibilityPrivate.dictationActivated_;
+  },
 };
\ No newline at end of file
diff --git a/chrome/browser/resources/chromeos/accessibility/common/testing/mock_input_ime.js b/chrome/browser/resources/chromeos/accessibility/common/testing/mock_input_ime.js
new file mode 100644
index 0000000..55b073d
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/common/testing/mock_input_ime.js
@@ -0,0 +1,86 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @typedef {{
+ *   contextID: number,
+ * }}
+ */
+let InputContext;
+
+/*
+ * A mock chrome.input.ime API for tests.
+ */
+var MockInputIme = {
+  /** @private {function<InputContext>} */
+  onFocusListener_: null,
+
+  /** @private {function<number>} */
+  onBlurListener_: null,
+
+  // Methods from chrome.input.ime API. //
+
+  onFocus: {
+    /**
+     * Adds a listener to onFocus.
+     * @param {function<InputContext>} listener
+     */
+    addListener: (listener) => {
+      MockInputIme.onFocusListener_ = listener;
+    },
+
+    /**
+     * Removes the listener.
+     * @param {function<InputContext>} listener
+     */
+    removeListener: (listener) => {
+      if (MockInputIme.onFocusListener_ === listener) {
+        MockInputIme.onFocusListener_ = null;
+      }
+    }
+  },
+
+  onBlur: {
+    /**
+     * Adds a listener to onBlur.
+     * @param {function<number>} listener
+     */
+    addListener: (listener) => {
+      MockInputIme.onBlurListener_ = listener;
+    },
+
+    /**
+     * Removes the listener.
+     * @param {function<number>} listener
+     */
+    removeListener: (listener) => {
+      if (MockInputIme.onBlurListener_ === listener) {
+        MockInputIme.onBlurListener_ = null;
+      }
+    }
+  },
+
+  // Methods for testing. //
+
+  /**
+   * Calls listeners for chrome.input.ime.onFocus with a InputContext with the
+   * given contextID.
+   * @param {number} contextID
+   */
+  callOnFocus(contextID) {
+    if (MockInputIme.onFocusListener_) {
+      MockInputIme.onFocusListener_({contextID});
+    }
+  },
+
+  /**
+   * Calls listeners for chrome.input.ime.onBlur with the given contextID.
+   * @param {number} contextID
+   */
+  callOnBlur(contextID) {
+    if (MockInputIme.onBlurListener_) {
+      MockInputIme.onBlurListener_(contextID);
+    }
+  },
+};
\ No newline at end of file
diff --git a/chrome/browser/resources/chromeos/accessibility/common/testing/mock_input_method_private.js b/chrome/browser/resources/chromeos/accessibility/common/testing/mock_input_method_private.js
new file mode 100644
index 0000000..03de695a
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/common/testing/mock_input_method_private.js
@@ -0,0 +1,42 @@
+// 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.
+
+/*
+ * A mock chrome.inputMethodPrivate API for tests.
+ */
+var MockInputMethodPrivate = {
+  /** @private {string} */
+  currentInputMethod_: '',
+
+  // Methods from chrome.inputMethodPrivate API. //
+
+  /**
+   * Gets the current input method.
+   * @param {function<string>} callback
+   */
+  getCurrentInputMethod(callback) {
+    callback(this.currentInputMethod_);
+  },
+
+
+  /**
+   * Sets the current input method.
+   * @param {string} inputMethodId The input method to set.
+   * @param {function<>} callback Callback called on success.
+   */
+  setCurrentInputMethod(inputMethodId, callback) {
+    MockInputMethodPrivate.currentInputMethod_ = inputMethodId;
+    callback && callback();
+  },
+
+  // Methods for testing. //
+
+  /**
+   * Gets the current input method.
+   * @return {string}
+   */
+  getCurrentInputMethodForTest() {
+    return MockInputMethodPrivate.currentInputMethod_;
+  },
+};
\ No newline at end of file
diff --git a/chrome/browser/resources/chromeos/accessibility/common/testing/mock_language_settings_private.js b/chrome/browser/resources/chromeos/accessibility/common/testing/mock_language_settings_private.js
new file mode 100644
index 0000000..42207b7c
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/common/testing/mock_language_settings_private.js
@@ -0,0 +1,39 @@
+// 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.
+
+/*
+ * A mock chrome.languageSettingsPrivate API for tests.
+ */
+var MockLanguageSettingsPrivate = {
+  /** @private {array<string>} */
+  inputMethods: [],
+
+  // Methods from chrome.languageSettingsPrivate API. //
+
+  /**
+   * Adds an input method ID.
+   * @param {string} methodId
+   */
+  addInputMethod(methodId) {
+    MockLanguageSettingsPrivate.inputMethods.push(methodId);
+  },
+
+  removeInputMethod(methodId) {
+    const index = MockLanguageSettingsPrivate.inputMethods.indexOf(methodId);
+    if (index >= 0) {
+      MockLanguageSettingsPrivate.inputMethods.splice(index, 1);
+    }
+  },
+
+  // Methods for testing. //
+
+  /**
+   * Checks if an input method exists.
+   * @param {stromg} methodId
+   * @return {boolean} True if the method is present.
+   */
+  hasInputMethod(methodId) {
+    return MockLanguageSettingsPrivate.inputMethods.includes(methodId);
+  },
+};
\ No newline at end of file
diff --git a/chrome/browser/resources/new_tab_page/modules/cart/module.js b/chrome/browser/resources/new_tab_page/modules/cart/module.js
index 2934b0c..e1c8050 100644
--- a/chrome/browser/resources/new_tab_page/modules/cart/module.js
+++ b/chrome/browser/resources/new_tab_page/modules/cart/module.js
@@ -391,7 +391,7 @@
       return;
     }
     ChromeCartProxy.getInstance().handler.prepareForNavigation(
-        this.cartItems[index].cartUrl);
+        this.cartItems[index].cartUrl, /*isNavigating=*/ true);
     this.dispatchEvent(new Event('usage', {bubbles: true, composed: true}));
     chrome.metricsPrivate.recordSmallCount('NewTabPage.Carts.ClickCart', index);
   }
@@ -423,7 +423,7 @@
   onCartItemContextMenuClick_(e) {
     const index = this.$.cartItemRepeat.indexForElement(e.target);
     ChromeCartProxy.getInstance().handler.prepareForNavigation(
-        this.cartItems[index].cartUrl);
+        this.cartItems[index].cartUrl, /*isNavigating=*/ false);
   }
 }
 
diff --git a/chrome/browser/resources/settings/chromeos/os_apps_page/os_apps_page.html b/chrome/browser/resources/settings/chromeos/os_apps_page/os_apps_page.html
index 6913fc9..346f5b2 100644
--- a/chrome/browser/resources/settings/chromeos/os_apps_page/os_apps_page.html
+++ b/chrome/browser/resources/settings/chromeos/os_apps_page/os_apps_page.html
@@ -29,7 +29,8 @@
 <dom-module id="os-settings-apps-page">
   <template>
   <style include="settings-shared">
-    #appManagement {
+    #appManagement, 
+    #appNotifications {
         border-bottom: var(--cr-separator-line);
     }
   </style>
@@ -42,6 +43,13 @@
         on-click="onClickAppManagement_"
         role-description="$i18n{subpageArrowRoleDescription}">
       </cr-link-row>
+      <template is="dom-if" if="[[showAppNotificationsRow_]]">
+        <cr-link-row id="appNotifications"
+            label="$i18n{appNotificationsTitle}"
+            on-click="onClickAppNotifications_"
+            role-description="$i18n{subpageArrowRoleDescription}">
+        </cr-link-row>
+      </template>
       <template is="dom-if" if="[[showAndroidApps]]">
         <template is="dom-if" if="[[havePlayStoreApp]]" restamp>
           <div id="android-apps" class="settings-box first"
@@ -96,6 +104,12 @@
       </template>
     </div>
 
+    <!-- APP NOTIFICATIONS -->
+    <template is="dom-if" route-path="/app-notifications" no-search>
+      <settings-subpage page-title="$i18n{appNotificationsTitle}">
+      </settings-subpage>
+    </template>
+
     <!-- APP MANAGEMENT -->
     <template is="dom-if" route-path="/app-management" no-search>
       <settings-subpage
diff --git a/chrome/browser/resources/settings/chromeos/os_apps_page/os_apps_page.js b/chrome/browser/resources/settings/chromeos/os_apps_page/os_apps_page.js
index 3f64f05..123cc81 100644
--- a/chrome/browser/resources/settings/chromeos/os_apps_page/os_apps_page.js
+++ b/chrome/browser/resources/settings/chromeos/os_apps_page/os_apps_page.js
@@ -49,6 +49,17 @@
     showAndroidApps: Boolean,
 
     /**
+     * Whether the App Notifications page should be shown.
+     * @type {boolean}
+     */
+    showAppNotificationsRow_: {
+      type: Boolean,
+      value() {
+        return loadTimeData.getBoolean('showOsSettingsAppNotificationsRow');
+      },
+    },
+
+    /**
      * Show Plugin VM shared folders sub-page.
      * @type {boolean}
      */
@@ -137,6 +148,11 @@
     settings.Router.getInstance().navigateTo(settings.routes.APP_MANAGEMENT);
   },
 
+  /** @private */
+  onClickAppNotifications_() {
+    settings.Router.getInstance().navigateTo(settings.routes.APP_NOTIFICATIONS);
+  },
+
   /**
    * @param {!Event} event
    * @private
diff --git a/chrome/browser/resources/settings/chromeos/os_languages_page/add_input_methods_dialog.js b/chrome/browser/resources/settings/chromeos/os_languages_page/add_input_methods_dialog.js
index 9aaafb0a..d198d5cd 100644
--- a/chrome/browser/resources/settings/chromeos/os_languages_page/add_input_methods_dialog.js
+++ b/chrome/browser/resources/settings/chromeos/os_languages_page/add_input_methods_dialog.js
@@ -2,6 +2,11 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+// The IME ID for the Accessibility Common extension used by Dictation.
+/** @type {string} */
+const ACCESSIBILITY_COMMON_IME_ID =
+    '_ext_ime_egfdjlfmgnehecnclamagfafdccgfndpdictation';
+
 /**
  * @fileoverview 'os-settings-add-input-methods-dialog' is a dialog for
  * adding input methods.
@@ -92,6 +97,10 @@
       if (this.languageHelper.isInputMethodEnabled(inputMethod.id)) {
         return false;
       }
+      // Don't show the Dictation (Accessibility Common) extension in this list.
+      if (inputMethod.id === ACCESSIBILITY_COMMON_IME_ID) {
+        return false;
+      }
       // Show input methods whose tags match the query.
       return inputMethod.tags.some(
           tag => tag.toLocaleLowerCase().includes(this.lowercaseQueryString_));
diff --git a/chrome/browser/resources/settings/chromeos/os_languages_page/os_languages_section.js b/chrome/browser/resources/settings/chromeos/os_languages_page/os_languages_section.js
index d7556ed..e502692 100644
--- a/chrome/browser/resources/settings/chromeos/os_languages_page/os_languages_section.js
+++ b/chrome/browser/resources/settings/chromeos/os_languages_page/os_languages_section.js
@@ -2,6 +2,11 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+// The IME ID for the Accessibility Common extension used by Dictation.
+/** @type {string} */
+const ACCESSIBILITY_COMMON_IME_ID =
+    '_ext_ime_egfdjlfmgnehecnclamagfafdccgfndpdictation';
+
 /**
  * @fileoverview
  * 'os-settings-languages-section' is the top-level settings section for
@@ -106,6 +111,9 @@
    * @private
    */
   getInputMethodDisplayName_(id, languageHelper) {
+    if (id === ACCESSIBILITY_COMMON_IME_ID) {
+      return '';
+    }
     // LanguageHelper.getInputMethodDisplayName will throw an error if the ID
     // isn't found, such as when using CrOS on Linux.
     try {
diff --git a/chrome/browser/resources/settings/chromeos/os_route.js b/chrome/browser/resources/settings/chromeos/os_route.js
index dad20fe..821e00f 100644
--- a/chrome/browser/resources/settings/chromeos/os_route.js
+++ b/chrome/browser/resources/settings/chromeos/os_route.js
@@ -179,6 +179,12 @@
 
     // Apps section.
     r.APPS = createSection(r.BASIC, mojom.APPS_SECTION_PATH, Section.kApps);
+    r.APP_NOTIFICATIONS = createSubpage(
+        r.APPS, mojom.APP_NOTIFICATIONS_SUBPAGE_PATH,
+        Subpage.kAppNotifications);
+    r.APP_NOTIFICATIONS_DETAIL = createSubpage(
+        r.APP_NOTIFICATIONS, mojom.APP_DETAILS_SUBPAGE_PATH,
+        Subpage.kAppDetails);
     r.APP_MANAGEMENT = createSubpage(
         r.APPS, mojom.APP_MANAGEMENT_SUBPAGE_PATH, Subpage.kAppManagement);
     r.APP_MANAGEMENT_DETAIL = createSubpage(
diff --git a/chrome/browser/resources/settings/chromeos/os_settings_routes.js b/chrome/browser/resources/settings/chromeos/os_settings_routes.js
index fbee9a0..787a9bf 100644
--- a/chrome/browser/resources/settings/chromeos/os_settings_routes.js
+++ b/chrome/browser/resources/settings/chromeos/os_settings_routes.js
@@ -15,6 +15,7 @@
  *   ADVANCED: !settings.Route,
  *   AMBIENT_MODE: !settings.Route,
  *   AMBIENT_MODE_PHOTOS: !settings.Route,
+ *   APP_NOTIFICATIONS: !settings.Route,
  *   APP_MANAGEMENT: !settings.Route,
  *   APP_MANAGEMENT_DETAIL: !settings.Route,
  *   APP_MANAGEMENT_PLUGIN_VM_SHARED_PATHS: !settings.Route,
diff --git a/chrome/browser/resources/settings/languages_page/languages.js b/chrome/browser/resources/settings/languages_page/languages.js
index fb1cd784..df300e6 100644
--- a/chrome/browser/resources/settings/languages_page/languages.js
+++ b/chrome/browser/resources/settings/languages_page/languages.js
@@ -47,6 +47,13 @@
 // one in ui/base/ime/chromeos/extension_ime_util.h.
 const kArcImeLanguage = '_arc_ime_language_';
 
+// <if expr="chromeos">
+// The IME ID for the Accessibility Common extension used by Dictation.
+/** @type {string} */
+const ACCESSIBILITY_COMMON_IME_ID =
+    '_ext_ime_egfdjlfmgnehecnclamagfafdccgfndpdictation';
+// </if>
+
 let preferredLanguagesPrefName = 'intl.accept_languages';
 // <if expr="chromeos">
 preferredLanguagesPrefName = 'settings.language.preferred_languages';
@@ -1214,11 +1221,13 @@
             .value.split(','));
     this.enabledInputMethodSet_ = new Set(enabledInputMethodIds);
 
-    // Return only supported input methods.
+    // Return only supported input methods. Don't include the Dictation
+    // (Accessibility Common) input method.
     return enabledInputMethodIds
         .map(id => this.supportedInputMethodMap_.get(id))
         .filter(function(inputMethod) {
-          return !!inputMethod;
+          return !!inputMethod &&
+              inputMethod.id !== ACCESSIBILITY_COMMON_IME_ID;
         });
   },
 
diff --git a/chrome/browser/sessions/session_restore_browsertest.cc b/chrome/browser/sessions/session_restore_browsertest.cc
index 970bf38..1792702 100644
--- a/chrome/browser/sessions/session_restore_browsertest.cc
+++ b/chrome/browser/sessions/session_restore_browsertest.cc
@@ -2899,6 +2899,63 @@
   EXPECT_FALSE(back_observer.was_same_document());
 }
 
+IN_PROC_BROWSER_TEST_F(SessionRestoreTest, OmitFromSessionRestore) {
+  // Tests start with one browser window; navigate it to url 1.
+  ui_test_utils::NavigateToURLWithDisposition(
+      browser(), GetUrl1(), WindowOpenDisposition::CURRENT_TAB,
+      ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
+  ASSERT_EQ(1, browser()->tab_strip_model()->count());
+  EXPECT_EQ(
+      GetUrl1(),
+      browser()->tab_strip_model()->GetWebContentsAt(0)->GetLastCommittedURL());
+
+  // Make a second window; navigate it to url 2.
+  Browser* browser2 = CreateBrowser(browser()->profile());
+  ui_test_utils::NavigateToURLWithDisposition(
+      browser2, GetUrl2(), WindowOpenDisposition::CURRENT_TAB,
+      ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
+  ASSERT_EQ(1, browser2->tab_strip_model()->count());
+  EXPECT_EQ(
+      GetUrl2(),
+      browser2->tab_strip_model()->GetWebContentsAt(0)->GetLastCommittedURL());
+
+  // Make a third window that is omitted from session restore; navigate it to
+  // url 3.
+  Browser::CreateParams params(browser()->profile(), true);
+  params.omit_from_session_restore = true;
+  Browser* browser3 = Browser::Create(params);
+  content::WebContents* tab = chrome::AddSelectedTabWithURL(
+      browser3, GURL(GetUrl3()), ui::PAGE_TRANSITION_AUTO_TOPLEVEL);
+  content::TestNavigationObserver observer(tab);
+  observer.Wait();
+  ASSERT_EQ(1, browser3->tab_strip_model()->count());
+  EXPECT_EQ(
+      GetUrl3(),
+      browser3->tab_strip_model()->GetWebContentsAt(0)->GetLastCommittedURL());
+
+  // Simulate an exit by shutting down the session service. If we don't do this
+  // the first two window closes are treated as though the user closed the
+  // windows and won't be restored.
+  SessionServiceFactory::ShutdownForProfile(browser()->profile());
+
+  // Then close all the browsers and "restart" Chromium.
+  CloseBrowserSynchronously(browser3);
+  CloseBrowserSynchronously(browser2);
+  QuitBrowserAndRestore(browser());
+
+  // The first two browsers with url1 and url2 should be back, but the browser
+  // with url3 shouldn't.
+  ASSERT_EQ(2u, active_browser_list_->size());
+  EXPECT_EQ(GetUrl1(), active_browser_list_->get(0)
+                           ->tab_strip_model()
+                           ->GetWebContentsAt(0)
+                           ->GetLastCommittedURL());
+  EXPECT_EQ(GetUrl2(), active_browser_list_->get(1)
+                           ->tab_strip_model()
+                           ->GetWebContentsAt(0)
+                           ->GetLastCommittedURL());
+}
+
 #if BUILDFLAG(ENABLE_APP_SESSION_SERVICE)
 class AppSessionRestoreTest : public SessionRestoreTest {
  public:
diff --git a/chrome/browser/sessions/session_restore_browsertest_chromeos.cc b/chrome/browser/sessions/session_restore_browsertest_chromeos.cc
index 5b12424..e34252e 100644
--- a/chrome/browser/sessions/session_restore_browsertest_chromeos.cc
+++ b/chrome/browser/sessions/session_restore_browsertest_chromeos.cc
@@ -428,28 +428,6 @@
   EXPECT_NE(2u, minimized_count);
 }
 
-IN_PROC_BROWSER_TEST_F(SessionRestoreTestChromeOS, PRE_OmitTerminalApp) {
-  const std::string terminal_app_name =
-      web_app::GenerateApplicationNameFromAppId(
-          crostini::kCrostiniTerminalSystemAppId);
-  CreateBrowserWithParams(CreateParamsForApp(test_app_name1, true));
-  CreateBrowserWithParams(CreateParamsForApp(terminal_app_name, true));
-  TurnOnSessionRestore();
-}
-
-IN_PROC_BROWSER_TEST_F(SessionRestoreTestChromeOS, OmitTerminalApp) {
-  const std::string terminal_app_name =
-      web_app::GenerateApplicationNameFromAppId(
-          crostini::kCrostiniTerminalSystemAppId);
-  size_t total_count = 0;
-  for (auto* browser : *BrowserList::GetInstance()) {
-    ++total_count;
-    EXPECT_NE(terminal_app_name, browser->app_name());
-  }
-  // We should only count browser() and test_app_name1.
-  EXPECT_EQ(2u, total_count);
-}
-
 class SystemWebAppSessionRestoreTestChromeOS
     : public web_app::SystemWebAppManagerBrowserTest {
  public:
diff --git a/chrome/browser/sessions/session_service_base.cc b/chrome/browser/sessions/session_service_base.cc
index 692d7b1..74dfe65f 100644
--- a/chrome/browser/sessions/session_service_base.cc
+++ b/chrome/browser/sessions/session_service_base.cc
@@ -29,8 +29,6 @@
 #include "chrome/browser/ui/startup/startup_browser_creator.h"
 #include "chrome/browser/ui/tabs/tab_group.h"
 #include "chrome/browser/ui/tabs/tab_group_model.h"
-#include "chrome/browser/ui/web_applications/app_browser_controller.h"
-#include "chrome/browser/web_applications/components/web_app_helpers.h"
 #include "components/sessions/content/content_serialized_navigation_builder.h"
 #include "components/sessions/content/session_tab_helper.h"
 #include "components/sessions/core/command_storage_manager.h"
@@ -42,10 +40,6 @@
 #include "content/public/browser/notification_service.h"
 #include "content/public/browser/session_storage_namespace.h"
 
-#if BUILDFLAG(IS_CHROMEOS_ASH)
-#include "chrome/browser/ash/crostini/crostini_util.h"
-#endif
-
 #if defined(OS_MAC)
 #include "chrome/browser/app_controller_mac.h"
 #endif
@@ -658,26 +652,10 @@
 bool SessionServiceBase::ShouldTrackBrowser(Browser* browser) const {
   if (browser->profile() != profile())
     return false;
-#if BUILDFLAG(IS_CHROMEOS_ASH)
-  // Do not track Crostini apps or terminal.  Apps will fail since VMs are not
-  // restarted on restore, and we don't want terminal to force the VM to start.
-  if (web_app::GetAppIdFromApplicationName(browser->app_name()) ==
-      crostini::kCrostiniTerminalSystemAppId) {
-    return false;
-  }
 
-  // System Web App windows can't be properly restored without storing the app
-  // type. Until that is implemented we skip them for session restore.
-  // TODO(crbug.com/1003170): Enable session restore for System Web Apps.
-  if (browser->app_controller() &&
-      browser->app_controller()->is_for_system_web_app()) {
+  if (browser->omit_from_session_restore())
     return false;
-  }
 
-  // Don't track custom_tab browser. It doesn't need to be restored.
-  if (browser->is_type_custom_tab())
-    return false;
-#endif
   // Never track app popup windows that do not have a trusted source (i.e.
   // popup windows spawned by an app). If this logic changes, be sure to also
   // change SessionRestoreImpl::CreateRestoredBrowser().
diff --git a/chrome/browser/sessions/session_service_utils.cc b/chrome/browser/sessions/session_service_utils.cc
index 1fc759c..4d45f5e 100644
--- a/chrome/browser/sessions/session_service_utils.cc
+++ b/chrome/browser/sessions/session_service_utils.cc
@@ -21,9 +21,7 @@
       return sessions::SessionWindow::TYPE_APP_POPUP;
 #if BUILDFLAG(IS_CHROMEOS_ASH)
     case Browser::TYPE_CUSTOM_TAB:
-      // Session restore isn't supported for CUSTOM_TAB browser.
-      // This method must never be called for this type.
-      NOTREACHED();
+      return sessions::SessionWindow::TYPE_CUSTOM_TAB;
 #endif
   }
   NOTREACHED();
@@ -43,6 +41,10 @@
       return Browser::TYPE_DEVTOOLS;
     case sessions::SessionWindow::TYPE_APP_POPUP:
       return Browser::TYPE_APP_POPUP;
+#if BUILDFLAG(IS_CHROMEOS_ASH)
+    case sessions::SessionWindow::TYPE_CUSTOM_TAB:
+      return Browser::TYPE_CUSTOM_TAB;
+#endif
   }
   NOTREACHED();
   return Browser::TYPE_NORMAL;
diff --git a/chrome/browser/tracing/background_tracing_field_trial.cc b/chrome/browser/tracing/background_tracing_field_trial.cc
index b6040680..c529ee5 100644
--- a/chrome/browser/tracing/background_tracing_field_trial.cc
+++ b/chrome/browser/tracing/background_tracing_field_trial.cc
@@ -9,10 +9,13 @@
 
 #include "base/bind.h"
 #include "base/command_line.h"
+#include "base/feature_list.h"
 #include "base/files/file_path.h"
 #include "base/files/file_util.h"
 #include "base/json/json_reader.h"
 #include "base/metrics/field_trial.h"
+#include "base/task/task_traits.h"
+#include "base/task/thread_pool.h"
 #include "base/trace_event/trace_log.h"
 #include "build/build_config.h"
 #include "chrome/browser/browser_process.h"
@@ -25,17 +28,21 @@
 #include "net/base/network_change_notifier.h"
 #include "services/network/public/cpp/shared_url_loader_factory.h"
 #include "services/tracing/public/cpp/perfetto/trace_event_data_source.h"
+#include "services/tracing/public/cpp/tracing_features.h"
 #include "url/gurl.h"
 
 namespace tracing {
 
 namespace {
 
+using content::BackgroundTracingConfig;
+using content::BackgroundTracingManager;
+
 const char kBackgroundTracingFieldTrial[] = "BackgroundTracing";
 
 void OnBackgroundTracingUploadComplete(
     TraceCrashServiceUploader* uploader,
-    content::BackgroundTracingManager::FinishedProcessingCallback done_callback,
+    BackgroundTracingManager::FinishedProcessingCallback done_callback,
     bool success,
     const std::string& feedback) {
   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
@@ -45,7 +52,7 @@
 void BackgroundTracingUploadCallback(
     const std::string& upload_url,
     std::unique_ptr<std::string> file_contents,
-    content::BackgroundTracingManager::FinishedProcessingCallback callback) {
+    BackgroundTracingManager::FinishedProcessingCallback callback) {
   TraceCrashServiceUploader* uploader = new TraceCrashServiceUploader(
       g_browser_process->shared_url_loader_factory());
 
@@ -71,11 +78,49 @@
                      std::move(callback)));
 }
 
+bool BlockingWriteTraceToFile(const base::FilePath& output_file,
+                              std::unique_ptr<std::string> file_contents) {
+  if (base::WriteFile(output_file, *file_contents)) {
+    LOG(ERROR) << "Background trace written to "
+               << output_file.LossyDisplayName();
+    return true;
+  }
+  LOG(ERROR) << "Failed to write background trace to "
+             << output_file.LossyDisplayName();
+  return false;
+}
+
+void WriteTraceToFile(
+    const base::FilePath& output_file,
+    std::unique_ptr<std::string> file_contents,
+    BackgroundTracingManager::FinishedProcessingCallback done_callback) {
+  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+  base::ThreadPool::PostTaskAndReplyWithResult(
+      FROM_HERE, {base::MayBlock()},
+      base::BindOnce(&BlockingWriteTraceToFile, output_file,
+                     std::move(file_contents)),
+      std::move(done_callback));
+}
+
 void SetupBackgroundTracingFromConfigFile(const base::FilePath& config_file,
+                                          const base::FilePath& output_file,
                                           const std::string& upload_url) {
+  // Exactly one destination should be given.
+  BackgroundTracingManager::ReceiveCallback receive_callback;
+  if (!output_file.empty()) {
+    DCHECK(upload_url.empty());
+    receive_callback = base::BindRepeating(&WriteTraceToFile, output_file);
+  } else if (!upload_url.empty()) {
+    DCHECK(output_file.empty());
+    receive_callback =
+        base::BindRepeating(&BackgroundTracingUploadCallback, upload_url);
+  } else {
+    NOTREACHED();
+    return;
+  }
+
   std::string config_text;
-  if (upload_url.empty() ||
-      !base::ReadFileToString(config_file, &config_text) ||
+  if (!base::ReadFileToString(config_file, &config_text) ||
       config_text.empty()) {
     LOG(ERROR) << "Failed to read background tracing config file "
                << config_file.value();
@@ -97,41 +142,91 @@
     return;
   }
 
-  std::unique_ptr<content::BackgroundTracingConfig> config =
-      content::BackgroundTracingConfig::FromDict(dict);
+  std::unique_ptr<BackgroundTracingConfig> config =
+      BackgroundTracingConfig::FromDict(dict);
   if (!config) {
     LOG(ERROR) << "Background tracing config dict has invalid contents";
     return;
   }
-  content::BackgroundTracingManager::GetInstance()->SetActiveScenario(
-      std::move(config),
-      base::BindRepeating(&BackgroundTracingUploadCallback, upload_url),
-      content::BackgroundTracingManager::NO_DATA_FILTERING);
+
+  // Consider all tracing set up with a local config file to have local output
+  // for metrics, since even if `upload_url` is given it will point to a local
+  // test server and not a production upload endpoint.
+  BackgroundTracingManager::GetInstance()->SetActiveScenarioWithReceiveCallback(
+      std::move(config), std::move(receive_callback),
+      BackgroundTracingManager::NO_DATA_FILTERING,
+      /*local_output=*/true);
 }
 
 }  // namespace
 
-void SetupBackgroundTracingFieldTrial() {
+BackgroundTracingSetupMode GetBackgroundTracingSetupMode() {
   auto* command_line = base::CommandLine::ForCurrentProcess();
-  if (command_line->HasSwitch(switches::kEnableBackgroundTracing) &&
-      command_line->HasSwitch(switches::kTraceUploadURL)) {
-    tracing::SetupBackgroundTracingFromConfigFile(
-        command_line->GetSwitchValuePath(switches::kEnableBackgroundTracing),
-        command_line->GetSwitchValueASCII(switches::kTraceUploadURL));
-    return;
+  if (!command_line->HasSwitch(switches::kEnableBackgroundTracing))
+    return BackgroundTracingSetupMode::kFromFieldTrial;
+
+  if (command_line->GetSwitchValueNative(switches::kEnableBackgroundTracing)
+          .empty()) {
+    LOG(ERROR) << "--enable-background-tracing needs a config file path";
+    return BackgroundTracingSetupMode::kDisabledInvalidCommandLine;
   }
 
-  std::unique_ptr<content::BackgroundTracingConfig> config =
-      content::BackgroundTracingManager::GetInstance()
-          ->GetBackgroundTracingConfig(kBackgroundTracingFieldTrial);
+  const auto output_file = command_line->GetSwitchValueNative(
+      switches::kBackgroundTracingOutputFile);
+  const auto upload_url =
+      command_line->GetSwitchValueNative(switches::kTraceUploadURL);
+  if ((output_file.empty() && upload_url.empty()) ||
+      (!output_file.empty() && !upload_url.empty())) {
+    LOG(ERROR) << "Specify one of --background-tracing-output-file or "
+                  "--trace-upload-url";
+    return BackgroundTracingSetupMode::kDisabledInvalidCommandLine;
+  }
 
-  content::BackgroundTracingManager::GetInstance()->SetActiveScenario(
-      std::move(config),
-      base::BindRepeating(
-          &BackgroundTracingUploadCallback,
-          content::BackgroundTracingManager::GetInstance()
-              ->GetBackgroundTracingUploadUrl(kBackgroundTracingFieldTrial)),
-      content::BackgroundTracingManager::ANONYMIZE_DATA);
+  if (!upload_url.empty() &&
+      base::FeatureList::IsEnabled(features::kBackgroundTracingProtoOutput)) {
+    LOG(ERROR) << "--trace-upload-url can only be used with legacy JSON traces";
+    return BackgroundTracingSetupMode::kDisabledInvalidCommandLine;
+  }
+
+  return BackgroundTracingSetupMode::kFromConfigFile;
+}
+
+void SetupBackgroundTracingFieldTrial() {
+  switch (GetBackgroundTracingSetupMode()) {
+    case BackgroundTracingSetupMode::kDisabledInvalidCommandLine:
+      // Abort setup.
+      return;
+    case BackgroundTracingSetupMode::kFromConfigFile: {
+      auto* command_line = base::CommandLine::ForCurrentProcess();
+      SetupBackgroundTracingFromConfigFile(
+          command_line->GetSwitchValuePath(switches::kEnableBackgroundTracing),
+          command_line->GetSwitchValuePath(
+              switches::kBackgroundTracingOutputFile),
+          command_line->GetSwitchValueASCII(switches::kTraceUploadURL));
+      return;
+    }
+    case BackgroundTracingSetupMode::kFromFieldTrial:
+      // Fall through.
+      break;
+  }
+
+  auto* manager = BackgroundTracingManager::GetInstance();
+  std::unique_ptr<BackgroundTracingConfig> config =
+      manager->GetBackgroundTracingConfig(kBackgroundTracingFieldTrial);
+
+  if (base::FeatureList::IsEnabled(features::kBackgroundTracingProtoOutput)) {
+    manager->SetActiveScenario(std::move(config),
+                               BackgroundTracingManager::ANONYMIZE_DATA);
+  } else {
+    // JSON traces must be uploaded through BackgroundTracingUploadCallback.
+    manager->SetActiveScenarioWithReceiveCallback(
+        std::move(config),
+        base::BindRepeating(&BackgroundTracingUploadCallback,
+                            manager->GetBackgroundTracingUploadUrl(
+                                kBackgroundTracingFieldTrial)),
+        BackgroundTracingManager::ANONYMIZE_DATA,
+        /*local_output=*/false);
+  }
 }
 
 }  // namespace tracing
diff --git a/chrome/browser/tracing/background_tracing_field_trial.h b/chrome/browser/tracing/background_tracing_field_trial.h
index 94a0448..7db64e2 100644
--- a/chrome/browser/tracing/background_tracing_field_trial.h
+++ b/chrome/browser/tracing/background_tracing_field_trial.h
@@ -7,6 +7,20 @@
 
 namespace tracing {
 
+enum class BackgroundTracingSetupMode {
+  // Background tracing config comes from a field trial.
+  kFromFieldTrial,
+
+  // Background tracing config comes from a config file passed on the
+  // command-line (for local testing).
+  kFromConfigFile,
+
+  // Background tracing is disabled due to invalid command-line flags.
+  kDisabledInvalidCommandLine,
+};
+
+BackgroundTracingSetupMode GetBackgroundTracingSetupMode();
+
 void SetupBackgroundTracingFieldTrial();
 
 }  // namespace tracing
diff --git a/chrome/browser/tracing/background_tracing_field_trial_unittest.cc b/chrome/browser/tracing/background_tracing_field_trial_unittest.cc
index 0b4e8f1..5c0c5dc 100644
--- a/chrome/browser/tracing/background_tracing_field_trial_unittest.cc
+++ b/chrome/browser/tracing/background_tracing_field_trial_unittest.cc
@@ -4,16 +4,20 @@
 
 #include "chrome/browser/tracing/background_tracing_field_trial.h"
 
+#include <vector>
+
 #include "base/files/file_util.h"
 #include "base/metrics/field_trial.h"
 #include "base/metrics/field_trial_params.h"
 #include "base/test/scoped_command_line.h"
+#include "base/test/scoped_feature_list.h"
 #include "chrome/test/base/testing_browser_process.h"
 #include "chrome/test/base/testing_profile_manager.h"
 #include "components/tracing/common/tracing_switches.h"
 #include "content/public/browser/background_tracing_config.h"
 #include "content/public/browser/background_tracing_manager.h"
 #include "content/public/test/browser_task_environment.h"
+#include "services/tracing/public/cpp/tracing_features.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 class BackgroundTracingTest : public testing::Test {
@@ -33,7 +37,6 @@
 const char kTestConfig[] = "test";
 bool g_test_config_loaded = false;
 
-const char kUploadUrl[] = "http://localhost:8080";
 const char kInvalidTracingConfig[] = "{][}";
 const char kValidTracingConfig[] = R"(
   {
@@ -56,8 +59,84 @@
   return config;
 }
 
+using tracing::BackgroundTracingSetupMode;
+
+struct SetupModeParams {
+  const char* enable_background_tracing = nullptr;
+  const char* trace_output_file = nullptr;
+  const char* trace_upload_url = nullptr;
+  BackgroundTracingSetupMode expected_mode_json;
+  BackgroundTracingSetupMode expected_mode_proto;
+};
+
 }  // namespace
 
+TEST_F(BackgroundTracingTest, GetBackgroundTracingSetupMode) {
+  const std::vector<const SetupModeParams> kParams = {
+      // No config file param.
+      {nullptr, nullptr, nullptr, BackgroundTracingSetupMode::kFromFieldTrial,
+       BackgroundTracingSetupMode::kFromFieldTrial},
+      // Empty config filename.
+      {"", "output_file.gz", nullptr,
+       BackgroundTracingSetupMode::kDisabledInvalidCommandLine,
+       BackgroundTracingSetupMode::kDisabledInvalidCommandLine},
+      // No output location switches.
+      {"config.json", nullptr, nullptr,
+       BackgroundTracingSetupMode::kDisabledInvalidCommandLine,
+       BackgroundTracingSetupMode::kDisabledInvalidCommandLine},
+      // Empty output location switches.
+      {"config.json", "", "",
+       BackgroundTracingSetupMode::kDisabledInvalidCommandLine,
+       BackgroundTracingSetupMode::kDisabledInvalidCommandLine},
+      // Both output location switches.
+      {"config.json", "output_file.gz", "http://localhost:8080",
+       BackgroundTracingSetupMode::kDisabledInvalidCommandLine,
+       BackgroundTracingSetupMode::kDisabledInvalidCommandLine},
+      // url is only valid for legacy JSON traces.
+      {"config.json", "", "http://localhost:8080",
+       BackgroundTracingSetupMode::kFromConfigFile,
+       BackgroundTracingSetupMode::kDisabledInvalidCommandLine},
+      // file is valid for both JSON and proto traces.
+      {"config.json", "output_file.gz", "",
+       BackgroundTracingSetupMode::kFromConfigFile,
+       BackgroundTracingSetupMode::kFromConfigFile},
+  };
+
+  for (bool enable_proto_traces : {true, false}) {
+    for (const SetupModeParams& params : kParams) {
+      SCOPED_TRACE(::testing::Message()
+                   << "enable_background_tracing "
+                   << params.enable_background_tracing << " trace_output_file "
+                   << params.trace_output_file << " trace_upload_url "
+                   << params.trace_upload_url << " enable_proto_traces "
+                   << enable_proto_traces);
+      base::test::ScopedFeatureList feature_list;
+      feature_list.InitWithFeatureState(features::kBackgroundTracingProtoOutput,
+                                        enable_proto_traces);
+
+      base::test::ScopedCommandLine scoped_command_line;
+      base::CommandLine* command_line =
+          scoped_command_line.GetProcessCommandLine();
+      if (params.enable_background_tracing) {
+        command_line->AppendSwitchASCII(switches::kEnableBackgroundTracing,
+                                        params.enable_background_tracing);
+      }
+      if (params.trace_output_file) {
+        command_line->AppendSwitchASCII(switches::kBackgroundTracingOutputFile,
+                                        params.trace_output_file);
+      }
+      if (params.trace_upload_url) {
+        command_line->AppendSwitchASCII(switches::kTraceUploadURL,
+                                        params.trace_upload_url);
+      }
+
+      EXPECT_EQ(tracing::GetBackgroundTracingSetupMode(),
+                enable_proto_traces ? params.expected_mode_proto
+                                    : params.expected_mode_json);
+    }
+  }
+}
+
 TEST_F(BackgroundTracingTest, SetupBackgroundTracingFieldTrial) {
   const std::string kTrialName = "BackgroundTracing";
   const std::string kExperimentName = "SlowStart";
@@ -75,6 +154,8 @@
   content::BackgroundTracingManager::GetInstance()
       ->SetConfigTextFilterForTesting(base::BindRepeating(&CheckConfig));
 
+  ASSERT_EQ(tracing::GetBackgroundTracingSetupMode(),
+            BackgroundTracingSetupMode::kFromFieldTrial);
   tracing::SetupBackgroundTracingFieldTrial();
   EXPECT_TRUE(g_test_config_loaded);
 }
@@ -88,9 +169,11 @@
 
   base::test::ScopedCommandLine scoped_command_line;
   base::CommandLine* command_line = scoped_command_line.GetProcessCommandLine();
-  command_line->AppendSwitchASCII(switches::kTraceUploadURL, kUploadUrl);
+  command_line->AppendSwitchASCII(switches::kBackgroundTracingOutputFile, "");
   command_line->AppendSwitchASCII(switches::kEnableBackgroundTracing, "");
 
+  ASSERT_EQ(tracing::GetBackgroundTracingSetupMode(),
+            BackgroundTracingSetupMode::kDisabledInvalidCommandLine);
   tracing::SetupBackgroundTracingFieldTrial();
   EXPECT_FALSE(
       content::BackgroundTracingManager::GetInstance()->HasActiveScenario());
@@ -112,9 +195,13 @@
 
   base::test::ScopedCommandLine scoped_command_line;
   base::CommandLine* command_line = scoped_command_line.GetProcessCommandLine();
-  command_line->AppendSwitchASCII(switches::kTraceUploadURL, kUploadUrl);
+  command_line->AppendSwitchPath(
+      switches::kBackgroundTracingOutputFile,
+      temp_dir.GetPath().AppendASCII("test_trace.perfetto.gz"));
   command_line->AppendSwitchPath(switches::kEnableBackgroundTracing, file_path);
 
+  ASSERT_EQ(tracing::GetBackgroundTracingSetupMode(),
+            BackgroundTracingSetupMode::kFromConfigFile);
   tracing::SetupBackgroundTracingFieldTrial();
   EXPECT_FALSE(
       content::BackgroundTracingManager::GetInstance()->HasActiveScenario());
@@ -133,9 +220,13 @@
 
   base::test::ScopedCommandLine scoped_command_line;
   base::CommandLine* command_line = scoped_command_line.GetProcessCommandLine();
-  command_line->AppendSwitchASCII(switches::kTraceUploadURL, kUploadUrl);
+  command_line->AppendSwitchPath(
+      switches::kBackgroundTracingOutputFile,
+      temp_dir.GetPath().AppendASCII("test_trace.perfetto.gz"));
   command_line->AppendSwitchPath(switches::kEnableBackgroundTracing, file_path);
 
+  ASSERT_EQ(tracing::GetBackgroundTracingSetupMode(),
+            BackgroundTracingSetupMode::kFromConfigFile);
   tracing::SetupBackgroundTracingFieldTrial();
   EXPECT_TRUE(
       content::BackgroundTracingManager::GetInstance()->HasActiveScenario());
diff --git a/chrome/browser/tracing/background_tracing_metrics_provider_unittest.cc b/chrome/browser/tracing/background_tracing_metrics_provider_unittest.cc
index cf105916..193fcb1 100644
--- a/chrome/browser/tracing/background_tracing_metrics_provider_unittest.cc
+++ b/chrome/browser/tracing/background_tracing_metrics_provider_unittest.cc
@@ -46,9 +46,6 @@
     ASSERT_TRUE(
         content::BackgroundTracingManager::GetInstance()->SetActiveScenario(
             std::move(config),
-            base::BindRepeating([](std::unique_ptr<std::string>,
-                                   content::BackgroundTracingManager::
-                                       FinishedProcessingCallback) {}),
             content::BackgroundTracingManager::ANONYMIZE_DATA));
   }
 
diff --git a/chrome/browser/tracing/chrome_tracing_delegate.cc b/chrome/browser/tracing/chrome_tracing_delegate.cc
index 06cf70f..6d31725 100644
--- a/chrome/browser/tracing/chrome_tracing_delegate.cc
+++ b/chrome/browser/tracing/chrome_tracing_delegate.cc
@@ -19,13 +19,13 @@
 #include "chrome/browser/browser_process.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/browser/tracing/background_tracing_field_trial.h"
 #include "chrome/browser/tracing/crash_service_uploader.h"
 #include "chrome/browser/ui/browser_otr_state.h"
 #include "chrome/common/pref_names.h"
 #include "components/metrics/metrics_pref_names.h"
 #include "components/prefs/pref_registry_simple.h"
 #include "components/prefs/pref_service.h"
-#include "components/tracing/common/tracing_switches.h"
 #include "components/variations/active_field_trials.h"
 #include "components/version_info/version_info.h"
 #include "content/public/browser/background_tracing_config.h"
@@ -138,9 +138,8 @@
                            bool is_crash_scenario) {
   // If the background tracing is specified on the command-line, we allow
   // any scenario to be traced.
-  auto* command_line = base::CommandLine::ForCurrentProcess();
-  if (command_line->HasSwitch(switches::kEnableBackgroundTracing) &&
-      command_line->HasSwitch(switches::kTraceUploadURL)) {
+  if (tracing::GetBackgroundTracingSetupMode() ==
+      tracing::BackgroundTracingSetupMode::kFromConfigFile) {
     return true;
   }
 
diff --git a/chrome/browser/tracing/chrome_tracing_delegate_browsertest.cc b/chrome/browser/tracing/chrome_tracing_delegate_browsertest.cc
index d2247a3..0a33a0a 100644
--- a/chrome/browser/tracing/chrome_tracing_delegate_browsertest.cc
+++ b/chrome/browser/tracing/chrome_tracing_delegate_browsertest.cc
@@ -68,13 +68,22 @@
         content::BackgroundTracingConfig::FromDict(&dict));
 
     DCHECK(config);
+    // Proto output is uploaded through
+    // BackgroundTracingManager::SetTraceToUpload, with no ReceiveCallback.
+    if (base::FeatureList::IsEnabled(features::kBackgroundTracingProtoOutput)) {
+      return content::BackgroundTracingManager::GetInstance()
+          ->SetActiveScenario(std::move(config), data_filtering);
+    }
+
+    // Legacy JSON output needs a receive callback.
     wait_for_upload_ = std::make_unique<base::RunLoop>();
     content::BackgroundTracingManager::ReceiveCallback receive_callback =
         base::BindRepeating(&ChromeTracingDelegateBrowserTest::OnUpload,
                             base::Unretained(this));
 
-    return content::BackgroundTracingManager::GetInstance()->SetActiveScenario(
-        std::move(config), std::move(receive_callback), data_filtering);
+    return content::BackgroundTracingManager::GetInstance()
+        ->SetActiveScenarioWithReceiveCallback(
+            std::move(config), std::move(receive_callback), data_filtering);
   }
 
   void TriggerPreemptiveScenario(
@@ -94,18 +103,19 @@
   }
 
   void WaitForUpload() {
-    if (base::FeatureList::IsEnabled(features::kBackgroundTracingProtoOutput)) {
-      while (!content::BackgroundTracingManager::GetInstance()
-                  ->HasTraceToUpload()) {
-        base::RunLoop().RunUntilIdle();
-      }
-      EXPECT_FALSE(content::BackgroundTracingManager::GetInstance()
-                       ->GetLatestTraceToUpload()
-                       .empty());
-      receive_count_++;
-    } else {
+    if (wait_for_upload_) {
+      // Wait for the ReceiveCallback to quit this RunLoop.
       wait_for_upload_->Run();
+      return;
     }
+
+    // No ReceiveCallback set, so wait for SetTraceToUpload to be called.
+    auto* manager = content::BackgroundTracingManager::GetInstance();
+    while (!manager->HasTraceToUpload()) {
+      base::RunLoop().RunUntilIdle();
+    }
+    EXPECT_FALSE(manager->GetLatestTraceToUpload().empty());
+    receive_count_++;
   }
 
   int get_receive_count() const { return receive_count_; }
@@ -124,8 +134,10 @@
 
     content::GetUIThreadTaskRunner({})->PostTask(
         FROM_HERE, base::BindOnce(std::move(done_callback), true));
-    content::GetUIThreadTaskRunner({})->PostTask(
-        FROM_HERE, wait_for_upload_->QuitClosure());
+    if (wait_for_upload_) {
+      content::GetUIThreadTaskRunner({})->PostTask(
+          FROM_HERE, wait_for_upload_->QuitClosure());
+    }
   }
 
   void OnStartedFinalizing(bool success) {
diff --git a/chrome/browser/ui/android/strings/android_chrome_strings.grd b/chrome/browser/ui/android/strings/android_chrome_strings.grd
index 03e500d..fb566d8 100644
--- a/chrome/browser/ui/android/strings/android_chrome_strings.grd
+++ b/chrome/browser/ui/android/strings/android_chrome_strings.grd
@@ -4785,6 +4785,12 @@
       <message name="IDS_CONTENT_CREATION_NOTE_FILENAME_PREFIX" desc="Prefix used to name files when saving a Note's image representation. Underscores need to be used in place of whitespaces to avoid breaking the file path. Random numbers will be appended to this suffix to guarantee uniqueness.">
         chrome_stylized_highlight_
       </message>
+      <message name="IDS_CONTENT_CREATION_NOTE_TEMPLATE_SELECTED" desc="Announcment string for accessibility when selected template changes.">
+        <ph name="TEMPLATE_TITLE">%1$s<ex>Classic</ex> template selected</ph>
+      </message>
+      <message name="IDS_CONTENT_CREATION_NOTE_DIALOG_DESCRIPTION" desc="Accessibility information for note template picking dialog.">
+        Select a template for your highlight
+      </message>
     </messages>
   </release>
 </grit>
diff --git a/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_CONTENT_CREATION_NOTE_DIALOG_DESCRIPTION.png.sha1 b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_CONTENT_CREATION_NOTE_DIALOG_DESCRIPTION.png.sha1
new file mode 100644
index 0000000..e8ce1c2
--- /dev/null
+++ b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_CONTENT_CREATION_NOTE_DIALOG_DESCRIPTION.png.sha1
@@ -0,0 +1 @@
+b23f690fb6297233c054a27788c60005e8cd6dc2
\ No newline at end of file
diff --git a/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_CONTENT_CREATION_NOTE_TEMPLATE_SELECTED.png.sha1 b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_CONTENT_CREATION_NOTE_TEMPLATE_SELECTED.png.sha1
new file mode 100644
index 0000000..e8ce1c2
--- /dev/null
+++ b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_CONTENT_CREATION_NOTE_TEMPLATE_SELECTED.png.sha1
@@ -0,0 +1 @@
+b23f690fb6297233c054a27788c60005e8cd6dc2
\ No newline at end of file
diff --git a/chrome/browser/ui/app_list/arc/arc_app_utils.cc b/chrome/browser/ui/app_list/arc/arc_app_utils.cc
index bfeb92d..01125ee8 100644
--- a/chrome/browser/ui/app_list/arc/arc_app_utils.cc
+++ b/chrome/browser/ui/app_list/arc/arc_app_utils.cc
@@ -97,7 +97,7 @@
 
 constexpr char const* kAppIdsHiddenInLauncher[] = {
     kAndroidClockAppId, kSettingsAppId, kAndroidFilesAppId,
-    kAndroidContactsAppId};
+    kAndroidContactsAppId, kPlayGamesAppId};
 
 // Returns true if |event_flags| came from a mouse or touch event.
 bool IsMouseOrTouchEventFromFlags(int event_flags) {
diff --git a/chrome/browser/ui/ash/chrome_new_window_client.cc b/chrome/browser/ui/ash/chrome_new_window_client.cc
index 6ba768e..b9ad1e518 100644
--- a/chrome/browser/ui/ash/chrome_new_window_client.cc
+++ b/chrome/browser/ui/ash/chrome_new_window_client.cc
@@ -650,8 +650,10 @@
   // |custom_tab_browser| will be destroyed when its tab strip becomes empty,
   // either due to the user opening the custom tab page in a tabbed browser or
   // because of the CustomTabSessionImpl object getting destroyed.
-  auto* custom_tab_browser = Browser::Create(Browser::CreateParams(
-      Browser::TYPE_CUSTOM_TAB, profile, /* user_gesture= */ true));
+  Browser::CreateParams params(Browser::TYPE_CUSTOM_TAB, profile,
+                               /* user_gesture= */ true);
+  params.omit_from_session_restore = true;
+  auto* custom_tab_browser = Browser::Create(params);
 
   custom_tab_browser->tab_strip_model()->AppendWebContents(
       std::move(web_contents), /* foreground= */ true);
diff --git a/chrome/browser/ui/ash/holding_space/holding_space_client_impl_browsertest.cc b/chrome/browser/ui/ash/holding_space/holding_space_client_impl_browsertest.cc
index d394f99..0f39495 100644
--- a/chrome/browser/ui/ash/holding_space/holding_space_client_impl_browsertest.cc
+++ b/chrome/browser/ui/ash/holding_space/holding_space_client_impl_browsertest.cc
@@ -319,7 +319,7 @@
   // same text and file path as the original download holding space item.
   HoldingSpaceItem* pinned_file_item = holding_space_model->items()[1].get();
   EXPECT_EQ(pinned_file_item->type(), HoldingSpaceItem::Type::kPinnedFile);
-  EXPECT_EQ(download_item->text(), pinned_file_item->text());
+  EXPECT_EQ(download_item->GetText(), pinned_file_item->GetText());
   EXPECT_EQ(download_item->file_path(), pinned_file_item->file_path());
 }
 
diff --git a/chrome/browser/ui/ash/holding_space/holding_space_downloads_delegate.cc b/chrome/browser/ui/ash/holding_space/holding_space_downloads_delegate.cc
index ed3b084..42a7833 100644
--- a/chrome/browser/ui/ash/holding_space/holding_space_downloads_delegate.cc
+++ b/chrome/browser/ui/ash/holding_space/holding_space_downloads_delegate.cc
@@ -73,12 +73,6 @@
   // invoked in direct response to an explicit user action.
   void Resume() { download_item_->Resume(/*from_user=*/true); }
 
-  // Returns the number of bytes received from the underlying `download_item`.
-  absl::optional<int64_t> GetBytesReceived() const {
-    const int64_t bytes = download_item_->GetReceivedBytes();
-    return bytes >= 0 ? absl::make_optional(bytes) : absl::nullopt;
-  }
-
   // Returns the file path associated with the underlying `download_item_`.
   // NOTE: The file path may be empty before a target file path has been picked.
   const base::FilePath& GetFilePath() const {
@@ -392,7 +386,6 @@
       ->SetBackingFile(in_progress_download->GetFilePath(),
                        holding_space_util::ResolveFileSystemUrl(
                            profile(), in_progress_download->GetFilePath()))
-      .SetCurrentSizeInBytes(in_progress_download->GetBytesReceived())
       .SetPaused(in_progress_download->IsPaused())
       .SetProgress(in_progress_download->GetProgress());
 }
diff --git a/chrome/browser/ui/ash/holding_space/holding_space_keyed_service_unittest.cc b/chrome/browser/ui/ash/holding_space/holding_space_keyed_service_unittest.cc
index f05b6fa..534ef1f 100644
--- a/chrome/browser/ui/ash/holding_space/holding_space_keyed_service_unittest.cc
+++ b/chrome/browser/ui/ash/holding_space/holding_space_keyed_service_unittest.cc
@@ -811,7 +811,7 @@
     // Verify that the holding space item has been updated in place.
     ASSERT_EQ(holding_space_item->file_path(), new_file_path);
     ASSERT_EQ(holding_space_item->file_system_url(), new_file_path_url);
-    ASSERT_EQ(holding_space_item->text(),
+    ASSERT_EQ(holding_space_item->GetText(),
               new_file_path.BaseName().LossyDisplayName());
 
     // Verify that persistence has been updated.
@@ -846,7 +846,7 @@
     // Verify that the holding space item has been updated in place.
     ASSERT_EQ(holding_space_item->file_path(), new_file_path);
     ASSERT_EQ(holding_space_item->file_system_url(), new_file_path_url);
-    ASSERT_EQ(holding_space_item->text(),
+    ASSERT_EQ(holding_space_item->GetText(),
               new_file_path.BaseName().LossyDisplayName());
 
     // Verify that persistence has been updated.
@@ -1234,7 +1234,7 @@
 
     EXPECT_EQ(item->id(), restored_item->id());
     EXPECT_EQ(item->type(), restored_item->type());
-    EXPECT_EQ(item->text(), restored_item->text());
+    EXPECT_EQ(item->GetText(), restored_item->GetText());
     EXPECT_EQ(item->file_path(), item->file_path());
     // NOTE: `restored_item` was created with a fake file system URL (as it
     // could not be properly resolved at the time of item creation).
@@ -1361,7 +1361,7 @@
 
     EXPECT_EQ(item->id(), restored_item->id());
     EXPECT_EQ(item->type(), restored_item->type());
-    EXPECT_EQ(item->text(), restored_item->text());
+    EXPECT_EQ(item->GetText(), restored_item->GetText());
     EXPECT_EQ(item->file_path(), item->file_path());
     // NOTE: `restored_item` was created with a fake file system URL (as it
     // could not be properly resolved at the time of item creation).
@@ -1480,7 +1480,7 @@
 
     EXPECT_EQ(item->id(), restored_item->id());
     EXPECT_EQ(item->type(), restored_item->type());
-    EXPECT_EQ(item->text(), restored_item->text());
+    EXPECT_EQ(item->GetText(), restored_item->GetText());
     EXPECT_EQ(item->file_path(), item->file_path());
     // NOTE: `restored_item` was created with a fake file system URL (as it
     // could not be properly resolved at the time of item creation).
@@ -1906,7 +1906,7 @@
   // Verify holding space `item` metadata.
   HoldingSpaceItem* const item = model->items()[0].get();
   EXPECT_EQ(item->type(), GetType());
-  EXPECT_EQ(item->text(), file_path.BaseName().LossyDisplayName());
+  EXPECT_EQ(item->GetText(), file_path.BaseName().LossyDisplayName());
   EXPECT_EQ(item->file_path(), file_path);
   EXPECT_EQ(item->file_system_url(),
             holding_space_util::ResolveFileSystemUrl(profile, file_path));
@@ -2044,7 +2044,7 @@
   EXPECT_EQ(item_1_virtual_path,
             GetVirtualPathFromUrl(item_1->file_system_url(),
                                   downloads_mount->name()));
-  EXPECT_EQ(u"File 1.png", item_1->text());
+  EXPECT_EQ(u"File 1.png", item_1->GetText());
 
   const HoldingSpaceItem* item_2 = model->items()[1].get();
   EXPECT_EQ(item_2_full_path, item_2->file_path());
@@ -2060,7 +2060,7 @@
   EXPECT_EQ(item_2_virtual_path,
             GetVirtualPathFromUrl(item_2->file_system_url(),
                                   downloads_mount->name()));
-  EXPECT_EQ(u"File 2.png", item_2->text());
+  EXPECT_EQ(u"File 2.png", item_2->GetText());
 }
 
 // Base class for tests of print-to-PDF integration. Parameterized by whether
diff --git a/chrome/browser/ui/autofill/chrome_autofill_client.cc b/chrome/browser/ui/autofill/chrome_autofill_client.cc
index 56b2be3..74a49e4 100644
--- a/chrome/browser/ui/autofill/chrome_autofill_client.cc
+++ b/chrome/browser/ui/autofill/chrome_autofill_client.cc
@@ -715,9 +715,15 @@
 #endif
 }
 
-void ChromeAutofillClient::OnVirtualCardFetched(const CreditCard* credit_card,
+void ChromeAutofillClient::OnVirtualCardFetched(CreditCardFetchResult result,
+                                                const CreditCard* credit_card,
                                                 const std::u16string& cvc,
                                                 const gfx::Image& card_image) {
+  if (result != CreditCardFetchResult::kSuccess) {
+    // TODO(crbug.com/1196021): Shows the error dialog.
+    return;
+  }
+
   GetFormDataImporter()->CacheFetchedVirtualCard(credit_card->LastFourDigits());
 #if defined(OS_ANDROID)
   (new AutofillSnackbarControllerImpl(web_contents()))->Show();
diff --git a/chrome/browser/ui/autofill/chrome_autofill_client.h b/chrome/browser/ui/autofill/chrome_autofill_client.h
index cd00185..b67cec50 100644
--- a/chrome/browser/ui/autofill/chrome_autofill_client.h
+++ b/chrome/browser/ui/autofill/chrome_autofill_client.h
@@ -150,7 +150,8 @@
   void HideAutofillPopup(PopupHidingReason reason) override;
   void ShowOfferNotificationIfApplicable(
       const AutofillOfferData* offer) override;
-  void OnVirtualCardFetched(const CreditCard* card,
+  void OnVirtualCardFetched(CreditCardFetchResult result,
+                            const CreditCard* credit_card,
                             const std::u16string& cvc,
                             const gfx::Image& card_image) override;
   bool IsAutofillAssistantShowing() override;
diff --git a/chrome/browser/ui/autofill/payments/autofill_snackbar_controller_impl.cc b/chrome/browser/ui/autofill/payments/autofill_snackbar_controller_impl.cc
index 07c7c97..8d5a438 100644
--- a/chrome/browser/ui/autofill/payments/autofill_snackbar_controller_impl.cc
+++ b/chrome/browser/ui/autofill/payments/autofill_snackbar_controller_impl.cc
@@ -5,6 +5,7 @@
 
 #include <string>
 #include "base/macros.h"
+#include "base/metrics/histogram_functions.h"
 #include "build/build_config.h"
 #include "chrome/browser/autofill/manual_filling_controller.h"
 #include "chrome/browser/autofill/manual_filling_controller_impl.h"
@@ -28,6 +29,7 @@
     autofill_snackbar_view_ = AutofillSnackbarView::Create(this);
   }
   autofill_snackbar_view_->Show();
+  base::UmaHistogramBoolean("Autofill.Snackbar.VirtualCard.Shown", true);
 }
 
 void AutofillSnackbarControllerImpl::Dismiss() {
@@ -44,12 +46,12 @@
 void AutofillSnackbarControllerImpl::OnActionClicked() {
   ManualFillingControllerImpl::GetOrCreate(web_contents_)
       ->ShowAccessorySheetTab(autofill::AccessoryTabType::CREDIT_CARDS);
-  // TODO(crbug.com/1196021):  Log the action.
+  base::UmaHistogramBoolean("Autofill.Snackbar.VirtualCard.ActionClicked",
+                            true);
 }
 
 void AutofillSnackbarControllerImpl::OnDismissed() {
   autofill_snackbar_view_ = nullptr;
-  // TODO(crbug.com/1196021): Log that no user action was taken.
 }
 
 std::u16string AutofillSnackbarControllerImpl::GetMessageText() const {
diff --git a/chrome/browser/ui/autofill/payments/autofill_snackbar_controller_impl_unittest.cc b/chrome/browser/ui/autofill/payments/autofill_snackbar_controller_impl_unittest.cc
new file mode 100644
index 0000000..1e2f8a4
--- /dev/null
+++ b/chrome/browser/ui/autofill/payments/autofill_snackbar_controller_impl_unittest.cc
@@ -0,0 +1,78 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/autofill/payments/autofill_snackbar_controller_impl.h"
+
+#include "base/test/metrics/histogram_tester.h"
+#include "chrome/browser/autofill/manual_filling_controller_impl.h"
+#include "chrome/browser/autofill/mock_address_accessory_controller.h"
+#include "chrome/browser/autofill/mock_credit_card_accessory_controller.h"
+#include "chrome/browser/autofill/mock_manual_filling_view.h"
+#include "chrome/browser/autofill/mock_password_accessory_controller.h"
+#include "chrome/browser/ui/autofill/payments/autofill_snackbar_view.h"
+#include "chrome/test/base/chrome_render_view_host_test_harness.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using testing::NiceMock;
+
+namespace autofill {
+
+class MockAutofillSnackbarView : public AutofillSnackbarView {
+ public:
+  MockAutofillSnackbarView() = default;
+  void Show() override {}
+  void Dismiss() override {}
+};
+
+class AutofillSnackbarControllerImplTest
+    : public ChromeRenderViewHostTestHarness {
+ public:
+  AutofillSnackbarControllerImplTest() = default;
+
+  void SetUp() override {
+    ChromeRenderViewHostTestHarness::SetUp();
+    controller()->SetViewForTesting(new MockAutofillSnackbarView());
+    ManualFillingControllerImpl::CreateForWebContentsForTesting(
+        web_contents(), mock_pwd_controller_.AsWeakPtr(),
+        mock_address_controller_.AsWeakPtr(), mock_cc_controller_.AsWeakPtr(),
+        std::make_unique<NiceMock<MockManualFillingView>>());
+  }
+
+  AutofillSnackbarControllerImpl* controller() {
+    if (!controller_)
+      controller_ = new AutofillSnackbarControllerImpl(web_contents());
+    return controller_;
+  }
+
+ private:
+  AutofillSnackbarControllerImpl* controller_ = nullptr;
+  NiceMock<MockPasswordAccessoryController> mock_pwd_controller_;
+  NiceMock<MockAddressAccessoryController> mock_address_controller_;
+  NiceMock<MockCreditCardAccessoryController> mock_cc_controller_;
+};
+
+TEST_F(AutofillSnackbarControllerImplTest, MetricsTest) {
+  base::HistogramTester histogram_tester;
+  controller()->Show();
+  // Verify that the count for Shown is incremented and ActionClicked hasn't
+  // changed.
+  histogram_tester.ExpectUniqueSample("Autofill.Snackbar.VirtualCard.Shown", 1,
+                                      1);
+  histogram_tester.ExpectUniqueSample(
+      "Autofill.Snackbar.VirtualCard.ActionClicked", 1, 0);
+  controller()->OnDismissed();
+
+  // Reset the mock view.
+  controller()->SetViewForTesting(new MockAutofillSnackbarView());
+  controller()->Show();
+  controller()->OnActionClicked();
+  // Verify that the count for both Shown and ActionClicked is incremented.
+  histogram_tester.ExpectUniqueSample("Autofill.Snackbar.VirtualCard.Shown", 1,
+                                      2);
+  histogram_tester.ExpectUniqueSample(
+      "Autofill.Snackbar.VirtualCard.ActionClicked", 1, 1);
+}
+
+}  // namespace autofill
diff --git a/chrome/browser/ui/browser.cc b/chrome/browser/ui/browser.cc
index 203ec23..049d34f 100644
--- a/chrome/browser/ui/browser.cc
+++ b/chrome/browser/ui/browser.cc
@@ -457,6 +457,7 @@
       app_name_(params.app_name),
       is_trusted_source_(params.trusted_source),
       session_id_(SessionID::NewUnique()),
+      omit_from_session_restore_(params.omit_from_session_restore),
       cancel_download_confirmation_state_(NOT_PROMPTED),
       override_bounds_(params.initial_bounds),
       initial_show_state_(params.initial_show_state),
diff --git a/chrome/browser/ui/browser.h b/chrome/browser/ui/browser.h
index 1d3806e..ab9c5fb 100644
--- a/chrome/browser/ui/browser.h
+++ b/chrome/browser/ui/browser.h
@@ -233,9 +233,13 @@
     // The associated profile.
     Profile* profile;
 
-    // Specifies the browser is_trusted_source_ value.
+    // Specifies the browser `is_trusted_source_` value.
     bool trusted_source = false;
 
+    // Specifies the browser `omit_from_session_restore_` value, whether the new
+    // Browser should be omitted from being saved/restored by session restore.
+    bool omit_from_session_restore = false;
+
     // The bounds of the window to open.
     gfx::Rect initial_bounds;
 
@@ -379,6 +383,7 @@
     return command_controller_.get();
   }
   const SessionID& session_id() const { return session_id_; }
+  bool omit_from_session_restore() const { return omit_from_session_restore_; }
   BrowserContentSettingBubbleModelDelegate*
   content_setting_bubble_model_delegate() {
     return content_setting_bubble_model_delegate_.get();
@@ -1132,6 +1137,10 @@
   // across sessions.
   const SessionID session_id_;
 
+  // Whether this Browser should be omitted from being saved/restored by session
+  // restore.
+  bool omit_from_session_restore_ = false;
+
   // The model for the toolbar view.
   std::unique_ptr<LocationBarModel> location_bar_model_;
 
diff --git a/chrome/browser/ui/extensions/hosted_app_browsertest.cc b/chrome/browser/ui/extensions/hosted_app_browsertest.cc
index 85f8099..f8a5367 100644
--- a/chrome/browser/ui/extensions/hosted_app_browsertest.cc
+++ b/chrome/browser/ui/extensions/hosted_app_browsertest.cc
@@ -23,6 +23,7 @@
 #include "build/chromeos_buildflags.h"
 #include "chrome/app/chrome_command_ids.h"
 #include "chrome/browser/apps/app_service/app_service_test.h"
+#include "chrome/browser/chrome_content_browser_client.h"
 #include "chrome/browser/extensions/extension_browsertest.h"
 #include "chrome/browser/predictors/loading_predictor_config.h"
 #include "chrome/browser/renderer_context_menu/render_view_context_menu_test_util.h"
@@ -61,6 +62,7 @@
 #include "content/public/browser/render_frame_host.h"
 #include "content/public/browser/render_process_host.h"
 #include "content/public/browser/web_contents.h"
+#include "content/public/common/content_client.h"
 #include "content/public/common/content_features.h"
 #include "content/public/common/content_switches.h"
 #include "content/public/test/browser_test.h"
@@ -1592,6 +1594,129 @@
   EXPECT_TRUE(process_map->Contains(bar_process->GetID()));
 }
 
+template <bool jit_disabled_by_default>
+class HostedAppJitTestBase : public HostedAppProcessModelTest {
+ public:
+  void SetUpCommandLine(base::CommandLine* command_line) override {
+    HostedOrWebAppTest::SetUpCommandLine(command_line);
+    ASSERT_TRUE(embedded_test_server()->InitializeAndListen());
+    content::IsolateAllSitesForTesting(command_line);
+  }
+
+  void SetUpOnMainThread() override {
+    HostedAppProcessModelTest::SetUpOnMainThread();
+    scoped_client_override_ =
+        std::make_unique<ScopedJitChromeBrowserClientOverride>(
+            jit_disabled_by_default);
+  }
+
+ protected:
+  HostedAppJitTestBase() = default;
+  ~HostedAppJitTestBase() override = default;
+
+  // Utility class to override ChromeBrowserClient within a scope with a
+  // BrowserClient that has a different JIT policy.
+  class ScopedJitChromeBrowserClientOverride {
+   public:
+    // A custom ContentBrowserClient to selectively turn off JIT for certain
+    // sites.
+    class JitChromeContentBrowserClient : public ChromeContentBrowserClient {
+     public:
+      explicit JitChromeContentBrowserClient(bool jit_disabled_default)
+          : is_jit_disabled_by_default_(jit_disabled_default) {}
+
+      bool IsJitDisabledForSite(content::BrowserContext* browser_context,
+                                const GURL& site_url) override {
+        if (site_url.is_empty())
+          return is_jit_disabled_by_default_;
+        if (site_url.DomainIs("jit-disabled.com"))
+          return true;
+        if (site_url.DomainIs("jit-enabled.com"))
+          return false;
+        return is_jit_disabled_by_default_;
+      }
+
+     private:
+      bool is_jit_disabled_by_default_;
+    };
+
+    explicit ScopedJitChromeBrowserClientOverride(
+        bool is_jit_disabled_by_default) {
+      overriden_client_ = std::make_unique<JitChromeContentBrowserClient>(
+          is_jit_disabled_by_default);
+      original_client_ =
+          content::SetBrowserClientForTesting(overriden_client_.get());
+    }
+
+    ~ScopedJitChromeBrowserClientOverride() {
+      content::SetBrowserClientForTesting(original_client_);
+    }
+
+   private:
+    std::unique_ptr<JitChromeContentBrowserClient> overriden_client_;
+    content::ContentBrowserClient* original_client_;
+  };
+
+  void JitTestInternal() {
+    // Set up a hosted app covering http://jit-disabled.com.
+    GURL jit_disabled_app_url(
+        embedded_test_server()->GetURL("jit-disabled.com", "/title2.html"));
+    constexpr const char kHostedAppManifest[] =
+        R"( { "name": "Hosted App With SitePerProcess Test",
+              "version": "1",
+              "manifest_version": 2,
+              "app": {
+                "launch": {
+                  "web_url": "%s"
+                },
+                "urls": ["http://jit-disabled.com/", "http://jit-enabled.com/"]
+              }
+            } )";
+    {
+      extensions::TestExtensionDir test_app_dir;
+      test_app_dir.WriteManifest(base::StringPrintf(
+          kHostedAppManifest, jit_disabled_app_url.spec().c_str()));
+      SetupApp(test_app_dir.UnpackedPath());
+    }
+
+    // Navigate main window to a jit-disabled.com app URL.
+    ui_test_utils::NavigateToURL(browser(), jit_disabled_app_url);
+    content::WebContents* web_contents =
+        browser()->tab_strip_model()->GetActiveWebContents();
+    EXPECT_EQ(jit_disabled_app_url, web_contents->GetLastCommittedURL());
+    scoped_refptr<content::SiteInstance> site_instance =
+        web_contents->GetMainFrame()->GetSiteInstance();
+    EXPECT_TRUE(
+        site_instance->GetSiteURL().SchemeIs(extensions::kExtensionScheme));
+    EXPECT_TRUE(site_instance->GetProcess()->IsJitDisabled());
+
+    // Navigate main window to a jit-enabled.com app URL.
+    GURL jit_enabled_app_url(
+        embedded_test_server()->GetURL("jit-enabled.com", "/title2.html"));
+    ui_test_utils::NavigateToURL(browser(), jit_enabled_app_url);
+    web_contents = browser()->tab_strip_model()->GetActiveWebContents();
+    EXPECT_EQ(jit_enabled_app_url, web_contents->GetLastCommittedURL());
+    site_instance = web_contents->GetMainFrame()->GetSiteInstance();
+    EXPECT_TRUE(
+        site_instance->GetSiteURL().SchemeIs(extensions::kExtensionScheme));
+    EXPECT_FALSE(site_instance->GetProcess()->IsJitDisabled());
+  }
+
+ private:
+  std::unique_ptr<ScopedJitChromeBrowserClientOverride> scoped_client_override_;
+};
+
+typedef HostedAppJitTestBase<false> HostedAppJitTestBaseDefaultEnabled;
+typedef HostedAppJitTestBase<true> HostedAppJitTestBaseDefaultDisabled;
+
+IN_PROC_BROWSER_TEST_P(HostedAppJitTestBaseDefaultEnabled, JITDisabledTest) {
+  JitTestInternal();
+}
+
+IN_PROC_BROWSER_TEST_P(HostedAppJitTestBaseDefaultDisabled, JITDisabledTest) {
+  JitTestInternal();
+}
+
 // Check that when a hosted app covers multiple sites in its web extent,
 // navigating from one of these sites to another swaps processes.
 IN_PROC_BROWSER_TEST_P(HostedAppSitePerProcessTest,
@@ -2006,3 +2131,11 @@
     All,
     HostedAppSitePerProcessTest,
     ::testing::Values(AppType::HOSTED_APP));
+
+INSTANTIATE_TEST_SUITE_P(All,
+                         HostedAppJitTestBaseDefaultEnabled,
+                         ::testing::Values(AppType::HOSTED_APP));
+
+INSTANTIATE_TEST_SUITE_P(All,
+                         HostedAppJitTestBaseDefaultDisabled,
+                         ::testing::Values(AppType::HOSTED_APP));
diff --git a/chrome/browser/ui/views/toolbar/chrome_labs_bubble_view.cc b/chrome/browser/ui/views/toolbar/chrome_labs_bubble_view.cc
index 0f7ebe3..8ec1f10 100644
--- a/chrome/browser/ui/views/toolbar/chrome_labs_bubble_view.cc
+++ b/chrome/browser/ui/views/toolbar/chrome_labs_bubble_view.cc
@@ -51,7 +51,8 @@
   kUnspecifiedSelected = 0,
   kReadLaterSelected = 1,
   kTabScrollingSelected = 3,
-  kMaxValue = kTabScrollingSelected,
+  kSidePanelSelected = 4,
+  kMaxValue = kSidePanelSelected,
 };
 
 void EmitToHistogram(const std::u16string& selected_lab_state,
@@ -78,6 +79,8 @@
       return ChromeLabsSelectedLab::kReadLaterSelected;
     } else if (internal_name == flag_descriptions::kScrollableTabStripFlagId) {
       return ChromeLabsSelectedLab::kTabScrollingSelected;
+    } else if (internal_name == flag_descriptions::kSidePanelFlagId) {
+      return ChromeLabsSelectedLab::kSidePanelSelected;
     } else {
       return ChromeLabsSelectedLab::kUnspecifiedSelected;
     }
diff --git a/chrome/browser/ui/views/toolbar/chrome_labs_bubble_view_model.cc b/chrome/browser/ui/views/toolbar/chrome_labs_bubble_view_model.cc
index b74bd84..8f43a9b 100644
--- a/chrome/browser/ui/views/toolbar/chrome_labs_bubble_view_model.cc
+++ b/chrome/browser/ui/views/toolbar/chrome_labs_bubble_view_model.cc
@@ -35,6 +35,13 @@
   static const base::NoDestructor<std::vector<LabInfo>> lab_info_([]() {
     std::vector<LabInfo> lab_info;
 
+    // Side Panel.
+    lab_info.emplace_back(LabInfo(
+        flag_descriptions::kSidePanelFlagId,
+        l10n_util::GetStringUTF16(IDS_SIDE_PANEL_EXPERIMENT_NAME),
+        l10n_util::GetStringUTF16(IDS_SIDE_PANEL_EXPERIMENT_DESCRIPTION),
+        "chrome-labs-side-panel", version_info::Channel::DEV));
+
     // Read Later.
     lab_info.emplace_back(LabInfo(
         flag_descriptions::kReadLaterFlagId,
diff --git a/chrome/browser/ui/web_applications/system_web_app_ui_utils.cc b/chrome/browser/ui/web_applications/system_web_app_ui_utils.cc
index ff1fb00..2fa2904 100644
--- a/chrome/browser/ui/web_applications/system_web_app_ui_utils.cc
+++ b/chrome/browser/ui/web_applications/system_web_app_ui_utils.cc
@@ -240,10 +240,16 @@
   bool can_maximize =
       provider->system_web_app_manager().IsMaximizableWindow(app_type);
 
+  // System Web App windows can't be properly restored without storing the app
+  // type. Until that is implemented, skip them for session restore.
+  // TODO(crbug.com/1003170): Enable session restore for System Web Apps by
+  // passing through the underlying value of params.omit_from_session_restore.
+  const bool omit_from_session_restore = true;
+
   if (!browser) {
-    browser =
-        CreateWebApplicationWindow(profile, params.app_id, params.disposition,
-                                   params.restore_id, can_resize, can_maximize);
+    browser = CreateWebApplicationWindow(
+        profile, params.app_id, params.disposition, params.restore_id,
+        omit_from_session_restore, can_resize, can_maximize);
   }
 
   // Navigate application window to application's |url| if necessary.
diff --git a/chrome/browser/ui/web_applications/web_app_launch_manager.cc b/chrome/browser/ui/web_applications/web_app_launch_manager.cc
index 3982aba..d25aa9e0 100644
--- a/chrome/browser/ui/web_applications/web_app_launch_manager.cc
+++ b/chrome/browser/ui/web_applications/web_app_launch_manager.cc
@@ -210,6 +210,7 @@
                                     const std::string& app_id,
                                     WindowOpenDisposition disposition,
                                     int32_t restore_id,
+                                    bool omit_from_session_restore,
                                     bool can_resize,
                                     bool can_maximize) {
   std::string app_name = GenerateApplicationNameFromAppId(app_id);
@@ -226,6 +227,7 @@
 #if BUILDFLAG(IS_CHROMEOS_ASH)
   browser_params.restore_id = restore_id;
 #endif
+  browser_params.omit_from_session_restore = omit_from_session_restore;
   browser_params.can_resize = can_resize;
   browser_params.can_maximize = can_maximize;
   return Browser::Create(browser_params);
diff --git a/chrome/browser/ui/web_applications/web_app_launch_manager.h b/chrome/browser/ui/web_applications/web_app_launch_manager.h
index 9f67410..2cc81c3 100644
--- a/chrome/browser/ui/web_applications/web_app_launch_manager.h
+++ b/chrome/browser/ui/web_applications/web_app_launch_manager.h
@@ -80,6 +80,7 @@
                                     const std::string& app_id,
                                     WindowOpenDisposition disposition,
                                     int32_t restore_id,
+                                    bool omit_from_session_restore = false,
                                     bool can_resize = true,
                                     bool can_maximize = true);
 
diff --git a/chrome/browser/ui/webui/settings/chromeos/apps_section.cc b/chrome/browser/ui/webui/settings/chromeos/apps_section.cc
index c630657f..5d22fb9 100644
--- a/chrome/browser/ui/webui/settings/chromeos/apps_section.cc
+++ b/chrome/browser/ui/webui/settings/chromeos/apps_section.cc
@@ -251,6 +251,7 @@
   static constexpr webui::LocalizedString kLocalizedStrings[] = {
       {"appsPageTitle", IDS_SETTINGS_APPS_TITLE},
       {"appManagementTitle", IDS_SETTINGS_APPS_LINK_TEXT},
+      {"appNotificationsTitle", IDS_SETTINGS_APP_NOTIFICATIONS_LINK_TEXT},
   };
   html_source->AddLocalizedStrings(kLocalizedStrings);
 
@@ -265,6 +266,11 @@
                           arc::IsArcAllowedForProfile(profile()));
   html_source->AddBoolean("havePlayStoreApp", arc::IsPlayStoreAvailable());
 
+  html_source->AddBoolean(
+      "showOsSettingsAppNotificationsRow",
+      base::FeatureList::IsEnabled(
+          chromeos::features::kOsSettingsAppNotificationsPage));
+
   AddAppManagementStrings(html_source);
   AddGuestOsStrings(html_source);
   AddAndroidAppStrings(html_source);
@@ -313,6 +319,12 @@
                                      mojom::SearchResultIcon::kAppsGrid,
                                      mojom::SearchResultDefaultRank::kMedium,
                                      mojom::kAppManagementSubpagePath);
+  // App Notifications
+  generator->RegisterTopLevelSubpage(IDS_SETTINGS_APP_NOTIFICATIONS_LINK_TEXT,
+                                     mojom::Subpage::kAppNotifications,
+                                     mojom::SearchResultIcon::kAppsGrid,
+                                     mojom::SearchResultDefaultRank::kMedium,
+                                     mojom::kAppNotificationsSubpagePath);
   // Note: The subpage name in the UI is updated dynamically based on the app
   // being shown, but we use a generic "App details" string here.
   generator->RegisterNestedSubpage(
diff --git a/chrome/browser/ui/webui/settings/chromeos/constants/routes.mojom b/chrome/browser/ui/webui/settings/chromeos/constants/routes.mojom
index dd1bbf3..2a98339 100644
--- a/chrome/browser/ui/webui/settings/chromeos/constants/routes.mojom
+++ b/chrome/browser/ui/webui/settings/chromeos/constants/routes.mojom
@@ -95,6 +95,7 @@
   kPluginVmSharedPaths = 703,
   kPluginVmUsbPreferences = 704,
   kOnStartup = 705,
+  kAppNotifications = 706,
 
   // Crostini section.
   kCrostiniDetails = 800,
@@ -205,6 +206,7 @@
 // Apps section.
 const string kAppsSectionPath = "apps";
 const string kAppManagementSubpagePath = "app-management";
+const string kAppNotificationsSubpagePath = "app-notifications";
 const string kAppDetailsSubpagePath = "app-management/detail";
 const string kGooglePlayStoreSubpagePath = "androidAppsDetails";
 const string kPluginVmSharedPathsSubpagePath =
diff --git a/chrome/browser/ui/webui/settings/site_settings_helper.cc b/chrome/browser/ui/webui/settings/site_settings_helper.cc
index 3a156ea2..62aed02 100644
--- a/chrome/browser/ui/webui/settings/site_settings_helper.cc
+++ b/chrome/browser/ui/webui/settings/site_settings_helper.cc
@@ -159,6 +159,7 @@
     {ContentSettingsType::DISPLAY_CAPTURE, nullptr},
     {ContentSettingsType::FEDERATED_IDENTITY_SHARING, nullptr},
     {ContentSettingsType::FEDERATED_IDENTITY_REQUEST, nullptr},
+    {ContentSettingsType::JAVASCRIPT_JIT, nullptr},
 };
 
 static_assert(base::size(kContentSettingsTypeGroupNames) ==
diff --git a/chrome/browser/ui/webui/tab_search/tab_search_page_handler.cc b/chrome/browser/ui/webui/tab_search/tab_search_page_handler.cc
index 235956a..da7fbe6 100644
--- a/chrome/browser/ui/webui/tab_search/tab_search_page_handler.cc
+++ b/chrome/browser/ui/webui/tab_search/tab_search_page_handler.cc
@@ -11,6 +11,7 @@
 #include <vector>
 
 #include "base/base64.h"
+#include "base/debug/dump_without_crashing.h"
 #include "base/metrics/histogram_functions.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/time/time.h"
@@ -190,6 +191,8 @@
     for (int i = 0; i < tab_strip_model->count(); ++i) {
       tab_search::mojom::TabPtr tab =
           GetTab(tab_strip_model, tab_strip_model->GetWebContentsAt(i), i);
+      if (tab->url.empty())
+        base::debug::DumpWithoutCrashing();
       tab_urls.insert(tab->url);
       window->tabs.push_back(std::move(tab));
     }
@@ -272,6 +275,9 @@
   tab_search::mojom::RecentlyClosedTabPtr recently_closed_tab =
       GetRecentlyClosedTab(tab);
 
+  if (recently_closed_tab->url.empty())
+    base::debug::DumpWithoutCrashing();
+
   // New tab page entries may exist inside a window and should be
   // ignored.
   if (recently_closed_tab->url == GURL(chrome::kChromeUINewTabPageURL))
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index 0993f77..8aee0e34 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -702,6 +702,7 @@
       "../browser/metrics/ukm_browsertest.cc",
       "../browser/net/cert_verify_proc_browsertest.cc",
       "../browser/page_load_metrics/observers/ad_metrics/ad_density_intervention_android_browsertest.cc",
+      "../browser/policy/policy_prefs_browsertest.cc",
       "../browser/profiles/profile_browsertest_android.cc",
       "../browser/safe_browsing/test_safe_browsing_database_helper.cc",
       "../browser/safe_browsing/test_safe_browsing_database_helper.h",
@@ -722,6 +723,8 @@
 
     deps += [
       "//components/autofill/content/renderer:test_support",
+      "//components/enterprise:test_support",
+      "//components/policy/core/browser:test_support",
       "//components/safe_browsing/content/renderer/phishing_classifier:phishing_classifier",
       "//components/safe_browsing/core:client_model_proto",
       "//components/safe_browsing/core/fbs:client_model",
@@ -781,6 +784,7 @@
       "//components/test/data/payments/",
       "//chrome/test/data/android/customtabs/",
       "//chrome/test/data/banners/",
+      "//chrome/test/data/policy/",
       "//chrome/test/data/ssl/",
       "//content/test/data/",
     ]
@@ -1635,6 +1639,7 @@
       "../browser/policy/test/force_google_safe_search_policy_browsertest.cc",
       "../browser/policy/test/hide_webstore_icon_policy_browsertest.cc",
       "../browser/policy/test/hsts_policy_browsertest.cc",
+      "../browser/policy/test/jit_policy_browsertest.cc",
       "../browser/policy/test/media_stream_policy_browsertest.cc",
       "../browser/policy/test/network_prediction_policy_browsertest.cc",
       "../browser/policy/test/policy_browsertest.cc",
@@ -5331,6 +5336,7 @@
       "../browser/sharing/sms/sms_fetch_request_handler_unittest.cc",
       "../browser/touch_to_fill/touch_to_fill_controller_unittest.cc",
       "../browser/translate/translate_manager_render_view_host_android_unittest.cc",
+      "../browser/ui/autofill/payments/autofill_snackbar_controller_impl_unittest.cc",
     ]
 
     deps += [
diff --git a/chrome/test/data/policy/policy_test_cases.json b/chrome/test/data/policy/policy_test_cases.json
index 5a59ace..a104ec1 100644
--- a/chrome/test/data/policy/policy_test_cases.json
+++ b/chrome/test/data/policy/policy_test_cases.json
@@ -127,6 +127,7 @@
   },
 
   "AlternateErrorPagesEnabled": {
+    "note": "TODO(http://crbug.com/1141521): enable this policy test on Android",
     "os": ["win", "linux", "mac", "chromeos"],
     "can_be_recommended": true,
     "policy_pref_mapping_tests": [
@@ -138,7 +139,7 @@
   },
 
   "SearchSuggestEnabled": {
-    "os": ["win", "linux", "mac", "chromeos"],
+    "os": ["win", "linux", "mac", "chromeos", "android"],
     "can_be_recommended": true,
     "policy_pref_mapping_tests": [
       {
@@ -176,7 +177,7 @@
   },
 
   "NetworkPredictionOptions": {
-    "os": ["win", "linux", "mac", "chromeos"],
+    "os": ["win", "linux", "mac", "chromeos", "android"],
     "can_be_recommended": true,
     "policy_pref_mapping_tests": [
       {
@@ -208,7 +209,7 @@
   "Http09OnNonDefaultPortsEnabled": {},
 
   "JavascriptEnabled": {
-    "os": ["win", "linux", "mac", "chromeos"],
+    "os": ["win", "linux", "mac", "chromeos", "android"],
     "policy_pref_mapping_tests": [
       {
         "policies": { "JavascriptEnabled": false },
@@ -218,7 +219,7 @@
   },
 
   "IncognitoEnabled": {
-    "os": ["win", "linux", "mac", "chromeos"],
+    "os": ["win", "linux", "mac", "chromeos", "android"],
     "policy_pref_mapping_tests": [
       {
         "policies": { "IncognitoEnabled": false },
@@ -228,7 +229,7 @@
   },
 
   "IncognitoModeAvailability": {
-    "os": ["win", "linux", "mac", "chromeos"],
+    "os": ["win", "linux", "mac", "chromeos", "android"],
     "policy_pref_mapping_tests": [
       {
         "policies": { "IncognitoModeAvailability": 1 },
@@ -238,7 +239,7 @@
   },
 
   "SavingBrowserHistoryDisabled": {
-    "os": ["win", "linux", "mac", "chromeos"],
+    "os": ["win", "linux", "mac", "chromeos", "android"],
     "policy_pref_mapping_tests": [
       {
         "policies": { "SavingBrowserHistoryDisabled": true },
@@ -304,7 +305,7 @@
   "RemoteAccessHostMaximumSessionDurationMinutes": {},
 
   "PrintingEnabled": {
-    "os": ["win", "linux", "mac", "chromeos"],
+    "os": ["win", "linux", "mac", "chromeos", "android"],
     "policy_pref_mapping_tests": [
       {
         "policies": { "PrintingEnabled": false },
@@ -822,7 +823,8 @@
   },
 
   "ForceYouTubeRestrict": {
-    "os": ["win", "linux", "mac", "chromeos", "android"],
+    "note": "TODO(http://crbug.com/1141521): enable this policy test on Android",
+    "os": ["win", "linux", "mac", "chromeos"],
     "policy_pref_mapping_tests": [
       {
         "policies": { "ForceYouTubeRestrict": 1 },
@@ -844,7 +846,7 @@
   },
 
   "PasswordManagerEnabled": {
-    "os": ["win", "linux", "mac", "chromeos"],
+    "os": ["win", "linux", "mac", "chromeos", "android"],
     "can_be_recommended": true,
     "policy_pref_mapping_tests": [
       {
@@ -876,7 +878,7 @@
   },
 
   "AutoFillEnabled": {
-    "os": ["win", "linux", "mac", "chromeos"],
+    "os": ["win", "linux", "mac", "chromeos", "android"],
     "can_be_recommended": true,
     "policy_pref_mapping_tests": [
       {
@@ -887,7 +889,7 @@
   },
 
   "AutofillAddressEnabled": {
-    "os": ["win", "linux", "mac", "chromeos"],
+    "os": ["win", "linux", "mac", "chromeos", "android"],
     "can_be_recommended": true,
     "policy_pref_mapping_tests": [
       {
@@ -898,7 +900,7 @@
   },
 
   "AutofillCreditCardEnabled": {
-    "os": ["win", "linux", "mac", "chromeos"],
+    "os": ["win", "linux", "mac", "chromeos", "android"],
     "can_be_recommended": true,
     "policy_pref_mapping_tests": [
       {
@@ -975,6 +977,7 @@
   },
 
   "SigninAllowed": {
+    "note": "TODO(http://crbug.com/1141521): enable this policy test on Android",
     "os": ["win", "linux", "mac"],
     "policy_pref_mapping_tests": [
       {
@@ -1101,7 +1104,7 @@
   },
 
   "ProxyMode": {
-    "os": ["win", "mac", "linux"],
+    "os": ["win", "mac", "linux", "android"],
     "policy_pref_mapping_tests": [
       {
         "policies": { "ProxyMode": "direct" },
@@ -1111,7 +1114,7 @@
   },
 
   "ProxyServerMode": {
-    "os": ["win", "mac", "linux"],
+    "os": ["win", "mac", "linux", "android"],
     "policy_pref_mapping_tests": [
       {
         "policies": { "ProxyServerMode": 0 },
@@ -1121,7 +1124,7 @@
   },
 
   "ProxyServer": {
-    "os": ["win", "mac", "linux"],
+    "os": ["win", "mac", "linux", "android"],
     "policy_pref_mapping_tests": [
       {
         "policies": { "ProxyMode": "fixed_servers", "ProxyServer": "http://localhost:8080" },
@@ -1131,7 +1134,7 @@
   },
 
   "ProxyPacUrl": {
-    "os": ["win", "mac", "linux"],
+    "os": ["win", "mac", "linux", "android"],
     "policy_pref_mapping_tests": [
       {
         "policies": { "ProxyMode": "pac_script", "ProxyPacUrl": "http://localhost:8080/proxy.pac" },
@@ -1141,7 +1144,7 @@
   },
 
   "ProxyBypassList": {
-    "os": ["win", "mac", "linux"],
+    "os": ["win", "mac", "linux", "android"],
     "policy_pref_mapping_tests": [
       {
         "policies": { "ProxyMode": "fixed_servers", "ProxyServer": "http://localhost:8080", "ProxyBypassList": "localhost" },
@@ -1151,7 +1154,7 @@
   },
 
   "ProxySettings": {
-    "os": ["linux", "win"],
+    "os": ["linux", "win", "android"],
     "policy_pref_mapping_tests": [
       {
         "policies": { "ProxySettings": { "ProxyMode": "direct" } },
@@ -1243,7 +1246,7 @@
   },
 
   "BasicAuthOverHttpEnabled": {
-    "os": ["win", "linux", "mac", "chromeos", "android"],
+    "os": ["win", "linux", "mac", "chromeos"],
     "policy_pref_mapping_tests": [
       {
         "policies": { "BasicAuthOverHttpEnabled": false },
@@ -1402,7 +1405,7 @@
   },
 
   "PromptForDownloadLocation": {
-    "os": ["win", "linux", "mac", "chromeos"],
+    "os": ["win", "linux", "mac", "chromeos", "android"],
     "policy_pref_mapping_tests": [
       {
         "policies": { "PromptForDownloadLocation": false },
@@ -1999,7 +2002,7 @@
   },
 
   "DefaultCookiesSetting": {
-    "os": ["win", "linux", "mac", "chromeos"],
+    "os": ["win", "linux", "mac", "chromeos", "android"],
     "note": "TODO(http://crbug.com/106682): Flag this with can_be_recommended when bug is fixed.",
     "policy_pref_mapping_tests": [
       {
@@ -2048,7 +2051,7 @@
   },
 
   "DefaultJavaScriptSetting": {
-    "os": ["win", "linux", "mac", "chromeos"],
+    "os": ["win", "linux", "mac", "chromeos", "android"],
     "note": "TODO(http://crbug.com/106682): Flag this with can_be_recommended when bug is fixed.",
     "policy_pref_mapping_tests": [
       {
@@ -2062,6 +2065,21 @@
     ]
   },
 
+  "DefaultJavaScriptJitSetting": {
+    "os": ["win", "linux", "mac", "chromeos"],
+    "note": "TODO(http://crbug.com/106682): Flag this with can_be_recommended when bug is fixed.",
+    "policy_pref_mapping_tests": [
+      {
+        "policies": { "DefaultJavaScriptJitSetting": 1 },
+        "prefs": { "profile.managed_default_content_settings.javascript_jit": { "value": 1 } }
+      },
+      {
+        "policies": { "DefaultJavaScriptJitSetting": 2 },
+        "prefs": { "profile.managed_default_content_settings.javascript_jit": { "value": 2 } }
+      }
+    ]
+  },
+
   "DefaultKeygenSetting": {
     "note": "This policy is retired, see https://crbug.com/568184."
   },
@@ -2071,7 +2089,7 @@
   },
 
   "LegacySameSiteCookieBehaviorEnabledForDomainList": {
-    "os": ["win", "linux", "mac", "chromeos"],
+    "os": ["win", "linux", "mac", "chromeos", "android"],
     "policy_pref_mapping_tests": [
       {
         "policies": { "LegacySameSiteCookieBehaviorEnabledForDomainList": ["[*.]google.com"] },
@@ -2081,7 +2099,7 @@
   },
 
   "InsecurePrivateNetworkRequestsAllowed": {
-    "os": ["win", "linux", "mac", "chromeos"],
+    "os": ["win", "linux", "mac", "chromeos", "android"],
     "policy_pref_mapping_tests": [
       {
         "policies": {},
@@ -2099,7 +2117,7 @@
   },
 
   "InsecurePrivateNetworkRequestsAllowedForUrls": {
-    "os": ["win", "linux", "mac", "chromeos"],
+    "os": ["win", "linux", "mac", "chromeos", "android"],
     "policy_pref_mapping_tests": [
       {
         "policies": { "InsecurePrivateNetworkRequestsAllowedForUrls": ["[*.]google.com", "http://example.com:1234"] },
@@ -2117,7 +2135,7 @@
   },
 
   "DefaultPopupsSetting": {
-    "os": ["win", "linux", "mac", "chromeos"],
+    "os": ["win", "linux", "mac", "chromeos", "android"],
     "note": "TODO(http://crbug.com/106682): Flag this with can_be_recommended when bug is fixed.",
     "policy_pref_mapping_tests": [
       {
@@ -2151,7 +2169,7 @@
   },
 
   "DefaultGeolocationSetting": {
-    "os": ["win", "linux", "mac", "chromeos"],
+    "os": ["win", "linux", "mac", "chromeos", "android"],
     "note": "TODO(http://crbug.com/106682): Flag this with can_be_recommended when bug is fixed.",
     "policy_pref_mapping_tests": [
       {
@@ -2222,7 +2240,7 @@
   },
 
   "DefaultSensorsSetting": {
-    "os": ["win", "linux", "mac"],
+    "os": ["win", "linux", "mac", "android"],
     "note": "TODO(http://crbug.com/106682): Flag this with can_be_recommended when bug is fixed.",
     "policy_pref_mapping_tests": [
       {
@@ -2287,7 +2305,7 @@
   },
 
   "DefaultWebBluetoothGuardSetting": {
-    "os": ["win", "linux", "mac", "chromeos"],
+    "os": ["win", "linux", "mac", "chromeos", "android"],
     "policy_pref_mapping_tests": [
       {
         "policies": { "DefaultWebBluetoothGuardSetting": 2 },
@@ -2337,7 +2355,7 @@
   },
 
   "CookiesAllowedForUrls": {
-    "os": ["win", "linux", "mac", "chromeos"],
+    "os": ["win", "linux", "mac", "chromeos", "android"],
     "note": "TODO(http://crbug.com/106682): Flag this with can_be_recommended when bug is fixed.",
     "policy_pref_mapping_tests": [
       {
@@ -2348,7 +2366,7 @@
   },
 
   "CookiesBlockedForUrls": {
-    "os": ["win", "linux", "mac", "chromeos"],
+    "os": ["win", "linux", "mac", "chromeos", "android"],
     "note": "TODO(http://crbug.com/106682): Flag this with can_be_recommended when bug is fixed.",
     "policy_pref_mapping_tests": [
       {
@@ -2359,7 +2377,7 @@
   },
 
   "CookiesSessionOnlyForUrls": {
-    "os": ["win", "linux", "mac", "chromeos"],
+    "os": ["win", "linux", "mac", "chromeos", "android"],
     "note": "TODO(http://crbug.com/106682): Flag this with can_be_recommended when bug is fixed.",
     "policy_pref_mapping_tests": [
       {
@@ -2418,7 +2436,7 @@
   },
 
   "JavaScriptAllowedForUrls": {
-    "os": ["win", "linux", "mac", "chromeos"],
+    "os": ["win", "linux", "mac", "chromeos", "android"],
     "note": "TODO(http://crbug.com/106682): Flag this with can_be_recommended when bug is fixed.",
     "policy_pref_mapping_tests": [
       {
@@ -2431,7 +2449,7 @@
   },
 
   "JavaScriptBlockedForUrls": {
-    "os": ["win", "linux", "mac", "chromeos"],
+    "os": ["win", "linux", "mac", "chromeos", "android"],
     "note": "TODO(http://crbug.com/106682): Flag this with can_be_recommended when bug is fixed.",
     "policy_pref_mapping_tests": [
       {
@@ -2441,6 +2459,30 @@
     ]
   },
 
+  "JavaScriptJitAllowedForSites": {
+    "os": ["win", "linux", "mac", "chromeos"],
+    "note": "TODO(http://crbug.com/106682): Flag this with can_be_recommended when bug is fixed.",
+    "policy_pref_mapping_tests": [
+      {
+        "policies": {
+          "JavaScriptJitAllowedForSites": ["[*.]google.com"]
+        },
+        "prefs": { "profile.managed_javascript_jit_allowed_for_sites": {} }
+      }
+    ]
+  },
+
+  "JavaScriptJitBlockedForSites": {
+    "os": ["win", "linux", "mac", "chromeos"],
+    "note": "TODO(http://crbug.com/106682): Flag this with can_be_recommended when bug is fixed.",
+    "policy_pref_mapping_tests": [
+      {
+        "policies": { "JavaScriptJitBlockedForSites": ["[*.]google.com"]},
+        "prefs": { "profile.managed_javascript_jit_blocked_for_sites": {} }
+      }
+    ]
+  },
+
   "KeygenAllowedForUrls": {
     "note": "This policy is retired, see https://crbug.com/568184."
   },
@@ -2458,7 +2500,7 @@
   },
 
   "PopupsAllowedForUrls": {
-    "os": ["win", "linux", "mac", "chromeos"],
+    "os": ["win", "linux", "mac", "chromeos", "android"],
     "note": "TODO(http://crbug.com/106682): Flag this with can_be_recommended when bug is fixed.",
     "policy_pref_mapping_tests": [
       {
@@ -2469,7 +2511,7 @@
   },
 
   "PopupsBlockedForUrls": {
-    "os": ["win", "linux", "mac", "chromeos"],
+    "os": ["win", "linux", "mac", "chromeos", "android"],
     "note": "TODO(http://crbug.com/106682): Flag this with can_be_recommended when bug is fixed.",
     "policy_pref_mapping_tests": [
       {
@@ -2502,7 +2544,7 @@
   },
 
   "SensorsAllowedForUrls": {
-    "os": ["win", "linux", "mac"],
+    "os": ["win", "linux", "mac", "android"],
     "note": "TODO(http://crbug.com/106682): Flag this with can_be_recommended when bug is fixed.",
     "policy_pref_mapping_tests": [
       {
@@ -2513,7 +2555,7 @@
   },
 
   "SensorsBlockedForUrls": {
-    "os": ["win", "linux", "mac"],
+    "os": ["win", "linux", "mac", "android"],
     "note": "TODO(http://crbug.com/106682): Flag this with can_be_recommended when bug is fixed.",
     "policy_pref_mapping_tests": [
       {
@@ -2661,7 +2703,7 @@
   "InstantEnabled": {},
 
   "TranslateEnabled": {
-    "os": ["win", "linux", "mac", "chromeos"],
+    "os": ["win", "linux", "mac", "chromeos", "android"],
     "can_be_recommended": true,
     "policy_pref_mapping_tests": [
       {
@@ -2707,7 +2749,7 @@
   },
 
   "EditBookmarksEnabled": {
-    "os": ["win", "linux", "mac", "chromeos"],
+    "os": ["win", "linux", "mac", "chromeos", "android"],
     "policy_pref_mapping_tests": [
       {
         "policies": {"EditBookmarksEnabled": false},
@@ -2954,7 +2996,7 @@
   },
 
   "DisableSafeBrowsingProceedAnyway": {
-    "os": ["win", "linux", "mac", "chromeos"],
+    "os": ["win", "linux", "mac", "chromeos", "android"],
     "policy_pref_mapping_tests": [
       {
         "policies": {"DisableSafeBrowsingProceedAnyway": true},
@@ -2990,7 +3032,7 @@
   },
 
   "BuiltInDnsClientEnabled": {
-    "os": ["win", "linux", "mac"],
+    "os": ["win", "linux", "mac", "android"],
     "policy_pref_mapping_tests": [
       {
         "policies": {
@@ -3061,6 +3103,7 @@
   },
 
   "VariationsRestrictParameter": {
+    "note": "TODO(http://crbug.com/1141521): enable this policy test on Android",
     "policy_pref_mapping_tests": [
       {
         "policies": {
@@ -3164,7 +3207,7 @@
   },
 
   "ForceBrowserSignin": {
-    "os": ["win"],
+    "os": ["win", "android"],
     "policy_pref_mapping_tests": [
       {
         "policies": {
@@ -3254,7 +3297,7 @@
   },
 
   "SSLVersionMin": {
-    "os": ["win", "linux", "mac", "chromeos"],
+    "os": ["win", "linux", "mac", "chromeos", "android"],
     "policy_pref_mapping_tests": [
       {
         "policies": { "SSLVersionMin": "tls1.1" },
@@ -3373,16 +3416,10 @@
   "AllowedDomainsForApps": {
     "policy_pref_mapping_tests": [
       {
+        "os": ["win", "linux", "mac", "chromeos", "android"],
         "policies": {
           "AllowedDomainsForApps": "google.com,mit.edu"
         },
-        "os": [
-          "win",
-          "linux",
-          "mac",
-          "chromeos",
-          "android"
-        ],
         "prefs": { "settings.allowed_domains_for_apps": {} }
       }
     ]
@@ -3710,7 +3747,7 @@
   },
 
   "GloballyScopeHTTPAuthCacheEnabled": {
-    "os": ["win", "linux", "mac", "chromeos", "android"],
+    "os": ["win", "linux", "mac", "chromeos"],
     "policy_pref_mapping_tests": [
       {
         "policies": {
@@ -4743,7 +4780,7 @@
   },
 
   "SSLErrorOverrideAllowed": {
-    "os": ["win", "linux", "mac", "chromeos"],
+    "os": ["win", "linux", "mac", "chromeos", "android"],
     "policy_pref_mapping_tests": [
       {
         "policies": { "SSLErrorOverrideAllowed": true },
@@ -4753,7 +4790,7 @@
   },
 
   "SSLErrorOverrideAllowedForOrigins": {
-    "os": ["win", "mac", "linux", "chromeos"],
+    "os": ["win", "mac", "linux", "chromeos", "android"],
     "policy_pref_mapping_tests": [
       {
         "policies": { "SSLErrorOverrideAllowedForOrigins": ["google.com"] },
@@ -5195,7 +5232,7 @@
   },
 
   "AmbientAuthenticationInPrivateModesEnabled":{
-    "os": ["win", "linux", "mac", "chromeos", "android"],
+    "os": ["win", "linux", "mac", "chromeos"],
     "policy_pref_mapping_tests": [
       {
         "policies": {
@@ -5329,7 +5366,7 @@
   },
 
   "SignedHTTPExchangeEnabled": {
-    "os": ["win", "linux", "mac", "chromeos", "android"],
+    "os": ["win", "linux", "mac", "chromeos"],
     "policy_pref_mapping_tests": [
       {
         "policies": {
@@ -5399,7 +5436,8 @@
   },
 
   "SharedClipboardEnabled": {
-    "os": ["win", "linux", "mac", "chromeos", "android"],
+    "note": "TODO(http://crbug.com/1141521): enable this policy test on Android",
+    "os": ["win", "linux", "mac", "chromeos"],
     "policy_pref_mapping_tests": [
       {
         "policies": {
@@ -7285,6 +7323,7 @@
   },
 
   "CloudReportingEnabled": {
+    "note": "TODO(http://crbug.com/1141521): enable this policy test on Android",
     "os": ["win", "linux", "mac", "chromeos"],
     "policy_pref_mapping_tests": [
       {
@@ -7912,7 +7951,7 @@
     "reason_for_missing_test": "Maps into CrosSettings"
   },
   "BackForwardCacheEnabled": {
-    "os": ["android"],
+    "note": "TODO(http://crbug.com/1141521): enable this policy test on Android",
     "policy_pref_mapping_tests": [
       {
         "policies": { "BackForwardCacheEnabled": true },
@@ -8126,7 +8165,8 @@
     ]
   },
   "BrowsingDataLifetime": {
-    "os": ["win", "linux", "mac", "chromeos", "android"],
+    "note": "TODO(http://crbug.com/1141521): enable this policy test on Android",
+    "os": ["win", "linux", "mac", "chromeos"],
     "policy_pref_mapping_tests": [
       {
         "policies": { "BrowsingDataLifetime": [{"data_types": ["browsing_history"], "time_to_live_in_hours": 2}], "SyncDisabled": true },
@@ -8157,6 +8197,7 @@
     ]
   },
   "ClearBrowsingDataOnExitList": {
+    "note": "TODO(http://crbug.com/1141521): enable this policy test on Android",
     "os": ["win", "linux", "mac", "chromeos"],
     "policy_pref_mapping_tests": [
       {
@@ -8195,7 +8236,7 @@
     ]
   },
   "WebXRImmersiveArEnabled": {
-    "os": ["android"],
+    "note": "TODO(http://crbug.com/1141521): enable this policy test on Android",
     "policy_pref_mapping_tests": [
       {
         "policies": {
diff --git a/chrome/test/data/webui/chromeos/personalization_app/test_mojo_interface_provider.js b/chrome/test/data/webui/chromeos/personalization_app/test_mojo_interface_provider.js
index 3e31019..4edf9a8 100644
--- a/chrome/test/data/webui/chromeos/personalization_app/test_mojo_interface_provider.js
+++ b/chrome/test/data/webui/chromeos/personalization_app/test_mojo_interface_provider.js
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import {unguessableTokenToString} from 'chrome://personalization/common/utils.js';
 import {assertTrue} from '../../chai_assert.js';
 import {TestBrowserProxy} from '../../test_browser_proxy.m.js';
 
@@ -56,6 +57,24 @@
       },
     ];
 
+    /** @type {?Array<!chromeos.personalizationApp.mojom.LocalImage>} */
+    this.localImages = [
+      {
+        id: {high: BigInt(100), low: BigInt(10)},
+        name: 'LocalImage0',
+      },
+      {
+        id: {high: BigInt(200), low: BigInt(20)},
+        name: 'LocalImage1',
+      }
+    ];
+
+    /** @type {!Object<string, string>} */
+    this.localImageData = {
+      '100,10': 'localimage0data',
+      '200,20': 'localimage1data',
+    };
+
     /**
      * @public
      * @type {!chromeos.personalizationApp.mojom.WallpaperImage}
@@ -99,13 +118,14 @@
   /** @override */
   getLocalImages() {
     this.methodCalled('getLocalImages');
-    return Promise.resolve({images: []});
+    return Promise.resolve({images: this.localImages});
   }
 
   /** @override */
   getLocalImageThumbnail(id) {
     this.methodCalled('getLocalImageThumbnail', id);
-    return Promise.resolve({data: ''});
+    return Promise.resolve(
+        {data: this.localImageData[unguessableTokenToString(id)]});
   }
 
   /** @override */
diff --git a/chrome/test/data/webui/chromeos/personalization_app/wallpaper_collections_element_test.js b/chrome/test/data/webui/chromeos/personalization_app/wallpaper_collections_element_test.js
index 824b494..e50bb18 100644
--- a/chrome/test/data/webui/chromeos/personalization_app/wallpaper_collections_element_test.js
+++ b/chrome/test/data/webui/chromeos/personalization_app/wallpaper_collections_element_test.js
@@ -2,9 +2,10 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import {unguessableTokenToString} from 'chrome://personalization/common/utils.js';
 import {emptyState} from 'chrome://personalization/trusted/personalization_reducers.js';
-import {promisifySendCollectionsForTesting, WallpaperCollections} from 'chrome://personalization/trusted/wallpaper_collections_element.js';
-import {assertDeepEquals, assertFalse, assertTrue} from '../../chai_assert.js';
+import {kMaximumImageThumbnailsCount, promisifyIframeFunctionsForTesting, WallpaperCollections} from 'chrome://personalization/trusted/wallpaper_collections_element.js';
+import {assertDeepEquals, assertEquals, assertFalse, assertTrue} from '../../chai_assert.js';
 import {waitAfterNextRender} from '../../test_util.m.js';
 import {assertWindowObjectsEqual, baseSetup, initElement, teardownElement} from './personalization_app_test_utils.js';
 import {TestWallpaperProvider} from './test_mojo_interface_provider.js';
@@ -45,7 +46,8 @@
   });
 
   test('shows wallpaper collections when loaded', async () => {
-    const sendCollectionsPromise = promisifySendCollectionsForTesting();
+    const {sendCollections: sendCollectionsPromise} =
+        promisifyIframeFunctionsForTesting();
     wallpaperCollectionsElement = initElement(WallpaperCollections.is);
 
     const spinner = wallpaperCollectionsElement.shadowRoot.querySelector(
@@ -72,6 +74,33 @@
     assertDeepEquals(wallpaperProvider.collections, data);
   });
 
+  test('sends local images when loaded', async () => {
+    const {sendLocalImages: sendLocalImagesPromise} =
+        promisifyIframeFunctionsForTesting();
+
+    wallpaperCollectionsElement = initElement(WallpaperCollections.is);
+
+    personalizationStore.data.loading = {
+      collections: false,
+      local: {images: false}
+    };
+    personalizationStore.data.local.images = wallpaperProvider.localImages;
+    personalizationStore.data.backdrop.collections =
+        wallpaperProvider.collections;
+    personalizationStore.notifyObservers();
+
+    // Wait for |sendLocalImages| to be called.
+    const [target, data] = await sendLocalImagesPromise;
+    await waitAfterNextRender(wallpaperCollectionsElement);
+
+    const iframe =
+        wallpaperCollectionsElement.shadowRoot.querySelector('iframe');
+    assertFalse(iframe.hidden);
+
+    assertWindowObjectsEqual(iframe.contentWindow, target);
+    assertDeepEquals(wallpaperProvider.localImages, data);
+  });
+
   test('shows error when fails to load', async () => {
     wallpaperCollectionsElement = initElement(WallpaperCollections.is);
 
@@ -97,17 +126,24 @@
         wallpaperCollectionsElement.shadowRoot.querySelector('iframe').hidden);
   });
 
-  test('loads backdrop data and saves to store', async () => {
+  test('loads backdrop and local data and saves to store', async () => {
     // Make sure state starts at expected value.
     assertDeepEquals(emptyState(), personalizationStore.data);
     // Actually run the reducers.
     personalizationStore.setReducersEnabled(true);
 
-    const sendCollectionsPromise = promisifySendCollectionsForTesting();
+    const {
+      sendCollections: sendCollectionsPromise,
+      sendLocalImages: sendLocalImagesPromise
+    } = promisifyIframeFunctionsForTesting();
+
     wallpaperCollectionsElement = initElement(WallpaperCollections.is);
 
-    const [_, data] = await sendCollectionsPromise;
-    assertDeepEquals(wallpaperProvider.collections, data);
+    const [_, collections] = await sendCollectionsPromise;
+    assertDeepEquals(wallpaperProvider.collections, collections);
+
+    const [__, localImages] = await sendLocalImagesPromise;
+    assertDeepEquals(wallpaperProvider.localImages, localImages);
 
     assertDeepEquals(
         {
@@ -121,14 +157,94 @@
     );
     assertDeepEquals(
         {
+          images: wallpaperProvider.localImages,
+          data: wallpaperProvider.localImageData,
+        },
+        personalizationStore.data.local,
+    );
+    assertDeepEquals(
+        {
           ...emptyState().loading,
           collections: false,
           images: {
             'id_0': false,
             'id_1': false,
           },
+          local: {
+            images: false,
+            data: {
+              '100,10': false,
+              '200,20': false,
+            },
+          },
         },
         personalizationStore.data.loading,
     );
   });
+
+  test(
+      'sends the first local images that successfully load thumbnails',
+      async () => {
+        // Set up store data. Local image list is loaded, but thumbnails are
+        // still loading in.
+        personalizationStore.data.loading.local.images = false;
+        personalizationStore.data.local.images = [];
+        for (let i = 0; i < kMaximumImageThumbnailsCount * 2; i++) {
+          personalizationStore.data.local.images.push(
+              {id: {high: BigInt(i * 2), low: BigInt(i)}, name: `local-${i}`});
+          personalizationStore.data.loading.local.data[`${i * 2},${i}`] = true;
+        }
+        // Collections are finished loading.
+        personalizationStore.data.backdrop.collections =
+            wallpaperProvider.collections;
+        personalizationStore.data.loading.collections = false;
+
+        let {sendLocalImages, sendLocalImageData} =
+            promisifyIframeFunctionsForTesting();
+
+        wallpaperCollectionsElement = initElement(WallpaperCollections.is);
+
+        await sendLocalImages;
+
+        // No thumbnails loaded so none sent.
+        assertEquals(0, wallpaperCollectionsElement.sentLocalImages_.size);
+
+        // First thumbnail loads in.
+        personalizationStore.data.loading.local.data = {'0,0': false};
+        personalizationStore.data.local.data = {'0,0': 'local_data_0'};
+        personalizationStore.notifyObservers();
+
+        // This thumbnail should have just loaded in.
+        let sent = await sendLocalImageData;
+        assertDeepEquals(
+            ['0,0'], Array.from(wallpaperCollectionsElement.sentLocalImages_));
+        assertEquals('0,0', unguessableTokenToString(sent[1].id));
+        assertEquals('local_data_0', sent[2]);
+
+        sendLocalImageData =
+            promisifyIframeFunctionsForTesting().sendLocalImageData;
+
+        // Second thumbnail fails loading. Third succeeds.
+        personalizationStore.data.loading.local.data = {
+          ...personalizationStore.data.loading.local.data,
+          '2,1': false,
+          '4,2': false,
+        };
+        personalizationStore.data.local.data = {
+          ...personalizationStore.data.local.data,
+          '2,1': null,
+          '4,2': 'local_data_2',
+        };
+        personalizationStore.notifyObservers();
+
+        sent = await sendLocalImageData;
+        // '4,2' successfully loaded and '2,1' did not. '4,2' should have been
+        // sent to iframe.
+        assertDeepEquals(
+            ['0,0', '4,2'],
+            Array.from(wallpaperCollectionsElement.sentLocalImages_));
+
+        assertEquals('4,2', unguessableTokenToString(sent[1].id));
+        assertEquals('local_data_2', sent[2]);
+      });
 }
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 2e67fa0..f329893 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
@@ -877,9 +877,12 @@
       cartItems[1].querySelector('.thumbnail-container').click();
 
       // Assert.
-      Array(4).forEach(
-          index => assertEquals(
-              1, metrics.count('NewTabPage.Carts.ClickCart', index)));
+      assertEquals(4, testProxy.handler.getCallCount('prepareForNavigation'));
+      for (let index = 0; index < 4; index++) {
+        assertEquals(1, metrics.count('NewTabPage.Carts.ClickCart', index));
+        assertEquals(
+            true, testProxy.handler.getArgs('prepareForNavigation')[index][1]);
+      }
       assertEquals(0, testProxy.handler.getCallCount('getDiscountURL'));
     });
 
diff --git a/chromecast/media/gpu/cast_gpu_factory_impl.cc b/chromecast/media/gpu/cast_gpu_factory_impl.cc
index debd3ef..383362a 100644
--- a/chromecast/media/gpu/cast_gpu_factory_impl.cc
+++ b/chromecast/media/gpu/cast_gpu_factory_impl.cc
@@ -14,7 +14,6 @@
 #include "media/base/media_util.h"
 #include "media/gpu/gpu_video_accelerator_util.h"
 #include "media/gpu/ipc/client/gpu_video_decode_accelerator_host.h"
-#include "media/gpu/ipc/common/media_messages.h"
 #include "media/mojo/clients/mojo_video_decoder.h"
 #include "media/mojo/clients/mojo_video_encode_accelerator.h"
 #include "services/viz/public/cpp/gpu/context_provider_command_buffer.h"
diff --git a/chromeos/components/personalization_app/resources/common/BUILD.gn b/chromeos/components/personalization_app/resources/common/BUILD.gn
index 0ba2051..653a628 100644
--- a/chromeos/components/personalization_app/resources/common/BUILD.gn
+++ b/chromeos/components/personalization_app/resources/common/BUILD.gn
@@ -16,6 +16,9 @@
   ]
 }
 
+js_library("styles") {
+}
+
 js_library("utils") {
 }
 
@@ -35,6 +38,7 @@
   in_files = [
     "common/constants.js",
     "common/iframe_api.js",
+    "common/styles.js",
     "common/utils.js",
   ]
 }
diff --git a/chromeos/components/personalization_app/resources/common/constants.js b/chromeos/components/personalization_app/resources/common/constants.js
index 67d9a80..a94d2f9 100644
--- a/chromeos/components/personalization_app/resources/common/constants.js
+++ b/chromeos/components/personalization_app/resources/common/constants.js
@@ -14,13 +14,20 @@
 export const EventType = {
   SEND_COLLECTIONS: 'send_collections',
   SELECT_COLLECTION: 'select_collection',
+  SELECT_LOCAL_COLLECTION: 'select_local_collection',
   SEND_IMAGES: 'send_images',
+  SEND_LOCAL_IMAGE_DATA: 'send_local_image_data',
+  SEND_LOCAL_IMAGES: 'send_local_images',
   SELECT_IMAGE: 'select_image',
+  SELECT_LOCAL_IMAGE: 'select_local_image',
 };
 
 /**
- * @typedef {{ type: EventType, collections:
- *     !Array<!chromeos.personalizationApp.mojom.WallpaperCollection> }}
+ * @typedef {{
+ *   type: EventType,
+ *   collections:
+ *     !Array<!chromeos.personalizationApp.mojom.WallpaperCollection>,
+ * }}
  */
 export let SendCollectionsEvent;
 
@@ -30,12 +37,37 @@
 export let SelectCollectionEvent;
 
 /**
- * @typedef {{ type: EventType, images:
- *     !Array<!chromeos.personalizationApp.mojom.WallpaperImage> }}
+ * @typedef {{ type: EventType }}
+ */
+export let SelectLocalCollectionEvent;
+
+/**
+ * @typedef {{
+ *   type: EventType,
+ *   images: !Array<!chromeos.personalizationApp.mojom.WallpaperImage>,
+ * }}
  */
 export let SendImagesEvent;
 
 /**
+ * @typedef {{
+ *   type: EventType,
+ *   images: !Array<!chromeos.personalizationApp.mojom.LocalImage>,
+ * }}
+ */
+export let SendLocalImagesEvent;
+
+/**
+ * @typedef {{
+ *   type: EventType,
+ *   id: !mojoBase.mojom.UnguessableToken,
+ *   data: string,
+ * }}
+ */
+export let SendLocalImageDataEvent;
+
+
+/**
  * @typedef {{ type: EventType, assetId: bigint }}
  */
 export let SelectImageEvent;
diff --git a/chromeos/components/personalization_app/resources/common/iframe_api.js b/chromeos/components/personalization_app/resources/common/iframe_api.js
index 27664c3..7fd9caf 100644
--- a/chromeos/components/personalization_app/resources/common/iframe_api.js
+++ b/chromeos/components/personalization_app/resources/common/iframe_api.js
@@ -3,7 +3,7 @@
 // found in the LICENSE file.
 
 import {assert, assertNotReached} from '/assert.m.js';
-import {EventType, SelectCollectionEvent, SelectImageEvent, SendCollectionsEvent, SendImagesEvent, trustedOrigin, untrustedOrigin} from './constants.js';
+import {EventType, SelectCollectionEvent, SelectImageEvent, SelectLocalCollectionEvent, SendCollectionsEvent, SendImagesEvent, SendLocalImageDataEvent, SendLocalImagesEvent, trustedOrigin, untrustedOrigin} from './constants.js';
 import {isNonEmptyArray} from './utils.js';
 
 /**
@@ -29,7 +29,7 @@
 /**
  * Send an array of wallpaper images to chrome-untrusted://.
  * Will clear the page if images is empty array.
- * @param {!Object} target the iframe window to send the message to.
+ * @param {!Window} target the iframe window to send the message to.
  * @param {!Array<!chromeos.personalizationApp.mojom.WallpaperImage>} images
  */
 export function sendImages(target, images) {
@@ -39,21 +39,37 @@
 }
 
 /**
+ * Send an array of local images to chrome-untrusted://.
+ * @param {!Window} target the iframe window to send the message to.
+ * @param {!Array<!chromeos.personalizationApp.mojom.LocalImage>} images
+ */
+export function sendLocalImages(target, images) {
+  /** @type {!SendLocalImagesEvent} */
+  const event = {type: EventType.SEND_LOCAL_IMAGES, images};
+  target.postMessage(event, untrustedOrigin);
+}
+
+/**
+ * @param {!Window} target
+ * @param {!chromeos.personalizationApp.mojom.LocalImage} image
+ * @param {!string} data
+ */
+export function sendLocalImageData(target, image, data) {
+  /** @type {!SendLocalImageDataEvent} */
+  const event = {type: EventType.SEND_LOCAL_IMAGE_DATA, id: image.id, data};
+  target.postMessage(event, untrustedOrigin);
+}
+
+/**
  * Called from trusted code to validate that a received postMessage event
  * contains valid data. Ignores messages that are not of the expected type.
  * @param {!Event} event from untrusted to select a collection or image
- * @param {!EventType} expectedEventType
  * @param {Array<!T>} choices array of valid objects to pick from
  * @return {!T}
  * @template T
  */
-export function validateReceivedSelection(event, expectedEventType, choices) {
+export function validateReceivedSelection(event, choices) {
   assert(isNonEmptyArray(choices), 'choices must be a non-empty array');
-  assert(
-      event.origin === untrustedOrigin, 'Message not from the correct origin');
-  assert(
-      event.data.type === expectedEventType,
-      `Expected event type: ${expectedEventType}`);
 
   /** @type {SelectCollectionEvent|SelectImageEvent} */
   const data = event.data;
@@ -92,6 +108,12 @@
   target.postMessage(event, trustedOrigin);
 }
 
+export function selectLocalCollection(target) {
+  /** @type {!SelectLocalCollectionEvent} */
+  const event = {type: EventType.SELECT_LOCAL_COLLECTION};
+  target.postMessage(event, trustedOrigin);
+}
+
 /**
  * Select an image. Sent from untrusted to trusted.
  * @param {!Object} target the window to post the message to.
@@ -124,8 +146,9 @@
     case EventType.SEND_COLLECTIONS:
       assert(isNonEmptyArray(data.collections), 'Expected collections array');
       return data.collections;
+    case EventType.SEND_LOCAL_IMAGES:
     case EventType.SEND_IMAGES:
-      // Images array may be empty to clear the prior view.
+      // Images array may be empty.
       assert(Array.isArray(data.images), 'Expected images array');
       return data.images;
     default:
diff --git a/chromeos/components/personalization_app/resources/untrusted/styles.js b/chromeos/components/personalization_app/resources/common/styles.js
similarity index 67%
rename from chromeos/components/personalization_app/resources/untrusted/styles.js
rename to chromeos/components/personalization_app/resources/common/styles.js
index 443db54..cfd9af9 100644
--- a/chromeos/components/personalization_app/resources/untrusted/styles.js
+++ b/chromeos/components/personalization_app/resources/common/styles.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 '//personalization/polymer/v3_0/polymer/polymer_bundled.min.js';
-
 const styles = document.createElement('dom-module');
 
 styles.innerHTML = `<template>
@@ -17,7 +15,14 @@
             object-fit: contain;
             width: 100%;
         }
+        .photo-container > .collection-name,
+        .photo-container > .image-name {
+            bottom: 0;
+            position: absolute;
+            text-align: center;
+            width: 100%;
+        }
     </style>
   </template>`;
 
-styles.register('untrusted-style');
+styles.register('common-style');
diff --git a/chromeos/components/personalization_app/resources/trusted/BUILD.gn b/chromeos/components/personalization_app/resources/trusted/BUILD.gn
index c373c0d..2dd4ce8 100644
--- a/chromeos/components/personalization_app/resources/trusted/BUILD.gn
+++ b/chromeos/components/personalization_app/resources/trusted/BUILD.gn
@@ -7,6 +7,7 @@
 import("//tools/polymer/html_to_js.gni")
 
 polymer_element_files = [
+  "local_images_element.js",
   "wallpaper_collections_element.js",
   "wallpaper_images_element.js",
   "wallpaper_selected_element.js",
@@ -25,6 +26,18 @@
   "styles.js",
 ]
 
+js_library("local_images_element") {
+  deps = [
+    ":personalization_controller",
+    ":styles",
+    "../common:constants",
+    "../common:styles",
+    "//third_party/polymer/v3_0/components-chromium/iron-list:iron-list",
+    "//third_party/polymer/v3_0/components-chromium/paper-spinner:paper-spinner-lite",
+    "//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
+  ]
+}
+
 js_library("mojo_interface_provider") {
   deps = [
     "../../mojom:mojom_js_library_for_compile",
@@ -38,6 +51,7 @@
 
 js_library("personalization_app") {
   deps = [
+    ":local_images_element",
     ":personalization_message_handler",
     ":personalization_reducers",
     ":personalization_router_element",
@@ -133,6 +147,7 @@
   is_polymer3 = true
   closure_flags = default_closure_args + [ "language_in=ECMASCRIPT_2020" ]
   deps = [
+    ":local_images_element",
     ":mojo_interface_provider",
     ":personalization_actions",
     ":personalization_app",
diff --git a/chromeos/components/personalization_app/resources/trusted/local_images_element.html b/chromeos/components/personalization_app/resources/trusted/local_images_element.html
new file mode 100644
index 0000000..d8e3c3de
--- /dev/null
+++ b/chromeos/components/personalization_app/resources/trusted/local_images_element.html
@@ -0,0 +1,14 @@
+<style include="trusted-style common-style"></style>
+<paper-spinner-lite active="[[imagesLoading_]]">
+</paper-spinner-lite>
+<iron-list items="[[getImages_(hidden, images_)]]" grid>
+  <template>
+    <div class="photo-container">
+      <template is="dom-if"
+          if="[[shouldShowImage_(item, imageData_, imageDataLoading_)]]">
+        <img src="[[getImageData_(item, imageData_)]]">
+      </template>
+      <p class="image-name">[[item.name]]</p>
+    </div>
+  </template>
+</iron-list>
diff --git a/chromeos/components/personalization_app/resources/trusted/local_images_element.js b/chromeos/components/personalization_app/resources/trusted/local_images_element.js
new file mode 100644
index 0000000..6765bca
--- /dev/null
+++ b/chromeos/components/personalization_app/resources/trusted/local_images_element.js
@@ -0,0 +1,119 @@
+// 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.
+
+/**
+ * @fileoverview WallpaperImages displays a list of wallpaper images from a
+ * wallpaper collection. It requires a parameter collection-id to fetch
+ * and display the images. It also caches the list of wallpaper images by
+ * wallpaper collection id to avoid refetching data unnecessarily.
+ */
+
+import 'chrome://resources/polymer/v3_0/paper-spinner/paper-spinner-lite.js';
+import 'chrome://resources/polymer/v3_0/iron-list/iron-list.js';
+import './styles.js';
+import '../common/styles.js';
+import {html} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+import {unguessableTokenToString} from '../common/utils.js';
+import {WithPersonalizationStore} from './personalization_store.js';
+
+/** @polymer */
+export class LocalImages extends WithPersonalizationStore {
+  static get is() {
+    return 'local-images';
+  }
+
+  static get template() {
+    return html`{__html_template__}`;
+  }
+
+  static get properties() {
+    return {
+      hidden: {
+        type: Boolean,
+        value: true,
+        reflectToAttribute: true,
+      },
+
+      /**
+       * @type {!Array<!chromeos.personalizationApp.mojom.LocalImage>}
+       * @private
+       */
+      images_: {
+        type: Array,
+      },
+
+      /** @private */
+      imagesLoading_: {
+        type: Boolean,
+      },
+
+      /**
+       * Mapping of stringified local image id to data url.
+       * @type {!Object<string, string>}
+       * @private
+       */
+      imageData_: {
+        type: Object,
+      },
+
+      /**
+       * Mapping of stringified local image id to boolean.
+       * @type {!Object<string, boolean>}
+       * @private
+       */
+      imageDataLoading_: {
+        type: Object,
+      },
+    };
+  }
+
+  /** @override */
+  connectedCallback() {
+    super.connectedCallback();
+    this.watch('images_', state => state.local.images);
+    this.watch('imagesLoading_', state => state.loading.local.images);
+    this.watch('imageData_', state => state.local.data);
+    this.watch('imageDataLoading_', state => state.loading.local.data);
+    this.updateFromStore();
+  }
+
+  /**
+   * Forces iron-list to re-evaluate when hidden changes.
+   * @private
+   * @param {boolean} hidden
+   * @param {!Array<!chromeos.personalizationApp.mojom.LocalImage>} images
+   * @return {!Array<!chromeos.personalizationApp.mojom.LocalImage>}
+   */
+  getImages_(hidden, images) {
+    return hidden ? [] : images;
+  }
+
+  /**
+   * @private
+   * @param {chromeos.personalizationApp.mojom.LocalImage} image
+   * @param {Object<string, string>} imageData
+   * @param {Object<string, boolean>} imageDataLoading
+   * @return {boolean}
+   */
+  shouldShowImage_(image, imageData, imageDataLoading) {
+    if (!image || !imageData || !imageDataLoading) {
+      return false;
+    }
+    const key = unguessableTokenToString(image.id);
+    return !!imageData[key] && imageDataLoading[key] === false;
+  }
+
+  /**
+   * @private
+   * @param {chromeos.personalizationApp.mojom.LocalImage} image
+   * @param {Object<string, string>} imageData
+   * @return {string}
+   */
+  getImageData_(image, imageData) {
+    const key = unguessableTokenToString(image.id);
+    return imageData[key];
+  }
+}
+
+customElements.define(LocalImages.is, LocalImages);
diff --git a/chromeos/components/personalization_app/resources/trusted/personalization_app.js b/chromeos/components/personalization_app/resources/trusted/personalization_app.js
index 8f4d6b8..435480d 100644
--- a/chromeos/components/personalization_app/resources/trusted/personalization_app.js
+++ b/chromeos/components/personalization_app/resources/trusted/personalization_app.js
@@ -9,6 +9,7 @@
  */
 
 import '/strings.m.js';
+import './local_images_element.js';
 import './personalization_router_element.js';
 import './wallpaper_collections_element.js';
 import './wallpaper_images_element.js';
diff --git a/chromeos/components/personalization_app/resources/trusted/personalization_controller.js b/chromeos/components/personalization_app/resources/trusted/personalization_controller.js
index 475a11b..b76fe99 100644
--- a/chromeos/components/personalization_app/resources/trusted/personalization_controller.js
+++ b/chromeos/components/personalization_app/resources/trusted/personalization_controller.js
@@ -18,30 +18,31 @@
  * @param {!chromeos.personalizationApp.mojom.WallpaperProviderInterface}
  *     provider
  * @param {!PersonalizationStore} store
- * @return {!Promise<Array<chromeos.personalizationApp.mojom.WallpaperCollection>>}
  */
-export async function fetchCollections(provider, store) {
+async function fetchCollections(provider, store) {
   let {collections} = await provider.fetchCollections();
   if (!isNonEmptyArray(collections)) {
     console.warn('Failed to fetch wallpaper collections');
     collections = null;
   }
   store.dispatch(setCollectionsAction(collections));
-  return collections;
 }
 
 /**
  * Fetch all of the wallpaper collections one at a time.
- * @param {!Array<!chromeos.personalizationApp.mojom.WallpaperCollection>}
- *     collections
  * @param {!chromeos.personalizationApp.mojom.WallpaperProviderInterface}
  *     provider
  * @param {!PersonalizationStore} store
  */
-export async function fetchAllImagesForCollections(
-    collections, provider, store) {
+async function fetchAllImagesForCollections(provider, store) {
+  const collections = store.data.backdrop.collections;
+  if (!Array.isArray(collections)) {
+    console.warn(
+        'Cannot fetch data for collections when it is not initialized');
+    return;
+  }
   store.dispatch(beginLoadImagesForCollectionsAction(collections));
-  for (const {id} of collections) {
+  for (const {id} of /** @type {!Array<{id: string}>} */ (collections)) {
     let {images} = await provider.fetchImagesForCollection(id);
     if (!isNonEmptyArray(images)) {
       console.warn('Failed to fetch images for collection id', id);
@@ -52,12 +53,12 @@
 }
 
 /**
- * Get list of local images from disk.
+ * Get list of local images from disk and save it to the store.
  * @param {!chromeos.personalizationApp.mojom.WallpaperProviderInterface}
  *     provider
  * @param {!PersonalizationStore} store
  */
-export async function getLocalImages(provider, store) {
+async function getLocalImages(provider, store) {
   const {images} = await provider.getLocalImages();
   if (images == null) {
     console.warn('Failed to fetch local images');
@@ -66,15 +67,15 @@
 }
 
 /**
- * Get an image thumbnail for each image one at a time.
+ * Get an image thumbnail for every local image one at a time.
  * @param {!chromeos.personalizationApp.mojom.WallpaperProviderInterface}
  *     provider
  * @param {!PersonalizationStore} store
  */
-export async function getAllLocalImageThumbnails(provider, store) {
+async function getAllLocalImageThumbnails(provider, store) {
   const images = store.data.local.images;
   if (!Array.isArray(images)) {
-    console.warn('Cannot fetch thumbnails when local image list is null');
+    console.warn('Cannot fetch thumbnails of null image list');
     return;
   }
   for (const image of images) {
@@ -124,9 +125,18 @@
  * @param {!PersonalizationStore} store
  */
 export async function initializeBackdropData(provider, store) {
-  const collections = await fetchCollections(provider, store);
-  if (!Array.isArray(collections)) {
-    return;
-  }
-  await fetchAllImagesForCollections(collections, provider, store);
+  await fetchCollections(provider, store);
+  await fetchAllImagesForCollections(provider, store);
+}
+
+/**
+ * Gets list of local images, then fetches image thumbnails for each local
+ * image.
+ * @param {!chromeos.personalizationApp.mojom.WallpaperProviderInterface}
+ *     provider
+ * @param {!PersonalizationStore} store
+ */
+export async function initializeLocalData(provider, store) {
+  await getLocalImages(provider, store);
+  await getAllLocalImageThumbnails(provider, store);
 }
diff --git a/chromeos/components/personalization_app/resources/trusted/personalization_message_handler.js b/chromeos/components/personalization_app/resources/trusted/personalization_message_handler.js
index 8506549..b6c746f 100644
--- a/chromeos/components/personalization_app/resources/trusted/personalization_message_handler.js
+++ b/chromeos/components/personalization_app/resources/trusted/personalization_message_handler.js
@@ -2,11 +2,11 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import {EventType} from '../common/constants.js';
+import {assert} from '/assert.m.js';
+import {EventType, untrustedOrigin} from '../common/constants.js';
 import {validateReceivedSelection} from '../common/iframe_api.js';
-
 import {getWallpaperProvider} from './mojo_interface_provider.js';
-import {getCurrentWallpaper, selectWallpaper} from './personalization_controller.js';
+import {selectWallpaper} from './personalization_controller.js';
 import {PersonalizationRouter} from './personalization_router_element.js';
 import {PersonalizationStore} from './personalization_store.js';
 
@@ -18,22 +18,25 @@
  * @param {!Event} event
  */
 export function onMessageReceived(event) {
+  assert(
+      event.origin === untrustedOrigin, 'Message not from the correct origin');
+
   const store = PersonalizationStore.getInstance();
 
   switch (event.data.type) {
     case EventType.SELECT_COLLECTION:
       const collections = store.data.backdrop.collections;
 
-      const selectedCollection = validateReceivedSelection(
-          event, EventType.SELECT_COLLECTION, collections);
-
+      const selectedCollection = validateReceivedSelection(event, collections);
       PersonalizationRouter.instance().selectCollection(selectedCollection.id);
       break;
+    case EventType.SELECT_LOCAL_COLLECTION:
+      PersonalizationRouter.instance().selectLocalCollection();
+      break;
     case EventType.SELECT_IMAGE:
       const collectionId = PersonalizationRouter.instance().collectionId;
       const images = store.data.backdrop.images[collectionId];
-      const selectedImage =
-          validateReceivedSelection(event, EventType.SELECT_IMAGE, images);
+      const selectedImage = validateReceivedSelection(event, images);
       selectWallpaper(selectedImage, getWallpaperProvider(), store);
       break;
   }
diff --git a/chromeos/components/personalization_app/resources/trusted/personalization_reducers.js b/chromeos/components/personalization_app/resources/trusted/personalization_reducers.js
index e1c5d20..253e348 100644
--- a/chromeos/components/personalization_app/resources/trusted/personalization_reducers.js
+++ b/chromeos/components/personalization_app/resources/trusted/personalization_reducers.js
@@ -165,13 +165,24 @@
         ...state,
         images: {...state.images, [action.collectionId]: false},
       });
+    case ActionName.SET_LOCAL_IMAGES:
+      return /** @type {!LoadingState} */ ({
+        ...state,
+        local: {
+          ...state.local,
+          images: false,
+        },
+      });
     case ActionName.SET_LOCAL_IMAGE_DATA:
       return /** @type {!LoadingState} */ ({
         ...state,
         local: {
           ...state.local,
-          data: {...state.local.data, [action.id]: action.data}
-        }
+          data: {
+            ...state.local.data,
+            [action.id]: false,
+          },
+        },
       });
     case ActionName.SET_SELECTED_IMAGE:
       return /** @type {!LoadingState} */ ({...state, selected: false});
diff --git a/chromeos/components/personalization_app/resources/trusted/personalization_router_element.html b/chromeos/components/personalization_app/resources/trusted/personalization_router_element.html
index 27df515..4e8508b 100644
--- a/chromeos/components/personalization_app/resources/trusted/personalization_router_element.html
+++ b/chromeos/components/personalization_app/resources/trusted/personalization_router_element.html
@@ -7,3 +7,5 @@
 </wallpaper-collections>
 <wallpaper-images collection-id="[[queryParams_.id]]"
     hidden$="[[!shouldShowCollectionImages_(path_)]]"></wallpaper-images>
+<!-- do not use hidden$ here - need to listen on property change in element. -->
+<local-images hidden="[[!shouldShowLocalCollection_(path_)]]"></local-images>
diff --git a/chromeos/components/personalization_app/resources/trusted/personalization_router_element.js b/chromeos/components/personalization_app/resources/trusted/personalization_router_element.js
index e4e0f15..5434a3c 100644
--- a/chromeos/components/personalization_app/resources/trusted/personalization_router_element.js
+++ b/chromeos/components/personalization_app/resources/trusted/personalization_router_element.js
@@ -15,6 +15,7 @@
 export const Paths = {
   CollectionImages: '/collection',
   Collections: '/',
+  LocalCollection: '/local',
 };
 
 export class PersonalizationRouter extends PolymerElement {
@@ -67,6 +68,13 @@
   }
 
   /**
+   * Navigate to the local collection page.
+   */
+  selectLocalCollection() {
+    this.setProperties({path_: Paths.LocalCollection, query_: ''});
+  }
+
+  /**
    * @param {string} path
    * @return {boolean}
    * @private
@@ -83,6 +91,15 @@
   shouldShowCollectionImages_(path) {
     return path === Paths.CollectionImages;
   }
+
+  /**
+   * @param {string} path
+   * @return  {boolean}
+   * @private
+   */
+  shouldShowLocalCollection_(path) {
+    return path === Paths.LocalCollection;
+  }
 }
 
 customElements.define(PersonalizationRouter.is, PersonalizationRouter);
diff --git a/chromeos/components/personalization_app/resources/trusted/styles.js b/chromeos/components/personalization_app/resources/trusted/styles.js
index edd091a..993f7db5 100644
--- a/chromeos/components/personalization_app/resources/trusted/styles.js
+++ b/chromeos/components/personalization_app/resources/trusted/styles.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/polymer/v3_0/polymer/polymer_bundled.min.js';
-
 const styles = document.createElement('dom-module');
 
 styles.innerHTML = `<template>
@@ -16,11 +14,11 @@
       paper-spinner-lite[active] {
         display: block;
       }
-      iframe {
+      iframe, iron-list {
         height: 80vh;
         width: 100%;
       }
     </style>
   </template>`;
 
-styles.register('shared-style');
+styles.register('trusted-style');
diff --git a/chromeos/components/personalization_app/resources/trusted/wallpaper_collections_element.html b/chromeos/components/personalization_app/resources/trusted/wallpaper_collections_element.html
index 38bcaa7..e58da8c 100644
--- a/chromeos/components/personalization_app/resources/trusted/wallpaper_collections_element.html
+++ b/chromeos/components/personalization_app/resources/trusted/wallpaper_collections_element.html
@@ -1,13 +1,7 @@
-<style include="shared-style"></style>
+<style include="trusted-style"></style>
 <paper-spinner-lite active="[[collectionsLoading_]]"></paper-spinner-lite>
-<!-- TODO(b/189968254) move local images to untrusted -->
-<dom-repeat items="[[localImages_]]">
-  <template>
-    <img src="[[item]]">
-  </template>
-</dom-repeat>
 <!-- TODO(b/181697575) handle error cases and update error string to UI spec -->
 <p hidden$="[[!hasError_]]" id="error">error</p>
 <iframe id="collections-iframe" frameBorder="0" hidden$="[[!showCollections_]]"
     src="chrome-untrusted://personalization/untrusted/collections.html">
-</iframe>
\ No newline at end of file
+</iframe>
diff --git a/chromeos/components/personalization_app/resources/trusted/wallpaper_collections_element.js b/chromeos/components/personalization_app/resources/trusted/wallpaper_collections_element.js
index 3dc067e..5c8ae32 100644
--- a/chromeos/components/personalization_app/resources/trusted/wallpaper_collections_element.js
+++ b/chromeos/components/personalization_app/resources/trusted/wallpaper_collections_element.js
@@ -12,21 +12,42 @@
 import 'chrome://resources/polymer/v3_0/paper-spinner/paper-spinner-lite.js';
 import './styles.js';
 import {afterNextRender, html} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
-import {sendCollections} from '../common/iframe_api.js';
+import {sendCollections, sendLocalImageData, sendLocalImages} from '../common/iframe_api.js';
 import {isNonEmptyArray, promisifyOnload, unguessableTokenToString} from '../common/utils.js';
 import {getWallpaperProvider} from './mojo_interface_provider.js';
-import {getAllLocalImageThumbnails, getLocalImages, initializeBackdropData} from './personalization_controller.js';
+import {initializeBackdropData, initializeLocalData} from './personalization_controller.js';
 import {WithPersonalizationStore} from './personalization_store.js';
 
 let sendCollectionsFunction = sendCollections;
+let sendLocalImagesFunction = sendLocalImages;
+let sendLocalImageDataFunction = sendLocalImageData;
 
-export function promisifySendCollectionsForTesting() {
-  let resolver;
-  const promise = new Promise((resolve) => resolver = resolve);
-  sendCollectionsFunction = (...args) => resolver(args);
-  return promise;
+/**
+ * Mock out the iframe api functions for testing. Return promises that are
+ * resolved when the function is called by |WallpaperCollectionsElement|.
+ * @return {{
+ *   sendCollections: Promise<?>,
+ *   sendLocalImages: Promise<?>,
+ *   sendLocalImageData: Promise<?>,
+ * }}
+ */
+export function promisifyIframeFunctionsForTesting() {
+  let resolvers = {};
+  const promises = [
+    sendCollections, sendLocalImages, sendLocalImageData
+  ].reduce((result, next) => {
+    result[next.name] = new Promise(resolve => resolvers[next.name] = resolve);
+    return result;
+  }, {});
+  sendCollectionsFunction = (...args) => resolvers[sendCollections.name](args);
+  sendLocalImagesFunction = (...args) => resolvers[sendLocalImages.name](args);
+  sendLocalImageDataFunction = (...args) =>
+      resolvers[sendLocalImageData.name](args);
+  return promises;
 }
 
+export const kMaximumImageThumbnailsCount = 3;
+
 /** @polymer */
 export class WallpaperCollections extends WithPersonalizationStore {
   static get is() {
@@ -54,10 +75,21 @@
 
       /**
        * @private
-       * @type {!Array<string>}
+       * @type {Array<!chromeos.personalizationApp.mojom.LocalImage>}
        */
       localImages_: {
         type: Array,
+        observer: 'onLocalImagesChanged_',
+      },
+
+      /**
+       * Stores a mapping of local image id to thumbnail data.
+       * @private
+       * @type {Object<string, string>}
+       */
+      localImageData_: {
+        type: Object,
+        observer: 'onLocalImageDataChanged_',
       },
 
       /** @private */
@@ -77,12 +109,23 @@
     };
   }
 
+  static get observers() {
+    return ['onLocalImageDataChanged_(localImages_, localImageData_)'];
+  }
+
   constructor() {
     super();
     /** @private */
     this.wallpaperProvider_ = getWallpaperProvider();
     this.iframePromise_ = /** @type {!Promise<!HTMLIFrameElement>} */ (
         promisifyOnload(this, 'collections-iframe', afterNextRender));
+
+    /**
+     * Stores a set of local image ids that have already sent thumbnail data to
+     * untrusted.
+     * @type {!Set<string>}
+     */
+    this.sentLocalImages_ = new Set();
   }
 
   /** @override */
@@ -90,18 +133,12 @@
     super.connectedCallback();
     this.watch('collections_', state => state.backdrop.collections);
     this.watch('collectionsLoading_', state => state.loading.collections);
-    this.watch(
-        'localImages_',
-        state => Array.isArray(state.local.images) ?
-            state.local.images.map(image => unguessableTokenToString(image.id))
-                .filter(id => !!state.local.data[id])
-                .map(id => state.local.data[id]) :
-            null);
+    this.watch('localImages_', state => state.local.images);
+    this.watch('localImageData_', state => state.local.data);
     this.updateFromStore();
     const store = this.getStore();
     initializeBackdropData(this.wallpaperProvider_, store);
-    getLocalImages(this.wallpaperProvider_, store)
-        .then(() => getAllLocalImageThumbnails(this.wallpaperProvider_, store));
+    initializeLocalData(this.wallpaperProvider_, store);
   }
 
   /**
@@ -136,6 +173,46 @@
       sendCollectionsFunction(iframe.contentWindow, this.collections_);
     }
   }
+
+  /**
+   * Send updated local images list to the iframe.
+   * @param {?Array<!chromeos.personalizationApp.mojom.LocalImage>} value
+   */
+  async onLocalImagesChanged_(value) {
+    if (Array.isArray(value)) {
+      const iframe = await this.iframePromise_;
+      sendLocalImagesFunction(
+          /** @type {!Window} */ (iframe.contentWindow), value);
+    }
+  }
+
+  /**
+   * Send up to |maximumImageThumbnailsCount| image thumbnails to untrusted.
+   * @param {?Array<!chromeos.personalizationApp.mojom.LocalImage>} images
+   * @param {?Object<string, string>} imageData
+   */
+  async onLocalImageDataChanged_(images, imageData) {
+    if (!Array.isArray(images) || !imageData) {
+      return;
+    }
+    const iframe = await this.iframePromise_;
+
+    for (const image of images) {
+      if (this.sentLocalImages_.size >= kMaximumImageThumbnailsCount) {
+        return;
+      }
+      const key = unguessableTokenToString(image.id);
+      if (this.sentLocalImages_.has(key)) {
+        continue;
+      }
+      const data = imageData[key];
+      if (data) {
+        sendLocalImageDataFunction(
+            /** @type {!Window} */ (iframe.contentWindow), image, data);
+        this.sentLocalImages_.add(key);
+      }
+    }
+  }
 }
 
 customElements.define(WallpaperCollections.is, WallpaperCollections);
diff --git a/chromeos/components/personalization_app/resources/trusted/wallpaper_images_element.html b/chromeos/components/personalization_app/resources/trusted/wallpaper_images_element.html
index bdac771d..6aea3ac 100644
--- a/chromeos/components/personalization_app/resources/trusted/wallpaper_images_element.html
+++ b/chromeos/components/personalization_app/resources/trusted/wallpaper_images_element.html
@@ -1,4 +1,4 @@
-<style include="shared-style"></style>
+<style include="trusted-style"></style>
 <paper-spinner-lite
     active="[[isLoading_(imagesLoading_, collectionId)]]">
 </paper-spinner-lite>
diff --git a/chromeos/components/personalization_app/resources/untrusted/BUILD.gn b/chromeos/components/personalization_app/resources/untrusted/BUILD.gn
index 60cda24..f1ab85c 100644
--- a/chromeos/components/personalization_app/resources/untrusted/BUILD.gn
+++ b/chromeos/components/personalization_app/resources/untrusted/BUILD.gn
@@ -8,10 +8,10 @@
 
 js_library("collections_grid") {
   deps = [
-    ":styles",
     "../../mojom:mojom_js_library_for_compile",
     "../common:constants",
     "../common:iframe_api",
+    "../common:styles",
     "//third_party/polymer/v3_0/components-chromium/iron-list:iron-list",
     "//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
   ]
@@ -19,31 +19,25 @@
 
 js_library("images_grid") {
   deps = [
-    ":styles",
     "../../mojom:mojom_js_library_for_compile",
     "../common:constants",
     "../common:iframe_api",
+    "../common:styles",
     "//third_party/polymer/v3_0/components-chromium/iron-list:iron-list",
     "//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
   ]
 }
 
-js_library("styles") {
-  deps = [
-    "//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
-  ]
-}
-
 js_type_check("closure_compile") {
   is_polymer3 = true
   closure_flags = default_closure_args + [
+                    "language_in=ECMASCRIPT_2020",
                     "browser_resolver_prefix_replacements=\"chrome-untrusted://personalization/polymer/v3_0/=../../third_party/polymer/v3_0/components-chromium/\"",
                     "browser_resolver_prefix_replacements=\"chrome-untrusted://personalization/polymer/v3_0/polymer/polymer_bundled.min.js=../../third_party/polymer/v3_0/components-chromium/polymer/polymer_bundled.js\"",
                   ]
   deps = [
     ":collections_grid",
     ":images_grid",
-    ":styles",
   ]
 }
 
@@ -58,7 +52,6 @@
   sources = [
     "collections.html",
     "images.html",
-    "styles.js",
   ]
   outputs = [ "$target_gen_dir/{{source_file_part}}" ]
 }
@@ -76,6 +69,5 @@
     "untrusted/collections_grid.js",
     "untrusted/images.html",
     "untrusted/images_grid.js",
-    "untrusted/styles.js",
   ]
 }
diff --git a/chromeos/components/personalization_app/resources/untrusted/collections_grid.html b/chromeos/components/personalization_app/resources/untrusted/collections_grid.html
index dd46394..7015792 100644
--- a/chromeos/components/personalization_app/resources/untrusted/collections_grid.html
+++ b/chromeos/components/personalization_app/resources/untrusted/collections_grid.html
@@ -1,16 +1,11 @@
-<style include="untrusted-style">
-  .collection-name {
-    bottom: 0;
-    position: absolute;
-    text-align: center;
-    width: 100%;
-  }
-</style>
-<iron-list items="[[collections_]]" grid>
+<style include="common-style"></style>
+<iron-list items="[[tiles_]]" grid>
   <template>
     <div class="photo-container" data-id$="[[item.id]]"
-        on-click="onCollectionClicked_">
-      <img src="[[item.preview.url]]">
+      on-click="onCollectionClicked_">
+      <template is="dom-if" if="[[item.preview]]">
+        <img src="[[item.preview.url]]">
+      </template>
       <p class="collection-name">[[item.name]]</p>
     </div>
   </template>
diff --git a/chromeos/components/personalization_app/resources/untrusted/collections_grid.js b/chromeos/components/personalization_app/resources/untrusted/collections_grid.js
index 9d1da6d..337053c 100644
--- a/chromeos/components/personalization_app/resources/untrusted/collections_grid.js
+++ b/chromeos/components/personalization_app/resources/untrusted/collections_grid.js
@@ -3,16 +3,47 @@
 // found in the LICENSE file.
 
 import '//personalization/polymer/v3_0/iron-list/iron-list.js';
-import './styles.js';
+import '../common/styles.js';
 import {html, PolymerElement} from 'chrome-untrusted://personalization/polymer/v3_0/polymer/polymer_bundled.min.js';
 import {EventType} from '../common/constants.js';
-import {selectCollection, validateReceivedData} from '../common/iframe_api.js';
+import {selectCollection, selectLocalCollection, validateReceivedData} from '../common/iframe_api.js';
+import {unguessableTokenToString} from '../common/utils.js';
 
 /**
  * @fileoverview Responds to |SendCollectionsEvent| from trusted. Handles user
  * input and responds with |SelectCollectionEvent| when an image is selected.
  */
 
+const kLocalCollectionId = 'local_';
+
+/**
+ * @typedef {{id: string, name: string, preview: ?url.mojom.Url}}
+ */
+let Tile;
+
+/**
+ * A common display format between local images and WallpaperCollection.
+ * Get the first displayable image with data from the list of possible images.
+ * TODO(b/184774974) display a collage of up to three images.
+ * @param {Array<!chromeos.personalizationApp.mojom.LocalImage>} localImages
+ * @param {Object<string, string>} localImageData
+ * @return {!Tile}
+ */
+function getLocalTile(localImages, localImageData) {
+  if (localImageData && Array.isArray(localImages)) {
+    for (const {id, name} of localImages) {
+      const key = unguessableTokenToString(id);
+      const data = localImageData[key];
+      if (!data) {
+        continue;
+      }
+      return {name, preview: {url: data}, id: kLocalCollectionId};
+    }
+  }
+  // TODO(b/184774974) replace zero state with translated string from UI spec.
+  return {name: 'No Images', preview: null, id: kLocalCollectionId};
+}
+
 class CollectionsGrid extends PolymerElement {
   static get is() {
     return 'collections-grid';
@@ -30,11 +61,38 @@
        */
       collections_: {
         type: Array,
+      },
+
+      /**
+       * @type {!Array<!chromeos.personalizationApp.mojom.LocalImage>}
+       * @private
+       */
+      localImages_: {
+        type: Array,
         value: [],
       },
+
+      /**
+       * Stores a mapping of local image id to thumbnail data.
+       * @private
+       * @type {!Object<string, string>}
+       */
+      localImageData_: {
+        type: Object,
+        value: {},
+      },
+
+      /**
+       * @type {!Array<!Tile>}
+       */
+      tiles_: {
+        type: Array,
+        computed: 'computeTiles_(collections_, localImages_, localImageData_)',
+      },
     };
   }
 
+  /** @override */
   constructor() {
     super();
     this.onMessageReceived_ = this.onMessageReceived_.bind(this);
@@ -53,18 +111,49 @@
   }
 
   /**
+   * @param {!Array<!chromeos.personalizationApp.mojom.WallpaperCollection>}
+   *     collections
+   * @param {!Array<!chromeos.personalizationApp.mojom.LocalImage>} localImages
+   */
+  computeTiles_(collections, localImages, localImageData) {
+    return [getLocalTile(localImages, localImageData), ...(collections || [])];
+  }
+
+  /**
    * Handler for messages from trusted code. Expects only SendImagesEvent and
    * will error on any other event.
    * @param {!Event} message
    * @private
    */
   onMessageReceived_(message) {
-    try {
-      this.collections_ =
-          validateReceivedData(message, EventType.SEND_COLLECTIONS);
-    } catch (e) {
-      console.warn('Invalid collections received', e);
-      this.collections_ = [];
+    switch (message.data.type) {
+      case EventType.SEND_COLLECTIONS:
+        try {
+          this.collections_ =
+              validateReceivedData(message, EventType.SEND_COLLECTIONS);
+        } catch (e) {
+          console.warn('Invalid collections received', e);
+          this.collections_ = [];
+        }
+        break;
+      case EventType.SEND_LOCAL_IMAGES:
+        try {
+          this.localImages_ =
+              validateReceivedData(message, EventType.SEND_LOCAL_IMAGES);
+        } catch (e) {
+          console.warn('Invalid local images received', e);
+          this.localImages_ = [];
+        }
+        break;
+      case EventType.SEND_LOCAL_IMAGE_DATA:
+        this.localImageData_ = {
+          ...this.localImageData_,
+          [unguessableTokenToString(message.data.id)]: message.data.data,
+        };
+        break;
+      default:
+        console.error(`Unexpected event type ${message.data.type}`);
+        break;
     }
   }
 
@@ -74,7 +163,12 @@
    * @param {!Event} e
    */
   onCollectionClicked_(e) {
-    selectCollection(window.parent, e.currentTarget.dataset.id);
+    const id = e.currentTarget.dataset.id;
+    if (id === kLocalCollectionId) {
+      selectLocalCollection(window.parent);
+      return;
+    }
+    selectCollection(window.parent, id);
   }
 }
 
diff --git a/chromeos/components/personalization_app/resources/untrusted/images_grid.html b/chromeos/components/personalization_app/resources/untrusted/images_grid.html
index a40b0b7..180a85f 100644
--- a/chromeos/components/personalization_app/resources/untrusted/images_grid.html
+++ b/chromeos/components/personalization_app/resources/untrusted/images_grid.html
@@ -1,4 +1,4 @@
-<style include="untrusted-style">
+<style include="common-style">
 </style>
 <iron-list grid items="[[images_]]">
   <template>
diff --git a/chromeos/components/personalization_app/resources/untrusted/images_grid.js b/chromeos/components/personalization_app/resources/untrusted/images_grid.js
index 4cf0273..4088880 100644
--- a/chromeos/components/personalization_app/resources/untrusted/images_grid.js
+++ b/chromeos/components/personalization_app/resources/untrusted/images_grid.js
@@ -3,7 +3,7 @@
 // found in the LICENSE file.
 
 import 'chrome-untrusted://personalization/polymer/v3_0/iron-list/iron-list.js';
-import './styles.js';
+import '../common/styles.js';
 import {html, PolymerElement} from 'chrome-untrusted://personalization/polymer/v3_0/polymer/polymer_bundled.min.js';
 import {EventType} from '../common/constants.js';
 import {selectImage, validateReceivedData} from '../common/iframe_api.js';
diff --git a/chromeos/components/personalization_app/untrusted_personalization_app_ui_config.cc b/chromeos/components/personalization_app/untrusted_personalization_app_ui_config.cc
index b95f461b..41cbca5 100644
--- a/chromeos/components/personalization_app/untrusted_personalization_app_ui_config.cc
+++ b/chromeos/components/personalization_app/untrusted_personalization_app_ui_config.cc
@@ -54,7 +54,7 @@
     // Allow images only from this url.
     source->OverrideContentSecurityPolicy(
         network::mojom::CSPDirectiveName::ImgSrc,
-        "img-src https://*.googleusercontent.com;");
+        "img-src data: https://*.googleusercontent.com;");
 
     source->OverrideContentSecurityPolicy(
         network::mojom::CSPDirectiveName::ScriptSrc, "script-src 'self';");
diff --git a/chromeos/services/assistant/public/cpp/features.cc b/chromeos/services/assistant/public/cpp/features.cc
index 9324cb0..252349b9 100644
--- a/chromeos/services/assistant/public/cpp/features.cc
+++ b/chromeos/services/assistant/public/cpp/features.cc
@@ -55,7 +55,7 @@
                                        base::FEATURE_DISABLED_BY_DEFAULT};
 
 const base::Feature kEnableLibAssistantSandbox{
-    "LibAssistantSandbox", base::FEATURE_DISABLED_BY_DEFAULT};
+    "LibAssistantSandbox", base::FEATURE_ENABLED_BY_DEFAULT};
 
 bool IsAppSupportEnabled() {
   return base::FeatureList::IsEnabled(
diff --git a/components/autofill/core/browser/autofill_client.cc b/components/autofill/core/browser/autofill_client.cc
index ab4db5e..61f51fb 100644
--- a/components/autofill/core/browser/autofill_client.cc
+++ b/components/autofill/core/browser/autofill_client.cc
@@ -6,6 +6,7 @@
 
 #include "base/no_destructor.h"
 #include "components/autofill/core/browser/autofill_ablation_study.h"
+#include "components/autofill/core/browser/payments/credit_card_access_manager.h"
 #include "components/autofill/core/browser/ui/suggestion.h"
 #include "components/version_info/channel.h"
 
@@ -65,7 +66,8 @@
   // ChromeAutofillClient (Chrome Desktop and Clank) implements this.
 }
 
-void AutofillClient::OnVirtualCardFetched(const CreditCard* credit_card,
+void AutofillClient::OnVirtualCardFetched(CreditCardFetchResult result,
+                                          const CreditCard* credit_card,
                                           const std::u16string& cvc,
                                           const gfx::Image& card_image) {
   // This is overridden by platform subclasses. Currently only
diff --git a/components/autofill/core/browser/autofill_client.h b/components/autofill/core/browser/autofill_client.h
index f76acd1..8c9221f 100644
--- a/components/autofill/core/browser/autofill_client.h
+++ b/components/autofill/core/browser/autofill_client.h
@@ -65,6 +65,7 @@
 class AutofillPopupDelegate;
 class CardUnmaskDelegate;
 class CreditCard;
+enum class CreditCardFetchResult;
 class FormDataImporter;
 class FormStructure;
 class LogManager;
@@ -562,12 +563,15 @@
   virtual void ShowOfferNotificationIfApplicable(
       const AutofillOfferData* offer);
 
-  // Indicates that the virtual card was fetched in order to allow the user to
-  // manually fill payment form with the fetched |credit_card| and |cvc|.
-  // |card_image| is used for manual fallback bubble.
-  virtual void OnVirtualCardFetched(const CreditCard* credit_card,
-                                    const std::u16string& cvc,
-                                    const gfx::Image& card_image);
+  // Called when the result of fetching a virtual card from the server returns.
+  // |result| indicates whether the fetching was successful. |credit_card| and
+  // |cvc| include the information that allow the user to manually fill payment
+  // form. |card_image| is used for manual fallback bubble.
+  virtual void OnVirtualCardFetched(
+      CreditCardFetchResult result,
+      const CreditCard* credit_card = nullptr,
+      const std::u16string& cvc = std::u16string(),
+      const gfx::Image& card_image = gfx::Image());
 
   // Returns true if the Autofill Assistant UI is currently being shown.
   virtual bool IsAutofillAssistantShowing();
diff --git a/components/autofill/core/browser/autofill_metrics.cc b/components/autofill/core/browser/autofill_metrics.cc
index 977c275..20c2b19 100644
--- a/components/autofill/core/browser/autofill_metrics.cc
+++ b/components/autofill/core/browser/autofill_metrics.cc
@@ -1264,9 +1264,11 @@
     case AutofillClient::NETWORK_ERROR:
       result_suffix = "NetworkError";
       break;
-    case AutofillClient::NONE:
     case AutofillClient::VCN_RETRIEVAL_TRY_AGAIN_FAILURE:
     case AutofillClient::VCN_RETRIEVAL_PERMANENT_FAILURE:
+      result_suffix = "VcnRetrievalFailure";
+      break;
+    case AutofillClient::NONE:
       NOTREACHED();
       return;
   }
@@ -1276,6 +1278,8 @@
       card_type_suffix = "ServerCard";
       break;
     case AutofillClient::VIRTUAL_CARD:
+      card_type_suffix = "VirtualCard";
+      break;
     case AutofillClient::UNKNOWN_TYPE:
       NOTREACHED();
       return;
diff --git a/components/autofill/core/browser/browser_autofill_manager.cc b/components/autofill/core/browser/browser_autofill_manager.cc
index 09b7409..c9f4010 100644
--- a/components/autofill/core/browser/browser_autofill_manager.cc
+++ b/components/autofill/core/browser/browser_autofill_manager.cc
@@ -1455,10 +1455,13 @@
   client()->PropagateAutofillPredictions(rfh, forms);
 }
 
-void BrowserAutofillManager::OnCreditCardFetched(bool did_succeed,
+void BrowserAutofillManager::OnCreditCardFetched(CreditCardFetchResult result,
                                                  const CreditCard* credit_card,
                                                  const std::u16string& cvc) {
-  if (!did_succeed) {
+  if (result != CreditCardFetchResult::kSuccess) {
+    if (credit_card && credit_card->record_type() == CreditCard::VIRTUAL_CARD)
+      client()->OnVirtualCardFetched(result);
+
     driver()->RendererShouldClearPreviewedForm();
     return;
   }
@@ -1484,8 +1487,8 @@
   // show the UI to help user to manually fill the form, if needed.
   if (credit_card->record_type() == CreditCard::VIRTUAL_CARD) {
     // TODO(crbug.com/1196021): Pass in real card image.
-    client()->OnVirtualCardFetched(credit_card, cvc,
-                                   /* card_image */ gfx::Image());
+    client()->OnVirtualCardFetched(CreditCardFetchResult::kSuccess, credit_card,
+                                   cvc, /*card_image=*/gfx::Image());
   }
 
   FillCreditCardForm(credit_card_query_id_, credit_card_form_,
diff --git a/components/autofill/core/browser/browser_autofill_manager.h b/components/autofill/core/browser/browser_autofill_manager.h
index d5d20ba..ac33adc 100644
--- a/components/autofill/core/browser/browser_autofill_manager.h
+++ b/components/autofill/core/browser/browser_autofill_manager.h
@@ -469,7 +469,7 @@
 
   // CreditCardAccessManager::Accessor
   void OnCreditCardFetched(
-      bool did_succeed,
+      CreditCardFetchResult result,
       const CreditCard* credit_card = nullptr,
       const std::u16string& cvc = std::u16string()) override;
 
diff --git a/components/autofill/core/browser/payments/credit_card_access_manager.cc b/components/autofill/core/browser/payments/credit_card_access_manager.cc
index ebc314c..243c32d 100644
--- a/components/autofill/core/browser/payments/credit_card_access_manager.cc
+++ b/components/autofill/core/browser/payments/credit_card_access_manager.cc
@@ -271,7 +271,8 @@
     const base::TimeTicks& form_parsed_timestamp) {
   // Return error if authentication is already in progress or card is nullptr.
   if (is_authentication_in_progress_ || !card) {
-    accessor->OnCreditCardFetched(/*did_succeed=*/false, nullptr);
+    accessor->OnCreditCardFetched(CreditCardFetchResult::kTransientError,
+                                  nullptr);
     return;
   }
 
@@ -282,7 +283,7 @@
   std::unordered_map<std::string, CachedServerCardInfo>::iterator it =
       unmasked_card_cache_.find(identifier);
   if (it != unmasked_card_cache_.end()) {  // key is in cache
-    accessor->OnCreditCardFetched(/*did_succeed=*/true,
+    accessor->OnCreditCardFetched(CreditCardFetchResult::kSuccess,
                                   /*credit_card=*/&it->second.card,
                                   /*cvc=*/it->second.cvc);
     std::string metrics_name = card->record_type() == CreditCard::VIRTUAL_CARD
@@ -301,7 +302,7 @@
   // Return immediately if local card and log that unmask details were ignored.
   if (card->record_type() != CreditCard::MASKED_SERVER_CARD &&
       card->record_type() != CreditCard::VIRTUAL_CARD) {
-    accessor->OnCreditCardFetched(/*did_succeed=*/true, card);
+    accessor->OnCreditCardFetched(CreditCardFetchResult::kSuccess, card);
 #if !defined(OS_IOS)
     if (should_log_latency_metrics) {
       AutofillMetrics::LogUserPerceivedLatencyOnCardSelection(
@@ -596,8 +597,10 @@
   // can't be true at the same time
   DCHECK(!(should_respond_immediately && should_authorize_with_fido));
   if (should_respond_immediately) {
-    accessor_->OnCreditCardFetched(response.did_succeed, response.card,
-                                   response.cvc);
+    accessor_->OnCreditCardFetched(response.did_succeed
+                                       ? CreditCardFetchResult::kSuccess
+                                       : CreditCardFetchResult::kTransientError,
+                                   response.card, response.cvc);
     unmask_auth_flow_type_ = UnmaskAuthFlowType::kNone;
   } else if (should_authorize_with_fido) {
     AdditionallyPerformFidoAuth(response, request_options->Clone());
@@ -633,9 +636,7 @@
 
 #if !defined(OS_IOS)
 void CreditCardAccessManager::OnFIDOAuthenticationComplete(
-    bool did_succeed,
-    const CreditCard* card,
-    const std::u16string& cvc) {
+    const CreditCardFIDOAuthenticator::FidoAuthenticationResponse& response) {
 #if !defined(OS_ANDROID)
   // Close the Webauthn verify pending dialog. If FIDO authentication succeeded,
   // card is filled to the form, otherwise fall back to CVC authentication which
@@ -643,15 +644,35 @@
   client_->CloseWebauthnDialog();
 #endif
 
-  if (did_succeed) {
+  if (response.did_succeed) {
     is_authentication_in_progress_ = false;
-    accessor_->OnCreditCardFetched(did_succeed, card, cvc);
+    accessor_->OnCreditCardFetched(response.did_succeed
+                                       ? CreditCardFetchResult::kSuccess
+                                       : CreditCardFetchResult::kTransientError,
+                                   response.card, response.cvc);
     can_fetch_unmask_details_.Signal();
 
     form_event_logger_->LogCardUnmaskAuthenticationPromptCompleted(
         unmask_auth_flow_type_);
     unmask_auth_flow_type_ = UnmaskAuthFlowType::kNone;
+  } else if (
+      response.failure_type ==
+          payments::FullCardRequest::VIRTUAL_CARD_RETRIEVAL_TRANSIENT_FAILURE ||
+      response.failure_type ==
+          payments::FullCardRequest::VIRTUAL_CARD_RETRIEVAL_PERMANENT_FAILURE) {
+    CreditCardFetchResult result =
+        response.failure_type == payments::FullCardRequest::
+                                     VIRTUAL_CARD_RETRIEVAL_TRANSIENT_FAILURE
+            ? CreditCardFetchResult::kTransientError
+            : CreditCardFetchResult::kPermanentError;
+    // If it is an virtual card retrieval error, we don't want to invoke the CVC
+    // authentication afterwards. Instead reset all states and notify accessor.
+    is_authentication_in_progress_ = false;
+    unmask_auth_flow_type_ = UnmaskAuthFlowType::kNone;
+    can_fetch_unmask_details_.Signal();
+    accessor_->OnCreditCardFetched(result, card_.get());
   } else {
+    // If it is an authentication error, start the CVC authentication process.
     unmask_auth_flow_type_ = UnmaskAuthFlowType::kCvcFallbackFromFido;
     form_event_logger_->LogCardUnmaskAuthenticationPromptShown(
         unmask_auth_flow_type_);
@@ -663,7 +684,8 @@
 
 void CreditCardAccessManager::OnFidoAuthorizationComplete(bool did_succeed) {
   if (did_succeed) {
-    accessor_->OnCreditCardFetched(/*did_succeed=*/true, card_.get(), cvc_);
+    accessor_->OnCreditCardFetched(CreditCardFetchResult::kSuccess, card_.get(),
+                                   cvc_);
     form_event_logger_->LogCardUnmaskAuthenticationPromptCompleted(
         unmask_auth_flow_type_);
   }
@@ -734,7 +756,9 @@
 
       // Indicate that FIDO authentication was canceled, resulting in falling
       // back to CVC auth.
-      OnFIDOAuthenticationComplete(/*did_succeed=*/false);
+      CreditCardFIDOAuthenticator::FidoAuthenticationResponse response{
+          .did_succeed = false};
+      OnFIDOAuthenticationComplete(response);
       break;
   }
 }
diff --git a/components/autofill/core/browser/payments/credit_card_access_manager.h b/components/autofill/core/browser/payments/credit_card_access_manager.h
index bd86db5..bee77f0f 100644
--- a/components/autofill/core/browser/payments/credit_card_access_manager.h
+++ b/components/autofill/core/browser/payments/credit_card_access_manager.h
@@ -45,6 +45,18 @@
   kCvcFallbackFromFido = 4,
 };
 
+// The result of the attempt to fetch full information for a credit card.
+enum class CreditCardFetchResult {
+  kNone = 0,
+  // The attempt succeeded retrieving the full information of a credit card.
+  kSuccess = 1,
+  // The attempt failed due to a transient error.
+  kTransientError = 2,
+  // The attempt failed due to a permanent error.
+  kPermanentError = 3,
+  kMaxValue = kPermanentError,
+};
+
 struct CachedServerCardInfo {
  public:
   // An unmasked CreditCard.
@@ -69,7 +81,7 @@
    public:
     virtual ~Accessor() {}
     virtual void OnCreditCardFetched(
-        bool did_succeed,
+        CreditCardFetchResult result,
         const CreditCard* credit_card = nullptr,
         const std::u16string& cvc = std::u16string()) = 0;
   };
@@ -203,9 +215,8 @@
 #if !defined(OS_IOS)
   // CreditCardFIDOAuthenticator::Requester:
   void OnFIDOAuthenticationComplete(
-      bool did_succeed,
-      const CreditCard* card = nullptr,
-      const std::u16string& cvc = std::u16string()) override;
+      const CreditCardFIDOAuthenticator::FidoAuthenticationResponse& response)
+      override;
   void OnFidoAuthorizationComplete(bool did_succeed) override;
 #endif
 
diff --git a/components/autofill/core/browser/payments/credit_card_access_manager_unittest.cc b/components/autofill/core/browser/payments/credit_card_access_manager_unittest.cc
index cec8024..8facc551 100644
--- a/components/autofill/core/browser/payments/credit_card_access_manager_unittest.cc
+++ b/components/autofill/core/browser/payments/credit_card_access_manager_unittest.cc
@@ -111,11 +111,11 @@
     return weak_ptr_factory_.GetWeakPtr();
   }
 
-  void OnCreditCardFetched(bool did_succeed,
+  void OnCreditCardFetched(CreditCardFetchResult result,
                            const CreditCard* card,
                            const std::u16string& cvc) override {
-    did_succeed_ = did_succeed;
-    if (did_succeed_) {
+    result_ = result;
+    if (result == CreditCardFetchResult::kSuccess) {
       DCHECK(card);
       number_ = card->number();
       cvc_ = cvc;
@@ -124,12 +124,11 @@
 
   std::u16string number() { return number_; }
   std::u16string cvc() { return cvc_; }
-
-  bool did_succeed() { return did_succeed_; }
+  CreditCardFetchResult result() { return result_; }
 
  private:
-  // Is set to true if authentication was successful.
-  bool did_succeed_ = false;
+  // The result of the credit card fetching.
+  CreditCardFetchResult result_ = CreditCardFetchResult::kNone;
   // The card number returned from OnCreditCardFetched().
   std::u16string number_;
   // The returned CVC, if any.
@@ -498,7 +497,7 @@
 
   credit_card_access_manager_->FetchCreditCard(card, accessor_->GetWeakPtr());
 
-  EXPECT_TRUE(accessor_->did_succeed());
+  EXPECT_EQ(accessor_->result(), CreditCardFetchResult::kSuccess);
   EXPECT_EQ(kTestNumber16, accessor_->number());
 }
 
@@ -511,7 +510,7 @@
 
   credit_card_access_manager_->FetchCreditCard(nullptr,
                                                accessor_->GetWeakPtr());
-  EXPECT_FALSE(accessor_->did_succeed());
+  EXPECT_NE(accessor_->result(), CreditCardFetchResult::kSuccess);
 }
 
 // Ensures that FetchCreditCard() returns the full PAN upon a successful
@@ -531,7 +530,7 @@
       CreditCardFormEventLogger::UnmaskAuthFlowEvent::kPromptShown, 1);
 
   EXPECT_TRUE(GetRealPanForCVCAuth(AutofillClient::SUCCESS, kTestNumber));
-  EXPECT_TRUE(accessor_->did_succeed());
+  EXPECT_EQ(accessor_->result(), CreditCardFetchResult::kSuccess);
   EXPECT_EQ(kTestNumber16, accessor_->number());
   EXPECT_EQ(kTestCvc16, accessor_->cvc());
 
@@ -553,7 +552,7 @@
 
   EXPECT_TRUE(
       GetRealPanForCVCAuth(AutofillClient::NETWORK_ERROR, std::string()));
-  EXPECT_FALSE(accessor_->did_succeed());
+  EXPECT_NE(accessor_->result(), CreditCardFetchResult::kSuccess);
 }
 
 // Ensures that FetchCreditCard() returns a failure upon a negative response
@@ -569,7 +568,7 @@
 
   EXPECT_TRUE(
       GetRealPanForCVCAuth(AutofillClient::PERMANENT_FAILURE, std::string()));
-  EXPECT_FALSE(accessor_->did_succeed());
+  EXPECT_NE(accessor_->result(), CreditCardFetchResult::kSuccess);
 }
 
 // Ensures that a "try again" response from payments does not end the flow.
@@ -581,10 +580,10 @@
 
   EXPECT_TRUE(
       GetRealPanForCVCAuth(AutofillClient::TRY_AGAIN_FAILURE, std::string()));
-  EXPECT_FALSE(accessor_->did_succeed());
+  EXPECT_NE(accessor_->result(), CreditCardFetchResult::kSuccess);
 
   EXPECT_TRUE(GetRealPanForCVCAuth(AutofillClient::SUCCESS, kTestNumber));
-  EXPECT_TRUE(accessor_->did_succeed());
+  EXPECT_EQ(accessor_->result(), CreditCardFetchResult::kSuccess);
   EXPECT_EQ(kTestNumber16, accessor_->number());
   EXPECT_EQ(kTestCvc16, accessor_->cvc());
 }
@@ -706,7 +705,7 @@
   TestCreditCardFIDOAuthenticator::GetAssertion(GetFIDOAuthenticator(),
                                                 /*did_succeed=*/true);
   EXPECT_TRUE(GetRealPanForFIDOAuth(AutofillClient::SUCCESS, kTestNumber));
-  EXPECT_TRUE(accessor_->did_succeed());
+  EXPECT_EQ(accessor_->result(), CreditCardFetchResult::kSuccess);
 
   EXPECT_EQ(kCredentialId,
             BytesToBase64(GetFIDOAuthenticator()->GetCredentialId()));
@@ -761,7 +760,7 @@
       GetRealPanForFIDOAuth(AutofillClient::SUCCESS, kTestNumber, kTestCvc));
 
   // Expect accessor to successfully retrieve the DCVV.
-  EXPECT_TRUE(accessor_->did_succeed());
+  EXPECT_EQ(accessor_->result(), CreditCardFetchResult::kSuccess);
   EXPECT_EQ(kTestNumber16, accessor_->number());
   EXPECT_EQ(kTestCvc16, accessor_->cvc());
 }
@@ -804,13 +803,13 @@
       CreditCardFormEventLogger::UnmaskAuthFlowEvent::kPromptShown, 1);
 
   EXPECT_FALSE(GetRealPanForFIDOAuth(AutofillClient::SUCCESS, kTestNumber));
-  EXPECT_FALSE(accessor_->did_succeed());
+  EXPECT_NE(accessor_->result(), CreditCardFetchResult::kSuccess);
 
   // Followed by a fallback to CVC.
   EXPECT_EQ(CreditCardFIDOAuthenticator::Flow::NONE_FLOW,
             GetFIDOAuthenticator()->current_flow());
   EXPECT_TRUE(GetRealPanForCVCAuth(AutofillClient::SUCCESS, kTestNumber));
-  EXPECT_TRUE(accessor_->did_succeed());
+  EXPECT_EQ(accessor_->result(), CreditCardFetchResult::kSuccess);
   EXPECT_EQ(kTestNumber16, accessor_->number());
   EXPECT_EQ(kTestCvc16, accessor_->cvc());
 
@@ -853,13 +852,13 @@
                                                 /*did_succeed=*/true);
   EXPECT_TRUE(
       GetRealPanForFIDOAuth(AutofillClient::PERMANENT_FAILURE, kTestNumber));
-  EXPECT_FALSE(accessor_->did_succeed());
+  EXPECT_NE(accessor_->result(), CreditCardFetchResult::kSuccess);
 
   // Followed by a fallback to CVC.
   EXPECT_EQ(CreditCardFIDOAuthenticator::Flow::NONE_FLOW,
             GetFIDOAuthenticator()->current_flow());
   EXPECT_TRUE(GetRealPanForCVCAuth(AutofillClient::SUCCESS, kTestNumber));
-  EXPECT_TRUE(accessor_->did_succeed());
+  EXPECT_EQ(accessor_->result(), CreditCardFetchResult::kSuccess);
   EXPECT_EQ(kTestNumber16, accessor_->number());
   EXPECT_EQ(kTestCvc16, accessor_->cvc());
 
@@ -891,11 +890,11 @@
 
   // FIDO Failure.
   EXPECT_FALSE(GetRealPanForFIDOAuth(AutofillClient::SUCCESS, kTestNumber));
-  EXPECT_FALSE(accessor_->did_succeed());
+  EXPECT_NE(accessor_->result(), CreditCardFetchResult::kSuccess);
 
   // Followed by a fallback to CVC.
   EXPECT_TRUE(GetRealPanForCVCAuth(AutofillClient::SUCCESS, kTestNumber));
-  EXPECT_TRUE(accessor_->did_succeed());
+  EXPECT_EQ(accessor_->result(), CreditCardFetchResult::kSuccess);
   EXPECT_EQ(kTestNumber16, accessor_->number());
   EXPECT_EQ(kTestCvc16, accessor_->cvc());
 }
@@ -913,11 +912,53 @@
   WaitForCallbacks();
 
   EXPECT_TRUE(GetRealPanForCVCAuth(AutofillClient::SUCCESS, kTestNumber));
-  EXPECT_TRUE(accessor_->did_succeed());
+  EXPECT_EQ(accessor_->result(), CreditCardFetchResult::kSuccess);
   EXPECT_EQ(kTestNumber16, accessor_->number());
   EXPECT_EQ(kTestCvc16, accessor_->cvc());
 }
 
+// Ensures that CVC prompt is not invoked after payments returns an error from
+// GetRealPan via FIDO for a virtual card.
+TEST_F(CreditCardAccessManagerTest, FetchVirtualCardFIDOFailureNoCVCFallback) {
+  base::HistogramTester histogram_tester;
+
+  CreateServerCard(kTestGUID, kTestNumber);
+  CreditCard* card = credit_card_access_manager_->GetCreditCard(kTestGUID);
+  GetFIDOAuthenticator()->SetUserVerifiable(true);
+  SetUserOptedIn(true);
+  payments_client_->AddFidoEligibleCard(card->server_id(), kCredentialId,
+                                        kGooglePaymentsRpid);
+
+  credit_card_access_manager_->PrepareToFetchCreditCard();
+  WaitForCallbacks();
+
+  card->set_record_type(CreditCard::VIRTUAL_CARD);
+  credit_card_access_manager_->FetchCreditCard(card, accessor_->GetWeakPtr());
+  WaitForCallbacks();
+
+  // FIDO Failure.
+  EXPECT_EQ(CreditCardFIDOAuthenticator::Flow::AUTHENTICATION_FLOW,
+            GetFIDOAuthenticator()->current_flow());
+  TestCreditCardFIDOAuthenticator::GetAssertion(GetFIDOAuthenticator(),
+                                                /*did_succeed=*/true);
+  EXPECT_TRUE(GetRealPanForFIDOAuth(
+      AutofillClient::VCN_RETRIEVAL_PERMANENT_FAILURE, kTestNumber,
+      std::string(), /*is_virtual_card=*/true));
+  EXPECT_EQ(accessor_->result(), CreditCardFetchResult::kPermanentError);
+  EXPECT_EQ(CreditCardFIDOAuthenticator::Flow::NONE_FLOW,
+            GetFIDOAuthenticator()->current_flow());
+
+  histogram_tester.ExpectUniqueSample(
+      "Autofill.BetterAuth.WebauthnResult.ImmediateAuthentication",
+      AutofillMetrics::WebauthnResultMetric::kSuccess, 1);
+  histogram_tester.ExpectTotalCount(
+      "Autofill.BetterAuth.CardUnmaskDuration.Fido", 1);
+  histogram_tester.ExpectTotalCount(
+      "Autofill.BetterAuth.CardUnmaskDuration.Fido.VirtualCard."
+      "VcnRetrievalFailure",
+      1);
+}
+
 // Ensures the existence of user-perceived latency during the preflight call is
 // correctly logged.
 TEST_F(CreditCardAccessManagerTest,
diff --git a/components/autofill/core/browser/payments/credit_card_fido_authenticator.cc b/components/autofill/core/browser/payments/credit_card_fido_authenticator.cc
index 0dc1da91..490db53 100644
--- a/components/autofill/core/browser/payments/credit_card_fido_authenticator.cc
+++ b/components/autofill/core/browser/payments/credit_card_fido_authenticator.cc
@@ -84,7 +84,8 @@
     current_flow_ = AUTHENTICATION_FLOW;
     GetAssertion(ParseRequestOptions(std::move(request_options)));
   } else {
-    requester_->OnFIDOAuthenticationComplete(/*did_succeed=*/false);
+    FidoAuthenticationResponse response{.did_succeed = false};
+    requester_->OnFIDOAuthenticationComplete(response);
   }
 }
 
@@ -387,8 +388,10 @@
   // End the flow if there was an authentication error.
   if (status != AuthenticatorStatus::SUCCESS) {
     // Report failure to |requester_| if card unmasking was requested.
-    if (current_flow_ == AUTHENTICATION_FLOW)
-      requester_->OnFIDOAuthenticationComplete(/*did_succeed=*/false);
+    if (current_flow_ == AUTHENTICATION_FLOW) {
+      FidoAuthenticationResponse response{.did_succeed = false};
+      requester_->OnFIDOAuthenticationComplete(response);
+    }
     if (current_flow_ == FOLLOWUP_AFTER_CVC_AUTH_FLOW)
       requester_->OnFidoAuthorizationComplete(/*did_succeed=*/false);
 
@@ -523,14 +526,18 @@
     const std::u16string& cvc) {
   DCHECK_EQ(AUTHENTICATION_FLOW, current_flow_);
   current_flow_ = NONE_FLOW;
-  requester_->OnFIDOAuthenticationComplete(/*did_succeed=*/true, &card, cvc);
+  FidoAuthenticationResponse response{
+      .did_succeed = true, .card = &card, .cvc = cvc};
+  requester_->OnFIDOAuthenticationComplete(response);
 }
 
 void CreditCardFIDOAuthenticator::OnFullCardRequestFailed(
     payments::FullCardRequest::FailureType failure_type) {
   DCHECK_EQ(AUTHENTICATION_FLOW, current_flow_);
   current_flow_ = NONE_FLOW;
-  requester_->OnFIDOAuthenticationComplete(/*did_succeed=*/false);
+  FidoAuthenticationResponse response{.did_succeed = false,
+                                      .failure_type = failure_type};
+  requester_->OnFIDOAuthenticationComplete(response);
 }
 
 PublicKeyCredentialRequestOptionsPtr
diff --git a/components/autofill/core/browser/payments/credit_card_fido_authenticator.h b/components/autofill/core/browser/payments/credit_card_fido_authenticator.h
index ac4ce6c..b313642 100644
--- a/components/autofill/core/browser/payments/credit_card_fido_authenticator.h
+++ b/components/autofill/core/browser/payments/credit_card_fido_authenticator.h
@@ -76,13 +76,28 @@
     // Authorization of a new card.
     FOLLOWUP_AFTER_CVC_AUTH_FLOW,
   };
+  // The response of FIDO authentication, including necessary information needed
+  // by the subclasses.
+  struct FidoAuthenticationResponse {
+    FidoAuthenticationResponse() = default;
+    ~FidoAuthenticationResponse() = default;
+
+    // Whether the authentication was successful.
+    bool did_succeed = false;
+    // The fetched credit card if the authentication was successful. Can be
+    // nullptr if authentication failed.
+    const CreditCard* card = nullptr;
+    // The CVC of the fetched credit card. Can be empty string.
+    std::u16string cvc = std::u16string();
+    // The type of the failure of the full card request.
+    payments::FullCardRequest::FailureType failure_type =
+        payments::FullCardRequest::UNKNOWN;
+  };
   class Requester {
    public:
     virtual ~Requester() {}
     virtual void OnFIDOAuthenticationComplete(
-        bool did_succeed,
-        const CreditCard* card = nullptr,
-        const std::u16string& cvc = std::u16string()) = 0;
+        const FidoAuthenticationResponse& response) = 0;
     virtual void OnFidoAuthorizationComplete(bool did_succeed) = 0;
   };
   CreditCardFIDOAuthenticator(AutofillDriver* driver, AutofillClient* client);
diff --git a/components/autofill/core/browser/payments/credit_card_fido_authenticator_unittest.cc b/components/autofill/core/browser/payments/credit_card_fido_authenticator_unittest.cc
index cacd1df..6db9cf9 100644
--- a/components/autofill/core/browser/payments/credit_card_fido_authenticator_unittest.cc
+++ b/components/autofill/core/browser/payments/credit_card_fido_authenticator_unittest.cc
@@ -445,6 +445,29 @@
   EXPECT_FALSE(requester_->did_succeed());
 }
 
+TEST_F(CreditCardFIDOAuthenticatorTest,
+       AuthenticateCard_PaymentsResponseVcnRetrievalError) {
+  CreditCard card = CreateServerCard(kTestGUID, kTestNumber);
+
+  fido_authenticator_->Authenticate(
+      &card, requester_->GetWeakPtr(), AutofillTickClock::NowTicks(),
+      GetTestRequestOptions(kTestChallenge, kTestRelyingPartyId,
+                            kTestCredentialId));
+  EXPECT_EQ(CreditCardFIDOAuthenticator::Flow::AUTHENTICATION_FLOW,
+            fido_authenticator_->current_flow());
+
+  // Mock user verification.
+  TestCreditCardFIDOAuthenticator::GetAssertion(fido_authenticator_.get(),
+                                                /*did_succeed=*/true);
+  GetRealPan(AutofillClient::PaymentsRpcResult::VCN_RETRIEVAL_PERMANENT_FAILURE,
+             "", /*is_virtual_card=*/true);
+
+  EXPECT_FALSE(requester_->did_succeed());
+  EXPECT_EQ(
+      requester_->failure_type(),
+      payments::FullCardRequest::VIRTUAL_CARD_RETRIEVAL_PERMANENT_FAILURE);
+}
+
 TEST_F(CreditCardFIDOAuthenticatorTest, AuthenticateCard_Success) {
   CreditCard card = CreateServerCard(kTestGUID, kTestNumber);
 
diff --git a/components/autofill/core/browser/payments/full_card_request.cc b/components/autofill/core/browser/payments/full_card_request.cc
index 4f34ad3..b4db461 100644
--- a/components/autofill/core/browser/payments/full_card_request.cc
+++ b/components/autofill/core/browser/payments/full_card_request.cc
@@ -260,11 +260,18 @@
       Reset();
       break;
     }
-    case AutofillClient::VCN_RETRIEVAL_TRY_AGAIN_FAILURE:
+    case AutofillClient::VCN_RETRIEVAL_TRY_AGAIN_FAILURE: {
+      if (result_delegate_) {
+        result_delegate_->OnFullCardRequestFailed(
+            FailureType::VIRTUAL_CARD_RETRIEVAL_TRANSIENT_FAILURE);
+      }
+      Reset();
+      break;
+    }
     case AutofillClient::VCN_RETRIEVAL_PERMANENT_FAILURE: {
       if (result_delegate_) {
         result_delegate_->OnFullCardRequestFailed(
-            FailureType::VIRTUAL_CARD_RETRIEVAL_FAILURE);
+            FailureType::VIRTUAL_CARD_RETRIEVAL_PERMANENT_FAILURE);
       }
       Reset();
       break;
diff --git a/components/autofill/core/browser/payments/full_card_request.h b/components/autofill/core/browser/payments/full_card_request.h
index d1e10c911..a03fbf0 100644
--- a/components/autofill/core/browser/payments/full_card_request.h
+++ b/components/autofill/core/browser/payments/full_card_request.h
@@ -33,6 +33,8 @@
  public:
   // The type of failure.
   enum FailureType {
+    UNKNOWN,
+
     // The user closed the prompt. The following scenarios are possible:
     // 1) The user declined to enter their CVC and closed the prompt.
     // 2) The user provided their CVC, got auth declined and then closed the
@@ -44,8 +46,13 @@
     // The card could not be looked up due to card auth declined or failed.
     VERIFICATION_DECLINED,
 
-    // The request failed when retrieving virtual card information.
-    VIRTUAL_CARD_RETRIEVAL_FAILURE,
+    // The request failed due to transient failures when retrieving virtual card
+    // information.
+    VIRTUAL_CARD_RETRIEVAL_TRANSIENT_FAILURE,
+
+    // The request failed due to permanent failures when retrieving virtual card
+    // information.
+    VIRTUAL_CARD_RETRIEVAL_PERMANENT_FAILURE,
 
     // The request failed for technical reasons, such as a closing page or lack
     // of network connection.
diff --git a/components/autofill/core/browser/payments/full_card_request_unittest.cc b/components/autofill/core/browser/payments/full_card_request_unittest.cc
index 9bcee62..025fa47 100644
--- a/components/autofill/core/browser/payments/full_card_request_unittest.cc
+++ b/components/autofill/core/browser/payments/full_card_request_unittest.cc
@@ -550,8 +550,8 @@
 TEST_F(FullCardRequestTest, VcnRetrievalTryAgainFailure) {
   EXPECT_CALL(
       *result_delegate(),
-      OnFullCardRequestFailed(
-          FullCardRequest::FailureType::VIRTUAL_CARD_RETRIEVAL_FAILURE));
+      OnFullCardRequestFailed(FullCardRequest::FailureType::
+                                  VIRTUAL_CARD_RETRIEVAL_TRANSIENT_FAILURE));
   EXPECT_CALL(*ui_delegate(), ShowUnmaskPrompt(_, _, _));
   EXPECT_CALL(*ui_delegate(),
               OnUnmaskVerificationResult(
@@ -576,8 +576,8 @@
 TEST_F(FullCardRequestTest, VcnRetrievalPermanentFailure) {
   EXPECT_CALL(
       *result_delegate(),
-      OnFullCardRequestFailed(
-          FullCardRequest::FailureType::VIRTUAL_CARD_RETRIEVAL_FAILURE));
+      OnFullCardRequestFailed(FullCardRequest::FailureType::
+                                  VIRTUAL_CARD_RETRIEVAL_PERMANENT_FAILURE));
   EXPECT_CALL(*ui_delegate(), ShowUnmaskPrompt(_, _, _));
   EXPECT_CALL(*ui_delegate(),
               OnUnmaskVerificationResult(
diff --git a/components/autofill/core/browser/payments/test_authentication_requester.cc b/components/autofill/core/browser/payments/test_authentication_requester.cc
index 97b7fb1..8ae3dfa4 100644
--- a/components/autofill/core/browser/payments/test_authentication_requester.cc
+++ b/components/autofill/core/browser/payments/test_authentication_requester.cc
@@ -42,14 +42,13 @@
 
 #if !defined(OS_IOS)
 void TestAuthenticationRequester::OnFIDOAuthenticationComplete(
-    bool did_succeed,
-    const CreditCard* card,
-    const std::u16string& cvc) {
-  did_succeed_ = did_succeed;
+    const CreditCardFIDOAuthenticator::FidoAuthenticationResponse& response) {
+  did_succeed_ = response.did_succeed;
   if (did_succeed_) {
-    DCHECK(card);
-    number_ = card->number();
+    DCHECK(response.card);
+    number_ = response.card->number();
   }
+  failure_type_ = response.failure_type;
 }
 
 void TestAuthenticationRequester::OnFidoAuthorizationComplete(
diff --git a/components/autofill/core/browser/payments/test_authentication_requester.h b/components/autofill/core/browser/payments/test_authentication_requester.h
index 0562996..7e18952 100644
--- a/components/autofill/core/browser/payments/test_authentication_requester.h
+++ b/components/autofill/core/browser/payments/test_authentication_requester.h
@@ -13,6 +13,7 @@
 #include "build/build_config.h"
 #include "components/autofill/core/browser/data_model/credit_card.h"
 #include "components/autofill/core/browser/payments/credit_card_cvc_authenticator.h"
+#include "components/autofill/core/browser/payments/full_card_request.h"
 
 #if !defined(OS_IOS)
 #include "components/autofill/core/browser/payments/credit_card_fido_authenticator.h"
@@ -46,9 +47,8 @@
 #if !defined(OS_IOS)
   // CreditCardFIDOAuthenticator::Requester:
   void OnFIDOAuthenticationComplete(
-      bool did_succeed,
-      const CreditCard* card = nullptr,
-      const std::u16string& cvc = std::u16string()) override;
+      const CreditCardFIDOAuthenticator::FidoAuthenticationResponse& response)
+      override;
   void OnFidoAuthorizationComplete(bool did_succeed) override;
 
   void IsUserVerifiableCallback(bool is_user_verifiable);
@@ -62,6 +62,10 @@
 
   std::u16string number() { return number_; }
 
+  payments::FullCardRequest::FailureType failure_type() {
+    return failure_type_;
+  }
+
  private:
   // Set when CreditCardFIDOAuthenticator invokes IsUserVerifiableCallback().
   absl::optional<bool> is_user_verifiable_;
@@ -69,6 +73,11 @@
   // Is set to true if authentication was successful.
   bool did_succeed_ = false;
 
+  // The failure type of the full card request. Set when the request is
+  // finished.
+  payments::FullCardRequest::FailureType failure_type_ =
+      payments::FullCardRequest::UNKNOWN;
+
   // The card number returned from On*AuthenticationComplete().
   std::u16string number_;
 
diff --git a/components/browsing_data/content/README.md b/components/browsing_data/content/README.md
new file mode 100644
index 0000000..4e03fcd
--- /dev/null
+++ b/components/browsing_data/content/README.md
@@ -0,0 +1,41 @@
+Types in this directory are used to populate a `CookiesTreeModel`;
+see //chrome/browser/browsing_data.
+
+## XYZHelper
+
+Instances of this type are used to fully populate a `CookiesTreeModel`
+with full details (e.g. origin/size/modified) for different storage
+types, e.g. to report storage used by all origins.
+
+When `StartFetching()` is called, a call is made into the relevant
+storage context to enumerate usage info - usually, a set of tuples of
+(origin, size, last modified). The CookiesTreeModel assembles this
+into the tree of nodes used to populate UI.
+
+Some UI also uses this to delete origin data, which again calls into
+the storage context.
+
+## CannedXYZHelper
+
+Note that despite the name ("canned"), this is *not* a test-only type.
+
+
+Subclass of the above. These are created to sparsely populate a
+`CookiesTreeModel` on demand by `LocalSharedObjectContainer`, with
+only some details (e.g. full details for cookies, but only the usage
+of other storage typess).
+
+* `PageSpecificContentSettings` is notified on storage access/blocked.
+* It calls into the "canned" helper instance for the storage type.
+* The "canned" instance records necessary "pending" info about the access.
+* On demand, the "pending" info is used to populate a CookiesTreeModel.
+
+This "pending" info only needs to record the origin for most storage
+types.
+
+## MockXYZHelper
+
+Mock class for testing, only.
+
+Adds an `AddXYZSamples()` method that populates the instance with
+test data.
diff --git a/components/cast/message_port/BUILD.gn b/components/cast/message_port/BUILD.gn
index 392580a..e9df1dc0 100644
--- a/components/cast/message_port/BUILD.gn
+++ b/components/cast/message_port/BUILD.gn
@@ -3,10 +3,14 @@
 # found in the LICENSE file.
 
 import("//build/config/features.gni")
+import("//chromecast/chromecast.gni")
+import("//media/media_options.gni")
 import("//testing/test.gni")
 
 source_set("message_port") {
-  if (is_fuchsia) {
+  if (enable_cast_runtime_e2e_testing) {
+    public_deps = [ ":message_port_mojo" ]
+  } else if (is_fuchsia) {
     public_deps = [ ":message_port_fuchsia" ]
   } else {
     public_deps = [ ":message_port_cast" ]
@@ -61,6 +65,19 @@
   ]
 }
 
+source_set("message_port_mojo") {
+  public = [ "message_port_mojo.h" ]
+
+  sources = [ "message_port_mojo.cc" ]
+
+  public_deps = [
+    ":public",
+    "//components/cast/message_port/mojom",
+  ]
+
+  deps = [ ":public" ]
+}
+
 source_set("message_port_unittest") {
   testonly = true
   sources = [ "message_port_unittest.cc" ]
@@ -68,6 +85,9 @@
     ":message_port",
     ":test_message_port_receiver",
     "//base/test:test_support",
+    "//build:buildflag_header_h",
+    "//components/cast/message_port/mojom",
+    "//media:media_buildflags",
     "//testing/gtest",
   ]
 }
diff --git a/components/cast/message_port/DEPS b/components/cast/message_port/DEPS
index 362bdec..955f55b 100644
--- a/components/cast/message_port/DEPS
+++ b/components/cast/message_port/DEPS
@@ -1,5 +1,8 @@
 include_rules = [
   # TODO(crbug.com/1120731): Remove dependencies on //fuchsia.
   "+fuchsia/base/mem_buffer_util.h",
+
+  "+media/media_buildflags.h",
+  "+mojo/public",
   "+third_party/blink/public/common/messaging",
 ]
diff --git a/components/cast/message_port/message_port_mojo.cc b/components/cast/message_port/message_port_mojo.cc
new file mode 100644
index 0000000..98309a8
--- /dev/null
+++ b/components/cast/message_port/message_port_mojo.cc
@@ -0,0 +1,123 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/cast/message_port/message_port_mojo.h"
+
+#include <vector>
+
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "mojo/public/cpp/bindings/pending_remote.h"
+
+namespace cast_api_bindings {
+
+// static
+void MessagePort::CreatePair(std::unique_ptr<MessagePort>* client,
+                             std::unique_ptr<MessagePort>* server) {
+  mojo::PendingRemote<mojom::UnidirectionalMessagePort> client_remote;
+  mojo::PendingRemote<mojom::UnidirectionalMessagePort> server_remote;
+
+  auto client_receiver = server_remote.InitWithNewPipeAndPassReceiver();
+  auto server_receiver = client_remote.InitWithNewPipeAndPassReceiver();
+
+  *client = std::make_unique<MessagePortMojo>(std::move(client_receiver),
+                                              std::move(client_remote));
+  *server = std::make_unique<MessagePortMojo>(std::move(server_receiver),
+                                              std::move(server_remote));
+}
+
+MessagePortMojo::MessagePortMojo(
+    mojo::PendingReceiver<mojom::UnidirectionalMessagePort> receiver,
+    mojo::PendingRemote<mojom::UnidirectionalMessagePort> remote)
+    : mojo_remote_(std::move(remote)),
+      mojo_receiver_(this, std::move(receiver)),
+      weak_factory_(this) {
+  mojo_receiver_.set_disconnect_handler(base::BindOnce(
+      &MessagePortMojo::OnMojoDisconnected, weak_factory_.GetWeakPtr()));
+  mojo_remote_.set_disconnect_handler(base::BindOnce(
+      &MessagePortMojo::OnMojoDisconnected, weak_factory_.GetWeakPtr()));
+}
+
+MessagePortMojo::~MessagePortMojo() = default;
+
+bool MessagePortMojo::PostMessage(base::StringPiece message) {
+  return PostMessageWithTransferables(std::move(message), {});
+}
+
+bool MessagePortMojo::PostMessageWithTransferables(
+    base::StringPiece message,
+    std::vector<std::unique_ptr<MessagePort>> ports) {
+  if (!CanPostMessage()) {
+    return false;
+  }
+
+  std::vector<mojom::MessagePortPtr> mojo_ports;
+  mojo_ports.reserve(ports.size());
+  for (std::unique_ptr<MessagePort>& port : ports) {
+    DCHECK(port);
+
+    // The |mojo_remote_| object used below would be invalidated in this
+    // process.
+    DCHECK(port.get() != this);
+
+    // This is safe because there is one MessagePort implementation used at a
+    // time.
+    MessagePortMojo* casted_port = static_cast<MessagePortMojo*>(port.get());
+
+    mojo_ports.push_back(casted_port->Unbind());
+  }
+
+  mojo_remote_->PostMessageWithTransferables(
+      std::string(message.begin(), message.end()), std::move(mojo_ports));
+  return true;
+}
+
+void MessagePortMojo::SetReceiver(MessagePort::Receiver* receiver) {
+  receiver_ = receiver;
+}
+
+void MessagePortMojo::Close() {
+  mojo_remote_.reset();
+  mojo_receiver_.reset();
+}
+
+bool MessagePortMojo::CanPostMessage() const {
+  return mojo_remote_.is_bound();
+}
+
+void MessagePortMojo::PostMessageWithTransferables(
+    const std::string& message,
+    std::vector<mojom::MessagePortPtr> ports) {
+  if (!receiver_) {
+    return;
+  }
+
+  std::vector<std::unique_ptr<MessagePort>> mojo_ports;
+  mojo_ports.reserve(ports.size());
+  for (mojom::MessagePortPtr& port : ports) {
+    mojo_ports.push_back(std::make_unique<MessagePortMojo>(
+        std::move(port.get()->receiver), std::move(port.get()->remote)));
+  }
+
+  receiver_->OnMessage(message, std::move(mojo_ports));
+}
+
+mojom::MessagePortPtr MessagePortMojo::Unbind() {
+  if (!mojo_remote_.is_bound() || !mojo_receiver_.is_bound()) {
+    return nullptr;
+  }
+
+  return mojom::MessagePort::New(mojo_receiver_.Unbind(),
+                                 mojo_remote_.Unbind());
+}
+
+void MessagePortMojo::OnMojoDisconnected() {
+  if (!receiver_) {
+    return;
+  }
+
+  receiver_->OnPipeError();
+  Close();
+}
+
+}  // namespace cast_api_bindings
diff --git a/components/cast/message_port/message_port_mojo.h b/components/cast/message_port/message_port_mojo.h
new file mode 100644
index 0000000..a1b792a
--- /dev/null
+++ b/components/cast/message_port/message_port_mojo.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 COMPONENTS_CAST_MESSAGE_PORT_MESSAGE_PORT_MOJO_H_
+#define COMPONENTS_CAST_MESSAGE_PORT_MESSAGE_PORT_MOJO_H_
+
+#include <string>
+
+#include "components/cast/message_port/message_port.h"
+#include "components/cast/message_port/mojom/unidirectional_message_port.mojom.h"
+#include "mojo/public/cpp/bindings/receiver.h"
+#include "mojo/public/cpp/bindings/remote.h"
+
+namespace cast_api_bindings {
+
+// A MessagePort implementation built on top of a Mojo stream.
+class MessagePortMojo : public mojom::UnidirectionalMessagePort,
+                        public MessagePort {
+ public:
+  MessagePortMojo(
+      mojo::PendingReceiver<mojom::UnidirectionalMessagePort> receiver,
+      mojo::PendingRemote<mojom::UnidirectionalMessagePort> remote);
+  ~MessagePortMojo() override;
+
+  // MessagePort overrides.
+  bool PostMessage(base::StringPiece message) override;
+  bool PostMessageWithTransferables(
+      base::StringPiece message,
+      std::vector<std::unique_ptr<MessagePort>> ports) override;
+  void SetReceiver(MessagePort::Receiver* receiver) override;
+  void Close() override;
+  bool CanPostMessage() const override;
+
+ private:
+  // mojom::UnidirectionalMessagePort overrides.
+  void PostMessageWithTransferables(
+      const std::string& message,
+      std::vector<mojom::MessagePortPtr> ports) override;
+
+  // Returns the mojo::MessagePort containing both the |sender_| and |receiver_|
+  // associated with this instance, which is invalidated by this method.
+  mojom::MessagePortPtr Unbind();
+
+  // Called as a disconnect handler for |sender_| and |receiver_|.
+  void OnMojoDisconnected();
+
+  MessagePort::Receiver* receiver_ = nullptr;
+
+  mojo::Remote<mojom::UnidirectionalMessagePort> mojo_remote_;
+  mojo::Receiver<mojom::UnidirectionalMessagePort> mojo_receiver_;
+
+  base::WeakPtrFactory<MessagePortMojo> weak_factory_;
+};
+
+}  // namespace cast_api_bindings
+
+#endif  // COMPONENTS_CAST_MESSAGE_PORT_MESSAGE_PORT_MOJO_H_
diff --git a/components/cast/message_port/message_port_unittest.cc b/components/cast/message_port/message_port_unittest.cc
index 4b8a190..02250bc 100644
--- a/components/cast/message_port/message_port_unittest.cc
+++ b/components/cast/message_port/message_port_unittest.cc
@@ -6,15 +6,19 @@
 #include "base/run_loop.h"
 #include "base/test/task_environment.h"
 #include "build/build_config.h"
+#include "build/buildflag.h"
 #include "components/cast/message_port/test_message_port_receiver.h"
+#include "media/media_buildflags.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
-#if defined(OS_FUCHSIA)
+#if BUILDFLAG(ENABLE_CAST_RUNTIME_E2E_TESTING)
+#include "components/cast/message_port/message_port_mojo.h"  // nogncheck
+#elif defined(OS_FUCHSIA)
 #include "components/cast/message_port/message_port_fuchsia.h"
 #else
 #include "components/cast/message_port/message_port_cast.h"  // nogncheck
 #include "third_party/blink/public/common/messaging/web_message_port.h"  // nogncheck
-#endif  // defined(OS_FUCHSIA)
+#endif  // BUILDFLAG(ENABLE_CAST_RUNTIME_E2E_TESTING)
 
 #ifdef PostMessage
 #undef PostMessage
@@ -92,21 +96,6 @@
   ASSERT_FALSE(server_->CanPostMessage());
 }
 
-TEST_F(MessagePortTest, OnError) {
-  server_receiver_.SetOnMessageResult(false);
-  SetDefaultReceivers();
-  client_->PostMessage("");
-
-#if defined(OS_FUCHSIA)
-  // blink::WebMessagePort reports failure when PostMessage returns false, but
-  // fuchsia::web::MessagePort will not report the error until the port closes
-  server_receiver_.RunUntilMessageCountEqual(1);
-  server_.reset();
-#endif
-
-  client_receiver_.RunUntilDisconnected();
-}
-
 TEST_F(MessagePortTest, PostMessage) {
   TestPostMessage();
 }
@@ -139,6 +128,22 @@
   PostMessages({"from port1"}, port1.get(), &port0_receiver);
 }
 
+#if !BUILDFLAG(ENABLE_CAST_RUNTIME_E2E_TESTING)
+TEST_F(MessagePortTest, OnError) {
+  server_receiver_.SetOnMessageResult(false);
+  SetDefaultReceivers();
+  client_->PostMessage("");
+
+#if defined(OS_FUCHSIA)
+  // blink::WebMessagePort reports failure when PostMessage returns false, but
+  // fuchsia::web::MessagePort will not report the error until the port closes
+  server_receiver_.RunUntilMessageCountEqual(1);
+  server_.reset();
+#endif
+
+  client_receiver_.RunUntilDisconnected();
+}
+
 TEST_F(MessagePortTest, WrapPlatformPort) {
   // Initialize ports from the platform type instead of agnostic CreatePair
 #if defined(OS_FUCHSIA)
@@ -171,5 +176,6 @@
 
   TestPostMessage();
 }
+#endif  // !BUILDFLAG(ENABLE_CAST_RUNTIME_E2E_TESTING)
 
 }  // namespace cast_api_bindings
diff --git a/components/cast/message_port/mojom/BUILD.gn b/components/cast/message_port/mojom/BUILD.gn
new file mode 100644
index 0000000..7016dc4
--- /dev/null
+++ b/components/cast/message_port/mojom/BUILD.gn
@@ -0,0 +1,10 @@
+# Copyright 2021 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//mojo/public/tools/bindings/mojom.gni")
+
+mojom("mojom") {
+  sources = [ "unidirectional_message_port.mojom" ]
+  public_deps = [ "//mojo/public/mojom/base" ]
+}
diff --git a/components/cast/message_port/mojom/OWNERS b/components/cast/message_port/mojom/OWNERS
new file mode 100644
index 0000000..08850f4
--- /dev/null
+++ b/components/cast/message_port/mojom/OWNERS
@@ -0,0 +1,2 @@
+per-file *.mojom=set noparent
+per-file *.mojom=file://ipc/SECURITY_OWNERS
diff --git a/components/cast/message_port/mojom/unidirectional_message_port.mojom b/components/cast/message_port/mojom/unidirectional_message_port.mojom
new file mode 100644
index 0000000..438face
--- /dev/null
+++ b/components/cast/message_port/mojom/unidirectional_message_port.mojom
@@ -0,0 +1,21 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+module cast_api_bindings.mojom;
+
+// Mojo MessagePort representation based on a pair of
+// UnidirectionalMessagePorts.
+struct MessagePort {
+  pending_receiver<UnidirectionalMessagePort> receiver;
+  pending_remote<UnidirectionalMessagePort> remote;
+};
+
+// A UnidirectionalMessagePort represents a single flow direction in a
+// bidirectional MessagePort instance. MessagePorts can both send and receive
+// data and MessagePorts so a single MessagePort corresponds to the above
+// struct, which contains both a receiver and a remote endpoint of this
+// interface.
+interface UnidirectionalMessagePort {
+  PostMessageWithTransferables(string message, array<MessagePort> ports);
+};
diff --git a/components/cast_streaming/browser/BUILD.gn b/components/cast_streaming/browser/BUILD.gn
index d740ff4b..468d1849 100644
--- a/components/cast_streaming/browser/BUILD.gn
+++ b/components/cast_streaming/browser/BUILD.gn
@@ -2,6 +2,7 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+import("//media/media_options.gni")
 import("//testing/test.gni")
 
 # TODO(crbug.com/1207718): Remove cast_message_port_impl code from this
@@ -22,16 +23,28 @@
   sources = [
     "cast_message_port_impl.cc",
     "cast_message_port_impl.h",
-    "config_conversions.cc",
-    "config_conversions.h",
     "message_serialization.cc",
     "message_serialization.h",
   ]
 }
 
+source_set("receiver_session_public") {
+  deps = [
+    "//base",
+    "//mojo/public/cpp/system",
+  ]
+  public_deps = [
+    "//components/cast_streaming/public/mojom",
+    "//third_party/openscreen/src/cast/streaming:receiver",
+  ]
+  visibility = [ ":*" ]
+  sources = [ "public/receiver_session.h" ]
+}
+
 source_set("receiver_session") {
   deps = [
     ":core",
+    ":receiver_session_public",
     ":streaming_session",
     "//base",
     "//media",
@@ -41,10 +54,9 @@
   ]
   public_deps = [
     "//components/cast_streaming/public/mojom",
-    "//third_party/openscreen/src/cast/common:channel",
+    "//third_party/openscreen/src/cast/streaming:receiver",
   ]
   visibility = [ ":*" ]
-  public = [ "public/receiver_session.h" ]
   sources = [
     "receiver_session_impl.cc",
     "receiver_session_impl.h",
@@ -54,7 +66,9 @@
 source_set("streaming_session") {
   deps = [
     ":core",
+    ":receiver_session_public",
     "//base",
+    "//components/cast_streaming/public",
     "//components/openscreen_platform",
     "//media",
     "//media/mojo/common",
@@ -84,13 +98,12 @@
   sources = [ "network_context_getter.cc" ]
 }
 
-# TODO(crbug.com/1208194): Also move cast_streaming_session_client here from
-# //fuchsia.
 source_set("browser") {
   public_deps = [
     ":network_context",
-    ":receiver_session",
+    ":receiver_session_public",
   ]
+  deps = [ ":receiver_session" ]
 }
 
 # TODO(crbug.com/1207715): Move to /tests directory.
@@ -98,6 +111,7 @@
   testonly = true
   deps = [
     ":core",
+    "//components/cast_streaming/public",
     "//media/mojo/common",
     "//media/mojo/mojom",
     "//mojo/public/cpp/system",
@@ -125,6 +139,7 @@
   deps = [
     ":streaming_session",
     "//base",
+    "//components/cast_streaming/public",
     "//components/openscreen_platform",
     "//media",
     "//media/mojo/common",
@@ -172,5 +187,8 @@
     "//components/cast/message_port:test_message_port_receiver",
     "//testing/gtest",
   ]
-  sources = [ "cast_message_port_impl_unittest.cc" ]
+  sources = []
+  if (!enable_cast_runtime_e2e_testing) {
+    sources += [ "cast_message_port_impl_unittest.cc" ]
+  }
 }
diff --git a/components/cast_streaming/browser/cast_streaming_session.cc b/components/cast_streaming/browser/cast_streaming_session.cc
index 3251d90..b63cc0f 100644
--- a/components/cast_streaming/browser/cast_streaming_session.cc
+++ b/components/cast_streaming/browser/cast_streaming_session.cc
@@ -6,8 +6,8 @@
 
 #include "base/bind.h"
 #include "base/time/time.h"
-#include "components/cast_streaming/browser/config_conversions.h"
 #include "components/cast_streaming/browser/stream_consumer.h"
+#include "components/cast_streaming/public/config_conversions.h"
 #include "media/base/timestamp_constants.h"
 #include "media/mojo/common/mojo_decoder_buffer_converter.h"
 #include "mojo/public/cpp/system/data_pipe.h"
@@ -38,6 +38,7 @@
 
 CastStreamingSession::ReceiverSessionClient::ReceiverSessionClient(
     CastStreamingSession::Client* client,
+    std::unique_ptr<ReceiverSession::AVConstraints> av_constraints,
     std::unique_ptr<cast_api_bindings::MessagePort> message_port,
     scoped_refptr<base::SequencedTaskRunner> task_runner)
     : task_runner_(task_runner),
@@ -51,15 +52,9 @@
   DCHECK(task_runner);
   DCHECK(client_);
 
-  // TODO(crbug.com/1087520): Add streaming session Constraints and
-  // DisplayDescription.
   receiver_session_ = std::make_unique<openscreen::cast::ReceiverSession>(
       this, &environment_, &cast_message_port_impl_,
-      openscreen::cast::ReceiverSession::Preferences(
-          {openscreen::cast::VideoCodec::kH264,
-           openscreen::cast::VideoCodec::kVp8},
-          {openscreen::cast::AudioCodec::kAac,
-           openscreen::cast::AudioCodec::kOpus}));
+      std::move(*av_constraints));
 
   init_timeout_timer_.Start(
       FROM_HERE, kInitTimeout,
@@ -102,9 +97,8 @@
       base::BindRepeating(&base::OneShotTimer::Reset,
                           base::Unretained(&data_timeout_timer_)));
 
-  return AudioStreamInfo{
-      AudioCaptureConfigToAudioDecoderConfig(audio_capture_config),
-      std::move(data_pipe_consumer)};
+  return AudioStreamInfo{ToAudioDecoderConfig(audio_capture_config),
+                         std::move(data_pipe_consumer)};
 }
 
 absl::optional<CastStreamingSession::VideoStreamInfo>
@@ -137,9 +131,8 @@
       base::BindRepeating(&base::OneShotTimer::Reset,
                           base::Unretained(&data_timeout_timer_)));
 
-  return VideoStreamInfo{
-      VideoCaptureConfigToVideoDecoderConfig(video_capture_config),
-      std::move(data_pipe_consumer)};
+  return VideoStreamInfo{ToVideoDecoderConfig(video_capture_config),
+                         std::move(data_pipe_consumer)};
 }
 
 void CastStreamingSession::ReceiverSessionClient::OnNegotiated(
@@ -266,13 +259,14 @@
 
 void CastStreamingSession::Start(
     Client* client,
+    std::unique_ptr<ReceiverSession::AVConstraints> av_constraints,
     std::unique_ptr<cast_api_bindings::MessagePort> message_port,
     scoped_refptr<base::SequencedTaskRunner> task_runner) {
   DVLOG(1) << __func__;
   DCHECK(client);
   DCHECK(!receiver_session_);
   receiver_session_ = std::make_unique<ReceiverSessionClient>(
-      client, std::move(message_port), task_runner);
+      client, std::move(av_constraints), std::move(message_port), task_runner);
 }
 
 void CastStreamingSession::Stop() {
diff --git a/components/cast_streaming/browser/cast_streaming_session.h b/components/cast_streaming/browser/cast_streaming_session.h
index 2002f67..199f7fa 100644
--- a/components/cast_streaming/browser/cast_streaming_session.h
+++ b/components/cast_streaming/browser/cast_streaming_session.h
@@ -12,6 +12,7 @@
 #include "base/timer/timer.h"
 #include "components/cast/message_port/message_port.h"
 #include "components/cast_streaming/browser/cast_message_port_impl.h"
+#include "components/cast_streaming/browser/public/receiver_session.h"
 #include "components/openscreen_platform/network_util.h"
 #include "components/openscreen_platform/task_runner.h"
 #include "media/base/audio_decoder_config.h"
@@ -85,7 +86,11 @@
   // * On failure, OnSessionEnded() will be called.
   // * When a new offer is sent by the Cast Streaming Sender,
   //   OnSessionReinitialization() will be called.
+  //
+  // |av_constraints| specifies the supported media codecs and limitations
+  // surrounding this support.
   void Start(Client* client,
+             std::unique_ptr<ReceiverSession::AVConstraints> av_constraints,
              std::unique_ptr<cast_api_bindings::MessagePort> message_port,
              scoped_refptr<base::SequencedTaskRunner> task_runner);
 
@@ -101,6 +106,7 @@
    public:
     ReceiverSessionClient(
         CastStreamingSession::Client* client,
+        std::unique_ptr<ReceiverSession::AVConstraints> av_constraints,
         std::unique_ptr<cast_api_bindings::MessagePort> message_port,
         scoped_refptr<base::SequencedTaskRunner> task_runner);
     ~ReceiverSessionClient() final;
diff --git a/components/cast_streaming/browser/config_conversions.cc b/components/cast_streaming/browser/config_conversions.cc
deleted file mode 100644
index e41d2f0..0000000
--- a/components/cast_streaming/browser/config_conversions.cc
+++ /dev/null
@@ -1,112 +0,0 @@
-// Copyright 2020 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "components/cast_streaming/browser/config_conversions.h"
-
-#include "base/notreached.h"
-#include "media/base/media_util.h"
-
-namespace cast_streaming {
-
-openscreen::cast::AudioCaptureConfig AudioDecoderConfigToAudioCaptureConfig(
-    const media::AudioDecoderConfig& audio_config) {
-  openscreen::cast::AudioCaptureConfig audio_capture_config;
-
-  switch (audio_config.codec()) {
-    case media::AudioCodec::kCodecAAC:
-      audio_capture_config.codec = openscreen::cast::AudioCodec::kAac;
-      break;
-    case media::AudioCodec::kCodecOpus:
-      audio_capture_config.codec = openscreen::cast::AudioCodec::kOpus;
-      break;
-    default:
-      NOTREACHED();
-  }
-
-  audio_capture_config.channels =
-      media::ChannelLayoutToChannelCount(audio_config.channel_layout());
-  audio_capture_config.sample_rate = audio_config.samples_per_second();
-
-  return audio_capture_config;
-}
-
-openscreen::cast::VideoCaptureConfig VideoDecoderConfigToVideoCaptureConfig(
-    const media::VideoDecoderConfig& video_config) {
-  openscreen::cast::VideoCaptureConfig video_capture_config;
-
-  switch (video_config.codec()) {
-    case media::VideoCodec::kCodecH264:
-      video_capture_config.codec = openscreen::cast::VideoCodec::kH264;
-      break;
-    case media::VideoCodec::kCodecVP8:
-      video_capture_config.codec = openscreen::cast::VideoCodec::kVp8;
-      break;
-    default:
-      NOTREACHED();
-  }
-
-  video_capture_config.resolutions.push_back(
-      {video_config.visible_rect().width(),
-       video_config.visible_rect().height()});
-  return video_capture_config;
-}
-
-media::AudioDecoderConfig AudioCaptureConfigToAudioDecoderConfig(
-    const openscreen::cast::AudioCaptureConfig& audio_capture_config) {
-  // Gather data for the audio decoder config.
-  media::AudioCodec media_audio_codec = media::AudioCodec::kUnknownAudioCodec;
-  switch (audio_capture_config.codec) {
-    case openscreen::cast::AudioCodec::kAac:
-      media_audio_codec = media::AudioCodec::kCodecAAC;
-      break;
-    case openscreen::cast::AudioCodec::kOpus:
-      media_audio_codec = media::AudioCodec::kCodecOpus;
-      break;
-    default:
-      NOTREACHED();
-      break;
-  }
-
-  return media::AudioDecoderConfig(
-      media_audio_codec, media::SampleFormat::kSampleFormatF32,
-      media::GuessChannelLayout(audio_capture_config.channels),
-      audio_capture_config.sample_rate /* samples_per_second */,
-      media::EmptyExtraData(), media::EncryptionScheme::kUnencrypted);
-}
-
-media::VideoDecoderConfig VideoCaptureConfigToVideoDecoderConfig(
-    const openscreen::cast::VideoCaptureConfig& video_capture_config) {
-  // Gather data for the video decoder config.
-  uint32_t video_width = video_capture_config.resolutions[0].width;
-  uint32_t video_height = video_capture_config.resolutions[0].height;
-  gfx::Size video_size(video_width, video_height);
-  gfx::Rect video_rect(video_width, video_height);
-
-  media::VideoCodec media_video_codec = media::VideoCodec::kUnknownVideoCodec;
-  media::VideoCodecProfile video_codec_profile =
-      media::VideoCodecProfile::VIDEO_CODEC_PROFILE_UNKNOWN;
-  switch (video_capture_config.codec) {
-    case openscreen::cast::VideoCodec::kH264:
-      media_video_codec = media::VideoCodec::kCodecH264;
-      video_codec_profile = media::VideoCodecProfile::H264PROFILE_BASELINE;
-      break;
-    case openscreen::cast::VideoCodec::kVp8:
-      media_video_codec = media::VideoCodec::kCodecVP8;
-      video_codec_profile = media::VideoCodecProfile::VP8PROFILE_MIN;
-      break;
-    case openscreen::cast::VideoCodec::kHevc:
-    case openscreen::cast::VideoCodec::kVp9:
-    default:
-      NOTREACHED();
-      break;
-  }
-
-  return media::VideoDecoderConfig(
-      media_video_codec, video_codec_profile,
-      media::VideoDecoderConfig::AlphaMode::kIsOpaque, media::VideoColorSpace(),
-      media::VideoTransformation(), video_size, video_rect, video_size,
-      media::EmptyExtraData(), media::EncryptionScheme::kUnencrypted);
-}
-
-}  // namespace cast_streaming
diff --git a/components/cast_streaming/browser/config_conversions.h b/components/cast_streaming/browser/config_conversions.h
deleted file mode 100644
index ce3044a..0000000
--- a/components/cast_streaming/browser/config_conversions.h
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright 2020 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef COMPONENTS_CAST_STREAMING_BROWSER_CONFIG_CONVERSIONS_H_
-#define COMPONENTS_CAST_STREAMING_BROWSER_CONFIG_CONVERSIONS_H_
-
-#include "media/base/audio_decoder_config.h"
-#include "media/base/video_decoder_config.h"
-#include "third_party/openscreen/src/cast/streaming/capture_configs.h"
-
-namespace cast_streaming {
-
-// Utility functions to convert between media and Open Screen types.
-
-openscreen::cast::AudioCaptureConfig AudioDecoderConfigToAudioCaptureConfig(
-    const media::AudioDecoderConfig& audio_config);
-
-openscreen::cast::VideoCaptureConfig VideoDecoderConfigToVideoCaptureConfig(
-    const media::VideoDecoderConfig& video_config);
-
-media::AudioDecoderConfig AudioCaptureConfigToAudioDecoderConfig(
-    const openscreen::cast::AudioCaptureConfig& audio_capture_config);
-
-media::VideoDecoderConfig VideoCaptureConfigToVideoDecoderConfig(
-    const openscreen::cast::VideoCaptureConfig& video_capture_config);
-
-}  // namespace cast_streaming
-
-#endif  // COMPONENTS_CAST_STREAMING_BROWSER_CONFIG_CONVERSIONS_H_
diff --git a/components/cast_streaming/browser/public/receiver_session.h b/components/cast_streaming/browser/public/receiver_session.h
index 2db3ddbc..80a8b3c 100644
--- a/components/cast_streaming/browser/public/receiver_session.h
+++ b/components/cast_streaming/browser/public/receiver_session.h
@@ -10,6 +10,7 @@
 #include "base/callback.h"
 #include "components/cast_streaming/public/mojom/cast_streaming_session.mojom.h"
 #include "mojo/public/cpp/bindings/associated_remote.h"
+#include "third_party/openscreen/src/cast/streaming/receiver_session.h"
 
 namespace cast_api_bindings {
 class MessagePort;
@@ -21,14 +22,25 @@
 // |message_port| and with a given |cast_streaming_receiver|. On destruction,
 // the Cast Streaming Receiver Session will be terminated if it was ever
 // started.
+// TODO(1220176): Forward declare ReceiverSession::Preferences instead of
+// requiring the import above.
 class ReceiverSession {
  public:
   using MessagePortProvider =
       base::OnceCallback<std::unique_ptr<cast_api_bindings::MessagePort>()>;
+  using AVConstraints = openscreen::cast::ReceiverSession::Preferences;
 
   virtual ~ReceiverSession() = default;
 
+  // |av_constraints| specifies the supported media codecs, an ordering to
+  // signify the receiver's preferences of which codecs should be used, and any
+  // limitations surrounding this support.
+  // |message_port_provider| creates a new MessagePort to be used for sending
+  // and receiving Cast messages.
+  // TODO(crbug.com/1219079): Add conversion functions to create the
+  // ReceiverSession::Preferences object from //media types.
   static std::unique_ptr<ReceiverSession> Create(
+      std::unique_ptr<AVConstraints> av_constraints,
       MessagePortProvider message_port_provider);
 
   // Sets up the CastStreamingReceiver mojo remote. This will immediately call
diff --git a/components/cast_streaming/browser/receiver_session_impl.cc b/components/cast_streaming/browser/receiver_session_impl.cc
index e391c2d..d8fb639 100644
--- a/components/cast_streaming/browser/receiver_session_impl.cc
+++ b/components/cast_streaming/browser/receiver_session_impl.cc
@@ -13,14 +13,19 @@
 
 // static
 std::unique_ptr<ReceiverSession> ReceiverSession::Create(
+    std::unique_ptr<ReceiverSession::AVConstraints> av_constraints,
     ReceiverSession::MessagePortProvider message_port_provider) {
   return std::make_unique<ReceiverSessionImpl>(
-      std::move(message_port_provider));
+      std::move(av_constraints), std::move(message_port_provider));
 }
 
 ReceiverSessionImpl::ReceiverSessionImpl(
+    std::unique_ptr<ReceiverSession::AVConstraints> av_constraints,
     ReceiverSession::MessagePortProvider message_port_provider)
-    : message_port_provider_(std::move(message_port_provider)) {
+    : message_port_provider_(std::move(message_port_provider)),
+      av_constraints_(std::move(av_constraints)) {
+  // TODO(crbug.com/1218495): Validate the provided codecs against build flags.
+  DCHECK(av_constraints_);
   DCHECK(message_port_provider_);
 }
 
@@ -44,7 +49,8 @@
 void ReceiverSessionImpl::OnReceiverEnabled() {
   DVLOG(1) << __func__;
   DCHECK(message_port_provider_);
-  cast_streaming_session_.Start(this, std::move(message_port_provider_).Run(),
+  cast_streaming_session_.Start(this, std::move(av_constraints_),
+                                std::move(message_port_provider_).Run(),
                                 base::SequencedTaskRunnerHandle::Get());
 }
 
@@ -128,6 +134,7 @@
 
   // Close the underlying connection.
   if (message_port_provider_) {
+    av_constraints_ = std::make_unique<ReceiverSession::AVConstraints>();
     std::move(message_port_provider_).Run().reset();
   }
 
diff --git a/components/cast_streaming/browser/receiver_session_impl.h b/components/cast_streaming/browser/receiver_session_impl.h
index 8d64941..33b6d78 100644
--- a/components/cast_streaming/browser/receiver_session_impl.h
+++ b/components/cast_streaming/browser/receiver_session_impl.h
@@ -19,7 +19,11 @@
 class ReceiverSessionImpl : public cast_streaming::CastStreamingSession::Client,
                             public ReceiverSession {
  public:
-  explicit ReceiverSessionImpl(MessagePortProvider message_port_provider);
+  // |av_constraints| specifies the supported media codecs and limitations
+  // surrounding this support.
+  ReceiverSessionImpl(
+      std::unique_ptr<ReceiverSession::AVConstraints> av_constraints,
+      MessagePortProvider message_port_provider);
   ~ReceiverSessionImpl() final;
 
   ReceiverSessionImpl(const ReceiverSessionImpl&) = delete;
@@ -55,6 +59,7 @@
   // Populated in the ctor, and empty following a call to either
   // OnReceiverEnabled() or OnMojoDisconnect().
   MessagePortProvider message_port_provider_;
+  std::unique_ptr<ReceiverSession::AVConstraints> av_constraints_;
 
   mojo::AssociatedRemote<mojom::CastStreamingReceiver> cast_streaming_receiver_;
   cast_streaming::CastStreamingSession cast_streaming_session_;
diff --git a/components/cast_streaming/browser/test/cast_streaming_test_receiver.cc b/components/cast_streaming/browser/test/cast_streaming_test_receiver.cc
index 140448e..cfc927f 100644
--- a/components/cast_streaming/browser/test/cast_streaming_test_receiver.cc
+++ b/components/cast_streaming/browser/test/cast_streaming_test_receiver.cc
@@ -7,6 +7,7 @@
 #include "base/logging.h"
 #include "base/run_loop.h"
 #include "base/threading/sequenced_task_runner_handle.h"
+#include "components/cast_streaming/public/config_conversions.h"
 
 namespace cast_streaming {
 
@@ -16,7 +17,14 @@
 void CastStreamingTestReceiver::Start(
     std::unique_ptr<cast_api_bindings::MessagePort> message_port) {
   VLOG(1) << __func__;
-  receiver_session_.Start(this, std::move(message_port),
+  auto stream_config =
+      std::make_unique<cast_streaming::ReceiverSession::AVConstraints>(
+          ToVideoCaptureConfigCodecs(media::VideoCodec::kCodecH264,
+                                     media::VideoCodec::kCodecVP8),
+          ToAudioCaptureConfigCodecs(media::AudioCodec::kCodecAAC,
+                                     media::AudioCodec::kCodecOpus));
+  receiver_session_.Start(this, std::move(stream_config),
+                          std::move(message_port),
                           base::SequencedTaskRunnerHandle::Get());
 }
 
diff --git a/components/cast_streaming/browser/test/cast_streaming_test_sender.cc b/components/cast_streaming/browser/test/cast_streaming_test_sender.cc
index 2a52881..2e3cfb6 100644
--- a/components/cast_streaming/browser/test/cast_streaming_test_sender.cc
+++ b/components/cast_streaming/browser/test/cast_streaming_test_sender.cc
@@ -10,7 +10,7 @@
 #include "base/logging.h"
 #include "base/run_loop.h"
 #include "base/threading/sequenced_task_runner_handle.h"
-#include "components/cast_streaming/browser/config_conversions.h"
+#include "components/cast_streaming/public/config_conversions.h"
 #include "third_party/openscreen/src/cast/streaming/capture_recommendations.h"
 
 namespace cast_streaming {
@@ -88,14 +88,12 @@
 
   std::vector<openscreen::cast::AudioCaptureConfig> audio_configs;
   if (audio_config) {
-    audio_configs.push_back(
-        AudioDecoderConfigToAudioCaptureConfig(audio_config.value()));
+    audio_configs.push_back(ToAudioCaptureConfig(audio_config.value()));
   }
 
   std::vector<openscreen::cast::VideoCaptureConfig> video_configs;
   if (video_config) {
-    video_configs.push_back(
-        VideoDecoderConfigToVideoCaptureConfig(video_config.value()));
+    video_configs.push_back(ToVideoCaptureConfig(video_config.value()));
   }
 
   openscreen::Error error = sender_session_->Negotiate(
@@ -191,14 +189,12 @@
 
   if (senders.audio_sender) {
     audio_sender_ = senders.audio_sender;
-    audio_decoder_config_ =
-        AudioCaptureConfigToAudioDecoderConfig(senders.audio_config);
+    audio_decoder_config_ = ToAudioDecoderConfig(senders.audio_config);
   }
 
   if (senders.video_sender) {
     video_sender_ = senders.video_sender;
-    video_decoder_config_ =
-        VideoCaptureConfigToVideoDecoderConfig(senders.video_config);
+    video_decoder_config_ = ToVideoDecoderConfig(senders.video_config);
   }
 
   is_active_ = true;
diff --git a/components/cast_streaming/public/BUILD.gn b/components/cast_streaming/public/BUILD.gn
index 46cee4c..b194c7b 100644
--- a/components/cast_streaming/public/BUILD.gn
+++ b/components/cast_streaming/public/BUILD.gn
@@ -5,18 +5,33 @@
 import("//testing/test.gni")
 
 source_set("public") {
-  deps = [ "//url" ]
+  deps = [
+    "//base",
+    "//media",
+    "//ui/gfx/geometry",
+    "//url",
+  ]
+  public_deps = [
+    "//third_party/openscreen/src/cast/streaming:receiver",
+    "//third_party/openscreen/src/cast/streaming:streaming_configs",
+  ]
   sources = [
     "cast_streaming_url.cc",
     "cast_streaming_url.h",
+    "config_conversions.cc",
+    "config_conversions.h",
   ]
 }
 
-# NOTE: This source set is intentionally empty. It is used to force the building
-# of the code defined in this directory, as it is production code which must
-# be validated as part of the CQ.
 source_set("unit_tests") {
   testonly = true
-  deps = [ ":public" ]
-  sources = []
+  deps = [
+    ":public",
+    "//base/test:test_support",
+    "//media:test_support",
+    "//media/mojo:test_support",
+    "//testing/gmock",
+    "//testing/gtest",
+  ]
+  sources = [ "config_conversions_unittest.cc" ]
 }
diff --git a/components/cast_streaming/public/DEPS b/components/cast_streaming/public/DEPS
new file mode 100644
index 0000000..0beb8c9
--- /dev/null
+++ b/components/cast_streaming/public/DEPS
@@ -0,0 +1,6 @@
+include_rules = [
+  "+media/base",
+  "+third_party/openscreen/src/cast",
+  "+ui/gfx/geometry",
+  "+url",
+]
diff --git a/components/cast_streaming/public/config_conversions.cc b/components/cast_streaming/public/config_conversions.cc
new file mode 100644
index 0000000..4822eb2
--- /dev/null
+++ b/components/cast_streaming/public/config_conversions.cc
@@ -0,0 +1,166 @@
+// 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 "components/cast_streaming/public/config_conversions.h"
+
+#include "base/check.h"
+#include "base/notreached.h"
+#include "media/base/media_util.h"
+#include "ui/gfx/geometry/rect.h"
+#include "ui/gfx/geometry/size.h"
+
+namespace cast_streaming {
+namespace {
+
+media::VideoCodecProfile ToVideoDecoderConfigCodecProfile(
+    openscreen::cast::VideoCodec codec) {
+  switch (codec) {
+    // TODO(b/186875732): Determine the values for Hevc and Vp9 experimentally.
+    case openscreen::cast::VideoCodec::kH264:
+      return media::VideoCodecProfile::H264PROFILE_BASELINE;
+    case openscreen::cast::VideoCodec::kHevc:
+      return media::VideoCodecProfile::HEVCPROFILE_MAIN;
+    case openscreen::cast::VideoCodec::kVp8:
+      return media::VideoCodecProfile::VP8PROFILE_MIN;
+    case openscreen::cast::VideoCodec::kVp9:
+      return media::VideoCodecProfile::VP9PROFILE_PROFILE0;
+    case openscreen::cast::VideoCodec::kNotSpecified:
+      break;
+  }
+
+  NOTREACHED();
+  return media::VideoCodecProfile::VIDEO_CODEC_PROFILE_UNKNOWN;
+}
+
+media::AudioCodec ToAudioDecoderConfigCodec(
+    openscreen::cast::AudioCodec codec) {
+  switch (codec) {
+    case openscreen::cast::AudioCodec::kAac:
+      return media::AudioCodec::kCodecAAC;
+    case openscreen::cast::AudioCodec::kOpus:
+      return media::AudioCodec::kCodecOpus;
+    case openscreen::cast::AudioCodec::kNotSpecified:
+      break;
+  }
+
+  NOTREACHED();
+  return media::AudioCodec::kUnknownAudioCodec;
+}
+
+media::VideoCodec ToVideoDecoderConfigCodec(
+    openscreen::cast::VideoCodec codec) {
+  switch (codec) {
+    case openscreen::cast::VideoCodec::kH264:
+      return media::VideoCodec::kCodecH264;
+    case openscreen::cast::VideoCodec::kVp8:
+      return media::VideoCodec::kCodecVP8;
+    case openscreen::cast::VideoCodec::kHevc:
+      return media::VideoCodec::kCodecHEVC;
+    case openscreen::cast::VideoCodec::kVp9:
+      return media::VideoCodec::kCodecVP9;
+    case openscreen::cast::VideoCodec::kNotSpecified:
+      break;
+  }
+
+  NOTREACHED();
+  return media::VideoCodec::kUnknownVideoCodec;
+}
+
+}  // namespace
+
+openscreen::cast::AudioCodec ToAudioCaptureConfigCodec(
+    media::AudioCodec codec) {
+  switch (codec) {
+    case media::AudioCodec::kCodecAAC:
+      return openscreen::cast::AudioCodec::kAac;
+    case media::AudioCodec::kCodecOpus:
+      return openscreen::cast::AudioCodec::kOpus;
+    default:
+      break;
+  }
+
+  NOTREACHED();
+  return openscreen::cast::AudioCodec::kNotSpecified;
+}
+
+openscreen::cast::VideoCodec ToVideoCaptureConfigCodec(
+    media::VideoCodec codec) {
+  switch (codec) {
+    case media::VideoCodec::kCodecH264:
+      return openscreen::cast::VideoCodec::kH264;
+    case media::VideoCodec::kCodecVP8:
+      return openscreen::cast::VideoCodec::kVp8;
+    case media::VideoCodec::kCodecHEVC:
+      return openscreen::cast::VideoCodec::kHevc;
+    case media::VideoCodec::kCodecVP9:
+      return openscreen::cast::VideoCodec::kVp9;
+    default:
+      break;
+  }
+
+  NOTREACHED();
+  return openscreen::cast::VideoCodec::kNotSpecified;
+}
+
+openscreen::cast::AudioCaptureConfig ToAudioCaptureConfig(
+    const media::AudioDecoderConfig& audio_config) {
+  DCHECK(!audio_config.is_encrypted());
+
+  openscreen::cast::AudioCaptureConfig audio_capture_config;
+  audio_capture_config.codec = ToAudioCaptureConfigCodec(audio_config.codec());
+  audio_capture_config.channels =
+      media::ChannelLayoutToChannelCount(audio_config.channel_layout());
+  audio_capture_config.sample_rate = audio_config.samples_per_second();
+  audio_capture_config.bit_rate = 0;  // Selected by the sender.
+
+  return audio_capture_config;
+}
+
+openscreen::cast::VideoCaptureConfig ToVideoCaptureConfig(
+    const media::VideoDecoderConfig& video_config) {
+  DCHECK(!video_config.is_encrypted());
+
+  openscreen::cast::VideoCaptureConfig video_capture_config;
+  video_capture_config.codec = ToVideoCaptureConfigCodec(video_config.codec());
+  video_capture_config.resolutions.push_back(
+      {video_config.visible_rect().width(),
+       video_config.visible_rect().height()});
+  video_capture_config.max_bit_rate = 0;  // Selected by the sender.
+  return video_capture_config;
+}
+
+media::AudioDecoderConfig ToAudioDecoderConfig(
+    const openscreen::cast::AudioCaptureConfig& audio_capture_config) {
+  media::AudioCodec media_audio_codec =
+      ToAudioDecoderConfigCodec(audio_capture_config.codec);
+
+  return media::AudioDecoderConfig(
+      media_audio_codec, media::SampleFormat::kSampleFormatF32,
+      media::GuessChannelLayout(audio_capture_config.channels),
+      audio_capture_config.sample_rate /* samples_per_second */,
+      media::EmptyExtraData(), media::EncryptionScheme::kUnencrypted);
+}
+
+media::VideoDecoderConfig ToVideoDecoderConfig(
+    const openscreen::cast::VideoCaptureConfig& video_capture_config) {
+  // Gather data for the video decoder config.
+  DCHECK(video_capture_config.resolutions.size());
+  uint32_t video_width = video_capture_config.resolutions[0].width;
+  uint32_t video_height = video_capture_config.resolutions[0].height;
+  gfx::Size video_size(video_width, video_height);
+  gfx::Rect video_rect(video_width, video_height);
+
+  media::VideoCodec media_video_codec =
+      ToVideoDecoderConfigCodec(video_capture_config.codec);
+  media::VideoCodecProfile video_codec_profile =
+      ToVideoDecoderConfigCodecProfile(video_capture_config.codec);
+
+  return media::VideoDecoderConfig(
+      media_video_codec, video_codec_profile,
+      media::VideoDecoderConfig::AlphaMode::kIsOpaque, media::VideoColorSpace(),
+      media::VideoTransformation(), video_size, video_rect, video_size,
+      media::EmptyExtraData(), media::EncryptionScheme::kUnencrypted);
+}
+
+}  // namespace cast_streaming
diff --git a/components/cast_streaming/public/config_conversions.h b/components/cast_streaming/public/config_conversions.h
new file mode 100644
index 0000000..61d34a4
--- /dev/null
+++ b/components/cast_streaming/public/config_conversions.h
@@ -0,0 +1,50 @@
+// 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 COMPONENTS_CAST_STREAMING_PUBLIC_CONFIG_CONVERSIONS_H_
+#define COMPONENTS_CAST_STREAMING_PUBLIC_CONFIG_CONVERSIONS_H_
+
+#include <vector>
+
+#include "media/base/audio_decoder_config.h"
+#include "media/base/video_decoder_config.h"
+#include "third_party/openscreen/src/cast/streaming/capture_configs.h"
+
+namespace cast_streaming {
+
+// Utility functions to convert between media and Open Screen types.
+
+openscreen::cast::AudioCaptureConfig ToAudioCaptureConfig(
+    const media::AudioDecoderConfig& audio_config);
+
+openscreen::cast::VideoCaptureConfig ToVideoCaptureConfig(
+    const media::VideoDecoderConfig& video_config);
+
+media::AudioDecoderConfig ToAudioDecoderConfig(
+    const openscreen::cast::AudioCaptureConfig& audio_capture_config);
+
+media::VideoDecoderConfig ToVideoDecoderConfig(
+    const openscreen::cast::VideoCaptureConfig& video_capture_config);
+
+openscreen::cast::AudioCodec ToAudioCaptureConfigCodec(media::AudioCodec codec);
+
+openscreen::cast::VideoCodec ToVideoCaptureConfigCodec(media::VideoCodec codec);
+
+template <typename... TCodecs>
+std::vector<openscreen::cast::AudioCodec> ToAudioCaptureConfigCodecs(
+    TCodecs... codecs) {
+  return std::vector<openscreen::cast::AudioCodec>{
+      ToAudioCaptureConfigCodec(codecs)...};
+}
+
+template <typename... TCodecs>
+std::vector<openscreen::cast::VideoCodec> ToVideoCaptureConfigCodecs(
+    TCodecs... codecs) {
+  return std::vector<openscreen::cast::VideoCodec>{
+      ToVideoCaptureConfigCodec(codecs)...};
+}
+
+}  // namespace cast_streaming
+
+#endif  // COMPONENTS_CAST_STREAMING_PUBLIC_CONFIG_CONVERSIONS_H_
diff --git a/components/cast_streaming/public/config_conversions_unittest.cc b/components/cast_streaming/public/config_conversions_unittest.cc
new file mode 100644
index 0000000..b8fd2faf
--- /dev/null
+++ b/components/cast_streaming/public/config_conversions_unittest.cc
@@ -0,0 +1,241 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/cast_streaming/public/config_conversions.h"
+
+#include "media/base/media_util.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/openscreen/src/cast/streaming/capture_configs.h"
+#include "ui/gfx/geometry/rect.h"
+#include "ui/gfx/geometry/size.h"
+
+namespace cast_streaming {
+namespace {
+
+void ValidateAudioConfig(const media::AudioDecoderConfig& config,
+                         const media::AudioDecoderConfig& expected) {
+  EXPECT_EQ(config.codec(), expected.codec());
+  EXPECT_EQ(config.sample_format(), media::SampleFormat::kSampleFormatF32);
+  EXPECT_EQ(config.channel_layout(), expected.channel_layout());
+  EXPECT_EQ(config.samples_per_second(), expected.samples_per_second());
+  EXPECT_EQ(config.extra_data().size(), size_t{0});
+  EXPECT_FALSE(config.is_encrypted());
+}
+
+void ValidateAudioConfig(const openscreen::cast::AudioCaptureConfig& config,
+                         const openscreen::cast::AudioCaptureConfig& expected) {
+  EXPECT_EQ(config.codec, expected.codec);
+  EXPECT_EQ(config.channels, expected.channels);
+  EXPECT_EQ(config.bit_rate, expected.bit_rate);
+  EXPECT_EQ(config.sample_rate, expected.sample_rate);
+  EXPECT_EQ(config.target_playout_delay, expected.target_playout_delay);
+}
+
+void ValidateVideoConfig(const media::VideoDecoderConfig& config,
+                         const media::VideoDecoderConfig& expected) {
+  EXPECT_EQ(config.codec(), expected.codec());
+  EXPECT_EQ(config.profile(), expected.profile());
+  EXPECT_EQ(config.alpha_mode(),
+            media::VideoDecoderConfig::AlphaMode::kIsOpaque);
+  EXPECT_EQ(config.extra_data().size(), size_t{0});
+  EXPECT_FALSE(config.is_encrypted());
+
+  EXPECT_EQ(config.coded_size().width(), expected.coded_size().width());
+  EXPECT_EQ(config.coded_size().height(), expected.coded_size().height());
+
+  EXPECT_EQ(config.visible_rect().width(), expected.visible_rect().width());
+  EXPECT_EQ(config.visible_rect().height(), expected.visible_rect().height());
+
+  EXPECT_EQ(config.natural_size().width(), expected.natural_size().width());
+  EXPECT_EQ(config.natural_size().height(), expected.natural_size().height());
+}
+
+void ValidateVideoConfig(const openscreen::cast::VideoCaptureConfig& config,
+                         const openscreen::cast::VideoCaptureConfig& expected) {
+  EXPECT_EQ(config.codec, expected.codec);
+  EXPECT_EQ(config.max_frame_rate, expected.max_frame_rate);
+  EXPECT_EQ(config.max_bit_rate, expected.max_bit_rate);
+  EXPECT_EQ(config.target_playout_delay, expected.target_playout_delay);
+  ASSERT_EQ(config.resolutions.size(), expected.resolutions.size());
+  for (const auto& resolution : config.resolutions) {
+    EXPECT_TRUE(std::find(expected.resolutions.begin(),
+                          expected.resolutions.end(),
+                          resolution) != expected.resolutions.end());
+  }
+}
+
+openscreen::cast::AudioCaptureConfig CreateAudioCaptureConfig() {
+  openscreen::cast::AudioCaptureConfig config;
+  config.codec = openscreen::cast::AudioCodec::kAac;
+  config.channels = 2;
+  config.sample_rate = 42;
+  return config;
+}
+
+media::AudioDecoderConfig CreateAudioDecoderConfig(
+    media::AudioCodec codec,
+    media::ChannelLayout channel_layout,
+    int samples_per_second) {
+  return media::AudioDecoderConfig(codec, media::SampleFormat::kSampleFormatF32,
+                                   channel_layout, samples_per_second,
+                                   media::EmptyExtraData(),
+                                   media::EncryptionScheme::kUnencrypted);
+}
+
+openscreen::cast::VideoCaptureConfig CreateVideoCaptureConfig() {
+  openscreen::cast::VideoCaptureConfig config;
+  config.codec = openscreen::cast::VideoCodec::kH264;
+  config.resolutions.push_back({1080, 720});
+  return config;
+}
+
+media::VideoDecoderConfig CreateVideoDecoderConfig(
+    media::VideoCodec codec,
+    media::VideoCodecProfile codec_profile,
+    int width,
+    int height) {
+  gfx::Size video_size(width, height);
+  gfx::Rect video_rect(width, height);
+  return media::VideoDecoderConfig(
+      codec, codec_profile, media::VideoDecoderConfig::AlphaMode::kIsOpaque,
+      media::VideoColorSpace(), media::VideoTransformation(), video_size,
+      video_rect, video_size, media::EmptyExtraData(),
+      media::EncryptionScheme::kUnencrypted);
+}
+
+}  // namespace
+
+TEST(ConfigConversionsTest, AudioConfigCodecConversion) {
+  auto capture_config = CreateAudioCaptureConfig();
+  auto decoder_config =
+      CreateAudioDecoderConfig(media::AudioCodec::kCodecAAC,
+                               media::ChannelLayout::CHANNEL_LAYOUT_STEREO, 42);
+  ValidateAudioConfig(ToAudioDecoderConfig(capture_config), decoder_config);
+  ValidateAudioConfig(ToAudioCaptureConfig(decoder_config), capture_config);
+
+  capture_config.codec = openscreen::cast::AudioCodec::kOpus;
+  decoder_config =
+      CreateAudioDecoderConfig(media::AudioCodec::kCodecOpus,
+                               media::ChannelLayout::CHANNEL_LAYOUT_STEREO, 42);
+  ValidateAudioConfig(ToAudioDecoderConfig(capture_config), decoder_config);
+  ValidateAudioConfig(ToAudioCaptureConfig(decoder_config), capture_config);
+}
+
+TEST(ConfigConversionsTest, AudioConfigChannelsConversion) {
+  auto capture_config = CreateAudioCaptureConfig();
+  auto decoder_config =
+      CreateAudioDecoderConfig(media::AudioCodec::kCodecAAC,
+                               media::ChannelLayout::CHANNEL_LAYOUT_STEREO, 42);
+  ValidateAudioConfig(ToAudioDecoderConfig(capture_config), decoder_config);
+  ValidateAudioConfig(ToAudioCaptureConfig(decoder_config), capture_config);
+
+  capture_config.channels = 1;
+  decoder_config =
+      CreateAudioDecoderConfig(media::AudioCodec::kCodecAAC,
+                               media::ChannelLayout::CHANNEL_LAYOUT_MONO, 42);
+  ValidateAudioConfig(ToAudioDecoderConfig(capture_config), decoder_config);
+  ValidateAudioConfig(ToAudioCaptureConfig(decoder_config), capture_config);
+
+  // Other configurations are not expected in practice.
+}
+
+TEST(ConfigConversionsTest, AudioConfigSampleRateConversion) {
+  auto capture_config = CreateAudioCaptureConfig();
+  auto decoder_config =
+      CreateAudioDecoderConfig(media::AudioCodec::kCodecAAC,
+                               media::ChannelLayout::CHANNEL_LAYOUT_STEREO, 42);
+  ValidateAudioConfig(ToAudioDecoderConfig(capture_config), decoder_config);
+  ValidateAudioConfig(ToAudioCaptureConfig(decoder_config), capture_config);
+
+  capture_config.sample_rate = 1234;
+  decoder_config = CreateAudioDecoderConfig(
+      media::AudioCodec::kCodecAAC, media::ChannelLayout::CHANNEL_LAYOUT_STEREO,
+      1234);
+  ValidateAudioConfig(ToAudioDecoderConfig(capture_config), decoder_config);
+  ValidateAudioConfig(ToAudioCaptureConfig(decoder_config), capture_config);
+
+  capture_config.sample_rate = -1;
+  decoder_config =
+      CreateAudioDecoderConfig(media::AudioCodec::kCodecAAC,
+                               media::ChannelLayout::CHANNEL_LAYOUT_STEREO, -1);
+  ValidateAudioConfig(ToAudioDecoderConfig(capture_config), decoder_config);
+  ValidateAudioConfig(ToAudioCaptureConfig(decoder_config), capture_config);
+
+  capture_config.sample_rate = 0;
+  decoder_config =
+      CreateAudioDecoderConfig(media::AudioCodec::kCodecAAC,
+                               media::ChannelLayout::CHANNEL_LAYOUT_STEREO, 0);
+  ValidateAudioConfig(ToAudioDecoderConfig(capture_config), decoder_config);
+  ValidateAudioConfig(ToAudioCaptureConfig(decoder_config), capture_config);
+}
+
+TEST(ConfigConversionsTest, VideoConfigCodecConversion) {
+  const int width = 1080;
+  const int height = 720;
+  auto capture_config = CreateVideoCaptureConfig();
+  auto decoder_config = CreateVideoDecoderConfig(
+      media::VideoCodec::kCodecH264,
+      media::VideoCodecProfile::H264PROFILE_BASELINE, width, height);
+  ValidateVideoConfig(ToVideoDecoderConfig(capture_config), decoder_config);
+  ValidateVideoConfig(ToVideoCaptureConfig(decoder_config), capture_config);
+
+  capture_config.codec = openscreen::cast::VideoCodec::kVp8;
+  decoder_config = CreateVideoDecoderConfig(
+      media::VideoCodec::kCodecVP8, media::VideoCodecProfile::VP8PROFILE_MIN,
+      width, height);
+  ValidateVideoConfig(ToVideoDecoderConfig(capture_config), decoder_config);
+  ValidateVideoConfig(ToVideoCaptureConfig(decoder_config), capture_config);
+
+  capture_config.codec = openscreen::cast::VideoCodec::kHevc;
+  decoder_config = CreateVideoDecoderConfig(
+      media::VideoCodec::kCodecHEVC, media::VideoCodecProfile::HEVCPROFILE_MAIN,
+      width, height);
+  ValidateVideoConfig(ToVideoDecoderConfig(capture_config), decoder_config);
+  ValidateVideoConfig(ToVideoCaptureConfig(decoder_config), capture_config);
+
+  capture_config.codec = openscreen::cast::VideoCodec::kVp9;
+  decoder_config = CreateVideoDecoderConfig(
+      media::VideoCodec::kCodecVP9,
+      media::VideoCodecProfile::VP9PROFILE_PROFILE0, width, height);
+  ValidateVideoConfig(ToVideoDecoderConfig(capture_config), decoder_config);
+  ValidateVideoConfig(ToVideoCaptureConfig(decoder_config), capture_config);
+}
+
+TEST(ConfigConversionsTest, VideoConfigResolutionConversion) {
+  auto capture_config = CreateVideoCaptureConfig();
+  auto decoder_config = CreateVideoDecoderConfig(
+      media::VideoCodec::kCodecH264,
+      media::VideoCodecProfile::H264PROFILE_BASELINE, 1080, 720);
+  ValidateVideoConfig(ToVideoDecoderConfig(capture_config), decoder_config);
+  ValidateVideoConfig(ToVideoCaptureConfig(decoder_config), capture_config);
+
+  ASSERT_EQ(capture_config.resolutions.size(), size_t{1});
+
+  capture_config.resolutions[0].width = 42;
+  capture_config.resolutions[0].height = 16;
+  decoder_config = CreateVideoDecoderConfig(
+      media::VideoCodec::kCodecH264,
+      media::VideoCodecProfile::H264PROFILE_BASELINE, 42, 16);
+  ValidateVideoConfig(ToVideoDecoderConfig(capture_config), decoder_config);
+  ValidateVideoConfig(ToVideoCaptureConfig(decoder_config), capture_config);
+
+  capture_config.resolutions[0].width = 1;
+  capture_config.resolutions[0].height = 2;
+  decoder_config = CreateVideoDecoderConfig(
+      media::VideoCodec::kCodecH264,
+      media::VideoCodecProfile::H264PROFILE_BASELINE, 1, 2);
+  ValidateVideoConfig(ToVideoDecoderConfig(capture_config), decoder_config);
+  ValidateVideoConfig(ToVideoCaptureConfig(decoder_config), capture_config);
+
+  capture_config.resolutions[0].width = 0;
+  capture_config.resolutions[0].height = 0;
+  decoder_config = CreateVideoDecoderConfig(
+      media::VideoCodec::kCodecH264,
+      media::VideoCodecProfile::H264PROFILE_BASELINE, 0, 0);
+  ValidateVideoConfig(ToVideoDecoderConfig(capture_config), decoder_config);
+  ValidateVideoConfig(ToVideoCaptureConfig(decoder_config), capture_config);
+}
+
+}  // namespace cast_streaming
diff --git a/components/certificate_transparency/chrome_ct_policy_enforcer.cc b/components/certificate_transparency/chrome_ct_policy_enforcer.cc
index a518809..421a7ea 100644
--- a/components/certificate_transparency/chrome_ct_policy_enforcer.cc
+++ b/components/certificate_transparency/chrome_ct_policy_enforcer.cc
@@ -142,8 +142,10 @@
 }
 
 void ChromeCTPolicyEnforcer::UpdateCTLogList(
+    base::Time update_time,
     std::vector<std::pair<std::string, base::TimeDelta>> disqualified_logs,
     std::vector<std::string> operated_by_google_logs) {
+  log_list_date_ = update_time;
   disqualified_logs_ = std::move(disqualified_logs);
   operated_by_google_logs_ = std::move(operated_by_google_logs);
 }
diff --git a/components/certificate_transparency/chrome_ct_policy_enforcer.h b/components/certificate_transparency/chrome_ct_policy_enforcer.h
index 14d021e..922a28a 100644
--- a/components/certificate_transparency/chrome_ct_policy_enforcer.h
+++ b/components/certificate_transparency/chrome_ct_policy_enforcer.h
@@ -47,6 +47,7 @@
   // a map of log ID to disqualification date.  |operated_by_google_logs| is a
   // list of log IDs operated by Google
   void UpdateCTLogList(
+      base::Time update_time,
       std::vector<std::pair<std::string, base::TimeDelta>> disqualified_logs,
       std::vector<std::string> operated_by_google_logs);
 
@@ -95,7 +96,7 @@
 
   // The time at which |disqualified_logs_| and |operated_by_google_logs_| were
   // generated.
-  const base::Time log_list_date_;
+  base::Time log_list_date_;
 };
 
 }  // namespace certificate_transparency
diff --git a/components/certificate_transparency/chrome_ct_policy_enforcer_unittest.cc b/components/certificate_transparency/chrome_ct_policy_enforcer_unittest.cc
index a18eca47..1425e12 100644
--- a/components/certificate_transparency/chrome_ct_policy_enforcer_unittest.cc
+++ b/components/certificate_transparency/chrome_ct_policy_enforcer_unittest.cc
@@ -480,7 +480,7 @@
 
   std::vector<std::pair<std::string, base::TimeDelta>> disqualified_logs;
   std::vector<std::string> operated_by_google_logs;
-  chrome_policy_enforcer->UpdateCTLogList(disqualified_logs,
+  chrome_policy_enforcer->UpdateCTLogList(base::Time::Now(), disqualified_logs,
                                           operated_by_google_logs);
 
   // The check should fail since the Google Aviator log is no longer in the
@@ -492,7 +492,7 @@
   // Update the list again, this time including all the known operated by Google
   // logs.
   operated_by_google_logs = certificate_transparency::GetLogsOperatedByGoogle();
-  chrome_policy_enforcer->UpdateCTLogList(disqualified_logs,
+  chrome_policy_enforcer->UpdateCTLogList(base::Time::Now(), disqualified_logs,
                                           operated_by_google_logs);
 
   // The check should now succeed.
@@ -501,6 +501,37 @@
                                                     NetLogWithSource()));
 }
 
+TEST_F(ChromeCTPolicyEnforcerTest, TimestampUpdates) {
+  ChromeCTPolicyEnforcer* chrome_policy_enforcer =
+      static_cast<ChromeCTPolicyEnforcer*>(policy_enforcer_.get());
+  SCTList scts;
+  FillListWithSCTsOfOrigin(SignedCertificateTimestamp::SCT_FROM_TLS_EXTENSION,
+                           2, &scts);
+
+  // Clear the log list and set the last updated time to more than 10 weeks ago.
+  std::vector<std::pair<std::string, base::TimeDelta>> disqualified_logs;
+  std::vector<std::string> operated_by_google_logs;
+  chrome_policy_enforcer->UpdateCTLogList(
+      base::Time::Now() - base::TimeDelta::FromDays(71), disqualified_logs,
+      operated_by_google_logs);
+
+  // The check should return build not timely even though the Google Aviator log
+  // is no longer in the list, since the last update time is greater than 10
+  // weeks.
+  EXPECT_EQ(CTPolicyCompliance::CT_POLICY_BUILD_NOT_TIMELY,
+            chrome_policy_enforcer->CheckCompliance(chain_.get(), scts,
+                                                    NetLogWithSource()));
+
+  // Update the last update time value again, this time with a recent time.
+  chrome_policy_enforcer->UpdateCTLogList(base::Time::Now(), disqualified_logs,
+                                          operated_by_google_logs);
+
+  // The check should now fail
+  EXPECT_EQ(CTPolicyCompliance::CT_POLICY_NOT_DIVERSE_SCTS,
+            chrome_policy_enforcer->CheckCompliance(chain_.get(), scts,
+                                                    NetLogWithSource()));
+}
+
 }  // namespace
 
 }  // namespace certificate_transparency
diff --git a/components/content_settings/core/browser/content_settings_policy_provider.cc b/components/content_settings/core/browser/content_settings_policy_provider.cc
index f807733..c8f9ab3 100644
--- a/components/content_settings/core/browser/content_settings_policy_provider.cc
+++ b/components/content_settings/core/browser/content_settings_policy_provider.cc
@@ -88,6 +88,10 @@
          CONTENT_SETTING_BLOCK},
         {prefs::kManagedInsecurePrivateNetworkAllowedForUrls,
          ContentSettingsType::INSECURE_PRIVATE_NETWORK, CONTENT_SETTING_ALLOW},
+        {prefs::kManagedJavaScriptJitAllowedForSites,
+         ContentSettingsType::JAVASCRIPT_JIT, CONTENT_SETTING_ALLOW},
+        {prefs::kManagedJavaScriptJitBlockedForSites,
+         ContentSettingsType::JAVASCRIPT_JIT, CONTENT_SETTING_BLOCK},
 };
 
 constexpr const char* kManagedPrefs[] = {
@@ -120,6 +124,8 @@
     prefs::kManagedWebUsbAllowDevicesForUrls,
     prefs::kManagedWebUsbAskForUrls,
     prefs::kManagedWebUsbBlockedForUrls,
+    prefs::kManagedJavaScriptJitAllowedForSites,
+    prefs::kManagedJavaScriptJitBlockedForSites,
 };
 
 // The following preferences are only used to indicate if a default content
@@ -146,6 +152,7 @@
     prefs::kManagedDefaultSerialGuardSetting,
     prefs::kManagedDefaultWebBluetoothGuardSetting,
     prefs::kManagedDefaultWebUsbGuardSetting,
+    prefs::kManagedDefaultJavaScriptJitSetting,
 };
 
 }  // namespace
@@ -193,6 +200,8 @@
         {ContentSettingsType::SENSORS, prefs::kManagedDefaultSensorsSetting},
         {ContentSettingsType::INSECURE_PRIVATE_NETWORK,
          prefs::kManagedDefaultInsecurePrivateNetworkSetting},
+        {ContentSettingsType::JAVASCRIPT_JIT,
+         prefs::kManagedDefaultJavaScriptJitSetting},
 };
 
 // static
diff --git a/components/content_settings/core/browser/content_settings_registry.cc b/components/content_settings/core/browser/content_settings_registry.cc
index 6abf923..692dc9f 100644
--- a/components/content_settings/core/browser/content_settings_registry.cc
+++ b/components/content_settings/core/browser/content_settings_registry.cc
@@ -612,6 +612,16 @@
            ContentSettingsInfo::INHERIT_IF_LESS_PERMISSIVE,
            ContentSettingsInfo::PERSISTENT,
            ContentSettingsInfo::EXCEPTIONS_ON_SECURE_ORIGINS_ONLY);
+
+  Register(ContentSettingsType::JAVASCRIPT_JIT, "javascript-jit",
+           CONTENT_SETTING_ALLOW, WebsiteSettingsInfo::UNSYNCABLE,
+           AllowlistedSchemes(),
+           ValidSettings(CONTENT_SETTING_ALLOW, CONTENT_SETTING_BLOCK),
+           WebsiteSettingsInfo::SINGLE_ORIGIN_ONLY_SCOPE,
+           WebsiteSettingsRegistry::ALL_PLATFORMS,
+           ContentSettingsInfo::INHERIT_IN_INCOGNITO,
+           ContentSettingsInfo::PERSISTENT,
+           ContentSettingsInfo::EXCEPTIONS_ON_SECURE_AND_INSECURE_ORIGINS);
 }
 
 void ContentSettingsRegistry::Register(
diff --git a/components/content_settings/core/common/content_settings.cc b/components/content_settings/core/common/content_settings.cc
index 807a39d..c9f2d85 100644
--- a/components/content_settings/core/common/content_settings.cc
+++ b/components/content_settings/core/common/content_settings.cc
@@ -97,6 +97,7 @@
     {ContentSettingsType::FILE_SYSTEM_ACCESS_CHOOSER_DATA, 76},
     {ContentSettingsType::FEDERATED_IDENTITY_SHARING, 77},
     {ContentSettingsType::FEDERATED_IDENTITY_REQUEST, 78},
+    {ContentSettingsType::JAVASCRIPT_JIT, 79},
 };
 
 }  // namespace
diff --git a/components/content_settings/core/common/content_settings_types.h b/components/content_settings/core/common/content_settings_types.h
index 47d6eff..4bb4ad71 100644
--- a/components/content_settings/core/common/content_settings_types.h
+++ b/components/content_settings/core/common/content_settings_types.h
@@ -261,6 +261,9 @@
   // associated with the relying party's origin.
   FEDERATED_IDENTITY_REQUEST,
 
+  // Whether to use the v8 optimized JIT for running JavaScript on the page.
+  JAVASCRIPT_JIT,
+
   NUM_TYPES,
 };
 
diff --git a/components/content_settings/core/common/pref_names.cc b/components/content_settings/core/common/pref_names.cc
index bd5d9d7..07498435 100644
--- a/components/content_settings/core/common/pref_names.cc
+++ b/components/content_settings/core/common/pref_names.cc
@@ -55,6 +55,8 @@
     "profile.managed_default_content_settings.serial_guard";
 const char kManagedDefaultInsecurePrivateNetworkSetting[] =
     "profile.managed_default_content_settings.insecure_private_network";
+const char kManagedDefaultJavaScriptJitSetting[] =
+    "profile.managed_default_content_settings.javascript_jit";
 
 // Preferences that are exclusively used to store managed
 // content settings patterns.
@@ -114,6 +116,10 @@
     "profile.managed_serial_blocked_for_urls";
 const char kManagedInsecurePrivateNetworkAllowedForUrls[] =
     "profile.managed_insecure_private_network_allowed_for_urls";
+const char kManagedJavaScriptJitAllowedForSites[] =
+    "profile.managed_javascript_jit_allowed_for_sites";
+const char kManagedJavaScriptJitBlockedForSites[] =
+    "profile.managed_javascript_jit_blocked_for_sites";
 
 // Boolean indicating whether the quiet UI is enabled for notification
 // permission requests.
diff --git a/components/content_settings/core/common/pref_names.h b/components/content_settings/core/common/pref_names.h
index 4666870..d54cbc8 100644
--- a/components/content_settings/core/common/pref_names.h
+++ b/components/content_settings/core/common/pref_names.h
@@ -35,6 +35,7 @@
 extern const char kManagedDefaultFileSystemWriteGuardSetting[];
 extern const char kManagedDefaultSerialGuardSetting[];
 extern const char kManagedDefaultInsecurePrivateNetworkSetting[];
+extern const char kManagedDefaultJavaScriptJitSetting[];
 
 extern const char kManagedCookiesAllowedForUrls[];
 extern const char kManagedCookiesBlockedForUrls[];
@@ -65,6 +66,8 @@
 extern const char kManagedSerialAskForUrls[];
 extern const char kManagedSerialBlockedForUrls[];
 extern const char kManagedInsecurePrivateNetworkAllowedForUrls[];
+extern const char kManagedJavaScriptJitAllowedForSites[];
+extern const char kManagedJavaScriptJitBlockedForSites[];
 
 extern const char kEnableQuietNotificationPermissionUi[];
 extern const char kQuietNotificationPermissionUiEnablingMethod[];
diff --git a/components/feed/core/v2/api_test/feed_api_test.cc b/components/feed/core/v2/api_test/feed_api_test.cc
index cb544ae6..e74c724c 100644
--- a/components/feed/core/v2/api_test/feed_api_test.cc
+++ b/components/feed/core/v2/api_test/feed_api_test.cc
@@ -587,9 +587,10 @@
                                 loaded_new_content_from_network,
                                 stored_content_age, std::move(latencies));
 }
-void TestMetricsReporter::OnLoadMoreBegin(SurfaceId surface_id) {
+void TestMetricsReporter::OnLoadMoreBegin(const StreamType& stream_type,
+                                          SurfaceId surface_id) {
   load_more_surface_id = surface_id;
-  MetricsReporter::OnLoadMoreBegin(surface_id);
+  MetricsReporter::OnLoadMoreBegin(stream_type, surface_id);
 }
 void TestMetricsReporter::OnLoadMore(LoadStreamStatus final_status) {
   load_more_status = final_status;
diff --git a/components/feed/core/v2/api_test/feed_api_test.h b/components/feed/core/v2/api_test/feed_api_test.h
index 5443ca7..cc150e4 100644
--- a/components/feed/core/v2/api_test/feed_api_test.h
+++ b/components/feed/core/v2/api_test/feed_api_test.h
@@ -328,7 +328,8 @@
                     bool loaded_new_content_from_network,
                     base::TimeDelta stored_content_age,
                     std::unique_ptr<LoadLatencyTimes> latencies) override;
-  void OnLoadMoreBegin(SurfaceId surface_id) override;
+  void OnLoadMoreBegin(const StreamType& stream_type,
+                       SurfaceId surface_id) override;
   void OnLoadMore(LoadStreamStatus final_status) override;
   void OnBackgroundRefresh(LoadStreamStatus final_status) override;
   void OnClearAll(base::TimeDelta time_since_last_clear) override;
diff --git a/components/feed/core/v2/feed_stream.cc b/components/feed/core/v2/feed_stream.cc
index 58371e35b..bd0079d 100644
--- a/components/feed/core/v2/feed_stream.cc
+++ b/components/feed/core/v2/feed_stream.cc
@@ -328,7 +328,8 @@
 }
 
 void FeedStream::AttachSurface(FeedStreamSurface* surface) {
-  metrics_reporter_->SurfaceOpened(surface->GetSurfaceId());
+  metrics_reporter_->SurfaceOpened(surface->GetStreamType(),
+                                   surface->GetSurfaceId());
   Stream& stream = GetStream(surface->GetStreamType());
   // Skip normal processing when overriding stream data from the internals page.
   if (forced_stream_update_for_debugging_.updated_slices_size() > 0) {
@@ -442,7 +443,8 @@
     return std::move(callback).Run(false);
   }
 
-  metrics_reporter_->OnLoadMoreBegin(surface.GetSurfaceId());
+  metrics_reporter_->OnLoadMoreBegin(surface.GetStreamType(),
+                                     surface.GetSurfaceId());
   stream.surface_updater->SetLoadingMore(true);
 
   // Have at most one in-flight LoadMore() request per stream. Send the result
diff --git a/components/feed/core/v2/metrics_reporter.cc b/components/feed/core/v2/metrics_reporter.cc
index 1db3d822..27116ca 100644
--- a/components/feed/core/v2/metrics_reporter.cc
+++ b/components/feed/core/v2/metrics_reporter.cc
@@ -9,6 +9,7 @@
 #include "base/logging.h"
 #include "base/metrics/histogram_functions.h"
 #include "base/metrics/user_metrics.h"
+#include "base/strings/strcat.h"
 #include "base/threading/thread_task_runner_handle.h"
 #include "base/time/time.h"
 #include "components/feed/core/v2/prefs.h"
@@ -91,6 +92,21 @@
 
 }  // namespace
 
+MetricsReporter::SurfaceWaiting::SurfaceWaiting() = default;
+MetricsReporter::SurfaceWaiting::SurfaceWaiting(
+    const feed::StreamType& stream_type,
+    base::TimeTicks wait_start)
+    : stream_type(stream_type), wait_start(wait_start) {}
+
+MetricsReporter::SurfaceWaiting::~SurfaceWaiting() = default;
+MetricsReporter::SurfaceWaiting::SurfaceWaiting(const SurfaceWaiting&) =
+    default;
+MetricsReporter::SurfaceWaiting::SurfaceWaiting(SurfaceWaiting&&) = default;
+MetricsReporter::SurfaceWaiting& MetricsReporter::SurfaceWaiting::operator=(
+    const SurfaceWaiting&) = default;
+MetricsReporter::SurfaceWaiting& MetricsReporter::SurfaceWaiting::operator=(
+    SurfaceWaiting&&) = default;
+
 MetricsReporter::MetricsReporter(PrefService* profile_prefs)
     : profile_prefs_(profile_prefs) {
   persistent_data_ = prefs::GetPersistentMetricsData(*profile_prefs_);
@@ -238,7 +254,7 @@
 
 void MetricsReporter::OpenAction(const StreamType& stream_type,
                                  int index_in_stream) {
-  CardOpenBegin();
+  CardOpenBegin(stream_type);
   ReportUserActionHistogram(FeedUserActionType::kTappedOnCard);
   base::RecordAction(
       base::UserMetricsAction("ContentSuggestions.Feed.CardAction.Open"));
@@ -253,7 +269,7 @@
 
 void MetricsReporter::OpenInNewTabAction(const StreamType& stream_type,
                                          int index_in_stream) {
-  CardOpenBegin();
+  CardOpenBegin(stream_type);
   ReportUserActionHistogram(FeedUserActionType::kTappedOpenInNewTab);
   base::RecordAction(base::UserMetricsAction(
       "ContentSuggestions.Feed.CardAction.OpenInNewTab"));
@@ -362,9 +378,11 @@
   }
 }
 
-void MetricsReporter::SurfaceOpened(SurfaceId surface_id) {
+void MetricsReporter::SurfaceOpened(const StreamType& stream_type,
+                                    SurfaceId surface_id) {
   ReportPersistentDataIfDayIsDone();
-  surfaces_waiting_for_content_.emplace(surface_id, base::TimeTicks::Now());
+  surfaces_waiting_for_content_.emplace(
+      surface_id, SurfaceWaiting{stream_type, base::TimeTicks::Now()});
   ReportUserActionHistogram(FeedUserActionType::kOpenedFeedSurface);
   base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
       FROM_HERE,
@@ -397,18 +415,15 @@
   auto iter = surfaces_waiting_for_content_.find(surface_id);
   if (iter == surfaces_waiting_for_content_.end())
     return;
-  base::TimeDelta latency = base::TimeTicks::Now() - iter->second;
+  SurfaceWaiting surface_waiting = std::move(iter->second);
   surfaces_waiting_for_content_.erase(iter);
 
-  if (success) {
-    base::UmaHistogramCustomTimes(
-        "ContentSuggestions.Feed.UserJourney.OpenFeed.SuccessDuration", latency,
-        base::TimeDelta::FromMilliseconds(50), kLoadTimeout, 50);
-  } else {
-    base::UmaHistogramCustomTimes(
-        "ContentSuggestions.Feed.UserJourney.OpenFeed.FailureDuration", latency,
-        base::TimeDelta::FromMilliseconds(50), kLoadTimeout, 50);
-  }
+  base::UmaHistogramCustomTimes(
+      base::StrCat({"ContentSuggestions.Feed.UserJourney.OpenFeed",
+                    surface_waiting.stream_type.IsWebFeed() ? ".WebFeed" : "",
+                    success ? ".SuccessDuration" : ".FailureDuration"}),
+      base::TimeTicks::Now() - surface_waiting.wait_start,
+      base::TimeDelta::FromMilliseconds(50), kLoadTimeout, 50);
 }
 
 void MetricsReporter::ReportGetMoreIfNeeded(SurfaceId surface_id,
@@ -416,47 +431,50 @@
   auto iter = surfaces_waiting_for_more_content_.find(surface_id);
   if (iter == surfaces_waiting_for_more_content_.end())
     return;
-  base::TimeDelta latency = base::TimeTicks::Now() - iter->second;
+  SurfaceWaiting surface_waiting = std::move(iter->second);
   surfaces_waiting_for_more_content_.erase(iter);
-  if (success) {
-    base::UmaHistogramCustomTimes(
-        "ContentSuggestions.Feed.UserJourney.GetMore.SuccessDuration", latency,
-        base::TimeDelta::FromMilliseconds(50), kLoadTimeout, 50);
-  } else {
-    base::UmaHistogramCustomTimes(
-        "ContentSuggestions.Feed.UserJourney.GetMore.FailureDuration", latency,
-        base::TimeDelta::FromMilliseconds(50), kLoadTimeout, 50);
-  }
+
+  base::UmaHistogramCustomTimes(
+      base::StrCat({"ContentSuggestions.Feed.UserJourney.GetMore.",
+                    success ? "SuccessDuration" : "FailureDuration"}),
+      base::TimeTicks::Now() - surface_waiting.wait_start,
+      base::TimeDelta::FromMilliseconds(50), kLoadTimeout, 50);
 }
 
-void MetricsReporter::CardOpenBegin() {
+void MetricsReporter::CardOpenBegin(const StreamType& stream_type) {
   ReportCardOpenEndIfNeeded(false);
-  pending_open_ = base::TimeTicks::Now();
+  pending_open_ = {stream_type, base::TimeTicks::Now()};
   base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
       FROM_HERE,
       base::BindOnce(&MetricsReporter::CardOpenTimeout, GetWeakPtr(),
-                     *pending_open_),
+                     pending_open_.wait_start),
       kOpenTimeout);
 }
 
 void MetricsReporter::CardOpenTimeout(base::TimeTicks start_ticks) {
-  if (pending_open_ && start_ticks == *pending_open_)
+  if (pending_open_ && start_ticks == pending_open_.wait_start)
     ReportCardOpenEndIfNeeded(false);
 }
 
 void MetricsReporter::ReportCardOpenEndIfNeeded(bool success) {
   if (!pending_open_)
     return;
-  base::TimeDelta latency = base::TimeTicks::Now() - *pending_open_;
-  pending_open_.reset();
+  base::TimeDelta latency = base::TimeTicks::Now() - pending_open_.wait_start;
+
+  std::string histogram_name =
+      base::StrCat({"ContentSuggestions.Feed.UserJourney.OpenCard",
+                    pending_open_.stream_type.IsWebFeed() ? ".WebFeed" : "",
+                    success ? ".SuccessDuration" : ".Failure"});
+
   if (success) {
-    base::UmaHistogramCustomTimes(
-        "ContentSuggestions.Feed.UserJourney.OpenCard.SuccessDuration", latency,
-        base::TimeDelta::FromMilliseconds(100), kOpenTimeout, 50);
+    base::UmaHistogramCustomTimes(histogram_name, latency,
+                                  base::TimeDelta::FromMilliseconds(100),
+                                  kOpenTimeout, 50);
   } else {
-    base::UmaHistogramBoolean(
-        "ContentSuggestions.Feed.UserJourney.OpenCard.Failure", true);
+    base::UmaHistogramBoolean(histogram_name, true);
   }
+
+  pending_open_ = {};
 }
 
 void MetricsReporter::NetworkRequestComplete(NetworkRequestType type,
@@ -565,10 +583,11 @@
       final_status);
 }
 
-void MetricsReporter::OnLoadMoreBegin(SurfaceId surface_id) {
+void MetricsReporter::OnLoadMoreBegin(const StreamType& stream_type,
+                                      SurfaceId surface_id) {
   ReportGetMoreIfNeeded(surface_id, false);
-  surfaces_waiting_for_more_content_.emplace(surface_id,
-                                             base::TimeTicks::Now());
+  surfaces_waiting_for_more_content_.emplace(
+      surface_id, SurfaceWaiting{stream_type, base::TimeTicks::Now()});
 
   base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
       FROM_HERE,
diff --git a/components/feed/core/v2/metrics_reporter.h b/components/feed/core/v2/metrics_reporter.h
index 3582b30..ca89997bb 100644
--- a/components/feed/core/v2/metrics_reporter.h
+++ b/components/feed/core/v2/metrics_reporter.h
@@ -52,7 +52,7 @@
   void StreamScrollStart();
 
   // Called when the Feed surface is opened and closed.
-  void SurfaceOpened(SurfaceId surface_id);
+  void SurfaceOpened(const StreamType& stream_type, SurfaceId surface_id);
   void SurfaceClosed(SurfaceId surface_id);
 
   // Network metrics.
@@ -68,7 +68,8 @@
                             base::TimeDelta stored_content_age,
                             std::unique_ptr<LoadLatencyTimes> load_latencies);
   virtual void OnBackgroundRefresh(LoadStreamStatus final_status);
-  virtual void OnLoadMoreBegin(SurfaceId surface_id);
+  virtual void OnLoadMoreBegin(const StreamType& stream_type,
+                               SurfaceId surface_id);
   virtual void OnLoadMore(LoadStreamStatus final_status);
   virtual void OnClearAll(base::TimeDelta time_since_last_clear);
   // Called each time the surface receives new content.
@@ -102,12 +103,27 @@
     bool engaged_reported_ = false;
     bool scrolled_reported_ = false;
   };
+  struct SurfaceWaiting {
+    explicit operator bool() const { return !wait_start.is_null(); }
+    SurfaceWaiting();
+    SurfaceWaiting(const feed::StreamType& stream_type,
+                   base::TimeTicks wait_start);
+    ~SurfaceWaiting();
+    SurfaceWaiting(const SurfaceWaiting&);
+    SurfaceWaiting(SurfaceWaiting&&);
+    SurfaceWaiting& operator=(const SurfaceWaiting&);
+    SurfaceWaiting& operator=(SurfaceWaiting&&);
+
+    feed::StreamType stream_type;
+    base::TimeTicks wait_start;
+  };
+
   base::WeakPtr<MetricsReporter> GetWeakPtr() {
     return weak_ptr_factory_.GetWeakPtr();
   }
 
   void ReportPersistentDataIfDayIsDone();
-  void CardOpenBegin();
+  void CardOpenBegin(const StreamType& stream_type);
   void CardOpenTimeout(base::TimeTicks start_ticks);
   void ReportCardOpenEndIfNeeded(bool success);
   void RecordEngagement(const StreamType& stream_type,
@@ -135,17 +151,17 @@
   base::TimeTicks visit_start_time_;
 
   // The time a surface was opened, for surfaces still waiting for content.
-  std::map<SurfaceId, base::TimeTicks> surfaces_waiting_for_content_;
+  std::map<SurfaceId, SurfaceWaiting> surfaces_waiting_for_content_;
   // The time a surface requested more content, for surfaces still waiting for
   // more content.
-  std::map<SurfaceId, base::TimeTicks> surfaces_waiting_for_more_content_;
+  std::map<SurfaceId, SurfaceWaiting> surfaces_waiting_for_more_content_;
 
   // Tracking ContentSuggestions.Feed.UserJourney.OpenCard.*:
   // We assume at most one card is opened at a time. The time the card was
   // tapped is stored here. Upon timeout, another open attempt, or
   // |ChromeStopping()|, the open is considered failed. Otherwise, if the
   // loading the page succeeds, the open is considered successful.
-  absl::optional<base::TimeTicks> pending_open_;
+  SurfaceWaiting pending_open_;
 
   // For tracking time spent in the Feed.
   absl::optional<base::TimeTicks> time_in_feed_start_;
diff --git a/components/feed/core/v2/metrics_reporter_unittest.cc b/components/feed/core/v2/metrics_reporter_unittest.cc
index bd25cd2..e4b93e4b 100644
--- a/components/feed/core/v2/metrics_reporter_unittest.cc
+++ b/components/feed/core/v2/metrics_reporter_unittest.cc
@@ -14,6 +14,7 @@
 #include "components/feed/core/shared_prefs/pref_names.h"
 #include "components/feed/core/v2/public/common_enums.h"
 #include "components/feed/core/v2/public/feed_api.h"
+#include "components/feed/core/v2/public/stream_type.h"
 #include "components/prefs/testing_pref_service.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
@@ -455,7 +456,7 @@
 }
 
 TEST_F(MetricsReporterTest, SurfaceOpened) {
-  reporter_->SurfaceOpened(kSurfaceId);
+  reporter_->SurfaceOpened(kForYouStream, kSurfaceId);
 
   std::map<FeedEngagementType, int> want_empty;
   EXPECT_EQ(want_empty, ReportedEngagementType(kForYouStream));
@@ -464,7 +465,7 @@
 }
 
 TEST_F(MetricsReporterTest, OpenFeedSuccessDuration) {
-  reporter_->SurfaceOpened(kSurfaceId);
+  reporter_->SurfaceOpened(kForYouStream, kSurfaceId);
   task_environment_.FastForwardBy(base::TimeDelta::FromSeconds(9));
   reporter_->FeedViewed(kSurfaceId);
 
@@ -473,8 +474,18 @@
       base::TimeDelta::FromSeconds(9), 1);
 }
 
+TEST_F(MetricsReporterTest, WebFeed_OpenFeedSuccessDuration) {
+  reporter_->SurfaceOpened(kWebFeedStream, kSurfaceId);
+  task_environment_.FastForwardBy(base::TimeDelta::FromSeconds(9));
+  reporter_->FeedViewed(kSurfaceId);
+
+  histogram_.ExpectUniqueTimeSample(
+      "ContentSuggestions.Feed.UserJourney.OpenFeed.WebFeed.SuccessDuration",
+      base::TimeDelta::FromSeconds(9), 1);
+}
+
 TEST_F(MetricsReporterTest, OpenFeedLoadTimeout) {
-  reporter_->SurfaceOpened(kSurfaceId);
+  reporter_->SurfaceOpened(kForYouStream, kSurfaceId);
   task_environment_.FastForwardBy(base::TimeDelta::FromSeconds(16));
 
   histogram_.ExpectUniqueTimeSample(
@@ -484,8 +495,20 @@
       "ContentSuggestions.Feed.UserJourney.OpenFeed.SuccessDuration", 0);
 }
 
+TEST_F(MetricsReporterTest, WebFeed_OpenFeedLoadTimeout) {
+  reporter_->SurfaceOpened(kWebFeedStream, kSurfaceId);
+  task_environment_.FastForwardBy(base::TimeDelta::FromSeconds(16));
+
+  histogram_.ExpectUniqueTimeSample(
+      "ContentSuggestions.Feed.UserJourney.OpenFeed.WebFeed.FailureDuration",
+      base::TimeDelta::FromSeconds(15), 1);
+  histogram_.ExpectTotalCount(
+      "ContentSuggestions.Feed.UserJourney.OpenFeed.WebFeed.SuccessDuration",
+      0);
+}
+
 TEST_F(MetricsReporterTest, OpenFeedCloseBeforeLoad) {
-  reporter_->SurfaceOpened(kSurfaceId);
+  reporter_->SurfaceOpened(kForYouStream, kSurfaceId);
   task_environment_.FastForwardBy(base::TimeDelta::FromSeconds(14));
   reporter_->SurfaceClosed(kSurfaceId);
 
@@ -496,6 +519,19 @@
       "ContentSuggestions.Feed.UserJourney.OpenFeed.SuccessDuration", 0);
 }
 
+TEST_F(MetricsReporterTest, WebFeed_OpenFeedCloseBeforeLoad) {
+  reporter_->SurfaceOpened(kWebFeedStream, kSurfaceId);
+  task_environment_.FastForwardBy(base::TimeDelta::FromSeconds(14));
+  reporter_->SurfaceClosed(kSurfaceId);
+
+  histogram_.ExpectUniqueTimeSample(
+      "ContentSuggestions.Feed.UserJourney.OpenFeed.WebFeed.FailureDuration",
+      base::TimeDelta::FromSeconds(14), 1);
+  histogram_.ExpectTotalCount(
+      "ContentSuggestions.Feed.UserJourney.OpenFeed.WebFeed.SuccessDuration",
+      0);
+}
+
 TEST_F(MetricsReporterTest, OpenCardSuccessDuration) {
   reporter_->OpenAction(kForYouStream, 0);
   task_environment_.FastForwardBy(base::TimeDelta::FromSeconds(19));
@@ -506,6 +542,16 @@
       base::TimeDelta::FromSeconds(19), 1);
 }
 
+TEST_F(MetricsReporterTest, WebFeed_OpenCardSuccessDuration) {
+  reporter_->OpenAction(kWebFeedStream, 0);
+  task_environment_.FastForwardBy(base::TimeDelta::FromSeconds(19));
+  reporter_->PageLoaded();
+
+  histogram_.ExpectUniqueTimeSample(
+      "ContentSuggestions.Feed.UserJourney.OpenCard.WebFeed.SuccessDuration",
+      base::TimeDelta::FromSeconds(19), 1);
+}
+
 TEST_F(MetricsReporterTest, OpenCardTimeout) {
   reporter_->OpenAction(kForYouStream, 0);
   task_environment_.FastForwardBy(base::TimeDelta::FromSeconds(21));
@@ -517,6 +563,18 @@
       "ContentSuggestions.Feed.UserJourney.OpenCard.SuccessDuration", 0);
 }
 
+TEST_F(MetricsReporterTest, WebFeed_OpenCardTimeout) {
+  reporter_->OpenAction(kWebFeedStream, 0);
+  task_environment_.FastForwardBy(base::TimeDelta::FromSeconds(21));
+  reporter_->PageLoaded();
+
+  histogram_.ExpectUniqueSample(
+      "ContentSuggestions.Feed.UserJourney.OpenCard.WebFeed.Failure", 1, 1);
+  histogram_.ExpectTotalCount(
+      "ContentSuggestions.Feed.UserJourney.OpenCard.WebFeed.SuccessDuration",
+      0);
+}
+
 TEST_F(MetricsReporterTest, OpenCardFailureTwiceAndThenSucceed) {
   reporter_->OpenAction(kForYouStream, 0);
   reporter_->OpenAction(kForYouStream, 1);
@@ -529,6 +587,19 @@
       "ContentSuggestions.Feed.UserJourney.OpenCard.SuccessDuration", 1);
 }
 
+TEST_F(MetricsReporterTest, WebFeed_OpenCardFailureTwiceAndThenSucceed) {
+  reporter_->OpenAction(kWebFeedStream, 0);
+  reporter_->OpenAction(kWebFeedStream, 1);
+  reporter_->OpenAction(kWebFeedStream, 2);
+  reporter_->PageLoaded();
+
+  histogram_.ExpectUniqueSample(
+      "ContentSuggestions.Feed.UserJourney.OpenCard.WebFeed.Failure", 1, 2);
+  histogram_.ExpectTotalCount(
+      "ContentSuggestions.Feed.UserJourney.OpenCard.WebFeed.SuccessDuration",
+      1);
+}
+
 TEST_F(MetricsReporterTest, OpenCardCloseChromeFailure) {
   reporter_->OpenAction(kForYouStream, 0);
   reporter_->OnEnterBackground();
diff --git a/components/mirroring/service/fake_network_service.h b/components/mirroring/service/fake_network_service.h
index 5af9804c..b8c9c94 100644
--- a/components/mirroring/service/fake_network_service.h
+++ b/components/mirroring/service/fake_network_service.h
@@ -69,7 +69,7 @@
   DISALLOW_COPY_AND_ASSIGN(MockUdpSocket);
 };
 
-class MockNetworkContext final : public network::TestNetworkContext {
+class MockNetworkContext : public network::TestNetworkContext {
  public:
   explicit MockNetworkContext(
       mojo::PendingReceiver<network::mojom::NetworkContext> receiver);
diff --git a/components/mirroring/service/fake_video_capture_host.h b/components/mirroring/service/fake_video_capture_host.h
index bc4f36f..7a5bd6d 100644
--- a/components/mirroring/service/fake_video_capture_host.h
+++ b/components/mirroring/service/fake_video_capture_host.h
@@ -16,7 +16,7 @@
 
 namespace mirroring {
 
-class FakeVideoCaptureHost final : public media::mojom::VideoCaptureHost {
+class FakeVideoCaptureHost : public media::mojom::VideoCaptureHost {
  public:
   explicit FakeVideoCaptureHost(
       mojo::PendingReceiver<media::mojom::VideoCaptureHost> receiver);
diff --git a/components/mirroring/service/session_unittest.cc b/components/mirroring/service/session_unittest.cc
index 0e547f2..d8ed259 100644
--- a/components/mirroring/service/session_unittest.cc
+++ b/components/mirroring/service/session_unittest.cc
@@ -4,6 +4,11 @@
 
 #include "components/mirroring/service/session.h"
 
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
 #include "base/bind.h"
 #include "base/callback.h"
 #include "base/json/json_reader.h"
@@ -29,18 +34,19 @@
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/openscreen/src/cast/streaming/ssrc.h"
 
-using ::testing::InvokeWithoutArgs;
-using ::testing::_;
-using ::testing::AtLeast;
-using ::testing::Mock;
 using media::cast::FrameSenderConfig;
 using media::cast::Packet;
-using media::mojom::RemotingStopReason;
-using media::mojom::RemotingStartFailReason;
 using media::mojom::RemotingSinkMetadata;
 using media::mojom::RemotingSinkMetadataPtr;
-using mirroring::mojom::SessionType;
+using media::mojom::RemotingStartFailReason;
+using media::mojom::RemotingStopReason;
 using mirroring::mojom::SessionError;
+using mirroring::mojom::SessionType;
+using ::testing::_;
+using ::testing::AtLeast;
+using ::testing::InvokeWithoutArgs;
+using ::testing::Mock;
+using ::testing::NiceMock;
 
 namespace mirroring {
 
@@ -67,7 +73,7 @@
     },
 };
 
-class MockRemotingSource final : public media::mojom::RemotingSource {
+class MockRemotingSource : public media::mojom::RemotingSource {
  public:
   MockRemotingSource() {}
   ~MockRemotingSource() override {}
@@ -97,7 +103,10 @@
                     public mojom::CastMessageChannel,
                     public ::testing::Test {
  public:
-  SessionTest() : receiver_endpoint_(media::cast::test::GetFreeLocalPort()) {}
+  SessionTest() = default;
+
+  SessionTest(const SessionTest&) = delete;
+  SessionTest& operator=(const SessionTest&) = delete;
 
   ~SessionTest() override { task_environment_.RunUntilIdle(); }
 
@@ -148,14 +157,15 @@
   void BindGpu(mojo::PendingReceiver<viz::mojom::Gpu> receiver) override {}
   void GetVideoCaptureHost(
       mojo::PendingReceiver<media::mojom::VideoCaptureHost> receiver) override {
-    video_host_ = std::make_unique<FakeVideoCaptureHost>(std::move(receiver));
+    video_host_ =
+        std::make_unique<NiceMock<FakeVideoCaptureHost>>(std::move(receiver));
     OnGetVideoCaptureHost();
   }
 
   void GetNetworkContext(
       mojo::PendingReceiver<network::mojom::NetworkContext> receiver) override {
     network_context_ =
-        std::make_unique<MockNetworkContext>(std::move(receiver));
+        std::make_unique<NiceMock<MockNetworkContext>>(std::move(receiver));
     OnGetNetworkContext();
   }
 
@@ -260,7 +270,7 @@
 
   // Starts the mirroring session.
   void StartSession() {
-    ASSERT_TRUE(cast_mode_ == "mirroring");
+    ASSERT_EQ(cast_mode_, "mirroring");
     // Except mirroing session starts after receiving ANSWER message.
     const int num_to_get_video_host =
         session_type_ == SessionType::AUDIO_ONLY ? 0 : 1;
@@ -286,7 +296,7 @@
   }
 
   void CaptureOneVideoFrame() {
-    ASSERT_TRUE(cast_mode_ == "mirroring");
+    ASSERT_EQ(cast_mode_, "mirroring");
     ASSERT_TRUE(video_host_);
     // Expect to send out some UDP packets.
     EXPECT_CALL(*network_context_->udp_socket(), OnSend()).Times(AtLeast(1));
@@ -352,7 +362,7 @@
   }
 
   void RemotingStarted() {
-    ASSERT_TRUE(cast_mode_ == "remoting");
+    ASSERT_EQ(cast_mode_, "remoting");
     EXPECT_CALL(remoting_source_, OnStarted()).Times(1);
     SendAnswer();
     task_environment_.RunUntilIdle();
@@ -361,7 +371,7 @@
   }
 
   void StopRemoting() {
-    ASSERT_TRUE(cast_mode_ == "remoting");
+    ASSERT_EQ(cast_mode_, "remoting");
     const RemotingStopReason reason = RemotingStopReason::LOCAL_PLAYBACK;
     // Expect to send OFFER message to fallback on mirroring.
     EXPECT_CALL(*this, OnOutboundMessage("OFFER")).Times(1);
@@ -383,14 +393,15 @@
 
  private:
   base::test::TaskEnvironment task_environment_;
-  const net::IPEndPoint receiver_endpoint_;
+  const net::IPEndPoint receiver_endpoint_ =
+      media::cast::test::GetFreeLocalPort();
   mojo::Receiver<mojom::ResourceProvider> resource_provider_receiver_{this};
   mojo::Receiver<mojom::SessionObserver> session_observer_receiver_{this};
   mojo::Receiver<mojom::CastMessageChannel> outbound_channel_receiver_{this};
   mojo::Remote<mojom::CastMessageChannel> inbound_channel_;
   SessionType session_type_ = SessionType::AUDIO_AND_VIDEO;
   mojo::Remote<media::mojom::Remoter> remoter_;
-  MockRemotingSource remoting_source_;
+  NiceMock<MockRemotingSource> remoting_source_;
   std::string cast_mode_;
   int32_t offer_sequence_number_ = -1;
   int32_t capability_sequence_number_ = -1;
@@ -400,7 +411,6 @@
   std::unique_ptr<FakeVideoCaptureHost> video_host_;
   std::unique_ptr<MockNetworkContext> network_context_;
   std::unique_ptr<openscreen::cast::Answer> answer_;
-  DISALLOW_COPY_AND_ASSIGN(SessionTest);
 };
 
 TEST_F(SessionTest, AudioOnlyMirroring) {
diff --git a/components/page_info/page_info.cc b/components/page_info/page_info.cc
index 9eaadaf..875820b 100644
--- a/components/page_info/page_info.cc
+++ b/components/page_info/page_info.cc
@@ -307,6 +307,18 @@
   DCHECK(delegate_);
   security_level_ = delegate_->GetSecurityLevel();
   visible_security_state_for_metrics_ = delegate_->GetVisibleSecurityState();
+
+  // TabSpecificContentSetting needs to be created before page load.
+  DCHECK(GetPageSpecificContentSettings());
+  ComputeUIInputs(site_url_);
+
+  // Every time this is created, page info dialog is opened.
+  // So this counts how often the page Info dialog is opened.
+  RecordPageInfoAction(PAGE_INFO_OPENED);
+
+  // Record the time when the page info dialog is opened so the total time it is
+  // open can be measured.
+  start_time_ = base::TimeTicks::Now();
 }
 
 PageInfo::~PageInfo() {
@@ -393,22 +405,11 @@
 void PageInfo::InitializeUiState(PageInfoUI* ui) {
   ui_ = ui;
   DCHECK(ui_);
-  // TabSpecificContentSetting needs to be created before page load.
-  DCHECK(GetPageSpecificContentSettings());
 
-  ComputeUIInputs(site_url_);
   PresentSitePermissions();
   PresentSiteIdentity();
   PresentSiteData();
   PresentPageFeatureInfo();
-
-  // Every time the Page Info UI is opened, this method is called.
-  // So this counts how often the Page Info UI is opened.
-  RecordPageInfoAction(PAGE_INFO_OPENED);
-
-  // Record the time when the Page Info UI is opened so the total time it is
-  // open can be measured.
-  start_time_ = base::TimeTicks::Now();
 }
 
 void PageInfo::UpdateSecurityState() {
diff --git a/components/page_info/page_info.h b/components/page_info/page_info.h
index dd35e5a..00eed8a 100644
--- a/components/page_info/page_info.h
+++ b/components/page_info/page_info.h
@@ -176,7 +176,10 @@
   };
 
   // Creates a PageInfo for the passed |url| using the given |ssl| status
-  // object to determine the status of the site's connection.
+  // object to determine the status of the site's connection. Computes the UI
+  // inputs and records page info opened action. It is assumed that this is
+  // created when page info dialog is opened and destroyed when the dialog is
+  // closed.
   PageInfo(std::unique_ptr<PageInfoDelegate> delegate,
            content::WebContents* web_contents,
            const GURL& url);
@@ -195,13 +198,8 @@
   // Returns whether this page info is for an internal page.
   static bool IsFileOrInternalPage(const GURL& url);
 
-  // Initializes UI state that is dependent on having access to the PageInfoUI
-  // object associated with this object. This explicit post-construction
-  // initialization step is necessary as PageInfoUI subclasses create this
-  // object and also may invoke it as part of the initialization flow that
-  // occurs in this method. If this initialization flow was done as part of
-  // PageInfo's constructor, those subclasses would not have their PageInfo
-  // member set and crashes would ensue.
+  // Initializes the current UI and calls present data methods on it to notify
+  // the current UI about the data it is subscribed to.
   void InitializeUiState(PageInfoUI* ui);
 
   // This method is called to update the presenter's security state and forwards
diff --git a/components/page_load_metrics/browser/layout_shift_normalization.cc b/components/page_load_metrics/browser/layout_shift_normalization.cc
index d6df884..08a74c9b 100644
--- a/components/page_load_metrics/browser/layout_shift_normalization.cc
+++ b/components/page_load_metrics/browser/layout_shift_normalization.cc
@@ -12,16 +12,6 @@
 LayoutShiftNormalization::LayoutShiftNormalization() = default;
 LayoutShiftNormalization::~LayoutShiftNormalization() = default;
 
-void LayoutShiftNormalization::AddInputTimeStamps(
-    const std::vector<base::TimeTicks>& input_timestamps) {
-  recent_input_timestamps_.insert(recent_input_timestamps_.end(),
-                                  input_timestamps.begin(),
-                                  input_timestamps.end());
-  std::inplace_merge(recent_input_timestamps_.begin(),
-                     recent_input_timestamps_.end() - input_timestamps.size(),
-                     recent_input_timestamps_.end());
-}
-
 void LayoutShiftNormalization::AddNewLayoutShifts(
     const std::vector<page_load_metrics::mojom::LayoutShiftPtr>& new_shifts,
     base::TimeTicks current_time,
@@ -74,7 +64,6 @@
 void LayoutShiftNormalization::ClearAllLayoutShifts() {
   normalized_cls_data_ = NormalizedCLSData();
   recent_layout_shifts_.clear();
-  recent_input_timestamps_.clear();
   sliding_300ms_.clear();
   sliding_1000ms_.clear();
   session_gap1000ms_max5000ms_ = SessionWindow();
@@ -114,20 +103,14 @@
     base::TimeDelta max_duration,
     std::vector<std::pair<base::TimeTicks, double>>::const_iterator begin,
     std::vector<std::pair<base::TimeTicks, double>>::const_iterator end,
-    std::vector<base::TimeTicks>& input_timestamps,
     float& max_score,
     uint32_t& count) {
   for (auto it = begin; it != end; ++it) {
     if ((it->first - session_window->last_time > gap) ||
-        (it->first - session_window->start_time > max_duration) ||
-        (!input_timestamps.empty() && input_timestamps.front() <= it->first)) {
+        (it->first - session_window->start_time > max_duration)) {
       session_window->start_time = it->first;
       session_window->layout_shift_score = 0;
       ++count;
-      while (!input_timestamps.empty() &&
-             input_timestamps.front() <= it->first) {
-        input_timestamps.erase(input_timestamps.begin());
-      }
     }
     session_window->last_time = it->first;
     session_window->layout_shift_score += it->second;
@@ -159,7 +142,6 @@
     float cumulative_layout_shift_score) {
   float dummy_max = 0.0;
   uint32_t dummy_count = 0;
-  std::vector<base::TimeTicks> dummy_input_timestamps;
   // Update Sliding Windows.
   UpdateSlidingWindow(
       &sliding_300ms_, base::TimeDelta::FromMilliseconds(300), current_time,
@@ -176,34 +158,22 @@
   UpdateSessionWindow(
       &session_gap1000ms_max5000ms_, base::TimeDelta::FromMilliseconds(1000),
       base::TimeDelta::FromMilliseconds(5000), first, first_non_stale,
-      dummy_input_timestamps,
       normalized_cls_data_.session_windows_gap1000ms_max5000ms_max_cls,
       dummy_count);
   UpdateSessionWindow(
       &session_gap1000ms_, base::TimeDelta::FromMilliseconds(1000),
-      base::TimeDelta::Max(), first, first_non_stale, dummy_input_timestamps,
+      base::TimeDelta::Max(), first, first_non_stale,
       normalized_cls_data_.session_windows_gap1000ms_maxMax_max_cls,
       dummy_count);
-  UpdateSessionWindow(&session_by_inputs_gap1000ms_max5000ms_,
-                      base::TimeDelta::FromMilliseconds(1000),
-                      base::TimeDelta::FromMilliseconds(5000), first,
-                      first_non_stale, recent_input_timestamps_,
-                      potential_max_cls_session_by_inputs_gap1000ms_max5000ms_,
-                      dummy_count);
-  normalized_cls_data_.session_windows_by_inputs_gap1000ms_max5000ms_max_cls =
-      potential_max_cls_session_by_inputs_gap1000ms_max5000ms_;
-  UpdateSessionWindow(
-      &session_gap5000ms_, base::TimeDelta::FromMilliseconds(5000),
-      base::TimeDelta::Max(), first, first_non_stale, dummy_input_timestamps,
-      dummy_max, session_gap5000ms_count_);
+  UpdateSessionWindow(&session_gap5000ms_,
+                      base::TimeDelta::FromMilliseconds(5000),
+                      base::TimeDelta::Max(), first, first_non_stale, dummy_max,
+                      session_gap5000ms_count_);
 
   auto tmp_session_gap1000ms_max5000ms = session_gap1000ms_max5000ms_;
   auto tmp_session_gap1000ms_ = session_gap1000ms_;
   auto tmp_session_gap5000ms_ = session_gap5000ms_;
   auto tmp_session_gap5000ms_count_ = session_gap5000ms_count_;
-  auto tmp_session_by_inputs_gap1000ms_max5000ms_ =
-      session_by_inputs_gap1000ms_max5000ms_;
-  auto tmp_recent_input_timestamps_ = recent_input_timestamps_;
 
   UpdateSlidingWindow(
       &tmp_sliding_300ms, base::TimeDelta::FromMilliseconds(300), current_time,
@@ -216,26 +186,17 @@
   UpdateSessionWindow(
       &tmp_session_gap1000ms_max5000ms, base::TimeDelta::FromMilliseconds(1000),
       base::TimeDelta::FromMilliseconds(5000), first_non_stale, last,
-      dummy_input_timestamps,
       normalized_cls_data_.session_windows_gap1000ms_max5000ms_max_cls,
       dummy_count);
   UpdateSessionWindow(
       &tmp_session_gap1000ms_, base::TimeDelta::FromMilliseconds(1000),
-      base::TimeDelta::Max(), first_non_stale, last, dummy_input_timestamps,
+      base::TimeDelta::Max(), first_non_stale, last,
       normalized_cls_data_.session_windows_gap1000ms_maxMax_max_cls,
       dummy_count);
-  UpdateSessionWindow(
-      &tmp_session_by_inputs_gap1000ms_max5000ms_,
-      base::TimeDelta::FromMilliseconds(1000),
-      base::TimeDelta::FromMilliseconds(5000), first_non_stale, last,
-      tmp_recent_input_timestamps_,
-      normalized_cls_data_
-          .session_windows_by_inputs_gap1000ms_max5000ms_max_cls,
-      dummy_count);
-  UpdateSessionWindow(
-      &tmp_session_gap5000ms_, base::TimeDelta::FromMilliseconds(5000),
-      base::TimeDelta::Max(), first_non_stale, last, dummy_input_timestamps,
-      dummy_max, tmp_session_gap5000ms_count_);
+  UpdateSessionWindow(&tmp_session_gap5000ms_,
+                      base::TimeDelta::FromMilliseconds(5000),
+                      base::TimeDelta::Max(), first_non_stale, last, dummy_max,
+                      tmp_session_gap5000ms_count_);
   normalized_cls_data_.session_windows_gap5000ms_maxMax_average_cls =
       cumulative_layout_shift_score / tmp_session_gap5000ms_count_;
 }
diff --git a/components/page_load_metrics/browser/layout_shift_normalization.h b/components/page_load_metrics/browser/layout_shift_normalization.h
index 4f70683..0db2b79 100644
--- a/components/page_load_metrics/browser/layout_shift_normalization.h
+++ b/components/page_load_metrics/browser/layout_shift_normalization.h
@@ -22,7 +22,6 @@
     return normalized_cls_data_;
   }
 
-  void AddInputTimeStamps(const std::vector<base::TimeTicks>& input_timestamps);
   void AddNewLayoutShifts(
       const std::vector<page_load_metrics::mojom::LayoutShiftPtr>& new_shifts,
       base::TimeTicks current_time,
@@ -64,7 +63,6 @@
       base::TimeDelta max_duration,
       std::vector<std::pair<base::TimeTicks, double>>::const_iterator begin,
       std::vector<std::pair<base::TimeTicks, double>>::const_iterator end,
-      std::vector<base::TimeTicks>& input_timestamps,
       float& max_score,
       uint32_t& count);
 
@@ -74,20 +72,12 @@
   // This vector is maintained in sorted order.
   std::vector<std::pair<base::TimeTicks, double>> recent_layout_shifts_;
 
-  // This vector which contains input timestamps is maintained in sorted order.
-  std::vector<base::TimeTicks> recent_input_timestamps_;
-
   // Sliding window vectors are maintained in sorted order.
   std::vector<SlidingWindow> sliding_300ms_;
   std::vector<SlidingWindow> sliding_1000ms_;
   SessionWindow session_gap1000ms_max5000ms_;
   SessionWindow session_gap1000ms_;
   SessionWindow session_gap5000ms_;
-  SessionWindow session_by_inputs_gap1000ms_max5000ms_;
-  // A new input in non-stale data can split the session window and make the
-  // max_cls smaller. We need to store the "max_cls" calculated by the stale
-  // data.
-  float potential_max_cls_session_by_inputs_gap1000ms_max5000ms_ = 0.0;
   uint32_t session_gap5000ms_count_ = 0;
 
   DISALLOW_COPY_AND_ASSIGN(LayoutShiftNormalization);
diff --git a/components/page_load_metrics/browser/layout_shift_normalization_unittest.cc b/components/page_load_metrics/browser/layout_shift_normalization_unittest.cc
index 53617ff..c81474a 100644
--- a/components/page_load_metrics/browser/layout_shift_normalization_unittest.cc
+++ b/components/page_load_metrics/browser/layout_shift_normalization_unittest.cc
@@ -20,11 +20,6 @@
         new_shifts, current_time, cumulative_layoutshift_score_);
   }
 
-  void AddInputTimeStamps(
-      const std::vector<base::TimeTicks>& input_timestamps) {
-    layout_shift_normalization_.AddInputTimeStamps(input_timestamps);
-  }
-
   const page_load_metrics::NormalizedCLSData& normalized_cls_data() const {
     return layout_shift_normalization_.normalized_cls_data();
   }
@@ -150,29 +145,3 @@
             3.5);
   EXPECT_EQ(normalized_cls_data().data_tainted, false);
 }
-
-TEST_F(LayoutShiftNormalizationTest, SessionWindowByInputs) {
-  base::TimeTicks time_origin = base::TimeTicks::Now();
-  std::vector<page_load_metrics::mojom::LayoutShiftPtr> new_shifts;
-  // Insert new layout shifts. The insertion order matters.
-  InsertNewLayoutShifts(new_shifts, time_origin,
-                        {{2100, 1.5}, {1800, 1.5}, {1300, 1.5}, {1000, 1.5}});
-  // Update CLS normalization data.
-  AddInputTimeStamps({time_origin - base::TimeDelta::FromMilliseconds(2200),
-                      time_origin - base::TimeDelta::FromMilliseconds(1100),
-                      time_origin - base::TimeDelta::FromMilliseconds(800)});
-  AddNewLayoutShifts(new_shifts, time_origin);
-
-  EXPECT_EQ(normalized_cls_data().sliding_windows_duration300ms_max_cls, 3.0);
-  EXPECT_EQ(normalized_cls_data().sliding_windows_duration1000ms_max_cls, 4.5);
-  EXPECT_EQ(normalized_cls_data().session_windows_gap1000ms_max5000ms_max_cls,
-            6.0);
-  EXPECT_EQ(normalized_cls_data().session_windows_gap1000ms_maxMax_max_cls,
-            6.0);
-  EXPECT_EQ(normalized_cls_data().session_windows_gap5000ms_maxMax_average_cls,
-            6.0);
-  EXPECT_EQ(normalized_cls_data()
-                .session_windows_by_inputs_gap1000ms_max5000ms_max_cls,
-            4.5);
-  EXPECT_EQ(normalized_cls_data().data_tainted, false);
-}
\ No newline at end of file
diff --git a/components/page_load_metrics/browser/page_load_metrics_observer.h b/components/page_load_metrics/browser/page_load_metrics_observer.h
index f730ab2..5afcd18 100644
--- a/components/page_load_metrics/browser/page_load_metrics_observer.h
+++ b/components/page_load_metrics/browser/page_load_metrics_observer.h
@@ -161,11 +161,6 @@
   // is not bigger than 5000ms.
   float session_windows_gap5000ms_maxMax_average_cls = 0.0;
 
-  // Maximum CLS of session windows. The gap between two consecutive shifts is
-  // not bigger than 1000ms or segmented by a user input. The maximum window
-  // size is 5000ms.
-  float session_windows_by_inputs_gap1000ms_max5000ms_max_cls = 0.0;
-
   // If true, will not report the data in UKM.
   bool data_tainted = false;
 };
diff --git a/components/page_load_metrics/common/page_load_metrics.mojom b/components/page_load_metrics/common/page_load_metrics.mojom
index bd509b6..121c700 100644
--- a/components/page_load_metrics/common/page_load_metrics.mojom
+++ b/components/page_load_metrics/common/page_load_metrics.mojom
@@ -299,9 +299,6 @@
 
   // New layout shifts with timestamps.
   array<LayoutShift> new_layout_shifts;
-
-  // Recent input timestamps for layout shift tracking.
-  array<mojo_base.mojom.TimeTicks> input_timestamps;
 };
 
 // Metrics about the time spent in tasks (cpu time) by a frame.
diff --git a/components/page_load_metrics/renderer/metrics_render_frame_observer.cc b/components/page_load_metrics/renderer/metrics_render_frame_observer.cc
index 5344887..248f1f9 100644
--- a/components/page_load_metrics/renderer/metrics_render_frame_observer.cc
+++ b/components/page_load_metrics/renderer/metrics_render_frame_observer.cc
@@ -139,14 +139,6 @@
                                                        after_input_or_scroll);
 }
 
-void MetricsRenderFrameObserver::DidObserveInputForLayoutShiftTracking(
-    base::TimeTicks timestamp) {
-  if (page_timing_metrics_sender_) {
-    page_timing_metrics_sender_->DidObserveInputForLayoutShiftTracking(
-        timestamp);
-  }
-}
-
 void MetricsRenderFrameObserver::DidObserveLayoutNg(
     uint32_t all_block_count,
     uint32_t ng_block_count,
diff --git a/components/page_load_metrics/renderer/metrics_render_frame_observer.h b/components/page_load_metrics/renderer/metrics_render_frame_observer.h
index ed2bf9a..18a8e9c 100644
--- a/components/page_load_metrics/renderer/metrics_render_frame_observer.h
+++ b/components/page_load_metrics/renderer/metrics_render_frame_observer.h
@@ -50,8 +50,6 @@
   void DidObserveNewFeatureUsage(
       const blink::UseCounterFeature& feature) override;
   void DidObserveLayoutShift(double score, bool after_input_or_scroll) override;
-  void DidObserveInputForLayoutShiftTracking(
-      base::TimeTicks timestamp) override;
   void DidObserveLayoutNg(uint32_t all_block_count,
                           uint32_t ng_block_count,
                           uint32_t all_call_count,
diff --git a/components/page_load_metrics/renderer/page_timing_metrics_sender.cc b/components/page_load_metrics/renderer/page_timing_metrics_sender.cc
index a660f73..2a6c57b3 100644
--- a/components/page_load_metrics/renderer/page_timing_metrics_sender.cc
+++ b/components/page_load_metrics/renderer/page_timing_metrics_sender.cc
@@ -30,7 +30,6 @@
 namespace {
 const int kInitialTimerDelayMillis = 50;
 const int64_t kInputDelayAdjustmentMillis = int64_t(50);
-constexpr auto MAX_INPUT_TIMESTAMPS_SIZE = 300;
 }  // namespace
 
 PageTimingMetricsSender::PageTimingMetricsSender(
@@ -97,13 +96,6 @@
   EnsureSendTimer();
 }
 
-void PageTimingMetricsSender::DidObserveInputForLayoutShiftTracking(
-    base::TimeTicks timestamp) {
-  if (render_data_.input_timestamps.size() < MAX_INPUT_TIMESTAMPS_SIZE)
-    render_data_.input_timestamps.push_back(timestamp);
-  EnsureSendTimer();
-}
-
 void PageTimingMetricsSender::DidObserveLayoutNg(
     uint32_t all_block_count,
     uint32_t ng_block_count,
@@ -332,8 +324,6 @@
       page_resource_data_use_.erase(resource->resource_id());
     }
   }
-  std::sort(render_data_.input_timestamps.begin(),
-            render_data_.input_timestamps.end());
   sender_->SendTiming(last_timing_, metadata_, std::move(new_features_),
                       std::move(resources), render_data_, last_cpu_timing_,
                       std::move(new_deferred_resource_data_),
@@ -345,7 +335,6 @@
   last_cpu_timing_->task_time = base::TimeDelta();
   modified_resources_.clear();
   render_data_.new_layout_shifts.clear();
-  render_data_.input_timestamps.clear();
   render_data_.layout_shift_delta = 0;
   render_data_.layout_shift_delta_before_input_or_scroll = 0;
   render_data_.all_layout_block_count_delta = 0;
diff --git a/components/page_load_metrics/renderer/page_timing_metrics_sender.h b/components/page_load_metrics/renderer/page_timing_metrics_sender.h
index 826c5130..fc82053 100644
--- a/components/page_load_metrics/renderer/page_timing_metrics_sender.h
+++ b/components/page_load_metrics/renderer/page_timing_metrics_sender.h
@@ -49,7 +49,6 @@
   void DidObserveLoadingBehavior(blink::LoadingBehaviorFlag behavior);
   void DidObserveNewFeatureUsage(const blink::UseCounterFeature& feature);
   void DidObserveLayoutShift(double score, bool after_input_or_scroll);
-  void DidObserveInputForLayoutShiftTracking(base::TimeTicks timestamp);
   void DidObserveLayoutNg(uint32_t all_block_count,
                           uint32_t ng_block_count,
                           uint32_t all_call_count,
diff --git a/components/page_load_metrics/renderer/page_timing_metrics_sender_unittest.cc b/components/page_load_metrics/renderer/page_timing_metrics_sender_unittest.cc
index 672022d..0acd6ac 100644
--- a/components/page_load_metrics/renderer/page_timing_metrics_sender_unittest.cc
+++ b/components/page_load_metrics/renderer/page_timing_metrics_sender_unittest.cc
@@ -311,8 +311,7 @@
   metrics_sender_->DidObserveLayoutNg(2, 0, 7, 5, 13, 15);
   metrics_sender_->DidObserveLayoutShift(0.5, true);
 
-  mojom::FrameRenderDataUpdate render_data(1.5, 1.0, 5, 2, 17, 9, 21, 26, {},
-                                           {});
+  mojom::FrameRenderDataUpdate render_data(1.5, 1.0, 5, 2, 17, 9, 21, 26, {});
   validator_.UpdateExpectFrameRenderDataUpdate(render_data);
 
   metrics_sender_->mock_timer()->Fire();
diff --git a/components/pdf/browser/pdf_web_contents_helper.cc b/components/pdf/browser/pdf_web_contents_helper.cc
index b2f6605..6026e52 100644
--- a/components/pdf/browser/pdf_web_contents_helper.cc
+++ b/components/pdf/browser/pdf_web_contents_helper.cc
@@ -260,13 +260,16 @@
 }
 
 void PDFWebContentsHelper::SaveUrlAs(const GURL& url,
-                                     blink::mojom::ReferrerPtr referrer) {
+                                     network::mojom::ReferrerPolicy policy) {
   client_->OnSaveURL(web_contents());
 
-  if (content::RenderFrameHost* rfh =
-          web_contents()->GetOuterWebContentsFrame()) {
-    web_contents()->SaveFrame(url, referrer.To<content::Referrer>(), rfh);
-  }
+  content::RenderFrameHost* rfh = web_contents()->GetOuterWebContentsFrame();
+  if (!rfh)
+    return;
+
+  content::Referrer referrer(url, policy);
+  referrer = content::Referrer::SanitizeForRequest(url, referrer);
+  web_contents()->SaveFrame(url, referrer, rfh);
 }
 
 void PDFWebContentsHelper::UpdateContentRestrictions(
diff --git a/components/pdf/browser/pdf_web_contents_helper.h b/components/pdf/browser/pdf_web_contents_helper.h
index 83c9c02..9c270aaf 100644
--- a/components/pdf/browser/pdf_web_contents_helper.h
+++ b/components/pdf/browser/pdf_web_contents_helper.h
@@ -82,7 +82,8 @@
   // mojom::PdfService:
   void SetListener(mojo::PendingRemote<mojom::PdfListener> listener) override;
   void HasUnsupportedFeature() override;
-  void SaveUrlAs(const GURL& url, blink::mojom::ReferrerPtr referrer) override;
+  void SaveUrlAs(const GURL& url,
+                 network::mojom::ReferrerPolicy policy) override;
   void UpdateContentRestrictions(int32_t content_restrictions) override;
   void SelectionChanged(const gfx::PointF& left,
                         int32_t left_height,
diff --git a/components/pdf/common/BUILD.gn b/components/pdf/common/BUILD.gn
index ecbfc3c..da1c143 100644
--- a/components/pdf/common/BUILD.gn
+++ b/components/pdf/common/BUILD.gn
@@ -9,16 +9,10 @@
   sources = [ "pdf.mojom" ]
 
   public_deps = [
-    "//third_party/blink/public/mojom:mojom_platform",
+    "//services/network/public/mojom:url_loader_base",
     "//ui/gfx/geometry/mojom",
     "//url/mojom:url_mojom_gurl",
   ]
 
-  overridden_deps = [ "//third_party/blink/public/mojom:mojom_platform" ]
-  component_deps = [ "//content/public/common" ]
-
-