diff --git a/BUILD.gn b/BUILD.gn
index 92c06f2f..06d0f01 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -1215,10 +1215,6 @@
       "//testing/scripts/run_isolated_script_test.py",
       "//testing/xvfb.py",
       "//third_party/blink/tools/",
-      "//third_party/blink/web_tests/VirtualTestSuites",
-      "//third_party/blink/web_tests/external/WPT_BASE_MANIFEST_8.json",
-      "//third_party/blink/web_tests/external/wpt/common/",
-      "//third_party/blink/web_tests/external/wpt/resources/",
       "//third_party/blink/web_tests/resources/",
       "//third_party/pywebsocket3/src/mod_pywebsocket/",
       "//third_party/test_fonts/test_fonts/",
@@ -1540,7 +1536,10 @@
       "//third_party/webgpu-cts",
     ]
     data = [
+      "//third_party/blink/web_tests/external/wpt/common/",
+      "//third_party/blink/web_tests/external/wpt/resources/",
       "//third_party/blink/web_tests/FlagSpecificConfig",
+      "//third_party/blink/web_tests/VirtualTestSuites",
       "//third_party/blink/web_tests/WebGPUExpectations",
       "//third_party/blink/web_tests/wpt_internal/",
       "//third_party/webgpu-cts/scripts/",
diff --git a/DEPS b/DEPS
index 301f7a6fe..8e5ae552 100644
--- a/DEPS
+++ b/DEPS
@@ -297,11 +297,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': 'e9e06c11b7234a3e9e44bc73033aa64561c01fc2',
+  'skia_revision': 'c5fb1f796056d9d6f7f72328d92f915fc6ca678d',
   # 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': 'c655393ae2edf28735aa458290e6fdf00319caf6',
+  'v8_revision': '6850f6876e0046a78aaea1b27eaf1529baa5dd34',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling ANGLE
   # and whatever else without interference from each other.
@@ -368,7 +368,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling catapult
   # and whatever else without interference from each other.
-  'catapult_revision': '84a7988038ae00e1e861381fedf76e233337843f',
+  'catapult_revision': '7e701fa6365e481bb9090ecd351c8f3cca8ee944',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling libFuzzer
   # and whatever else without interference from each other.
@@ -412,7 +412,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling feed
   # and whatever else without interference from each other.
-  'dawn_revision': '6fe1f515d4b4411fb44bb656ab804b833d16526a',
+  'dawn_revision': 'a4038bb1fda9b3e5b74619a37c07551af485539b',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling feed
   # and whatever else without interference from each other.
@@ -440,7 +440,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling nearby
   # and whatever else without interference from each other.
-  'nearby_revision': 'a37a7b7a839b1f3c189662af6720b94e3eaa3280',
+  'nearby_revision': '0d4964da7babe0a0ae01cd4950c5215dbd7dd8d1',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling securemessage
   # and whatever else without interference from each other.
@@ -792,7 +792,7 @@
   },
 
   'src/ios/third_party/material_components_ios/src': {
-      'url': Var('chromium_git') + '/external/github.com/material-components/material-components-ios.git' + '@' + 'c2607a4213a6efd3e6ebf874ae3145d92ac0dead',
+      'url': Var('chromium_git') + '/external/github.com/material-components/material-components-ios.git' + '@' + '0937378683bd95ffda6ac42074922c2301f7d703',
       'condition': 'checkout_ios',
   },
 
@@ -1573,7 +1573,7 @@
   },
 
   'src/third_party/perfetto':
-    Var('android_git') + '/platform/external/perfetto.git' + '@' + '566ee6c9e7612dd409089bd64c11a793870c1ee4',
+    Var('android_git') + '/platform/external/perfetto.git' + '@' + '01993ba7f5a26a65af195c874e8a8ee3ca030fda',
 
   'src/third_party/perl': {
       'url': Var('chromium_git') + '/chromium/deps/perl.git' + '@' + '6f3e5028eb65d0b4c5fdd792106ac4c84eee1eb3',
@@ -1729,10 +1729,10 @@
     Var('chromium_git') + '/external/khronosgroup/webgl.git' + '@' + '44e4c8770158c505b03ee7feafa4859d083b0912',
 
   'src/third_party/webgpu-cts/src':
-    Var('chromium_git') + '/external/github.com/gpuweb/cts.git' + '@' + '7af1f6b276edb0adce2ad6c2426a5c10981bda3d',
+    Var('chromium_git') + '/external/github.com/gpuweb/cts.git' + '@' + '94fd83896c67bb1a995337c501bbed02bd63361f',
 
   'src/third_party/webrtc':
-    Var('webrtc_git') + '/src.git' + '@' + 'eb8813b847b6859c4df972849eed19dd530edcb6',
+    Var('webrtc_git') + '/src.git' + '@' + '195b7ded1589960b14f37ddc3d2054fdc7c235b6',
 
   'src/third_party/libgifcodec':
      Var('skia_git') + '/libgifcodec' + '@'+  Var('libgifcodec_revision'),
@@ -1805,7 +1805,7 @@
     Var('chromium_git') + '/v8/v8.git' + '@' +  Var('v8_revision'),
 
   'src-internal': {
-    'url': 'https://chrome-internal.googlesource.com/chrome/src-internal.git@e46e4fc017c5cd8912c132cf8937529651d838c7',
+    'url': 'https://chrome-internal.googlesource.com/chrome/src-internal.git@ca28931612a053dfc58e3c4d7b9489c4dbb050b0',
     'condition': 'checkout_src_internal',
   },
 
@@ -1846,7 +1846,7 @@
     'packages': [
       {
         'package': 'chromeos_internal/apps/media_app/app',
-        'version': 'kROfPmt7XUdSWZ-LmtJQqDEYhSmmwEjnr4EokFqphHUC',
+        'version': 'qg8l0bKn_SqJM5CXus8trjrhts0XJ09OzUzRnR714d8C',
       },
     ],
     'condition': 'checkout_chromeos and checkout_src_internal',
diff --git a/android_webview/tools/system_webview_shell/apk/res/menu/main_menu.xml b/android_webview/tools/system_webview_shell/apk/res/menu/main_menu.xml
index 03909212..3b3b12c 100644
--- a/android_webview/tools/system_webview_shell/apk/res/menu/main_menu.xml
+++ b/android_webview/tools/system_webview_shell/apk/res/menu/main_menu.xml
@@ -21,6 +21,12 @@
         <item android:id="@+id/menu_force_dark_on"
               android:title="@string/menu_force_dark_on"/>
     </group>
+        <item android:id="@+id/menu_night_mode_on"
+        android:checkable="true"
+              android:title="@string/menu_night_mode_on"/>
+        <item android:id="@+id/menu_algorithmic_darkening_on"
+              android:checkable="true"
+              android:title="@string/menu_algorithmic_darkening_on"/>
     <item android:id="@+id/menu_print"
           android:title="@string/menu_print"/>
     <item android:id="@+id/start_animation_activity"
diff --git a/android_webview/tools/system_webview_shell/apk/res/values/strings.xml b/android_webview/tools/system_webview_shell/apk/res/values/strings.xml
index 125f35c..102f8fd3 100644
--- a/android_webview/tools/system_webview_shell/apk/res/values/strings.xml
+++ b/android_webview/tools/system_webview_shell/apk/res/values/strings.xml
@@ -22,6 +22,8 @@
     <string name="menu_force_dark_off">Force Dark Off</string>
     <string name="menu_force_dark_auto">Force Dark Auto</string>
     <string name="menu_force_dark_on">Force Dark On</string>
+    <string name="menu_algorithmic_darkening_on">Algorithmic Darkening ON</string>
+    <string name="menu_night_mode_on">Night Mode ON</string>
     <string name="menu_start_animation_activity">Animation test</string>
     <string name="menu_print">Print</string>
     <string name="menu_about">About WebView</string>
diff --git a/android_webview/tools/system_webview_shell/apk/src/org/chromium/webview_shell/WebViewBrowserActivity.java b/android_webview/tools/system_webview_shell/apk/src/org/chromium/webview_shell/WebViewBrowserActivity.java
index 49be5a5..7d95825 100644
--- a/android_webview/tools/system_webview_shell/apk/src/org/chromium/webview_shell/WebViewBrowserActivity.java
+++ b/android_webview/tools/system_webview_shell/apk/src/org/chromium/webview_shell/WebViewBrowserActivity.java
@@ -8,6 +8,7 @@
 import android.annotation.SuppressLint;
 import android.app.Activity;
 import android.app.AlertDialog;
+import android.app.UiModeManager;
 import android.content.ActivityNotFoundException;
 import android.content.Context;
 import android.content.Intent;
@@ -51,6 +52,7 @@
 import androidx.activity.result.ActivityResultLauncher;
 import androidx.annotation.RequiresApi;
 import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.app.AppCompatDelegate;
 import androidx.appcompat.widget.Toolbar;
 import androidx.webkit.TracingConfig;
 import androidx.webkit.TracingController;
@@ -59,6 +61,7 @@
 import androidx.webkit.WebViewCompat;
 import androidx.webkit.WebViewFeature;
 
+import org.chromium.base.BuildInfo;
 import org.chromium.base.ContextUtils;
 import org.chromium.base.Log;
 import org.chromium.base.PackageManagerUtils;
@@ -300,13 +303,18 @@
         // * detectCleartextNetwork() to permit testing http:// URLs
         // * detectFileUriExposure() to permit testing file:// URLs
         // * detectLeakedClosableObjects() because of drag and drop (https://crbug.com/1090841#c40)
-        StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
-                .detectActivityLeaks()
-                .detectLeakedRegistrationObjects()
-                .detectLeakedSqlLiteObjects()
-                .penaltyLog()
-                .penaltyDeath()
-                .build());
+        StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+            // WebViewBrowserActivity will have two instances when switching night mode back and
+            // forth for the 3rd times. Don't know the reason, this probably needs the investigation
+            // to rule out WebView holding the instance. (crbug.com/1348615)
+            builder = builder.detectActivityLeaks();
+        }
+        StrictMode.setVmPolicy(builder.detectLeakedRegistrationObjects()
+                                       .detectLeakedSqlLiteObjects()
+                                       .penaltyLog()
+                                       .penaltyDeath()
+                                       .build());
     }
 
     @Override
@@ -368,6 +376,15 @@
     }
 
     @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        ViewGroup viewGroup = (ViewGroup) (mWebView.getParent());
+        viewGroup.removeView(mWebView);
+        mWebView.destroy();
+        mWebView = null;
+    }
+
+    @Override
     public void onSaveInstanceState(Bundle savedInstanceState) {
         super.onSaveInstanceState(savedInstanceState);
         // Deliberately don't catch TransactionTooLargeException here.
@@ -600,11 +617,18 @@
         if (!WebViewFeature.isFeatureSupported(WebViewFeature.TRACING_CONTROLLER_BASIC_USAGE)) {
             menu.findItem(R.id.menu_enable_tracing).setEnabled(false);
         }
-        if (!WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
+        if (!WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)
+                || BuildInfo.targetsAtLeastT()) {
             menu.findItem(R.id.menu_force_dark_off).setEnabled(false);
             menu.findItem(R.id.menu_force_dark_auto).setEnabled(false);
             menu.findItem(R.id.menu_force_dark_on).setEnabled(false);
         }
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+            menu.findItem(R.id.menu_night_mode_on).setEnabled(false);
+        }
+        if (!BuildInfo.targetsAtLeastT()) {
+            menu.findItem(R.id.menu_algorithmic_darkening_on).setEnabled(false);
+        }
         return super.onCreateOptionsMenu(menu);
     }
 
@@ -617,7 +641,8 @@
         } else {
             menu.findItem(R.id.menu_enable_tracing).setEnabled(false);
         }
-        if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
+        if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)
+                && !BuildInfo.targetsAtLeastT()) {
             int forceDarkState = WebSettingsCompat.getForceDark(mWebView.getSettings());
             switch (forceDarkState) {
                 case WebSettingsCompat.FORCE_DARK_OFF:
@@ -631,6 +656,24 @@
                     break;
             }
         }
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+            boolean checked =
+                    AppCompatDelegate.MODE_NIGHT_YES == AppCompatDelegate.getDefaultNightMode();
+            int defaultNightMode = AppCompatDelegate.getDefaultNightMode();
+            if (defaultNightMode == AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
+                    || defaultNightMode == AppCompatDelegate.MODE_NIGHT_UNSPECIFIED) {
+                UiModeManager uiModeManager =
+                        (UiModeManager) this.getApplicationContext().getSystemService(
+                                UI_MODE_SERVICE);
+                checked = UiModeManager.MODE_NIGHT_YES == uiModeManager.getNightMode();
+            }
+            menu.findItem(R.id.menu_night_mode_on).setChecked(checked);
+        }
+        if (BuildInfo.targetsAtLeastT()) {
+            menu.findItem(R.id.menu_algorithmic_darkening_on)
+                    .setChecked(WebSettingsCompat.isAlgorithmicDarkeningAllowed(
+                            mWebView.getSettings()));
+        }
         return true;
     }
 
@@ -691,6 +734,15 @@
             WebSettingsCompat.setForceDark(mWebView.getSettings(), WebSettingsCompat.FORCE_DARK_ON);
             item.setChecked(true);
             return true;
+        } else if (itemId == R.id.menu_night_mode_on) {
+            AppCompatDelegate.setDefaultNightMode(item.isChecked()
+                            ? AppCompatDelegate.MODE_NIGHT_NO
+                            : AppCompatDelegate.MODE_NIGHT_YES);
+            return true;
+        } else if (itemId == R.id.menu_algorithmic_darkening_on) {
+            WebSettingsCompat.setAlgorithmicDarkeningAllowed(mWebView.getSettings(),
+                    !WebSettingsCompat.isAlgorithmicDarkeningAllowed(mWebView.getSettings()));
+            return true;
         } else if (itemId == R.id.start_animation_activity) {
             startActivity(new Intent(this, WebViewAnimationTestActivity.class));
             return true;
diff --git a/ash/app_list/views/app_drag_icon_proxy.cc b/ash/app_list/views/app_drag_icon_proxy.cc
index 06c901a..149fa71 100644
--- a/ash/app_list/views/app_drag_icon_proxy.cc
+++ b/ash/app_list/views/app_drag_icon_proxy.cc
@@ -8,6 +8,7 @@
 
 #include "ash/drag_drop/drag_image_view.h"
 #include "ash/public/cpp/style/color_provider.h"
+#include "ash/style/system_shadow.h"
 #include "ui/aura/window.h"
 #include "ui/base/dragdrop/mojom/drag_drop_types.mojom-shared.h"
 #include "ui/compositor/layer.h"
@@ -21,6 +22,15 @@
 
 namespace ash {
 
+constexpr SystemShadow::Type kShadowType = SystemShadow::Type::kElevation12;
+
+// For all app icons, there is an intended transparent ring around the visible
+// icon that makes the icon looks smaller than its actual size. The shadow is
+// needed to resize to align with the visual icon. Note that this constant is
+// the same as `kBackgroundCircleScale` in
+// chrome/browser/apps/icon_standardizer.cc
+constexpr float kShadowScaleFactor = 176.f / 192.f;
+
 AppDragIconProxy::AppDragIconProxy(
     aura::Window* root_window,
     const gfx::ImageSkia& icon,
@@ -36,6 +46,7 @@
   drag_image->SetImage(icon);
 
   gfx::Size size = drag_image->GetPreferredSize();
+
   size.set_width(std::round(size.width() * scale_factor));
   size.set_height(std::round(size.height() * scale_factor));
 
@@ -52,6 +63,19 @@
   drag_image->SetPaintToLayer();
   drag_image->layer()->SetFillsBoundsOpaquely(false);
 
+  // Create the shadow layer.
+  gfx::Size shadow_size = gfx::ScaleToFlooredSize(size, kShadowScaleFactor);
+  gfx::Point shadow_offset((size.width() - shadow_size.width()) / 2,
+                           (size.height() - shadow_size.height()) / 2);
+  shadow_ = SystemShadow::CreateShadowOnTextureLayer(kShadowType);
+  shadow_->SetRoundedCornerRadius(shadow_size.width() / 2);
+  auto* shadow_layer = shadow_->GetLayer();
+  auto* image_layer = drag_image->layer();
+
+  image_layer->Add(shadow_layer);
+  image_layer->StackAtBottom(shadow_layer);
+  shadow_->SetContentBounds(gfx::Rect(shadow_offset, shadow_size));
+
   if (use_blurred_background) {
     const float radius = size.width() / 2.0f;
     drag_image->layer()->SetRoundedCornerRadius(
diff --git a/ash/app_list/views/app_drag_icon_proxy.h b/ash/app_list/views/app_drag_icon_proxy.h
index 14e898e2..8e3f207 100644
--- a/ash/app_list/views/app_drag_icon_proxy.h
+++ b/ash/app_list/views/app_drag_icon_proxy.h
@@ -5,6 +5,7 @@
 #ifndef ASH_APP_LIST_VIEWS_APP_DRAG_ICON_PROXY_H_
 #define ASH_APP_LIST_VIEWS_APP_DRAG_ICON_PROXY_H_
 
+#include <memory>
 #include "base/callback.h"
 #include "ui/compositor/layer_animation_observer.h"
 #include "ui/gfx/geometry/vector2d.h"
@@ -25,6 +26,7 @@
 }  // namespace ui
 
 namespace ash {
+class SystemShadow;
 
 // Manages the drag image shown while an app is being dragged in app list or
 // shelf. It creates a DragImageView widget in a window container used for
@@ -83,6 +85,8 @@
   // progress.
   bool closing_widget_ = false;
 
+  std::unique_ptr<SystemShadow> shadow_;
+
   // The widget used to display the drag image.
   views::UniqueWidgetPtr drag_image_widget_;
 
diff --git a/ash/components/phonehub/BUILD.gn b/ash/components/phonehub/BUILD.gn
index c9cfd28..8e4dcb1 100644
--- a/ash/components/phonehub/BUILD.gn
+++ b/ash/components/phonehub/BUILD.gn
@@ -37,6 +37,8 @@
     "do_not_disturb_controller.h",
     "do_not_disturb_controller_impl.cc",
     "do_not_disturb_controller_impl.h",
+    "feature_setup_response_processor.cc",
+    "feature_setup_response_processor.h",
     "feature_status.cc",
     "feature_status.h",
     "feature_status_provider.cc",
@@ -239,6 +241,7 @@
     "connection_scheduler_impl_unittest.cc",
     "cros_state_sender_unittest.cc",
     "do_not_disturb_controller_impl_unittest.cc",
+    "feature_setup_response_processor_unittest.cc",
     "feature_status_provider_impl_unittest.cc",
     "find_my_device_controller_impl_unittest.cc",
     "icon_decoder_impl_unittest.cc",
diff --git a/ash/components/phonehub/combined_access_setup_operation.cc b/ash/components/phonehub/combined_access_setup_operation.cc
index d781859..562d239c 100644
--- a/ash/components/phonehub/combined_access_setup_operation.cc
+++ b/ash/components/phonehub/combined_access_setup_operation.cc
@@ -16,12 +16,18 @@
 // Status values which are considered "final" - i.e., once the status of an
 // operation changes to one of these values, the operation has completed. These
 // status values indicate either a success or a fatal error.
-constexpr std::array<CombinedAccessSetupOperation::Status, 4>
+constexpr std::array<CombinedAccessSetupOperation::Status, 8>
     kOperationFinishedStatus{
         CombinedAccessSetupOperation::Status::kTimedOutConnecting,
         CombinedAccessSetupOperation::Status::kConnectionDisconnected,
         CombinedAccessSetupOperation::Status::kCompletedSuccessfully,
         CombinedAccessSetupOperation::Status::kProhibitedFromProvidingAccess,
+        CombinedAccessSetupOperation::Status::kCompletedUserRejectedAllAccess,
+        CombinedAccessSetupOperation::Status::kOperationFailedOrCancelled,
+        CombinedAccessSetupOperation::Status::
+            kCameraRollGrantedNotificationRejected,
+        CombinedAccessSetupOperation::Status::
+            kCameraRollRejectedNotificationGranted,
     };
 
 }  // namespace
@@ -73,6 +79,22 @@
     case CombinedAccessSetupOperation::Status::kProhibitedFromProvidingAccess:
       stream << "[Prohibited from providing access]";
       break;
+    case CombinedAccessSetupOperation::Status::kCompletedUserRejectedAllAccess:
+      stream << "[User rejected to grant access]";
+      break;
+    case CombinedAccessSetupOperation::Status::kOperationFailedOrCancelled:
+      stream << "[Operation failed or cancelled]";
+      break;
+    case CombinedAccessSetupOperation::Status::
+        kCameraRollGrantedNotificationRejected:
+      stream << "[User granted access to Camera Roll but rejected access to "
+                "notification]";
+      break;
+    case CombinedAccessSetupOperation::Status::
+        kCameraRollRejectedNotificationGranted:
+      stream << "[User rejected access to Camera Roll but granted access to "
+                "notification]";
+      break;
   }
 
   return stream;
diff --git a/ash/components/phonehub/combined_access_setup_operation.h b/ash/components/phonehub/combined_access_setup_operation.h
index 5171ddb5..8a9d9dca 100644
--- a/ash/components/phonehub/combined_access_setup_operation.h
+++ b/ash/components/phonehub/combined_access_setup_operation.h
@@ -56,7 +56,19 @@
     // the user could be using a Work Profile).
     kProhibitedFromProvidingAccess = 6,
 
-    kMaxValue = kProhibitedFromProvidingAccess
+    // The user rejected all access during setup.
+    kCompletedUserRejectedAllAccess = 7,
+
+    // The setup was interrupted.
+    kOperationFailedOrCancelled = 8,
+
+    // Only camera roll access is granted.
+    kCameraRollGrantedNotificationRejected = 9,
+
+    // Only notification access is granted.
+    kCameraRollRejectedNotificationGranted = 10,
+
+    kMaxValue = kCameraRollRejectedNotificationGranted
   };
 
   // Returns true if the provided status is the final one for this operation,
diff --git a/ash/components/phonehub/fake_message_receiver.h b/ash/components/phonehub/fake_message_receiver.h
index ba15115..a07044ddb 100644
--- a/ash/components/phonehub/fake_message_receiver.h
+++ b/ash/components/phonehub/fake_message_receiver.h
@@ -16,6 +16,7 @@
   FakeMessageReceiver() = default;
   ~FakeMessageReceiver() override = default;
 
+  using MessageReceiver::NotifyFeatureSetupResponseReceived;
   using MessageReceiver::NotifyFetchCameraRollItemDataResponseReceived;
   using MessageReceiver::NotifyFetchCameraRollItemsResponseReceived;
   using MessageReceiver::NotifyPhoneStatusSnapshotReceived;
diff --git a/ash/components/phonehub/feature_setup_response_processor.cc b/ash/components/phonehub/feature_setup_response_processor.cc
new file mode 100644
index 0000000..f3607920
--- /dev/null
+++ b/ash/components/phonehub/feature_setup_response_processor.cc
@@ -0,0 +1,63 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ash/components/phonehub/feature_setup_response_processor.h"
+#include "ash/components/multidevice/logging/logging.h"
+#include "ash/components/phonehub/message_receiver.h"
+#include "ash/components/phonehub/multidevice_feature_access_manager.h"
+#include "ash/components/phonehub/proto/phonehub_api.pb.h"
+
+namespace ash {
+namespace phonehub {
+
+FeatureSetupResponseProcessor::FeatureSetupResponseProcessor(
+    MessageReceiver* message_receiver,
+    MultideviceFeatureAccessManager* multidevice_feature_access_manager)
+    : message_receiver_(message_receiver),
+      multidevice_feature_access_manager_(multidevice_feature_access_manager) {
+  DCHECK(message_receiver_);
+  DCHECK(multidevice_feature_access_manager_);
+
+  message_receiver_->AddObserver(this);
+}
+
+FeatureSetupResponseProcessor::~FeatureSetupResponseProcessor() {
+  message_receiver_->RemoveObserver(this);
+}
+
+void FeatureSetupResponseProcessor::OnFeatureSetupResponseReceived(
+    proto::FeatureSetupResponse response) {
+  if (response.camera_roll_setup_result() ==
+          proto::FeatureSetupResult::RESULT_ERROR_ACTION_CANCELED ||
+      response.notification_setup_result() ==
+          proto::FeatureSetupResult::RESULT_ERROR_ACTION_CANCELED ||
+      response.notification_setup_result() ==
+          proto::FeatureSetupResult::RESULT_ERROR_ACTION_TIMEOUT) {
+    multidevice_feature_access_manager_->SetCombinedSetupOperationStatus(
+        CombinedAccessSetupOperation::Status::kOperationFailedOrCancelled);
+  } else if (response.camera_roll_setup_result() ==
+                 proto::FeatureSetupResult::RESULT_PERMISSION_GRANTED &&
+             response.notification_setup_result() ==
+                 proto::FeatureSetupResult::RESULT_ERROR_USER_REJECT) {
+    multidevice_feature_access_manager_->SetCombinedSetupOperationStatus(
+        CombinedAccessSetupOperation::Status::
+            kCameraRollGrantedNotificationRejected);
+  } else if (response.camera_roll_setup_result() ==
+                 proto::FeatureSetupResult::RESULT_ERROR_USER_REJECT &&
+             response.notification_setup_result() ==
+                 proto::FeatureSetupResult::RESULT_PERMISSION_GRANTED) {
+    multidevice_feature_access_manager_->SetCombinedSetupOperationStatus(
+        CombinedAccessSetupOperation::Status::
+            kCameraRollRejectedNotificationGranted);
+  } else if (response.camera_roll_setup_result() ==
+                 proto::FeatureSetupResult::RESULT_ERROR_USER_REJECT ||
+             response.notification_setup_result() ==
+                 proto::FeatureSetupResult::RESULT_ERROR_USER_REJECT) {
+    multidevice_feature_access_manager_->SetCombinedSetupOperationStatus(
+        CombinedAccessSetupOperation::Status::kCompletedUserRejectedAllAccess);
+  }
+}
+
+}  // namespace phonehub
+}  // namespace ash
diff --git a/ash/components/phonehub/feature_setup_response_processor.h b/ash/components/phonehub/feature_setup_response_processor.h
new file mode 100644
index 0000000..f859f989
--- /dev/null
+++ b/ash/components/phonehub/feature_setup_response_processor.h
@@ -0,0 +1,41 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef ASH_COMPONENTS_PHONEHUB_FEATURE_SETUP_RESPONSE_PROCESSOR_H_
+#define ASH_COMPONENTS_PHONEHUB_FEATURE_SETUP_RESPONSE_PROCESSOR_H_
+
+#include "ash/components/phonehub/message_receiver.h"
+#include "ash/components/phonehub/proto/phonehub_api.pb.h"
+
+namespace ash {
+namespace phonehub {
+
+class MultideviceFeatureAccessManager;
+
+class FeatureSetupResponseProcessor : public MessageReceiver::Observer {
+ public:
+  FeatureSetupResponseProcessor(
+      MessageReceiver* message_receiver,
+      MultideviceFeatureAccessManager* multidevice_feature_access_manager);
+
+  ~FeatureSetupResponseProcessor() override;
+
+  FeatureSetupResponseProcessor(const FeatureSetupResponseProcessor&) = delete;
+  FeatureSetupResponseProcessor& operator=(
+      const FeatureSetupResponseProcessor&) = delete;
+
+ private:
+  friend class FeatureSetupResponseProcessorTest;
+
+  // MessageReceiver::Observer:
+  void OnFeatureSetupResponseReceived(
+      proto::FeatureSetupResponse response) override;
+
+  MessageReceiver* message_receiver_;
+  MultideviceFeatureAccessManager* multidevice_feature_access_manager_;
+};
+
+}  // namespace phonehub
+}  // namespace ash
+#endif
\ No newline at end of file
diff --git a/ash/components/phonehub/feature_setup_response_processor_unittest.cc b/ash/components/phonehub/feature_setup_response_processor_unittest.cc
new file mode 100644
index 0000000..0e3bd806
--- /dev/null
+++ b/ash/components/phonehub/feature_setup_response_processor_unittest.cc
@@ -0,0 +1,274 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+#include "ash/components/phonehub/feature_setup_response_processor.h"
+
+#include <memory>
+#include <utility>
+
+#include "ash/components/multidevice/logging/logging.h"
+#include "ash/components/phonehub/combined_access_setup_operation.h"
+#include "ash/components/phonehub/fake_message_receiver.h"
+#include "ash/components/phonehub/fake_multidevice_feature_access_manager.h"
+#include "ash/components/phonehub/proto/phonehub_api.pb.h"
+#include "ash/constants/ash_features.h"
+#include "base/test/scoped_feature_list.h"
+#include "base/test/task_environment.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace ash {
+namespace phonehub {
+namespace {
+class FakeCombinedAccessSetupOperationDelegate
+    : public CombinedAccessSetupOperation::Delegate {
+ public:
+  FakeCombinedAccessSetupOperationDelegate() = default;
+  ~FakeCombinedAccessSetupOperationDelegate() override = default;
+
+  CombinedAccessSetupOperation::Status status() const { return status_; }
+
+  // CombinedAccessSetupOperation::Delegate:
+  void OnCombinedStatusChange(
+      CombinedAccessSetupOperation::Status new_status) override {
+    status_ = new_status;
+  }
+
+ private:
+  CombinedAccessSetupOperation::Status status_ =
+      CombinedAccessSetupOperation::Status::kConnecting;
+};
+}  // namespace
+
+class FeatureSetupResponseProcessorTest : public testing::Test {
+ protected:
+  FeatureSetupResponseProcessorTest() = default;
+  FeatureSetupResponseProcessorTest(const FeatureSetupResponseProcessorTest&) =
+      delete;
+  FeatureSetupResponseProcessorTest& operator=(
+      const FeatureSetupResponseProcessorTest&) = delete;
+  ~FeatureSetupResponseProcessorTest() override = default;
+
+  void SetUp() override {
+    fake_message_receiver_ = std::make_unique<FakeMessageReceiver>();
+    fake_multidevice_feature_access_manager_ =
+        std::make_unique<FakeMultideviceFeatureAccessManager>();
+    fake_multidevice_feature_access_manager_
+        ->SetFeatureSetupRequestSupportedInternal(true);
+    scoped_feature_list_.InitWithFeatures(
+        /*enabled_features=*/{features::kEcheSWA, features::kPhoneHubCameraRoll,
+                              features::kPhoneHubFeatureSetupErrorHandling},
+        /*disabled_features=*/{});
+  }
+
+  void CreateFeatureSetupResponseProcessor() {
+    feature_setup_response_processor_ =
+        std::make_unique<FeatureSetupResponseProcessor>(
+            fake_message_receiver_.get(),
+            fake_multidevice_feature_access_manager_.get());
+  }
+
+  CombinedAccessSetupOperation::Status GetCombinedSetupOperationStatus() {
+    return fake_combined_delegate_.status();
+  }
+
+  base::test::ScopedFeatureList scoped_feature_list_;
+  std::unique_ptr<FakeMessageReceiver> fake_message_receiver_;
+  std::unique_ptr<FakeMultideviceFeatureAccessManager>
+      fake_multidevice_feature_access_manager_;
+  std::unique_ptr<FeatureSetupResponseProcessor>
+      feature_setup_response_processor_;
+  FakeCombinedAccessSetupOperationDelegate fake_combined_delegate_;
+};
+
+TEST_F(FeatureSetupResponseProcessorTest, ResponseReceived_All_Access_Granted) {
+  auto operation =
+      fake_multidevice_feature_access_manager_->AttemptCombinedFeatureSetup(
+          true, true, &fake_combined_delegate_);
+  EXPECT_TRUE(operation);
+  CreateFeatureSetupResponseProcessor();
+  proto::FeatureSetupResponse setupResponse;
+  setupResponse.set_camera_roll_setup_result(
+      proto::FeatureSetupResult::RESULT_PERMISSION_GRANTED);
+  setupResponse.set_notification_setup_result(
+      proto::FeatureSetupResult::RESULT_PERMISSION_GRANTED);
+
+  fake_message_receiver_->NotifyFeatureSetupResponseReceived(setupResponse);
+
+  // Success cases should not be handled by this processor
+  EXPECT_EQ(CombinedAccessSetupOperation::Status::kConnecting,
+            GetCombinedSetupOperationStatus());
+  EXPECT_TRUE(fake_multidevice_feature_access_manager_
+                  ->IsCombinedSetupOperationInProgress());
+}
+
+TEST_F(FeatureSetupResponseProcessorTest,
+       ResponseReceived_All_Access_Declined) {
+  auto operation =
+      fake_multidevice_feature_access_manager_->AttemptCombinedFeatureSetup(
+          true, true, &fake_combined_delegate_);
+  EXPECT_TRUE(operation);
+  CreateFeatureSetupResponseProcessor();
+  proto::FeatureSetupResponse setupResponse;
+  setupResponse.set_camera_roll_setup_result(
+      proto::FeatureSetupResult::RESULT_ERROR_USER_REJECT);
+  setupResponse.set_notification_setup_result(
+      proto::FeatureSetupResult::RESULT_ERROR_USER_REJECT);
+
+  fake_message_receiver_->NotifyFeatureSetupResponseReceived(setupResponse);
+
+  EXPECT_EQ(
+      CombinedAccessSetupOperation::Status::kCompletedUserRejectedAllAccess,
+      GetCombinedSetupOperationStatus());
+  EXPECT_FALSE(fake_multidevice_feature_access_manager_
+                   ->IsCombinedSetupOperationInProgress());
+}
+
+TEST_F(FeatureSetupResponseProcessorTest,
+       ResponseReceived_All_Requested_Notification_Access_Decliend) {
+  auto operation =
+      fake_multidevice_feature_access_manager_->AttemptCombinedFeatureSetup(
+          true, true, &fake_combined_delegate_);
+  EXPECT_TRUE(operation);
+  CreateFeatureSetupResponseProcessor();
+  proto::FeatureSetupResponse setupResponse;
+  setupResponse.set_camera_roll_setup_result(
+      proto::FeatureSetupResult::RESULT_PERMISSION_GRANTED);
+  setupResponse.set_notification_setup_result(
+      proto::FeatureSetupResult::RESULT_ERROR_USER_REJECT);
+
+  fake_message_receiver_->NotifyFeatureSetupResponseReceived(setupResponse);
+
+  EXPECT_EQ(CombinedAccessSetupOperation::Status::
+                kCameraRollGrantedNotificationRejected,
+            GetCombinedSetupOperationStatus());
+  EXPECT_FALSE(fake_multidevice_feature_access_manager_
+                   ->IsCombinedSetupOperationInProgress());
+}
+
+TEST_F(FeatureSetupResponseProcessorTest,
+       ResponseReceived_All_Requested_CameraRoll_Access_Decliend) {
+  auto operation =
+      fake_multidevice_feature_access_manager_->AttemptCombinedFeatureSetup(
+          true, true, &fake_combined_delegate_);
+  EXPECT_TRUE(operation);
+  CreateFeatureSetupResponseProcessor();
+  proto::FeatureSetupResponse setupResponse;
+  setupResponse.set_camera_roll_setup_result(
+      proto::FeatureSetupResult::RESULT_ERROR_USER_REJECT);
+  setupResponse.set_notification_setup_result(
+      proto::FeatureSetupResult::RESULT_PERMISSION_GRANTED);
+
+  fake_message_receiver_->NotifyFeatureSetupResponseReceived(setupResponse);
+
+  EXPECT_EQ(CombinedAccessSetupOperation::Status::
+                kCameraRollRejectedNotificationGranted,
+            GetCombinedSetupOperationStatus());
+  EXPECT_FALSE(fake_multidevice_feature_access_manager_
+                   ->IsCombinedSetupOperationInProgress());
+}
+
+TEST_F(FeatureSetupResponseProcessorTest,
+       ResponseReceived_CameraRoll_Requested_Access_Decliend) {
+  auto operation =
+      fake_multidevice_feature_access_manager_->AttemptCombinedFeatureSetup(
+          true, false, &fake_combined_delegate_);
+  EXPECT_TRUE(operation);
+  CreateFeatureSetupResponseProcessor();
+  proto::FeatureSetupResponse setupResponse;
+  setupResponse.set_camera_roll_setup_result(
+      proto::FeatureSetupResult::RESULT_ERROR_USER_REJECT);
+
+  fake_message_receiver_->NotifyFeatureSetupResponseReceived(setupResponse);
+
+  EXPECT_EQ(
+      CombinedAccessSetupOperation::Status::kCompletedUserRejectedAllAccess,
+      GetCombinedSetupOperationStatus());
+  EXPECT_FALSE(fake_multidevice_feature_access_manager_
+                   ->IsCombinedSetupOperationInProgress());
+}
+
+TEST_F(FeatureSetupResponseProcessorTest,
+       ResponseReceived_Notification_Requested_Access_Decliend) {
+  auto operation =
+      fake_multidevice_feature_access_manager_->AttemptCombinedFeatureSetup(
+          false, true, &fake_combined_delegate_);
+  EXPECT_TRUE(operation);
+  CreateFeatureSetupResponseProcessor();
+  proto::FeatureSetupResponse setupResponse;
+  setupResponse.set_notification_setup_result(
+      proto::FeatureSetupResult::RESULT_ERROR_USER_REJECT);
+
+  fake_message_receiver_->NotifyFeatureSetupResponseReceived(setupResponse);
+
+  EXPECT_EQ(
+      CombinedAccessSetupOperation::Status::kCompletedUserRejectedAllAccess,
+      GetCombinedSetupOperationStatus());
+  EXPECT_FALSE(fake_multidevice_feature_access_manager_
+                   ->IsCombinedSetupOperationInProgress());
+}
+
+TEST_F(FeatureSetupResponseProcessorTest,
+       ResponseReceived_All_Requested_CameraRoll_Setup_Interrupted) {
+  auto operation =
+      fake_multidevice_feature_access_manager_->AttemptCombinedFeatureSetup(
+          true, true, &fake_combined_delegate_);
+  EXPECT_TRUE(operation);
+  CreateFeatureSetupResponseProcessor();
+  proto::FeatureSetupResponse setupResponse;
+  setupResponse.set_camera_roll_setup_result(
+      proto::FeatureSetupResult::RESULT_ERROR_ACTION_CANCELED);
+  setupResponse.set_notification_setup_result(
+      proto::FeatureSetupResult::RESULT_ERROR_ACTION_CANCELED);
+
+  fake_message_receiver_->NotifyFeatureSetupResponseReceived(setupResponse);
+
+  EXPECT_EQ(CombinedAccessSetupOperation::Status::kOperationFailedOrCancelled,
+            GetCombinedSetupOperationStatus());
+  EXPECT_FALSE(fake_multidevice_feature_access_manager_
+                   ->IsCombinedSetupOperationInProgress());
+}
+
+TEST_F(FeatureSetupResponseProcessorTest,
+       ResponseReceived_All_Requested_Notification_Setup_Interrupted) {
+  auto operation =
+      fake_multidevice_feature_access_manager_->AttemptCombinedFeatureSetup(
+          true, true, &fake_combined_delegate_);
+  EXPECT_TRUE(operation);
+  CreateFeatureSetupResponseProcessor();
+  proto::FeatureSetupResponse setupResponse;
+  setupResponse.set_camera_roll_setup_result(
+      proto::FeatureSetupResult::RESULT_PERMISSION_GRANTED);
+  setupResponse.set_notification_setup_result(
+      proto::FeatureSetupResult::RESULT_ERROR_ACTION_CANCELED);
+
+  fake_message_receiver_->NotifyFeatureSetupResponseReceived(setupResponse);
+
+  EXPECT_EQ(CombinedAccessSetupOperation::Status::kOperationFailedOrCancelled,
+            GetCombinedSetupOperationStatus());
+  EXPECT_FALSE(fake_multidevice_feature_access_manager_
+                   ->IsCombinedSetupOperationInProgress());
+}
+
+TEST_F(FeatureSetupResponseProcessorTest,
+       ResponseReceived_All_Requested_Notification_Setup_Timeout) {
+  auto operation =
+      fake_multidevice_feature_access_manager_->AttemptCombinedFeatureSetup(
+          true, true, &fake_combined_delegate_);
+  EXPECT_TRUE(operation);
+  CreateFeatureSetupResponseProcessor();
+  proto::FeatureSetupResponse setupResponse;
+  setupResponse.set_camera_roll_setup_result(
+      proto::FeatureSetupResult::RESULT_ERROR_USER_REJECT);
+  setupResponse.set_notification_setup_result(
+      proto::FeatureSetupResult::RESULT_ERROR_ACTION_TIMEOUT);
+
+  fake_message_receiver_->NotifyFeatureSetupResponseReceived(setupResponse);
+
+  EXPECT_EQ(CombinedAccessSetupOperation::Status::kOperationFailedOrCancelled,
+            GetCombinedSetupOperationStatus());
+  EXPECT_FALSE(fake_multidevice_feature_access_manager_
+                   ->IsCombinedSetupOperationInProgress());
+}
+
+}  // namespace phonehub
+}  // namespace ash
\ No newline at end of file
diff --git a/ash/components/phonehub/message_receiver.cc b/ash/components/phonehub/message_receiver.cc
index 1282175..b1d7785e 100644
--- a/ash/components/phonehub/message_receiver.cc
+++ b/ash/components/phonehub/message_receiver.cc
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 #include "ash/components/phonehub/message_receiver.h"
+#include "ash/components/phonehub/proto/phonehub_api.pb.h"
 
 namespace ash {
 namespace phonehub {
@@ -30,6 +31,12 @@
     observer.OnPhoneStatusUpdateReceived(phone_status_update);
 }
 
+void MessageReceiver::NotifyFeatureSetupResponseReceived(
+    proto::FeatureSetupResponse response) {
+  for (auto& observer : observer_list_)
+    observer.OnFeatureSetupResponseReceived(response);
+}
+
 void MessageReceiver::NotifyFetchCameraRollItemsResponseReceived(
     const proto::FetchCameraRollItemsResponse& response) {
   for (auto& observer : observer_list_)
diff --git a/ash/components/phonehub/message_receiver.h b/ash/components/phonehub/message_receiver.h
index 593a65c2..73bf806b6 100644
--- a/ash/components/phonehub/message_receiver.h
+++ b/ash/components/phonehub/message_receiver.h
@@ -30,6 +30,10 @@
     virtual void OnPhoneStatusUpdateReceived(
         proto::PhoneStatusUpdate phone_status_update) {}
 
+    // Called when the remote feature setup is finished on the remote pohone.
+    virtual void OnFeatureSetupResponseReceived(
+        proto::FeatureSetupResponse feature_setup_response) {}
+
     // Called when the remote phone sends the list of camera roll items that
     // should be displayed via FetchCameraRollItemsResponse.
     virtual void OnFetchCameraRollItemsResponseReceived(
@@ -55,6 +59,7 @@
       proto::PhoneStatusSnapshot phone_status_snapshot);
   void NotifyPhoneStatusUpdateReceived(
       proto::PhoneStatusUpdate phone_status_update);
+  void NotifyFeatureSetupResponseReceived(proto::FeatureSetupResponse response);
   void NotifyFetchCameraRollItemsResponseReceived(
       const proto::FetchCameraRollItemsResponse& response);
   void NotifyFetchCameraRollItemDataResponseReceived(
diff --git a/ash/components/phonehub/message_receiver_impl.cc b/ash/components/phonehub/message_receiver_impl.cc
index c01f9c8..f306b49 100644
--- a/ash/components/phonehub/message_receiver_impl.cc
+++ b/ash/components/phonehub/message_receiver_impl.cc
@@ -40,6 +40,8 @@
       return "FETCH_CAMERA_ROLL_ITEMS_RESPONSE";
     case proto::MessageType::FETCH_CAMERA_ROLL_ITEM_DATA_RESPONSE:
       return "FETCH_CAMERA_ROLL_ITEM_DATA_RESPONSE";
+    case proto::MessageType::FEATURE_SETUP_RESPONSE:
+      return "FEATURE_SETUP_RESPONSE";
     default:
       return "UNKOWN_MESSAGE";
   }
@@ -98,6 +100,18 @@
     return;
   }
 
+  if (features::IsPhoneHubFeatureSetupErrorHandlingEnabled() &&
+      message_type == proto::MessageType::FEATURE_SETUP_RESPONSE) {
+    proto::FeatureSetupResponse response;
+    // Serialized proto is after the first two bytes of |payload|.
+    if (!response.ParseFromString(payload.substr(2))) {
+      PA_LOG(ERROR) << "OnMessageReceived() could not deserialize the "
+                    << "FeatureSetupResponse proto message.";
+      return;
+    }
+    NotifyFeatureSetupResponseReceived(response);
+  }
+
   if (features::IsPhoneHubCameraRollEnabled() &&
       message_type == proto::MessageType::FETCH_CAMERA_ROLL_ITEMS_RESPONSE) {
     proto::FetchCameraRollItemsResponse response;
diff --git a/ash/components/phonehub/message_receiver_unittest.cc b/ash/components/phonehub/message_receiver_unittest.cc
index e70e91f..fccb601 100644
--- a/ash/components/phonehub/message_receiver_unittest.cc
+++ b/ash/components/phonehub/message_receiver_unittest.cc
@@ -32,6 +32,10 @@
     return phone_status_updated_num_calls_;
   }
 
+  size_t feature_setup_response_num_calls() const {
+    return feature_setup_response_num_calls_;
+  }
+
   size_t fetch_camera_roll_items_response_calls() const {
     return fetch_camera_roll_items_response_calls_;
   }
@@ -46,6 +50,10 @@
     return last_status_update_;
   }
 
+  proto::FeatureSetupResponse last_feature_setup_response() const {
+    return last_feature_setup_response_;
+  }
+
   proto::FetchCameraRollItemsResponse last_fetch_camera_roll_items_response()
       const {
     return last_fetch_camera_roll_items_response_;
@@ -69,6 +77,12 @@
     ++phone_status_updated_num_calls_;
   }
 
+  void OnFeatureSetupResponseReceived(
+      proto::FeatureSetupResponse feature_setup_response) override {
+    last_feature_setup_response_ = feature_setup_response;
+    ++feature_setup_response_num_calls_;
+  }
+
   void OnFetchCameraRollItemsResponseReceived(
       const proto::FetchCameraRollItemsResponse& response) override {
     last_fetch_camera_roll_items_response_ = response;
@@ -84,10 +98,12 @@
  private:
   size_t phone_status_snapshot_updated_num_calls_ = 0;
   size_t phone_status_updated_num_calls_ = 0;
+  size_t feature_setup_response_num_calls_ = 0;
   size_t fetch_camera_roll_items_response_calls_ = 0;
   size_t fetch_camera_roll_item_data_response_calls_ = 0;
   proto::PhoneStatusSnapshot last_snapshot_;
   proto::PhoneStatusUpdate last_status_update_;
+  proto::FeatureSetupResponse last_feature_setup_response_;
   proto::FetchCameraRollItemsResponse last_fetch_camera_roll_items_response_;
   proto::FetchCameraRollItemDataResponse
       last_fetch_camera_roll_item_data_response_;
@@ -134,6 +150,10 @@
     return fake_observer_.status_updated_num_calls();
   }
 
+  size_t GetNumFeatureSetupResponseCalls() const {
+    return fake_observer_.feature_setup_response_num_calls();
+  }
+
   size_t GetNumFetchCameraRollItemsResponseCalls() const {
     return fake_observer_.fetch_camera_roll_items_response_calls();
   }
@@ -150,6 +170,10 @@
     return fake_observer_.last_status_update();
   }
 
+  proto::FeatureSetupResponse GetLastFeatureSetupResponse() const {
+    return fake_observer_.last_feature_setup_response();
+  }
+
   proto::FetchCameraRollItemsResponse GetLastFetchCameraRollItemsResponse()
       const {
     return fake_observer_.last_fetch_camera_roll_items_response();
@@ -222,6 +246,56 @@
 }
 
 TEST_F(MessageReceiverImplTest,
+       OnFeatrueSetupResponseReceivedWithFeatureEnabled) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(
+      features::kPhoneHubFeatureSetupErrorHandling);
+
+  proto::FeatureSetupResponse expected_response;
+  expected_response.set_camera_roll_setup_result(
+      proto::FeatureSetupResult::RESULT_PERMISSION_GRANTED);
+  expected_response.set_notification_setup_result(
+      proto::FeatureSetupResult::RESULT_PERMISSION_GRANTED);
+
+  const std::string expected_message =
+      SerializeMessage(proto::FEATURE_SETUP_RESPONSE, &expected_response);
+  fake_connection_manager_->NotifyMessageReceived(expected_message);
+
+  proto::FeatureSetupResponse actual_response = GetLastFeatureSetupResponse();
+
+  EXPECT_EQ(0u, GetNumPhoneStatusSnapshotCalls());
+  EXPECT_EQ(0u, GetNumPhoneStatusUpdatedCalls());
+  EXPECT_EQ(1u, GetNumFeatureSetupResponseCalls());
+  EXPECT_EQ(proto::FeatureSetupResult::RESULT_PERMISSION_GRANTED,
+            actual_response.camera_roll_setup_result());
+  EXPECT_EQ(proto::FeatureSetupResult::RESULT_PERMISSION_GRANTED,
+            actual_response.notification_setup_result());
+}
+
+TEST_F(MessageReceiverImplTest,
+       OnFeatrueSetupResponseReceivedWithFeatureDisabled) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndDisableFeature(
+      features::kPhoneHubFeatureSetupErrorHandling);
+
+  proto::FeatureSetupResponse expected_response;
+  expected_response.set_camera_roll_setup_result(
+      proto::FeatureSetupResult::RESULT_PERMISSION_GRANTED);
+  expected_response.set_notification_setup_result(
+      proto::FeatureSetupResult::RESULT_PERMISSION_GRANTED);
+
+  const std::string expected_message =
+      SerializeMessage(proto::FEATURE_SETUP_RESPONSE, &expected_response);
+  fake_connection_manager_->NotifyMessageReceived(expected_message);
+
+  proto::FeatureSetupResponse actual_response = GetLastFeatureSetupResponse();
+
+  EXPECT_EQ(0u, GetNumPhoneStatusSnapshotCalls());
+  EXPECT_EQ(0u, GetNumPhoneStatusUpdatedCalls());
+  EXPECT_EQ(0u, GetNumFeatureSetupResponseCalls());
+}
+
+TEST_F(MessageReceiverImplTest,
        OnFetchCameraRollItemsResponseReceivedWthFeatureEnabled) {
   base::test::ScopedFeatureList feature_list;
   feature_list.InitAndEnableFeature(features::kPhoneHubCameraRoll);
@@ -243,6 +317,7 @@
 
   EXPECT_EQ(0u, GetNumPhoneStatusSnapshotCalls());
   EXPECT_EQ(0u, GetNumPhoneStatusUpdatedCalls());
+  EXPECT_EQ(0u, GetNumFeatureSetupResponseCalls());
   EXPECT_EQ(1u, GetNumFetchCameraRollItemsResponseCalls());
   EXPECT_EQ(1, actual_response.items_size());
   EXPECT_EQ("key", actual_response.items(0).metadata().key());
@@ -269,6 +344,7 @@
 
   EXPECT_EQ(0u, GetNumPhoneStatusSnapshotCalls());
   EXPECT_EQ(0u, GetNumPhoneStatusUpdatedCalls());
+  EXPECT_EQ(0u, GetNumFeatureSetupResponseCalls());
   EXPECT_EQ(0u, GetNumFetchCameraRollItemsResponseCalls());
 }
 
@@ -293,6 +369,7 @@
 
   EXPECT_EQ(0u, GetNumPhoneStatusSnapshotCalls());
   EXPECT_EQ(0u, GetNumPhoneStatusUpdatedCalls());
+  EXPECT_EQ(0u, GetNumFeatureSetupResponseCalls());
   EXPECT_EQ(0u, GetNumFetchCameraRollItemsResponseCalls());
   EXPECT_EQ(1u, GetNumFetchCameraRollItemDataResponseCalls());
   EXPECT_EQ("key", actual_response.metadata().key());
@@ -319,6 +396,7 @@
 
   EXPECT_EQ(0u, GetNumPhoneStatusSnapshotCalls());
   EXPECT_EQ(0u, GetNumPhoneStatusUpdatedCalls());
+  EXPECT_EQ(0u, GetNumFeatureSetupResponseCalls());
   EXPECT_EQ(0u, GetNumFetchCameraRollItemsResponseCalls());
   EXPECT_EQ(0u, GetNumFetchCameraRollItemDataResponseCalls());
 }
diff --git a/ash/components/phonehub/multidevice_feature_access_manager.h b/ash/components/phonehub/multidevice_feature_access_manager.h
index 7d46f8c..afcdb876 100644
--- a/ash/components/phonehub/multidevice_feature_access_manager.h
+++ b/ash/components/phonehub/multidevice_feature_access_manager.h
@@ -8,6 +8,7 @@
 #include <ostream>
 
 #include "ash/components/phonehub/combined_access_setup_operation.h"
+#include "ash/components/phonehub/feature_setup_response_processor.h"
 #include "ash/components/phonehub/notification_access_setup_operation.h"
 #include "ash/services/multidevice_setup/public/mojom/multidevice_setup.mojom.h"
 #include "base/containers/flat_map.h"
@@ -171,6 +172,7 @@
  private:
   friend class MultideviceFeatureAccessManagerImplTest;
   friend class PhoneStatusProcessor;
+  friend class FeatureSetupResponseProcessor;
 
   // Sets the internal AccessStatus but does not send a request for
   // a new status to the remote phone device.
diff --git a/ash/components/phonehub/phone_hub_manager_impl.cc b/ash/components/phonehub/phone_hub_manager_impl.cc
index 7b7a5603c..9e780ec 100644
--- a/ash/components/phonehub/phone_hub_manager_impl.cc
+++ b/ash/components/phonehub/phone_hub_manager_impl.cc
@@ -157,7 +157,13 @@
                                      multidevice_setup_client,
                                      connection_manager_.get(),
                                      std::move(camera_roll_download_manager))
-                               : nullptr) {}
+                               : nullptr),
+      feature_setup_response_processor_(
+          features::IsPhoneHubFeatureSetupErrorHandlingEnabled()
+              ? std::make_unique<FeatureSetupResponseProcessor>(
+                    message_receiver_.get(),
+                    multidevice_feature_access_manager_.get())
+              : nullptr) {}
 
 PhoneHubManagerImpl::~PhoneHubManagerImpl() = default;
 
@@ -232,6 +238,7 @@
 // NOTE: These should be destroyed in the opposite order of how these objects
 // are initialized in the constructor.
 void PhoneHubManagerImpl::Shutdown() {
+  feature_setup_response_processor_.reset();
   camera_roll_manager_.reset();
   invalid_connection_disconnector_.reset();
   multidevice_setup_state_updater_.reset();
diff --git a/ash/components/phonehub/phone_hub_manager_impl.h b/ash/components/phonehub/phone_hub_manager_impl.h
index af06eb7..c34ab89 100644
--- a/ash/components/phonehub/phone_hub_manager_impl.h
+++ b/ash/components/phonehub/phone_hub_manager_impl.h
@@ -7,6 +7,7 @@
 
 #include <memory>
 
+#include "ash/components/phonehub/feature_setup_response_processor.h"
 #include "ash/components/phonehub/phone_hub_manager.h"
 // TODO(https://crbug.com/1164001): move to forward declaration.
 #include "ash/services/secure_channel/public/cpp/client/connection_manager.h"
@@ -111,6 +112,8 @@
   std::unique_ptr<InvalidConnectionDisconnector>
       invalid_connection_disconnector_;
   std::unique_ptr<CameraRollManager> camera_roll_manager_;
+  std::unique_ptr<FeatureSetupResponseProcessor>
+      feature_setup_response_processor_;
 };
 
 }  // namespace phonehub
diff --git a/ash/constants/ash_features.cc b/ash/constants/ash_features.cc
index 70dab32..bc6f0544 100644
--- a/ash/constants/ash_features.cc
+++ b/ash/constants/ash_features.cc
@@ -581,7 +581,7 @@
 // diagnostics app routines, network events, and system snapshot.
 // TODO(ashleydp): Remove this after the feature is launched.
 const base::Feature kEnableLogControllerForDiagnosticsApp{
-    "EnableLogControllerForDiagnosticsApp", base::FEATURE_DISABLED_BY_DEFAULT};
+    "EnableLogControllerForDiagnosticsApp", base::FEATURE_ENABLED_BY_DEFAULT};
 
 // If enabled, the networking cards will be shown in the diagnostics app.
 const base::Feature kEnableNetworkingInDiagnosticsApp{
@@ -1277,6 +1277,11 @@
     "ProjectorUseOAuthForGetVideoInfo",
     base::FEATURE_ENABLED_BY_DEFAULT);
 
+// Controls whether to allow viewing screencast with local playback URL when
+// screencast is being transcoded.
+const base::Feature kProjectorLocalPlayback("ProjectorLocalPlayback",
+                                            base::FEATURE_DISABLED_BY_DEFAULT);
+
 // Controls whether the quick dim prototype is enabled.
 const base::Feature kQuickDim{"QuickDim", base::FEATURE_ENABLED_BY_DEFAULT};
 
@@ -2192,6 +2197,10 @@
   return base::FeatureList::IsEnabled(kPhoneHubMonochromeNotificationIcons);
 }
 
+bool IsPhoneHubFeatureSetupErrorHandlingEnabled() {
+  return base::FeatureList::IsEnabled(kPhoneHubFeatureSetupErrorHandling);
+}
+
 bool IsPerformantSplitViewResizingEnabled() {
   return base::FeatureList::IsEnabled(kPerformantSplitViewResizing);
 }
@@ -2281,6 +2290,10 @@
   return base::FeatureList::IsEnabled(kProjectorUseOAuthForGetVideoInfo);
 }
 
+bool IsProjectorLocalPlaybackEnabled() {
+  return base::FeatureList::IsEnabled(kProjectorLocalPlayback);
+}
+
 bool IsQuickDimEnabled() {
   return base::FeatureList::IsEnabled(kQuickDim) && ash::switches::HasHps();
 }
diff --git a/ash/constants/ash_features.h b/ash/constants/ash_features.h
index 407e7e6e..92d1d79 100644
--- a/ash/constants/ash_features.h
+++ b/ash/constants/ash_features.h
@@ -514,6 +514,8 @@
 COMPONENT_EXPORT(ASH_CONSTANTS)
 extern const base::Feature kProjectorUseOAuthForGetVideoInfo;
 COMPONENT_EXPORT(ASH_CONSTANTS)
+extern const base::Feature kProjectorLocalPlayback;
+COMPONENT_EXPORT(ASH_CONSTANTS)
 extern const base::Feature kQuickDim;
 COMPONENT_EXPORT(ASH_CONSTANTS)
 extern const base::Feature kQuickSettingsNetworkRevamp;
@@ -794,6 +796,8 @@
 COMPONENT_EXPORT(ASH_CONSTANTS) bool IsPerDeskShelfEnabled();
 COMPONENT_EXPORT(ASH_CONSTANTS) bool IsPhoneHubCameraRollEnabled();
 COMPONENT_EXPORT(ASH_CONSTANTS)
+bool IsPhoneHubFeatureSetupErrorHandlingEnabled();
+COMPONENT_EXPORT(ASH_CONSTANTS)
 bool IsPhoneHubMonochromeNotificationIconsEnabled();
 COMPONENT_EXPORT(ASH_CONSTANTS) bool IsPerformantSplitViewResizingEnabled();
 COMPONENT_EXPORT(ASH_CONSTANTS) bool IsPersonalizationHubEnabled();
@@ -813,6 +817,7 @@
 COMPONENT_EXPORT(ASH_CONSTANTS) bool IsProjectorExcludeTranscriptEnabled();
 COMPONENT_EXPORT(ASH_CONSTANTS) bool IsProjectorTutorialVideoViewEnabled();
 COMPONENT_EXPORT(ASH_CONSTANTS) bool IsProjectorCustomThumbnailEnabled();
+COMPONENT_EXPORT(ASH_CONSTANTS) bool IsProjectorLocalPlaybackEnabled();
 COMPONENT_EXPORT(ASH_CONSTANTS)
 bool IsProjectorManagedUserIgnorePolicyEnabled();
 COMPONENT_EXPORT(ASH_CONSTANTS)
diff --git a/ash/public/cpp/projector/projector_client.h b/ash/public/cpp/projector/projector_client.h
index 656accf..33df33f 100644
--- a/ash/public/cpp/projector/projector_client.h
+++ b/ash/public/cpp/projector/projector_client.h
@@ -13,7 +13,6 @@
 
 namespace ash {
 
-class AnnotatorMessageHandler;
 struct NewScreencastPrecondition;
 
 // Creates interface to access Browser side functionalities for the
@@ -41,15 +40,6 @@
   virtual void MinimizeProjectorApp() const = 0;
   // Closes Projector SWA.
   virtual void CloseProjectorApp() const = 0;
-
-  // Registers the AnnotatorMessageHandler that is owned by the WebUI that
-  // contains the Projector annotator.
-  virtual void SetAnnotatorMessageHandler(AnnotatorMessageHandler* handler) = 0;
-  // Resets the stored AnnotatorMessageHandler if it matches the one that is
-  // passed in.
-  virtual void ResetAnnotatorMessageHandler(
-      AnnotatorMessageHandler* handler) = 0;
-
   // Notifies the Projector SWA if it can trigger a new Projector session.
   virtual void OnNewScreencastPreconditionChanged(
       const NewScreencastPrecondition& precondition) const = 0;
diff --git a/ash/public/cpp/test/mock_projector_client.h b/ash/public/cpp/test/mock_projector_client.h
index 4246c6f..43d07f1 100644
--- a/ash/public/cpp/test/mock_projector_client.h
+++ b/ash/public/cpp/test/mock_projector_client.h
@@ -38,8 +38,6 @@
   MOCK_CONST_METHOD0(CloseProjectorApp, void());
   MOCK_CONST_METHOD1(OnNewScreencastPreconditionChanged,
                      void(const NewScreencastPrecondition&));
-  MOCK_METHOD1(SetAnnotatorMessageHandler, void(AnnotatorMessageHandler*));
-  MOCK_METHOD1(ResetAnnotatorMessageHandler, void(AnnotatorMessageHandler*));
 
   // ProjectorAnnotatorController:
   MOCK_METHOD1(SetTool, void(const AnnotatorTool&));
diff --git a/ash/shelf/shelf_app_button.cc b/ash/shelf/shelf_app_button.cc
index f9e14d19..58ba72d 100644
--- a/ash/shelf/shelf_app_button.cc
+++ b/ash/shelf/shelf_app_button.cc
@@ -408,20 +408,7 @@
   }
   icon_image_ = image;
 
-  const int icon_size = shelf_view_->GetButtonIconSize() * icon_scale_;
-
-  // Resize the image maintaining our aspect ratio.
-  float aspect_ratio = static_cast<float>(icon_image_.width()) /
-                       static_cast<float>(icon_image_.height());
-  int height = icon_size;
-  int width = static_cast<int>(aspect_ratio * height);
-  if (width > icon_size) {
-    width = icon_size;
-    height = static_cast<int>(width / aspect_ratio);
-  }
-
-  const gfx::Size preferred_size(width, height);
-
+  gfx::Size preferred_size = GetPreferredIconSize();
   if (image.size() == preferred_size) {
     SetShadowedImage(image);
     return;
@@ -435,6 +422,15 @@
   return icon_view_->GetImage();
 }
 
+gfx::ImageSkia ShelfAppButton::GetIconImage() const {
+  const gfx::Size preferred_size = GetPreferredSize();
+  if (icon_image_.size() == preferred_size)
+    return icon_image_;
+
+  return gfx::ImageSkiaOperations::CreateResizedImage(
+      icon_image_, skia::ImageOperations::RESIZE_BEST, GetPreferredIconSize());
+}
+
 void ShelfAppButton::AddState(State state) {
   if (!(state_ & state)) {
     state_ |= state;
@@ -719,7 +715,6 @@
 
   // Expand bounds to include shadows.
   gfx::Insets insets_shadows = gfx::ShadowValue::GetMargin(icon_shadows_);
-  // insets_shadows = insets_shadows.Scale(icon_scale);
   // Center icon with respect to the secondary axis.
   if (is_horizontal_shelf)
     x_offset = std::max(0.0f, button_bounds.width() - icon_width + 1) / 2;
@@ -949,6 +944,22 @@
   return gfx::TransformBetweenRects(target_bounds, pre_scaling_bounds);
 }
 
+gfx::Size ShelfAppButton::GetPreferredIconSize() const {
+  const int icon_size = shelf_view_->GetButtonIconSize() * icon_scale_;
+
+  // Resize the image maintaining our aspect ratio.
+  float aspect_ratio = static_cast<float>(icon_image_.width()) /
+                       static_cast<float>(icon_image_.height());
+  int height = icon_size;
+  int width = static_cast<int>(aspect_ratio * height);
+  if (width > icon_size) {
+    width = icon_size;
+    height = static_cast<int>(width / aspect_ratio);
+  }
+
+  return gfx::Size(width, height);
+}
+
 void ShelfAppButton::ScaleAppIcon(bool scale_up) {
   StopObservingImplicitAnimations();
 
diff --git a/ash/shelf/shelf_app_button.h b/ash/shelf/shelf_app_button.h
index 7c54c77d..862ad52 100644
--- a/ash/shelf/shelf_app_button.h
+++ b/ash/shelf/shelf_app_button.h
@@ -74,6 +74,9 @@
   // Retrieve the image to show proxy operations.
   gfx::ImageSkia GetImage() const;
 
+  // Gets the resized `icon_image_` without the shadow.
+  gfx::ImageSkia GetIconImage() const;
+
   // |state| is or'd into the current state.
   void AddState(State state);
   void ClearState(State state);
@@ -162,6 +165,9 @@
   // Invoked when |ripple_activation_timer_| fires to activate the ink drop.
   void OnRippleTimer();
 
+  // Calculates the preferred size of the icon.
+  gfx::Size GetPreferredIconSize() const;
+
   // Scales up app icon if |scale_up| is true, otherwise scales it back to
   // normal size.
   void ScaleAppIcon(bool scale_up);
diff --git a/ash/shelf/shelf_view.cc b/ash/shelf/shelf_view.cc
index a2f49c9..6676c7cb 100644
--- a/ash/shelf/shelf_view.cc
+++ b/ash/shelf/shelf_view.cc
@@ -1519,8 +1519,8 @@
                              ? 1.0f
                              : kDragAndDropProxyScale;
     drag_icon_proxy_ = std::make_unique<AppDragIconProxy>(
-        root_window, drag_view_->GetImage(), screen_location, gfx::Vector2d(),
-        scale_factor, /*use_blurred_background=*/false);
+        root_window, drag_view_->GetIconImage(), screen_location,
+        gfx::Vector2d(), scale_factor, /*use_blurred_background=*/false);
 
     if (pointer == MOUSE) {
       haptics_util::PlayHapticTouchpadEffect(
@@ -1658,7 +1658,7 @@
     if (GetBoundsForDragInsertInScreen().Contains(screen_location)) {
       if (!is_active_drag_and_drop_host_) {
         drag_icon_proxy_ = std::make_unique<AppDragIconProxy>(
-            root_window, drag_view_->GetImage(), screen_location,
+            root_window, drag_view_->GetIconImage(), screen_location,
             /*cursor_offset_from_center=*/gfx::Vector2d(),
             /*scale_factor=*/1.0f,
             /*use_blurred_background=*/false);
@@ -1692,7 +1692,7 @@
       const gfx::Point center = drag_view_->GetLocalBounds().CenterPoint();
       const gfx::Vector2d cursor_offset_from_center = drag_origin_ - center;
       drag_icon_proxy_ = std::make_unique<AppDragIconProxy>(
-          root_window, drag_view_->GetImage(), screen_location,
+          root_window, drag_view_->GetIconImage(), screen_location,
           cursor_offset_from_center, /*scale_factor=*/1.0f,
           /*use_blurred_background=*/false);
       delegate_->CancelScrollForItemDrag();
diff --git a/ash/system/message_center/ash_notification_view.cc b/ash/system/message_center/ash_notification_view.cc
index ebbfaae3..671faca 100644
--- a/ash/system/message_center/ash_notification_view.cc
+++ b/ash/system/message_center/ash_notification_view.cc
@@ -184,16 +184,18 @@
       .SetBetweenChildSpacing(ash::kGroupedCollapsedSummaryLabelSpacing)
       .SetOrientation(views::BoxLayout::Orientation::kHorizontal)
       .SetVisible(false)
-      .AddChild(
-          views::Builder<views::Label>()
-              .SetText(notification.title())
-              .SetFontList(gfx::FontList({kGoogleSansFont}, gfx::Font::NORMAL,
-                                         message_center::kTitleFontSize,
-                                         gfx::Font::Weight::MEDIUM)))
+      .AddChild(views::Builder<views::Label>()
+                    .SetText(notification.title())
+                    .SetFontList(gfx::FontList(
+                        {kGoogleSansFont}, gfx::Font::NORMAL, kTitleLabelSize,
+                        gfx::Font::Weight::MEDIUM)))
       .AddChild(views::Builder<views::Label>()
                     .SetText(notification.message())
                     .SetTextContext(views::style::CONTEXT_DIALOG_BODY_TEXT)
-                    .SetTextStyle(views::style::STYLE_SECONDARY));
+                    .SetTextStyle(views::style::STYLE_SECONDARY)
+                    .SetFontList(gfx::FontList(
+                        {kGoogleSansFont}, gfx::Font::NORMAL, kMessageLabelSize,
+                        gfx::Font::Weight::MEDIUM)));
 }
 
 views::Builder<ash::AshNotificationView::GroupedNotificationsContainer>
@@ -904,15 +906,11 @@
   // are collapsed.
   bool use_expanded_padding = expanded || is_grouped_parent_view_;
 
-  bool is_single_expanded_notification =
-      !is_grouped_child_view_ && !is_grouped_parent_view_ && expanded;
-  header_row()->SetVisible(is_grouped_parent_view_ ||
-                           (is_single_expanded_notification));
+  header_row()->SetVisible(is_grouped_parent_view_ || expanded);
   header_row()->SetTimestampVisible(!is_grouped_parent_view_ || !expanded);
 
   if (title_row_) {
-    title_row_->UpdateVisibility(is_grouped_child_view_ ||
-                                 (IsExpandable() && !expanded));
+    title_row_->UpdateVisibility(IsExpandable() && !expanded);
     title_row_->title_view()->SetMaxLines(
         expanded ? kTitleLabelExpandedMaxLines : kTitleLabelCollapsedMaxLines);
     title_row_->title_view()->SetMaximumWidth(GetExpandedTitleLabelWidth());
@@ -994,7 +992,7 @@
   if (is_grouped_child_view_ && !is_nested())
     SetIsNested();
 
-  header_row()->SetVisible(!is_grouped_child_view_);
+  header_row()->SetIsInGroupChildNotification(is_grouped_child_view_);
   UpdateMessageLabelInExpandedState(notification);
 
   NotificationViewBase::UpdateWithNotification(notification);
diff --git a/ash/system/message_center/message_center_constants.h b/ash/system/message_center/message_center_constants.h
index 4d258e0..d21ad541 100644
--- a/ash/system/message_center/message_center_constants.h
+++ b/ash/system/message_center/message_center_constants.h
@@ -15,8 +15,8 @@
 constexpr int kGroupedCollapsedSummaryMessageLength = 250;
 constexpr auto kGroupedCollapsedSummaryInsets = gfx::Insets::TLBR(0, 50, 0, 16);
 
-constexpr int kGroupedNotificationsExpandedSpacing = 16;
-constexpr int kGroupedNotificationsCollapsedSpacing = 12;
+constexpr int kGroupedNotificationsExpandedSpacing = 0;
+constexpr int kGroupedNotificationsCollapsedSpacing = 4;
 constexpr auto kGroupedNotificationContainerCollapsedInsets =
     gfx::Insets::TLBR(0, 0, 20, 0);
 constexpr auto kGroupedNotificationContainerExpandedInsets =
diff --git a/ash/system/time/calendar_view.cc b/ash/system/time/calendar_view.cc
index 74bdb94..ed9b811 100644
--- a/ash/system/time/calendar_view.cc
+++ b/ash/system/time/calendar_view.cc
@@ -1110,6 +1110,13 @@
                     gfx::Tween::FAST_OUT_SLOW_IN);
 }
 
+void CalendarView::OnSelectedDateUpdated() {
+  // If the event list is already open and the date cell is focused, moves the
+  // focusing ring to the close button.
+  if (event_list_view_ && IsDateCellViewFocused())
+    RequestFocusForEventListCloseButton();
+}
+
 void CalendarView::ScrollUpOneMonth() {
   calendar_view_controller_->UpdateMonth(
       calendar_view_controller_->GetPreviousMonthFirstDayUTC(1));
@@ -1633,16 +1640,8 @@
 
   // Moves focusing ring to the close button of the event list if it's opened
   // from the date cell view focus.
-  if (IsDateCellViewFocused()) {
-    auto* focus_manager = GetFocusManager();
-    event_list_view_->RequestFocus();
-    focus_manager->AdvanceFocus(/*reverse=*/false);
-    current_month_->DisableFocus();
-    previous_month_->DisableFocus();
-    next_month_->DisableFocus();
-    next_next_month_->DisableFocus();
-    content_view_->SetFocusBehavior(FocusBehavior::ALWAYS);
-  }
+  if (IsDateCellViewFocused())
+    RequestFocusForEventListCloseButton();
 
   up_button_->SetTooltipText(l10n_util::GetStringUTF16(
       IDS_ASH_CALENDAR_UP_BUTTON_EVENT_LIST_ACCESSIBLE_DESCRIPTION));
@@ -1677,6 +1676,18 @@
       IDS_ASH_CALENDAR_DOWN_BUTTON_ACCESSIBLE_DESCRIPTION));
 }
 
+void CalendarView::RequestFocusForEventListCloseButton() {
+  DCHECK(event_list_view_);
+  auto* focus_manager = GetFocusManager();
+  event_list_view_->RequestFocus();
+  focus_manager->AdvanceFocus(/*reverse=*/false);
+  current_month_->DisableFocus();
+  previous_month_->DisableFocus();
+  next_month_->DisableFocus();
+  next_next_month_->DisableFocus();
+  content_view_->SetFocusBehavior(FocusBehavior::ALWAYS);
+}
+
 void CalendarView::OnResetToTodayAnimationComplete() {
   SetShouldMonthsAnimateAndScrollEnabled(/*enabled=*/true);
   ResetToToday();
diff --git a/ash/system/time/calendar_view.h b/ash/system/time/calendar_view.h
index a621b82..4f81873 100644
--- a/ash/system/time/calendar_view.h
+++ b/ash/system/time/calendar_view.h
@@ -90,6 +90,7 @@
   void OnMonthChanged() override;
   void OpenEventList() override;
   void CloseEventList() override;
+  void OnSelectedDateUpdated() override;
 
   // views::ViewObserver:
   void OnViewBoundsChanged(views::View* observed_view) override;
@@ -293,6 +294,9 @@
   void OnOpenEventListAnimationComplete();
   void OnCloseEventListAnimationComplete();
 
+  // Requests the focusing ring to go to the close button of `event_list_view_`.
+  void RequestFocusForEventListCloseButton();
+
   // Animates the month and scrolls back to today and resets the
   // `scrolling_settled_timer_` to update the `on_screen_month_` map after the
   // resetting to today animation.
diff --git a/ash/system/time/calendar_view_unittest.cc b/ash/system/time/calendar_view_unittest.cc
index 34d4632..4380c02a 100644
--- a/ash/system/time/calendar_view_unittest.cc
+++ b/ash/system/time/calendar_view_unittest.cc
@@ -187,6 +187,10 @@
   }
   void ResetToToday() { calendar_view_->ResetToToday(); }
 
+  void RequestFocusForEventListCloseButton() {
+    calendar_view_->RequestFocusForEventListCloseButton();
+  }
+
   void PressTab() {
     ui::test::EventGenerator generator(Shell::GetPrimaryRootWindow());
     generator.PressKey(ui::KeyboardCode::VKEY_TAB, ui::EF_NONE);
@@ -787,6 +791,116 @@
   EXPECT_EQ(todays_date_cell_view, focus_manager->GetFocusedView());
 }
 
+// Tests `RequestFocusForEventListCloseButton()`.
+TEST_F(CalendarViewTest, CloseButtonFocusing) {
+  base::Time date;
+  // Create a monthview based on Jun,7th 2021.
+  ASSERT_TRUE(base::Time::FromString("7 Jun 2021 10:00 GMT", &date));
+
+  // Set time override.
+  SetFakeNow(date);
+  base::subtle::ScopedTimeClockOverrides time_override(
+      &CalendarViewTest::FakeTimeNow, /*time_ticks_override=*/nullptr,
+      /*thread_ticks_override=*/nullptr);
+
+  CreateCalendarView();
+
+  auto* focus_manager = calendar_view()->GetFocusManager();
+  // Todays DateCellView should be focused on open.
+  ASSERT_EQ(u"7",
+            static_cast<views::LabelButton*>(focus_manager->GetFocusedView())
+                ->GetText());
+  ASSERT_FALSE(event_list_view());
+
+  PressEnter();
+  EXPECT_TRUE(event_list_view());
+
+  EXPECT_EQ(focus_manager->GetFocusedView(), close_button());
+
+  // Focus moves back to the date cell.
+  PressShiftTab();
+  EXPECT_EQ(u"7",
+            static_cast<views::LabelButton*>(focus_manager->GetFocusedView())
+                ->GetText());
+
+  // Manually moves the focus to the close button.
+  RequestFocusForEventListCloseButton();
+  EXPECT_EQ(focus_manager->GetFocusedView(), close_button());
+}
+
+// Tests when the focus changes to another date cell with the event list opened,
+// the focusing ring will go to the close button automatically.
+TEST_F(CalendarViewTest, FocusingToCloseButtonWithEventListOpened) {
+  base::Time date;
+  // Create a monthview based on Jun,7th 2021.
+  ASSERT_TRUE(base::Time::FromString("7 Jun 2021 10:00 GMT", &date));
+
+  // Set time override.
+  SetFakeNow(date);
+  base::subtle::ScopedTimeClockOverrides time_override(
+      &CalendarViewTest::FakeTimeNow, /*time_ticks_override=*/nullptr,
+      /*thread_ticks_override=*/nullptr);
+
+  CreateCalendarView();
+
+  auto* focus_manager = calendar_view()->GetFocusManager();
+  // Todays DateCellView should be focused on open.
+  ASSERT_EQ(u"7",
+            static_cast<views::LabelButton*>(focus_manager->GetFocusedView())
+                ->GetText());
+  ASSERT_FALSE(event_list_view());
+
+  PressEnter();
+  EXPECT_TRUE(event_list_view());
+
+  EXPECT_EQ(focus_manager->GetFocusedView(), close_button());
+
+  // Focus moves back to today's date cell.
+  PressShiftTab();
+  EXPECT_EQ(u"7",
+            static_cast<views::LabelButton*>(focus_manager->GetFocusedView())
+                ->GetText());
+
+  // Navigates to another date cell and focuses on it. The focusing ring should
+  // go to the close button automatically.
+  PressUp();
+  EXPECT_EQ(u"31",
+            static_cast<views::LabelButton*>(focus_manager->GetFocusedView())
+                ->GetText());
+  PressEnter();
+  EXPECT_TRUE(event_list_view());
+  EXPECT_EQ(focus_manager->GetFocusedView(), close_button());
+
+  // Tests different date cells and expects the same focusing behavior.
+  PressShiftTab();
+  PressLeft();
+  EXPECT_EQ(u"29",
+            static_cast<views::LabelButton*>(focus_manager->GetFocusedView())
+                ->GetText());
+  PressEnter();
+  EXPECT_TRUE(event_list_view());
+  EXPECT_EQ(focus_manager->GetFocusedView(), close_button());
+
+  PressShiftTab();
+  PressRight();
+  PressRight();
+  EXPECT_EQ(u"25",
+            static_cast<views::LabelButton*>(focus_manager->GetFocusedView())
+                ->GetText());
+  PressEnter();
+  EXPECT_TRUE(event_list_view());
+  EXPECT_EQ(focus_manager->GetFocusedView(), close_button());
+
+  PressShiftTab();
+  PressDown();
+  EXPECT_EQ(u"30",
+            static_cast<views::LabelButton*>(focus_manager->GetFocusedView())
+                ->GetText());
+  PressEnter();
+  EXPECT_TRUE(event_list_view());
+  EXPECT_EQ(focus_manager->GetFocusedView(), close_button());
+}
+
 TEST_F(CalendarViewTest, MonthViewFocusing) {
   base::Time date;
   // Create a monthview based on Jun,7th 2021.
diff --git a/ash/wallpaper/wallpaper_controller_impl.cc b/ash/wallpaper/wallpaper_controller_impl.cc
index 260bafc5..08ab9be 100644
--- a/ash/wallpaper/wallpaper_controller_impl.cc
+++ b/ash/wallpaper/wallpaper_controller_impl.cc
@@ -100,6 +100,9 @@
 
 namespace {
 
+// Global to hold a WallpaperPrefManager for testing in `Create`.
+std::unique_ptr<WallpaperPrefManager> g_test_pref_manager;
+
 // The file name of the policy wallpaper.
 constexpr char kPolicyWallpaperFile[] = "policy-controlled.jpeg";
 
@@ -657,11 +660,23 @@
     PrefService* local_state) {
   auto online_wallpaper_variant_fetcher =
       std::make_unique<OnlineWallpaperVariantInfoFetcher>();
+  if (g_test_pref_manager) {
+    return std::make_unique<WallpaperControllerImpl>(
+        std::move(g_test_pref_manager),
+        std::move(online_wallpaper_variant_fetcher));
+  }
+
   auto pref_manager = WallpaperPrefManager::Create(local_state);
   return std::make_unique<WallpaperControllerImpl>(
       std::move(pref_manager), std::move(online_wallpaper_variant_fetcher));
 }
 
+// static
+void WallpaperControllerImpl::SetWallpaperPrefManagerForTesting(
+    std::unique_ptr<WallpaperPrefManager> pref_manager) {
+  g_test_pref_manager.swap(pref_manager);
+}
+
 WallpaperControllerImpl::WallpaperControllerImpl(
     std::unique_ptr<WallpaperPrefManager> pref_manager,
     std::unique_ptr<OnlineWallpaperVariantInfoFetcher> online_fetcher)
diff --git a/ash/wallpaper/wallpaper_controller_impl.h b/ash/wallpaper/wallpaper_controller_impl.h
index d3c7f6e..66694dc 100644
--- a/ash/wallpaper/wallpaper_controller_impl.h
+++ b/ash/wallpaper/wallpaper_controller_impl.h
@@ -102,6 +102,9 @@
   static std::unique_ptr<WallpaperControllerImpl> Create(
       PrefService* local_state);
 
+  static void SetWallpaperPrefManagerForTesting(
+      std::unique_ptr<WallpaperPrefManager> pref_manager);
+
   // Prefer to use to obtain an new instance unless injecting non-production
   // members i.e. in tests.
   explicit WallpaperControllerImpl(
@@ -201,11 +204,6 @@
   bool SetUserWallpaperInfo(const AccountId& account_id,
                             const WallpaperInfo& info);
 
-  // Gets wallpaper info of |account_id| from local state, or memory if the user
-  // is ephemeral. Returns false if wallpaper info is not found.
-  bool GetUserWallpaperInfo(const AccountId& account_id,
-                            WallpaperInfo* info) const;
-
   // Gets encoded wallpaper from cache. Returns true if success.
   bool GetWallpaperFromCache(const AccountId& account_id,
                              gfx::ImageSkia* image);
@@ -391,6 +389,11 @@
     base::FilePath file_path;
   };
 
+  // Gets wallpaper info of |account_id| from local state, or memory if the user
+  // is ephemeral. Returns false if wallpaper info is not found.
+  bool GetUserWallpaperInfo(const AccountId& account_id,
+                            WallpaperInfo* info) const;
+
   // Update a Wallpaper for |root_window|.
   void UpdateWallpaperForRootWindow(aura::Window* root_window,
                                     bool lock_state_changed,
diff --git a/ash/wallpaper/wallpaper_controller_unittest.cc b/ash/wallpaper/wallpaper_controller_unittest.cc
index 64b44373..7fc155f 100644
--- a/ash/wallpaper/wallpaper_controller_unittest.cc
+++ b/ash/wallpaper/wallpaper_controller_unittest.cc
@@ -262,85 +262,11 @@
   }
 }
 
-base::Value::Dict CreateWallpaperInfoDict(WallpaperInfo info) {
-  base::Value::Dict wallpaper_info_dict;
-  if (info.asset_id.has_value()) {
-    wallpaper_info_dict.Set(WallpaperPrefManager::kNewWallpaperAssetIdNodeName,
-                            base::NumberToString(info.asset_id.value()));
-  }
-  if (info.dedup_key.has_value()) {
-    wallpaper_info_dict.Set(WallpaperPrefManager::kNewWallpaperDedupKeyNodeName,
-                            info.dedup_key.value());
-  }
-  if (info.unit_id.has_value()) {
-    wallpaper_info_dict.Set(WallpaperPrefManager::kNewWallpaperUnitIdNodeName,
-                            base::NumberToString(info.unit_id.value()));
-  }
-  base::Value::List online_wallpaper_variant_list;
-  for (const auto& variant : info.variants) {
-    base::Value::Dict online_wallpaper_variant_dict;
-    online_wallpaper_variant_dict.Set(
-        WallpaperPrefManager::kNewWallpaperAssetIdNodeName,
-        base::NumberToString(variant.asset_id));
-    online_wallpaper_variant_dict.Set(
-        WallpaperPrefManager::kOnlineWallpaperUrlNodeName,
-        variant.raw_url.spec());
-    online_wallpaper_variant_dict.Set(
-        WallpaperPrefManager::kOnlineWallpaperTypeNodeName,
-        static_cast<int>(variant.type));
-    online_wallpaper_variant_list.Append(
-        std::move(online_wallpaper_variant_dict));
-  }
-  wallpaper_info_dict.Set(
-      WallpaperPrefManager::kNewWallpaperVariantListNodeName,
-      std::move(online_wallpaper_variant_list));
-  wallpaper_info_dict.Set(
-      WallpaperPrefManager::kNewWallpaperCollectionIdNodeName,
-      info.collection_id);
-  wallpaper_info_dict.Set(WallpaperPrefManager::kNewWallpaperDateNodeName,
-                          base::NumberToString(info.date.ToInternalValue()));
-  wallpaper_info_dict.Set(WallpaperPrefManager::kNewWallpaperLocationNodeName,
-                          info.location);
-  wallpaper_info_dict.Set(
-      WallpaperPrefManager::kNewWallpaperUserFilePathNodeName,
-      info.user_file_path);
-  wallpaper_info_dict.Set(WallpaperPrefManager::kNewWallpaperLayoutNodeName,
-                          info.layout);
-  wallpaper_info_dict.Set(WallpaperPrefManager::kNewWallpaperTypeNodeName,
-                          static_cast<int>(info.type));
-  return wallpaper_info_dict;
-}
-
-PrefService* GetLocalPrefService() {
-  return Shell::Get()->local_state();
-}
-
 PrefService* GetProfilePrefService(const AccountId& account_id) {
   return Shell::Get()->session_controller()->GetUserPrefServiceForUser(
       account_id);
 }
 
-void PutWallpaperInfoInPrefs(AccountId account_id,
-                             WallpaperInfo info,
-                             PrefService* pref_service,
-                             const std::string& pref_name) {
-  DictionaryPrefUpdate wallpaper_update(pref_service, pref_name);
-  base::Value::Dict wallpaper_info_dict = CreateWallpaperInfoDict(info);
-  wallpaper_update->SetKey(account_id.GetUserEmail(),
-                           base::Value(std::move(wallpaper_info_dict)));
-}
-
-void AssertWallpaperInfoInPrefs(const PrefService* pref_service,
-                                const char pref_name[],
-                                AccountId account_id,
-                                WallpaperInfo info) {
-  const base::Value::Dict& pref_dict = pref_service->GetValueDict(pref_name);
-  const base::Value::Dict* stored_info_dict =
-      pref_dict.FindDict(account_id.GetUserEmail());
-  base::Value::Dict expected_info_dict = CreateWallpaperInfoDict(info);
-  EXPECT_EQ(expected_info_dict, *stored_info_dict);
-}
-
 WallpaperInfo InfoWithType(WallpaperType type) {
   WallpaperInfo info(std::string(), WALLPAPER_LAYOUT_CENTER_CROPPED, type,
                      base::Time::Now());
@@ -419,6 +345,13 @@
   WallpaperControllerTest& operator=(const WallpaperControllerTest&) = delete;
 
   void SetUp() override {
+    auto pref_manager = WallpaperPrefManager::Create(local_state());
+    pref_manager_ = pref_manager.get();
+    // Override the pref manager that will be used to construct the
+    // WallpaperController.
+    WallpaperControllerImpl::SetWallpaperPrefManagerForTesting(
+        std::move(pref_manager));
+
     AshTestBase::SetUp();
 
     TestSessionControllerClient* const client = GetSessionControllerClient();
@@ -568,13 +501,13 @@
     WallpaperInfo info = {relative_path, WALLPAPER_LAYOUT_CENTER_CROPPED,
                           WallpaperType::kCustomized,
                           base::Time::Now().LocalMidnight()};
-    ASSERT_TRUE(controller_->SetUserWallpaperInfo(account_id, info));
+    ASSERT_TRUE(pref_manager_->SetUserWallpaperInfo(account_id, info));
   }
 
   // Simulates setting a custom wallpaper by directly setting the wallpaper
   // info.
   void SimulateSettingCustomWallpaper(const AccountId& account_id) {
-    ASSERT_TRUE(controller_->SetUserWallpaperInfo(
+    ASSERT_TRUE(pref_manager_->SetUserWallpaperInfo(
         account_id,
         WallpaperInfo("dummy_file_location", WALLPAPER_LAYOUT_CENTER,
                       WallpaperType::kCustomized,
@@ -726,7 +659,8 @@
     RunAllTasksUntilIdle();
   }
 
-  WallpaperControllerImpl* controller_ = nullptr;  // Not owned.
+  WallpaperControllerImpl* controller_;
+  WallpaperPrefManager* pref_manager_ = nullptr;  // owned by controller
 
   base::ScopedTempDir user_data_dir_;
   base::ScopedTempDir online_wallpaper_dir_;
@@ -1120,7 +1054,8 @@
       WallpaperController::SetWallpaperCallback());
   RunAllTasksUntilIdle();
   // Verify that the user wallpaper info is updated.
-  EXPECT_TRUE(controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+  EXPECT_TRUE(
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
   WallpaperInfo expected_wallpaper_info(
       kDummyUrl, WALLPAPER_LAYOUT_CENTER_CROPPED, WallpaperType::kOnline,
       base::Time::Now().LocalMidnight());
@@ -1142,7 +1077,8 @@
       WallpaperController::SetWallpaperCallback());
   RunAllTasksUntilIdle();
   EXPECT_EQ(0, GetWallpaperCount());
-  EXPECT_TRUE(controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+  EXPECT_TRUE(
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
   WallpaperInfo expected_wallpaper_info_2(
       kDummyUrl2, WALLPAPER_LAYOUT_CENTER_CROPPED, WallpaperType::kOnline,
       base::Time::Now().LocalMidnight());
@@ -1176,7 +1112,8 @@
   EXPECT_EQ(controller_->GetWallpaperType(), WallpaperType::kOnline);
   // Verify that the user wallpaper info is updated.
   WallpaperInfo wallpaper_info;
-  EXPECT_TRUE(controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+  EXPECT_TRUE(
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
   WallpaperInfo expected_wallpaper_info(params);
   EXPECT_EQ(wallpaper_info, expected_wallpaper_info);
   // Verify that wallpaper & collection metrics are logged.
@@ -1208,20 +1145,21 @@
   // The user starts with no wallpaper info and is not controlled by policy.
   WallpaperInfo wallpaper_info;
   EXPECT_FALSE(
-      controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
   EXPECT_FALSE(controller_->IsWallpaperControlledByPolicy(account_id_1));
   // A default wallpaper is shown for the user.
   ClearWallpaperCount();
   controller_->ShowUserWallpaper(account_id_1);
   EXPECT_EQ(1, GetWallpaperCount());
-  EXPECT_EQ(controller_->GetWallpaperType(), WallpaperType::kDefault);
+  ASSERT_EQ(controller_->GetWallpaperType(), WallpaperType::kDefault);
 
   // Set a policy wallpaper. Verify that the user becomes policy controlled and
   // the wallpaper info is updated.
   ClearWallpaperCount();
   controller_->SetPolicyWallpaper(account_id_1, std::string() /*data=*/);
   RunAllTasksUntilIdle();
-  EXPECT_TRUE(controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+  EXPECT_TRUE(
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
   WallpaperInfo policy_wallpaper_info(base::FilePath(wallpaper_files_id_1)
                                           .Append("policy-controlled.jpeg")
                                           .value(),
@@ -1238,7 +1176,7 @@
   ClearWallpaperCount();
   controller_->ShowUserWallpaper(account_id_1);
   EXPECT_EQ(1, GetWallpaperCount());
-  EXPECT_EQ(controller_->GetWallpaperType(), WallpaperType::kPolicy);
+  ASSERT_EQ(controller_->GetWallpaperType(), WallpaperType::kPolicy);
 
   // Clear the wallpaper and log out the user. Verify the policy wallpaper is
   // shown in the login screen.
@@ -1254,7 +1192,8 @@
   ClearWallpaperCount();
   controller_->RemovePolicyWallpaper(account_id_1);
   WaitUntilCustomWallpapersDeleted(account_id_1);
-  EXPECT_TRUE(controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+  EXPECT_TRUE(
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
   WallpaperInfo default_wallpaper_info(
       std::string(), WALLPAPER_LAYOUT_CENTER_CROPPED, WallpaperType::kDefault,
       base::Time::Now().LocalMidnight());
@@ -1288,7 +1227,8 @@
 
   // Verify the wallpaper was set.
   WallpaperInfo wallpaper_info;
-  ASSERT_TRUE(controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+  ASSERT_TRUE(
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
   ASSERT_EQ(WallpaperType::kCustomized, wallpaper_info.type);
   ASSERT_EQ("user1@test.com-hash/user1@test.com-file", wallpaper_info.location);
 
@@ -1333,7 +1273,8 @@
 
   // Verify the wallpaper was set.
   WallpaperInfo wallpaper_info;
-  ASSERT_TRUE(controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+  ASSERT_TRUE(
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
   ASSERT_EQ(WallpaperType::kCustomized, wallpaper_info.type);
   ASSERT_EQ("user1@test.com-hash/user1@test.com-file", wallpaper_info.location);
 
@@ -1360,7 +1301,7 @@
 
     WallpaperInfo wallpaper_info;
     EXPECT_TRUE(
-        controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+        pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
     WallpaperInfo expected_wallpaper_info(
         base::FilePath(wallpaper_files_id_1).Append(file_name_1).value(),
         WALLPAPER_LAYOUT_CENTER, WallpaperType::kCustomized,
@@ -1394,7 +1335,7 @@
   // Verify the user starts with no wallpaper info.
   WallpaperInfo wallpaper_info;
   EXPECT_FALSE(
-      controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
 
   // Set a third-party wallpaper for |kUser1|.
   const WallpaperLayout layout = WALLPAPER_LAYOUT_CENTER;
@@ -1405,7 +1346,8 @@
   // Verify the wallpaper is shown.
   EXPECT_EQ(1, GetWallpaperCount());
   // Verify the user wallpaper info is updated.
-  EXPECT_TRUE(controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+  EXPECT_TRUE(
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
   WallpaperInfo expected_wallpaper_info(
       base::FilePath(wallpaper_files_id_1).Append(file_name_1).value(), layout,
       WallpaperType::kCustomized, base::Time::Now().LocalMidnight());
@@ -1422,7 +1364,8 @@
   EXPECT_EQ(0, GetWallpaperCount());
   // Verify the wallpaper info for |kUser1| is updated, because setting
   // wallpaper is still allowed for non-active users.
-  EXPECT_TRUE(controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+  EXPECT_TRUE(
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
   WallpaperInfo expected_wallpaper_info_2(
       base::FilePath(wallpaper_files_id_1).Append(file_name_2).value(), layout,
       WallpaperType::kCustomized, base::Time::Now().LocalMidnight());
@@ -1446,7 +1389,8 @@
   // info.
   EXPECT_TRUE(controller_->IsWallpaperControlledByPolicy(account_id_2));
   EXPECT_TRUE(controller_->IsActiveUserWallpaperControlledByPolicy());
-  EXPECT_TRUE(controller_->GetUserWallpaperInfo(account_id_2, &wallpaper_info));
+  EXPECT_TRUE(
+      pref_manager_->GetUserWallpaperInfo(account_id_2, &wallpaper_info));
   WallpaperInfo policy_wallpaper_info(base::FilePath(wallpaper_files_id_2)
                                           .Append("policy-controlled.jpeg")
                                           .value(),
@@ -1463,7 +1407,8 @@
   // First, simulate setting a user custom wallpaper.
   SimulateSettingCustomWallpaper(account_id_1);
   WallpaperInfo wallpaper_info;
-  EXPECT_TRUE(controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+  EXPECT_TRUE(
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
   WallpaperInfo default_wallpaper_info(
       std::string(), WALLPAPER_LAYOUT_CENTER_CROPPED, WallpaperType::kDefault,
       base::Time::Now().LocalMidnight());
@@ -1485,7 +1430,8 @@
   EXPECT_EQ(default_wallpaper_dir_.GetPath().Append(kDefaultLargeWallpaperName),
             GetDecodeFilePaths()[0]);
 
-  EXPECT_TRUE(controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+  EXPECT_TRUE(
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
   // The user wallpaper info has been reset to the default value.
   EXPECT_EQ(wallpaper_info, default_wallpaper_info);
 
@@ -1506,7 +1452,8 @@
   EXPECT_EQ(default_wallpaper_dir_.GetPath().Append(kDefaultSmallWallpaperName),
             GetDecodeFilePaths()[0]);
 
-  EXPECT_TRUE(controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+  EXPECT_TRUE(
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
   // The user wallpaper info has been reset to the default value.
   EXPECT_EQ(wallpaper_info, default_wallpaper_info);
 
@@ -1527,7 +1474,8 @@
   EXPECT_EQ(default_wallpaper_dir_.GetPath().Append(kDefaultSmallWallpaperName),
             GetDecodeFilePaths()[0]);
 
-  EXPECT_TRUE(controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+  EXPECT_TRUE(
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
   // The user wallpaper info has been reset to the default value.
   EXPECT_EQ(wallpaper_info, default_wallpaper_info);
 }
@@ -1602,7 +1550,7 @@
   // guest session.
   EXPECT_EQ(1, GetWallpaperCount());
   EXPECT_EQ(controller_->GetWallpaperType(), WallpaperType::kDefault);
-  EXPECT_TRUE(controller_->GetUserWallpaperInfo(guest_id, &wallpaper_info));
+  EXPECT_TRUE(pref_manager_->GetUserWallpaperInfo(guest_id, &wallpaper_info));
   EXPECT_EQ(wallpaper_info, default_wallpaper_info);
   ASSERT_EQ(1u, GetDecodeFilePaths().size());
   EXPECT_EQ(default_wallpaper_dir_.GetPath().Append(kGuestLargeWallpaperName),
@@ -1612,8 +1560,8 @@
   // user and verifying that the policy has been applied successfully.
   WallpaperInfo policy_wallpaper_info;
   controller_->SetPolicyWallpaper(account_id_1, /*data=*/std::string());
-  EXPECT_TRUE(
-      controller_->GetUserWallpaperInfo(account_id_1, &policy_wallpaper_info));
+  EXPECT_TRUE(pref_manager_->GetUserWallpaperInfo(account_id_1,
+                                                  &policy_wallpaper_info));
   WallpaperInfo expected_policy_wallpaper_info(
       base::FilePath(wallpaper_files_id_1)
           .Append("policy-controlled.jpeg")
@@ -1628,7 +1576,7 @@
 
   EXPECT_EQ(1, GetWallpaperCount());
   EXPECT_EQ(controller_->GetWallpaperType(), WallpaperType::kDefault);
-  EXPECT_TRUE(controller_->GetUserWallpaperInfo(guest_id, &wallpaper_info));
+  EXPECT_TRUE(pref_manager_->GetUserWallpaperInfo(guest_id, &wallpaper_info));
   EXPECT_EQ(wallpaper_info, default_wallpaper_info);
   ASSERT_EQ(1u, GetDecodeFilePaths().size());
   EXPECT_EQ(default_wallpaper_dir_.GetPath().Append(kGuestLargeWallpaperName),
@@ -1642,7 +1590,8 @@
   SimulateUserLogin(account_id_1);
   SimulateSettingCustomWallpaper(account_id_1);
   WallpaperInfo wallpaper_info;
-  EXPECT_TRUE(controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+  EXPECT_TRUE(
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
   WallpaperInfo default_wallpaper_info(
       std::string(), WALLPAPER_LAYOUT_CENTER_CROPPED, WallpaperType::kDefault,
       base::Time::Now().LocalMidnight());
@@ -1664,7 +1613,7 @@
   RunAllTasksUntilIdle();
   EXPECT_EQ(1, GetWallpaperCount());
   EXPECT_EQ(controller_->GetWallpaperType(), WallpaperType::kDefault);
-  EXPECT_TRUE(controller_->GetUserWallpaperInfo(guest_id, &wallpaper_info));
+  EXPECT_TRUE(pref_manager_->GetUserWallpaperInfo(guest_id, &wallpaper_info));
   EXPECT_EQ(wallpaper_info, default_wallpaper_info);
   ASSERT_EQ(1u, GetDecodeFilePaths().size());
   EXPECT_EQ(default_wallpaper_dir_.GetPath().Append(kGuestLargeWallpaperName),
@@ -1691,7 +1640,8 @@
   // First, simulate setting a user custom wallpaper.
   SimulateSettingCustomWallpaper(account_id_1);
   WallpaperInfo wallpaper_info;
-  EXPECT_TRUE(controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+  EXPECT_TRUE(
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
   EXPECT_NE(wallpaper_info.type, WallpaperType::kDefault);
 
   TestWallpaperControllerObserver observer(controller_);
@@ -1726,7 +1676,7 @@
   EXPECT_EQ(0, GetWallpaperCount());
   WallpaperInfo wallpaper_info;
   EXPECT_FALSE(
-      controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
 
   // Verify that |SetOnlineWallpaperFromData| doesn't set wallpaper in kiosk
   // mode, and |account_id_1|'s wallpaper info is not updated.
@@ -1747,7 +1697,7 @@
   run_loop->Run();
   EXPECT_EQ(0, GetWallpaperCount());
   EXPECT_FALSE(
-      controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
 
   // Verify that |SetDefaultWallpaper| doesn't set wallpaper in kiosk mode, and
   // |account_id_1|'s wallpaper info is not updated.
@@ -1757,7 +1707,7 @@
   RunAllTasksUntilIdle();
   EXPECT_EQ(0, GetWallpaperCount());
   EXPECT_FALSE(
-      controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
 }
 
 // Disable the wallpaper setting for public session since it is ephemeral.
@@ -1794,7 +1744,7 @@
     RunAllTasksUntilIdle();
     EXPECT_EQ(0, GetWallpaperCount());
     EXPECT_TRUE(
-        controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+        pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
     EXPECT_EQ(wallpaper_info, policy_wallpaper_info);
   }
 
@@ -1813,7 +1763,7 @@
     run_loop->Run();
     EXPECT_EQ(0, GetWallpaperCount());
     EXPECT_TRUE(
-        controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+        pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
     EXPECT_EQ(wallpaper_info, policy_wallpaper_info);
   }
 
@@ -1837,7 +1787,7 @@
     run_loop->Run();
     EXPECT_EQ(0, GetWallpaperCount());
     EXPECT_TRUE(
-        controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+        pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
     EXPECT_EQ(wallpaper_info, policy_wallpaper_info);
   }
 
@@ -1860,7 +1810,7 @@
     run_loop->Run();
     EXPECT_EQ(0, GetWallpaperCount());
     EXPECT_TRUE(
-        controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+        pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
     EXPECT_EQ(wallpaper_info, policy_wallpaper_info);
   }
 
@@ -1873,7 +1823,7 @@
     RunAllTasksUntilIdle();
     EXPECT_EQ(0, GetWallpaperCount());
     EXPECT_TRUE(
-        controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+        pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
     EXPECT_EQ(wallpaper_info, policy_wallpaper_info);
   }
 }
@@ -2108,7 +2058,8 @@
   EXPECT_EQ(1, GetWallpaperCount());
   EXPECT_EQ(controller_->GetWallpaperLayout(), layout);
   WallpaperInfo wallpaper_info;
-  EXPECT_TRUE(controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+  EXPECT_TRUE(
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
   WallpaperInfo expected_custom_wallpaper_info(
       base::FilePath(wallpaper_files_id_1).Append(file_name_1).value(), layout,
       WallpaperType::kCustomized, base::Time::Now().LocalMidnight());
@@ -2121,7 +2072,8 @@
   RunAllTasksUntilIdle();
   EXPECT_EQ(1, GetWallpaperCount());
   EXPECT_EQ(controller_->GetWallpaperLayout(), new_layout);
-  EXPECT_TRUE(controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+  EXPECT_TRUE(
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
   expected_custom_wallpaper_info.layout = new_layout;
   EXPECT_EQ(wallpaper_info, expected_custom_wallpaper_info);
 
@@ -2144,7 +2096,7 @@
               WallpaperType::kOnceGooglePhotos);
     EXPECT_EQ(controller_->GetWallpaperLayout(), layout);
     EXPECT_TRUE(
-        controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+        pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
     EXPECT_EQ(wallpaper_info,
               WallpaperInfo(GooglePhotosWallpaperParams(
                   account_id_1, "id", /*daily_refresh_enabled=*/false, layout,
@@ -2158,7 +2110,7 @@
     EXPECT_EQ(1, GetWallpaperCount());
     EXPECT_EQ(controller_->GetWallpaperLayout(), new_layout);
     EXPECT_TRUE(
-        controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+        pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
     EXPECT_EQ(wallpaper_info,
               WallpaperInfo(GooglePhotosWallpaperParams(
                   account_id_1, "id", /*daily_refresh_enabled=*/false,
@@ -2182,7 +2134,8 @@
   EXPECT_EQ(1, GetWallpaperCount());
   EXPECT_EQ(controller_->GetWallpaperType(), WallpaperType::kOnline);
   EXPECT_EQ(controller_->GetWallpaperLayout(), layout);
-  EXPECT_TRUE(controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+  EXPECT_TRUE(
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
   WallpaperInfo expected_online_wallpaper_info(
       kDummyUrl, layout, WallpaperType::kOnline,
       base::Time::Now().LocalMidnight());
@@ -2196,7 +2149,8 @@
   EXPECT_EQ(0, GetWallpaperCount());
   EXPECT_EQ(controller_->GetWallpaperLayout(), layout);
   // The saved wallpaper info is not updated.
-  EXPECT_TRUE(controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+  EXPECT_TRUE(
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
   EXPECT_EQ(wallpaper_info, expected_online_wallpaper_info);
 }
 
@@ -2518,7 +2472,7 @@
       std::string(), WALLPAPER_LAYOUT_CENTER_CROPPED, WallpaperType::kDefault,
       base::Time::Now().LocalMidnight());
   EXPECT_TRUE(
-      controller_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
   EXPECT_EQ(user_wallpaper_info, default_wallpaper_info);
 
   // Simulate opening the wallpaper picker window.
@@ -2542,7 +2496,7 @@
   EXPECT_EQ(kWallpaperColor, GetWallpaperColor());
   // Verify that the user wallpaper info remains unchanged during the preview.
   EXPECT_TRUE(
-      controller_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
   EXPECT_EQ(user_wallpaper_info, default_wallpaper_info);
 
   // Now enter overview mode. Verify the wallpaper changes back to the default,
@@ -2572,7 +2526,7 @@
       std::string(), WALLPAPER_LAYOUT_CENTER_CROPPED, WallpaperType::kDefault,
       base::Time::Now().LocalMidnight());
   EXPECT_TRUE(
-      controller_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
   EXPECT_EQ(user_wallpaper_info, default_wallpaper_info);
 
   // Simulate opening the wallpaper picker window.
@@ -2596,7 +2550,7 @@
   EXPECT_EQ(kWallpaperColor, GetWallpaperColor());
   // Verify that the user wallpaper info remains unchanged during the preview.
   EXPECT_TRUE(
-      controller_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
   EXPECT_EQ(user_wallpaper_info, default_wallpaper_info);
 
   // Now start window cycle. Verify the wallpaper changes back to the default,
@@ -2627,7 +2581,7 @@
       std::string(), WALLPAPER_LAYOUT_CENTER_CROPPED, WallpaperType::kDefault,
       base::Time::Now().LocalMidnight());
   EXPECT_TRUE(
-      controller_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
   EXPECT_EQ(user_wallpaper_info, default_wallpaper_info);
 
   // Simulate opening the wallpaper picker window.
@@ -2651,7 +2605,7 @@
   EXPECT_EQ(kWallpaperColor, GetWallpaperColor());
   // Verify that the user wallpaper info remains unchanged during the preview.
   EXPECT_TRUE(
-      controller_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
   EXPECT_EQ(user_wallpaper_info, default_wallpaper_info);
 
   // Now switch to another user. Verify the wallpaper changes back to the
@@ -2665,7 +2619,7 @@
   EXPECT_NE(kWallpaperColor, GetWallpaperColor());
   EXPECT_EQ(controller_->GetWallpaperType(), WallpaperType::kDefault);
   EXPECT_TRUE(
-      controller_->GetUserWallpaperInfo(account_id_2, &user_wallpaper_info));
+      pref_manager_->GetUserWallpaperInfo(account_id_2, &user_wallpaper_info));
   EXPECT_EQ(user_wallpaper_info, default_wallpaper_info);
 }
 
@@ -2682,7 +2636,7 @@
       std::string(), WALLPAPER_LAYOUT_CENTER_CROPPED, WallpaperType::kDefault,
       base::Time::Now().LocalMidnight());
   EXPECT_TRUE(
-      controller_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
   EXPECT_EQ(user_wallpaper_info, default_wallpaper_info);
 
   // Simulate opening the wallpaper picker window.
@@ -2703,7 +2657,7 @@
   EXPECT_EQ(kWallpaperColor, GetWallpaperColor());
   // Verify that the user wallpaper info remains unchanged during the preview.
   EXPECT_TRUE(
-      controller_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
   EXPECT_EQ(user_wallpaper_info, default_wallpaper_info);
   histogram_tester().ExpectTotalCount("Ash.Wallpaper.Preview.Show", 1);
 
@@ -2721,7 +2675,7 @@
       base::FilePath(wallpaper_files_id_1).Append(file_name_1).value(), layout,
       WallpaperType::kCustomized, base::Time::Now().LocalMidnight());
   EXPECT_TRUE(
-      controller_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
   EXPECT_EQ(user_wallpaper_info, custom_wallpaper_info);
 
   // Set an empty online wallpaper for the user, verify it fails.
@@ -2761,7 +2715,7 @@
   EXPECT_EQ(online_wallpaper_color, GetWallpaperColor());
   // Verify that the user wallpaper info remains unchanged during the preview.
   EXPECT_TRUE(
-      controller_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
   EXPECT_EQ(user_wallpaper_info, custom_wallpaper_info);
 
   // Now confirm the preview wallpaper, verify that there's no wallpaper change
@@ -2782,7 +2736,7 @@
       /*variants=*/
       std::vector<OnlineWallpaperVariant>()));
   EXPECT_TRUE(
-      controller_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
   EXPECT_EQ(user_wallpaper_info, online_wallpaper_info);
 }
 
@@ -2799,7 +2753,7 @@
       std::string(), WALLPAPER_LAYOUT_CENTER_CROPPED, WallpaperType::kDefault,
       base::Time::Now().LocalMidnight());
   EXPECT_TRUE(
-      controller_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
   EXPECT_EQ(user_wallpaper_info, default_wallpaper_info);
 
   // Simulate opening the wallpaper picker window.
@@ -2820,7 +2774,7 @@
   EXPECT_EQ(kWallpaperColor, GetWallpaperColor());
   // Verify that the user wallpaper info remains unchanged during the preview.
   EXPECT_TRUE(
-      controller_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
   EXPECT_EQ(user_wallpaper_info, default_wallpaper_info);
 
   // Now cancel the preview. Verify the wallpaper changes back to the default
@@ -2851,7 +2805,7 @@
   EXPECT_EQ(online_wallpaper_color, GetWallpaperColor());
   // Verify that the user wallpaper info remains unchanged during the preview.
   EXPECT_TRUE(
-      controller_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
   EXPECT_EQ(user_wallpaper_info, default_wallpaper_info);
 
   // Now cancel the preview. Verify the wallpaper changes back to the default
@@ -2878,7 +2832,7 @@
       std::string(), WALLPAPER_LAYOUT_CENTER_CROPPED, WallpaperType::kDefault,
       base::Time::Now().LocalMidnight());
   EXPECT_TRUE(
-      controller_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
   EXPECT_EQ(user_wallpaper_info, default_wallpaper_info);
 
   // Simulate opening the wallpaper picker window.
@@ -2899,7 +2853,7 @@
   EXPECT_EQ(kWallpaperColor, GetWallpaperColor());
   // Verify that the user wallpaper info remains unchanged during the preview.
   EXPECT_TRUE(
-      controller_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
   EXPECT_EQ(user_wallpaper_info, default_wallpaper_info);
 
   // Now set another custom wallpaper for the user and disable preview (this
@@ -2921,7 +2875,7 @@
       base::FilePath(wallpaper_files_id_1).Append(file_name_2).value(), layout,
       WallpaperType::kCustomized, base::Time::Now().LocalMidnight());
   EXPECT_TRUE(
-      controller_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
   EXPECT_EQ(user_wallpaper_info, synced_custom_wallpaper_info);
 
   // Now cancel the preview. Verify the synced custom wallpaper is shown instead
@@ -2933,7 +2887,7 @@
   EXPECT_EQ(1, GetWallpaperCount());
   EXPECT_EQ(synced_custom_wallpaper_color, GetWallpaperColor());
   EXPECT_TRUE(
-      controller_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
   EXPECT_EQ(user_wallpaper_info, synced_custom_wallpaper_info);
 
   // Repeat the above steps for online wallpapers: set a online wallpaper for
@@ -2954,7 +2908,7 @@
   EXPECT_EQ(kWallpaperColor, GetWallpaperColor());
   // Verify that the user wallpaper info remains unchanged during the preview.
   EXPECT_TRUE(
-      controller_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
   EXPECT_EQ(user_wallpaper_info, synced_custom_wallpaper_info);
 
   // Now set another online wallpaper for the user and disable preview. Verify
@@ -2984,7 +2938,7 @@
           /*variants=*/
           std::vector<OnlineWallpaperVariant>()));
   EXPECT_TRUE(
-      controller_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
   EXPECT_EQ(user_wallpaper_info, synced_online_wallpaper_info);
 
   // Now cancel the preview. Verify the synced online wallpaper is shown instead
@@ -2996,7 +2950,7 @@
   EXPECT_EQ(1, GetWallpaperCount());
   EXPECT_EQ(synced_online_wallpaper_color, GetWallpaperColor());
   EXPECT_TRUE(
-      controller_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
   EXPECT_EQ(user_wallpaper_info, synced_online_wallpaper_info);
 }
 
@@ -3054,7 +3008,8 @@
       base::FilePath(wallpaper_files_id_1).Append(file_name_1).value(), layout,
       WallpaperType::kCustomized, base::Time::Now().LocalMidnight());
   WallpaperInfo wallpaper_info;
-  EXPECT_TRUE(controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+  EXPECT_TRUE(
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
   EXPECT_EQ(expected_wallpaper_info, wallpaper_info);
 
   // Show a one-shot wallpaper. Verify it is shown successfully.
@@ -3082,7 +3037,8 @@
 
   // Verify the user wallpaper info is unaffected, and the one-shot wallpaper
   // can be replaced by the user wallpaper.
-  EXPECT_TRUE(controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+  EXPECT_TRUE(
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
   EXPECT_EQ(expected_wallpaper_info, wallpaper_info);
   ClearWallpaperCount();
   controller_->ShowUserWallpaper(account_id_1);
@@ -3277,40 +3233,6 @@
                    ->is_animating());
 }
 
-TEST_F(WallpaperControllerTest, GetWallpaperInfo) {
-  WallpaperInfo expected_info = InfoWithType(WallpaperType::kDaily);
-  controller_->SetUserWallpaperInfo(account_id_1, expected_info);
-
-  WallpaperInfo actual_info;
-  EXPECT_TRUE(controller_->GetUserWallpaperInfo(account_id_1, &actual_info));
-  EXPECT_EQ(expected_info, actual_info);
-}
-
-TEST_F(WallpaperControllerTest, GetWallpaperInfoNothingToGet) {
-  WallpaperInfo info;
-  EXPECT_FALSE(controller_->GetUserWallpaperInfo(account_id_1, &info));
-}
-
-TEST_F(WallpaperControllerTest, SetWallpaperInfoLocal) {
-  WallpaperInfo info(
-      GetDummyFileName(account_id_1), WALLPAPER_LAYOUT_CENTER_CROPPED,
-      WallpaperType::kThirdParty, base::Time::Now().LocalMidnight());
-  EXPECT_TRUE(controller_->SetUserWallpaperInfo(account_id_1, info));
-  AssertWallpaperInfoInPrefs(GetLocalPrefService(), prefs::kUserWallpaperInfo,
-                             account_id_1, info);
-}
-
-TEST_F(WallpaperControllerTest, SetWallpaperInfoLocalFromGooglePhotos) {
-  WallpaperInfo info(
-      GooglePhotosWallpaperParams{account_id_1, kFakeGooglePhotosPhotoId,
-                                  /*daily_refresh_enabled=*/false,
-                                  WallpaperLayout::WALLPAPER_LAYOUT_STRETCH,
-                                  /*preview_mode=*/false, "dedup_key"});
-  EXPECT_TRUE(controller_->SetUserWallpaperInfo(account_id_1, info));
-  AssertWallpaperInfoInPrefs(GetLocalPrefService(), prefs::kUserWallpaperInfo,
-                             account_id_1, info);
-}
-
 TEST_F(WallpaperControllerTest, SetCustomWallpaper) {
   gfx::ImageSkia image = CreateImage(640, 480, kWallpaperColor);
   WallpaperLayout layout = WALLPAPER_LAYOUT_CENTER;
@@ -3327,7 +3249,8 @@
   EXPECT_EQ(kWallpaperColor, GetWallpaperColor());
   EXPECT_EQ(controller_->GetWallpaperType(), WallpaperType::kCustomized);
   WallpaperInfo wallpaper_info;
-  EXPECT_TRUE(controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+  EXPECT_TRUE(
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
   WallpaperInfo expected_wallpaper_info(
       base::FilePath(wallpaper_files_id_1).Append(file_name_1).value(), layout,
       WallpaperType::kCustomized, base::Time::Now().LocalMidnight());
@@ -3346,7 +3269,8 @@
   RunAllTasksUntilIdle();
   EXPECT_EQ(0, GetWallpaperCount());
   EXPECT_EQ(kWallpaperColor, GetWallpaperColor());
-  EXPECT_TRUE(controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+  EXPECT_TRUE(
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
   EXPECT_EQ(wallpaper_info, expected_wallpaper_info);
 
   // Verify the updated wallpaper is shown after |kUser1| becomes active again.
@@ -3358,53 +3282,6 @@
   EXPECT_EQ(custom_wallpaper_color, GetWallpaperColor());
 }
 
-TEST_F(WallpaperControllerTest, SetWallpaperInfoSynced) {
-  WallpaperInfo info = InfoWithType(WallpaperType::kOnline);
-  EXPECT_TRUE(controller_->SetUserWallpaperInfo(account_id_1, info));
-  AssertWallpaperInfoInPrefs(GetProfilePrefService(account_id_1),
-                             prefs::kSyncableWallpaperInfo, account_id_1, info);
-}
-
-TEST_F(WallpaperControllerTest, SetWallpaperInfoSyncedFromGooglePhotos) {
-  WallpaperInfo info = InfoWithType(WallpaperType::kOnceGooglePhotos);
-  EXPECT_TRUE(controller_->SetUserWallpaperInfo(account_id_1, info));
-  AssertWallpaperInfoInPrefs(GetProfilePrefService(account_id_1),
-                             prefs::kSyncableWallpaperInfo, account_id_1, info);
-}
-
-TEST_F(WallpaperControllerTest, SetWallpaperInfoSyncDisabled) {
-  client_.set_wallpaper_sync_enabled(false);
-
-  WallpaperInfo expected_info = InfoWithType(WallpaperType::kCustomized);
-  PutWallpaperInfoInPrefs(account_id_1, expected_info,
-                          GetProfilePrefService(account_id_1),
-                          prefs::kSyncableWallpaperInfo);
-
-  WallpaperInfo info = InfoWithType(WallpaperType::kOnline);
-  EXPECT_TRUE(controller_->SetUserWallpaperInfo(account_id_1, info));
-
-  AssertWallpaperInfoInPrefs(GetProfilePrefService(account_id_1),
-                             prefs::kSyncableWallpaperInfo, account_id_1,
-                             expected_info);
-}
-
-TEST_F(WallpaperControllerTest, SetWallpaperInfoCustom) {
-  WallpaperInfo synced_info = InfoWithType(WallpaperType::kOnline);
-  PutWallpaperInfoInPrefs(account_id_1, synced_info,
-                          GetProfilePrefService(account_id_1),
-                          prefs::kSyncableWallpaperInfo);
-
-  WallpaperInfo info = InfoWithType(WallpaperType::kCustomized);
-  EXPECT_TRUE(controller_->SetUserWallpaperInfo(account_id_1, info));
-
-  // Custom wallpaper infos should not be propagated to synced preferences until
-  // the image is uploaded to drivefs. That is not done in
-  // |SetUserWallpaperInfo|.
-  AssertWallpaperInfoInPrefs(GetProfilePrefService(account_id_1),
-                             prefs::kSyncableWallpaperInfo, account_id_1,
-                             synced_info);
-}
-
 TEST_F(WallpaperControllerTest, OldOnlineInfoSynced_Discarded) {
   // Create a dictionary that looks like the preference from crrev.com/a040384.
   // DO NOT CHANGE as there are preferences like this in production.
@@ -3434,27 +3311,25 @@
 
   // Unmigrated synced wallpaper info are discarded.
   WallpaperInfo actual;
-  EXPECT_FALSE(controller_->GetUserWallpaperInfo(account_id_1, &actual));
+  EXPECT_FALSE(pref_manager_->GetUserWallpaperInfo(account_id_1, &actual));
 }
 
-TEST_F(WallpaperControllerTest, MigrateWallpaperInfo) {
+TEST_F(WallpaperControllerTest, MigrateWallpaperInfo_Online) {
   WallpaperInfo expected_info = InfoWithType(WallpaperType::kOnline);
-  PutWallpaperInfoInPrefs(account_id_1, expected_info, GetLocalPrefService(),
-                          prefs::kUserWallpaperInfo);
+  pref_manager_->SetLocalWallpaperInfo(account_id_1, expected_info);
   SimulateUserLogin(account_id_1);
-  AssertWallpaperInfoInPrefs(GetProfilePrefService(account_id_1),
-                             prefs::kSyncableWallpaperInfo, account_id_1,
-                             expected_info);
+  WallpaperInfo info;
+  ASSERT_TRUE(pref_manager_->GetSyncedWallpaperInfo(account_id_1, &info));
+  EXPECT_EQ(expected_info, info);
 }
 
 TEST_F(WallpaperControllerTest, MigrateWallpaperInfoCustomized) {
   WallpaperInfo expected_info = InfoWithType(WallpaperType::kCustomized);
-  PutWallpaperInfoInPrefs(account_id_1, expected_info, GetLocalPrefService(),
-                          prefs::kUserWallpaperInfo);
+  pref_manager_->SetLocalWallpaperInfo(account_id_1, expected_info);
   SimulateUserLogin(account_id_1);
-  AssertWallpaperInfoInPrefs(GetProfilePrefService(account_id_1),
-                             prefs::kSyncableWallpaperInfo, account_id_1,
-                             expected_info);
+  WallpaperInfo info;
+  ASSERT_TRUE(pref_manager_->GetSyncedWallpaperInfo(account_id_1, &info));
+  EXPECT_EQ(expected_info, info);
 }
 
 TEST_F(WallpaperControllerTest, MigrateWallpaperInfoDaily) {
@@ -3464,12 +3339,11 @@
       WALLPAPER_LAYOUT_CENTER, /*preview_mode=*/false, /*from_user=*/false,
       /*daily_refresh_enabled=*/false, kUnitId,
       std::vector<OnlineWallpaperVariant>()));
-  PutWallpaperInfoInPrefs(account_id_1, expected_info, GetLocalPrefService(),
-                          prefs::kUserWallpaperInfo);
+  pref_manager_->SetLocalWallpaperInfo(account_id_1, expected_info);
   SimulateUserLogin(account_id_1);
-  AssertWallpaperInfoInPrefs(GetProfilePrefService(account_id_1),
-                             prefs::kSyncableWallpaperInfo, account_id_1,
-                             expected_info);
+  WallpaperInfo info;
+  ASSERT_TRUE(pref_manager_->GetSyncedWallpaperInfo(account_id_1, &info));
+  EXPECT_EQ(expected_info, info);
   EXPECT_EQ(client_.migrate_collection_id_from_chrome_app_count(), 1u);
 }
 
@@ -3489,15 +3363,13 @@
       WALLPAPER_LAYOUT_CENTER, /*preview_mode=*/false, /*from_user=*/false,
       /*daily_refresh_enabled=*/false, kUnitId,
       std::vector<OnlineWallpaperVariant>()));
-  PutWallpaperInfoInPrefs(account_id_1, local_info, GetLocalPrefService(),
-                          prefs::kUserWallpaperInfo);
-  PutWallpaperInfoInPrefs(account_id_1, synced_info,
-                          GetProfilePrefService(account_id_1),
-                          prefs::kSyncableWallpaperInfo);
+  pref_manager_->SetLocalWallpaperInfo(account_id_1, local_info);
+  pref_manager_->SetSyncedWallpaperInfo(account_id_1, synced_info);
   SimulateUserLogin(account_id_1);
-  AssertWallpaperInfoInPrefs(GetProfilePrefService(account_id_1),
-                             prefs::kSyncableWallpaperInfo, account_id_1,
-                             synced_info);
+  WallpaperInfo info;
+  ASSERT_TRUE(pref_manager_->GetSyncedWallpaperInfo(account_id_1, &info));
+  // Synced info should be the same if local is the same age.
+  EXPECT_EQ(synced_info, info);
 }
 
 TEST_F(WallpaperControllerTest,
@@ -3508,14 +3380,11 @@
                                WallpaperType::kOnline, base::Time::Now()};
   synced_info.asset_id = kAssetId;
   synced_info.collection_id = TestWallpaperControllerClient::kDummyCollectionId;
-  PutWallpaperInfoInPrefs(account_id_1, synced_info,
-                          GetProfilePrefService(account_id_1),
-                          prefs::kSyncableWallpaperInfo);
+  pref_manager_->SetSyncedWallpaperInfo(account_id_1, synced_info);
 
   WallpaperInfo local_info = InfoWithType(WallpaperType::kThirdParty);
   local_info.date = DayBeforeYesterdayish();
-  PutWallpaperInfoInPrefs(account_id_1, local_info, GetLocalPrefService(),
-                          prefs::kUserWallpaperInfo);
+  pref_manager_->SetLocalWallpaperInfo(account_id_1, local_info);
 
   client_.ResetCounts();
 
@@ -3523,7 +3392,7 @@
       GetProfilePrefService(account_id_1));
   RunAllTasksUntilIdle();
   WallpaperInfo actual_info;
-  EXPECT_TRUE(controller_->GetUserWallpaperInfo(account_id_1, &actual_info));
+  EXPECT_TRUE(pref_manager_->GetUserWallpaperInfo(account_id_1, &actual_info));
   EXPECT_EQ(WallpaperType::kOnline, actual_info.type);
 }
 
@@ -3533,14 +3402,11 @@
                                WallpaperType::kOnline, base::Time::Now()};
   synced_info.asset_id = kAssetId;
   synced_info.collection_id = TestWallpaperControllerClient::kDummyCollectionId;
-  PutWallpaperInfoInPrefs(account_id_1, synced_info,
-                          GetProfilePrefService(account_id_1),
-                          prefs::kSyncableWallpaperInfo);
+  pref_manager_->SetSyncedWallpaperInfo(account_id_1, synced_info);
 
   WallpaperInfo local_info = InfoWithType(WallpaperType::kThirdParty);
   local_info.date = DayBeforeYesterdayish();
-  PutWallpaperInfoInPrefs(account_id_1, local_info, GetLocalPrefService(),
-                          prefs::kUserWallpaperInfo);
+  pref_manager_->SetLocalWallpaperInfo(account_id_1, local_info);
 
   client_.ResetCounts();
 
@@ -3549,25 +3415,23 @@
   controller_->OnActiveUserPrefServiceChanged(
       GetProfilePrefService(account_id_1));
   WallpaperInfo actual_info;
-  EXPECT_TRUE(controller_->GetUserWallpaperInfo(account_id_1, &actual_info));
+  EXPECT_TRUE(pref_manager_->GetUserWallpaperInfo(account_id_1, &actual_info));
   EXPECT_EQ(WallpaperType::kThirdParty, actual_info.type);
 }
 
 TEST_F(WallpaperControllerTest, HandleWallpaperInfoSyncedLocalIsPolicy) {
   CacheOnlineWallpaper(kDummyUrl);
-  PutWallpaperInfoInPrefs(account_id_1, InfoWithType(WallpaperType::kPolicy),
-                          GetLocalPrefService(), prefs::kUserWallpaperInfo);
+  pref_manager_->SetLocalWallpaperInfo(account_id_1,
+                                       InfoWithType(WallpaperType::kPolicy));
 
   SimulateUserLogin(account_id_1);
   WallpaperInfo synced_info = {kDummyUrl, WALLPAPER_LAYOUT_CENTER_CROPPED,
                                WallpaperType::kOnline, base::Time::Now()};
-  PutWallpaperInfoInPrefs(account_id_1, synced_info,
-                          GetProfilePrefService(account_id_1),
-                          prefs::kSyncableWallpaperInfo);
+  pref_manager_->SetSyncedWallpaperInfo(account_id_1, synced_info);
   RunAllTasksUntilIdle();
 
   WallpaperInfo actual_info;
-  EXPECT_TRUE(controller_->GetUserWallpaperInfo(account_id_1, &actual_info));
+  EXPECT_TRUE(pref_manager_->GetUserWallpaperInfo(account_id_1, &actual_info));
   EXPECT_NE(WallpaperType::kOnline, actual_info.type);
 }
 
@@ -3577,44 +3441,36 @@
 
   WallpaperInfo local_info = InfoWithType(WallpaperType::kThirdParty);
   local_info.date = DayBeforeYesterdayish();
-  PutWallpaperInfoInPrefs(account_id_1, local_info, GetLocalPrefService(),
-                          prefs::kUserWallpaperInfo);
+  pref_manager_->SetLocalWallpaperInfo(account_id_1, local_info);
 
   SimulateUserLogin(account_id_1);
   WallpaperInfo synced_info = {kDummyUrl, WALLPAPER_LAYOUT_CENTER_CROPPED,
                                WallpaperType::kOnline, base::Time::Now()};
   synced_info.asset_id = kAssetId;
   synced_info.collection_id = TestWallpaperControllerClient::kDummyCollectionId;
-  PutWallpaperInfoInPrefs(account_id_1, synced_info,
-                          GetProfilePrefService(account_id_1),
-                          prefs::kSyncableWallpaperInfo);
+  pref_manager_->SetSyncedWallpaperInfo(account_id_1, synced_info);
   RunAllTasksUntilIdle();
 
   WallpaperInfo actual_info;
-  EXPECT_TRUE(controller_->GetUserWallpaperInfo(account_id_1, &actual_info));
+  EXPECT_TRUE(pref_manager_->GetUserWallpaperInfo(account_id_1, &actual_info));
   EXPECT_EQ(WallpaperType::kOnline, actual_info.type);
 }
 
 TEST_F(WallpaperControllerTest,
        HandleWallpaperInfoSyncedLocalIsThirdPartyAndNewer) {
   CacheOnlineWallpaper(kDummyUrl);
-  PutWallpaperInfoInPrefs(account_id_1,
-                          InfoWithType(WallpaperType::kThirdParty),
-                          GetLocalPrefService(), prefs::kUserWallpaperInfo);
+  pref_manager_->SetLocalWallpaperInfo(
+      account_id_1, InfoWithType(WallpaperType::kThirdParty));
 
   WallpaperInfo synced_info = {kDummyUrl, WALLPAPER_LAYOUT_CENTER_CROPPED,
                                WallpaperType::kOnline, DayBeforeYesterdayish()};
-  PutWallpaperInfoInPrefs(account_id_1, synced_info,
-                          GetProfilePrefService(account_id_1),
-                          prefs::kSyncableWallpaperInfo);
+  pref_manager_->SetSyncedWallpaperInfo(account_id_1, synced_info);
   SimulateUserLogin(account_id_1);
-  PutWallpaperInfoInPrefs(account_id_1, synced_info,
-                          GetProfilePrefService(account_id_1),
-                          prefs::kSyncableWallpaperInfo);
+  pref_manager_->SetSyncedWallpaperInfo(account_id_1, synced_info);
   RunAllTasksUntilIdle();
 
   WallpaperInfo actual_info;
-  EXPECT_TRUE(controller_->GetUserWallpaperInfo(account_id_1, &actual_info));
+  EXPECT_TRUE(pref_manager_->GetUserWallpaperInfo(account_id_1, &actual_info));
   EXPECT_EQ(WallpaperType::kThirdParty, actual_info.type);
 }
 
@@ -3631,9 +3487,8 @@
       WALLPAPER_LAYOUT_CENTER, /*preview_mode=*/false, /*from_user=*/false,
       /*daily_refresh_enabled=*/false, kUnitId,
       std::vector<OnlineWallpaperVariant>()));
-  PutWallpaperInfoInPrefs(account_id_1, info,
-                          GetProfilePrefService(account_id_1),
-                          prefs::kSyncableWallpaperInfo);
+  pref_manager_->SetSyncedWallpaperInfo(account_id_1, info);
+
   RunAllTasksUntilIdle();
   EXPECT_EQ(1, GetWallpaperCount());
   EXPECT_EQ(controller_->GetWallpaperType(), WallpaperType::kOnline);
@@ -3649,9 +3504,8 @@
   // it succeeds this time because |SetOnlineWallpaperFromData| has saved the
   // file.
   ClearWallpaperCount();
-  PutWallpaperInfoInPrefs(account_id_1, InfoWithType(WallpaperType::kOnline),
-                          GetProfilePrefService(account_id_1),
-                          prefs::kSyncableWallpaperInfo);
+  pref_manager_->SetSyncedWallpaperInfo(account_id_1,
+                                        InfoWithType(WallpaperType::kOnline));
   RunAllTasksUntilIdle();
   EXPECT_EQ(0, GetWallpaperCount());
   EXPECT_NE(controller_->GetWallpaperType(), WallpaperType::kOnline);
@@ -3665,7 +3519,7 @@
                         WallpaperType::kDaily, DayBeforeYesterdayish()};
   info.asset_id = kAssetId;
   info.collection_id = expected;
-  controller_->SetUserWallpaperInfo(account_id_1, info);
+  pref_manager_->SetUserWallpaperInfo(account_id_1, info);
 
   controller_->UpdateDailyRefreshWallpaperForTesting();
   EXPECT_EQ(expected, client_.get_fetch_daily_refresh_wallpaper_param());
@@ -3684,7 +3538,7 @@
       /*daily_refresh_enabled=*/true, /*unit_id=*/absl::nullopt,
       /*variants=*/std::vector<OnlineWallpaperVariant>()));
   info.date = DayBeforeYesterdayish();
-  controller_->SetUserWallpaperInfo(account_id_1, info);
+  pref_manager_->SetUserWallpaperInfo(account_id_1, info);
 
   ClearLogin();
   SimulateUserLogin(account_id_1);
@@ -3702,7 +3556,7 @@
   WallpaperInfo info = {std::string(), WALLPAPER_LAYOUT_CENTER,
                         WallpaperType::kOnline, DayBeforeYesterdayish()};
   info.collection_id = "fun_collection";
-  controller_->SetUserWallpaperInfo(account_id_1, info);
+  pref_manager_->SetUserWallpaperInfo(account_id_1, info);
 
   controller_->UpdateDailyRefreshWallpaperForTesting();
   EXPECT_EQ(std::string(), client_.get_fetch_daily_refresh_wallpaper_param());
@@ -3710,7 +3564,7 @@
 
 TEST_F(WallpaperControllerTest, UpdateDailyRefreshWallpaper_NoCollectionId) {
   SimulateUserLogin(account_id_1);
-  controller_->SetUserWallpaperInfo(
+  pref_manager_->SetUserWallpaperInfo(
       account_id_1,
       WallpaperInfo(std::string(), WALLPAPER_LAYOUT_CENTER,
                     WallpaperType::kDaily, DayBeforeYesterdayish()));
@@ -3728,7 +3582,7 @@
                         WallpaperType::kDaily,
                         base::Time::Now().LocalMidnight()};
   info.collection_id = "fun_collection";
-  controller_->SetUserWallpaperInfo(account_id_1, info);
+  pref_manager_->SetUserWallpaperInfo(account_id_1, info);
 
   controller_->OnActiveUserPrefServiceChanged(
       GetProfilePrefService(account_id_1));
@@ -3755,7 +3609,7 @@
   WallpaperInfo info = {std::string(), WALLPAPER_LAYOUT_CENTER,
                         WallpaperType::kDaily, DayBeforeYesterdayish()};
   info.collection_id = "fun_collection";
-  controller_->SetUserWallpaperInfo(account_id_1, info);
+  pref_manager_->SetUserWallpaperInfo(account_id_1, info);
 
   controller_->UpdateDailyRefreshWallpaperForTesting();
   Time run_time =
@@ -3777,7 +3631,7 @@
   WallpaperInfo info = {std::string(), WALLPAPER_LAYOUT_CENTER,
                         WallpaperType::kDaily, DayBeforeYesterdayish()};
   info.collection_id = "fun_collection";
-  controller_->SetUserWallpaperInfo(account_id_1, info);
+  pref_manager_->SetUserWallpaperInfo(account_id_1, info);
 
   test_image_downloader_->set_should_fail(true);
 
@@ -3812,8 +3666,7 @@
 
 TEST_F(WallpaperControllerTest, OnGoogleDriveMounted) {
   WallpaperInfo local_info = InfoWithType(WallpaperType::kCustomized);
-  PutWallpaperInfoInPrefs(account_id_1, local_info, GetLocalPrefService(),
-                          prefs::kUserWallpaperInfo);
+  pref_manager_->SetLocalWallpaperInfo(account_id_1, local_info);
 
   SimulateUserLogin(account_id_1);
   controller_->SyncLocalAndRemotePrefs(account_id_1);
@@ -3822,8 +3675,7 @@
 
 TEST_F(WallpaperControllerTest, OnGoogleDriveMounted_WallpaperIsntCustom) {
   WallpaperInfo local_info = InfoWithType(WallpaperType::kOnline);
-  PutWallpaperInfoInPrefs(account_id_1, local_info, GetLocalPrefService(),
-                          prefs::kUserWallpaperInfo);
+  pref_manager_->SetLocalWallpaperInfo(account_id_1, local_info);
 
   controller_->SyncLocalAndRemotePrefs(account_id_1);
   EXPECT_TRUE(client_.get_save_wallpaper_to_drive_fs_account_id().empty());
@@ -3831,8 +3683,7 @@
 
 TEST_F(WallpaperControllerTest, OnGoogleDriveMounted_AlreadySynced) {
   WallpaperInfo local_info = InfoWithType(WallpaperType::kCustomized);
-  PutWallpaperInfoInPrefs(account_id_1, local_info, GetLocalPrefService(),
-                          prefs::kUserWallpaperInfo);
+  pref_manager_->SetLocalWallpaperInfo(account_id_1, local_info);
 
   SimulateUserLogin(account_id_1);
 
@@ -3854,16 +3705,12 @@
   WallpaperInfo local_info =
       WallpaperInfo("a_url", WALLPAPER_LAYOUT_CENTER_CROPPED,
                     WallpaperType::kCustomized, DayBeforeYesterdayish());
-  PutWallpaperInfoInPrefs(account_id_1, local_info, GetLocalPrefService(),
-                          prefs::kUserWallpaperInfo);
+  pref_manager_->SetLocalWallpaperInfo(account_id_1, local_info);
 
   WallpaperInfo synced_info = WallpaperInfo(
       "b_url", WALLPAPER_LAYOUT_CENTER_CROPPED, WallpaperType::kCustomized,
       base::Time::Now().LocalMidnight());
-  PutWallpaperInfoInPrefs(account_id_1, synced_info,
-                          GetProfilePrefService(account_id_1),
-                          prefs::kSyncableWallpaperInfo);
-
+  pref_manager_->SetSyncedWallpaperInfo(account_id_1, synced_info);
   SimulateUserLogin(account_id_1);
 
   controller_->SyncLocalAndRemotePrefs(account_id_1);
@@ -3877,15 +3724,12 @@
   WallpaperInfo local_info = WallpaperInfo(
       "a_url", WALLPAPER_LAYOUT_CENTER_CROPPED, WallpaperType::kCustomized,
       base::Time::Now().LocalMidnight());
-  PutWallpaperInfoInPrefs(account_id_1, local_info, GetLocalPrefService(),
-                          prefs::kUserWallpaperInfo);
+  pref_manager_->SetLocalWallpaperInfo(account_id_1, local_info);
 
   WallpaperInfo synced_info =
       WallpaperInfo("b_url", WALLPAPER_LAYOUT_CENTER_CROPPED,
                     WallpaperType::kCustomized, DayBeforeYesterdayish());
-  PutWallpaperInfoInPrefs(account_id_1, synced_info,
-                          GetProfilePrefService(account_id_1),
-                          prefs::kSyncableWallpaperInfo);
+  pref_manager_->SetSyncedWallpaperInfo(account_id_1, synced_info);
 
   SimulateUserLogin(account_id_1);
 
@@ -3894,7 +3738,7 @@
 }
 
 TEST_F(WallpaperControllerTest, SetDailyRefreshCollectionId) {
-  controller_->SetUserWallpaperInfo(
+  pref_manager_->SetUserWallpaperInfo(
       account_id_1,
       WallpaperInfo(std::string(), WALLPAPER_LAYOUT_CENTER,
                     WallpaperType::kOnline, DayBeforeYesterdayish()));
@@ -3906,7 +3750,7 @@
   expected.collection_id = collection_id;
 
   WallpaperInfo actual;
-  controller_->GetUserWallpaperInfo(account_id_1, &actual);
+  pref_manager_->GetUserWallpaperInfo(account_id_1, &actual);
   // Type should be `WallpaperType::kDaily` now, and collection_id should be
   // updated.
   EXPECT_EQ(expected, actual);
@@ -3919,7 +3763,7 @@
   WallpaperInfo info = {std::string(), WALLPAPER_LAYOUT_CENTER,
                         WallpaperType::kDaily, DayBeforeYesterdayish()};
   info.collection_id = collection_id;
-  controller_->SetUserWallpaperInfo(account_id_1, info);
+  pref_manager_->SetUserWallpaperInfo(account_id_1, info);
 
   controller_->SetDailyRefreshCollectionId(account_id_1, std::string());
   WallpaperInfo expected = {std::string(), WALLPAPER_LAYOUT_CENTER,
@@ -3927,7 +3771,7 @@
   expected.collection_id = collection_id;
 
   WallpaperInfo actual;
-  controller_->GetUserWallpaperInfo(account_id_1, &actual);
+  pref_manager_->GetUserWallpaperInfo(account_id_1, &actual);
   // Type should be `WallpaperType::kOnline` now, and collection_id should be
   // `WallpaperType::EMPTY`.
   EXPECT_EQ(expected, actual);
@@ -3939,7 +3783,7 @@
 // WallpaperType isn't |WallpaperType::kDaily|.
 TEST_F(WallpaperControllerTest,
        SetDailyRefreshCollectionId_Empty_NotTypeDaily) {
-  controller_->SetUserWallpaperInfo(
+  pref_manager_->SetUserWallpaperInfo(
       account_id_1,
       WallpaperInfo(std::string(), WALLPAPER_LAYOUT_CENTER,
                     WallpaperType::kCustomized, DayBeforeYesterdayish()));
@@ -3950,7 +3794,7 @@
                     WallpaperType::kCustomized, DayBeforeYesterdayish());
 
   WallpaperInfo actual;
-  controller_->GetUserWallpaperInfo(account_id_1, &actual);
+  pref_manager_->GetUserWallpaperInfo(account_id_1, &actual);
   EXPECT_EQ(expected, actual);
   EXPECT_EQ(std::string(),
             controller_->GetDailyRefreshCollectionId(account_id_1));
@@ -3981,7 +3825,7 @@
   EXPECT_EQ(1, GetWallpaperCount());
   EXPECT_EQ(controller_->GetWallpaperType(), WallpaperType::kOnline);
 
-  controller_->SetUserWallpaperInfo(account_id_1, WallpaperInfo(params));
+  pref_manager_->SetUserWallpaperInfo(account_id_1, WallpaperInfo(params));
   Shell::Get()->session_controller()->GetActivePrefService()->SetBoolean(
       prefs::kDarkModeEnabled, true);
   controller_->OnColorModeChanged(true);
@@ -3995,7 +3839,7 @@
       /*from_user=*/true,
       /*daily_refresh_enabled=*/false, kUnitId, variants));
   WallpaperInfo actual;
-  EXPECT_TRUE(controller_->GetUserWallpaperInfo(account_id_1, &actual));
+  EXPECT_TRUE(pref_manager_->GetUserWallpaperInfo(account_id_1, &actual));
   EXPECT_EQ(expected, actual);
 }
 
@@ -4022,7 +3866,7 @@
   EXPECT_EQ(1, GetWallpaperCount());
   EXPECT_EQ(controller_->GetWallpaperType(), WallpaperType::kOnline);
 
-  controller_->SetUserWallpaperInfo(account_id_1, WallpaperInfo(params));
+  pref_manager_->SetUserWallpaperInfo(account_id_1, WallpaperInfo(params));
   Shell::Get()->session_controller()->GetActivePrefService()->SetBoolean(
       prefs::kDarkModeEnabled, true);
   controller_->OnColorModeChanged(true);
@@ -4041,7 +3885,7 @@
       /*from_user=*/true,
       /*daily_refresh_enabled=*/false, kUnitId, variants));
   WallpaperInfo actual;
-  EXPECT_TRUE(controller_->GetUserWallpaperInfo(account_id_1, &actual));
+  EXPECT_TRUE(pref_manager_->GetUserWallpaperInfo(account_id_1, &actual));
   EXPECT_EQ(expected, actual);
 }
 
@@ -4062,10 +3906,10 @@
                             /*preview_mode=*/false, /*from_user=*/true,
                             /*daily_refresh_enabled=*/false, kUnitId, variants);
 
-  controller_->SetUserWallpaperInfo(account_id_1, WallpaperInfo(params));
+  pref_manager_->SetUserWallpaperInfo(account_id_1, WallpaperInfo(params));
   WallpaperInfo expected = WallpaperInfo(params);
   WallpaperInfo actual;
-  controller_->GetUserWallpaperInfo(account_id_1, &actual);
+  pref_manager_->GetUserWallpaperInfo(account_id_1, &actual);
   EXPECT_EQ(expected, actual);
 }
 
@@ -4107,7 +3951,8 @@
   EXPECT_EQ(controller_->GetWallpaperType(), WallpaperType::kOnline);
   // Verify that the user wallpaper info is updated.
   WallpaperInfo wallpaper_info;
-  EXPECT_TRUE(controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+  EXPECT_TRUE(
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
   WallpaperInfo expected_wallpaper_info = WallpaperInfo(params);
   EXPECT_EQ(wallpaper_info, expected_wallpaper_info);
 
@@ -4161,8 +4006,7 @@
       /*daily_refresh_enabled=*/false, absl::nullopt, variants);
   // local info doesn't have unit_id.
   const WallpaperInfo& local_info = WallpaperInfo(params);
-  PutWallpaperInfoInPrefs(account_id_1, local_info, GetLocalPrefService(),
-                          prefs::kUserWallpaperInfo);
+  pref_manager_->SetLocalWallpaperInfo(account_id_1, local_info);
 
   const OnlineWallpaperParams& params2 =
       OnlineWallpaperParams(account_id_1, kAssetId2, GURL(kDummyUrl2),
@@ -4172,13 +4016,11 @@
                             /*daily_refresh_enabled=*/false, kUnitId, variants);
   // synced info tracks dark variant.
   const WallpaperInfo& synced_info = WallpaperInfo(params2);
-  PutWallpaperInfoInPrefs(account_id_1, synced_info,
-                          GetProfilePrefService(account_id_1),
-                          prefs::kSyncableWallpaperInfo);
+  pref_manager_->SetSyncedWallpaperInfo(account_id_1, synced_info);
   RunAllTasksUntilIdle();
 
   WallpaperInfo actual_info;
-  EXPECT_TRUE(controller_->GetUserWallpaperInfo(account_id_1, &actual_info));
+  EXPECT_TRUE(pref_manager_->GetUserWallpaperInfo(account_id_1, &actual_info));
   EXPECT_EQ(synced_info, actual_info);
   // Verify the wallpaper is set.
   EXPECT_EQ(1, GetWallpaperCount());
@@ -4203,8 +4045,7 @@
                             /*daily_refresh_enabled=*/false, kUnitId, variants);
   // local info tracks light variant.
   const WallpaperInfo& local_info = WallpaperInfo(params);
-  PutWallpaperInfoInPrefs(account_id_1, local_info, GetLocalPrefService(),
-                          prefs::kUserWallpaperInfo);
+  pref_manager_->SetLocalWallpaperInfo(account_id_1, local_info);
 
   const OnlineWallpaperParams& params2 =
       OnlineWallpaperParams(account_id_1, kAssetId2, GURL(kDummyUrl2),
@@ -4214,13 +4055,11 @@
                             /*daily_refresh_enabled=*/false, kUnitId, variants);
   // synced info tracks dark variant.
   const WallpaperInfo& synced_info = WallpaperInfo(params2);
-  PutWallpaperInfoInPrefs(account_id_1, synced_info,
-                          GetProfilePrefService(account_id_1),
-                          prefs::kSyncableWallpaperInfo);
+  pref_manager_->SetSyncedWallpaperInfo(account_id_1, synced_info);
   RunAllTasksUntilIdle();
 
   WallpaperInfo actual_info;
-  EXPECT_TRUE(controller_->GetUserWallpaperInfo(account_id_1, &actual_info));
+  EXPECT_TRUE(pref_manager_->GetUserWallpaperInfo(account_id_1, &actual_info));
   EXPECT_EQ(local_info, synced_info);
   EXPECT_EQ(local_info, actual_info);
   // Verify the wallpaper is not set again.
@@ -4313,7 +4152,8 @@
                                  WallpaperType::kOnceGooglePhotos);
 
   WallpaperInfo wallpaper_info;
-  EXPECT_TRUE(controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+  EXPECT_TRUE(
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
   WallpaperInfo expected_wallpaper_info(params);
   EXPECT_EQ(feature_enabled, wallpaper_info == expected_wallpaper_info);
 }
@@ -4355,7 +4195,8 @@
   EXPECT_NE(controller_->GetWallpaperType(), WallpaperType::kOnceGooglePhotos);
 
   WallpaperInfo wallpaper_info;
-  EXPECT_TRUE(controller_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
+  EXPECT_TRUE(
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &wallpaper_info));
   WallpaperInfo expected_wallpaper_info(online_params);
   EXPECT_EQ(wallpaper_info, expected_wallpaper_info);
 }
@@ -4416,7 +4257,7 @@
   WallpaperInfo info = {kFakeGooglePhotosPhotoId, WALLPAPER_LAYOUT_CENTER,
                         WallpaperType::kOnceGooglePhotos,
                         DayBeforeYesterdayish()};
-  controller_->SetUserWallpaperInfo(account_id_1, info);
+  pref_manager_->SetUserWallpaperInfo(account_id_1, info);
 
   client_.set_google_photo_has_been_deleted(true);
   // Trigger Google Photos wallpaper cache check.
@@ -4520,7 +4361,7 @@
       std::string(), WALLPAPER_LAYOUT_CENTER_CROPPED, WallpaperType::kDefault,
       base::Time::Now().LocalMidnight());
   EXPECT_TRUE(
-      controller_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
   EXPECT_EQ(user_wallpaper_info, default_wallpaper_info);
 
   // Simulate opening the wallpaper picker window.
@@ -4548,7 +4389,7 @@
 
   // Verify that the user wallpaper info remains unchanged during the preview.
   EXPECT_TRUE(
-      controller_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
   EXPECT_EQ(user_wallpaper_info, default_wallpaper_info);
   histogram_tester().ExpectTotalCount("Ash.Wallpaper.Preview.Show",
                                       GooglePhotosEnabled() ? 1 : 0);
@@ -4568,8 +4409,8 @@
     WallpaperInfo google_photos_wallpaper_info(
         photo_id, layout, WallpaperType::kOnceGooglePhotos,
         base::Time::Now().LocalMidnight());
-    EXPECT_TRUE(
-        controller_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
+    EXPECT_TRUE(pref_manager_->GetUserWallpaperInfo(account_id_1,
+                                                    &user_wallpaper_info));
     EXPECT_EQ(user_wallpaper_info, google_photos_wallpaper_info);
   }
 }
@@ -4587,7 +4428,7 @@
       std::string(), WALLPAPER_LAYOUT_CENTER_CROPPED, WallpaperType::kDefault,
       base::Time::Now().LocalMidnight());
   EXPECT_TRUE(
-      controller_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
   EXPECT_EQ(user_wallpaper_info, default_wallpaper_info);
 
   // Simulate opening the wallpaper picker window.
@@ -4614,7 +4455,7 @@
 
   // Verify that the user wallpaper info remains unchanged during the preview.
   EXPECT_TRUE(
-      controller_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
   EXPECT_EQ(user_wallpaper_info, default_wallpaper_info);
   histogram_tester().ExpectTotalCount("Ash.Wallpaper.Preview.Show",
                                       GooglePhotosEnabled() ? 1 : 0);
@@ -4645,7 +4486,7 @@
       std::string(), WALLPAPER_LAYOUT_CENTER_CROPPED, WallpaperType::kDefault,
       base::Time::Now().LocalMidnight());
   EXPECT_TRUE(
-      controller_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
   EXPECT_EQ(user_wallpaper_info, default_wallpaper_info);
 
   // Simulate opening the wallpaper picker window.
@@ -4673,7 +4514,7 @@
 
   // Verify that the user wallpaper info remains unchanged during the preview.
   EXPECT_TRUE(
-      controller_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
   EXPECT_EQ(user_wallpaper_info, default_wallpaper_info);
   histogram_tester().ExpectTotalCount("Ash.Wallpaper.Preview.Show",
                                       GooglePhotosEnabled() ? 1 : 0);
@@ -4698,8 +4539,8 @@
     WallpaperInfo synced_custom_wallpaper_info(
         base::FilePath(wallpaper_files_id_1).Append(file_name_1).value(),
         layout, WallpaperType::kCustomized, base::Time::Now().LocalMidnight());
-    EXPECT_TRUE(
-        controller_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
+    EXPECT_TRUE(pref_manager_->GetUserWallpaperInfo(account_id_1,
+                                                    &user_wallpaper_info));
     EXPECT_EQ(user_wallpaper_info, synced_custom_wallpaper_info);
 
     // Now cancel the preview. Verify the synced custom wallpaper is shown
@@ -4710,8 +4551,8 @@
     RunAllTasksUntilIdle();
     EXPECT_EQ(GetWallpaperCount(), 1);
     EXPECT_EQ(controller_->GetWallpaperType(), WallpaperType::kCustomized);
-    EXPECT_TRUE(
-        controller_->GetUserWallpaperInfo(account_id_1, &user_wallpaper_info));
+    EXPECT_TRUE(pref_manager_->GetUserWallpaperInfo(account_id_1,
+                                                    &user_wallpaper_info));
     EXPECT_EQ(user_wallpaper_info, synced_custom_wallpaper_info);
   }
 }
@@ -4730,14 +4571,14 @@
       /*daily_refresh_enabled=*/true, WALLPAPER_LAYOUT_CENTER_CROPPED,
       /*preview_mode=*/false, /*dedup_key=*/absl::nullopt);
   WallpaperInfo info(params);
-  controller_->SetUserWallpaperInfo(account_id_1, info);
+  pref_manager_->SetUserWallpaperInfo(account_id_1, info);
 
   controller_->UpdateDailyRefreshWallpaperForTesting();
   RunAllTasksUntilIdle();
 
   WallpaperInfo expected_info;
   bool success =
-      controller_->GetUserWallpaperInfo(account_id_1, &expected_info);
+      pref_manager_->GetUserWallpaperInfo(account_id_1, &expected_info);
   EXPECT_EQ(success, GooglePhotosEnabled());
   if (success) {
     EXPECT_EQ(expected_photo_id, expected_info.location);
@@ -4754,7 +4595,7 @@
       /*daily_refresh_enabled=*/true, WALLPAPER_LAYOUT_CENTER_CROPPED,
       /*preview_mode=*/false, /*dedup_key=*/absl::nullopt);
   WallpaperInfo info(params);
-  controller_->SetUserWallpaperInfo(account_id_1, info);
+  pref_manager_->SetUserWallpaperInfo(account_id_1, info);
 
   controller_->UpdateDailyRefreshWallpaperForTesting();
   RunAllTasksUntilIdle();
@@ -4782,7 +4623,7 @@
       /*daily_refresh_enabled=*/true, WALLPAPER_LAYOUT_CENTER_CROPPED,
       /*preview_mode=*/false, /*dedup_key=*/absl::nullopt);
   WallpaperInfo info(params);
-  controller_->SetUserWallpaperInfo(account_id_1, info);
+  pref_manager_->SetUserWallpaperInfo(account_id_1, info);
 
   client_.set_fetch_google_photos_photo_fails(true);
   controller_->UpdateDailyRefreshWallpaperForTesting();
@@ -4820,7 +4661,7 @@
       /*variants=*/std::vector<OnlineWallpaperVariant>());
 
   WallpaperInfo online_info(online_params);
-  controller_->SetUserWallpaperInfo(account_id_1, online_info);
+  pref_manager_->SetUserWallpaperInfo(account_id_1, online_info);
 
   client_.set_fetch_google_photos_photo_fails(true);
   controller_->SetGooglePhotosWallpaper(daily_google_photos_params,
@@ -4828,7 +4669,7 @@
   RunAllTasksUntilIdle();
 
   WallpaperInfo current_info;
-  controller_->GetUserWallpaperInfo(account_id_1, &current_info);
+  pref_manager_->GetUserWallpaperInfo(account_id_1, &current_info);
 
   EXPECT_EQ(online_info, current_info);
 }
@@ -4847,7 +4688,7 @@
   RunAllTasksUntilIdle();
 
   WallpaperInfo current_info;
-  controller_->GetUserWallpaperInfo(account_id_1, &current_info);
+  pref_manager_->GetUserWallpaperInfo(account_id_1, &current_info);
 
   EXPECT_EQ(GooglePhotosEnabled(),
             WallpaperType::kDailyGooglePhotos == current_info.type);
@@ -4859,7 +4700,7 @@
   controller_->UpdateDailyRefreshWallpaperForTesting();
   RunAllTasksUntilIdle();
 
-  controller_->GetUserWallpaperInfo(account_id_1, &current_info);
+  pref_manager_->GetUserWallpaperInfo(account_id_1, &current_info);
 
   EXPECT_EQ(GooglePhotosEnabled(),
             WallpaperType::kDefault == current_info.type);
@@ -4899,7 +4740,7 @@
 
   WallpaperInfo info = {kFakeGooglePhotosPhotoId, WALLPAPER_LAYOUT_CENTER, type,
                         base::Time::Now()};
-  controller_->SetUserWallpaperInfo(account_id_1, info);
+  pref_manager_->SetUserWallpaperInfo(account_id_1, info);
   controller_->ShowUserWallpaper(account_id_1);
   RunAllTasksUntilIdle();
 
diff --git a/ash/wallpaper/wallpaper_pref_manager_unittest.cc b/ash/wallpaper/wallpaper_pref_manager_unittest.cc
index 2aeca0d..b9e9e10f 100644
--- a/ash/wallpaper/wallpaper_pref_manager_unittest.cc
+++ b/ash/wallpaper/wallpaper_pref_manager_unittest.cc
@@ -33,6 +33,8 @@
 constexpr char kUser1[] = "user1@test.com";
 const AccountId account_id_1 = AccountId::FromUserEmailGaiaId(kUser1, kUser1);
 
+constexpr char kFakeGooglePhotosPhotoId[] = "fake_photo";
+
 WallpaperInfo InfoWithType(WallpaperType type) {
   return WallpaperInfo(std::string(), WALLPAPER_LAYOUT_CENTER_CROPPED, type,
                        base::Time::Now());
@@ -91,6 +93,7 @@
                              WallpaperInfo info,
                              PrefService* pref_service,
                              const std::string& pref_name) {
+  DCHECK(pref_service);
   DictionaryPrefUpdate wallpaper_update(pref_service, pref_name);
   base::Value wallpaper_info_dict = CreateWallpaperInfoDict(info);
   wallpaper_update->SetKey(account_id.GetUserEmail(),
@@ -100,9 +103,10 @@
 void AssertWallpaperInfoInPrefs(const PrefService* pref_service,
                                 const char pref_name[],
                                 AccountId account_id,
-                                WallpaperInfo info) {
+                                const WallpaperInfo& info) {
   const base::Value::Dict* stored_info_dict =
       pref_service->GetValueDict(pref_name).FindDict(account_id.GetUserEmail());
+  DCHECK(stored_info_dict);
   base::Value expected_info_dict = CreateWallpaperInfoDict(info);
   EXPECT_EQ(expected_info_dict, *stored_info_dict);
 }
@@ -235,6 +239,17 @@
                              account_id_1, info);
 }
 
+TEST_F(WallpaperPrefManagerTest, SetWallpaperInfoLocalFromGooglePhotos) {
+  WallpaperInfo info(
+      GooglePhotosWallpaperParams{account_id_1, kFakeGooglePhotosPhotoId,
+                                  /*daily_refresh_enabled=*/false,
+                                  WallpaperLayout::WALLPAPER_LAYOUT_STRETCH,
+                                  /*preview_mode=*/false, "dedup_key"});
+  EXPECT_TRUE(pref_manager_->SetUserWallpaperInfo(account_id_1, info));
+  AssertWallpaperInfoInPrefs(GetLocalPrefService(), prefs::kUserWallpaperInfo,
+                             account_id_1, info);
+}
+
 TEST_F(WallpaperPrefManagerTest, SetWallpaperInfoSynced) {
   profile_helper_->RegisterPrefsForAccount(account_id_1);
 
@@ -245,6 +260,16 @@
       prefs::kSyncableWallpaperInfo, account_id_1, info);
 }
 
+TEST_F(WallpaperPrefManagerTest, SetWallpaperInfoSyncedFromGooglePhotos) {
+  profile_helper_->RegisterPrefsForAccount(account_id_1);
+
+  WallpaperInfo info = InfoWithType(WallpaperType::kOnceGooglePhotos);
+  EXPECT_TRUE(pref_manager_->SetUserWallpaperInfo(account_id_1, info));
+  AssertWallpaperInfoInPrefs(
+      profile_helper_->GetUserPrefServiceSyncable(account_id_1),
+      prefs::kSyncableWallpaperInfo, account_id_1, info);
+}
+
 TEST_F(WallpaperPrefManagerTest, SetWallpaperInfoSyncDisabled) {
   profile_helper_->RegisterPrefsForAccount(account_id_1);
   // This needs to be saved before sync is disabled or we can't get a pref
diff --git a/ash/webui/diagnostics_ui/backend/session_log_handler_unittest.cc b/ash/webui/diagnostics_ui/backend/session_log_handler_unittest.cc
index c0a2832..ea8fa7b 100644
--- a/ash/webui/diagnostics_ui/backend/session_log_handler_unittest.cc
+++ b/ash/webui/diagnostics_ui/backend/session_log_handler_unittest.cc
@@ -47,7 +47,6 @@
 namespace {
 
 constexpr char kHandlerFunctionName[] = "handlerFunctionName";
-constexpr char kRoutineLogFileName[] = "diagnostic_routine_log";
 
 mojom::SystemInfoPtr CreateSystemInfoPtr(const std::string& board_name,
                                          const std::string& marketing_name,
@@ -169,38 +168,49 @@
   base::FilePath selected_path_;
 };
 
-class SessionLogHandlerTest : public testing::Test {
+// Test class using NoSessionAshTestBase to ensure shell is available for
+// tests requiring DiagnosticsLogController singleton.
+class SessionLogHandlerTest : public NoSessionAshTestBase {
  public:
   SessionLogHandlerTest()
-      : task_environment_(),
+      : NoSessionAshTestBase(
+            base::test::TaskEnvironment::TimeSource::MOCK_TIME),
         task_runner_(new base::TestSimpleTaskRunner()),
         web_ui_(),
-        session_log_handler_() {
+        session_log_handler_() {}
+  ~SessionLogHandlerTest() override = default;
+
+  void SetUp() override {
     EXPECT_TRUE(temp_dir_.CreateUniqueTempDir());
-    base::FilePath routine_log_path =
-        temp_dir_.GetPath().AppendASCII(kRoutineLogFileName);
-    auto telemetry_log = std::make_unique<TelemetryLog>();
-    auto routine_log = std::make_unique<RoutineLog>(routine_log_path);
-    auto networking_log = std::make_unique<NetworkingLog>(temp_dir_.GetPath());
-    telemetry_log_ = telemetry_log.get();
-    routine_log_ = routine_log.get();
-    networking_log_ = networking_log.get();
+    // Setup to ensure ash::Shell can configure for tests.
+    ui::ResourceBundle::CleanupSharedInstance();
+    AshTestSuite::LoadTestResources();
+    NoSessionAshTestBase::SetUp();
+    DiagnosticsLogController::Initialize(
+        std::make_unique<FakeDiagnosticsBrowserDelegate>());
+    auto* controller = DiagnosticsLogController::Get();
+    telemetry_log_ = controller->GetTelemetryLog();
+    routine_log_ = controller->GetRoutineLog();
+    networking_log_ = controller->GetNetworkingLog();
     session_log_handler_ = std::make_unique<diagnostics::SessionLogHandler>(
         base::BindRepeating(&CreateTestSelectFilePolicy),
-        std::move(telemetry_log), std::move(routine_log),
-        std::move(networking_log), &holding_space_client_);
+        /*telemetry_log*/ nullptr, /*routine_log*/ nullptr,
+        /*networking_log*/ nullptr, &holding_space_client_);
     session_log_handler_->SetWebUIForTest(&web_ui_);
     session_log_handler_->RegisterMessages();
     session_log_handler_->SetTaskRunnerForTesting(task_runner_);
 
+    // Call handler to enable Javascript.
     base::ListValue args;
     web_ui_.HandleReceivedMessage("initialize", &args);
   }
 
-  ~SessionLogHandlerTest() override {
+  void TearDown() override {
     task_runner_.reset();
-    task_environment_.RunUntilIdle();
+    task_environment()->RunUntilIdle();
     ui::SelectFileDialog::SetFactory(nullptr);
+
+    NoSessionAshTestBase::TearDown();
   }
 
   void RunTasks() { task_runner_->RunPendingTasks(); }
@@ -214,18 +224,15 @@
   }
 
  protected:
-  base::test::TaskEnvironment task_environment_{
-      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
   // Task runner for tasks posted by save session log handler.
   scoped_refptr<base::TestSimpleTaskRunner> task_runner_;
-
   content::TestWebUI web_ui_;
-  std::unique_ptr<diagnostics::SessionLogHandler> session_log_handler_;
+  std::unique_ptr<SessionLogHandler> session_log_handler_;
+  base::ScopedTempDir temp_dir_;
   TelemetryLog* telemetry_log_;
   RoutineLog* routine_log_;
   NetworkingLog* networking_log_;
   testing::NiceMock<ash::MockHoldingSpaceClient> holding_space_client_;
-  base::ScopedTempDir temp_dir_;
 };
 
 // Flaky; see crbug.com/1336726
@@ -233,7 +240,7 @@
   base::RunLoop run_loop;
   // Populate routine log
   routine_log_->LogRoutineStarted(mojom::RoutineType::kCpuStress);
-  task_environment_.RunUntilIdle();
+  task_environment()->RunUntilIdle();
 
   // Populate telemetry log
   const std::string expected_board_name = "board_name";
@@ -304,52 +311,8 @@
   EXPECT_EQ("--- Network Events ---", log_lines[17]);
 }
 
-// Test class using NoSessionAshTestBase to ensure shell is available for
-// tests requiring DiagnosticsLogController singleton.
-class SessionLogHandlerAshTest : public NoSessionAshTestBase {
- public:
-  SessionLogHandlerAshTest() : task_runner_(new base::TestSimpleTaskRunner()) {}
-  ~SessionLogHandlerAshTest() override = default;
-
-  void SetUp() override {
-    // Setup to ensure ash::Shell can configure for tests.
-    ui::ResourceBundle::CleanupSharedInstance();
-    AshTestSuite::LoadTestResources();
-    // Setup feature list before setting up ash::Shell.
-    feature_list_.InitAndEnableFeature(
-        ash::features::kEnableLogControllerForDiagnosticsApp);
-    NoSessionAshTestBase::SetUp();
-    DiagnosticsLogController::Initialize(
-        std::make_unique<FakeDiagnosticsBrowserDelegate>());
-    session_log_handler_ = std::make_unique<SessionLogHandler>(
-        base::BindRepeating(&CreateTestSelectFilePolicy),
-        /*telemetry_log*/ nullptr,
-        /*routine_log*/ nullptr,
-        /*networking_log*/ nullptr,
-        /*holding_space_client*/ &holding_space_client_);
-    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
-    session_log_handler_->SetWebUIForTest(&web_ui_);
-    session_log_handler_->RegisterMessages();
-    session_log_handler_->SetTaskRunnerForTesting(task_runner_);
-    // Call handler to enable Javascript.
-    base::ListValue args;
-    web_ui_.HandleReceivedMessage("initialize", &args);
-  }
-
-  void RunTasks() { task_runner_->RunPendingTasks(); }
-
- protected:
-  base::test::ScopedFeatureList feature_list_;
-  std::unique_ptr<SessionLogHandler> session_log_handler_;
-  content::TestWebUI web_ui_;
-  base::ScopedTempDir temp_dir_;
-  testing::NiceMock<ash::MockHoldingSpaceClient> holding_space_client_;
-  // Task runner for tasks posted by save session log handler.
-  scoped_refptr<base::TestSimpleTaskRunner> task_runner_;
-};
-
 // Validates behavior when log controller is used to generate session log.
-TEST_F(SessionLogHandlerAshTest, SaveSessionLogFlagEnabled) {
+TEST_F(SessionLogHandlerTest, SaveHeaderOnlySessionLog) {
   base::RunLoop run_loop;
 
   // Simulate select file
diff --git a/ash/webui/os_feedback_ui/resources/os_feedback_shared_css.html b/ash/webui/os_feedback_ui/resources/os_feedback_shared_css.html
index cad5ac6..42f9d1d 100644
--- a/ash/webui/os_feedback_ui/resources/os_feedback_shared_css.html
+++ b/ash/webui/os_feedback_ui/resources/os_feedback_shared_css.html
@@ -11,8 +11,14 @@
       background-color: var(--cros-bg-color);
     }
 
-    a[href] {
+    a {
       color: var(--cros-link-color);
+      text-decoration: none;
+    }
+
+    a:focus,
+    a:hover {
+      text-decoration: underline;
     }
 
     #container {
diff --git a/ash/webui/os_feedback_ui/resources/share_data_page.html b/ash/webui/os_feedback_ui/resources/share_data_page.html
index ada18c2..cee3c65 100644
--- a/ash/webui/os_feedback_ui/resources/share_data_page.html
+++ b/ash/webui/os_feedback_ui/resources/share_data_page.html
@@ -1,10 +1,4 @@
 <style include="os-feedback-shared">
-  #legalHelpPageUrl,
-  #privacyPolicyUrl,
-  #termsOfServiceUrl {
-    text-decoration: none;
-  }
-
   #privacyNote {
     color: var(--cros-color-secondary);
     font-size: 13px;
@@ -44,6 +38,7 @@
 
   #screenshotCheckLabel {
     flex: 1;
+    margin-inline-end: 12px;
   }
 
   #screenshotContainer > button {
@@ -80,6 +75,7 @@
   }
 
   .md-select {
+    --md-select-side-padding: 16px;
     height: 32px;
     width: 248px;
   }
@@ -103,10 +99,6 @@
     color: var(--cros-text-color-disabled);
   }
 
-  #sysInfoCheckboxLabel {
-    line-height: 20px;
-  }
-
   #screenshotCheckbox {
     margin-inline-end: 10px;
     margin-inline-start: 12px;
@@ -114,7 +106,6 @@
 
   #screenshotCheckLabel {
     font-weight: 400;
-    line-height: 20px;
   }
 
   #imageButton {
@@ -137,6 +128,7 @@
 
   label {
     color: var(--cros-text-color-primary);
+    line-height: 20px;
   }
 </style>
 <div id="container">
diff --git a/ash/webui/projector_app/annotator_message_handler.cc b/ash/webui/projector_app/annotator_message_handler.cc
index e4f40fc3..9c797590 100644
--- a/ash/webui/projector_app/annotator_message_handler.cc
+++ b/ash/webui/projector_app/annotator_message_handler.cc
@@ -7,8 +7,8 @@
 #include <memory>
 
 #include "ash/public/cpp/projector/annotator_tool.h"
-#include "ash/public/cpp/projector/projector_client.h"
 #include "ash/public/cpp/projector/projector_controller.h"
+#include "ash/webui/projector_app/projector_app_client.h"
 #include "base/check.h"
 #include "base/json/values_util.h"
 #include "base/values.h"
@@ -17,11 +17,11 @@
 namespace ash {
 
 AnnotatorMessageHandler::AnnotatorMessageHandler() {
-  ProjectorClient::Get()->SetAnnotatorMessageHandler(this);
+  ProjectorAppClient::Get()->SetAnnotatorMessageHandler(this);
 }
 
 AnnotatorMessageHandler::~AnnotatorMessageHandler() {
-  ProjectorClient::Get()->ResetAnnotatorMessageHandler(this);
+  ProjectorAppClient::Get()->ResetAnnotatorMessageHandler(this);
 };
 
 void AnnotatorMessageHandler::RegisterMessages() {
diff --git a/ash/webui/projector_app/projector_app_client.h b/ash/webui/projector_app/projector_app_client.h
index c952d49a..238b9a5b 100644
--- a/ash/webui/projector_app/projector_app_client.h
+++ b/ash/webui/projector_app/projector_app_client.h
@@ -28,6 +28,9 @@
 
 namespace ash {
 
+class AnnotatorMessageHandler;
+
+struct AnnotatorTool;
 struct ProjectorScreencast;
 
 struct NewScreencastPrecondition;
@@ -157,6 +160,21 @@
   virtual void GetScreencast(const std::string& screencast_id,
                              OnGetScreencastCallback callback) = 0;
 
+  // Registers the AnnotatorMessageHandler that is owned by the WebUI that
+  // contains the Projector annotator.
+  virtual void SetAnnotatorMessageHandler(AnnotatorMessageHandler* handler) = 0;
+
+  // Resets the stored AnnotatorMessageHandler if it matches the one that is
+  // passed in.
+  virtual void ResetAnnotatorMessageHandler(
+      AnnotatorMessageHandler* handler) = 0;
+
+  // Sets the tool inside the annotator WebUI.
+  virtual void SetTool(const AnnotatorTool& tool) = 0;
+
+  // Clears the contents of the annotator canvas.
+  virtual void Clear() = 0;
+
  protected:
   ProjectorAppClient();
   virtual ~ProjectorAppClient();
diff --git a/ash/webui/projector_app/test/annotator_message_handler_unittest.cc b/ash/webui/projector_app/test/annotator_message_handler_unittest.cc
index 580b19f..8f5b3c88 100644
--- a/ash/webui/projector_app/test/annotator_message_handler_unittest.cc
+++ b/ash/webui/projector_app/test/annotator_message_handler_unittest.cc
@@ -5,8 +5,8 @@
 #include "ash/webui/projector_app/annotator_message_handler.h"
 
 #include "ash/public/cpp/projector/annotator_tool.h"
-#include "ash/public/cpp/test/mock_projector_client.h"
 #include "ash/public/cpp/test/mock_projector_controller.h"
+#include "ash/webui/projector_app/test/mock_app_client.h"
 #include "base/test/task_environment.h"
 #include "base/values.h"
 #include "content/public/test/test_web_ui.h"
@@ -70,7 +70,7 @@
   std::unique_ptr<AnnotatorMessageHandler> message_handler_;
   content::TestWebUI web_ui_;
   MockProjectorController controller_;
-  MockProjectorClient client_;
+  MockAppClient client_;
 };
 
 TEST_F(AnnotatorMessageHandlerTest, SetTool) {
diff --git a/ash/webui/projector_app/test/mock_app_client.h b/ash/webui/projector_app/test/mock_app_client.h
index 45ad9db..30b28d5 100644
--- a/ash/webui/projector_app/test/mock_app_client.h
+++ b/ash/webui/projector_app/test/mock_app_client.h
@@ -54,6 +54,10 @@
   MOCK_METHOD2(GetScreencast,
                void(const std::string&,
                     ProjectorAppClient::OnGetScreencastCallback));
+  MOCK_METHOD1(SetAnnotatorMessageHandler, void(AnnotatorMessageHandler*));
+  MOCK_METHOD1(ResetAnnotatorMessageHandler, void(AnnotatorMessageHandler*));
+  MOCK_METHOD1(SetTool, void(const AnnotatorTool&));
+  MOCK_METHOD0(Clear, void());
 
   void SetAutomaticIssueOfAccessTokens(bool success);
   void WaitForAccessRequest(const std::string& account_email);
diff --git a/base/process/memory_unittest.cc b/base/process/memory_unittest.cc
index f043c9e9..c8d374b 100644
--- a/base/process/memory_unittest.cc
+++ b/base/process/memory_unittest.cc
@@ -119,6 +119,7 @@
     !defined(MEMORY_TOOL_REPLACES_ALLOCATOR)
 
 namespace {
+
 #if BUILDFLAG(IS_WIN)
 
 // Windows raises an exception in order to make the exit code unique to OOM.
@@ -282,10 +283,13 @@
 }
 #endif  // BUILDFLAG(IS_WIN)
 
-// OS X and Android have no 2Gb allocation limit.
+// OS X has no 2Gb allocation limit.
 // See https://crbug.com/169327.
-#if !BUILDFLAG(IS_MAC) && !BUILDFLAG(IS_ANDROID)
+#if !BUILDFLAG(IS_MAC)
 TEST_F(OutOfMemoryDeathTest, SecurityNew) {
+  if (ShouldSkipTest()) {
+    return;
+  }
   ASSERT_OOM_DEATH({
     SetUpInDeathAssert();
     value_ = operator new(insecure_test_size_);
@@ -293,6 +297,9 @@
 }
 
 TEST_F(OutOfMemoryDeathTest, SecurityNewArray) {
+  if (ShouldSkipTest()) {
+    return;
+  }
   ASSERT_OOM_DEATH({
     SetUpInDeathAssert();
     value_ = new char[insecure_test_size_];
@@ -300,6 +307,9 @@
 }
 
 TEST_F(OutOfMemoryDeathTest, SecurityMalloc) {
+  if (ShouldSkipTest()) {
+    return;
+  }
   ASSERT_OOM_DEATH({
     SetUpInDeathAssert();
     value_ = malloc(insecure_test_size_);
@@ -307,6 +317,9 @@
 }
 
 TEST_F(OutOfMemoryDeathTest, SecurityRealloc) {
+  if (ShouldSkipTest()) {
+    return;
+  }
   ASSERT_OOM_DEATH({
     SetUpInDeathAssert();
     value_ = realloc(nullptr, insecure_test_size_);
@@ -314,6 +327,9 @@
 }
 
 TEST_F(OutOfMemoryDeathTest, SecurityCalloc) {
+  if (ShouldSkipTest()) {
+    return;
+  }
   ASSERT_OOM_DEATH({
     SetUpInDeathAssert();
     value_ = calloc(1024, insecure_test_size_ / 1024L);
@@ -321,6 +337,9 @@
 }
 
 TEST_F(OutOfMemoryDeathTest, SecurityAlignedAlloc) {
+  if (ShouldSkipTest()) {
+    return;
+  }
   ASSERT_OOM_DEATH({
     SetUpInDeathAssert();
     value_ = base::AlignedAlloc(insecure_test_size_, 8);
@@ -330,13 +349,16 @@
 // POSIX does not define an aligned realloc function.
 #if BUILDFLAG(IS_WIN)
 TEST_F(OutOfMemoryDeathTest, SecurityAlignedRealloc) {
+  if (ShouldSkipTest()) {
+    return;
+  }
   ASSERT_OOM_DEATH({
     SetUpInDeathAssert();
     value_ = _aligned_realloc(nullptr, insecure_test_size_, 8);
   });
 }
 #endif  // BUILDFLAG(IS_WIN)
-#endif  // !BUILDFLAG(IS_MAC) && !BUILDFLAG(IS_ANDROID)
+#endif  // !BUILDFLAG(IS_MAC)
 
 #if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
 
diff --git a/cc/base/synced_property.h b/cc/base/synced_property.h
index f5173bc..8d29bc7 100644
--- a/cc/base/synced_property.h
+++ b/cc/base/synced_property.h
@@ -5,7 +5,10 @@
 #ifndef CC_BASE_SYNCED_PROPERTY_H_
 #define CC_BASE_SYNCED_PROPERTY_H_
 
+#include <utility>
+
 #include "base/memory/ref_counted.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
 
 namespace cc {
 
@@ -39,8 +42,7 @@
   // Sets the value on the impl thread, due to an impl-thread-originating
   // action.  Returns true if this had any effect.  This will remain
   // impl-thread-only information at first, and will get pulled back to the main
-  // thread on the next call of PullDeltaToMainThread (which happens right
-  // before the commit).
+  // thread on the next call of PullDeltaForMainThread.
   bool SetCurrent(BaseT current) {
     DeltaT delta = T::DeltaBetweenBases(current, active_base_);
     if (active_delta_ == delta)
@@ -57,16 +59,27 @@
   // Returns the latest active tree delta and also makes a note that this value
   // was sent to the main thread.
   DeltaT PullDeltaForMainThread() {
-    reflected_delta_in_main_tree_ = PendingDelta();
-    return reflected_delta_in_main_tree_;
+    DCHECK(!next_reflected_delta_in_main_tree_.has_value());
+    DeltaT result = UnsentDelta();
+    if (reflected_delta_in_main_tree_.has_value()) {
+      next_reflected_delta_in_main_tree_.emplace(result);
+    } else {
+      reflected_delta_in_main_tree_.emplace(result);
+    }
+    return result;
   }
 
   // Push the latest value from the main thread onto pending tree-associated
   // state. Returns true if pushing the value results in different values
   // between the main layer tree and the pending tree.
   bool PushMainToPending(BaseT main_thread_value) {
-    reflected_delta_in_pending_tree_ = reflected_delta_in_main_tree_;
-    reflected_delta_in_main_tree_ = T::IdentityDelta();
+    DCHECK(reflected_delta_in_main_tree_.has_value() ||
+           !next_reflected_delta_in_main_tree_.has_value());
+    reflected_delta_in_pending_tree_ =
+        reflected_delta_in_main_tree_.value_or(T::IdentityDelta());
+    reflected_delta_in_main_tree_ =
+        std::move(next_reflected_delta_in_main_tree_);
+    next_reflected_delta_in_main_tree_.reset();
     pending_base_ = main_thread_value;
 
     return Current(false) != main_thread_value;
@@ -97,16 +110,52 @@
            active_value_before_push != current_active_value;
   }
 
-  // This simulates the consequences of the sent value getting committed and
-  // activated.
-  void AbortCommit() {
-    pending_base_ = T::ApplyDelta(pending_base_, reflected_delta_in_main_tree_);
-    active_base_ = T::ApplyDelta(active_base_, reflected_delta_in_main_tree_);
-    active_delta_ =
-        T::DeltaBetweenDeltas(active_delta_, reflected_delta_in_main_tree_);
-    reflected_delta_in_main_tree_ = T::IdentityDelta();
+  void AbortCommit(bool main_frame_applied_deltas) {
+    // Finish processing the delta that was sent to the main thread, and reset
+    // the corresponding the delta_in_main_tree_ variable. If
+    // main_frame_applied_deltas is true, we send the delta on to the active
+    // tree just as would happen for a successful commit. Otherwise, we treat
+    // the delta as never having been sent to the main thread and just drop it.
+    if (next_reflected_delta_in_main_tree_.has_value()) {
+      // If next_reflected_delta_in_main_tree_ is populated, we know two things:
+      //   - This abort corresponds to next_reflected_delta_in_main_tree_,
+      //     because we only send a "next" BeginMainFrame if the previous one
+      //     has already signaled "ready to commit".
+      //   - The previous main frame has not yet run commit. If it had, then
+      //     PushMainToPending would have promoted
+      //     next_reflected_delta_in_main_tree_ to reflected_delta_in_main_tree_
+      //     and next_reflected_delta_in_main_tree_ would be empty.
+      // In this case, if the main thread processed the delta from this aborted
+      // commit we can simply add the delta to reflected_delta_in_main_tree_.
+      if (main_frame_applied_deltas) {
+        reflected_delta_in_main_tree_ =
+            T::CombineDeltas(reflected_delta_in_main_tree_.value(),
+                             next_reflected_delta_in_main_tree_.value());
+      }
+      next_reflected_delta_in_main_tree_.reset();
+    } else {
+      // There is no "next" main frame, this abort was for the primary.
+      if (main_frame_applied_deltas) {
+        DeltaT delta =
+            reflected_delta_in_main_tree_.value_or(T::IdentityDelta());
+        // This simulates the consequences of the sent value getting committed
+        // and activated.
+        pending_base_ = T::ApplyDelta(pending_base_, delta);
+        active_base_ = T::ApplyDelta(active_base_, delta);
+        active_delta_ = T::DeltaBetweenDeltas(active_delta_, delta);
+      }
+      reflected_delta_in_main_tree_.reset();
+    }
   }
 
+  // Values sent to the main thread and not yet resolved in the pending or
+  // active tree.
+  const absl::optional<DeltaT>& reflected_delta_in_main_tree() const {
+    return reflected_delta_in_main_tree_;
+  }
+  const absl::optional<DeltaT>& next_reflected_delta_in_main_tree() const {
+    return next_reflected_delta_in_main_tree_;
+  }
   // Values as last pushed to the pending or active tree respectively, with no
   // impl-thread delta applied.
   BaseT PendingBase() const { return pending_base_; }
@@ -122,6 +171,12 @@
                                  reflected_delta_in_pending_tree_);
   }
 
+  DeltaT UnsentDelta() const {
+    return T::DeltaBetweenDeltas(
+        PendingDelta(),
+        reflected_delta_in_main_tree_.value_or(T::IdentityDelta()));
+  }
+
   void set_clobber_active_value() { clobber_active_value_ = true; }
 
  private:
@@ -134,9 +189,13 @@
   BaseT active_base_ = T::IdentityBase();
   // The difference between |active_base_| and the user-perceived value.
   DeltaT active_delta_ = T::IdentityDelta();
-  // The value sent to the main thread on the last BeginMainFrame.  This is
-  // always identity outside of the BeginMainFrame to (aborted)commit interval.
-  DeltaT reflected_delta_in_main_tree_ = T::IdentityDelta();
+  // A value sent to the main thread on a BeginMainFrame, but not yet applied to
+  // the resulting pending tree.
+  absl::optional<DeltaT> reflected_delta_in_main_tree_;
+  // A value sent to the main thread on a BeginMainFrame at a time when
+  // reflected_delta_in_main_tree_ is populated. This is used when a main frame
+  // is sent to the main thread before the previous one has committed.
+  absl::optional<DeltaT> next_reflected_delta_in_main_tree_;
   // The value that was sent to the main thread for BeginMainFrame for the
   // current pending tree.  This is always identity outside of the
   // BeginMainFrame to activation interval.
@@ -164,6 +223,7 @@
   static BaseT ApplyDelta(BaseT v, DeltaT delta) { return v + delta; }
   static DeltaT DeltaBetweenBases(BaseT v1, BaseT v2) { return v1 - v2; }
   static DeltaT DeltaBetweenDeltas(DeltaT d1, DeltaT d2) { return d1 - d2; }
+  static DeltaT CombineDeltas(DeltaT d1, DeltaT d2) { return d1 + d2; }
 };
 
 class ScaleGroup {
@@ -175,6 +235,7 @@
   static float ApplyDelta(float v, float delta) { return v * delta; }
   static float DeltaBetweenBases(float v1, float v2) { return v1 / v2; }
   static float DeltaBetweenDeltas(float d1, float d2) { return d1 / d2; }
+  static float CombineDeltas(float d1, float d2) { return d1 * d2; }
 };
 
 }  // namespace cc
diff --git a/cc/layers/layer_impl_unittest.cc b/cc/layers/layer_impl_unittest.cc
index 095e57d..eb85e3a7 100644
--- a/cc/layers/layer_impl_unittest.cc
+++ b/cc/layers/layer_impl_unittest.cc
@@ -417,7 +417,8 @@
                    scroll_tree(layer())->GetScrollOffsetBaseForTesting(
                        layer()->element_id()));
 
-  scroll_tree(layer())->ApplySentScrollDeltasFromAbortedCommit();
+  scroll_tree(layer())->ApplySentScrollDeltasFromAbortedCommit(
+      /*main_frame_applied_deltas=*/true);
 
   EXPECT_POINTF_EQ(scroll_offset + scroll_delta, CurrentScrollOffset(layer()));
   EXPECT_VECTOR2DF_EQ(scroll_delta - sent_scroll_delta, ScrollDelta(layer()));
diff --git a/cc/scheduler/commit_earlyout_reason.h b/cc/scheduler/commit_earlyout_reason.h
index 7fe59e4..050da07 100644
--- a/cc/scheduler/commit_earlyout_reason.h
+++ b/cc/scheduler/commit_earlyout_reason.h
@@ -32,7 +32,7 @@
   return "???";
 }
 
-inline bool CommitEarlyOutHandledCommit(CommitEarlyOutReason reason) {
+inline bool MainFrameAppliedDeltas(CommitEarlyOutReason reason) {
   return reason == CommitEarlyOutReason::FINISHED_NO_UPDATES;
 }
 
diff --git a/cc/trees/layer_tree_host_impl.cc b/cc/trees/layer_tree_host_impl.cc
index ac94526..6da2dc63 100644
--- a/cc/trees/layer_tree_host_impl.cc
+++ b/cc/trees/layer_tree_host_impl.cc
@@ -567,8 +567,10 @@
   // If the begin frame data was handled, then scroll and scale set was applied
   // by the main thread, so the active tree needs to be updated as if these sent
   // values were applied and committed.
-  if (CommitEarlyOutHandledCommit(reason)) {
-    active_tree_->ApplySentScrollAndScaleDeltasFromAbortedCommit();
+  bool main_frame_applied_deltas = MainFrameAppliedDeltas(reason);
+  active_tree_->ApplySentScrollAndScaleDeltasFromAbortedCommit(
+      main_frame_applied_deltas);
+  if (main_frame_applied_deltas) {
     if (pending_tree_) {
       pending_tree_->AppendSwapPromises(std::move(swap_promises));
     } else {
diff --git a/cc/trees/layer_tree_host_impl_unittest.cc b/cc/trees/layer_tree_host_impl_unittest.cc
index 916c156b..d50ecc7 100644
--- a/cc/trees/layer_tree_host_impl_unittest.cc
+++ b/cc/trees/layer_tree_host_impl_unittest.cc
@@ -138,6 +138,11 @@
   }
 };
 
+void ClearMainThreadDeltasForTesting(LayerTreeHostImpl* host) {
+  host->active_tree()->ApplySentScrollAndScaleDeltasFromAbortedCommit(
+      /*main_frame_applied_deltas=*/false);
+}
+
 }  // namespace
 
 class LayerTreeHostImplTest : public testing::Test,
@@ -360,6 +365,21 @@
         size, host_impl_->active_tree()->device_scale_factor());
   }
 
+  void PushScrollOffsetsToPendingTree(
+      const base::flat_map<ElementId, gfx::PointF>& offsets) {
+    PropertyTrees property_trees(*host_impl_);
+    for (auto& entry : offsets) {
+      property_trees.scroll_tree_mutable().SetBaseScrollOffset(entry.first,
+                                                               entry.second);
+    }
+    host_impl_->sync_tree()
+        ->property_trees()
+        ->scroll_tree_mutable()
+        .PushScrollUpdatesFromMainThread(
+            property_trees, host_impl_->sync_tree(),
+            host_impl_->settings().commit_fractional_scroll_deltas);
+  }
+
   static void ExpectClearedScrollDeltasRecursive(LayerImpl* root) {
     for (auto* layer : *root->layer_tree_impl())
       ASSERT_EQ(ScrollDelta(layer), gfx::Vector2d());
@@ -1095,8 +1115,6 @@
 
 TEST_P(ScrollUnifiedLayerTreeHostImplTest, ScrollDeltaRepeatedScrolls) {
   gfx::PointF scroll_offset(20, 30);
-  gfx::Vector2dF scroll_delta(11, -15);
-
   auto* root = SetupDefaultRootLayer(gfx::Size(110, 110));
   root->SetHitTestable(true);
   CreateScrollNode(root, gfx::Size(10, 10));
@@ -1106,25 +1124,121 @@
       .UpdateScrollOffsetBaseForTesting(root->element_id(), scroll_offset);
   UpdateDrawProperties(host_impl_->active_tree());
 
-  std::unique_ptr<CompositorCommitData> commit_data;
-
+  gfx::Vector2dF scroll_delta(11, -15);
+  std::unique_ptr<CompositorCommitData> commit_data1;
   root->ScrollBy(scroll_delta);
-  commit_data = host_impl_->ProcessCompositorDeltas();
-  ASSERT_EQ(commit_data->scrolls.size(), 1u);
+  commit_data1 = host_impl_->ProcessCompositorDeltas();
+  ASSERT_EQ(commit_data1->scrolls.size(), 1u);
   EXPECT_TRUE(
-      ScrollInfoContains(*commit_data, root->element_id(), scroll_delta));
+      ScrollInfoContains(*commit_data1, root->element_id(), scroll_delta));
+
+  std::unique_ptr<CompositorCommitData> commit_data2;
+  gfx::Vector2dF scroll_delta2(-5, 27);
+  root->ScrollBy(scroll_delta2);
+  commit_data2 = host_impl_->ProcessCompositorDeltas();
+  ASSERT_EQ(commit_data2->scrolls.size(), 1u);
+  EXPECT_TRUE(
+      ScrollInfoContains(*commit_data2, root->element_id(), scroll_delta2));
+
+  // Simulate first commit by pushing base scroll offsets to pending tree
+  PushScrollOffsetsToPendingTree(
+      {{root->element_id(), gfx::PointAtOffsetFromOrigin(scroll_delta)}});
+  EXPECT_EQ(host_impl_->sync_tree()
+                ->property_trees()
+                ->scroll_tree()
+                .GetScrollOffsetDeltaForTesting(root->element_id()),
+            scroll_delta2);
+
+  // Simulate second commit by pushing base scroll offsets to pending tree
+  PushScrollOffsetsToPendingTree(
+      {{root->element_id(), gfx::PointAtOffsetFromOrigin(scroll_delta2)}});
+  EXPECT_EQ(host_impl_->sync_tree()
+                ->property_trees()
+                ->scroll_tree()
+                .GetScrollOffsetDeltaForTesting(root->element_id()),
+            gfx::Vector2dF(0, 0));
+}
+
+TEST_P(ScrollUnifiedLayerTreeHostImplTest, SyncedScrollAbortedCommit) {
+  LayerTreeSettings settings = DefaultSettings();
+  settings.commit_to_active_tree = false;
+  CreateHostImpl(settings, CreateLayerTreeFrameSink());
+  CreatePendingTree();
+  gfx::PointF scroll_offset(20, 30);
+  auto* root = SetupDefaultRootLayer(gfx::Size(110, 110));
+  auto& scroll_tree =
+      root->layer_tree_impl()->property_trees()->scroll_tree_mutable();
+  root->SetHitTestable(true);
+
+  // SyncedProperty should be created on the pending tree and then pushed to the
+  // active tree, to avoid bifurcation. Simulate commit by pushing base scroll
+  // offsets to pending tree.
+  PushScrollOffsetsToPendingTree({{root->element_id(), gfx::PointF(0, 0)}});
+  host_impl_->active_tree()
+      ->property_trees()
+      ->scroll_tree_mutable()
+      .PushScrollUpdatesFromPendingTree(
+          host_impl_->pending_tree()->property_trees(),
+          host_impl_->active_tree());
+
+  CreateScrollNode(root, gfx::Size(10, 10));
+  auto* synced_scroll = scroll_tree.GetSyncedScrollOffset(root->element_id());
+  ASSERT_TRUE(synced_scroll);
+  scroll_tree.UpdateScrollOffsetBaseForTesting(root->element_id(),
+                                               scroll_offset);
+  UpdateDrawProperties(host_impl_->active_tree());
+
+  gfx::Vector2dF scroll_delta(11, -15);
+  root->ScrollBy(scroll_delta);
+  EXPECT_EQ(scroll_delta, synced_scroll->UnsentDelta());
+  host_impl_->ProcessCompositorDeltas();
+  EXPECT_TRUE(synced_scroll->reflected_delta_in_main_tree().has_value());
+  EXPECT_FALSE(synced_scroll->next_reflected_delta_in_main_tree().has_value());
+  EXPECT_EQ(scroll_delta,
+            synced_scroll->reflected_delta_in_main_tree().value());
 
   gfx::Vector2dF scroll_delta2(-5, 27);
   root->ScrollBy(scroll_delta2);
-  commit_data = host_impl_->ProcessCompositorDeltas();
-  ASSERT_EQ(commit_data->scrolls.size(), 1u);
-  EXPECT_TRUE(ScrollInfoContains(*commit_data, root->element_id(),
-                                 scroll_delta + scroll_delta2));
+  EXPECT_EQ(scroll_delta2, synced_scroll->UnsentDelta());
+  host_impl_->ProcessCompositorDeltas();
+  EXPECT_TRUE(synced_scroll->reflected_delta_in_main_tree().has_value());
+  EXPECT_TRUE(synced_scroll->next_reflected_delta_in_main_tree().has_value());
+  EXPECT_EQ(scroll_delta,
+            synced_scroll->reflected_delta_in_main_tree().value());
+  EXPECT_EQ(scroll_delta2,
+            synced_scroll->next_reflected_delta_in_main_tree().value());
 
-  root->ScrollBy(gfx::Vector2d());
-  commit_data = host_impl_->ProcessCompositorDeltas();
-  EXPECT_TRUE(ScrollInfoContains(*commit_data, root->element_id(),
-                                 scroll_delta + scroll_delta2));
+  // Simulate aborting the second main frame. Scroll deltas applied by the
+  // second frame should be combined with delta from first frame.
+  root->layer_tree_impl()->ApplySentScrollAndScaleDeltasFromAbortedCommit(
+      /*main_frame_applied_deltas=*/true);
+  EXPECT_TRUE(synced_scroll->reflected_delta_in_main_tree().has_value());
+  EXPECT_FALSE(synced_scroll->next_reflected_delta_in_main_tree().has_value());
+  EXPECT_EQ(scroll_delta + scroll_delta2,
+            synced_scroll->reflected_delta_in_main_tree().value());
+
+  // Send a third main frame, pipelined behind the first.
+  gfx::Vector2dF scroll_delta3(-2, -13);
+  root->ScrollBy(scroll_delta3);
+  EXPECT_EQ(scroll_delta3, synced_scroll->UnsentDelta());
+  host_impl_->ProcessCompositorDeltas();
+  EXPECT_TRUE(synced_scroll->reflected_delta_in_main_tree().has_value());
+  EXPECT_TRUE(synced_scroll->next_reflected_delta_in_main_tree().has_value());
+  EXPECT_EQ(scroll_delta + scroll_delta2,
+            synced_scroll->reflected_delta_in_main_tree().value());
+  EXPECT_EQ(scroll_delta3,
+            synced_scroll->next_reflected_delta_in_main_tree().value());
+
+  // Simulate commit of the first frame
+  PushScrollOffsetsToPendingTree(
+      {{root->element_id(), scroll_offset + scroll_delta + scroll_delta2}});
+  EXPECT_EQ(scroll_offset + scroll_delta + scroll_delta2,
+            synced_scroll->PendingBase());
+  EXPECT_EQ(scroll_delta3, synced_scroll->PendingDelta());
+  EXPECT_TRUE(synced_scroll->reflected_delta_in_main_tree().has_value());
+  EXPECT_FALSE(synced_scroll->next_reflected_delta_in_main_tree().has_value());
+  EXPECT_EQ(scroll_delta3,
+            synced_scroll->reflected_delta_in_main_tree().value());
 }
 
 TEST_F(CommitToPendingTreeLayerTreeHostImplTest,
@@ -3602,6 +3716,7 @@
     EXPECT_EQ(commit_data->page_scale_delta, page_scale_delta);
 
     EXPECT_EQ(gfx::PointF(75.0, 75.0), MaxScrollOffset(scroll_layer));
+    ClearMainThreadDeltasForTesting(host_impl_.get());
   }
 
   // Scrolling after a pinch gesture should always be in local space.  The
@@ -4193,6 +4308,7 @@
     std::unique_ptr<CompositorCommitData> commit_data =
         host_impl_->ProcessCompositorDeltas();
     EXPECT_EQ(commit_data->page_scale_delta, page_scale_delta);
+    ClearMainThreadDeltasForTesting(host_impl_.get());
   }
 
   // Zoom-in clamping
@@ -4216,6 +4332,7 @@
     std::unique_ptr<CompositorCommitData> commit_data =
         host_impl_->ProcessCompositorDeltas();
     EXPECT_EQ(commit_data->page_scale_delta, max_page_scale);
+    ClearMainThreadDeltasForTesting(host_impl_.get());
   }
 
   // Zoom-out clamping
@@ -4227,6 +4344,7 @@
         ->property_trees()
         ->scroll_tree_mutable()
         .CollectScrollDeltasForTesting();
+    ClearMainThreadDeltasForTesting(host_impl_.get());
     scroll_layer->layer_tree_impl()
         ->property_trees()
         ->scroll_tree_mutable()
@@ -4249,6 +4367,7 @@
     EXPECT_EQ(commit_data->page_scale_delta, min_page_scale);
 
     EXPECT_TRUE(commit_data->scrolls.empty());
+    ClearMainThreadDeltasForTesting(host_impl_.get());
   }
 
   // Two-finger panning should not happen based on pinch events only
@@ -4260,6 +4379,7 @@
         ->property_trees()
         ->scroll_tree_mutable()
         .CollectScrollDeltasForTesting();
+    ClearMainThreadDeltasForTesting(host_impl_.get());
     scroll_layer->layer_tree_impl()
         ->property_trees()
         ->scroll_tree_mutable()
@@ -4283,6 +4403,7 @@
         host_impl_->ProcessCompositorDeltas();
     EXPECT_EQ(commit_data->page_scale_delta, page_scale_delta);
     EXPECT_TRUE(commit_data->scrolls.empty());
+    ClearMainThreadDeltasForTesting(host_impl_.get());
   }
 
   // Two-finger panning should work with interleaved scroll events
@@ -4294,6 +4415,7 @@
         ->property_trees()
         ->scroll_tree_mutable()
         .CollectScrollDeltasForTesting();
+    ClearMainThreadDeltasForTesting(host_impl_.get());
     scroll_layer->layer_tree_impl()
         ->property_trees()
         ->scroll_tree_mutable()
@@ -4322,6 +4444,7 @@
     EXPECT_EQ(commit_data->page_scale_delta, page_scale_delta);
     EXPECT_TRUE(ScrollInfoContains(*commit_data, scroll_layer->element_id(),
                                    gfx::Vector2dF(-10, -10)));
+    ClearMainThreadDeltasForTesting(host_impl_.get());
   }
 
   // Two-finger panning should work when starting fully zoomed out.
@@ -4332,6 +4455,7 @@
         ->property_trees()
         ->scroll_tree_mutable()
         .CollectScrollDeltasForTesting();
+    ClearMainThreadDeltasForTesting(host_impl_.get());
     scroll_layer->layer_tree_impl()
         ->property_trees()
         ->scroll_tree_mutable()
@@ -4383,6 +4507,7 @@
       ->property_trees()
       ->scroll_tree_mutable()
       .CollectScrollDeltasForTesting();
+  ClearMainThreadDeltasForTesting(host_impl_.get());
   scroll_layer->layer_tree_impl()
       ->property_trees()
       ->scroll_tree_mutable()
@@ -4435,6 +4560,7 @@
       ->property_trees()
       ->scroll_tree_mutable()
       .CollectScrollDeltasForTesting();
+  ClearMainThreadDeltasForTesting(host_impl_.get());
   scroll_layer->layer_tree_impl()
       ->property_trees()
       ->scroll_tree_mutable()
@@ -4530,6 +4656,7 @@
     std::unique_ptr<CompositorCommitData> commit_data =
         host_impl_->ProcessCompositorDeltas();
     EXPECT_EQ(commit_data->page_scale_delta, 1);
+    ClearMainThreadDeltasForTesting(host_impl_.get());
   }
 
   start_time += base::Seconds(10);
@@ -4661,6 +4788,7 @@
     EXPECT_EQ(commit_data->page_scale_delta, 2);
     EXPECT_TRUE(ScrollInfoContains(*commit_data, scroll_layer->element_id(),
                                    gfx::Vector2dF(-50, -50)));
+    ClearMainThreadDeltasForTesting(host_impl_.get());
   }
 
   start_time += base::Seconds(10);
@@ -8424,16 +8552,19 @@
   UpdateDrawProperties(host_impl_->active_tree());
   host_impl_->active_tree()->DidBecomeActive();
 
+  gfx::PointF grand_child_base(0, 2);
+  gfx::Vector2dF grand_child_delta;
   grand_child_layer->layer_tree_impl()
       ->property_trees()
       ->scroll_tree_mutable()
       .UpdateScrollOffsetBaseForTesting(grand_child_layer->element_id(),
-                                        gfx::PointF(0, 2));
+                                        grand_child_base);
+  gfx::PointF child_base(0, 3);
+  gfx::Vector2dF child_delta;
   child_layer->layer_tree_impl()
       ->property_trees()
       ->scroll_tree_mutable()
-      .UpdateScrollOffsetBaseForTesting(child_layer->element_id(),
-                                        gfx::PointF(0, 3));
+      .UpdateScrollOffsetBaseForTesting(child_layer->element_id(), child_base);
 
   DrawFrame();
   {
@@ -8455,13 +8586,20 @@
         host_impl_->ProcessCompositorDeltas();
 
     // The grand child should have scrolled up to its limit.
+    grand_child_delta = gfx::Vector2dF(0, -2);
     EXPECT_TRUE(ScrollInfoContains(*commit_data.get(),
                                    grand_child_layer->element_id(),
-                                   gfx::Vector2dF(0, -2)));
+                                   grand_child_delta));
 
     // The child should not have scrolled.
     ExpectNone(*commit_data.get(), child_layer->element_id());
 
+    grand_child_base += grand_child_delta;
+    child_base += child_delta;
+    PushScrollOffsetsToPendingTree(
+        {{child_layer->element_id(), child_base},
+         {grand_child_layer->element_id(), grand_child_base}});
+
     // The next time we scroll we should only scroll the parent.
     scroll_delta = gfx::Vector2d(0, -3);
     EXPECT_EQ(ScrollThread::SCROLL_ON_IMPL_THREAD,
@@ -8481,16 +8619,23 @@
               child_layer->scroll_tree_index());
     GetInputHandler().ScrollEnd();
 
+    ClearMainThreadDeltasForTesting(host_impl_.get());
     commit_data = host_impl_->ProcessCompositorDeltas();
 
     // The child should have scrolled up to its limit.
-    EXPECT_TRUE(ScrollInfoContains(
-        *commit_data.get(), child_layer->element_id(), gfx::Vector2dF(0, -3)));
+    child_delta = gfx::Vector2dF(0, -3);
+    EXPECT_TRUE(ScrollInfoContains(*commit_data.get(),
+                                   child_layer->element_id(), child_delta));
 
     // The grand child should not have scrolled.
-    EXPECT_TRUE(ScrollInfoContains(*commit_data.get(),
-                                   grand_child_layer->element_id(),
-                                   gfx::Vector2dF(0, -2)));
+    grand_child_delta = gfx::Vector2dF();
+    ExpectNone(*commit_data.get(), grand_child_layer->element_id());
+
+    child_base += child_delta;
+    grand_child_base += grand_child_delta;
+    PushScrollOffsetsToPendingTree(
+        {{grand_child_layer->element_id(), grand_child_base},
+         {child_layer->element_id(), child_base}});
 
     // After scrolling the parent, another scroll on the opposite direction
     // should still scroll the child.
@@ -8512,16 +8657,24 @@
               grand_child_layer->scroll_tree_index());
     GetInputHandler().ScrollEnd();
 
+    ClearMainThreadDeltasForTesting(host_impl_.get());
     commit_data = host_impl_->ProcessCompositorDeltas();
 
     // The grand child should have scrolled.
+    grand_child_delta = gfx::Vector2dF(0, 7);
     EXPECT_TRUE(ScrollInfoContains(*commit_data.get(),
                                    grand_child_layer->element_id(),
-                                   gfx::Vector2dF(0, 5)));
+                                   grand_child_delta));
 
     // The child should not have scrolled.
-    EXPECT_TRUE(ScrollInfoContains(
-        *commit_data.get(), child_layer->element_id(), gfx::Vector2dF(0, -3)));
+    child_delta = gfx::Vector2dF();
+    ExpectNone(*commit_data.get(), child_layer->element_id());
+
+    grand_child_base += grand_child_delta;
+    child_base += child_delta;
+    PushScrollOffsetsToPendingTree(
+        {{grand_child_layer->element_id(), grand_child_base},
+         {child_layer->element_id(), child_base}});
 
     // Scrolling should be adjusted from viewport space.
     host_impl_->active_tree()->PushPageScaleFromMainThread(2, 2, 2);
@@ -8543,12 +8696,13 @@
             .get());
     GetInputHandler().ScrollEnd();
 
+    ClearMainThreadDeltasForTesting(host_impl_.get());
     commit_data = host_impl_->ProcessCompositorDeltas();
 
-    // Should have scrolled by half the amount in layer space (5 - 2/2)
+    // Should have scrolled by half the amount in layer space (-2/2)
     EXPECT_TRUE(ScrollInfoContains(*commit_data.get(),
                                    grand_child_layer->element_id(),
-                                   gfx::Vector2dF(0, 4)));
+                                   gfx::Vector2dF(0, -1)));
   }
 }
 
@@ -8691,6 +8845,10 @@
   EXPECT_TRUE(ScrollInfoContains(*commit_data.get(), scroll_layer->element_id(),
                                  gfx::Vector2dF(0, gesture_scroll_delta.x())));
 
+  // Push scrolls to pending tree
+  PushScrollOffsetsToPendingTree(
+      {{scroll_layer->element_id(), gfx::PointF(10, 0)}});
+
   // Reset and scroll down with the wheel.
   SetScrollOffsetDelta(scroll_layer, gfx::Vector2dF());
   gfx::Vector2dF wheel_scroll_delta(0, 10);
@@ -8773,6 +8931,10 @@
     // The root scroll layer should not have scrolled, because the input delta
     // was close to the layer's axis of movement.
     EXPECT_EQ(commit_data->scrolls.size(), 1u);
+
+    PushScrollOffsetsToPendingTree(
+        {{child_scroll_id,
+          gfx::PointAtOffsetFromOrigin(expected_scroll_delta)}});
   }
   {
     // Now reset and scroll the same amount horizontally.
@@ -8804,6 +8966,10 @@
 
     // The root scroll layer shouldn't have scrolled.
     ExpectNone(*commit_data.get(), scroll_layer->element_id());
+
+    PushScrollOffsetsToPendingTree(
+        {{child_scroll_id,
+          gfx::PointAtOffsetFromOrigin(expected_scroll_delta)}});
   }
 }
 
@@ -8883,6 +9049,11 @@
     // The root scroll layer should not have scrolled, because the input delta
     // was close to the layer's axis of movement.
     EXPECT_EQ(commit_data->scrolls.size(), 1u);
+
+    PushScrollOffsetsToPendingTree(
+        {{child->element_id(),
+          gfx::PointAtOffsetFromOrigin(expected_scroll_deltas[i])}});
+    ClearMainThreadDeltasForTesting(host_impl_.get());
   }
 }
 
@@ -8918,6 +9089,9 @@
       host_impl_->ProcessCompositorDeltas();
   EXPECT_TRUE(ScrollInfoContains(*commit_data.get(), scroll_layer->element_id(),
                                  gfx::Vector2dF(0, scroll_delta.y() / scale)));
+  PushScrollOffsetsToPendingTree(
+      {{scroll_layer->element_id(), gfx::PointAtOffsetFromOrigin(gfx::Vector2dF(
+                                        0, scroll_delta.y() / scale))}});
 
   // Reset and scroll down with the wheel.
   SetScrollOffsetDelta(scroll_layer, gfx::Vector2dF());
diff --git a/cc/trees/layer_tree_impl.cc b/cc/trees/layer_tree_impl.cc
index dfd7e261..8f8b9293 100644
--- a/cc/trees/layer_tree_impl.cc
+++ b/cc/trees/layer_tree_impl.cc
@@ -1483,19 +1483,21 @@
       gfx::Rect(root_scroll_node->bounds));
 }
 
-void LayerTreeImpl::ApplySentScrollAndScaleDeltasFromAbortedCommit() {
+void LayerTreeImpl::ApplySentScrollAndScaleDeltasFromAbortedCommit(
+    bool main_frame_applied_deltas) {
   DCHECK(IsActiveTree());
 
-  page_scale_factor()->AbortCommit();
-  top_controls_shown_ratio()->AbortCommit();
-  elastic_overscroll()->AbortCommit();
+  page_scale_factor()->AbortCommit(main_frame_applied_deltas);
+  top_controls_shown_ratio()->AbortCommit(main_frame_applied_deltas);
+  bottom_controls_shown_ratio()->AbortCommit(main_frame_applied_deltas);
+  elastic_overscroll()->AbortCommit(main_frame_applied_deltas);
 
   if (layer_list_.empty())
     return;
 
   property_trees()
       ->scroll_tree_mutable()
-      .ApplySentScrollDeltasFromAbortedCommit();
+      .ApplySentScrollDeltasFromAbortedCommit(main_frame_applied_deltas);
 }
 
 void LayerTreeImpl::SetViewportPropertyIds(const ViewportPropertyIds& ids) {
diff --git a/cc/trees/layer_tree_impl.h b/cc/trees/layer_tree_impl.h
index c4602129..f1d2b0b 100644
--- a/cc/trees/layer_tree_impl.h
+++ b/cc/trees/layer_tree_impl.h
@@ -360,7 +360,8 @@
   void SetCurrentlyScrollingNode(const ScrollNode* node);
   void ClearCurrentlyScrollingNode();
 
-  void ApplySentScrollAndScaleDeltasFromAbortedCommit();
+  void ApplySentScrollAndScaleDeltasFromAbortedCommit(
+      bool main_frame_applied_deltas);
 
   SkColor4f background_color() const { return background_color_; }
   void set_background_color(SkColor4f color) { background_color_ = color; }
diff --git a/cc/trees/property_tree.cc b/cc/trees/property_tree.cc
index d41648f5..b85b4b5 100644
--- a/cc/trees/property_tree.cc
+++ b/cc/trees/property_tree.cc
@@ -1704,10 +1704,15 @@
   for (auto map_entry = synced_scroll_offset_map_.begin();
        map_entry != synced_scroll_offset_map_.end();) {
     ElementId id = map_entry->first;
-    if (main_scroll_offset_map.find(id) == main_scroll_offset_map.end())
+    if (main_scroll_offset_map.find(id) == main_scroll_offset_map.end()) {
+      // This SyncedScrollOffset might still be used to send a delta from the
+      // active tree to the main thread, so we need to clear out the delta that
+      // was sent to the main thread for this commit.
+      map_entry->second->PushMainToPending(map_entry->second->Current(true));
       map_entry = synced_scroll_offset_map_.erase(map_entry);
-    else
+    } else {
       map_entry++;
+    }
   }
 
   for (auto map_entry : main_scroll_offset_map) {
@@ -1763,10 +1768,11 @@
   }
 }
 
-void ScrollTree::ApplySentScrollDeltasFromAbortedCommit() {
+void ScrollTree::ApplySentScrollDeltasFromAbortedCommit(
+    bool main_frame_applied_deltas) {
   DCHECK(property_trees()->is_active());
   for (auto& map_entry : synced_scroll_offset_map_)
-    map_entry.second->AbortCommit();
+    map_entry.second->AbortCommit(main_frame_applied_deltas);
 }
 
 void ScrollTree::SetBaseScrollOffset(ElementId id,
diff --git a/cc/trees/property_tree.h b/cc/trees/property_tree.h
index 7837efc..6c05ffe 100644
--- a/cc/trees/property_tree.h
+++ b/cc/trees/property_tree.h
@@ -538,7 +538,7 @@
 
   // Applies deltas sent in the previous main frame onto the impl thread state.
   // Should only be called on the impl thread side PropertyTrees.
-  void ApplySentScrollDeltasFromAbortedCommit();
+  void ApplySentScrollDeltasFromAbortedCommit(bool main_frame_applied_deltas);
 
   // Pushes scroll updates from the ScrollTree on the main thread onto the
   // impl thread associated state.
diff --git a/chrome/VERSION b/chrome/VERSION
index 555ef04..84d75ff 100644
--- a/chrome/VERSION
+++ b/chrome/VERSION
@@ -1,4 +1,4 @@
 MAJOR=106
 MINOR=0
-BUILD=5210
+BUILD=5211
 PATCH=0
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/app/ChromeActivity.java b/chrome/android/java/src/org/chromium/chrome/browser/app/ChromeActivity.java
index 9e45963..d9bd868 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/app/ChromeActivity.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/app/ChromeActivity.java
@@ -2116,13 +2116,13 @@
             if (mTabReparentingControllerSupplier.get() != null && didChangeTabletMode()) {
                 onScreenLayoutSizeChange();
             }
-            // We only handle VR UI mode and UI mode night changes. Any other changes should follow
-            // the default behavior of recreating the activity. Note that if UI mode night changes,
-            // with or without other changes, we will still recreate() until we get a callback from
-            // the ChromeBaseAppCompatActivity#onNightModeStateChanged or the overridden method in
+            // For UI mode type, we only need to recreate for TELEVISION to update refresh rate.
+            // Note that if UI mode night changes, with or without other changes, we will
+            // still recreate() when we get a callback from the
+            // ChromeBaseAppCompatActivity#onNightModeStateChanged or the overridden method in
             // sub-classes if necessary.
-            if (didChangeNonVrUiMode(mConfig.uiMode, newConfig.uiMode)
-                    && !didChangeUiModeNight(mConfig.uiMode, newConfig.uiMode)) {
+            if (didChangeUiModeType(
+                        mConfig.uiMode, newConfig.uiMode, Configuration.UI_MODE_TYPE_TELEVISION)) {
                 recreate();
                 return;
             }
@@ -2145,18 +2145,14 @@
         mConfig = newConfig;
     }
 
-    private static boolean didChangeNonVrUiMode(int oldMode, int newMode) {
-        if (oldMode == newMode) return false;
-        return isInVrUiMode(oldMode) == isInVrUiMode(newMode);
+    // Checks whether the given uiModeTypes were present on oldUiMode or newUiMode but not the
+    // other.
+    private static boolean didChangeUiModeType(int oldUiMode, int newUiMode, int uiModeType) {
+        return isInUiModeType(oldUiMode, uiModeType) != isInUiModeType(newUiMode, uiModeType);
     }
 
-    private static boolean isInVrUiMode(int uiMode) {
-        return (uiMode & Configuration.UI_MODE_TYPE_MASK) == Configuration.UI_MODE_TYPE_VR_HEADSET;
-    }
-
-    private static boolean didChangeUiModeNight(int oldMode, int newMode) {
-        return (oldMode & Configuration.UI_MODE_NIGHT_MASK)
-                != (newMode & Configuration.UI_MODE_NIGHT_MASK);
+    private static boolean isInUiModeType(int uiMode, int uiModeType) {
+        return (uiMode & Configuration.UI_MODE_TYPE_MASK) == uiModeType;
     }
 
     /**
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/TabPersistentStore.java b/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/TabPersistentStore.java
index 7bf7260..19b298b 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/TabPersistentStore.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/tabmodel/TabPersistentStore.java
@@ -103,6 +103,9 @@
     private static final int MIGRATE_TO_CRITICAL_PERSISTED_TAB_DATA_DEFAULT_BATCH_SIZE = 5;
     private static final String MIGRATE_TO_CRITICAL_PERSISTED_TAB_DATA_BATCH_SIZE_PARAM =
             "migrate_to_critical_persisted_tab_data_batch_size";
+    private static final String SAVE_CRITICAL_PERSISTED_TAB_DATA_NO_RESTORE =
+            "save_critical_persisted_tab_data_no_restore";
+
     private TabModelObserver mTabModelObserver;
 
     @IntDef({ActiveTabState.OTHER, ActiveTabState.NTP, ActiveTabState.EMPTY})
@@ -1359,7 +1362,8 @@
             if (mDestroyed || isCancelled()) return;
             if (mStateSaved) {
                 if (!mTab.isDestroyed()) TabStateAttributes.from(mTab).setIsTabStateDirty(false);
-                mTab.setIsTabSaveEnabled(isCriticalPersistedTabDataEnabled());
+                mTab.setIsTabSaveEnabled(isCriticalPersistedTabDataEnabled()
+                        || isCriticalPersistedTabDataSavingEnabled());
                 migrateSomeRemainingTabsToCriticalPersistedTabData();
             }
             mSaveTabTask = null;
@@ -1666,6 +1670,15 @@
         return ChromeFeatureList.sCriticalPersistedTabData.isEnabled();
     }
 
+    private static boolean isCriticalPersistedTabDataSavingEnabled() {
+        if (FeatureList.isInitialized()) {
+            return ChromeFeatureList.getFieldTrialParamByFeatureAsBoolean(
+                    ChromeFeatureList.CRITICAL_PERSISTED_TAB_DATA,
+                    SAVE_CRITICAL_PERSISTED_TAB_DATA_NO_RESTORE, false);
+        }
+        return false;
+    }
+
     private class LoadTabTask extends AsyncTask<TabState> {
         private final TabRestoreDetails mTabToRestore;
         private TabState mTabState;
@@ -1939,7 +1952,9 @@
     }
 
     private void migrateSomeRemainingTabsToCriticalPersistedTabData() {
-        if (!isCriticalPersistedTabDataEnabled()) return;
+        if (!isCriticalPersistedTabDataEnabled() && !isCriticalPersistedTabDataSavingEnabled()) {
+            return;
+        }
         int numMigrated = 0;
         while (numMigrated < getMigrateToCriticalPersistedTabDataBatchSize()
                 && !mTabsToMigrate.isEmpty()) {
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/omnibox/suggestions/OmniboxPedalsRenderTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/omnibox/suggestions/OmniboxPedalsRenderTest.java
index bc5550d..438f399 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/omnibox/suggestions/OmniboxPedalsRenderTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/omnibox/suggestions/OmniboxPedalsRenderTest.java
@@ -130,6 +130,7 @@
     @Test
     @MediumTest
     @Feature("RenderTest")
+    @DisabledTest(message = "crbug.com/1348691")
     public void testRunChromeSafetyCheckPedal() throws IOException, InterruptedException {
         List<AutocompleteMatch> suggestionsList = new ArrayList<>();
         suggestionsList.add(
diff --git a/chrome/app/chromeos_strings.grdp b/chrome/app/chromeos_strings.grdp
index 891e82bc..c52b1ab 100644
--- a/chrome/app/chromeos_strings.grdp
+++ b/chrome/app/chromeos_strings.grdp
@@ -3318,6 +3318,21 @@
   <message name="IDS_NETWORK_UI_REFRESH_TETHERING_CAPABILITIES_BUTTON_TEXT" desc="Text for button which, when pressed, refreshing the tethering capabilities.">
     Refresh Tethering Capabilities
   </message>
+  <message name="IDS_NETWORK_UI_TETHERING_STATUS_LABEL" desc="Title for the section for displaying and refreshing tethering status.">
+    Tethering Status:
+  </message>
+  <message name="IDS_NETWORK_UI_REFRESH_TETHERING_STATUS_BUTTON_TEXT" desc="Text for button which, when pressed, refresh the tethering status.">
+    Refresh Tethering Status
+  </message>
+  <message name="IDS_NETWORK_UI_TETHERING_CONFIG_LABEL" desc="Title for the section for displaying and refreshing tethering configuration.">
+    Tethering Configuration:
+  </message>
+  <message name="IDS_NETWORK_UI_REFRESH_TETHERING_CONFIG_BUTTON_TEXT" desc="Text for button which, when pressed, refresh the tethering configuration.">
+    Refresh Tethering Configuration
+  </message>
+  <message name="IDS_NETWORK_UI_SET_TETHERING_CONFIG_BUTTON_TEXT" desc="Text for button which, when pressed, set the tethering configuration.">
+    Set Tethering Configuration
+  </message>
 
   <message name="IDS_DEVICE_LOG_LINK_TEXT" desc="Message preceeding link to chrome://device-log">
     To view network UI logs, see: <ph name="DEVICE_LOG_LINK">&lt;a href="chrome://device-log"&gt;chrome://device-log&lt;/a&gt;</ph>
diff --git a/chrome/app/chromeos_strings_grdp/IDS_NETWORK_UI_REFRESH_TETHERING_CONFIG_BUTTON_TEXT.png.sha1 b/chrome/app/chromeos_strings_grdp/IDS_NETWORK_UI_REFRESH_TETHERING_CONFIG_BUTTON_TEXT.png.sha1
new file mode 100644
index 0000000..ec3b0a35
--- /dev/null
+++ b/chrome/app/chromeos_strings_grdp/IDS_NETWORK_UI_REFRESH_TETHERING_CONFIG_BUTTON_TEXT.png.sha1
@@ -0,0 +1 @@
+359e626996b055b45dd8d7cd89b05c5a52ba2aef
\ No newline at end of file
diff --git a/chrome/app/chromeos_strings_grdp/IDS_NETWORK_UI_REFRESH_TETHERING_STATUS_BUTTON_TEXT.png.sha1 b/chrome/app/chromeos_strings_grdp/IDS_NETWORK_UI_REFRESH_TETHERING_STATUS_BUTTON_TEXT.png.sha1
new file mode 100644
index 0000000..742ce6c
--- /dev/null
+++ b/chrome/app/chromeos_strings_grdp/IDS_NETWORK_UI_REFRESH_TETHERING_STATUS_BUTTON_TEXT.png.sha1
@@ -0,0 +1 @@
+2644a711af3e49965af1874691b3d1adfc3a3c98
\ No newline at end of file
diff --git a/chrome/app/chromeos_strings_grdp/IDS_NETWORK_UI_SET_TETHERING_CONFIG_BUTTON_TEXT.png.sha1 b/chrome/app/chromeos_strings_grdp/IDS_NETWORK_UI_SET_TETHERING_CONFIG_BUTTON_TEXT.png.sha1
new file mode 100644
index 0000000..b4cec5ff
--- /dev/null
+++ b/chrome/app/chromeos_strings_grdp/IDS_NETWORK_UI_SET_TETHERING_CONFIG_BUTTON_TEXT.png.sha1
@@ -0,0 +1 @@
+159653b32be20880d5394f881922a9c92f823351
\ No newline at end of file
diff --git a/chrome/app/chromeos_strings_grdp/IDS_NETWORK_UI_TETHERING_CONFIG_LABEL.png.sha1 b/chrome/app/chromeos_strings_grdp/IDS_NETWORK_UI_TETHERING_CONFIG_LABEL.png.sha1
new file mode 100644
index 0000000..c92987f
--- /dev/null
+++ b/chrome/app/chromeos_strings_grdp/IDS_NETWORK_UI_TETHERING_CONFIG_LABEL.png.sha1
@@ -0,0 +1 @@
+f390396c9b9a6ab0b6ddc8a56f54c3257dce521d
\ No newline at end of file
diff --git a/chrome/app/chromeos_strings_grdp/IDS_NETWORK_UI_TETHERING_STATUS_LABEL.png.sha1 b/chrome/app/chromeos_strings_grdp/IDS_NETWORK_UI_TETHERING_STATUS_LABEL.png.sha1
new file mode 100644
index 0000000..8d500f42
--- /dev/null
+++ b/chrome/app/chromeos_strings_grdp/IDS_NETWORK_UI_TETHERING_STATUS_LABEL.png.sha1
@@ -0,0 +1 @@
+d1474e1d457a8ede1115e766c1447d8eed111552
\ No newline at end of file
diff --git a/chrome/app/os_settings_search_tag_strings.grdp b/chrome/app/os_settings_search_tag_strings.grdp
index 456717b..ddb06a1 100644
--- a/chrome/app/os_settings_search_tag_strings.grdp
+++ b/chrome/app/os_settings_search_tag_strings.grdp
@@ -229,6 +229,9 @@
  <message name="IDS_OS_SETTINGS_TAG_FAST_PAIR_TURN_OFF_ALT1" desc="Text for search result item which, when clicked, navigates the user to Bluetooth settings, with a toggle to turn off Fast Pair. Alternate phrase for: 'Turn off Fast Pair'">
     Disable Fast Pair
   </message>
+  <message name="IDS_OS_SETTINGS_TAG_FAST_PAIR_SAVED_DEVICES" desc="Text for search result item which, when clicked, navigates the user to Bluetooth settings Saved Devices subpage">
+    Saved devices
+  </message>
 
   <!-- MultiDevice section. -->
   <message name="IDS_OS_SETTINGS_TAG_MULTIDEVICE" desc="Text for search result item which, when clicked, navigates the user to multi-device settings (connects Android phone to Chromebook). Alternate phrase for: 'Phone'">
diff --git a/chrome/app/os_settings_search_tag_strings_grdp/IDS_OS_SETTINGS_TAG_FAST_PAIR_SAVED_DEVICES.png.sha1 b/chrome/app/os_settings_search_tag_strings_grdp/IDS_OS_SETTINGS_TAG_FAST_PAIR_SAVED_DEVICES.png.sha1
new file mode 100644
index 0000000..214fc25a
--- /dev/null
+++ b/chrome/app/os_settings_search_tag_strings_grdp/IDS_OS_SETTINGS_TAG_FAST_PAIR_SAVED_DEVICES.png.sha1
@@ -0,0 +1 @@
+1f28e8cf4cfbbe5418c0a4a783636654d35d3029
\ No newline at end of file
diff --git a/chrome/app/os_settings_strings.grdp b/chrome/app/os_settings_strings.grdp
index 523a6d2..928a9a6 100644
--- a/chrome/app/os_settings_strings.grdp
+++ b/chrome/app/os_settings_strings.grdp
@@ -63,6 +63,9 @@
   <message name="IDS_SETTINGS_ABOUT_PAGE_RELAUNCH_AND_POWERWASH" desc="The label for the button that relaunches and powerwashes the browser once update is complete">
     Restart and reset
   </message>
+  <message name="IDS_SETTINGS_ABOUT_PAGE_RELAUNCH_AND_AUTO_UPDATE" desc="The label for the button that relaunches and enables automatic updates">
+    Restart and get automatic updates
+  </message>
   <message name="IDS_SETTINGS_UPGRADE_SUCCESSFUL_RELAUNCH" desc="Status label: Successfully updated ChromiumOS/ChromeOS">
     Nearly up to date! Restart your device to finish updating.
   </message>
diff --git a/chrome/app/os_settings_strings_grdp/IDS_SETTINGS_ABOUT_PAGE_RELAUNCH_AND_AUTO_UPDATE.png.sha1 b/chrome/app/os_settings_strings_grdp/IDS_SETTINGS_ABOUT_PAGE_RELAUNCH_AND_AUTO_UPDATE.png.sha1
new file mode 100644
index 0000000..f33f926e
--- /dev/null
+++ b/chrome/app/os_settings_strings_grdp/IDS_SETTINGS_ABOUT_PAGE_RELAUNCH_AND_AUTO_UPDATE.png.sha1
@@ -0,0 +1 @@
+b3fc98bbde3af98476650abb9416c1ea6e1a5d75
\ No newline at end of file
diff --git a/chrome/app/settings_strings.grdp b/chrome/app/settings_strings.grdp
index 2e3a106..6f1c991 100644
--- a/chrome/app/settings_strings.grdp
+++ b/chrome/app/settings_strings.grdp
@@ -1287,8 +1287,8 @@
     <message name="IDS_SETTINGS_LANGUAGES_EXPAND_ACCESSIBILITY_LABEL" desc="Label for the button that toggles showing the language options. Only visible by screen reader software.">
       Show language options
     </message>
-    <message name="IDS_SETTINGS_LANGUAGES_BROWSER_LANGUAGES_LIST_ORDERING_INSTRUCTIONS" desc="Explanatory message about ordering the list of languages.">
-      Order languages based on your preference
+    <message name="IDS_SETTINGS_LANGUAGES_PREFERRED_LANGUAGES_DESC" desc="Explanatory message about the list of preferred languages.">
+      Websites will show content in your preferred languages, when possible
     </message>
     <message name="IDS_SETTINGS_LANGUAGES_OFFER_TO_TRANSLATE_IN_THIS_LANGUAGE" desc="The label for a checkbox which indicates whether or not pages in this language should be translated by default.">
       Offer to translate pages in this language
diff --git a/chrome/app/settings_strings_grdp/IDS_SETTINGS_LANGUAGES_PREFERRED_LANGUAGES_DESC.png.sha1 b/chrome/app/settings_strings_grdp/IDS_SETTINGS_LANGUAGES_PREFERRED_LANGUAGES_DESC.png.sha1
new file mode 100644
index 0000000..5b6c75e
--- /dev/null
+++ b/chrome/app/settings_strings_grdp/IDS_SETTINGS_LANGUAGES_PREFERRED_LANGUAGES_DESC.png.sha1
@@ -0,0 +1 @@
+98ffb44cda9e1e09e6f5a94faca77a641d7cdf09
\ No newline at end of file
diff --git a/chrome/browser/accessibility/ax_screen_ai_annotator.cc b/chrome/browser/accessibility/ax_screen_ai_annotator.cc
index 22085df..bc458ef 100644
--- a/chrome/browser/accessibility/ax_screen_ai_annotator.cc
+++ b/chrome/browser/accessibility/ax_screen_ai_annotator.cc
@@ -35,11 +35,25 @@
   if (!native_view)
     return;
 
+// TODO(https://crbug.com/1278249): Add UMA for screenshot timing to ensure
+// the sync method is not blocking the browser process.
+#if BUILDFLAG(IS_MAC)
+  gfx::Image snapshot;
+  if (!ui::GrabViewSnapshot(native_view, gfx::Rect(web_contents->GetSize()),
+                            &snapshot)) {
+    VLOG(1) << "AxScreenAIAnnotator could not grab snapshot.";
+    return;
+  }
+
+  AXScreenAIAnnotator::OnScreenshotReceived(
+      web_contents->GetPrimaryMainFrame()->GetAXTreeID(), std::move(snapshot));
+#else
   ui::GrabViewSnapshotAsync(
       native_view, gfx::Rect(web_contents->GetSize()),
       base::BindOnce(&AXScreenAIAnnotator::OnScreenshotReceived,
                      weak_ptr_factory_.GetWeakPtr(),
                      web_contents->GetPrimaryMainFrame()->GetAXTreeID()));
+#endif
 }
 
 void AXScreenAIAnnotator::OnScreenshotReceived(const ui::AXTreeID& ax_tree_id,
diff --git a/chrome/browser/accessibility/ax_screen_ai_annotator.h b/chrome/browser/accessibility/ax_screen_ai_annotator.h
index fe64d751..ef179ddd 100644
--- a/chrome/browser/accessibility/ax_screen_ai_annotator.h
+++ b/chrome/browser/accessibility/ax_screen_ai_annotator.h
@@ -22,7 +22,7 @@
 class AXScreenAIAnnotator {
  public:
   explicit AXScreenAIAnnotator(Browser* browser);
-  ~AXScreenAIAnnotator();
+  virtual ~AXScreenAIAnnotator();
   AXScreenAIAnnotator(const AXScreenAIAnnotator&) = delete;
   AXScreenAIAnnotator& operator=(const AXScreenAIAnnotator&) = delete;
 
@@ -34,8 +34,8 @@
   // Receives an screenshot and sends it to ScreenAI library for processing.
   // |ax_tree_id| represents the accessibility tree that is associated with the
   // snapshot at the time of triggering the request.
-  void OnScreenshotReceived(const ui::AXTreeID& ax_tree_id,
-                            gfx::Image snapshot);
+  virtual void OnScreenshotReceived(const ui::AXTreeID& ax_tree_id,
+                                    gfx::Image snapshot);
 
   // Receives the annotations from ScreenAI service. |ax_tree_id| is the id of
   // the accessibility tree associated with the snapshot that was sent to
diff --git a/chrome/browser/accessibility/screen_ai_service_browsertest.cc b/chrome/browser/accessibility/screen_ai_service_browsertest.cc
new file mode 100644
index 0000000..f5e840575
--- /dev/null
+++ b/chrome/browser/accessibility/screen_ai_service_browsertest.cc
@@ -0,0 +1,61 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/accessibility/ax_screen_ai_annotator.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/test/base/in_process_browser_test.h"
+#include "content/public/test/browser_test.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "ui/gfx/image/image.h"
+
+namespace screen_ai {
+
+namespace {
+
+class MockAXScreenAIAnnotator : public AXScreenAIAnnotator {
+ public:
+  explicit MockAXScreenAIAnnotator(Browser* browser)
+      : AXScreenAIAnnotator(browser) {}
+  MOCK_METHOD(void,
+              OnScreenshotReceived,
+              (const ui::AXTreeID& ax_tree_id, gfx::Image snapshot),
+              (override));
+};
+
+}  // namespace
+
+using ScreenAIServiceTest = InProcessBrowserTest;
+
+// https://crbug.com/1348280: Creating AXScreenAIAnnotator triggers the sandbox
+// on Mac11 which is not implemented yet.
+#if BUILDFLAG(IS_MAC)
+#define MAYBE_ScreenshotTest DISABLED_ScreenshotTest
+#else
+#define MAYBE_ScreenshotTest ScreenshotTest
+#endif
+IN_PROC_BROWSER_TEST_F(ScreenAIServiceTest, MAYBE_ScreenshotTest) {
+  MockAXScreenAIAnnotator* annotator = new MockAXScreenAIAnnotator(browser());
+  browser()->SetScreenAIAnnotatorForTesting(
+      std::unique_ptr<AXScreenAIAnnotator>(annotator));
+
+  base::RunLoop run_loop;
+
+  EXPECT_CALL(*annotator, OnScreenshotReceived)
+      .WillOnce(
+          [&run_loop](const ui::AXTreeID& ax_tree_id, gfx::Image snapshot) {
+            EXPECT_FALSE(snapshot.IsEmpty());
+            EXPECT_GT(snapshot.Size().width(), 0);
+            EXPECT_GT(snapshot.Size().height(), 0);
+            run_loop.Quit();
+          });
+
+  browser()->RunScreenAIAnnotator();
+  run_loop.Run();
+
+  // TODO(https://crbug.com/1278249): Expect OnAnnotationReceived once library
+  // binary is available for test.
+}
+
+}  // namespace screen_ai
diff --git a/chrome/browser/apps/app_service/publishers/extension_apps_chromeos.h b/chrome/browser/apps/app_service/publishers/extension_apps_chromeos.h
index 333e1a0..2e83ec8 100644
--- a/chrome/browser/apps/app_service/publishers/extension_apps_chromeos.h
+++ b/chrome/browser/apps/app_service/publishers/extension_apps_chromeos.h
@@ -51,8 +51,6 @@
 // there are 2 ExtensionAppsChromeOs publishers for browser extensions and
 // Chrome apps(including hosted apps) separately.
 //
-// In the future, desktop PWAs will be migrated to a new system.
-//
 // See components/services/app_service/README.md.
 class ExtensionAppsChromeOs : public ExtensionAppsBase,
                               public extensions::AppWindowRegistry::Observer,
diff --git a/chrome/browser/ash/login/lock/fingerprint_unlock_browsertest.cc b/chrome/browser/ash/login/lock/fingerprint_unlock_browsertest.cc
index 4d19119..bebf6b7 100644
--- a/chrome/browser/ash/login/lock/fingerprint_unlock_browsertest.cc
+++ b/chrome/browser/ash/login/lock/fingerprint_unlock_browsertest.cc
@@ -429,6 +429,8 @@
     "Fingerprint.Unlock.AuthSuccessful";
 constexpr char kFingerprintAttemptsCountBeforeSuccessHistogramName[] =
     "Fingerprint.Unlock.AttemptsCountBeforeSuccess";
+constexpr char kFingerprintRecentAttemptsCountBeforeSuccessHistogramName[] =
+    "Fingerprint.Unlock.RecentAttemptsCountBeforeSuccess";
 constexpr char kFeatureUsageMetric[] = "ChromeOS.FeatureUsage.Fingerprint";
 
 // Verifies that fingerprint auth success is recorded correctly.
@@ -455,6 +457,8 @@
       static_cast<int>(quick_unlock::FingerprintUnlockResult::kSuccess), 1);
   histogram_tester.ExpectTotalCount(
       kFingerprintAttemptsCountBeforeSuccessHistogramName, 1);
+  histogram_tester.ExpectTotalCount(
+      kFingerprintRecentAttemptsCountBeforeSuccessHistogramName, 1);
   histogram_tester.ExpectBucketCount(
       kFeatureUsageMetric,
       static_cast<int>(
diff --git a/chrome/browser/ash/login/lock/screen_locker.cc b/chrome/browser/ash/login/lock/screen_locker.cc
index c1565b41..36b07cc 100644
--- a/chrome/browser/ash/login/lock/screen_locker.cc
+++ b/chrome/browser/ash/login/lock/screen_locker.cc
@@ -27,6 +27,7 @@
 #include "base/task/current_thread.h"
 #include "base/task/single_thread_task_runner.h"
 #include "base/threading/thread_task_runner_handle.h"
+#include "base/time/time.h"
 #include "chrome/browser/ash/accessibility/accessibility_manager.h"
 #include "chrome/browser/ash/authpolicy/authpolicy_helper.h"
 #include "chrome/browser/ash/login/easy_unlock/easy_unlock_service.h"
@@ -930,7 +931,8 @@
   if (quick_unlock_storage &&
       quick_unlock_storage->IsFingerprintAuthenticationAvailable(
           quick_unlock::Purpose::kUnlock)) {
-    quick_unlock_storage->fingerprint_storage()->AddUnlockAttempt();
+    quick_unlock_storage->fingerprint_storage()->AddUnlockAttempt(
+        base::TimeTicks::Now());
     if (quick_unlock_storage->fingerprint_storage()->ExceededUnlockAttempts()) {
       VLOG(1) << "Fingerprint unlock is disabled because it reached maximum"
               << " unlock attempt.";
diff --git a/chrome/browser/ash/login/quick_unlock/fingerprint_storage.cc b/chrome/browser/ash/login/quick_unlock/fingerprint_storage.cc
index 93aa06c..37c2b439 100644
--- a/chrome/browser/ash/login/quick_unlock/fingerprint_storage.cc
+++ b/chrome/browser/ash/login/quick_unlock/fingerprint_storage.cc
@@ -7,6 +7,7 @@
 
 #include "ash/constants/ash_pref_names.h"
 #include "base/metrics/histogram_functions.h"
+#include "base/time/time.h"
 #include "chrome/browser/ash/login/quick_unlock/quick_unlock_utils.h"
 #include "chrome/browser/ash/profiles/profile_helper.h"
 #include "chrome/browser/profiles/profile.h"
@@ -88,6 +89,9 @@
   if (success) {
     base::UmaHistogramCounts100("Fingerprint.Unlock.AttemptsCountBeforeSuccess",
                                 unlock_attempt_count());
+    base::UmaHistogramCounts100(
+        "Fingerprint.Unlock.RecentAttemptsCountBeforeSuccess",
+        GetRecentUnlockAttemptCount(base::TimeTicks::Now()));
   }
   feature_usage_metrics_service_->RecordUsage(success);
 }
@@ -102,18 +106,35 @@
              prefs::kQuickUnlockFingerprintRecord) != 0;
 }
 
-void FingerprintStorage::AddUnlockAttempt() {
+void FingerprintStorage::AddUnlockAttempt(base::TimeTicks timestamp) {
+  DCHECK_GE(timestamp, last_unlock_attempt_timestamp_);
+
   ++unlock_attempt_count_;
+  if (timestamp - last_unlock_attempt_timestamp_ < kRecentUnlockAttemptsDelta)
+    ++recent_unlock_attempt_count_;
+  else
+    recent_unlock_attempt_count_ = 1;
+  last_unlock_attempt_timestamp_ = timestamp;
 }
 
 void FingerprintStorage::ResetUnlockAttemptCount() {
   unlock_attempt_count_ = 0;
+  recent_unlock_attempt_count_ = 0;
 }
 
 bool FingerprintStorage::ExceededUnlockAttempts() const {
   return unlock_attempt_count() >= kMaximumUnlockAttempts;
 }
 
+int FingerprintStorage::GetRecentUnlockAttemptCount(base::TimeTicks timestamp) {
+  DCHECK_GE(timestamp, last_unlock_attempt_timestamp_);
+
+  if (timestamp - last_unlock_attempt_timestamp_ < kRecentUnlockAttemptsDelta)
+    return recent_unlock_attempt_count_;
+  else
+    return 0;
+}
+
 void FingerprintStorage::OnRestarted() {
   GetRecordsForUser();
 }
diff --git a/chrome/browser/ash/login/quick_unlock/fingerprint_storage.h b/chrome/browser/ash/login/quick_unlock/fingerprint_storage.h
index 17fc893..059feaf 100644
--- a/chrome/browser/ash/login/quick_unlock/fingerprint_storage.h
+++ b/chrome/browser/ash/login/quick_unlock/fingerprint_storage.h
@@ -39,6 +39,8 @@
       public device::mojom::FingerprintObserver {
  public:
   static const int kMaximumUnlockAttempts = 5;
+  static constexpr base::TimeDelta kRecentUnlockAttemptsDelta =
+      base::Seconds(3);
 
   // Registers profile prefs.
   static void RegisterProfilePrefs(PrefRegistrySimple* registry);
@@ -70,8 +72,8 @@
   // Returns true if the user has fingerprint record registered.
   bool HasRecord() const;
 
-  // Add a fingerprint unlock attempt count.
-  void AddUnlockAttempt();
+  // Add a fingerprint unlock attempt count that happened at timestamp.
+  void AddUnlockAttempt(base::TimeTicks timestamp);
 
   // Reset the number of unlock attempts to 0.
   void ResetUnlockAttemptCount();
@@ -79,8 +81,15 @@
   // Returns true if the user has exceeded fingerprint unlock attempts.
   bool ExceededUnlockAttempts() const;
 
+  // Returns the number of unlock attempts made before success, regardless of
+  // when they happened in time.
   int unlock_attempt_count() const { return unlock_attempt_count_; }
 
+  // Returns the number of recent unlock attempts made before success.
+  // Recent attempts are defined as happening within
+  // `kRecentUnlockAttemptsDelta` from each others.
+  int GetRecentUnlockAttemptCount(base::TimeTicks timestamp);
+
   // device::mojom::FingerprintObserver:
   void OnRestarted() override;
   void OnEnrollScanDone(device::mojom::ScanResult scan_result,
@@ -100,9 +109,16 @@
   friend class QuickUnlockStorage;
 
   Profile* const profile_;
-  // Number of fingerprint unlock attempt.
+  // Number of fingerprint unlock attempts.
   int unlock_attempt_count_ = 0;
 
+  // Number of recent fingerprint unlock attempts, i.e. attempts happening
+  // within 3 seconds from each others.
+  int recent_unlock_attempt_count_ = 0;
+
+  // Timestamps of the last fingerprint unlock attempt.
+  base::TimeTicks last_unlock_attempt_timestamp_ = base::TimeTicks::UnixEpoch();
+
   mojo::Remote<device::mojom::Fingerprint> fp_service_;
 
   mojo::Receiver<device::mojom::FingerprintObserver>
diff --git a/chrome/browser/ash/login/quick_unlock/fingerprint_storage_unittest.cc b/chrome/browser/ash/login/quick_unlock/fingerprint_storage_unittest.cc
index ca6e943..6c387f2 100644
--- a/chrome/browser/ash/login/quick_unlock/fingerprint_storage_unittest.cc
+++ b/chrome/browser/ash/login/quick_unlock/fingerprint_storage_unittest.cc
@@ -8,6 +8,7 @@
 
 #include "ash/constants/ash_pref_names.h"
 #include "base/test/metrics/histogram_tester.h"
+#include "base/time/time.h"
 #include "chrome/browser/ash/login/quick_unlock/quick_unlock_factory.h"
 #include "chrome/browser/ash/login/quick_unlock/quick_unlock_storage.h"
 #include "chrome/browser/ash/login/quick_unlock/quick_unlock_utils.h"
@@ -83,15 +84,93 @@
 
   EXPECT_EQ(0, fingerprint_storage->unlock_attempt_count());
 
-  fingerprint_storage->AddUnlockAttempt();
-  fingerprint_storage->AddUnlockAttempt();
-  fingerprint_storage->AddUnlockAttempt();
+  fingerprint_storage->AddUnlockAttempt(base::TimeTicks::Now());
+  fingerprint_storage->AddUnlockAttempt(base::TimeTicks::Now());
+  fingerprint_storage->AddUnlockAttempt(base::TimeTicks::Now());
   EXPECT_EQ(3, fingerprint_storage->unlock_attempt_count());
 
   fingerprint_storage->ResetUnlockAttemptCount();
   EXPECT_EQ(0, fingerprint_storage->unlock_attempt_count());
 }
 
+// Verifies that initial repeated unlock attempt count is zero.
+TEST_F(FingerprintStorageUnitTest, InitialRecentUnlockAttemptCountIsZero) {
+  FingerprintStorage* fingerprint_storage =
+      QuickUnlockFactory::GetForProfile(profile_.get())->fingerprint_storage();
+
+  EXPECT_EQ(0, fingerprint_storage->GetRecentUnlockAttemptCount(
+                   base::TimeTicks::Now()));
+}
+
+// Verify that recent unlock attempts correctly increases unlock attempt count.
+TEST_F(FingerprintStorageUnitTest,
+       RecentUnlockAttemptCountIsOneAfterOneAttempt) {
+  FingerprintStorage* fingerprint_storage =
+      QuickUnlockFactory::GetForProfile(profile_.get())->fingerprint_storage();
+
+  base::TimeTicks test_start = base::TimeTicks::Now();
+  fingerprint_storage->AddUnlockAttempt(test_start);
+  EXPECT_EQ(1, fingerprint_storage->GetRecentUnlockAttemptCount(
+                   test_start + base::Seconds(2)));
+}
+
+// Verify that recent unlock attempts correctly increases unlock attempt count.
+TEST_F(FingerprintStorageUnitTest,
+       RecentUnlockAttemptCountIncreasesWithRepeatedAttempts) {
+  FingerprintStorage* fingerprint_storage =
+      QuickUnlockFactory::GetForProfile(profile_.get())->fingerprint_storage();
+
+  base::TimeTicks test_start = base::TimeTicks::Now();
+  fingerprint_storage->AddUnlockAttempt(test_start);
+  fingerprint_storage->AddUnlockAttempt(test_start + base::Seconds(1));
+  fingerprint_storage->AddUnlockAttempt(test_start + base::Seconds(2));
+  EXPECT_EQ(3, fingerprint_storage->GetRecentUnlockAttemptCount(
+                   test_start + base::Seconds(3)));
+}
+
+// Verify that recent unlock attempts is zero after explicit reset call
+TEST_F(FingerprintStorageUnitTest, RecentUnlockAttemptCountIsZeroAfterReset) {
+  FingerprintStorage* fingerprint_storage =
+      QuickUnlockFactory::GetForProfile(profile_.get())->fingerprint_storage();
+
+  base::TimeTicks test_start = base::TimeTicks::Now();
+  fingerprint_storage->AddUnlockAttempt(test_start);
+  ASSERT_EQ(1, fingerprint_storage->GetRecentUnlockAttemptCount(
+                   test_start + base::Seconds(1)));
+
+  fingerprint_storage->ResetUnlockAttemptCount();
+  EXPECT_EQ(0, fingerprint_storage->GetRecentUnlockAttemptCount(
+                   test_start + base::Seconds(2)));
+}
+
+// Verify that dated attempts are not counted in recent unlock attempt count.
+TEST_F(FingerprintStorageUnitTest,
+       RecentUnlockAttemptCountExcludesDatedAttempts) {
+  FingerprintStorage* fingerprint_storage =
+      QuickUnlockFactory::GetForProfile(profile_.get())->fingerprint_storage();
+
+  base::TimeTicks test_start = base::TimeTicks::Now();
+  fingerprint_storage->AddUnlockAttempt(test_start);
+  fingerprint_storage->AddUnlockAttempt(test_start + base::Seconds(1));
+  fingerprint_storage->AddUnlockAttempt(test_start + base::Seconds(2));
+  EXPECT_EQ(0, fingerprint_storage->GetRecentUnlockAttemptCount(
+                   test_start + base::Seconds(10)));
+}
+
+// Verify that dated attempts are not counted in recent unlock attempt count.
+TEST_F(FingerprintStorageUnitTest,
+       RecentUnlockAttemptCountExcludesSomeAttempts) {
+  FingerprintStorage* fingerprint_storage =
+      QuickUnlockFactory::GetForProfile(profile_.get())->fingerprint_storage();
+
+  base::TimeTicks test_start = base::TimeTicks::Now();
+  fingerprint_storage->AddUnlockAttempt(test_start);
+  fingerprint_storage->AddUnlockAttempt(test_start + base::Seconds(1));
+  fingerprint_storage->AddUnlockAttempt(test_start + base::Seconds(5));
+  EXPECT_EQ(1, fingerprint_storage->GetRecentUnlockAttemptCount(
+                   test_start + base::Seconds(7)));
+}
+
 // Verifies that authentication is not available when
 // 1. No fingerprint records registered.
 // 2. Too many authentication attempts.
@@ -117,7 +196,7 @@
   // Too many authentication attempts make fingerprint authentication
   // unavailable.
   for (int i = 0; i < FingerprintStorage::kMaximumUnlockAttempts; ++i) {
-    fingerprint_storage->AddUnlockAttempt();
+    fingerprint_storage->AddUnlockAttempt(base::TimeTicks::Now());
   }
   EXPECT_FALSE(test_api.IsFingerprintAvailable());
   fingerprint_storage->ResetUnlockAttemptCount();
diff --git a/chrome/browser/ash/policy/reporting/user_added_removed/user_added_removed_reporter_browsertest.cc b/chrome/browser/ash/policy/reporting/user_added_removed/user_added_removed_reporter_browsertest.cc
new file mode 100644
index 0000000..94a6a19
--- /dev/null
+++ b/chrome/browser/ash/policy/reporting/user_added_removed/user_added_removed_reporter_browsertest.cc
@@ -0,0 +1,391 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <memory>
+#include <string>
+
+#include "ash/components/login/auth/public/user_context.h"
+#include "ash/components/settings/cros_settings_names.h"
+#include "ash/constants/ash_switches.h"
+#include "ash/public/cpp/login_screen_test_api.h"
+#include "base/auto_reset.h"
+#include "base/run_loop.h"
+#include "chrome/browser/ash/login/app_mode/kiosk_launch_controller.h"
+#include "chrome/browser/ash/login/test/device_state_mixin.h"
+#include "chrome/browser/ash/login/test/embedded_test_server_setup_mixin.h"
+#include "chrome/browser/ash/login/test/fake_gaia_mixin.h"
+#include "chrome/browser/ash/login/test/kiosk_apps_mixin.h"
+#include "chrome/browser/ash/login/test/login_manager_mixin.h"
+#include "chrome/browser/ash/login/test/session_manager_state_waiter.h"
+#include "chrome/browser/ash/login/test/user_policy_mixin.h"
+#include "chrome/browser/ash/policy/core/browser_policy_connector_ash.h"
+#include "chrome/browser/ash/policy/core/device_local_account.h"
+#include "chrome/browser/ash/policy/core/device_local_account_policy_service.h"
+#include "chrome/browser/ash/policy/core/device_policy_cros_browser_test.h"
+#include "chrome/browser/ash/settings/scoped_testing_cros_settings.h"
+#include "chrome/browser/ash/settings/stub_cros_settings_provider.h"
+#include "chrome/browser/extensions/browsertest_util.h"
+#include "chrome/browser/policy/messaging_layer/proto/synced/add_remove_user_event.pb.h"
+#include "chrome/test/base/mixin_based_in_process_browser_test.h"
+#include "chrome/test/base/testing_browser_process.h"
+#include "chromeos/ash/components/dbus/session_manager/fake_session_manager_client.h"
+#include "chromeos/dbus/missive/missive_client.h"
+#include "chromeos/dbus/missive/missive_client_test_observer.h"
+#include "components/account_id/account_id.h"
+#include "components/policy/core/common/cloud/mock_cloud_policy_store.h"
+#include "components/policy/proto/chrome_device_policy.pb.h"
+#include "components/policy/proto/device_management_backend.pb.h"
+#include "components/reporting/proto/synced/record.pb.h"
+#include "components/reporting/proto/synced/record_constants.pb.h"
+#include "components/signin/public/identity_manager/identity_test_utils.h"
+#include "components/user_manager/user_manager.h"
+#include "content/public/test/browser_test.h"
+#include "net/dns/mock_host_resolver.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+
+using ::chromeos::MissiveClientTestObserver;
+using ::enterprise_management::ChromeDeviceSettingsProto;
+using ::enterprise_management::DeviceLocalAccountInfoProto;
+using ::reporting::Destination;
+using ::reporting::Priority;
+using ::reporting::Record;
+using ::testing::Eq;
+using ::testing::InvokeWithoutArgs;
+using ::testing::StrEq;
+
+namespace ash::reporting {
+namespace {
+
+constexpr char kTestUserEmail[] = "test@example.com";
+constexpr char kTestAffiliationId[] = "test_affiliation_id";
+constexpr char kPublicSessionUserEmail[] = "public_session_user@localhost";
+
+Record GetNextUserAddedRemovedRecord(MissiveClientTestObserver* observer) {
+  const std::tuple<Priority, Record>& enqueued_record =
+      observer->GetNextEnqueuedRecord();
+  Priority priority = std::get<0>(enqueued_record);
+  Record record = std::get<1>(enqueued_record);
+
+  EXPECT_THAT(priority, Eq(Priority::IMMEDIATE));
+  return record;
+}
+
+absl::optional<Record> MaybeGetEnqueuedUserAddedRemovedRecord() {
+  const std::vector<Record>& records =
+      MissiveClient::Get()->GetTestInterface()->GetEnqueuedRecords(
+          Priority::IMMEDIATE);
+  for (const Record& record : records) {
+    if (record.destination() == Destination::ADDED_REMOVED_EVENTS) {
+      return record;
+    }
+  }
+  return absl::nullopt;
+}
+
+// Waiter used by tests during public session user creation.
+class PublicSessionUserCreationWaiter
+    : public user_manager::UserManager::Observer {
+ public:
+  PublicSessionUserCreationWaiter() = default;
+
+  PublicSessionUserCreationWaiter(const PublicSessionUserCreationWaiter&) =
+      delete;
+  PublicSessionUserCreationWaiter& operator=(
+      const PublicSessionUserCreationWaiter&) = delete;
+
+  ~PublicSessionUserCreationWaiter() override = default;
+
+  void Wait(const AccountId& public_session_account_id) {
+    if (user_manager::UserManager::Get()->IsKnownUser(
+            public_session_account_id)) {
+      return;
+    }
+
+    local_state_changed_run_loop_ = std::make_unique<base::RunLoop>();
+    user_manager::UserManager::Get()->AddObserver(this);
+    local_state_changed_run_loop_->Run();
+    user_manager::UserManager::Get()->RemoveObserver(this);
+  }
+
+  // user_manager::UserManager::Observer:
+  void LocalStateChanged(user_manager::UserManager* user_manager) override {
+    local_state_changed_run_loop_->Quit();
+  }
+
+ private:
+  std::unique_ptr<base::RunLoop> local_state_changed_run_loop_;
+};
+
+class UserAddedRemovedReporterBrowserTest
+    : public policy::DevicePolicyCrosBrowserTest {
+ protected:
+  UserAddedRemovedReporterBrowserTest() {
+    // Add unaffiliated user for testing purposes.
+    login_manager_mixin_.AppendRegularUsers(1);
+
+    login_manager_mixin_.set_session_restore_enabled();
+    scoped_testing_cros_settings_.device_settings()->SetBoolean(
+        kReportDeviceLoginLogout, true);
+  }
+
+  void SetUpCommandLine(base::CommandLine* command_line) override {
+    command_line->AppendSwitch(switches::kLoginManager);
+    command_line->AppendSwitch(switches::kAllowFailedPolicyFetchForTest);
+  }
+
+  void SetUpOnMainThread() override {
+    login_manager_mixin_.set_should_launch_browser(true);
+    FakeSessionManagerClient::Get()->set_supports_browser_restart(true);
+    policy::DevicePolicyCrosBrowserTest::SetUpOnMainThread();
+  }
+
+  void SetUpInProcessBrowserTestFixture() override {
+    policy::DevicePolicyCrosBrowserTest::SetUpInProcessBrowserTestFixture();
+
+    // Set up affiliation for the test user.
+    auto device_policy_update = device_state_.RequestDevicePolicyUpdate();
+    auto user_policy_update = user_policy_mixin_.RequestPolicyUpdate();
+
+    device_policy_update->policy_data()->add_device_affiliation_ids(
+        kTestAffiliationId);
+    user_policy_update->policy_data()->add_user_affiliation_ids(
+        kTestAffiliationId);
+  }
+
+  const AccountId test_account_id_ = AccountId::FromUserEmailGaiaId(
+      kTestUserEmail,
+      signin::GetTestGaiaIdForEmail(kTestUserEmail));
+  UserPolicyMixin user_policy_mixin_{&mixin_host_, test_account_id_};
+
+  FakeGaiaMixin fake_gaia_mixin_{&mixin_host_};
+  LoginManagerMixin login_manager_mixin_{
+      &mixin_host_, LoginManagerMixin::UserList(), &fake_gaia_mixin_};
+
+  ScopedTestingCrosSettings scoped_testing_cros_settings_;
+};
+
+IN_PROC_BROWSER_TEST_F(UserAddedRemovedReporterBrowserTest,
+                       ReportNewUnaffiliatedUser) {
+  MissiveClientTestObserver observer(Destination::ADDED_REMOVED_EVENTS);
+  login_manager_mixin_.LoginAsNewRegularUser();
+  test::WaitForPrimaryUserSessionStart();
+
+  const Record& record = GetNextUserAddedRemovedRecord(&observer);
+  ::reporting::UserAddedRemovedRecord record_data;
+  ASSERT_TRUE(record_data.ParseFromString(record.data()));
+  EXPECT_TRUE(record_data.has_user_added_event());
+  EXPECT_FALSE(record_data.has_affiliated_user());
+}
+
+IN_PROC_BROWSER_TEST_F(UserAddedRemovedReporterBrowserTest,
+                       ReportRemovedUnaffiliatedUser) {
+  MissiveClientTestObserver observer(Destination::ADDED_REMOVED_EVENTS);
+  ASSERT_TRUE(LoginScreenTestApi::RemoveUser(
+      login_manager_mixin_.users()[0].account_id));
+
+  const Record& record = GetNextUserAddedRemovedRecord(&observer);
+  ::reporting::UserAddedRemovedRecord record_data;
+  ASSERT_TRUE(record_data.ParseFromString(record.data()));
+  ASSERT_TRUE(record_data.has_user_removed_event());
+  EXPECT_THAT(record_data.user_removed_event().reason(),
+              Eq(::reporting::UserRemovalReason::LOCAL_USER_INITIATED));
+  EXPECT_FALSE(record_data.has_affiliated_user());
+}
+
+IN_PROC_BROWSER_TEST_F(UserAddedRemovedReporterBrowserTest,
+                       ReportNewAffiliatedUser) {
+  MissiveClientTestObserver observer(Destination::ADDED_REMOVED_EVENTS);
+  const LoginManagerMixin::TestUserInfo user_info(test_account_id_);
+  const auto& context = LoginManagerMixin::CreateDefaultUserContext(user_info);
+  login_manager_mixin_.LoginAsNewRegularUser(context);
+  test::WaitForPrimaryUserSessionStart();
+
+  const Record& record = GetNextUserAddedRemovedRecord(&observer);
+  ::reporting::UserAddedRemovedRecord record_data;
+  ASSERT_TRUE(record_data.ParseFromString(record.data()));
+  EXPECT_TRUE(record_data.has_user_added_event());
+  EXPECT_TRUE(record_data.has_affiliated_user());
+  EXPECT_TRUE(record_data.affiliated_user().has_user_email());
+  EXPECT_THAT(record_data.affiliated_user().user_email(),
+              StrEq(kTestUserEmail));
+}
+
+IN_PROC_BROWSER_TEST_F(UserAddedRemovedReporterBrowserTest,
+                       PRE_DoesNotReportGuestUser) {
+  base::RunLoop restart_job_waiter;
+  FakeSessionManagerClient::Get()->set_restart_job_callback(
+      restart_job_waiter.QuitClosure());
+
+  ASSERT_TRUE(LoginScreenTestApi::IsGuestButtonShown());
+  ASSERT_TRUE(LoginScreenTestApi::ClickGuestButton());
+
+  restart_job_waiter.Run();
+  EXPECT_TRUE(FakeSessionManagerClient::Get()->restart_job_argv().has_value());
+}
+
+IN_PROC_BROWSER_TEST_F(UserAddedRemovedReporterBrowserTest,
+                       DoesNotReportGuestUser) {
+  test::WaitForPrimaryUserSessionStart();
+  base::RunLoop().RunUntilIdle();
+
+  const user_manager::UserManager* const user_manager =
+      user_manager::UserManager::Get();
+  ASSERT_TRUE(user_manager->IsLoggedInAsGuest());
+
+  const absl::optional<Record> record =
+      MaybeGetEnqueuedUserAddedRemovedRecord();
+  ASSERT_FALSE(record.has_value());
+}
+
+class UserAddedRemovedReporterPublicSessionBrowserTest
+    : public policy::DevicePolicyCrosBrowserTest {
+ protected:
+  void SetUpOnMainThread() override {
+    policy::DevicePolicyCrosBrowserTest::SetUpOnMainThread();
+
+    // Wait for the public session user to be created.
+    PublicSessionUserCreationWaiter public_session_waiter;
+    public_session_waiter.Wait(public_session_account_id_);
+    EXPECT_TRUE(user_manager::UserManager::Get()->IsKnownUser(
+        public_session_account_id_));
+
+    // Wait for the device local account policy to be installed.
+    policy::CloudPolicyStore* const store =
+        TestingBrowserProcess::GetGlobal()
+            ->platform_part()
+            ->browser_policy_connector_ash()
+            ->GetDeviceLocalAccountPolicyService()
+            ->GetBrokerForUser(public_session_account_id_.GetUserEmail())
+            ->core()
+            ->store();
+    if (!store->has_policy()) {
+      policy::MockCloudPolicyStoreObserver observer;
+
+      base::RunLoop loop;
+      store->AddObserver(&observer);
+      EXPECT_CALL(observer, OnStoreLoaded(store))
+          .WillOnce(InvokeWithoutArgs(&loop, &base::RunLoop::Quit));
+      loop.Run();
+      store->RemoveObserver(&observer);
+    }
+  }
+
+  void SetUpInProcessBrowserTestFixture() override {
+    policy::DevicePolicyCrosBrowserTest::SetUpInProcessBrowserTestFixture();
+
+    // Setup the device policy.
+    ChromeDeviceSettingsProto& proto(device_policy()->payload());
+    DeviceLocalAccountInfoProto* account =
+        proto.mutable_device_local_accounts()->add_account();
+    account->set_account_id(kPublicSessionUserEmail);
+    account->set_type(DeviceLocalAccountInfoProto::ACCOUNT_TYPE_PUBLIC_SESSION);
+    // Enable login/logout reporting.
+    proto.mutable_device_reporting()->set_report_login_logout(true);
+    RefreshDevicePolicy();
+
+    // Setup the device local account policy.
+    policy::UserPolicyBuilder device_local_account_policy;
+    device_local_account_policy.policy_data().set_username(
+        kPublicSessionUserEmail);
+    device_local_account_policy.policy_data().set_policy_type(
+        policy::dm_protocol::kChromePublicAccountPolicyType);
+    device_local_account_policy.policy_data().set_settings_entity_id(
+        kPublicSessionUserEmail);
+    device_local_account_policy.Build();
+    session_manager_client()->set_device_local_account_policy(
+        kPublicSessionUserEmail, device_local_account_policy.GetBlob());
+  }
+
+  const AccountId public_session_account_id_ =
+      AccountId::FromUserEmail(policy::GenerateDeviceLocalAccountUserId(
+          kPublicSessionUserEmail,
+          policy::DeviceLocalAccount::TYPE_PUBLIC_SESSION));
+
+  const LoginManagerMixin login_manager_mixin_{&mixin_host_,
+                                               LoginManagerMixin::UserList()};
+};
+
+IN_PROC_BROWSER_TEST_F(UserAddedRemovedReporterPublicSessionBrowserTest,
+                       DoesNotReportPublicSessionUser) {
+  ASSERT_TRUE(
+      LoginScreenTestApi::ExpandPublicSessionPod(public_session_account_id_));
+  LoginScreenTestApi::ClickPublicExpandedSubmitButton();
+  test::WaitForPrimaryUserSessionStart();
+  base::RunLoop().RunUntilIdle();
+
+  const user_manager::UserManager* const user_manager =
+      user_manager::UserManager::Get();
+  ASSERT_TRUE(user_manager->IsLoggedInAsPublicAccount());
+
+  const absl::optional<Record> record =
+      MaybeGetEnqueuedUserAddedRemovedRecord();
+  ASSERT_FALSE(record.has_value());
+}
+
+class UserAddedRemovedReporterKioskBrowserTest
+    : public MixinBasedInProcessBrowserTest {
+ protected:
+  void SetUp() override {
+    skip_splash_wait_override_ =
+        KioskLaunchController::SkipSplashScreenWaitForTesting();
+    login_manager_mixin_.set_session_restore_enabled();
+
+    MixinBasedInProcessBrowserTest::SetUp();
+  }
+
+  void SetUpCommandLine(base::CommandLine* command_line) override {
+    MixinBasedInProcessBrowserTest::SetUpCommandLine(command_line);
+
+    fake_cws_.Init(embedded_test_server());
+    fake_cws_.SetUpdateCrx(GetTestAppId(), GetTestAppId() + ".crx", "1.0.0");
+  }
+
+  void SetUpInProcessBrowserTestFixture() override {
+    MixinBasedInProcessBrowserTest::SetUpInProcessBrowserTestFixture();
+
+    host_resolver()->AddRule("*", "127.0.0.1");
+    SessionManagerClient::InitializeFakeInMemory();
+    FakeSessionManagerClient::Get()->set_supports_browser_restart(true);
+
+    ChromeDeviceSettingsProto& proto(policy_helper_.device_policy()->payload());
+    KioskAppsMixin::AppendAutoLaunchKioskAccount(&proto);
+    proto.mutable_device_reporting()->set_report_login_logout(true);
+    policy_helper_.RefreshDevicePolicy();
+  }
+
+  void SetUpOnMainThread() override {
+    MixinBasedInProcessBrowserTest::SetUpOnMainThread();
+    extensions::browsertest_util::CreateAndInitializeLocalCache();
+  }
+
+  std::string GetTestAppId() const { return KioskAppsMixin::kKioskAppId; }
+
+  FakeCWS fake_cws_;
+  policy::DevicePolicyCrosTestHelper policy_helper_;
+  std::unique_ptr<base::AutoReset<bool>> skip_splash_wait_override_;
+  const EmbeddedTestServerSetupMixin embedded_test_server_{
+      &mixin_host_, embedded_test_server()};
+
+  const DeviceStateMixin device_state_{
+      &mixin_host_, DeviceStateMixin::State::OOBE_COMPLETED_CLOUD_ENROLLED};
+  LoginManagerMixin login_manager_mixin_{&mixin_host_,
+                                         LoginManagerMixin::UserList()};
+};
+
+IN_PROC_BROWSER_TEST_F(UserAddedRemovedReporterKioskBrowserTest,
+                       DoesNotReportKioskUser) {
+  test::WaitForPrimaryUserSessionStart();
+  const user_manager::UserManager* const user_manager =
+      user_manager::UserManager::Get();
+  ASSERT_TRUE(user_manager->IsLoggedInAsKioskApp());
+
+  const absl::optional<Record> record =
+      MaybeGetEnqueuedUserAddedRemovedRecord();
+  ASSERT_FALSE(record.has_value());
+}
+
+}  // namespace
+}  // namespace ash::reporting
diff --git a/chrome/browser/ash/web_applications/projector_app/projector_app_integration_browsertest.cc b/chrome/browser/ash/web_applications/projector_app/projector_app_integration_browsertest.cc
index 1824d02..5398a10 100644
--- a/chrome/browser/ash/web_applications/projector_app/projector_app_integration_browsertest.cc
+++ b/chrome/browser/ash/web_applications/projector_app/projector_app_integration_browsertest.cc
@@ -4,14 +4,14 @@
 
 #include "ash/capture_mode/capture_mode_controller.h"
 #include "ash/projector/projector_controller_impl.h"
-#include "ash/public/cpp/projector/projector_client.h"
 #include "ash/webui/media_app_ui/buildflags.h"
 #include "ash/webui/media_app_ui/test/media_app_ui_browsertest.h"
 #include "ash/webui/projector_app/buildflags.h"
+#include "ash/webui/projector_app/projector_app_client.h"
 #include "base/run_loop.h"
 #include "chrome/browser/ash/system_web_apps/system_web_app_manager.h"
 #include "chrome/browser/ash/system_web_apps/test_support/system_web_app_integration_test.h"
-#include "chrome/browser/ui/ash/projector/projector_client_impl.h"
+#include "chrome/browser/ui/ash/projector/projector_app_client_impl.h"
 #include "chrome/browser/ui/ash/system_web_apps/system_web_app_ui_utils.h"
 #include "content/public/test/browser_test.h"
 
@@ -65,10 +65,10 @@
   capture_mode_controller->PerformCapture();
   run_loop.Run();
 
-  ProjectorClientImpl* projector_client =
-      static_cast<ProjectorClientImpl*>(ash::ProjectorClient::Get());
+  ProjectorAppClientImpl* projector_app_client =
+      static_cast<ProjectorAppClientImpl*>(ash::ProjectorAppClient::Get());
   content::WebContents* annotator_embedder =
-      projector_client->get_annotator_message_handler_for_test()
+      projector_app_client->get_annotator_message_handler_for_test()
           ->get_web_ui_for_test()
           ->GetWebContents();
   PrepareAppForTest(annotator_embedder);
diff --git a/chrome/browser/ash/web_applications/projector_app/untrusted_projector_ui_config.cc b/chrome/browser/ash/web_applications/projector_app/untrusted_projector_ui_config.cc
index 9b4ee98c..850dd56 100644
--- a/chrome/browser/ash/web_applications/projector_app/untrusted_projector_ui_config.cc
+++ b/chrome/browser/ash/web_applications/projector_app/untrusted_projector_ui_config.cc
@@ -35,6 +35,8 @@
   source->AddBoolean(
       "isUseOAuthForGetVideoInfoEnabled",
       ash::features::IsProjectorUseOAuthForGetVideoInfoEnabled());
+  source->AddBoolean("isLocalPlaybackEnabled",
+                     ash::features::IsProjectorLocalPlaybackEnabled());
   source->AddString("appLocale", g_browser_process->GetApplicationLocale());
 }
 
diff --git a/chrome/browser/banners/android/java/src/org/chromium/chrome/browser/banners/AppBannerManagerTest.java b/chrome/browser/banners/android/java/src/org/chromium/chrome/browser/banners/AppBannerManagerTest.java
index 6796cf8..a624b83 100644
--- a/chrome/browser/banners/android/java/src/org/chromium/chrome/browser/banners/AppBannerManagerTest.java
+++ b/chrome/browser/banners/android/java/src/org/chromium/chrome/browser/banners/AppBannerManagerTest.java
@@ -156,7 +156,7 @@
             "/chrome/test/data/banners/manifest_prefer_related_chrome_app.json";
 
     private static final String WEB_APP_MANIFEST_FOR_BOTTOM_SHEET_INSTALL =
-            "/chrome/test/data/banners/manifest_bottom_sheet_install.json";
+            "/chrome/test/data/banners/manifest_with_screenshots.json";
 
     private static final String NATIVE_ICON_PATH = "/chrome/test/data/banners/launcher-icon-4x.png";
 
diff --git a/chrome/browser/extensions/api/debugger/debugger_api.cc b/chrome/browser/extensions/api/debugger/debugger_api.cc
index 4b303f2..3c12e5cd 100644
--- a/chrome/browser/extensions/api/debugger/debugger_api.cc
+++ b/chrome/browser/extensions/api/debugger/debugger_api.cc
@@ -824,6 +824,11 @@
   base::Value::List result;
   Profile* profile = Profile::FromBrowserContext(browser_context());
   for (auto& host : list) {
+    // TODO(crbug.com/1348385): hide all Tab targets for now to avoid
+    // compatibility problems. Consider exposing them later when they're fully
+    // supported, and compatibility considerations are better understood.
+    if (host->GetType() == DevToolsAgentHost::kTypeTab)
+      continue;
     if (!ExtensionMayAttachToTargetProfile(
             profile, include_incognito_information(), *host)) {
       continue;
diff --git a/chrome/browser/extensions/lazy_background_page_apitest.cc b/chrome/browser/extensions/lazy_background_page_apitest.cc
index cb4209b8..029528d 100644
--- a/chrome/browser/extensions/lazy_background_page_apitest.cc
+++ b/chrome/browser/extensions/lazy_background_page_apitest.cc
@@ -16,6 +16,7 @@
 #include "base/threading/thread_restrictions.h"
 #include "build/build_config.h"
 #include "chrome/browser/bookmarks/bookmark_model_factory.h"
+#include "chrome/browser/devtools/chrome_devtools_manager_delegate.h"
 #include "chrome/browser/devtools/devtools_window_testing.h"
 #include "chrome/browser/extensions/api/developer_private/developer_private_api.h"
 #include "chrome/browser/extensions/extension_action_test_util.h"
@@ -347,6 +348,8 @@
       content::DevToolsAgentHost::GetOrCreateAll();
   scoped_refptr<content::DevToolsAgentHost> service_worker_host;
   for (const scoped_refptr<content::DevToolsAgentHost>& host : targets) {
+    if (host->GetType() != ChromeDevToolsManagerDelegate::kTypeBackgroundPage)
+      continue;
     if (host->GetURL() == BackgroundInfo::GetBackgroundURL(extension.get())) {
       EXPECT_FALSE(service_worker_host);
       service_worker_host = host;
diff --git a/chrome/browser/external_protocol/external_protocol_policy_browsertest.cc b/chrome/browser/external_protocol/external_protocol_policy_browsertest.cc
index 129d363..277b353 100644
--- a/chrome/browser/external_protocol/external_protocol_policy_browsertest.cc
+++ b/chrome/browser/external_protocol/external_protocol_policy_browsertest.cc
@@ -36,14 +36,14 @@
   protocol_origins_map.SetStringKey(policy::external_protocol::kProtocolNameKey,
                                     kExampleScheme);
   // Set origins list with a wildcard origin matching pattern.
-  base::ListValue origins;
+  base::Value::List origins;
   origins.Append(kWildcardOrigin);
   protocol_origins_map.SetKey(policy::external_protocol::kOriginListKey,
-                              std::move(origins));
+                              base::Value(std::move(origins)));
   PolicyMap policies;
   policies.Set(key::kAutoLaunchProtocolsFromOrigins, POLICY_LEVEL_MANDATORY,
                POLICY_SCOPE_USER, POLICY_SOURCE_CLOUD,
-               protocol_origins_map.Clone(), nullptr);
+               base::Value(protocol_origins_map.Clone()), nullptr);
   UpdateProviderPolicy(policies);
 
   block_state = ExternalProtocolHandler::GetBlockState(
@@ -56,22 +56,22 @@
   const char kWildcardOrigin[] = "*";
   const char kExampleScheme[] = "custom";
 
-  base::ListValue protocol_origins_map_list;
+  base::Value::List protocol_origins_map_list;
   // Single dictionary in the list for this test case.
   base::DictionaryValue protocol_origins_map;
   // Set a protocol.
   protocol_origins_map.SetStringKey(policy::external_protocol::kProtocolNameKey,
                                     kExampleScheme);
   // Set an origins list with the wildcard origin matching pattern.
-  base::ListValue origins;
+  base::Value::List origins;
   origins.Append(kWildcardOrigin);
   protocol_origins_map.SetKey(policy::external_protocol::kOriginListKey,
-                              std::move(origins));
+                              base::Value(std::move(origins)));
   protocol_origins_map_list.Append(std::move(protocol_origins_map));
   PolicyMap policies;
   policies.Set(key::kAutoLaunchProtocolsFromOrigins, POLICY_LEVEL_MANDATORY,
                POLICY_SCOPE_USER, POLICY_SOURCE_CLOUD,
-               protocol_origins_map_list.Clone(), nullptr);
+               base::Value(protocol_origins_map_list.Clone()), nullptr);
   UpdateProviderPolicy(policies);
 
   // Calling GetBlockState with a null initiating_origin should
@@ -92,21 +92,21 @@
                                              browser()->profile());
   EXPECT_EQ(ExternalProtocolHandler::UNKNOWN, block_state);
 
-  base::ListValue protocol_origins_map_list;
+  base::Value::List protocol_origins_map_list;
   // Single dictionary in the list for this test case.
   base::DictionaryValue protocol_origins_map;
   // Set a protocol list with a matching protocol.
   protocol_origins_map.SetStringKey(policy::external_protocol::kProtocolNameKey,
                                     kExampleScheme);
   // Set an empty origins list.
-  base::ListValue origins;
+  base::Value::List origins;
   protocol_origins_map.SetKey(policy::external_protocol::kOriginListKey,
-                              std::move(origins));
+                              base::Value(std::move(origins)));
   protocol_origins_map_list.Append(std::move(protocol_origins_map));
   PolicyMap policies;
   policies.Set(key::kAutoLaunchProtocolsFromOrigins, POLICY_LEVEL_MANDATORY,
                POLICY_SCOPE_USER, POLICY_SOURCE_CLOUD,
-               protocol_origins_map_list.Clone(), nullptr);
+               base::Value(protocol_origins_map_list.Clone()), nullptr);
   UpdateProviderPolicy(policies);
 
   block_state = ExternalProtocolHandler::GetBlockState(
@@ -119,22 +119,22 @@
   const char kWildcardOrigin[] = "*";
   const char kExampleScheme[] = "custom";
 
-  base::ListValue protocol_origins_map_list;
+  base::Value::List protocol_origins_map_list;
   // Single dictionary in the list for this test case.
   base::DictionaryValue protocol_origins_map;
   // Set a protocol to match the test.
   protocol_origins_map.SetStringKey(policy::external_protocol::kProtocolNameKey,
                                     kExampleScheme);
   // Set an origins list with the wildcard origin matching pattern.
-  base::ListValue origins;
+  base::Value::List origins;
   origins.Append(kWildcardOrigin);
   protocol_origins_map.SetKey(policy::external_protocol::kOriginListKey,
-                              std::move(origins));
+                              base::Value(std::move(origins)));
   protocol_origins_map_list.Append(std::move(protocol_origins_map));
   PolicyMap policies;
   policies.Set(key::kAutoLaunchProtocolsFromOrigins, POLICY_LEVEL_MANDATORY,
                POLICY_SCOPE_USER, POLICY_SOURCE_CLOUD,
-               protocol_origins_map_list.Clone(), nullptr);
+               base::Value(protocol_origins_map_list.Clone()), nullptr);
   UpdateProviderPolicy(policies);
 
   url::Origin test_origin = url::Origin::Create(GURL("https://example.test"));
@@ -152,7 +152,7 @@
   const char kWildcardOrigin[] = "*";
   const char kExampleScheme[] = "custom";
 
-  base::ListValue protocol_origins_map_list;
+  base::Value::List protocol_origins_map_list;
 
   // Three dictionaries in the list for this test case.
   base::DictionaryValue protocol_origins_map1;
@@ -162,32 +162,32 @@
   // Set invalid protocols, each with the wildcard origin matching pattern.
   protocol_origins_map1.SetStringKey(
       policy::external_protocol::kProtocolNameKey, kInvalidProtocol1);
-  base::ListValue origins1;
+  base::Value::List origins1;
   origins1.Append(kWildcardOrigin);
   protocol_origins_map1.SetKey(policy::external_protocol::kOriginListKey,
-                               std::move(origins1));
+                               base::Value(std::move(origins1)));
   protocol_origins_map_list.Append(std::move(protocol_origins_map1));
 
   protocol_origins_map2.SetStringKey(
       policy::external_protocol::kProtocolNameKey, kInvalidProtocol2);
-  base::ListValue origins2;
+  base::Value::List origins2;
   origins2.Append(kWildcardOrigin);
   protocol_origins_map2.SetKey(policy::external_protocol::kOriginListKey,
-                               std::move(origins2));
+                               base::Value(std::move(origins2)));
   protocol_origins_map_list.Append(std::move(protocol_origins_map2));
 
   protocol_origins_map3.SetStringKey(
       policy::external_protocol::kProtocolNameKey, kInvalidProtocol3);
-  base::ListValue origins3;
+  base::Value::List origins3;
   origins3.Append(kWildcardOrigin);
   protocol_origins_map3.SetKey(policy::external_protocol::kOriginListKey,
-                               std::move(origins3));
+                               base::Value(std::move(origins3)));
   protocol_origins_map_list.Append(std::move(protocol_origins_map3));
 
   PolicyMap policies;
   policies.Set(key::kAutoLaunchProtocolsFromOrigins, POLICY_LEVEL_MANDATORY,
                POLICY_SCOPE_USER, POLICY_SOURCE_CLOUD,
-               protocol_origins_map_list.Clone(), nullptr);
+               base::Value(protocol_origins_map_list.Clone()), nullptr);
   UpdateProviderPolicy(policies);
 
   url::Origin test_origin = url::Origin::Create(GURL("https://example.test"));
@@ -202,7 +202,7 @@
   const char kExampleScheme[] = "custom";
   const char kHost[] = "www.example.test";
 
-  base::ListValue protocol_origins_map_list;
+  base::Value::List protocol_origins_map_list;
   // Single dictionary in the list for this test case.
   base::DictionaryValue protocol_origins_map;
   // Set a protocol to match the test.
@@ -210,15 +210,15 @@
                                     kExampleScheme);
   // Set an origins list with an origin matching pattern that matches but is
   // only the host name.
-  base::ListValue origins;
+  base::Value::List origins;
   origins.Append(kHost);
   protocol_origins_map.SetKey(policy::external_protocol::kOriginListKey,
-                              std::move(origins));
+                              base::Value(std::move(origins)));
   protocol_origins_map_list.Append(std::move(protocol_origins_map));
   PolicyMap policies;
   policies.Set(key::kAutoLaunchProtocolsFromOrigins, POLICY_LEVEL_MANDATORY,
                POLICY_SCOPE_USER, POLICY_SOURCE_CLOUD,
-               protocol_origins_map_list.Clone(), nullptr);
+               base::Value(protocol_origins_map_list.Clone()), nullptr);
   UpdateProviderPolicy(policies);
 
   // Test that secure origin matches.
@@ -247,7 +247,7 @@
   const char kExampleScheme[] = "custom";
   const char kExactHostName[] = ".www.example.test";
 
-  base::ListValue protocol_origins_map_list;
+  base::Value::List protocol_origins_map_list;
   // Single dictionary in the list for this test case.
   base::DictionaryValue protocol_origins_map;
   // Set a protocol to match the test.
@@ -255,15 +255,15 @@
                                     kExampleScheme);
   // Set an origins list with an origin matching pattern that matches exactly
   // but has no scheme.
-  base::ListValue origins;
+  base::Value::List origins;
   origins.Append(kExactHostName);
   protocol_origins_map.SetKey(policy::external_protocol::kOriginListKey,
-                              std::move(origins));
+                              base::Value(std::move(origins)));
   protocol_origins_map_list.Append(std::move(protocol_origins_map));
   PolicyMap policies;
   policies.Set(key::kAutoLaunchProtocolsFromOrigins, POLICY_LEVEL_MANDATORY,
                POLICY_SCOPE_USER, POLICY_SOURCE_CLOUD,
-               protocol_origins_map_list.Clone(), nullptr);
+               base::Value(protocol_origins_map_list.Clone()), nullptr);
   UpdateProviderPolicy(policies);
 
   // Test that secure origin matches.
@@ -292,7 +292,7 @@
   const char kExampleScheme[] = "custom";
   const char kParentDomain[] = "example.test";
 
-  base::ListValue protocol_origins_map_list;
+  base::Value::List protocol_origins_map_list;
   // Single dictionary in the list for this test case.
   base::DictionaryValue protocol_origins_map;
   // Set a protocol to match the test.
@@ -300,15 +300,15 @@
                                     kExampleScheme);
   // Set an origins list with an origin matching pattern that is the parent
   // domain but should match subdomains.
-  base::ListValue origins;
+  base::Value::List origins;
   origins.Append(kParentDomain);
   protocol_origins_map.SetKey(policy::external_protocol::kOriginListKey,
-                              std::move(origins));
+                              base::Value(std::move(origins)));
   protocol_origins_map_list.Append(std::move(protocol_origins_map));
   PolicyMap policies;
   policies.Set(key::kAutoLaunchProtocolsFromOrigins, POLICY_LEVEL_MANDATORY,
                POLICY_SCOPE_USER, POLICY_SOURCE_CLOUD,
-               protocol_origins_map_list.Clone(), nullptr);
+               base::Value(protocol_origins_map_list.Clone()), nullptr);
   UpdateProviderPolicy(policies);
 
   // Test that a subdomain matches.
@@ -325,7 +325,7 @@
   const char kExampleScheme[] = "custom";
   const char kProtocolWithWildcardHostname[] = "https://*";
 
-  base::ListValue protocol_origins_map_list;
+  base::Value::List protocol_origins_map_list;
   // Single dictionary in the list for this test case.
   base::DictionaryValue protocol_origins_map;
   // Set a protocol to match the test.
@@ -333,15 +333,15 @@
                                     kExampleScheme);
   // Set an origins list with an origin matching pattern that matches the scheme
   // and all hosts.
-  base::ListValue origins;
+  base::Value::List origins;
   origins.Append(kProtocolWithWildcardHostname);
   protocol_origins_map.SetKey(policy::external_protocol::kOriginListKey,
-                              std::move(origins));
+                              base::Value(std::move(origins)));
   protocol_origins_map_list.Append(std::move(protocol_origins_map));
   PolicyMap policies;
   policies.Set(key::kAutoLaunchProtocolsFromOrigins, POLICY_LEVEL_MANDATORY,
                POLICY_SCOPE_USER, POLICY_SOURCE_CLOUD,
-               protocol_origins_map_list.Clone(), nullptr);
+               base::Value(protocol_origins_map_list.Clone()), nullptr);
   UpdateProviderPolicy(policies);
 
   // Test that secure origin matches.
@@ -364,7 +364,7 @@
   const char kExampleScheme[] = "custom";
   const char kFullOrigin[] = "https://www.example.test:443";
 
-  base::ListValue protocol_origins_map_list;
+  base::Value::List protocol_origins_map_list;
   // Single dictionary in the list for this test case.
   base::DictionaryValue protocol_origins_map;
   // Set a protocol to match the test.
@@ -372,15 +372,15 @@
                                     kExampleScheme);
   // Set an origins list with an origin matching pattern that matches the full
   // origin exactly.
-  base::ListValue origins;
+  base::Value::List origins;
   origins.Append(kFullOrigin);
   protocol_origins_map.SetKey(policy::external_protocol::kOriginListKey,
-                              std::move(origins));
+                              base::Value(std::move(origins)));
   protocol_origins_map_list.Append(std::move(protocol_origins_map));
   PolicyMap policies;
   policies.Set(key::kAutoLaunchProtocolsFromOrigins, POLICY_LEVEL_MANDATORY,
                POLICY_SCOPE_USER, POLICY_SOURCE_CLOUD,
-               protocol_origins_map_list.Clone(), nullptr);
+               base::Value(protocol_origins_map_list.Clone()), nullptr);
   UpdateProviderPolicy(policies);
 
   // Test that default HTTPS port 443 matches.
@@ -409,7 +409,7 @@
   const char kExampleScheme[] = "custom";
   const char kExactParentDomain[] = ".example.com";
 
-  base::ListValue protocol_origins_map_list;
+  base::Value::List protocol_origins_map_list;
   // Single dictionary in the list for this test case.
   base::DictionaryValue protocol_origins_map;
   // Set a protocol to match the test.
@@ -417,15 +417,15 @@
                                     kExampleScheme);
   // Set an origins list with an origin matching pattern that doesn't match
   // because it is a parent domain that does not match subdomains.
-  base::ListValue origins;
+  base::Value::List origins;
   origins.Append(kExactParentDomain);
   protocol_origins_map.SetKey(policy::external_protocol::kOriginListKey,
-                              std::move(origins));
+                              base::Value(std::move(origins)));
   protocol_origins_map_list.Append(std::move(protocol_origins_map));
   PolicyMap policies;
   policies.Set(key::kAutoLaunchProtocolsFromOrigins, POLICY_LEVEL_MANDATORY,
                POLICY_SCOPE_USER, POLICY_SOURCE_CLOUD,
-               protocol_origins_map_list.Clone(), nullptr);
+               base::Value(protocol_origins_map_list.Clone()), nullptr);
   UpdateProviderPolicy(policies);
 
   url::Origin test_origin =
@@ -441,7 +441,7 @@
   const char kExampleScheme[] = "custom";
   const char kFullUrlWithPath[] = "https://example.test/home.html";
 
-  base::ListValue protocol_origins_map_list;
+  base::Value::List protocol_origins_map_list;
   // Single dictionary in the list for this test case.
   base::DictionaryValue protocol_origins_map;
   // Set a protocol to match the test.
@@ -449,15 +449,15 @@
                                     kExampleScheme);
   // Set an origins list with an origin matching pattern that doesn't match
   // because it contains a [/path] element.
-  base::ListValue origins;
+  base::Value::List origins;
   origins.Append(kFullUrlWithPath);
   protocol_origins_map.SetKey(policy::external_protocol::kOriginListKey,
-                              std::move(origins));
+                              base::Value(std::move(origins)));
   protocol_origins_map_list.Append(std::move(protocol_origins_map));
   PolicyMap policies;
   policies.Set(key::kAutoLaunchProtocolsFromOrigins, POLICY_LEVEL_MANDATORY,
                POLICY_SCOPE_USER, POLICY_SOURCE_CLOUD,
-               protocol_origins_map_list.Clone(), nullptr);
+               base::Value(protocol_origins_map_list.Clone()), nullptr);
   UpdateProviderPolicy(policies);
 
   url::Origin test_origin = url::Origin::Create(GURL(kFullUrlWithPath));
diff --git a/chrome/browser/installable/installable_manager_browsertest.cc b/chrome/browser/installable/installable_manager_browsertest.cc
index 2d2dde0..777c0860 100644
--- a/chrome/browser/installable/installable_manager_browsertest.cc
+++ b/chrome/browser/installable/installable_manager_browsertest.cc
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include "components/webapps/browser/features.h"
 #include "components/webapps/browser/installable/installable_manager.h"
 
 #include <memory>
@@ -258,6 +259,11 @@
 
 class InstallableManagerBrowserTest : public InProcessBrowserTest {
  public:
+  InstallableManagerBrowserTest() {
+    scoped_feature_list_.InitAndEnableFeature(
+        webapps::features::kDesktopPWAsDetailedInstallDialog);
+  }
+
   void SetUpOnMainThread() override {
     embedded_test_server()->ServeFilesFromSourceDirectory(
         "chrome/test/data/banners");
@@ -341,6 +347,9 @@
     run_loop.Run();
     return result;
   }
+
+ private:
+  base::test::ScopedFeatureList scoped_feature_list_;
 };
 
 class InstallableManagerAllowlistOriginBrowserTest
@@ -1937,7 +1946,7 @@
   NavigateAndRunInstallableManager(
       browser(), tester.get(), params,
       GetURLOfPageWithServiceWorkerAndManifest(
-          "/banners/manifest_bottom_sheet_install.json"));
+          "/banners/manifest_with_screenshots.json"));
 
   run_loop.Run();
 
@@ -1946,6 +1955,47 @@
 
   EXPECT_FALSE(tester->valid_manifest());
   EXPECT_EQ(1u, tester->screenshots().size());
+  // Corresponding platform should filter out the screenshot with mismatched
+  // platform.
+#if BUILDFLAG(IS_ANDROID)
+  EXPECT_LT(tester->screenshots()[0].width(),
+            tester->screenshots()[0].height());
+#else
+  EXPECT_GT(tester->screenshots()[0].width(),
+            tester->screenshots()[0].height());
+#endif
+  EXPECT_EQ(std::vector<InstallableStatusCode>{}, tester->errors());
+}
+
+IN_PROC_BROWSER_TEST_F(InstallableManagerBrowserTest,
+                       CheckScreenshotsPlatform) {
+  base::RunLoop run_loop;
+  std::unique_ptr<CallbackTester> tester(
+      new CallbackTester(run_loop.QuitClosure()));
+
+  InstallableParams params = GetManifestParams();
+  params.fetch_screenshots = true;
+
+  // Check if only screenshots with mismatched platform are available, they are
+  // still used.
+#if BUILDFLAG(IS_ANDROID)
+  NavigateAndRunInstallableManager(
+      browser(), tester.get(), params,
+      GetURLOfPageWithServiceWorkerAndManifest(
+          "/banners/manifest_with_only_wide_screenshots.json"));
+#else
+  NavigateAndRunInstallableManager(
+      browser(), tester.get(), params,
+      GetURLOfPageWithServiceWorkerAndManifest(
+          "/banners/manifest_with_only_narrow_screenshots.json"));
+#endif
+  run_loop.Run();
+
+  EXPECT_FALSE(blink::IsEmptyManifest(tester->manifest()));
+  EXPECT_FALSE(tester->manifest_url().is_empty());
+
+  EXPECT_FALSE(tester->valid_manifest());
+  EXPECT_EQ(1u, tester->screenshots().size());
   EXPECT_EQ(std::vector<InstallableStatusCode>{}, tester->errors());
 }
 
diff --git a/chrome/browser/lookalikes/lookalike_url_navigation_throttle.cc b/chrome/browser/lookalikes/lookalike_url_navigation_throttle.cc
index 889f36c1..fd16c69 100644
--- a/chrome/browser/lookalikes/lookalike_url_navigation_throttle.cc
+++ b/chrome/browser/lookalikes/lookalike_url_navigation_throttle.cc
@@ -416,7 +416,8 @@
 
   // TODO(crbug.com/1344981): Once the Combo Squatting heuristic is fully
   // launched, this console message should be removed.
-  if (match_type == LookalikeUrlMatchType::kComboSquatting) {
+  if (match_type == LookalikeUrlMatchType::kComboSquatting ||
+      match_type == LookalikeUrlMatchType::kComboSquattingSiteEngagement) {
     GURL lookalike_url = first_is_lookalike ? first_url : last_url;
 
     navigation_handle()->GetRenderFrameHost()->AddMessageToConsole(
diff --git a/chrome/browser/lookalikes/lookalike_url_navigation_throttle_browsertest.cc b/chrome/browser/lookalikes/lookalike_url_navigation_throttle_browsertest.cc
index 1d77529..cd20146 100644
--- a/chrome/browser/lookalikes/lookalike_url_navigation_throttle_browsertest.cc
+++ b/chrome/browser/lookalikes/lookalike_url_navigation_throttle_browsertest.cc
@@ -1549,9 +1549,9 @@
                            embedded_test_server()->GetURL("example.net", "/"));
 }
 
-// Test Combo Squatting heuristic. In this test, if kNavigatedUrl is
-// Combo Squatting, the metrics should be recorded but no interstitial
-// page (full page warning) is shown.
+// Navigate to a URL that triggers combo squatting heuristic via the
+// hard coded brand name list. This should record metrics but shouldn't show
+// an interstitial.
 IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest,
                        ComboSquatting_ShouldRecordMetricsWithoutUI) {
   base::HistogramTester histograms;
@@ -1569,6 +1569,29 @@
   CheckUkm({kNavigatedUrl}, "TriggeredByInitialUrl", false);
 }
 
+// Navigate to a URL that triggers combo squatting heuristic via a
+// brand name from engaged sites. This should record metrics but shouldn't show
+// an interstitial.
+IN_PROC_BROWSER_TEST_P(
+    LookalikeUrlNavigationThrottleBrowserTest,
+    ComboSquatting_EngagedSites_ShouldRecordMetricsWithoutUI) {
+  base::HistogramTester histograms;
+  SetEngagementScore(browser(), GURL("https://example.com"), kHighEngagement);
+  const GURL kNavigatedUrl = GetURL("example-login.com");
+  SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement);
+
+  TestInterstitialNotShown(browser(), kNavigatedUrl);
+
+  histograms.ExpectTotalCount(lookalikes::kHistogramName, 1);
+  histograms.ExpectBucketCount(
+      lookalikes::kHistogramName,
+      NavigationSuggestionEvent::kComboSquattingSiteEngagement, 1);
+
+  CheckUkm({kNavigatedUrl}, "MatchType",
+           LookalikeUrlMatchType::kComboSquattingSiteEngagement);
+  CheckUkm({kNavigatedUrl}, "TriggeredByInitialUrl", false);
+}
+
 // Combo Squatting shouldn't trigger on allowlisted sites and no
 // UKM should be recorded.
 IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest,
diff --git a/chrome/browser/metrics/chrome_metrics_services_manager_client.cc b/chrome/browser/metrics/chrome_metrics_services_manager_client.cc
index 4d4e33b..54ca655 100644
--- a/chrome/browser/metrics/chrome_metrics_services_manager_client.cc
+++ b/chrome/browser/metrics/chrome_metrics_services_manager_client.cc
@@ -144,11 +144,10 @@
 #if BUILDFLAG(IS_CHROMEOS_ASH)
 // Callback to update the metrics reporting state when the Chrome OS metrics
 // reporting setting changes.
-void OnCrosMetricsReportingSettingChange() {
+void OnCrosMetricsReportingSettingChange(
+    ChangeMetricsReportingStateCalledFrom called_from) {
   bool enable_metrics = ash::StatsReportingController::Get()->IsEnabled();
-  ChangeMetricsReportingState(
-      enable_metrics,
-      ChangeMetricsReportingStateCalledFrom::kCrosMetricsSettingsChange);
+  ChangeMetricsReportingState(enable_metrics, called_from);
 
   // TODO(crbug.com/1234538): This call ensures that structured metrics' state
   // is deleted when the reporting state is disabled. Long-term this should
@@ -243,11 +242,14 @@
 
 #if BUILDFLAG(IS_CHROMEOS_ASH)
 void ChromeMetricsServicesManagerClient::OnCrosSettingsCreated() {
+  // Listen for changes to metrics reporting state.
   reporting_setting_subscription_ =
-      ash::StatsReportingController::Get()->AddObserver(
-          base::BindRepeating(&OnCrosMetricsReportingSettingChange));
+      ash::StatsReportingController::Get()->AddObserver(base::BindRepeating(
+          &OnCrosMetricsReportingSettingChange,
+          ChangeMetricsReportingStateCalledFrom::kCrosMetricsSettingsChange));
   // Invoke the callback once initially to set the metrics reporting state.
-  OnCrosMetricsReportingSettingChange();
+  OnCrosMetricsReportingSettingChange(
+      ChangeMetricsReportingStateCalledFrom::kCrosMetricsSettingsCreated);
 }
 #endif
 
diff --git a/chrome/browser/metrics/metrics_reporting_state.cc b/chrome/browser/metrics/metrics_reporting_state.cc
index 05a9ea2..e2407d8 100644
--- a/chrome/browser/metrics/metrics_reporting_state.cc
+++ b/chrome/browser/metrics/metrics_reporting_state.cc
@@ -117,9 +117,14 @@
   // Chrome OS manages metrics settings externally and changes to reporting
   // should be propagated to metrics service regardless if the policy is managed
   // or not.
-  if (IsMetricsReportingPolicyManaged() &&
-      called_from !=
-          ChangeMetricsReportingStateCalledFrom::kCrosMetricsSettingsChange) {
+  // TODO(crbug/1346321): Possibly change |is_chrome_os| to use
+  // BUILDFLAG(IS_CHROMEOS_ASH).
+  bool is_chrome_os =
+      (called_from ==
+       ChangeMetricsReportingStateCalledFrom::kCrosMetricsSettingsChange) ||
+      (called_from ==
+       ChangeMetricsReportingStateCalledFrom::kCrosMetricsSettingsCreated);
+  if (IsMetricsReportingPolicyManaged() && !is_chrome_os) {
     if (!callback_fn.is_null()) {
       const bool metrics_enabled =
           ChromeMetricsServiceAccessor::IsMetricsAndCrashReportingEnabled();
@@ -145,6 +150,15 @@
 void UpdateMetricsPrefsOnPermissionChange(
     bool metrics_enabled,
     ChangeMetricsReportingStateCalledFrom called_from) {
+  // On Chrome OS settings creation, nothing should be performed (the metrics
+  // service is simply being initialized). Otherwise, for users who have
+  // metrics reporting disabled, their client ID and low entropy sources would
+  // be cleared on each log in. For users who have metrics reporting enabled,
+  // their stability metrics and histogram data would be cleared.
+  if (called_from ==
+      ChangeMetricsReportingStateCalledFrom::kCrosMetricsSettingsCreated) {
+    return;
+  }
   if (metrics_enabled) {
     // When a user opts in to the metrics reporting service, the previously
     // collected data should be cleared to ensure that nothing is reported
diff --git a/chrome/browser/metrics/metrics_reporting_state.h b/chrome/browser/metrics/metrics_reporting_state.h
index 504d161..d5f1910 100644
--- a/chrome/browser/metrics/metrics_reporting_state.h
+++ b/chrome/browser/metrics/metrics_reporting_state.h
@@ -23,6 +23,10 @@
   // Called from Chrome OS settings change. Chrome OS manages settings
   // externally and metrics service listens for changes.
   kCrosMetricsSettingsChange,
+
+  // Called from Chrome OS on settings creation/initialization. This happens
+  // once on each log in.
+  kCrosMetricsSettingsCreated,
 };
 
 // Changes metrics reporting state without caring about the success of the
diff --git a/chrome/browser/printing/print_browsertest.cc b/chrome/browser/printing/print_browsertest.cc
index 9d8d37f..169c73a 100644
--- a/chrome/browser/printing/print_browsertest.cc
+++ b/chrome/browser/printing/print_browsertest.cc
@@ -100,8 +100,8 @@
 using testing::_;
 
 #if BUILDFLAG(ENABLE_OOP_PRINTING)
-using OnDidInvokeUseDefaultSettingsCallback = base::RepeatingCallback<void()>;
-using OnDidInvokeGetSettingsWithUICallback = base::RepeatingCallback<void()>;
+using OnUseDefaultSettingsCallback = base::RepeatingCallback<void()>;
+using OnGetSettingsWithUICallback = base::RepeatingCallback<void()>;
 
 using ErrorCheckCallback =
     base::RepeatingCallback<void(mojom::ResultCode result)>;
@@ -128,9 +128,8 @@
 
 // Callbacks to run for overrides in `TestPrintJobWorker`.
 struct TestPrintCallbacks {
-  OnDidInvokeUseDefaultSettingsCallback
-      did_invoke_use_default_settings_callback;
-  OnDidInvokeGetSettingsWithUICallback did_invoke_get_settings_with_ui_callback;
+  OnUseDefaultSettingsCallback did_use_default_settings_callback;
+  OnGetSettingsWithUICallback did_get_settings_with_ui_callback;
   OnStopCallback did_stop_callback;
 };
 
@@ -2312,20 +2311,20 @@
   ~TestPrintJobWorker() override = default;
 
  private:
-  void InvokeUseDefaultSettings(SettingsCallback callback) override {
+  void UseDefaultSettings(SettingsCallback callback) override {
     DVLOG(1) << "Observed: invoke use default settings";
-    PrintJobWorker::InvokeUseDefaultSettings(std::move(callback));
-    callbacks_->did_invoke_use_default_settings_callback.Run();
+    PrintJobWorker::UseDefaultSettings(std::move(callback));
+    callbacks_->did_use_default_settings_callback.Run();
   }
 
-  void InvokeGetSettingsWithUI(uint32_t document_page_count,
-                               bool has_selection,
-                               bool is_scripted,
-                               SettingsCallback callback) override {
+  void GetSettingsWithUI(uint32_t document_page_count,
+                         bool has_selection,
+                         bool is_scripted,
+                         SettingsCallback callback) override {
     DVLOG(1) << "Observed: invoke get settings with UI";
-    PrintJobWorker::InvokeGetSettingsWithUI(document_page_count, has_selection,
-                                            is_scripted, std::move(callback));
-    callbacks_->did_invoke_get_settings_with_ui_callback.Run();
+    PrintJobWorker::GetSettingsWithUI(document_page_count, has_selection,
+                                      is_scripted, std::move(callback));
+    callbacks_->did_get_settings_with_ui_callback.Run();
   }
 
   void Stop() override {
@@ -2484,14 +2483,14 @@
           &SystemAccessProcessPrintBrowserTestBase::OnDidStop,
           base::Unretained(this));
     } else {
-      test_print_callbacks_.did_invoke_use_default_settings_callback =
-          base::BindRepeating(&SystemAccessProcessPrintBrowserTestBase::
-                                  OnDidInvokeUseDefaultSettings,
-                              base::Unretained(this));
-      test_print_callbacks_.did_invoke_get_settings_with_ui_callback =
-          base::BindRepeating(&SystemAccessProcessPrintBrowserTestBase::
-                                  OnDidInvokeGetSettingsWithUI,
-                              base::Unretained(this));
+      test_print_callbacks_.did_use_default_settings_callback =
+          base::BindRepeating(
+              &SystemAccessProcessPrintBrowserTestBase::OnUseDefaultSettings,
+              base::Unretained(this));
+      test_print_callbacks_.did_get_settings_with_ui_callback =
+          base::BindRepeating(
+              &SystemAccessProcessPrintBrowserTestBase::OnGetSettingsWithUI,
+              base::Unretained(this));
       test_print_callbacks_.did_stop_callback = base::BindRepeating(
           &SystemAccessProcessPrintBrowserTestBase::OnDidStop,
           base::Unretained(this));
@@ -2629,13 +2628,9 @@
         /*cause_errors=*/true);
   }
 
-  bool did_invoke_use_default_settings() const {
-    return did_invoke_use_default_settings_;
-  }
+  bool did_use_default_settings() const { return did_use_default_settings_; }
 
-  bool did_invoke_get_settings_with_ui() const {
-    return did_invoke_get_settings_with_ui_;
-  }
+  bool did_get_settings_with_ui() const { return did_get_settings_with_ui_; }
 
   bool print_backend_service_use_detected() const {
     return print_backend_service_use_detected_;
@@ -2780,14 +2775,14 @@
   }
 #endif  // BUILDFLAG(ENABLE_OOP_PRINTING)
 
-  void OnDidInvokeUseDefaultSettings() {
-    did_invoke_use_default_settings_ = true;
+  void OnUseDefaultSettings() {
+    did_use_default_settings_ = true;
     PrintBackendServiceDetectionCheck();
     CheckForQuit();
   }
 
-  void OnDidInvokeGetSettingsWithUI() {
-    did_invoke_get_settings_with_ui_ = true;
+  void OnGetSettingsWithUI() {
+    did_get_settings_with_ui_ = true;
     PrintBackendServiceDetectionCheck();
     CheckForQuit();
   }
@@ -2881,8 +2876,8 @@
   TestPrintCallbacks test_print_callbacks_;
   TestPrintOopCallbacks test_print_oop_callbacks_;
   CreatePrintJobWorkerCallback test_create_print_job_worker_callback_;
-  bool did_invoke_use_default_settings_ = false;
-  bool did_invoke_get_settings_with_ui_ = false;
+  bool did_use_default_settings_ = false;
+  bool did_get_settings_with_ui_ = false;
   bool print_backend_service_use_detected_ = false;
   bool simulate_spooling_memory_errors_ = false;
   mojo::Remote<mojom::PrintBackendService> test_remote_;
@@ -3369,8 +3364,8 @@
 
   WaitUntilCallbackReceived();
 
-  EXPECT_TRUE(did_invoke_use_default_settings());
-  EXPECT_TRUE(did_invoke_get_settings_with_ui());
+  EXPECT_TRUE(did_use_default_settings());
+  EXPECT_TRUE(did_get_settings_with_ui());
   EXPECT_TRUE(stop_invoked());
 
   // `PrintBackendService` should never be used when printing in-browser.
diff --git a/chrome/browser/printing/print_job_worker.cc b/chrome/browser/printing/print_job_worker.cc
index d9ae000..ab2f824 100644
--- a/chrome/browser/printing/print_job_worker.cc
+++ b/chrome/browser/printing/print_job_worker.cc
@@ -144,13 +144,13 @@
 }
 
 void PrintJobWorker::GetDefaultSettings(SettingsCallback callback) {
-  DCHECK(task_runner_->RunsTasksInCurrentSequence());
+  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
   DCHECK_EQ(page_number_, PageNumber::npos());
 
   printing_context_->set_margin_type(
       printing::mojom::MarginType::kDefaultMargins);
 
-  InvokeUseDefaultSettings(std::move(callback));
+  UseDefaultSettings(std::move(callback));
 }
 
 void PrintJobWorker::GetSettingsFromUser(uint32_t document_page_count,
@@ -158,40 +158,27 @@
                                          mojom::MarginType margin_type,
                                          bool is_scripted,
                                          SettingsCallback callback) {
-  DCHECK(task_runner_->RunsTasksInCurrentSequence());
+  DCHECK_CURRENTLY_ON(BrowserThread::UI);
   DCHECK_EQ(page_number_, PageNumber::npos());
 
   printing_context_->set_margin_type(margin_type);
 
-  InvokeGetSettingsWithUI(document_page_count, has_selection, is_scripted,
-                          std::move(callback));
-}
-
-void PrintJobWorker::SetSettings(base::Value::Dict new_settings,
-                                 SettingsCallback callback) {
-  DCHECK(task_runner_->RunsTasksInCurrentSequence());
-
-  content::GetUIThreadTaskRunner({})->PostTask(
-      FROM_HERE, base::BindOnce(&PrintJobWorker::UpdatePrintSettings,
-                                base::Unretained(this), std::move(new_settings),
-                                std::move(callback)));
+  GetSettingsWithUI(document_page_count, has_selection, is_scripted,
+                    std::move(callback));
 }
 
 #if BUILDFLAG(IS_CHROMEOS)
 void PrintJobWorker::SetSettingsFromPOD(
     std::unique_ptr<PrintSettings> new_settings,
     SettingsCallback callback) {
-  DCHECK(task_runner_->RunsTasksInCurrentSequence());
+  DCHECK_CURRENTLY_ON(BrowserThread::UI);
 
-  content::GetUIThreadTaskRunner({})->PostTask(
-      FROM_HERE, base::BindOnce(&PrintJobWorker::UpdatePrintSettingsFromPOD,
-                                base::Unretained(this), std::move(new_settings),
-                                std::move(callback)));
+  UpdatePrintSettingsFromPOD(std::move(new_settings), std::move(callback));
 }
 #endif
 
-void PrintJobWorker::UpdatePrintSettings(base::Value::Dict new_settings,
-                                         SettingsCallback callback) {
+void PrintJobWorker::SetSettings(base::Value::Dict new_settings,
+                                 SettingsCallback callback) {
   DCHECK_CURRENTLY_ON(BrowserThread::UI);
 
   std::unique_ptr<crash_keys::ScopedPrinterInfo> crash_key;
@@ -250,23 +237,6 @@
   std::move(callback).Run(printing_context_->TakeAndResetSettings(), result);
 }
 
-void PrintJobWorker::InvokeUseDefaultSettings(SettingsCallback callback) {
-  content::GetUIThreadTaskRunner({})->PostTask(
-      FROM_HERE, base::BindOnce(&PrintJobWorker::UseDefaultSettings,
-                                base::Unretained(this), std::move(callback)));
-}
-
-void PrintJobWorker::InvokeGetSettingsWithUI(uint32_t document_page_count,
-                                             bool has_selection,
-                                             bool is_scripted,
-                                             SettingsCallback callback) {
-  content::GetUIThreadTaskRunner({})->PostTask(
-      FROM_HERE,
-      base::BindOnce(&PrintJobWorker::GetSettingsWithUI, base::Unretained(this),
-                     document_page_count, has_selection, is_scripted,
-                     std::move(callback)));
-}
-
 void PrintJobWorker::GetSettingsWithUI(uint32_t document_page_count,
                                        bool has_selection,
                                        bool is_scripted,
@@ -312,6 +282,8 @@
 }
 
 void PrintJobWorker::UseDefaultSettings(SettingsCallback callback) {
+  DCHECK_CURRENTLY_ON(BrowserThread::UI);
+
   mojom::ResultCode result;
   {
 #if BUILDFLAG(IS_WIN)
diff --git a/chrome/browser/printing/print_job_worker.h b/chrome/browser/printing/print_job_worker.h
index f52f4254..bfa621cf 100644
--- a/chrome/browser/printing/print_job_worker.h
+++ b/chrome/browser/printing/print_job_worker.h
@@ -50,22 +50,26 @@
 
   /* The following functions may only be called before calling SetPrintJob(). */
 
+  // Initializes the default print settings. Must be called on the UI thread.
   void GetDefaultSettings(SettingsCallback callback);
 
-  // Initializes the print settings. A Print... dialog box will be shown to ask
-  // the user their preference.
-  // `is_scripted` should be true for calls coming straight from window.print().
+  // Initializes the print settings. Must be called on the UI thread. A Print...
+  // dialog box will be shown to ask the user their preference. `is_scripted`
+  // should be true for calls coming straight from window.print().
   void GetSettingsFromUser(uint32_t document_page_count,
                            bool has_selection,
                            mojom::MarginType margin_type,
                            bool is_scripted,
                            SettingsCallback callback);
 
-  // Set the new print settings from a dictionary value.
-  void SetSettings(base::Value::Dict new_settings, SettingsCallback callback);
+  // Set the new print settings from a dictionary value. Must be called on the
+  // UI thread.
+  virtual void SetSettings(base::Value::Dict new_settings,
+                           SettingsCallback callback);
 
 #if BUILDFLAG(IS_CHROMEOS)
-  // Set the new print settings from a POD type.
+  // Set the new print settings from a POD type. Must be called on the UI
+  // thread.
   void SetSettingsFromPOD(std::unique_ptr<printing::PrintSettings> new_settings,
                           SettingsCallback callback);
 #endif
@@ -136,22 +140,23 @@
   // Reports settings back to `callback`.
   void GetSettingsDone(SettingsCallback callback, mojom::ResultCode result);
 
-  // Helper functions to invoke the desired way of getting system print
-  // settings.
-  virtual void InvokeUseDefaultSettings(SettingsCallback callback);
-  virtual void InvokeGetSettingsWithUI(uint32_t document_page_count,
-                                       bool has_selection,
-                                       bool is_scripted,
-                                       SettingsCallback callback);
-
-  // Called on the UI thread to update the print settings.
-  virtual void UpdatePrintSettings(base::Value::Dict new_settings,
-                                   SettingsCallback callback);
-
   // Discards the current document, the current page and cancels the printing
   // context.
   virtual void OnFailure();
 
+  // Asks the user for print settings. Must be called on the UI thread.
+  // Required on Mac and Linux. Windows can display UI from non-main threads,
+  // but sticks with this for consistency.
+  virtual void GetSettingsWithUI(uint32_t document_page_count,
+                                 bool has_selection,
+                                 bool is_scripted,
+                                 SettingsCallback callback);
+
+  // Use the default settings. When using GTK+ or Mac, this can still end up
+  // displaying a dialog. So this needs to happen from the UI thread on these
+  // systems.
+  virtual void UseDefaultSettings(SettingsCallback callback);
+
   PrintingContext* printing_context() { return printing_context_.get(); }
   PrintedDocument* document() { return document_.get(); }
   PrintJob* print_job() { return print_job_; }
@@ -168,14 +173,6 @@
   bool OnNewPageHelperGdi();
 #endif  // BUILDFLAG(IS_WIN)
 
-  // Asks the user for print settings. Must be called on the UI thread.
-  // Required on Mac and Linux. Windows can display UI from non-main threads,
-  // but sticks with this for consistency.
-  void GetSettingsWithUI(uint32_t document_page_count,
-                         bool has_selection,
-                         bool is_scripted,
-                         SettingsCallback callback);
-
 #if BUILDFLAG(IS_CHROMEOS)
   // Called on the UI thread to update the print settings.
   void UpdatePrintSettingsFromPOD(
@@ -183,11 +180,6 @@
       SettingsCallback callback);
 #endif
 
-  // Use the default settings. When using GTK+ or Mac, this can still end up
-  // displaying a dialog. So this needs to happen from the UI thread on these
-  // systems.
-  void UseDefaultSettings(SettingsCallback callback);
-
   // Printing context delegate.
   const std::unique_ptr<PrintingContext::Delegate> printing_context_delegate_;
 
diff --git a/chrome/browser/printing/print_job_worker_oop.cc b/chrome/browser/printing/print_job_worker_oop.cc
index 1e158ec..398d59a 100644
--- a/chrome/browser/printing/print_job_worker_oop.cc
+++ b/chrome/browser/printing/print_job_worker_oop.cc
@@ -299,23 +299,17 @@
   // PrintBackend service.
 }
 
-void PrintJobWorkerOop::InvokeUseDefaultSettings(SettingsCallback callback) {
-  content::GetUIThreadTaskRunner({})->PostTask(
-      FROM_HERE,
-      base::BindOnce(&PrintJobWorkerOop::SendUseDefaultSettings,
-                     ui_weak_factory_.GetWeakPtr(), std::move(callback)));
+void PrintJobWorkerOop::UseDefaultSettings(SettingsCallback callback) {
+  SendUseDefaultSettings(std::move(callback));
 }
 
-void PrintJobWorkerOop::InvokeGetSettingsWithUI(uint32_t document_page_count,
-                                                bool has_selection,
-                                                bool is_scripted,
-                                                SettingsCallback callback) {
+void PrintJobWorkerOop::GetSettingsWithUI(uint32_t document_page_count,
+                                          bool has_selection,
+                                          bool is_scripted,
+                                          SettingsCallback callback) {
 #if BUILDFLAG(IS_WIN)
-  content::GetUIThreadTaskRunner({})->PostTask(
-      FROM_HERE,
-      base::BindOnce(&PrintJobWorkerOop::SendAskUserForSettings,
-                     ui_weak_factory_.GetWeakPtr(), document_page_count,
-                     has_selection, is_scripted, std::move(callback)));
+  SendAskUserForSettings(document_page_count, has_selection, is_scripted,
+                         std::move(callback));
 #else
   // Invoke the browser version of getting settings with the system UI:
   //   - macOS:  It is impossible to invoke a system dialog UI from a service
@@ -326,13 +320,13 @@
   //       browser process.
   //   - Other platforms don't have a system print UI or do not use OOP
   //     printing, so this does not matter.
-  PrintJobWorker::InvokeGetSettingsWithUI(document_page_count, has_selection,
-                                          is_scripted, std::move(callback));
+  PrintJobWorker::GetSettingsWithUI(document_page_count, has_selection,
+                                    is_scripted, std::move(callback));
 #endif
 }
 
-void PrintJobWorkerOop::UpdatePrintSettings(base::Value::Dict new_settings,
-                                            SettingsCallback callback) {
+void PrintJobWorkerOop::SetSettings(base::Value::Dict new_settings,
+                                    SettingsCallback callback) {
   DCHECK_CURRENTLY_ON(BrowserThread::UI);
 
   // Do not take a const reference, as `new_settings` will be modified below.
diff --git a/chrome/browser/printing/print_job_worker_oop.h b/chrome/browser/printing/print_job_worker_oop.h
index fe03920..87d655ac 100644
--- a/chrome/browser/printing/print_job_worker_oop.h
+++ b/chrome/browser/printing/print_job_worker_oop.h
@@ -68,13 +68,13 @@
 #endif
   bool SpoolDocument() override;
   void OnDocumentDone() override;
-  void InvokeUseDefaultSettings(SettingsCallback callback) override;
-  void InvokeGetSettingsWithUI(uint32_t document_page_count,
-                               bool has_selection,
-                               bool is_scripted,
-                               SettingsCallback callback) override;
-  void UpdatePrintSettings(base::Value::Dict new_settings,
-                           SettingsCallback callback) override;
+  void UseDefaultSettings(SettingsCallback callback) override;
+  void GetSettingsWithUI(uint32_t document_page_count,
+                         bool has_selection,
+                         bool is_scripted,
+                         SettingsCallback callback) override;
+  void SetSettings(base::Value::Dict new_settings,
+                   SettingsCallback callback) override;
   void OnFailure() override;
 
   // Show the print error dialog, virtual to support testing.
diff --git a/chrome/browser/printing/printer_query.cc b/chrome/browser/printing/printer_query.cc
index 9bee269b..d214a24 100644
--- a/chrome/browser/printing/printer_query.cc
+++ b/chrome/browser/printing/printer_query.cc
@@ -122,13 +122,9 @@
   // Real work is done in PrintJobWorker::GetDefaultSettings().
   is_print_dialog_box_shown_ = false;
   // `this` is owned by `callback`, so `base::Unretained()` is safe.
-  worker_->PostTask(
-      FROM_HERE,
-      base::BindOnce(&PrintJobWorker::GetDefaultSettings,
-                     base::Unretained(worker_.get()),
-                     base::BindOnce(&PrinterQuery::PostSettingsDone,
-                                    base::Unretained(this), std::move(callback),
-                                    is_modifiable)));
+  worker_->GetDefaultSettings(
+      base::BindOnce(&PrinterQuery::PostSettingsDone, base::Unretained(this),
+                     std::move(callback), is_modifiable));
 }
 
 void PrinterQuery::GetSettingsFromUser(uint32_t expected_page_count,
@@ -145,14 +141,10 @@
   // Real work is done in PrintJobWorker::GetSettingsFromUser().
   is_print_dialog_box_shown_ = true;
   // `this` is owned by `callback`, so `base::Unretained()` is safe.
-  worker_->PostTask(
-      FROM_HERE,
-      base::BindOnce(&PrintJobWorker::GetSettingsFromUser,
-                     base::Unretained(worker_.get()), expected_page_count,
-                     has_selection, margin_type, is_scripted,
-                     base::BindOnce(&PrinterQuery::PostSettingsDone,
-                                    base::Unretained(this), std::move(callback),
-                                    is_modifiable)));
+  worker_->GetSettingsFromUser(
+      expected_page_count, has_selection, margin_type, is_scripted,
+      base::BindOnce(&PrinterQuery::PostSettingsDone, base::Unretained(this),
+                     std::move(callback), is_modifiable));
 }
 
 void PrinterQuery::SetSettings(base::Value::Dict new_settings,
@@ -161,13 +153,11 @@
 
   StartWorker();
   // `this` is owned by `callback`, so `base::Unretained()` is safe.
-  worker_->PostTask(
-      FROM_HERE,
-      base::BindOnce(&PrintJobWorker::SetSettings,
-                     base::Unretained(worker_.get()), std::move(new_settings),
-                     base::BindOnce(&PrinterQuery::PostSettingsDone,
-                                    base::Unretained(this), std::move(callback),
-                                    /*maybe_is_modifiable=*/absl::nullopt)));
+  worker_->SetSettings(
+      std::move(new_settings),
+      base::BindOnce(&PrinterQuery::PostSettingsDone, base::Unretained(this),
+                     std::move(callback),
+                     /*maybe_is_modifiable=*/absl::nullopt));
 }
 
 #if BUILDFLAG(IS_CHROMEOS)
@@ -178,13 +168,11 @@
 
   StartWorker();
   // `this` is owned by `callback`, so `base::Unretained()` is safe.
-  worker_->PostTask(
-      FROM_HERE,
-      base::BindOnce(&PrintJobWorker::SetSettingsFromPOD,
-                     base::Unretained(worker_.get()), std::move(new_settings),
-                     base::BindOnce(&PrinterQuery::PostSettingsDone,
-                                    base::Unretained(this), std::move(callback),
-                                    /*maybe_is_modifiable=*/absl::nullopt)));
+  worker_->SetSettingsFromPOD(
+      std::move(new_settings),
+      base::BindOnce(&PrinterQuery::PostSettingsDone, base::Unretained(this),
+                     std::move(callback),
+                     /*maybe_is_modifiable=*/absl::nullopt));
 }
 #endif
 
diff --git a/chrome/browser/reputation/local_heuristics.cc b/chrome/browser/reputation/local_heuristics.cc
index 181581f..581267a 100644
--- a/chrome/browser/reputation/local_heuristics.cc
+++ b/chrome/browser/reputation/local_heuristics.cc
@@ -111,6 +111,7 @@
           reputation::HeuristicLaunchConfig::HEURISTIC_CHARACTER_SWAP_TOP_SITES,
           navigated_domain.domain_and_registry, chrome::GetChannel());
     case LookalikeUrlMatchType::kComboSquatting:
+    case LookalikeUrlMatchType::kComboSquattingSiteEngagement:
       return false;
     case LookalikeUrlMatchType::kNone:
       NOTREACHED();
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/range_automation_handler.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/range_automation_handler.js
index 21d26aa..9c38219 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/range_automation_handler.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/range_automation_handler.js
@@ -23,9 +23,7 @@
 const RoleType = chrome.automation.RoleType;
 const StateType = chrome.automation.StateType;
 
-/**
- * @implements {ChromeVoxStateObserver}
- */
+/** @implements {ChromeVoxStateObserver} */
 export class RangeAutomationHandler extends BaseAutomationHandler {
   /** @private */
   constructor() {
@@ -113,9 +111,7 @@
     this.addListener_(EventType.SORT_CHANGED, this.onAriaAttributeChanged);
   }
 
-  /**
-   * @param {!ChromeVoxEvent} evt
-   */
+  /** @param {!ChromeVoxEvent} evt */
   onEventIfInRange(evt) {
     if (BaseAutomationHandler.disallowEventFromAction(evt)) {
       return;
@@ -161,9 +157,7 @@
     }
   }
 
-  /**
-   * @param {!ChromeVoxEvent} evt
-   */
+  /** @param {!ChromeVoxEvent} evt */
   onAriaAttributeChanged(evt) {
     // Don't report changes on editable nodes since they interfere with text
     // selection changes. Users can query via Search+k for the current state
diff --git a/chrome/browser/resources/chromeos/network_ui/network_ui.html b/chrome/browser/resources/chromeos/network_ui/network_ui.html
index df669e2..4e50692 100644
--- a/chrome/browser/resources/chromeos/network_ui/network_ui.html
+++ b/chrome/browser/resources/chromeos/network_ui/network_ui.html
@@ -8,7 +8,9 @@
   }
 
   #global-policy,
-  #tethering-capabilities-div {
+  #tethering-capabilities-div,
+  #tethering-status-div,
+  #tethering-config-div {
     white-space: pre-wrap;
   }
 
@@ -22,6 +24,11 @@
     margin: 10px 0
   }
 
+  #tethering-config-input {
+    margin-bottom: 0;
+    margin-top: 10px;
+  }
+
   iron-pages {
     flex: 1;
     position: relative;
@@ -149,6 +156,28 @@
       <cr-button class="action-button" on-click="getTetheringCapabilities_">
         $i18n{refreshTetheringCapabilitiesButtonText}
       </cr-button>
+
+      <h2>$i18n{tetheringStatusLabel}</h2>
+      <div id="tethering-status-div"></div>
+      <cr-button class="action-button" on-click="getTetheringStatus_">
+        $i18n{refreshTetheringStatusButtonText}
+      </cr-button>
+
+      <h2>$i18n{tetheringConfigLabel}</h2>
+      <div id="tethering-config-div"></div>
+      <cr-button class="action-button" on-click="getTetheringConfig_">
+        $i18n{refreshTetheringConfigButtonText}
+      </cr-button>
+      <cr-input id="tethering-config-input" value="{{tetheringConfigToSet_}}"
+          placeholder="{}" type="text" on-input="validateJSON_"
+          error-message="Please enter valid JSON object"
+          invalid="[[invalidJSON_]]">
+      </cr-input>
+      <cr-button class="action-button" on-click="setTetheringConfig_"
+          disabled="[[invalidJSON_]]">
+        $i18n{setTetheringConfigButtonText}
+      </cr-button>
+      <div id="set-tethering-config-result"></div>
     </div>
   <template>
 </iron-pages>
diff --git a/chrome/browser/resources/chromeos/network_ui/network_ui.js b/chrome/browser/resources/chromeos/network_ui/network_ui.js
index 11451c3..4eff0283 100644
--- a/chrome/browser/resources/chromeos/network_ui/network_ui.js
+++ b/chrome/browser/resources/chromeos/network_ui/network_ui.js
@@ -74,6 +74,12 @@
     },
 
     /** @private */
+    tetheringConfigToSet_: {
+      type: String,
+      value: '',
+    },
+
+    /** @private */
     isGuestModeActive_: {
       type: Boolean,
       value() {
@@ -90,6 +96,13 @@
             loadTimeData.getBoolean('isHotspotEnabled');
       },
     },
+
+    /** @private */
+    invalidJSON_: {
+      type: Boolean,
+      value: false,
+    },
+
   },
 
   /** @type {?chromeos.networkConfig.mojom.CrosNetworkConfigRemote} */
@@ -116,6 +129,8 @@
 
     this.requestGlobalPolicy_();
     this.getTetheringCapabilities_();
+    this.getTetheringConfig_();
+    this.getTetheringStatus_();
     this.getHostname_();
     this.selectTabFromHash_();
     window.addEventListener('hashchange', () => {
@@ -123,6 +138,15 @@
     });
   },
 
+  /**
+   * @param {*} result that need to stringify to JSON
+   * @return {string}
+   * @private
+   */
+  stringifyJSON_(result) {
+    return JSON.stringify(result, null, '\t');
+  },
+
   /** @private */
   selectTabFromHash_() {
     const selectedTab = window.location.hash.substring(1);
@@ -211,7 +235,7 @@
   requestGlobalPolicy_() {
     this.networkConfig_.getGlobalPolicy().then(result => {
       this.$$('#global-policy').textContent =
-          JSON.stringify(result.result, null, '\t');
+          this.stringifyJSON_(result.result);
     });
   },
 
@@ -219,10 +243,63 @@
   getTetheringCapabilities_() {
     this.browserProxy_.getTetheringCapabilities().then(result => {
       this.$$('#tethering-capabilities-div').textContent =
-          JSON.stringify(result, null, '\t');
+          this.stringifyJSON_(result);
     });
   },
 
+  /** @private */
+  getTetheringStatus_() {
+    this.browserProxy_.getTetheringStatus().then(result => {
+      this.$$('#tethering-status-div').textContent =
+          this.stringifyJSON_(result);
+    });
+  },
+
+  /** @private */
+  getTetheringConfig_() {
+    this.browserProxy_.getTetheringConfig().then(result => {
+      this.$$('#tethering-config-div').textContent =
+          this.stringifyJSON_(result);
+    });
+  },
+
+  /** @private */
+  setTetheringConfig_() {
+    this.browserProxy_.setTetheringConfig(this.tetheringConfigToSet_)
+        .then((result) => {
+          const success = result === 'success';
+          const resultDiv = this.$$('#set-tethering-config-result');
+          resultDiv.innerText = result;
+          resultDiv.classList.toggle('error', !success);
+          if (success) {
+            this.getTetheringConfig_();
+          }
+        });
+  },
+
+  /**
+   * Check if the input tethering config string is a valid JSON object.
+   * @private
+   */
+  validateJSON_() {
+    if (this.tetheringConfigToSet_ === '') {
+      this.invalidJSON_ = false;
+      return;
+    }
+
+    try {
+      const parsed = JSON.parse(this.tetheringConfigToSet_);
+      // Check if the parsed JSON is object type by its constructor
+      if (parsed.constructor === ({}).constructor) {
+        this.invalidJSON_ = false;
+        return;
+      }
+      this.invalidJSON_ = true;
+    } catch (e) {
+      this.invalidJSON_ = true;
+    }
+  },
+
   /**
    * @param {!Event} event
    * @private
diff --git a/chrome/browser/resources/chromeos/network_ui/network_ui_browser_proxy.js b/chrome/browser/resources/chromeos/network_ui/network_ui_browser_proxy.js
index aaea2aa..01a74b7 100644
--- a/chrome/browser/resources/chromeos/network_ui/network_ui_browser_proxy.js
+++ b/chrome/browser/resources/chromeos/network_ui/network_ui_browser_proxy.js
@@ -82,6 +82,22 @@
    * @return {Promise<string>}
    */
   getTetheringCapabilities() {}
+
+  /**
+   * @return {Promise<string>}
+   */
+  getTetheringStatus() {}
+
+  /**
+   * @return {Promise<string>}
+   */
+  getTetheringConfig() {}
+
+  /**
+   * @param {string} config
+   * @return {Promise<string>}
+   */
+  setTetheringConfig(config) {}
 }
 
 /**
@@ -183,6 +199,28 @@
   getTetheringCapabilities() {
     return sendWithPromise('getTetheringCapabilities');
   }
+
+  /**
+   * @return {Promise<string>}
+   */
+  getTetheringStatus() {
+    return sendWithPromise('getTetheringStatus');
+  }
+
+  /**
+   * @return {Promise<string>}
+   */
+  getTetheringConfig() {
+    return sendWithPromise('getTetheringConfig');
+  }
+
+  /**
+   * @param {string} config
+   * @return {Promise<string>}
+   */
+  setTetheringConfig(config) {
+    return sendWithPromise('setTetheringConfig', config);
+  }
 }
 
 addSingletonGetter(NetworkUIBrowserProxyImpl);
diff --git a/chrome/browser/resources/settings/chromeos/BUILD.gn b/chrome/browser/resources/settings/chromeos/BUILD.gn
index 45864fa..ff1a8398 100644
--- a/chrome/browser/resources/settings/chromeos/BUILD.gn
+++ b/chrome/browser/resources/settings/chromeos/BUILD.gn
@@ -7,17 +7,19 @@
 import("//tools/grit/grit_rule.gni")
 import("//tools/grit/preprocess_if_expr.gni")
 import("//tools/polymer/html_to_js.gni")
+import("//tools/typescript/ts_library.gni")
 import("//ui/webui/resources/tools/generate_grd.gni")
 import("//ui/webui/webui_features.gni")
 import("../../tools/optimize_webui.gni")
 import("./os_settings.gni")
 
-assert(is_chromeos_ash)
+assert(is_chromeos_ash, "ChromeOS Settings is ChromeOS only")
 
 # root_gen_dir is "gen"
 # target_gen_dir is "gen/chrome/browser/resources/settings/chromeos"
 
 preprocessed_folder = "preprocessed"
+preprocessed_ts_folder = "preprocessed_ts"
 web_components_manifest = "web_components_manifest.json"
 non_web_component_files_manifest = "non_web_component_files_manifest.json"
 browser_settings_tsc_manifest = "browser_settings_tsc_manifest.json"
@@ -27,7 +29,7 @@
 if (optimize_webui) {
   build_manifest_v3 = "build_v3_manifest.json"
 
-  optimize_webui("build_polymer3") {
+  optimize_webui("optimize_bundle") {
     host = "os-settings"
     input = rebase_path("$target_gen_dir/$preprocessed_folder", root_build_dir)
     js_module_in_files = [
@@ -42,6 +44,7 @@
     out_manifest = "$target_gen_dir/$build_manifest_v3"
 
     deps = [
+      ":build_ts",
       ":preprocess_browser_settings_tsc",
       ":preprocess_mojo_webui",
       ":preprocess_non_web_component_files",
@@ -95,6 +98,42 @@
   }
 }
 
+# TypeScript Build Configuration
+# TODO(crbug/1315757) Gradually remove JS files from preprocess_web_components
+# and preprocess_non_web_component_files, and add them as input files here.
+# Eventually, ts_library() will be the only build path for all JS/TS files.
+# Any JS file in passed into this build rule is elligble to be converted to TS.
+ts_library("build_ts") {
+  tsconfig_base = "tsconfig_base.json"
+  deps = [
+    "//third_party/polymer/v3_0:library",
+    "//ui/webui/resources:library",
+  ]
+  extra_deps = [
+    ":preprocess_ts_non_web_component_files",
+    ":preprocess_ts_web_components",
+  ]
+  definitions = [ "//tools/typescript/definitions/chrome_send.d.ts" ]
+  root_dir = "$target_gen_dir/$preprocessed_ts_folder"
+  in_files = ts_non_web_component_files + ts_web_component_files
+  out_dir = "$target_gen_dir/$preprocessed_folder"
+}
+
+preprocess_if_expr("preprocess_ts_non_web_component_files") {
+  defines = chrome_grit_defines
+  in_folder = "../"
+  in_files = ts_non_web_component_files
+  out_folder = "$target_gen_dir/$preprocessed_ts_folder"
+}
+
+preprocess_if_expr("preprocess_ts_web_components") {
+  defines = chrome_grit_defines
+  deps = [ ":generate_web_components" ]
+  in_folder = get_path_info("../", "gen_dir")
+  in_files = ts_web_component_files
+  out_folder = "$target_gen_dir/$preprocessed_ts_folder"
+}
+
 # Preprocess all WebUI mojom files, which are bundled in optimized builds.
 preprocess_if_expr("preprocess_mojo_webui") {
   deps = [
@@ -264,7 +303,7 @@
   ]
 
   if (optimize_webui) {
-    deps += [ ":build_polymer3" ]
+    deps += [ ":optimize_bundle" ]
     manifest_files += [ "$target_gen_dir/$build_manifest_v3" ]
     input_files += [ "../../nearby_share/shared/nearby_shared_icons.html" ]
     resource_path_rewrites += [
@@ -276,6 +315,7 @@
     ]
   } else {
     deps += [
+      ":build_ts",
       ":preprocess_browser_settings_tsc",
       ":preprocess_mojo_webui",
       ":preprocess_non_web_component_files",
@@ -289,6 +329,7 @@
       "$target_gen_dir/$non_web_component_files_manifest",
       "$target_gen_dir/$browser_settings_tsc_manifest",
       "$target_gen_dir/$mojo_webui_manifest",
+      "$target_gen_dir/build_ts.manifest",
     ]
     resource_path_rewrites += [ "chromeos/os_settings.html|os_settings.html" ]
   }
@@ -300,20 +341,8 @@
   out_folder = "$target_gen_dir/$preprocessed_folder"
   out_manifest = "$target_gen_dir/$non_web_component_files_manifest"
   in_files = [
-    "chromeos/deep_linking_behavior.js",
-    "chromeos/metrics_recorder.js",
-    "chromeos/os_settings_routes.js",
-    "chromeos/route_origin_behavior.js",
-    "chromeos/combined_search_handler.js",
-    "chromeos/personalization_search_handler.js",
-    "chromeos/settings_search_handler.js",
-    "chromeos/os_route.js",
-    "chromeos/os_page_visibility.js",
     "chromeos/os_people_page/lock_state_behavior.js",
     "chromeos/os_people_page/os_sync_browser_proxy.js",
-    "chromeos/pref_to_setting_metric_converter.js",
-    "chromeos/ensure_lazy_loaded.js",
-    "chromeos/lazy_load.js",
     "chromeos/crostini_page/crostini_browser_proxy.js",
     "chromeos/date_time_page/date_time_types.js",
     "chromeos/date_time_page/timezone_browser_proxy.js",
@@ -321,7 +350,6 @@
     "chromeos/device_page/device_page_browser_proxy.js",
     "chromeos/device_page/drag_behavior.js",
     "chromeos/device_page/layout_behavior.js",
-    "chromeos/global_scroll_target_behavior.js",
     "chromeos/google_assistant_page/google_assistant_browser_proxy.js",
     "chromeos/guest_os/guest_os_browser_proxy.js",
     "chromeos/os_privacy_page/privacy_hub_browser_proxy.js",
@@ -377,19 +405,15 @@
     "chromeos/os_about_page/device_name_browser_proxy.js",
     "chromeos/os_about_page/device_name_util.js",
     "chromeos/os_reset_page/os_reset_browser_proxy.js",
-    "chromeos/os_settings.js",
     "chromeos/parental_controls_page/parental_controls_browser_proxy.js",
     "chromeos/personalization_page/change_picture_browser_proxy.js",
     "chromeos/personalization_page/personalization_hub_browser_proxy.js",
     "chromeos/personalization_page/wallpaper_browser_proxy.js",
-    "chromeos/prefs_behavior.js",
     "chromeos/os_privacy_page/peripheral_data_access_browser_proxy.js",
     "chromeos/os_privacy_page/metrics_consent_browser_proxy.js",
     "chromeos/os_people_page/account_manager_browser_proxy.js",
-    "chromeos/route_observer_behavior.js",
     "chromeos/ambient_mode_page/ambient_mode_browser_proxy.js",
     "chromeos/ambient_mode_page/constants.js",
-    "router.js",
   ]
 }
 
@@ -430,8 +454,8 @@
     "site_favicon.js",
   ]
 
+  # Files that don't have a corresponding HTML file.
   in_files = [
-    # Files that don't have a corresponding HTML file.
     "appearance_page/fonts_browser_proxy.js",
     "controls/cr_policy_pref_mixin.js",
     "controls/pref_control_mixin.js",
@@ -619,7 +643,6 @@
     "chromeos/os_bluetooth_page/settings_fast_pair_toggle.js",
     "chromeos/os_files_page/os_files_page.js",
     "chromeos/os_files_page/smb_shares_page.js",
-    "chromeos/os_icons.js",
     "chromeos/os_languages_page/add_input_methods_dialog.js",
     "chromeos/os_languages_page/add_items_dialog.js",
     "chromeos/os_languages_page/add_spellcheck_languages_dialog.js",
@@ -671,7 +694,6 @@
     "chromeos/os_search_page/os_search_selection_dialog.js",
     "chromeos/os_search_page/search_engine.js",
     "chromeos/os_search_page/search_subpage.js",
-    "chromeos/os_settings_icons_css.js",
     "chromeos/os_settings_main/os_settings_main.js",
     "chromeos/os_settings_menu/os_settings_menu.js",
     "chromeos/os_settings_page/os_settings_page.js",
diff --git a/chrome/browser/resources/settings/chromeos/device_page/audio.html b/chrome/browser/resources/settings/chromeos/device_page/audio.html
index 1238065..a65bfd5 100644
--- a/chrome/browser/resources/settings/chromeos/device_page/audio.html
+++ b/chrome/browser/resources/settings/chromeos/device_page/audio.html
@@ -1,4 +1,18 @@
 <style include="settings-shared">
+  .audio-mute-button {
+    margin-inline-end: var(--settings-control-label-spacing);
+  }
+
+  .audio-mute-button-container {
+    border-inline-end: 1px solid var(--cros-app-shield-color);
+    margin-inline-end: var(--settings-control-label-spacing);
+  }
+
+  .audio-output-options-container {
+    display: flex;
+    flex-direction: row;
+  }
+
   .audio-output-slider {
     width: 100px;
   }
@@ -25,6 +39,30 @@
     padding-inline-end: 0;
     padding-inline-start: 0;
   }
+
+  :host([is-output-muted_]) #outputVolumeSlider {
+    --cr-slider-active-color: var(--cros-slider-color-inactive);
+    --cr-slider-container-color: var(--cros-slider-track-color-inactive);
+    --cr-slider-knob-color-rgb: var(--cros-color-primary-rgb);
+  }
+
+  :host([is-output-muted_]) #audioOutputMuteButton {
+    --cr-icon-button-fill-color: var(--cros-color-prominent);
+    background-color: var(--cros-ripple-color-prominent);
+  }
+
+  :host([is-output-muted_]) #audioOutputMuteButton:hover {
+    --cr-icon-button-fill-color: var(--cros-color-prominent);
+    background-color: var(--cros-highlight-color);
+  }
+
+  :host(:not([is-output-muted_])) #audioOutputMuteButton {
+    --cr-icon-button-fill-color: var(--cros-color-primary);
+  }
+
+  :host(:not([is-output-muted_])) #audioOutputMuteButton:hover {
+    --cr-icon-button-fill-color: var(--cros-color-primary);
+  }
 </style>
 
 <!-- Output section -->
@@ -36,22 +74,31 @@
         <div class="start settings-box-text" id="audioOutputVolumeLabel">
           $i18n{audioVolumeTitle}
         </div>
-        <div class="audio-slider-wrapper" id="audioOutputSliderWrapper">
-          <iron-icon id="audioOutputSliderVolumeDownIcon"
-              icon="settings:volume-down">
-          </iron-icon>
-          <cr-slider class="audio-output-slider"
-              id ="outputVolumeSlider"
-              min="0"
-              max="100"
-              disabled="[[isOutputVolumeSliderDisabled_(
-                  audioSystemProperties_.outputMuteState
-                )]]"
-              value="[[audioSystemProperties_.outputVolumePercent]]">
-          </cr-slider>
-          <iron-icon id="audioOutputSliderVolumeUpIcon"
-              icon="settings:volume-up">
-          </iron-icon>
+        <!-- TODO(crbug.com/1092970): Update to new UI once approved. -->
+        <div class="audio-output-options-container">
+          <div class="audio-mute-button-container">
+            <cr-icon-button class="audio-mute-button"
+                id="audioOutputMuteButton" iron-icon="settings:volume-up-off">
+            </cr-icon-button>
+          </div>
+          <div class="audio-slider-wrapper" id="audioOutputSliderWrapper">
+            <!-- TODO(crbug.com/1092970): Change icons to buttons. -->
+            <iron-icon id="audioOutputSliderVolumeDownIcon"
+                icon="settings:volume-down">
+            </iron-icon>
+            <cr-slider class="audio-output-slider"
+                id ="outputVolumeSlider"
+                min="0"
+                max="100"
+                disabled="[[isOutputVolumeSliderDisabled_(
+                    audioSystemProperties_.outputMuteState
+                  )]]"
+                value="[[audioSystemProperties_.outputVolumePercent]]">
+            </cr-slider>
+            <iron-icon id="audioOutputSliderVolumeUpIcon"
+                icon="settings:volume-up">
+            </iron-icon>
+          </div>
         </div>
       </div>
     </div>
diff --git a/chrome/browser/resources/settings/chromeos/device_page/audio.js b/chrome/browser/resources/settings/chromeos/device_page/audio.js
index 2c41bd0..457f5fa6 100644
--- a/chrome/browser/resources/settings/chromeos/device_page/audio.js
+++ b/chrome/browser/resources/settings/chromeos/device_page/audio.js
@@ -52,6 +52,12 @@
       audioSystemProperties_: {
         type: Object,
       },
+
+      /** @protected */
+      isOutputMuted_: {
+        type: Boolean,
+        reflectToAttribute: true,
+      },
     };
   }
 
@@ -79,6 +85,19 @@
    */
   onPropertiesUpdated(properties) {
     this.audioSystemProperties_ = properties;
+
+    // TODO(crbug.com/1092970): Create and show managed by policy badge if
+    // kMutedByPolicy.
+    this.isOutputMuted_ =
+        this.audioSystemProperties_.outputMuteState !== MuteState.kNotMuted;
+  }
+
+  /**
+   * @public
+   * @return {boolean}
+   */
+  getIsOutputMutedForTest() {
+    return this.isOutputMuted_;
   }
 
   /** @protected */
@@ -88,8 +107,11 @@
             .bindNewPipeAndPassRemote());
   }
 
-  // TODO(crbug.com/1092970): Create onCrSliderChanged method for setting output
-  // volume.
+  // TODO(crbug.com/1092970): Create onCrSliderChanged_ method for setting
+  // output volume.
+
+  // TODO(crbug.com/1092970): Create onOutputMuteTap_ method for setting output
+  // mute state.
 
   /**
    * @protected
diff --git a/chrome/browser/resources/settings/chromeos/multidevice_page/multidevice_permissions_setup_dialog.js b/chrome/browser/resources/settings/chromeos/multidevice_page/multidevice_permissions_setup_dialog.js
index f8e9ecbc02..f4ad5e29 100644
--- a/chrome/browser/resources/settings/chromeos/multidevice_page/multidevice_permissions_setup_dialog.js
+++ b/chrome/browser/resources/settings/chromeos/multidevice_page/multidevice_permissions_setup_dialog.js
@@ -47,6 +47,8 @@
   NOTIFICATION_ACCESS_PROHIBITED: 6,
   COMPLETED_USER_REJECTED: 7,
   FAILED_OR_CANCELLED: 8,
+  CAMERA_ROLL_GRANTED_NOTIFICATION_REJECTED: 9,
+  CAMERA_ROLL_REJECTED_NOTIFICATION_GRANTED: 10,
 };
 
 /**
@@ -409,9 +411,10 @@
     // COMPLETED_USER_REJECTED we should continue on with the setup flow if
     // there are additional features, all other results will change the screen
     // that is shown and pause or terminate the setup flow.
-    if (combinedSetupResult !== PermissionsSetupStatus.COMPLETED_SUCCESSFULLY &&
-        combinedSetupResult !==
-            PermissionsSetupStatus.COMPLETED_USER_REJECTED) {
+    if (this.terminateCombinedSetup_(combinedSetupResult)) {
+      if (combinedSetupResult === PermissionsSetupStatus.FAILED_OR_CANCELLED) {
+        this.updateCamearRollSetupResultIfNeeded_();
+      }
       this.setupState_ = combinedSetupResult;
       return;
     }
@@ -420,17 +423,18 @@
     // this.completeMode_. Otherwise, we cannot use the final
     // this.completedMode_ to determine the completed title.
     if (combinedSetupResult === PermissionsSetupStatus.COMPLETED_SUCCESSFULLY) {
-      if (this.setupMode_ & CAMERA_ROLL_FEATURE && !this.showCameraRoll) {
-        this.completedMode_ |= CAMERA_ROLL_FEATURE;
-        this.browserProxy_.setFeatureEnabledState(
-            MultiDeviceFeature.PHONE_HUB_CAMERA_ROLL, true);
-      }
+      this.updateCamearRollSetupResultIfNeeded_();
+      this.updateNotificationsSetupResultIfNeeded_();
+    }
 
-      if (this.setupMode_ & NOTIFICATION_FEATURE && !this.showNotifications) {
-        this.completedMode_ |= NOTIFICATION_FEATURE;
-        this.browserProxy_.setFeatureEnabledState(
-            MultiDeviceFeature.PHONE_HUB_NOTIFICATIONS, true);
-      }
+    if (combinedSetupResult ===
+        PermissionsSetupStatus.CAMERA_ROLL_GRANTED_NOTIFICATION_REJECTED) {
+      this.updateCamearRollSetupResultIfNeeded_();
+    }
+
+    if (combinedSetupResult ===
+        PermissionsSetupStatus.CAMERA_ROLL_REJECTED_NOTIFICATION_GRANTED) {
+      this.updateNotificationsSetupResultIfNeeded_();
     }
 
     if (this.showAppStreaming) {
@@ -450,6 +454,54 @@
   }
 
   /**
+   * @param {!PermissionsSetupStatus} combinedSetupResult
+   * @return {boolean}
+   * @private
+   */
+  terminateCombinedSetup_(combinedSetupResult) {
+    switch (combinedSetupResult) {
+      case PermissionsSetupStatus.COMPLETED_SUCCESSFULLY:
+      case PermissionsSetupStatus.COMPLETED_USER_REJECTED:
+      case PermissionsSetupStatus.CAMERA_ROLL_GRANTED_NOTIFICATION_REJECTED:
+      case PermissionsSetupStatus.CAMERA_ROLL_REJECTED_NOTIFICATION_GRANTED:
+        return false;
+      case PermissionsSetupStatus.CONNECTION_REQUESTED:
+      case PermissionsSetupStatus.CONNECTING:
+      case PermissionsSetupStatus.TIMED_OUT_CONNECTING:
+      case PermissionsSetupStatus.CONNECTION_DISCONNECTED:
+      case PermissionsSetupStatus
+          .SENT_MESSAGE_TO_PHONE_AND_WAITING_FOR_RESPONSE:
+      case PermissionsSetupStatus.NOTIFICATION_ACCESS_PROHIBITED:
+      case PermissionsSetupStatus.FAILED_OR_CANCELLED:
+        return true;
+      default:
+        return true;
+    }
+  }
+
+  /**
+   * @private
+   */
+  updateCamearRollSetupResultIfNeeded_() {
+    if (this.setupMode_ & CAMERA_ROLL_FEATURE && !this.showCameraRoll) {
+      this.completedMode_ |= CAMERA_ROLL_FEATURE;
+      this.browserProxy_.setFeatureEnabledState(
+          MultiDeviceFeature.PHONE_HUB_CAMERA_ROLL, true);
+    }
+  }
+
+  /**
+   * @private
+   */
+  updateNotificationsSetupResultIfNeeded_() {
+    if (this.setupMode_ & NOTIFICATION_FEATURE && !this.showNotifications) {
+      this.completedMode_ |= NOTIFICATION_FEATURE;
+      this.browserProxy_.setFeatureEnabledState(
+          MultiDeviceFeature.PHONE_HUB_NOTIFICATIONS, true);
+    }
+  }
+
+  /**
    * @return {boolean}
    * @private
    */
@@ -483,7 +535,11 @@
   computeHasCompletedSetup_() {
     return this.setupState_ === PermissionsSetupStatus.COMPLETED_SUCCESSFULLY ||
         this.setupState_ === PermissionsSetupStatus.COMPLETED_USER_REJECTED ||
-        this.setupState_ === PermissionsSetupStatus.FAILED_OR_CANCELLED;
+        this.setupState_ === PermissionsSetupStatus.FAILED_OR_CANCELLED ||
+        this.setupState_ ===
+        PermissionsSetupStatus.CAMERA_ROLL_GRANTED_NOTIFICATION_REJECTED ||
+        this.setupState_ ===
+        PermissionsSetupStatus.CAMERA_ROLL_REJECTED_NOTIFICATION_GRANTED;
   }
 
   /**
@@ -634,6 +690,8 @@
       case Status.COMPLETED_SUCCESSFULLY:
       case Status.COMPLETED_USER_REJECTED:
       case Status.FAILED_OR_CANCELLED:
+      case Status.CAMERA_ROLL_GRANTED_NOTIFICATION_REJECTED:
+      case Status.CAMERA_ROLL_REJECTED_NOTIFICATION_GRANTED:
         return PhoneHubPermissionsSetupFlowScreens.CONNECTED;
       case Status.TIMED_OUT_CONNECTING:
         return PhoneHubPermissionsSetupFlowScreens.CONNECTION_TIME_OUT;
@@ -666,6 +724,8 @@
       case Status.COMPLETED_SUCCESSFULLY:
       case Status.COMPLETED_USER_REJECTED:
       case Status.FAILED_OR_CANCELLED:
+      case Status.CAMERA_ROLL_GRANTED_NOTIFICATION_REJECTED:
+      case Status.CAMERA_ROLL_REJECTED_NOTIFICATION_GRANTED:
         return this.getSetupCompleteTitle_();
       case Status.TIMED_OUT_CONNECTING:
         return this.i18n(
@@ -699,6 +759,8 @@
       case Status.COMPLETED_SUCCESSFULLY:
       case Status.COMPLETED_USER_REJECTED:
       case Status.FAILED_OR_CANCELLED:
+      case Status.CAMERA_ROLL_GRANTED_NOTIFICATION_REJECTED:
+      case Status.CAMERA_ROLL_REJECTED_NOTIFICATION_GRANTED:
         return (this.setupMode_ === this.completedMode_) ?
             '' :
             this.i18n(
@@ -740,7 +802,11 @@
         this.setupState_ !== PermissionsSetupStatus.COMPLETED_USER_REJECTED &&
         this.setupState_ !== PermissionsSetupStatus.FAILED_OR_CANCELLED &&
         this.setupState_ !==
-        PermissionsSetupStatus.NOTIFICATION_ACCESS_PROHIBITED;
+        PermissionsSetupStatus.NOTIFICATION_ACCESS_PROHIBITED &&
+        this.setupState_ !==
+        PermissionsSetupStatus.CAMERA_ROLL_GRANTED_NOTIFICATION_REJECTED &&
+        this.setupState_ !==
+        PermissionsSetupStatus.CAMERA_ROLL_REJECTED_NOTIFICATION_GRANTED;
   }
 
   /**
diff --git a/chrome/browser/resources/settings/chromeos/os_about_page/about_page_browser_proxy.js b/chrome/browser/resources/settings/chromeos/os_about_page/about_page_browser_proxy.js
index 88112f3..ab0498ed 100644
--- a/chrome/browser/resources/settings/chromeos/os_about_page/about_page_browser_proxy.js
+++ b/chrome/browser/resources/settings/chromeos/os_about_page/about_page_browser_proxy.js
@@ -86,6 +86,7 @@
   DISABLED: 'disabled',
   DISABLED_BY_ADMIN: 'disabled_by_admin',
   NEED_PERMISSION_TO_UPDATE: 'need_permission_to_update',
+  DEFERRED: 'deferred',
 };
 
 /**
@@ -146,6 +147,11 @@
 /** @interface */
 export class AboutPageBrowserProxy {
   /**
+   * Applies deferred update if it exists.
+   */
+  applyDeferredUpdate() {}
+
+  /**
    * Indicates to the browser that the page is ready.
    */
   pageReady() {}
@@ -270,6 +276,11 @@
   }
 
   /** @override */
+  applyDeferredUpdate() {
+    chrome.send('applyDeferredUpdate');
+  }
+
+  /** @override */
   pageReady() {
     chrome.send('aboutPageReady');
   }
diff --git a/chrome/browser/resources/settings/chromeos/os_about_page/os_about_page.html b/chrome/browser/resources/settings/chromeos/os_about_page/os_about_page.html
index d929683..f84711d 100644
--- a/chrome/browser/resources/settings/chromeos/os_about_page/os_about_page.html
+++ b/chrome/browser/resources/settings/chromeos/os_about_page/os_about_page.html
@@ -65,6 +65,11 @@
     margin-inline-end: -4px;
     margin-inline-start: 12px;
   }
+
+  #deferredUpdateButtons {
+    min-height: unset;
+    padding-bottom: 10px;
+  }
 </style>
 <iron-media-query query="(prefers-color-scheme: dark)"
     query-matches="{{isDarkModeActive_}}">
@@ -128,6 +133,18 @@
           </cr-button>
         </span>
       </div>
+      <div id="deferredUpdateButtons" class="settings-box first"
+          hidden="[[!hasDeferredUpdate_]]">
+        <div class="icon-container"></div>
+        <cr-button id="applyDeferredUpdate"
+            on-click="onApplyDeferredUpdateClick_">
+          $i18n{aboutRelaunch}
+        </cr-button>
+        <cr-button id="applyAndSetAutoUpdate"
+            on-click="onApplyAndSetAutoUpdateClick_">
+          $i18n{aboutRelaunchAndAutoUpdate}
+        </cr-button>
+      </div>
       <cr-link-row
           id="aboutTPMFirmwareUpdate"
           class="hr"
diff --git a/chrome/browser/resources/settings/chromeos/os_about_page/os_about_page.js b/chrome/browser/resources/settings/chromeos/os_about_page/os_about_page.js
index 95e9936d..3ec8ebe 100644
--- a/chrome/browser/resources/settings/chromeos/os_about_page/os_about_page.js
+++ b/chrome/browser/resources/settings/chromeos/os_about_page/os_about_page.js
@@ -147,6 +147,12 @@
       },
 
       /** @private */
+      hasDeferredUpdate_: {
+        type: Boolean,
+        value: false,
+      },
+
+      /** @private */
       eolMessageWithMonthAndYear_: {
         type: String,
         value: '',
@@ -376,6 +382,7 @@
       this.showUpdateWarningDialog_ = true;
       this.updateInfo_ = {version: event.version, size: event.size};
     }
+    this.hasDeferredUpdate_ = (event.status === UpdateStatus.DEFERRED);
     this.currentUpdateStatusEvent_ = event;
   }
 
@@ -541,6 +548,8 @@
         return this.i18nAdvanced('aboutUpgradeDownloadError');
       case UpdateStatus.DISABLED_BY_ADMIN:
         return this.i18nAdvanced('aboutUpgradeAdministrator');
+      case UpdateStatus.DEFERRED:
+        return this.i18nAdvanced('aboutUpgradeRelaunch');
       default:
         function formatMessage(msg) {
           return parseHtmlSubset('<b>' + msg + '</b>', ['br', 'pre'])
@@ -579,6 +588,7 @@
         return 'cr:error-outline';
       case UpdateStatus.UPDATED:
       case UpdateStatus.NEARLY_UPDATED:
+      case UpdateStatus.DEFERRED:
         // TODO(crbug.com/986596): Don't use browser icons here. Fork them.
         return 'settings:check-circle';
       default:
@@ -671,6 +681,18 @@
     this.$.updateStatusMessageInner.focus();
   }
 
+  /** @private */
+  onApplyDeferredUpdateClick_() {
+    this.aboutBrowserProxy_.applyDeferredUpdate();
+    this.$.updateStatusMessageInner.focus();
+  }
+
+  /** @private */
+  onApplyAndSetAutoUpdateClick_() {
+    this.aboutBrowserProxy_.setConsumerAutoUpdate(true);
+    this.onApplyDeferredUpdateClick_();
+  }
+
   /**
    * @return {boolean}
    * @private
diff --git a/chrome/browser/resources/settings/chromeos/os_settings.gni b/chrome/browser/resources/settings/chromeos/os_settings.gni
index 3534eff..3f087da 100644
--- a/chrome/browser/resources/settings/chromeos/os_settings.gni
+++ b/chrome/browser/resources/settings/chromeos/os_settings.gni
@@ -5,6 +5,31 @@
 import("//third_party/closure_compiler/compile_js.gni")
 import("../settings.gni")
 
+ts_web_component_files = [
+  "chromeos/os_icons.js",
+  "chromeos/os_settings_icons_css.js",
+]
+
+ts_non_web_component_files = [
+  "chromeos/combined_search_handler.js",
+  "chromeos/deep_linking_behavior.js",
+  "chromeos/ensure_lazy_loaded.js",
+  "chromeos/global_scroll_target_behavior.js",
+  "chromeos/lazy_load.js",
+  "chromeos/metrics_recorder.js",
+  "chromeos/os_page_visibility.js",
+  "chromeos/os_route.js",
+  "chromeos/os_settings.js",
+  "chromeos/os_settings_routes.js",
+  "chromeos/personalization_search_handler.js",
+  "chromeos/pref_to_setting_metric_converter.js",
+  "chromeos/prefs_behavior.js",
+  "chromeos/route_observer_behavior.js",
+  "chromeos/route_origin_behavior.js",
+  "chromeos/settings_search_handler.js",
+  "router.js",
+]
+
 # TODO(crbug.com/1121865): browser_resolver_prefix_replacements allows path
 # from ../../shared/* to resolve to ../../../nearby_share/shared/* for closure
 # purposes.
diff --git a/chrome/browser/resources/settings/chromeos/tsconfig_base.json b/chrome/browser/resources/settings/chromeos/tsconfig_base.json
new file mode 100644
index 0000000..3f69ccee
--- /dev/null
+++ b/chrome/browser/resources/settings/chromeos/tsconfig_base.json
@@ -0,0 +1,9 @@
+{
+  "extends": "../../../../../tools/typescript/tsconfig_base.json",
+  "compilerOptions": {
+    "allowJs": true,
+    "noUncheckedIndexedAccess": false,
+    "noUnusedLocals": false,
+    "strictPropertyInitialization": false
+  }
+}
diff --git a/chrome/browser/resources/settings/languages_page/languages_page.html b/chrome/browser/resources/settings/languages_page/languages_page.html
index 574c53b..d824efb9 100644
--- a/chrome/browser/resources/settings/languages_page/languages_page.html
+++ b/chrome/browser/resources/settings/languages_page/languages_page.html
@@ -64,7 +64,7 @@
     hidden="[[isHelpTextHidden_(languages.enabled.*)]]">
     <div class="cr-padded-text">
     <span>
-      $i18n{orderBrowserLanguagesInstructions}
+      $i18n{preferredLanguagesDesc}
     </span>
     </div>
   </div>
@@ -269,10 +269,8 @@
       body="[[i18n('languageManagedDialogBody')]]">
   </managed-dialog>
 </template>
-<if expr="not chromeos_ash">
-  <template is="dom-if" if="[[shouldShowRelaunchDialog]]" restamp>
-    <relaunch-confirmation-dialog restart-type="[[restartTypeEnum.RESTART]]"
-        on-close="onRelaunchDialogClose">
-    </relaunch-confirmation-dialog>
-  </template>
-</if>
+<template is="dom-if" if="[[shouldShowRelaunchDialog]]" restamp>
+  <relaunch-confirmation-dialog restart-type="[[restartTypeEnum.RESTART]]"
+      on-close="onRelaunchDialogClose">
+  </relaunch-confirmation-dialog>
+</template>
diff --git a/chrome/browser/resources/settings/languages_page/languages_page.ts b/chrome/browser/resources/settings/languages_page/languages_page.ts
index 0624a37..f8f6188 100644
--- a/chrome/browser/resources/settings/languages_page/languages_page.ts
+++ b/chrome/browser/resources/settings/languages_page/languages_page.ts
@@ -24,9 +24,7 @@
  import './languages.js';
  import '../controls/settings_toggle_button.js';
  import '../icons.html.js';
- // <if expr="not chromeos_ash">
  import '../relaunch_confirmation_dialog.js';
- // </if>
  import '../settings_shared.css.js';
  import '../settings_vars.css.js';
 
diff --git a/chrome/browser/resources/settings/system_page/system_page.html b/chrome/browser/resources/settings/system_page/system_page.html
index ae67135..392f055b 100644
--- a/chrome/browser/resources/settings/system_page/system_page.html
+++ b/chrome/browser/resources/settings/system_page/system_page.html
@@ -6,7 +6,7 @@
     </settings-toggle-button>
     <div class="hr"></div>
 </if>
-<if expr="not chromeos_ash and not chromeos_lacros">
+<if expr="not chromeos_lacros">
     <settings-toggle-button id="hardwareAcceleration"
         pref="{{prefs.hardware_acceleration_mode.enabled}}"
         label="$i18n{hardwareAccelerationLabel}">
@@ -51,12 +51,10 @@
       </div>
     </template>
 </if>
-    <if expr="not chromeos_ash">
-      <template is="dom-if" if="[[shouldShowRelaunchDialog]]" restamp>
-        <relaunch-confirmation-dialog restart-type="[[restartTypeEnum.RESTART]]"
-            on-close="onRelaunchDialogClose"></relaunch-confirmation-dialog>
-      </template>
-    </if>
+    <template is="dom-if" if="[[shouldShowRelaunchDialog]]" restamp>
+      <relaunch-confirmation-dialog restart-type="[[restartTypeEnum.RESTART]]"
+          on-close="onRelaunchDialogClose"></relaunch-confirmation-dialog>
+    </template>
 <if expr="chromeos_lacros">
     <template is="dom-if" if="[[isSecondaryUser_]]">
       <settings-toggle-button id="useAshProxy"
diff --git a/chrome/browser/resources/settings/system_page/system_page.ts b/chrome/browser/resources/settings/system_page/system_page.ts
index 5c1ddc2..21138b2 100644
--- a/chrome/browser/resources/settings/system_page/system_page.ts
+++ b/chrome/browser/resources/settings/system_page/system_page.ts
@@ -14,9 +14,7 @@
 import '../controls/extension_controlled_indicator.js';
 import '../controls/settings_toggle_button.js';
 import '../prefs/prefs.js';
-// <if expr="not chromeos_ash">
 import '../relaunch_confirmation_dialog.js';
-// </if>
 import '../settings_shared.css.js';
 
 import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
diff --git a/chrome/browser/safe_browsing/safe_browsing_blocking_page_test.cc b/chrome/browser/safe_browsing/safe_browsing_blocking_page_test.cc
index c4c26eed..849cea05 100644
--- a/chrome/browser/safe_browsing/safe_browsing_blocking_page_test.cc
+++ b/chrome/browser/safe_browsing/safe_browsing_blocking_page_test.cc
@@ -145,6 +145,7 @@
 const char kHTTPSPage[] = "/ssl/google.html";
 const char kMaliciousPage[] = "/safe_browsing/malware.html";
 const char kCrossSiteMaliciousPage[] = "/safe_browsing/malware2.html";
+const char kCrossSiteMaliciousEmbedPage[] = "/safe_browsing/malware4.html";
 const char kPageWithCrossOriginMaliciousIframe[] =
     "/safe_browsing/malware3.html";
 const char kCrossOriginMaliciousIframeHost[] = "malware.test";
@@ -535,6 +536,16 @@
         ->AddDangerousUrl(url, threat_type);
   }
 
+  void SetURLThreatPatternType(const GURL& url,
+                               ThreatPatternType threat_pattern_type) {
+    TestSafeBrowsingService* service = factory_.test_safe_browsing_service();
+    ASSERT_TRUE(service);
+
+    static_cast<FakeSafeBrowsingDatabaseManager*>(
+        service->database_manager().get())
+        ->AddDangerousUrlPattern(url, threat_pattern_type);
+  }
+
   void ClearBadURL(const GURL& url) {
     TestSafeBrowsingService* service = factory_.test_safe_browsing_service();
     ASSERT_TRUE(service);
@@ -1870,6 +1881,49 @@
   EXPECT_EQ(bad_url, contents->GetLastCommittedURL());
 }
 
+// Regression test for https://crbug.com/1333623.
+IN_PROC_BROWSER_TEST_P(SafeBrowsingBlockingPageBrowserTest,
+                       EmbedElementMalwareLandingInterstitial) {
+  GURL url = embedded_test_server()->GetURL(kCrossSiteMaliciousEmbedPage);
+  GURL embed_url = embedded_test_server()->GetURL(kMaliciousIframe);
+  SetURLThreatType(embed_url, SB_THREAT_TYPE_URL_MALWARE);
+  SetURLThreatPatternType(embed_url, ThreatPatternType::MALWARE_LANDING);
+
+  EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
+  base::RunLoop().RunUntilIdle();
+  WebContents* contents = browser()->tab_strip_model()->GetActiveWebContents();
+  EXPECT_TRUE(
+      content::WaitForRenderFrameReady(contents->GetPrimaryMainFrame()));
+  // Show an interstitial when the malware landing page is loaded as an <embed>
+  // element, because it can cause similar harm as <iframe>.
+  EXPECT_TRUE(IsShowingInterstitial(contents));
+}
+
+IN_PROC_BROWSER_TEST_P(SafeBrowsingBlockingPageBrowserTest,
+                       JsElementInterstitial) {
+  SBThreatType threat_type = GetThreatType();
+  GURL url = embedded_test_server()->GetURL(kMaliciousJsPage);
+  GURL js_url = embedded_test_server()->GetURL(kMaliciousJs);
+  SetURLThreatType(js_url, threat_type);
+  if (threat_type == SB_THREAT_TYPE_URL_MALWARE) {
+    SetURLThreatPatternType(js_url, ThreatPatternType::MALWARE_LANDING);
+  }
+
+  EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
+  base::RunLoop().RunUntilIdle();
+  WebContents* contents = browser()->tab_strip_model()->GetActiveWebContents();
+  EXPECT_TRUE(
+      content::WaitForRenderFrameReady(contents->GetPrimaryMainFrame()));
+  if (threat_type == SB_THREAT_TYPE_URL_MALWARE ||
+      threat_type == SB_THREAT_TYPE_URL_UNWANTED) {
+    // Do not show an interstitial when the malware landing page or UwS landing
+    // page is loaded as a subresource to avoid false positives.
+    EXPECT_FALSE(IsShowingInterstitial(contents));
+  } else {
+    EXPECT_TRUE(IsShowingInterstitial(contents));
+  }
+}
+
 class SafeBrowsingBlockingPageDelayedWarningBrowserTest
     : public InProcessBrowserTest,
       public testing::WithParamInterface<
diff --git a/chrome/browser/safe_browsing/tailored_security/chrome_tailored_security_service.cc b/chrome/browser/safe_browsing/tailored_security/chrome_tailored_security_service.cc
index 59e8a9f..80504af 100644
--- a/chrome/browser/safe_browsing/tailored_security/chrome_tailored_security_service.cc
+++ b/chrome/browser/safe_browsing/tailored_security/chrome_tailored_security_service.cc
@@ -56,7 +56,7 @@
 void RecordEnabledNotificationResult(
     TailoredSecurityNotificationResult result) {
   base::UmaHistogramEnumeration(
-      "SafeBrowsing.TailoredSecurity.SyncPromptEnabledNotificationResult",
+      "SafeBrowsing.TailoredSecurity.SyncPromptEnabledNotificationResult2",
       result);
 }
 
@@ -132,15 +132,19 @@
   if (base::FeatureList::IsEnabled(kTailoredSecurityDesktopNotice)) {
     Browser* browser = chrome::FindBrowserWithProfile(profile_);
     if (!browser) {
-      RecordEnabledNotificationResult(
-          TailoredSecurityNotificationResult::kNoBrowserAvailable);
+      if (is_enabled) {
+        RecordEnabledNotificationResult(
+            TailoredSecurityNotificationResult::kNoBrowserAvailable);
+      }
       return;
     }
     content::WebContents* web_contents =
         browser->tab_strip_model()->GetActiveWebContents();
     if (!web_contents) {
-      RecordEnabledNotificationResult(
-          TailoredSecurityNotificationResult::kNoWebContentsAvailable);
+      if (is_enabled) {
+        RecordEnabledNotificationResult(
+            TailoredSecurityNotificationResult::kNoWebContentsAvailable);
+      }
       return;
     }
     SetSafeBrowsingState(profile_->GetPrefs(),
diff --git a/chrome/browser/segmentation_platform/service_browsertest.cc b/chrome/browser/segmentation_platform/service_browsertest.cc
index 88be3ceb..e9b222101 100644
--- a/chrome/browser/segmentation_platform/service_browsertest.cc
+++ b/chrome/browser/segmentation_platform/service_browsertest.cc
@@ -60,10 +60,9 @@
   }
 
   bool HasResultPref(base::StringPiece segmentation_key) {
-    const base::Value* dictionary =
-        browser()->profile()->GetPrefs()->GetDictionary(
-            kSegmentationResultPref);
-    return !!dictionary->FindPath(segmentation_key);
+    const base::Value::Dict& dictionary =
+        browser()->profile()->GetPrefs()->GetValueDict(kSegmentationResultPref);
+    return !!dictionary.FindByDottedPath(segmentation_key);
   }
 
   void OnResultPrefUpdated() {
diff --git a/chrome/browser/ssl/sct_reporting_service_browsertest.cc b/chrome/browser/ssl/sct_reporting_service_browsertest.cc
index ad86cc7..b59ee6d 100644
--- a/chrome/browser/ssl/sct_reporting_service_browsertest.cc
+++ b/chrome/browser/ssl/sct_reporting_service_browsertest.cc
@@ -331,15 +331,17 @@
     auto http_response =
         std::make_unique<net::test_server::BasicHttpResponse>();
 
-    if (error_count_ > 0) {
-      http_response->set_code(net::HTTP_TOO_MANY_REQUESTS);
-      --error_count_;
-    } else {
-      http_response->set_code(net::HTTP_OK);
-    }
-
     if (request.relative_url.find("hashdance") == std::string::npos) {
       // Request is a report.
+
+      // Check if the server should just return an error for the full report
+      // request, otherwise just return OK.
+      if (error_count_ > 0) {
+        http_response->set_code(net::HTTP_TOO_MANY_REQUESTS);
+        --error_count_;
+      } else {
+        http_response->set_code(net::HTTP_OK);
+      }
       return http_response;
     }
 
@@ -422,7 +424,8 @@
 
   base::OnceClosure requests_closure_;
 
-  // How many times the report server should return an error before succeeding.
+  // How many times the report server should return an error before succeeding,
+  // specific to full report requests.
   size_t error_count_ = 0;
 };
 
@@ -940,6 +943,34 @@
   EXPECT_EQ(report_count, 1);
 }
 
+// Test that report count isn't incremented when retrying a single audit report.
+// Regression test for crbug.com/1348313.
+IN_PROC_BROWSER_TEST_F(SCTHashdanceBrowserTest,
+                       HashdanceReportCountNotIncrementedOnRetry) {
+  // Don't succeed for max_retries+1, for the *full report sending*, but the
+  // hashdance lookup query will always succeed.
+  set_error_count(16);
+
+  // Visit an HTTPS page and wait for the report to be sent.
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(
+      browser(), https_server()->GetURL("hashdance.test", "/")));
+
+  // Wait until the reporter completes 32 requests (16 lookup queries which
+  // succeed, and 16 full report requests which fail).
+  WaitForRequests(32);
+
+  // Check that 32 requests were seen and contains the expected details.
+  EXPECT_EQ(32u, requests_seen());
+  EXPECT_EQ(
+      "hashdance.test",
+      GetLastSeenReport().certificate_report(0).context().origin().hostname());
+
+  // Check that the report was only counted once towards the max-reports limit.
+  int report_count = g_browser_process->local_state()->GetInteger(
+      prefs::kSCTAuditingHashdanceReportCount);
+  EXPECT_EQ(report_count, 1);
+}
+
 IN_PROC_BROWSER_TEST_F(SCTHashdanceBrowserTest, HashdanceReportLimitReached) {
   // Override the report count to be the maximum.
   g_browser_process->local_state()->SetInteger(
diff --git a/chrome/browser/ui/app_list/search/search_controller_factory.cc b/chrome/browser/ui/app_list/search/search_controller_factory.cc
index 8d77488b..4331ed8 100644
--- a/chrome/browser/ui/app_list/search/search_controller_factory.cc
+++ b/chrome/browser/ui/app_list/search/search_controller_factory.cc
@@ -157,7 +157,7 @@
   //   true (the default).
   if (!ash::features::IsProductivityLauncherEnabled() ||
       base::GetFieldTrialParamByFeatureAsBool(
-          ash::features::kProductivityLauncher, "enable_continue", true)) {
+          ash::features::kProductivityLauncher, "enable_continue", false)) {
     size_t zero_state_files_group_id =
         controller->AddGroup(kMaxZeroStateFileResults);
     controller->AddProvider(zero_state_files_group_id,
diff --git a/chrome/browser/ui/ash/projector/projector_app_client_impl.cc b/chrome/browser/ui/ash/projector/projector_app_client_impl.cc
index 36739a8..7b0fa72 100644
--- a/chrome/browser/ui/ash/projector/projector_app_client_impl.cc
+++ b/chrome/browser/ui/ash/projector/projector_app_client_impl.cc
@@ -8,7 +8,9 @@
 
 #include "ash/constants/ash_features.h"
 #include "ash/constants/ash_pref_names.h"
+#include "ash/public/cpp/projector/annotator_tool.h"
 #include "ash/public/cpp/projector/projector_controller.h"
+#include "ash/webui/projector_app/annotator_message_handler.h"
 #include "ash/webui/projector_app/projector_screencast.h"
 #include "ash/webui/projector_app/public/cpp/projector_app_constants.h"
 #include "base/bind.h"
@@ -166,3 +168,25 @@
     ash::ProjectorAppClient::OnGetScreencastCallback callback) {
   screencast_manager_.GetScreencast(screencast_id, std::move(callback));
 }
+
+void ProjectorAppClientImpl::SetAnnotatorMessageHandler(
+    ash::AnnotatorMessageHandler* handler) {
+  annotator_message_handler_ = handler;
+}
+
+void ProjectorAppClientImpl::ResetAnnotatorMessageHandler(
+    ash::AnnotatorMessageHandler* handler) {
+  if (annotator_message_handler_ == handler) {
+    annotator_message_handler_ = nullptr;
+  }
+}
+
+void ProjectorAppClientImpl::SetTool(const ash::AnnotatorTool& tool) {
+  DCHECK(annotator_message_handler_);
+  annotator_message_handler_->SetTool(tool);
+}
+
+void ProjectorAppClientImpl::Clear() {
+  DCHECK(annotator_message_handler_);
+  annotator_message_handler_->Clear();
+}
diff --git a/chrome/browser/ui/ash/projector/projector_app_client_impl.h b/chrome/browser/ui/ash/projector/projector_app_client_impl.h
index 857e1a32..30f8df55 100644
--- a/chrome/browser/ui/ash/projector/projector_app_client_impl.h
+++ b/chrome/browser/ui/ash/projector/projector_app_client_impl.h
@@ -7,6 +7,8 @@
 
 #include <memory>
 
+#include "ash/public/cpp/projector/projector_annotator_controller.h"
+#include "ash/webui/projector_app/annotator_message_handler.h"
 #include "ash/webui/projector_app/projector_app_client.h"
 #include "base/observer_list.h"
 #include "chrome/browser/ui/ash/projector/pending_screencast_manager.h"
@@ -23,6 +25,10 @@
 class PrefRegistrySyncable;
 }  // namespace user_prefs
 
+namespace ash {
+class AnnotatorMessageHandler;
+}  // namespace ash
+
 // Implements the interface for Projector App.
 class ProjectorAppClientImpl : public ash::ProjectorAppClient {
  public:
@@ -50,6 +56,16 @@
   void GetScreencast(
       const std::string& screencast_id,
       ash::ProjectorAppClient::OnGetScreencastCallback callback) override;
+  void SetAnnotatorMessageHandler(
+      ash::AnnotatorMessageHandler* handler) override;
+  void ResetAnnotatorMessageHandler(
+      ash::AnnotatorMessageHandler* handler) override;
+  void SetTool(const ash::AnnotatorTool& tool) override;
+  void Clear() override;
+
+  ash::AnnotatorMessageHandler* get_annotator_message_handler_for_test() {
+    return annotator_message_handler_;
+  }
 
  private:
   void NotifyScreencastsPendingStatusChanged(
@@ -64,6 +80,8 @@
   PendingScreencastManager pending_screencast_manager_;
 
   ash::ScreencastManager screencast_manager_;
+
+  ash::AnnotatorMessageHandler* annotator_message_handler_ = nullptr;
 };
 
 #endif  // CHROME_BROWSER_UI_ASH_PROJECTOR_PROJECTOR_APP_CLIENT_IMPL_H_
diff --git a/chrome/browser/ui/ash/projector/projector_client_impl.cc b/chrome/browser/ui/ash/projector/projector_client_impl.cc
index 7414e7c..b74c8df4 100644
--- a/chrome/browser/ui/ash/projector/projector_client_impl.cc
+++ b/chrome/browser/ui/ash/projector/projector_client_impl.cc
@@ -9,7 +9,6 @@
 #include "ash/public/cpp/projector/annotator_tool.h"
 #include "ash/public/cpp/projector/projector_controller.h"
 #include "ash/public/cpp/projector/projector_new_screencast_precondition.h"
-#include "ash/webui/projector_app/annotator_message_handler.h"
 #include "ash/webui/projector_app/projector_app_client.h"
 #include "ash/webui/projector_app/public/cpp/projector_app_constants.h"
 #include "base/bind.h"
@@ -161,18 +160,6 @@
     app_client->OnNewScreencastPreconditionChanged(precondition);
 }
 
-void ProjectorClientImpl::SetAnnotatorMessageHandler(
-    ash::AnnotatorMessageHandler* handler) {
-  message_handler_ = handler;
-}
-
-void ProjectorClientImpl::ResetAnnotatorMessageHandler(
-    ash::AnnotatorMessageHandler* handler) {
-  if (message_handler_ == handler) {
-    message_handler_ = nullptr;
-  }
-}
-
 void ProjectorClientImpl::OnSpeechResult(
     const std::u16string& text,
     bool is_final,
@@ -205,8 +192,7 @@
 }
 
 void ProjectorClientImpl::SetTool(const ash::AnnotatorTool& tool) {
-  DCHECK(message_handler_);
-  message_handler_->SetTool(tool);
+  ash::ProjectorAppClient::Get()->SetTool(tool);
 }
 
 // TODO(b/220202359): Implement undo.
@@ -216,8 +202,7 @@
 void ProjectorClientImpl::Redo() {}
 
 void ProjectorClientImpl::Clear() {
-  DCHECK(message_handler_);
-  message_handler_->Clear();
+  ash::ProjectorAppClient::Get()->Clear();
 }
 
 void ProjectorClientImpl::OnFileSystemMounted() {
diff --git a/chrome/browser/ui/ash/projector/projector_client_impl.h b/chrome/browser/ui/ash/projector/projector_client_impl.h
index c5271ee..90da7a4 100644
--- a/chrome/browser/ui/ash/projector/projector_client_impl.h
+++ b/chrome/browser/ui/ash/projector/projector_client_impl.h
@@ -10,7 +10,6 @@
 #include "ash/public/cpp/projector/projector_annotator_controller.h"
 #include "ash/public/cpp/projector/projector_client.h"
 #include "ash/public/cpp/projector/projector_controller.h"
-#include "ash/webui/projector_app/annotator_message_handler.h"
 #include "base/memory/weak_ptr.h"
 #include "base/scoped_observation.h"
 #include "chrome/browser/ash/drive/drive_integration_service.h"
@@ -60,10 +59,6 @@
   void CloseProjectorApp() const override;
   void OnNewScreencastPreconditionChanged(
       const ash::NewScreencastPrecondition& precondition) const override;
-  void SetAnnotatorMessageHandler(
-      ash::AnnotatorMessageHandler* handler) override;
-  void ResetAnnotatorMessageHandler(
-      ash::AnnotatorMessageHandler* handler) override;
 
   // SpeechRecognizerDelegate:
   void OnSpeechResult(
@@ -94,10 +89,6 @@
   // user_manager::UserManager::UserSessionStateObserver:
   void ActiveUserChanged(user_manager::User* active_user) override;
 
-  ash::AnnotatorMessageHandler* get_annotator_message_handler_for_test() {
-    return message_handler_;
-  }
-
  private:
   // Maybe reset |drive_observation_| and observe the Drive integration service
   // of active profile when ActiveUserChanged and OnUserProfileLoaded.
@@ -111,7 +102,6 @@
   void SetAppIsDisabled(bool disabled);
 
   ash::ProjectorController* const controller_;
-  ash::AnnotatorMessageHandler* message_handler_ = nullptr;
   SpeechRecognizerStatus recognizer_status_ =
       SpeechRecognizerStatus::SPEECH_RECOGNIZER_OFF;
   std::unique_ptr<OnDeviceSpeechRecognizer> speech_recognizer_;
diff --git a/chrome/browser/ui/browser.cc b/chrome/browser/ui/browser.cc
index b3ccd623..ecf7d1e 100644
--- a/chrome/browser/ui/browser.cc
+++ b/chrome/browser/ui/browser.cc
@@ -3172,4 +3172,11 @@
   }
   screen_ai_annotator_->Run();
 }
+
+void Browser::SetScreenAIAnnotatorForTesting(
+    std::unique_ptr<screen_ai::AXScreenAIAnnotator> annotator) {
+  DCHECK(!screen_ai_annotator_);
+  screen_ai_annotator_.swap(annotator);
+}
+
 #endif
diff --git a/chrome/browser/ui/browser.h b/chrome/browser/ui/browser.h
index dda5c5a..b0f70cfa 100644
--- a/chrome/browser/ui/browser.h
+++ b/chrome/browser/ui/browser.h
@@ -759,6 +759,10 @@
 
 #if BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
   void RunScreenAIAnnotator();
+
+  // Ownership will be transferred to browser.
+  void SetScreenAIAnnotatorForTesting(
+      std::unique_ptr<screen_ai::AXScreenAIAnnotator> annotator);
 #endif
 
  private:
diff --git a/chrome/browser/ui/browser_commands_mac.mm b/chrome/browser/ui/browser_commands_mac.mm
index 9d75502c..a215b3bb 100644
--- a/chrome/browser/ui/browser_commands_mac.mm
+++ b/chrome/browser/ui/browser_commands_mac.mm
@@ -13,6 +13,7 @@
 #include "chrome/browser/ui/browser.h"
 #include "chrome/browser/ui/browser_commands.h"
 #include "chrome/browser/ui/exclusive_access/fullscreen_controller.h"
+#include "chrome/browser/ui/web_applications/app_browser_controller.h"
 #include "chrome/common/pref_names.h"
 #include "components/prefs/pref_service.h"
 
@@ -21,7 +22,14 @@
 void ToggleFullscreenToolbar(Browser* browser) {
   DCHECK(browser);
 
-  // Toggle the value of the preference.
+  // If this browser belongs to an app, toggle the value for that app.
+  web_app::AppBrowserController* app_controller = browser->app_controller();
+  if (app_controller) {
+    app_controller->ToggleAlwaysShowToolbarInFullscreen();
+    return;
+  }
+
+  // Otherwise toggle the value of the preference.
   PrefService* prefs = browser->profile()->GetPrefs();
   bool show_toolbar = prefs->GetBoolean(prefs::kShowFullscreenToolbar);
   prefs->SetBoolean(prefs::kShowFullscreenToolbar, !show_toolbar);
diff --git a/chrome/browser/ui/quick_answers/quick_answers_ui_controller.cc b/chrome/browser/ui/quick_answers/quick_answers_ui_controller.cc
index bae349ef..1fb818f 100644
--- a/chrome/browser/ui/quick_answers/quick_answers_ui_controller.cc
+++ b/chrome/browser/ui/quick_answers/quick_answers_ui_controller.cc
@@ -9,6 +9,7 @@
 #include "base/strings/stringprintf.h"
 #include "build/chromeos_buildflags.h"
 #include "chrome/browser/ui/quick_answers/quick_answers_controller_impl.h"
+#include "chromeos/components/quick_answers/public/cpp/quick_answers_state.h"
 #include "chromeos/components/quick_answers/quick_answers_model.h"
 #include "chromeos/strings/grit/chromeos_strings.h"
 #include "mojo/public/cpp/bindings/remote.h"
@@ -38,8 +39,11 @@
 using quick_answers::QuickAnswersExitPoint;
 
 constexpr char kGoogleSearchUrlPrefix[] = "https://www.google.com/search?q=";
+constexpr char kGoogleTranslateUrlTemplate[] =
+    "https://translate.google.com/?sl=auto&tl=%s&text=%s&op=translate";
 
 constexpr char kFeedbackDescriptionTemplate[] = "#QuickAnswers\nQuery:%s\n";
+constexpr char kTranslationQueryPrefix[] = "Translate:";
 
 constexpr char kQuickAnswersSettingsUrl[] =
     "chrome://os-settings/osSearch/search";
@@ -97,8 +101,21 @@
   // Route dismissal through |controller_| for logging impressions.
   controller_->DismissQuickAnswers(QuickAnswersExitPoint::kQuickAnswersClick);
 
-  OpenUrl(GURL(kGoogleSearchUrlPrefix +
-               base::EscapeUrlEncodedData(query_, /*use_plus=*/true)));
+  // TODO(b/240619915): Refactor so that we can access the request metadata
+  // instead of just the query itself.
+  if (base::StartsWith(query_, kTranslationQueryPrefix)) {
+    auto query_text = base::EscapeUrlEncodedData(
+        query_.substr(strlen(kTranslationQueryPrefix)), /*use_plus=*/true);
+    auto device_language =
+        l10n_util::GetLanguage(QuickAnswersState::Get()->application_locale());
+    auto translate_url =
+        base::StringPrintf(kGoogleTranslateUrlTemplate, device_language.c_str(),
+                           query_text.c_str());
+    OpenUrl(GURL(translate_url));
+  } else {
+    OpenUrl(GURL(kGoogleSearchUrlPrefix +
+                 base::EscapeUrlEncodedData(query_, /*use_plus=*/true)));
+  }
   controller_->OnQuickAnswerClick();
 }
 
diff --git a/chrome/browser/ui/tabs/existing_tab_group_sub_menu_model_unittest.cc b/chrome/browser/ui/tabs/existing_tab_group_sub_menu_model_unittest.cc
index feff0e8..31cba4f4 100644
--- a/chrome/browser/ui/tabs/existing_tab_group_sub_menu_model_unittest.cc
+++ b/chrome/browser/ui/tabs/existing_tab_group_sub_menu_model_unittest.cc
@@ -223,6 +223,13 @@
   EXPECT_EQ(3u, menu_1.GetItemCount());
   EXPECT_EQ(3u, menu_2.GetItemCount());
 
+  EXPECT_FALSE(ExistingTabGroupSubMenuModel::ShouldShowSubmenu(
+      model_1, 0, delegate_1.get()));
+  EXPECT_TRUE(ExistingTabGroupSubMenuModel::ShouldShowSubmenu(
+      model_1, 1, delegate_1.get()));
+  EXPECT_TRUE(ExistingTabGroupSubMenuModel::ShouldShowSubmenu(
+      model_2, 0, delegate_1.get()));
+
   new_browser.get()->tab_strip_model()->CloseAllTabs();
   new_browser.reset();
 }
diff --git a/chrome/browser/ui/views/bubble/webui_bubble_manager.h b/chrome/browser/ui/views/bubble/webui_bubble_manager.h
index 5315003..4051e87 100644
--- a/chrome/browser/ui/views/bubble/webui_bubble_manager.h
+++ b/chrome/browser/ui/views/bubble/webui_bubble_manager.h
@@ -40,6 +40,9 @@
   bool bubble_using_cached_web_contents() const {
     return bubble_using_cached_web_contents_;
   }
+
+  // Creates the persistent renderer process if the feature is enabled.
+  virtual void MaybeInitPersistentRenderer() = 0;
   virtual base::WeakPtr<WebUIBubbleDialogView> CreateWebUIBubbleDialog(
       const absl::optional<gfx::Rect>& anchor) = 0;
 
@@ -100,7 +103,10 @@
       : anchor_view_(anchor_view),
         profile_(profile),
         webui_url_(webui_url),
-        task_manager_string_id_(task_manager_string_id) {
+        task_manager_string_id_(task_manager_string_id) {}
+  ~WebUIBubbleManagerT() override = default;
+
+  void MaybeInitPersistentRenderer() override {
     if (base::FeatureList::IsEnabled(
             features::kWebUIBubblePerProfilePersistence)) {
       auto* service =
@@ -111,7 +117,6 @@
       }
     }
   }
-  ~WebUIBubbleManagerT() override = default;
 
   base::WeakPtr<WebUIBubbleDialogView> CreateWebUIBubbleDialog(
       const absl::optional<gfx::Rect>& anchor) override {
@@ -128,6 +133,7 @@
 
       // If using per-profile WebContents persistence get the associated
       // BubbleContentsWrapper from the BubbleContentsWrapperService.
+      MaybeInitPersistentRenderer();
       contents_wrapper = service->GetBubbleContentsWrapperFromURL(webui_url_);
       DCHECK(contents_wrapper);
 
diff --git a/chrome/browser/ui/views/bubble/webui_bubble_manager_unittest.cc b/chrome/browser/ui/views/bubble/webui_bubble_manager_unittest.cc
index 6cd7d4c5..cb6c9dfc 100644
--- a/chrome/browser/ui/views/bubble/webui_bubble_manager_unittest.cc
+++ b/chrome/browser/ui/views/bubble/webui_bubble_manager_unittest.cc
@@ -13,6 +13,7 @@
 #include "chrome/test/base/testing_profile_manager.h"
 #include "chrome/test/views/chrome_views_test_base.h"
 #include "testing/gtest/include/gtest/gtest.h"
+#include "ui/views/test/widget_test.h"
 #include "ui/webui/mojo_bubble_web_ui_controller.h"
 
 namespace {
@@ -82,17 +83,17 @@
           anchor_widget->GetContentsView(), test_profile, GURL(kTestURL), 1);
   bubble_manager->DisableCloseBubbleHelperForTesting();
 
-  // If using per-profile persistence the `contents_wrapper` should have been
-  // created before the bubble has been invoked.
-  // Owned by |service|.
+  // The per-profile persistent renderer will not have been created until the
+  // first time the bubble is invoked.
   BubbleContentsWrapper* contents_wrapper =
       service->GetBubbleContentsWrapperFromURL(GURL(kTestURL));
-  EXPECT_NE(nullptr, contents_wrapper);
+  EXPECT_EQ(nullptr, contents_wrapper);
 
-  // Open the bubble, the `contents_wrapper` used should match the one returned
-  // from the BubbleContentsWrapperService.
+  // Open the bubble, this should create the persistent renderer-backed
+  // `contents_wrapper`.
   EXPECT_EQ(nullptr, bubble_manager->GetBubbleWidget());
   bubble_manager->ShowBubble();
+  contents_wrapper = service->GetBubbleContentsWrapperFromURL(GURL(kTestURL));
   EXPECT_NE(nullptr, bubble_manager->GetBubbleWidget());
   EXPECT_FALSE(bubble_manager->GetBubbleWidget()->IsClosed());
   EXPECT_EQ(contents_wrapper, bubble_manager->bubble_view_for_testing()
@@ -194,10 +195,37 @@
   BubbleContentsWrapper* contents_wrapper_profile2 =
       service2->GetBubbleContentsWrapperFromURL(GURL(kTestURL));
 
-  // content wrappers for the same WebUI URL should be different per profile.
-  ASSERT_NE(nullptr, contents_wrapper_profile1);
-  ASSERT_NE(nullptr, contents_wrapper_profile2);
-  ASSERT_NE(contents_wrapper_profile1, contents_wrapper_profile2);
+  // Content wrappers should be null until the first time the bubble is shown.
+  EXPECT_EQ(nullptr, contents_wrapper_profile1);
+  EXPECT_EQ(nullptr, contents_wrapper_profile2);
+
+  // Show bubbles for each manager one at a time, manager1 and manager2 should
+  // leverage the same contents wrapper. manager3 should be using a unique
+  // contents wrapper as it is backed by a different profile.
+  auto show_bubble = [](WebUIBubbleManager* manager,
+                        BubbleContentsWrapperService* service) {
+    // Open the bubble for the given bubble manager
+    EXPECT_EQ(nullptr, manager->GetBubbleWidget());
+
+    manager->ShowBubble();
+    auto* contents_wrapper =
+        service->GetBubbleContentsWrapperFromURL(GURL(kTestURL));
+    EXPECT_NE(nullptr, manager->GetBubbleWidget());
+    EXPECT_NE(nullptr, contents_wrapper);
+    EXPECT_EQ(
+        contents_wrapper,
+        manager->bubble_view_for_testing()->get_contents_wrapper_for_testing());
+
+    manager->CloseBubble();
+    EXPECT_TRUE(manager->GetBubbleWidget()->IsClosed());
+    views::test::WidgetDestroyedWaiter destroyed_waiter(
+        manager->GetBubbleWidget());
+    destroyed_waiter.Wait();
+    return contents_wrapper;
+  };
+  contents_wrapper_profile1 = show_bubble(manager1.get(), service1);
+  EXPECT_EQ(contents_wrapper_profile1, show_bubble(manager2.get(), service1));
+  contents_wrapper_profile2 = show_bubble(manager3.get(), service2);
 
   auto test_manager = [](WebUIBubbleManager* manager,
                          BubbleContentsWrapperService* service,
diff --git a/chrome/browser/ui/views/frame/browser_frame_mac.mm b/chrome/browser/ui/views/frame/browser_frame_mac.mm
index 73c652c5..953ca19 100644
--- a/chrome/browser/ui/views/frame/browser_frame_mac.mm
+++ b/chrome/browser/ui/views/frame/browser_frame_mac.mm
@@ -20,6 +20,7 @@
 #include "chrome/browser/ui/views/frame/browser_frame.h"
 #include "chrome/browser/ui/views/frame/browser_non_client_frame_view.h"
 #include "chrome/browser/ui/views/frame/browser_view.h"
+#include "chrome/browser/ui/web_applications/app_browser_controller.h"
 #include "chrome/common/pref_names.h"
 #include "chrome/grit/generated_resources.h"
 #include "components/bookmarks/common/bookmark_pref_names.h"
@@ -231,9 +232,15 @@
       break;
     }
     case IDC_TOGGLE_FULLSCREEN_TOOLBAR: {
-      PrefService* prefs = browser->profile()->GetPrefs();
-      result->new_toggle_state =
-          prefs->GetBoolean(prefs::kShowFullscreenToolbar);
+      web_app::AppBrowserController* app_controller = browser->app_controller();
+      if (app_controller) {
+        result->new_toggle_state =
+            app_controller->AlwaysShowToolbarInFullscreen();
+      } else {
+        PrefService* prefs = browser->profile()->GetPrefs();
+        result->new_toggle_state =
+            prefs->GetBoolean(prefs::kShowFullscreenToolbar);
+      }
       break;
     }
     case IDC_SHOW_FULL_URLS: {
diff --git a/chrome/browser/ui/views/frame/browser_non_client_frame_view_mac.h b/chrome/browser/ui/views/frame/browser_non_client_frame_view_mac.h
index 16685bc..a059b06 100644
--- a/chrome/browser/ui/views/frame/browser_non_client_frame_view_mac.h
+++ b/chrome/browser/ui/views/frame/browser_non_client_frame_view_mac.h
@@ -12,6 +12,8 @@
 #include "base/gtest_prod_util.h"
 #include "base/mac/scoped_nsobject.h"
 #include "chrome/browser/ui/views/frame/browser_non_client_frame_view.h"
+#include "chrome/browser/web_applications/app_registrar_observer.h"
+#include "chrome/browser/web_applications/web_app_registrar.h"
 #include "components/prefs/pref_member.h"
 
 namespace views {
@@ -23,7 +25,8 @@
 class CaptionButtonPlaceholderContainer;
 class WindowControlsOverlayInputRoutingMac;
 
-class BrowserNonClientFrameViewMac : public BrowserNonClientFrameView {
+class BrowserNonClientFrameViewMac : public BrowserNonClientFrameView,
+                                     public web_app::AppRegistrarObserver {
  public:
   // Mac implementation of BrowserNonClientFrameView.
   BrowserNonClientFrameViewMac(BrowserFrame* frame, BrowserView* browser_view);
@@ -64,6 +67,10 @@
   gfx::Size GetMinimumSize() const override;
   void AddedToWidget() override;
 
+  // web_app::AppRegistrarObserver
+  void OnAlwaysShowToolbarInFullscreenChanged(const web_app::AppId& app_id,
+                                              bool show) override;
+
  protected:
   // views::View:
   void OnPaint(gfx::Canvas* canvas) override;
@@ -112,8 +119,17 @@
   // toolbar style is changed.
   void ToggleWebAppFrameToolbarViewVisibility();
 
+  // Returns the current value of the "always show toolbar in fullscreen"
+  // preference, either reading the value from the kShowFullscreenToolbar
+  // preference or if this is a window for an app, from the settings for that
+  // app.
+  bool AlwaysShowToolbarInFullscreen() const;
+
   // Used to keep track of the update of kShowFullscreenToolbar preference.
   BooleanPrefMember show_fullscreen_toolbar_;
+  base::ScopedObservation<web_app::WebAppRegistrar,
+                          web_app::AppRegistrarObserver>
+      always_show_toolbar_in_fullscreen_observation_{this};
 
   raw_ptr<views::Label> window_title_ = nullptr;
 
diff --git a/chrome/browser/ui/views/frame/browser_non_client_frame_view_mac.mm b/chrome/browser/ui/views/frame/browser_non_client_frame_view_mac.mm
index ee9cbfa..025baa81 100644
--- a/chrome/browser/ui/views/frame/browser_non_client_frame_view_mac.mm
+++ b/chrome/browser/ui/views/frame/browser_non_client_frame_view_mac.mm
@@ -29,6 +29,7 @@
 #include "chrome/browser/ui/views/web_apps/frame_toolbar/web_app_frame_toolbar_utils.h"
 #include "chrome/browser/ui/views/web_apps/frame_toolbar/web_app_frame_toolbar_view.h"
 #include "chrome/browser/ui/web_applications/app_browser_controller.h"
+#include "chrome/browser/web_applications/web_app_provider.h"
 #include "chrome/common/chrome_features.h"
 #include "chrome/common/chrome_switches.h"
 #include "chrome/common/pref_names.h"
@@ -62,16 +63,24 @@
     BrowserFrame* frame,
     BrowserView* browser_view)
     : BrowserNonClientFrameView(frame, browser_view) {
-  show_fullscreen_toolbar_.Init(
-      prefs::kShowFullscreenToolbar, browser_view->GetProfile()->GetPrefs(),
-      base::BindRepeating(&BrowserNonClientFrameViewMac::UpdateFullscreenTopUI,
-                          base::Unretained(this)));
+  if (web_app::AppBrowserController::IsWebApp(browser_view->browser())) {
+    auto* provider =
+        web_app::WebAppProvider::GetForWebApps(browser_view->GetProfile());
+    always_show_toolbar_in_fullscreen_observation_.Observe(
+        &provider->registrar());
+  } else {
+    show_fullscreen_toolbar_.Init(
+        prefs::kShowFullscreenToolbar, browser_view->GetProfile()->GetPrefs(),
+        base::BindRepeating(
+            &BrowserNonClientFrameViewMac::UpdateFullscreenTopUI,
+            base::Unretained(this)));
+  }
   if (!base::FeatureList::IsEnabled(features::kImmersiveFullscreen)) {
     fullscreen_toolbar_controller_.reset(
         [[FullscreenToolbarController alloc] initWithBrowserView:browser_view]);
     [fullscreen_toolbar_controller_
         setToolbarStyle:GetUserPreferredToolbarStyle(
-                            *show_fullscreen_toolbar_)];
+                            AlwaysShowToolbarInFullscreen())];
   }
 
   if (browser_view->GetIsWebAppType()) {
@@ -212,7 +221,7 @@
     browser_view()->HideDownloadShelf();
     new_style = FullscreenToolbarStyle::TOOLBAR_NONE;
   } else {
-    new_style = GetUserPreferredToolbarStyle(*show_fullscreen_toolbar_);
+    new_style = GetUserPreferredToolbarStyle(AlwaysShowToolbarInFullscreen());
     browser_view()->UnhideDownloadShelf();
   }
   [fullscreen_toolbar_controller_ setToolbarStyle:new_style];
@@ -235,6 +244,15 @@
   }
 }
 
+void BrowserNonClientFrameViewMac::OnAlwaysShowToolbarInFullscreenChanged(
+    const web_app::AppId& app_id,
+    bool show) {
+  if (web_app::AppBrowserController::IsForWebApp(browser_view()->browser(),
+                                                 app_id)) {
+    UpdateFullscreenTopUI();
+  }
+}
+
 bool BrowserNonClientFrameViewMac::ShouldHideTopUIForFullscreen() const {
   if (frame()->IsFullscreen()) {
     return [fullscreen_toolbar_controller_ toolbarStyle] !=
@@ -599,3 +617,13 @@
           remote_cocoa::mojom::WindowControlsOverlayNSViewType::
               kWebAppFrameToolbar);
 }
+
+bool BrowserNonClientFrameViewMac::AlwaysShowToolbarInFullscreen() const {
+  if (web_app::AppBrowserController::IsWebApp(browser_view()->browser())) {
+    web_app::AppBrowserController* controller =
+        browser_view()->browser()->app_controller();
+    return controller->AlwaysShowToolbarInFullscreen();
+  } else {
+    return *show_fullscreen_toolbar_;
+  }
+}
diff --git a/chrome/browser/ui/views/frame/browser_view.cc b/chrome/browser/ui/views/frame/browser_view.cc
index 3cb9ddb..c770f782 100644
--- a/chrome/browser/ui/views/frame/browser_view.cc
+++ b/chrome/browser/ui/views/frame/browser_view.cc
@@ -2351,8 +2351,16 @@
   // This Mac-only preference disables display of the toolbar in fullscreen mode
   // so we need to take it into account when determining if the toolbar is
   // visible - especially as pertains to anchoring views.
-  if (IsFullscreen() && !browser()->profile()->GetPrefs()->GetBoolean(
-                            prefs::kShowFullscreenToolbar)) {
+  bool show_fullscreen_toolbar = true;
+  if (web_app::AppBrowserController::IsWebApp(browser())) {
+    const web_app::AppBrowserController* controller =
+        browser()->app_controller();
+    show_fullscreen_toolbar = controller->AlwaysShowToolbarInFullscreen();
+  } else {
+    show_fullscreen_toolbar = browser()->profile()->GetPrefs()->GetBoolean(
+        prefs::kShowFullscreenToolbar);
+  }
+  if (IsFullscreen() && !show_fullscreen_toolbar) {
     return false;
   }
 #endif
diff --git a/chrome/browser/ui/views/lens/lens_region_search_controller_unittest.cc b/chrome/browser/ui/views/lens/lens_region_search_controller_unittest.cc
index 6591632..d7af22d 100644
--- a/chrome/browser/ui/views/lens/lens_region_search_controller_unittest.cc
+++ b/chrome/browser/ui/views/lens/lens_region_search_controller_unittest.cc
@@ -6,6 +6,7 @@
 #include "base/feature_list.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "chrome/browser/lens/metrics/lens_metrics.h"
+#include "chrome/browser/ui/ui_features.h"
 #include "chrome/browser/ui/views/frame/browser_view.h"
 #include "chrome/browser/ui/views/frame/test_with_browser_view.h"
 #include "components/lens/lens_features.h"
@@ -19,7 +20,8 @@
  public:
   void SetUp() override {
     base::test::ScopedFeatureList features;
-    features.InitWithFeatures({features::kLensStandalone}, {});
+    features.InitWithFeatures({features::kLensStandalone},
+                              {features::kUnifiedSidePanel});
     TestWithBrowserView::SetUp();
 
     // Create an active web contents.
diff --git a/chrome/browser/ui/views/lens/lens_side_panel_controller.cc b/chrome/browser/ui/views/lens/lens_side_panel_controller.cc
index 4414b6a..f34661a 100644
--- a/chrome/browser/ui/views/lens/lens_side_panel_controller.cc
+++ b/chrome/browser/ui/views/lens/lens_side_panel_controller.cc
@@ -17,7 +17,9 @@
 #include "content/public/browser/navigation_handle.h"
 #include "net/base/url_util.h"
 #include "ui/base/l10n/l10n_util.h"
+#include "ui/gfx/geometry/rect.h"
 #include "ui/views/controls/webview/webview.h"
+#include "ui/views/view.h"
 
 namespace lens {
 
@@ -34,13 +36,19 @@
               base::BindRepeating(&LensSidePanelController::CloseButtonClicked,
                                   base::Unretained(this)),
               base::BindRepeating(&LensSidePanelController::LoadResultsInNewTab,
-                                  base::Unretained(this))))) {
+                                  base::Unretained(this))))),
+      side_panel_url_params_(nullptr) {
   side_panel_->SetVisible(false);
   Observe(side_panel_view_->GetWebContents());
   side_panel_view_->GetWebContents()->SetDelegate(this);
+
+  // Observe changes in the side_panel_view_ sizing.
+  side_panel_view_->AddObserver(this);
 }
 
 LensSidePanelController::~LensSidePanelController() {
+  side_panel_view_->RemoveObserver(this);
+
   // check side_panel -> children() size for unit tests where all the children
   // are removed when side panel is destroyed.
   if (side_panel_view_ != nullptr && side_panel_->children().size() != 0) {
@@ -66,8 +74,6 @@
 
   browser_view_->MaybeClobberAllSideSearchSidePanels();
 
-  side_panel_view_->GetWebContents()->GetController().LoadURLWithParams(
-      content::NavigationController::LoadURLParams(params));
   if (side_panel_->GetVisible()) {
     // The user issued a follow-up Lens query.
     base::RecordAction(
@@ -76,6 +82,9 @@
     side_panel_->SetVisible(true);
     base::RecordAction(base::UserMetricsAction("LensSidePanel.Show"));
   }
+
+  side_panel_url_params_ = std::make_unique<content::OpenURLParams>(params);
+  MaybeLoadURLWithParams();
 }
 
 bool LensSidePanelController::IsShowing() const {
@@ -149,6 +158,33 @@
   Close();
 }
 
+void LensSidePanelController::MaybeLoadURLWithParams() {
+  // Ensure side panel has a width before loading URL. If side panel is still
+  // closed (width == 0), defer loading the URL to
+  // LensSidePanelController::OnViewBoundsChanged. The nullptr check ensures we
+  // don't rerender the same page on a unrelated resize event.
+  if (side_panel_view_->width() == 0 || !side_panel_url_params_)
+    return;
+  // Manually set web contents to the size of side panel view on initial load.
+  // This prevents a bug in Lens Web that renders the page as if it was 0px
+  // wide.
+  auto* web_contents = side_panel_view_->GetWebContents();
+  web_contents->Resize(side_panel_view_->bounds());
+  web_contents->GetController().LoadURLWithParams(
+      content::NavigationController::LoadURLParams(*side_panel_url_params_));
+
+  side_panel_url_params_.reset();
+}
+
+void LensSidePanelController::OnViewBoundsChanged(views::View* observed_view) {
+  // If side panel is closed when we first try to render the URL, we must wait
+  // until side panel is opened. This method is called once side panel view goes
+  // from 0px wide to ~320px wide. Rendering the page after it fully opens
+  // prevents a race condition which causes the page to load before side panel
+  // is open causing the page to render as if it were 0px wide.
+  MaybeLoadURLWithParams();
+}
+
 void LensSidePanelController::LoadProgressChanged(double progress) {
   bool is_content_visible = progress == 1.0;
   side_panel_view_->SetContentVisible(is_content_visible);
diff --git a/chrome/browser/ui/views/lens/lens_side_panel_controller.h b/chrome/browser/ui/views/lens/lens_side_panel_controller.h
index 07e97af4..9d1ac179 100644
--- a/chrome/browser/ui/views/lens/lens_side_panel_controller.h
+++ b/chrome/browser/ui/views/lens/lens_side_panel_controller.h
@@ -10,6 +10,8 @@
 #include "content/public/browser/navigation_handle.h"
 #include "content/public/browser/web_contents_delegate.h"
 #include "content/public/browser/web_contents_observer.h"
+#include "ui/views/view.h"
+#include "ui/views/view_observer.h"
 
 namespace content {
 struct OpenURLParams;
@@ -22,7 +24,8 @@
 
 // Controller for the Lens side panel.
 class LensSidePanelController : public content::WebContentsObserver,
-                                public content::WebContentsDelegate {
+                                public content::WebContentsDelegate,
+                                public views::ViewObserver {
  public:
   LensSidePanelController(base::OnceClosure close_callback,
                           SidePanel* side_panel,
@@ -33,9 +36,15 @@
 
   void LoadProgressChanged(double progress) override;
 
+  // views::ViewObserver:
+  void OnViewBoundsChanged(views::View* observed_view) override;
+
   // Opens the Lens side panel with the given Lens results URL.
   void OpenWithURL(const content::OpenURLParams& params);
 
+  // Loads the Lens website if the side panel view is ready with a width.
+  void MaybeLoadURLWithParams();
+
   // Returns whether the Lens side panel is currently showing.
   bool IsShowing() const;
 
@@ -67,6 +76,9 @@
   raw_ptr<SidePanel> side_panel_;
   raw_ptr<BrowserView> browser_view_;
   raw_ptr<lens::LensSidePanelView> side_panel_view_;
+
+  // Copy of the most recent URL params given to the controller.
+  std::unique_ptr<content::OpenURLParams> side_panel_url_params_;
 };
 
 }  // namespace lens
diff --git a/chrome/browser/ui/views/location_bar/location_bar_view.cc b/chrome/browser/ui/views/location_bar/location_bar_view.cc
index 1088979f..f05cedf 100644
--- a/chrome/browser/ui/views/location_bar/location_bar_view.cc
+++ b/chrome/browser/ui/views/location_bar/location_bar_view.cc
@@ -265,8 +265,14 @@
         std::u16string(), ChromeTextContext::CONTEXT_OMNIBOX_DEEMPHASIZED,
         views::style::STYLE_LINK);
     omnibox_additional_text_view->SetHorizontalAlignment(gfx::ALIGN_LEFT);
+    int left_margin =
+        OmniboxFieldTrial::kRichAutocompletionAdditionalTextWithParenthesis
+                .Get()
+            ? 10
+            : 0;
     omnibox_additional_text_view->SetBorder(
-        views::CreateEmptyBorder(gfx::Insets::TLBR(0, 10, 0, 0)));
+        views::CreateEmptyBorder(gfx::Insets::TLBR(0, left_margin, 0, 0)));
+    omnibox_additional_text_view->SetFontList(font_list);
     omnibox_additional_text_view->SetVisible(false);
     omnibox_additional_text_view_ =
         AddChildView(std::move(omnibox_additional_text_view));
@@ -433,11 +439,18 @@
   DCHECK(OmniboxFieldTrial::IsRichAutocompletionEnabled() || text.empty());
   if (!OmniboxFieldTrial::RichAutocompletionShowAdditionalText())
     return;
-  auto wrapped_text =
-      text.empty()
-          ? text
-          // TODO(pkasting): This should use a localizable string constant.
-          : u"(" + text + u")";
+
+  // TODO(pkasting): This should use a localizable string constant.
+  // TODO(manukh): '-' separator doesn't play nice with RTL; results in
+  //  'input[autocompletion] URL -'.
+  std::u16string wrapped_text;
+  if (!text.empty()) {
+    wrapped_text =
+        OmniboxFieldTrial::kRichAutocompletionAdditionalTextWithParenthesis
+                .Get()
+            ? u"(" + text + u")"
+            : u" - " + text;
+  }
   SetOmniboxAdjacentText(omnibox_additional_text_view_, wrapped_text);
 }
 
diff --git a/chrome/browser/ui/views/page_info/safety_tip_page_info_bubble_view_browsertest.cc b/chrome/browser/ui/views/page_info/safety_tip_page_info_bubble_view_browsertest.cc
index 4afcec3..95bdc100 100644
--- a/chrome/browser/ui/views/page_info/safety_tip_page_info_bubble_view_browsertest.cc
+++ b/chrome/browser/ui/views/page_info/safety_tip_page_info_bubble_view_browsertest.cc
@@ -1326,7 +1326,7 @@
 }
 
 // Test that a Safety Tips is not shown and metrics are recorded when
-// a combo squatting url is flagged.
+// a combo squatting url is flagged with a hard-coded brand name.
 IN_PROC_BROWSER_TEST_F(SafetyTipPageInfoBubbleViewBrowserTest,
                        DoesntTriggerOnComboSquatting) {
   base::HistogramTester histograms;
@@ -1341,12 +1341,36 @@
                                NavigationSuggestionEvent::kComboSquatting, 1);
 
   // TODO(crbug.com/1343630): keyword (embedded keyword) heuristic should
-  // be removed from the code. The last `false` value in
+  // be removed from the code. The second `false` value in
   // the input of CheckHeuristicsUkmRecord is correlated to this heuristic.
   CheckRecordedHeuristicsUkmCount(1);
   CheckHeuristicsUkmRecord({kNavigatedUrl, {false, false, true}}, 0);
 }
 
+// Test that a Safety Tips is not shown and metrics are recorded when
+// a combo squatting url is flagged with a brand name from engaged sites.
+IN_PROC_BROWSER_TEST_F(SafetyTipPageInfoBubbleViewBrowserTest,
+                       DoesntTriggerOnComboSquattingSiteEngagement) {
+  base::HistogramTester histograms;
+  const GURL kEngagedUrl = GetURL("example.com");
+  const GURL kNavigatedUrl = GetURL("example-login.com");
+  SetEngagementScore(browser(), kEngagedUrl, kHighEngagement);
+  SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement);
+
+  NavigateToURL(browser(), kNavigatedUrl, WindowOpenDisposition::CURRENT_TAB);
+  EXPECT_FALSE(IsUIShowing());
+
+  histograms.ExpectTotalCount(lookalikes::kHistogramName, 1);
+  histograms.ExpectBucketCount(
+      lookalikes::kHistogramName,
+      NavigationSuggestionEvent::kComboSquattingSiteEngagement, 1);
+
+  // Heuristics with no UI don't record Safety Tips UKM.
+  CheckRecordedHeuristicsUkmCount(0);
+  // TODO(crbug.com/1337475): Add CheckHeuristicsUkmRecord after showing the
+  // safety tip for this heuristic.
+}
+
 // Tests for Digital Asset Links for lookalike checks.
 // TODO(meacer): Refactor the DAL code in LookalikeNavigationThrottle tests and
 // reuse here.
diff --git a/chrome/browser/ui/views/tabs/fake_tab_slot_controller.h b/chrome/browser/ui/views/tabs/fake_tab_slot_controller.h
index 8eb6d6b3..7a5c364 100644
--- a/chrome/browser/ui/views/tabs/fake_tab_slot_controller.h
+++ b/chrome/browser/ui/views/tabs/fake_tab_slot_controller.h
@@ -45,6 +45,9 @@
       const tab_groups::TabGroupId group,
       ToggleTabGroupCollapsedStateOrigin origin =
           ToggleTabGroupCollapsedStateOrigin::kImplicitAction) override;
+  void NotifyTabGroupEditorBubbleOpened() override {}
+  void NotifyTabGroupEditorBubbleClosed() override {}
+
   void ShowContextMenuForTab(Tab* tab,
                              const gfx::Point& p,
                              ui::MenuSourceType source_type) override {}
diff --git a/chrome/browser/ui/views/tabs/tab_container.h b/chrome/browser/ui/views/tabs/tab_container.h
index d9e8b15..a7063da3 100644
--- a/chrome/browser/ui/views/tabs/tab_container.h
+++ b/chrome/browser/ui/views/tabs/tab_container.h
@@ -63,6 +63,8 @@
   virtual void OnGroupContentsChanged(const tab_groups::TabGroupId& group) = 0;
   virtual void OnGroupClosed(const tab_groups::TabGroupId& group) = 0;
   virtual void UpdateTabGroupVisuals(tab_groups::TabGroupId group_id) = 0;
+  virtual void NotifyTabGroupEditorBubbleOpened() = 0;
+  virtual void NotifyTabGroupEditorBubbleClosed() = 0;
 
   virtual int GetModelIndexOf(const TabSlotView* slot_view) const = 0;
 
diff --git a/chrome/browser/ui/views/tabs/tab_container_impl.cc b/chrome/browser/ui/views/tabs/tab_container_impl.cc
index 95aec58..08bccd4 100644
--- a/chrome/browser/ui/views/tabs/tab_container_impl.cc
+++ b/chrome/browser/ui/views/tabs/tab_container_impl.cc
@@ -421,6 +421,17 @@
     group_views->second->UpdateBounds();
 }
 
+void TabContainerImpl::NotifyTabGroupEditorBubbleOpened() {
+  // Suppress the mouse watching behavior of tab closing mode.
+  RemoveMessageLoopObserver();
+}
+
+void TabContainerImpl::NotifyTabGroupEditorBubbleClosed() {
+  // Restore the mouse watching behavior of tab closing mode.
+  if (in_tab_close_)
+    AddMessageLoopObserver();
+}
+
 // TODO(pkasting): This should really return an optional<size_t>
 int TabContainerImpl::GetModelIndexOf(const TabSlotView* slot_view) const {
   const absl::optional<size_t> index =
diff --git a/chrome/browser/ui/views/tabs/tab_container_impl.h b/chrome/browser/ui/views/tabs/tab_container_impl.h
index ebf3655..e74a483 100644
--- a/chrome/browser/ui/views/tabs/tab_container_impl.h
+++ b/chrome/browser/ui/views/tabs/tab_container_impl.h
@@ -67,6 +67,8 @@
   void OnGroupContentsChanged(const tab_groups::TabGroupId& group) override;
   void OnGroupClosed(const tab_groups::TabGroupId& group) override;
   void UpdateTabGroupVisuals(tab_groups::TabGroupId group_id) override;
+  void NotifyTabGroupEditorBubbleOpened() override;
+  void NotifyTabGroupEditorBubbleClosed() override;
 
   int GetModelIndexOf(const TabSlotView* slot_view) const override;
 
diff --git a/chrome/browser/ui/views/tabs/tab_group_header.cc b/chrome/browser/ui/views/tabs/tab_group_header.cc
index ce2ef54f..b368633 100644
--- a/chrome/browser/ui/views/tabs/tab_group_header.cc
+++ b/chrome/browser/ui/views/tabs/tab_group_header.cc
@@ -87,7 +87,8 @@
 
 TabGroupHeader::TabGroupHeader(TabSlotController* tab_slot_controller,
                                const tab_groups::TabGroupId& group)
-    : tab_slot_controller_(tab_slot_controller) {
+    : tab_slot_controller_(tab_slot_controller),
+      editor_bubble_tracker_(tab_slot_controller) {
   DCHECK(tab_slot_controller);
 
   set_group(group);
@@ -513,10 +514,15 @@
 ADD_READONLY_PROPERTY_METADATA(int, DesiredWidth)
 END_METADATA
 
+TabGroupHeader::EditorBubbleTracker::EditorBubbleTracker(
+    TabSlotController* tab_slot_controller)
+    : tab_slot_controller_(tab_slot_controller) {}
+
 TabGroupHeader::EditorBubbleTracker::~EditorBubbleTracker() {
   if (is_open_) {
     widget_->RemoveObserver(this);
     widget_->CloseWithReason(views::Widget::ClosedReason::kUnspecified);
+    OnWidgetDestroyed(widget_);
   }
   CHECK(!IsInObserverList());
 }
@@ -527,9 +533,11 @@
   widget_ = bubble_widget;
   is_open_ = true;
   bubble_widget->AddObserver(this);
+  tab_slot_controller_->NotifyTabGroupEditorBubbleOpened();
 }
 
 void TabGroupHeader::EditorBubbleTracker::OnWidgetDestroyed(
     views::Widget* widget) {
   is_open_ = false;
+  tab_slot_controller_->NotifyTabGroupEditorBubbleClosed();
 }
diff --git a/chrome/browser/ui/views/tabs/tab_group_header.h b/chrome/browser/ui/views/tabs/tab_group_header.h
index 0dcf6f1..390b48b 100644
--- a/chrome/browser/ui/views/tabs/tab_group_header.h
+++ b/chrome/browser/ui/views/tabs/tab_group_header.h
@@ -96,7 +96,7 @@
   // at once.
   class EditorBubbleTracker : public views::WidgetObserver {
    public:
-    EditorBubbleTracker() = default;
+    explicit EditorBubbleTracker(TabSlotController* tab_slot_controller);
     ~EditorBubbleTracker() override;
 
     void Opened(views::Widget* bubble_widget);
@@ -109,6 +109,9 @@
    private:
     bool is_open_ = false;
     raw_ptr<views::Widget> widget_;
+    // Outlives this because it's a dependency inversion interface for the
+    // header's parent View.
+    raw_ptr<TabSlotController> tab_slot_controller_;
   };
 
   EditorBubbleTracker editor_bubble_tracker_;
diff --git a/chrome/browser/ui/views/tabs/tab_slot_controller.h b/chrome/browser/ui/views/tabs/tab_slot_controller.h
index 3afecb7..f8841b7 100644
--- a/chrome/browser/ui/views/tabs/tab_slot_controller.h
+++ b/chrome/browser/ui/views/tabs/tab_slot_controller.h
@@ -95,6 +95,10 @@
       ToggleTabGroupCollapsedStateOrigin origin =
           ToggleTabGroupCollapsedStateOrigin::kImplicitAction) = 0;
 
+  // Notify this controller of a tab group editor bubble opening/closing.
+  virtual void NotifyTabGroupEditorBubbleOpened() = 0;
+  virtual void NotifyTabGroupEditorBubbleClosed() = 0;
+
   // Shows a context menu for the tab at the specified point in screen coords.
   virtual void ShowContextMenuForTab(Tab* tab,
                                      const gfx::Point& p,
diff --git a/chrome/browser/ui/views/tabs/tab_strip.cc b/chrome/browser/ui/views/tabs/tab_strip.cc
index c3be9cd..6075a70 100644
--- a/chrome/browser/ui/views/tabs/tab_strip.cc
+++ b/chrome/browser/ui/views/tabs/tab_strip.cc
@@ -1528,6 +1528,13 @@
   return controller_->ToggleTabGroupCollapsedState(group, origin);
 }
 
+void TabStrip::NotifyTabGroupEditorBubbleOpened() {
+  tab_container_->NotifyTabGroupEditorBubbleOpened();
+}
+void TabStrip::NotifyTabGroupEditorBubbleClosed() {
+  tab_container_->NotifyTabGroupEditorBubbleClosed();
+}
+
 void TabStrip::ShowContextMenuForTab(Tab* tab,
                                      const gfx::Point& p,
                                      ui::MenuSourceType source_type) {
diff --git a/chrome/browser/ui/views/tabs/tab_strip.h b/chrome/browser/ui/views/tabs/tab_strip.h
index 7e43faf..4c13b305 100644
--- a/chrome/browser/ui/views/tabs/tab_strip.h
+++ b/chrome/browser/ui/views/tabs/tab_strip.h
@@ -261,6 +261,8 @@
       const tab_groups::TabGroupId group,
       ToggleTabGroupCollapsedStateOrigin origin =
           ToggleTabGroupCollapsedStateOrigin::kImplicitAction) override;
+  void NotifyTabGroupEditorBubbleOpened() override;
+  void NotifyTabGroupEditorBubbleClosed() override;
   void ShowContextMenuForTab(Tab* tab,
                              const gfx::Point& p,
                              ui::MenuSourceType source_type) override;
diff --git a/chrome/browser/ui/web_applications/app_browser_controller.cc b/chrome/browser/ui/web_applications/app_browser_controller.cc
index 2300c37..e24b5b0 100644
--- a/chrome/browser/ui/web_applications/app_browser_controller.cc
+++ b/chrome/browser/ui/web_applications/app_browser_controller.cc
@@ -387,6 +387,14 @@
   return GetAppStartUrl();
 }
 
+#if BUILDFLAG(IS_MAC)
+bool AppBrowserController::AlwaysShowToolbarInFullscreen() const {
+  return true;
+}
+
+void AppBrowserController::ToggleAlwaysShowToolbarInFullscreen() {}
+#endif
+
 void AppBrowserController::OnTabStripModelChanged(
     TabStripModel* tab_strip_model,
     const TabStripModelChange& change,
diff --git a/chrome/browser/ui/web_applications/app_browser_controller.h b/chrome/browser/ui/web_applications/app_browser_controller.h
index 456ad10..562f1a5 100644
--- a/chrome/browser/ui/web_applications/app_browser_controller.h
+++ b/chrome/browser/ui/web_applications/app_browser_controller.h
@@ -149,6 +149,12 @@
   // Determines whether the specified url is 'inside' the app |this| controls.
   virtual bool IsUrlInAppScope(const GURL& url) const = 0;
 
+#if BUILDFLAG(IS_MAC)
+  // Whether the toolbar should always be shown when in fullscreen mode.
+  virtual bool AlwaysShowToolbarInFullscreen() const;
+  virtual void ToggleAlwaysShowToolbarInFullscreen();
+#endif
+
   // Safe downcast:
   virtual WebAppBrowserController* AsWebAppBrowserController();
 
diff --git a/chrome/browser/ui/web_applications/web_app_browser_controller.cc b/chrome/browser/ui/web_applications/web_app_browser_controller.cc
index 1d3e9261..f636988 100644
--- a/chrome/browser/ui/web_applications/web_app_browser_controller.cc
+++ b/chrome/browser/ui/web_applications/web_app_browser_controller.cc
@@ -21,6 +21,8 @@
 #include "chrome/browser/ui/web_applications/web_app_dialog_manager.h"
 #include "chrome/browser/ui/web_applications/web_app_launch_utils.h"
 #include "chrome/browser/ui/web_applications/web_app_ui_manager_impl.h"
+#include "chrome/browser/web_applications/commands/callback_command.h"
+#include "chrome/browser/web_applications/web_app_command_manager.h"
 #include "chrome/browser/web_applications/web_app_constants.h"
 #include "chrome/browser/web_applications/web_app_helpers.h"
 #include "chrome/browser/web_applications/web_app_icon_manager.h"
@@ -176,6 +178,25 @@
 }
 #endif  // BUILDFLAG(IS_CHROMEOS_ASH)
 
+#if BUILDFLAG(IS_MAC)
+bool WebAppBrowserController::AlwaysShowToolbarInFullscreen() const {
+  // Reading this setting synchronously rather than going through the command
+  // manager greatly simplifies where this is read. This should be fine, since
+  // this is only persisted in the web app db.
+  return registrar().AlwaysShowToolbarInFullscreen(app_id());
+}
+
+void WebAppBrowserController::ToggleAlwaysShowToolbarInFullscreen() {
+  // base::Unretained is safe as the command manager won't execute the command
+  // if the provider no longer exists.
+  provider_.command_manager().ScheduleCommand(std::make_unique<CallbackCommand>(
+      WebAppCommandLock::CreateForAppLock({app_id()}),
+      base::BindOnce(&WebAppSyncBridge::SetAlwaysShowToolbarInFullscreen,
+                     base::Unretained(&provider_.sync_bridge()), app_id(),
+                     !registrar().AlwaysShowToolbarInFullscreen(app_id()))));
+}
+#endif
+
 #if BUILDFLAG(IS_CHROMEOS)
 bool WebAppBrowserController::ShouldShowCustomTabBar() const {
   if (AppBrowserController::ShouldShowCustomTabBar())
diff --git a/chrome/browser/ui/web_applications/web_app_browser_controller.h b/chrome/browser/ui/web_applications/web_app_browser_controller.h
index 0b3ed40..6a93110 100644
--- a/chrome/browser/ui/web_applications/web_app_browser_controller.h
+++ b/chrome/browser/ui/web_applications/web_app_browser_controller.h
@@ -103,6 +103,11 @@
   bool ShouldShowCustomTabBar() const override;
 #endif  // BUILDFLAG(IS_CHROMEOS_ASH)
 
+#if BUILDFLAG(IS_MAC)
+  bool AlwaysShowToolbarInFullscreen() const override;
+  void ToggleAlwaysShowToolbarInFullscreen() override;
+#endif
+
   // WebAppInstallManagerObserver:
   void OnWebAppUninstalled(const AppId& app_id) override;
   void OnWebAppInstallManagerDestroyed() override;
diff --git a/chrome/browser/ui/web_applications/web_app_dialog_utils.cc b/chrome/browser/ui/web_applications/web_app_dialog_utils.cc
index 6c044a0b..cfac3b6a 100644
--- a/chrome/browser/ui/web_applications/web_app_dialog_utils.cc
+++ b/chrome/browser/ui/web_applications/web_app_dialog_utils.cc
@@ -27,8 +27,8 @@
 #include "chrome/browser/web_applications/web_app_install_utils.h"
 #include "chrome/browser/web_applications/web_app_provider.h"
 #include "chrome/browser/web_applications/web_app_utils.h"
-#include "chrome/common/chrome_features.h"
 #include "components/webapps/browser/banners/app_banner_manager.h"
+#include "components/webapps/browser/features.h"
 #include "components/webapps/browser/installable/installable_metrics.h"
 #include "content/public/browser/navigation_entry.h"
 
@@ -49,7 +49,7 @@
     case WebAppInstallFlow::kInstallSite:
       web_app_info->user_display_mode = UserDisplayMode::kStandalone;
       if (base::FeatureList::IsEnabled(
-              features::kDesktopPWAsDetailedInstallDialog) &&
+              webapps::features::kDesktopPWAsDetailedInstallDialog) &&
           webapps::AppBannerManager::FromWebContents(initiator_web_contents)
               ->screenshots()
               .size()) {
diff --git a/chrome/browser/ui/webui/browser_command/browser_command_handler.cc b/chrome/browser/ui/webui/browser_command/browser_command_handler.cc
index 145b236..7f11b52 100644
--- a/chrome/browser/ui/webui/browser_command/browser_command_handler.cc
+++ b/chrome/browser/ui/webui/browser_command/browser_command_handler.cc
@@ -178,7 +178,10 @@
   if (!context)
     return;
 
-  tutorial_service->StartTutorial(kTabGroupTutorialId, context);
+  bool started_tutorial =
+      tutorial_service->StartTutorial(kTabGroupTutorialId, context);
+  tutorial_service->LogStartedFromWhatsNewPage(kTabGroupTutorialId,
+                                               started_tutorial);
 }
 
 void BrowserCommandHandler::OpenFeedbackForm() {
diff --git a/chrome/browser/ui/webui/browser_command/browser_command_handler_unittest.cc b/chrome/browser/ui/webui/browser_command/browser_command_handler_unittest.cc
index 4660a3b..155c471 100644
--- a/chrome/browser/ui/webui/browser_command/browser_command_handler_unittest.cc
+++ b/chrome/browser/ui/webui/browser_command/browser_command_handler_unittest.cc
@@ -21,6 +21,7 @@
 #include "components/safe_browsing/core/common/safe_browsing_prefs.h"
 #include "components/sync_preferences/testing_pref_service_syncable.h"
 #include "components/user_education/common/help_bubble_factory_registry.h"
+#include "components/user_education/common/tutorial_identifier.h"
 #include "components/user_education/common/tutorial_registry.h"
 #include "components/user_education/common/tutorial_service.h"
 #include "content/public/test/browser_task_environment.h"
@@ -127,6 +128,8 @@
                     ui::ElementContext,
                     base::OnceClosure,
                     base::OnceClosure));
+  MOCK_METHOD2(LogStartedFromWhatsNewPage,
+               void(user_education::TutorialIdentifier, bool));
 };
 
 class MockCommandHandler : public TestCommandHandler {
@@ -426,7 +429,9 @@
   // The StartTabGroupTutorial command should start the tab group tutorial.
   ClickInfoPtr info = ClickInfo::New();
   EXPECT_CALL(service, StartTutorial(kTabGroupTutorialId, kTestContext1,
-                                     testing::_, testing::_));
+                                     testing::_, testing::_))
+      .WillOnce(testing::Return(true));
+  EXPECT_CALL(service, LogStartedFromWhatsNewPage(kTabGroupTutorialId, true));
   EXPECT_TRUE(ExecuteCommand(Command::kStartTabGroupTutorial, std::move(info)));
 }
 
diff --git a/chrome/browser/ui/webui/chromeos/network_ui.cc b/chrome/browser/ui/webui/chromeos/network_ui.cc
index 2823384..a12b8381 100644
--- a/chrome/browser/ui/webui/chromeos/network_ui.cc
+++ b/chrome/browser/ui/webui/chromeos/network_ui.cc
@@ -16,6 +16,7 @@
 #include "ash/webui/network_ui/network_health_resource_provider.h"
 #include "ash/webui/network_ui/traffic_counters_resource_provider.h"
 #include "base/bind.h"
+#include "base/json/json_reader.h"
 #include "base/memory/weak_ptr.h"
 #include "base/values.h"
 #include "chrome/browser/ash/net/network_health/network_health_service.h"
@@ -81,6 +82,9 @@
 constexpr char kGetHostname[] = "getHostname";
 constexpr char kSetHostname[] = "setHostname";
 constexpr char kGetTetheringCapabilities[] = "getTetheringCapabilities";
+constexpr char kGetTetheringStatus[] = "getTetheringStatus";
+constexpr char kGetTetheringConfig[] = "getTetheringConfig";
+constexpr char kSetTetheringConfig[] = "setTetheringConfig";
 
 bool GetServicePathFromGuid(const std::string& guid,
                             std::string* service_path) {
@@ -527,6 +531,18 @@
         base::BindRepeating(
             &HotspotConfigMessageHandler::GetTetheringCapabilities,
             base::Unretained(this)));
+    web_ui()->RegisterMessageCallback(
+        kGetTetheringStatus,
+        base::BindRepeating(&HotspotConfigMessageHandler::GetTetheringStatus,
+                            base::Unretained(this)));
+    web_ui()->RegisterMessageCallback(
+        kGetTetheringConfig,
+        base::BindRepeating(&HotspotConfigMessageHandler::GetTetheringConfig,
+                            base::Unretained(this)));
+    web_ui()->RegisterMessageCallback(
+        kSetTetheringConfig,
+        base::BindRepeating(&HotspotConfigMessageHandler::SetTetheringConfig,
+                            base::Unretained(this)));
   }
 
  private:
@@ -540,23 +556,85 @@
     std::string callback_id = arg_list[0].GetString();
 
     ShillManagerClient::Get()->GetProperties(base::BindOnce(
-        &HotspotConfigMessageHandler::OnGetShillTetheringCapabilities,
-        weak_ptr_factory_.GetWeakPtr(), callback_id));
+        &HotspotConfigMessageHandler::OnGetShillManagerDictPropertiesByKey,
+        weak_ptr_factory_.GetWeakPtr(), callback_id,
+        shill::kTetheringCapabilitiesProperty));
   }
 
-  void OnGetShillTetheringCapabilities(const std::string& callback_id,
-                                       absl::optional<base::Value> properties) {
+  void GetTetheringStatus(const base::Value::List& arg_list) {
+    CHECK_EQ(1u, arg_list.size());
+    std::string callback_id = arg_list[0].GetString();
+
+    ShillManagerClient::Get()->GetProperties(base::BindOnce(
+        &HotspotConfigMessageHandler::OnGetShillManagerDictPropertiesByKey,
+        weak_ptr_factory_.GetWeakPtr(), callback_id,
+        shill::kTetheringStatusProperty));
+  }
+
+  void GetTetheringConfig(const base::Value::List& arg_list) {
+    CHECK_EQ(1u, arg_list.size());
+    std::string callback_id = arg_list[0].GetString();
+
+    ShillManagerClient::Get()->GetProperties(base::BindOnce(
+        &HotspotConfigMessageHandler::OnGetShillManagerDictPropertiesByKey,
+        weak_ptr_factory_.GetWeakPtr(), callback_id,
+        shill::kTetheringConfigProperty));
+  }
+
+  void SetTetheringConfig(const base::Value::List& arg_list) {
+    CHECK_EQ(2u, arg_list.size());
+    std::string callback_id = arg_list[0].GetString();
+    std::string tethering_config = arg_list[1].GetString();
+    absl::optional<base::Value> value =
+        base::JSONReader::Read(tethering_config);
+
+    if (!value || !value->is_dict()) {
+      NET_LOG(ERROR) << "Invalid tethering configuration: " << tethering_config;
+      Respond(callback_id, base::Value("Invalid tethering configuration"));
+      return;
+    }
+    NET_LOG(USER) << "SetManagerProperty: " << shill::kTetheringConfigProperty
+                  << ": " << *value;
+    ShillManagerClient::Get()->SetProperty(
+        shill::kTetheringConfigProperty, *value,
+        base::BindOnce(
+            &HotspotConfigMessageHandler::SetManagerPropertiesSuccessCallback,
+            weak_ptr_factory_.GetWeakPtr(), callback_id),
+        base::BindOnce(
+            &HotspotConfigMessageHandler::SetManagerPropertiesErrorCallback,
+            weak_ptr_factory_.GetWeakPtr(), callback_id,
+            shill::kTetheringConfigProperty));
+  }
+
+  void OnGetShillManagerDictPropertiesByKey(
+      const std::string& callback_id,
+      const std::string& dict_key,
+      absl::optional<base::Value> properties) {
     if (!properties) {
-      NET_LOG(ERROR) << "Error get tethering capabilities.";
-      Respond(callback_id, base::Value("Error get tethering capabilities."));
+      NET_LOG(ERROR) << "Error getting Shill manager properties.";
+      Respond(callback_id,
+              base::Value("Error getting Shill manager properties."));
       return;
     }
 
-    const base::Value* tethering_capabilities =
-        properties->FindDictKey(shill::kTetheringCapabilitiesProperty);
-    Respond(callback_id, tethering_capabilities
-                             ? tethering_capabilities->Clone()
-                             : base::Value(base::Value::Type::DICTIONARY));
+    const base::Value* value = properties->FindDictKey(dict_key);
+    Respond(callback_id, value ? value->Clone()
+                               : base::Value(base::Value::Type::DICTIONARY));
+  }
+
+  void SetManagerPropertiesErrorCallback(
+      const std::string& callback_id,
+      const std::string& property_name,
+      const std::string& dbus_error_name,
+      const std::string& dbus_error_message) {
+    NET_LOG(ERROR) << "Error setting Shill manager properties: "
+                   << property_name << ", error: " << dbus_error_name
+                   << ", message: " << dbus_error_message;
+    Respond(callback_id, base::Value(dbus_error_name));
+  }
+
+  void SetManagerPropertiesSuccessCallback(const std::string& callback_id) {
+    Respond(callback_id, base::Value("success"));
   }
 
   base::WeakPtrFactory<HotspotConfigMessageHandler> weak_ptr_factory_{this};
@@ -732,6 +810,23 @@
       "refreshTetheringCapabilitiesButtonText",
       l10n_util::GetStringUTF16(
           IDS_NETWORK_UI_REFRESH_TETHERING_CAPABILITIES_BUTTON_TEXT));
+  localized_strings.Set(
+      "tetheringStatusLabel",
+      l10n_util::GetStringUTF16(IDS_NETWORK_UI_TETHERING_STATUS_LABEL));
+  localized_strings.Set(
+      "refreshTetheringStatusButtonText",
+      l10n_util::GetStringUTF16(
+          IDS_NETWORK_UI_REFRESH_TETHERING_STATUS_BUTTON_TEXT));
+  localized_strings.Set(
+      "tetheringConfigLabel",
+      l10n_util::GetStringUTF16(IDS_NETWORK_UI_TETHERING_CONFIG_LABEL));
+  localized_strings.Set(
+      "refreshTetheringConfigButtonText",
+      l10n_util::GetStringUTF16(
+          IDS_NETWORK_UI_REFRESH_TETHERING_CONFIG_BUTTON_TEXT));
+  localized_strings.Set("setTetheringConfigButtonText",
+                        l10n_util::GetStringUTF16(
+                            IDS_NETWORK_UI_SET_TETHERING_CONFIG_BUTTON_TEXT));
   return localized_strings;
 }
 
diff --git a/chrome/browser/ui/webui/help/test_version_updater.h b/chrome/browser/ui/webui/help/test_version_updater.h
index ec8ca19..d2249021e 100644
--- a/chrome/browser/ui/webui/help/test_version_updater.h
+++ b/chrome/browser/ui/webui/help/test_version_updater.h
@@ -43,6 +43,7 @@
   void SetUpdateOverCellularOneTimePermission(StatusCallback callback,
                                               const std::string& update_version,
                                               int64_t update_size) override {}
+  void ApplyDeferredUpdate() override {}
 #endif
 
  private:
diff --git a/chrome/browser/ui/webui/help/version_updater.h b/chrome/browser/ui/webui/help/version_updater.h
index 7eab1c2..2811138 100644
--- a/chrome/browser/ui/webui/help/version_updater.h
+++ b/chrome/browser/ui/webui/help/version_updater.h
@@ -37,7 +37,8 @@
     FAILED_HTTP,
     FAILED_DOWNLOAD,
     DISABLED,
-    DISABLED_BY_ADMIN
+    DISABLED_BY_ADMIN,
+    DEFERRED
   };
 
   // Promotion state (Mac-only).
@@ -127,6 +128,9 @@
       StatusCallback callback,
       const std::string& update_version,
       int64_t update_size) = 0;
+
+  // If an update is downloaded but deferred, apply the deferred update.
+  virtual void ApplyDeferredUpdate() = 0;
 #endif
 };
 
diff --git a/chrome/browser/ui/webui/help/version_updater_chromeos.cc b/chrome/browser/ui/webui/help/version_updater_chromeos.cc
index e6f00e6b..9b6c008 100644
--- a/chrome/browser/ui/webui/help/version_updater_chromeos.cc
+++ b/chrome/browser/ui/webui/help/version_updater_chromeos.cc
@@ -150,6 +150,15 @@
   this->UpdateStatusChanged(update_engine_client->GetLastStatus());
 }
 
+void VersionUpdaterCros::ApplyDeferredUpdate() {
+  UpdateEngineClient* update_engine_client = UpdateEngineClient::Get();
+
+  DCHECK(update_engine_client->GetLastStatus().current_operation() ==
+         update_engine::Operation::UPDATED_BUT_DEFERRED);
+
+  update_engine_client->ApplyDeferredUpdate(base::DoNothing());
+}
+
 void VersionUpdaterCros::CheckForUpdate(StatusCallback callback,
                                         PromoteCallback) {
   callback_ = std::move(callback);
@@ -343,10 +352,12 @@
       progress = 100;
       my_status = UPDATING;
       break;
-    case update_engine::Operation::UPDATED_BUT_DEFERRED:
     case update_engine::Operation::UPDATED_NEED_REBOOT:
       my_status = NEARLY_UPDATED;
       break;
+    case update_engine::Operation::UPDATED_BUT_DEFERRED:
+      my_status = DEFERRED;
+      break;
     default:
       NOTREACHED();
   }
diff --git a/chrome/browser/ui/webui/help/version_updater_chromeos.h b/chrome/browser/ui/webui/help/version_updater_chromeos.h
index 5af757a..5d65775 100644
--- a/chrome/browser/ui/webui/help/version_updater_chromeos.h
+++ b/chrome/browser/ui/webui/help/version_updater_chromeos.h
@@ -33,6 +33,7 @@
   void SetUpdateOverCellularOneTimePermission(StatusCallback callback,
                                               const std::string& update_version,
                                               int64_t update_size) override;
+  void ApplyDeferredUpdate() override;
 
   // Gets the last update status, without triggering a new check or download.
   void GetUpdateStatus(StatusCallback callback);
diff --git a/chrome/browser/ui/webui/help/version_updater_chromeos_unittest.cc b/chrome/browser/ui/webui/help/version_updater_chromeos_unittest.cc
index 616ef54b..4efe354 100644
--- a/chrome/browser/ui/webui/help/version_updater_chromeos_unittest.cc
+++ b/chrome/browser/ui/webui/help/version_updater_chromeos_unittest.cc
@@ -241,4 +241,15 @@
   EXPECT_EQ(1, fake_update_engine_client_->is_feature_enabled_count());
 }
 
+TEST_F(VersionUpdaterCrosTest, ApplyDeferredUpdate) {
+  update_engine::StatusResult status;
+  status.set_current_operation(update_engine::Operation::UPDATED_BUT_DEFERRED);
+  fake_update_engine_client_->set_default_status(status);
+  fake_update_engine_client_->NotifyObserversThatStatusChanged(status);
+
+  EXPECT_EQ(0, fake_update_engine_client_->apply_deferred_update_count());
+  version_updater_cros_ptr_->ApplyDeferredUpdate();
+  EXPECT_EQ(1, fake_update_engine_client_->apply_deferred_update_count());
+}
+
 }  // namespace chromeos
diff --git a/chrome/browser/ui/webui/reset_password/reset_password_ui.cc b/chrome/browser/ui/webui/reset_password/reset_password_ui.cc
index ca61b25b..5e650603 100644
--- a/chrome/browser/ui/webui/reset_password/reset_password_ui.cc
+++ b/chrome/browser/ui/webui/reset_password/reset_password_ui.cc
@@ -142,8 +142,11 @@
 base::Value::Dict ResetPasswordUI::PopulateStrings() const {
   auto* service = safe_browsing::ChromePasswordProtectionService::
       GetPasswordProtectionService(Profile::FromWebUI(web_ui()));
-  std::string org_name = service->GetOrganizationName(
-      service->reused_password_account_type_for_last_shown_warning());
+  std::string org_name =
+      service
+          ? service->GetOrganizationName(
+                service->reused_password_account_type_for_last_shown_warning())
+          : std::string();
   bool known_password_type =
       password_type_ != PasswordType::PASSWORD_TYPE_UNKNOWN;
 
diff --git a/chrome/browser/ui/webui/settings/about_handler.cc b/chrome/browser/ui/webui/settings/about_handler.cc
index ee7b1ea..670c647c 100644
--- a/chrome/browser/ui/webui/settings/about_handler.cc
+++ b/chrome/browser/ui/webui/settings/about_handler.cc
@@ -251,6 +251,9 @@
     case VersionUpdater::NEED_PERMISSION_TO_UPDATE:
       status_str = "need_permission_to_update";
       break;
+    case VersionUpdater::DEFERRED:
+      status_str = "deferred";
+      break;
   }
 
   return status_str;
@@ -309,6 +312,10 @@
       "setChannel", base::BindRepeating(&AboutHandler::HandleSetChannel,
                                         base::Unretained(this)));
   web_ui()->RegisterMessageCallback(
+      "applyDeferredUpdate",
+      base::BindRepeating(&AboutHandler::HandleApplyDeferredUpdate,
+                          base::Unretained(this)));
+  web_ui()->RegisterMessageCallback(
       "requestUpdate", base::BindRepeating(&AboutHandler::HandleRequestUpdate,
                                            base::Unretained(this)));
   web_ui()->RegisterMessageCallback(
@@ -605,6 +612,10 @@
   ResolveJavascriptCallback(base::Value(callback_id), *channel_info);
 }
 
+void AboutHandler::HandleApplyDeferredUpdate(const base::Value::List& args) {
+  version_updater_->ApplyDeferredUpdate();
+}
+
 void AboutHandler::HandleRequestUpdate(const base::Value::List& args) {
   RequestUpdate();
 }
diff --git a/chrome/browser/ui/webui/settings/about_handler.h b/chrome/browser/ui/webui/settings/about_handler.h
index da737e2..090e1d0 100644
--- a/chrome/browser/ui/webui/settings/about_handler.h
+++ b/chrome/browser/ui/webui/settings/about_handler.h
@@ -122,6 +122,9 @@
                           const std::string& current_channel,
                           const std::string& target_channel);
 
+  // Applies deferred update, triggered by JS.
+  void HandleApplyDeferredUpdate(const base::Value::List& args);
+
   // Checks for and applies update, triggered by JS.
   void HandleRequestUpdate(const base::Value::List& args);
 
diff --git a/chrome/browser/ui/webui/settings/about_handler_unittest.cc b/chrome/browser/ui/webui/settings/about_handler_unittest.cc
index 424db6a..3d1ab9f 100644
--- a/chrome/browser/ui/webui/settings/about_handler_unittest.cc
+++ b/chrome/browser/ui/webui/settings/about_handler_unittest.cc
@@ -129,6 +129,17 @@
   EXPECT_EQ("", CallGetEndOfLifeInfoAndReturnString(false /*=has_eol_passed*/));
 }
 
+TEST_F(AboutHandlerTest, DeferredUpdateMessageInAboutPage) {
+  update_engine::StatusResult status;
+  status.set_current_operation(update_engine::Operation::UPDATED_BUT_DEFERRED);
+  fake_update_engine_client_->set_default_status(status);
+  fake_update_engine_client_->NotifyObserversThatStatusChanged(status);
+
+  EXPECT_EQ(0, fake_update_engine_client_->apply_deferred_update_count());
+  web_ui_.HandleReceivedMessage("applyDeferredUpdate", base::Value::List());
+  EXPECT_EQ(1, fake_update_engine_client_->apply_deferred_update_count());
+}
+
 }  // namespace
 
 }  // namespace settings
diff --git a/chrome/browser/ui/webui/settings/chromeos/about_section.cc b/chrome/browser/ui/webui/settings/chromeos/about_section.cc
index 79cd42e..736865e7 100644
--- a/chrome/browser/ui/webui/settings/chromeos/about_section.cc
+++ b/chrome/browser/ui/webui/settings/chromeos/about_section.cc
@@ -260,6 +260,8 @@
 
     {"aboutEndOfLifeTitle", IDS_SETTINGS_ABOUT_PAGE_END_OF_LIFE_TITLE},
     {"aboutDeviceName", IDS_SETTINGS_ABOUT_PAGE_DEVICE_NAME},
+    {"aboutRelaunchAndAutoUpdate",
+     IDS_SETTINGS_ABOUT_PAGE_RELAUNCH_AND_AUTO_UPDATE},
     {"aboutRelaunchAndPowerwash",
      IDS_SETTINGS_ABOUT_PAGE_RELAUNCH_AND_POWERWASH},
     {"aboutRollbackInProgress", IDS_SETTINGS_UPGRADE_ROLLBACK_IN_PROGRESS},
diff --git a/chrome/browser/ui/webui/settings/chromeos/bluetooth_section.cc b/chrome/browser/ui/webui/settings/chromeos/bluetooth_section.cc
index b267479b..ac59338 100644
--- a/chrome/browser/ui/webui/settings/chromeos/bluetooth_section.cc
+++ b/chrome/browser/ui/webui/settings/chromeos/bluetooth_section.cc
@@ -153,6 +153,18 @@
   return *tags;
 }
 
+const std::vector<SearchConcept>& GetFastPairSavedDevicesSearchConcepts() {
+  static const base::NoDestructor<std::vector<SearchConcept>> tags({
+      {IDS_OS_SETTINGS_TAG_FAST_PAIR_SAVED_DEVICES,
+       mojom::kBluetoothSavedDevicesSubpagePath,
+       mojom::SearchResultIcon::kBluetooth,
+       mojom::SearchResultDefaultRank::kMedium,
+       mojom::SearchResultType::kSetting,
+       {.setting = mojom::Setting::kFastPairSavedDevices}},
+  });
+  return *tags;
+}
+
 }  // namespace
 
 BluetoothSection::BluetoothSection(Profile* profile,
@@ -391,7 +403,8 @@
 void BluetoothSection::AddHandlers(content::WebUI* web_ui) {
   web_ui->AddMessageHandler(std::make_unique<BluetoothHandler>());
 
-  if (features::IsFastPairSavedDevicesEnabled()) {
+  if (ash::features::IsFastPairEnabled() &&
+      features::IsFastPairSavedDevicesEnabled()) {
     web_ui->AddMessageHandler(std::make_unique<FastPairSavedDevicesHandler>());
   }
 }
@@ -433,7 +446,8 @@
                                      mojom::kBluetoothDevicesSubpagePath);
   static constexpr mojom::Setting kBluetoothDevicesSettings[] = {
       mojom::Setting::kBluetoothOnOff, mojom::Setting::kBluetoothPairDevice,
-      mojom::Setting::kBluetoothUnpairDevice, mojom::Setting::kFastPairOnOff};
+      mojom::Setting::kBluetoothUnpairDevice, mojom::Setting::kFastPairOnOff,
+      mojom::Setting::kFastPairSavedDevices};
   static constexpr mojom::Setting kBluetoothDevicesSettingsLegacy[] = {
       mojom::Setting::kBluetoothConnectToDevice,
       mojom::Setting::kBluetoothDisconnectFromDevice,
@@ -506,6 +520,7 @@
   updater.RemoveSearchTags(GetBluetoothOffSearchConcepts());
   updater.RemoveSearchTags(GetFastPairOnSearchConcepts());
   updater.RemoveSearchTags(GetFastPairOffSearchConcepts());
+  updater.RemoveSearchTags(GetFastPairSavedDevicesSearchConcepts());
   updater.RemoveSearchTags(GetBluetoothConnectableSearchConcepts());
   updater.RemoveSearchTags(GetBluetoothConnectedSearchConcepts());
   updater.RemoveSearchTags(GetBluetoothPairableSearchConcepts());
@@ -523,6 +538,10 @@
     } else {
       updater.AddSearchTags(GetFastPairOffSearchConcepts());
     }
+
+    if (features::IsFastPairSavedDevicesEnabled()) {
+      updater.AddSearchTags(GetFastPairSavedDevicesSearchConcepts());
+    }
   }
 
   if (!bluetooth_adapter_->IsPowered()) {
diff --git a/chrome/browser/ui/webui/settings/chromeos/constants/setting.mojom b/chrome/browser/ui/webui/settings/chromeos/constants/setting.mojom
index 0ebf9fb..b5d05d8b 100644
--- a/chrome/browser/ui/webui/settings/chromeos/constants/setting.mojom
+++ b/chrome/browser/ui/webui/settings/chromeos/constants/setting.mojom
@@ -47,6 +47,7 @@
   kBluetoothPairDevice = 103,
   kBluetoothUnpairDevice = 104,
   kFastPairOnOff = 105,
+  kFastPairSavedDevices= 106,
 
   // MultiDevice section.
   kSetUpMultiDevice = 200,
diff --git a/chrome/browser/ui/webui/settings/safety_check_handler.cc b/chrome/browser/ui/webui/settings/safety_check_handler.cc
index fb609d77..b07c95e 100644
--- a/chrome/browser/ui/webui/settings/safety_check_handler.cc
+++ b/chrome/browser/ui/webui/settings/safety_check_handler.cc
@@ -82,6 +82,7 @@
       return SafetyCheckHandler::UpdateStatus::kUpdated;
     case VersionUpdater::UPDATING:
       return SafetyCheckHandler::UpdateStatus::kUpdating;
+    case VersionUpdater::DEFERRED:
     case VersionUpdater::NEED_PERMISSION_TO_UPDATE:
     case VersionUpdater::NEARLY_UPDATED:
       return SafetyCheckHandler::UpdateStatus::kRelaunch;
diff --git a/chrome/browser/ui/webui/settings/settings_localized_strings_provider.cc b/chrome/browser/ui/webui/settings/settings_localized_strings_provider.cc
index 0752171..e842fa4 100644
--- a/chrome/browser/ui/webui/settings/settings_localized_strings_provider.cc
+++ b/chrome/browser/ui/webui/settings/settings_localized_strings_provider.cc
@@ -749,8 +749,8 @@
     {"searchLanguages", IDS_SETTINGS_LANGUAGE_SEARCH},
     {"languagesExpandA11yLabel",
      IDS_SETTINGS_LANGUAGES_EXPAND_ACCESSIBILITY_LABEL},
-    {"orderBrowserLanguagesInstructions",
-     IDS_SETTINGS_LANGUAGES_BROWSER_LANGUAGES_LIST_ORDERING_INSTRUCTIONS},
+    {"preferredLanguagesDesc",
+     IDS_SETTINGS_LANGUAGES_PREFERRED_LANGUAGES_DESC},
     {"moveToTop", IDS_SETTINGS_LANGUAGES_LANGUAGES_LIST_MOVE_TO_TOP},
     {"moveUp", IDS_SETTINGS_LANGUAGES_LANGUAGES_LIST_MOVE_UP},
     {"moveDown", IDS_SETTINGS_LANGUAGES_LANGUAGES_LIST_MOVE_DOWN},
diff --git a/chrome/browser/web_applications/app_registrar_observer.h b/chrome/browser/web_applications/app_registrar_observer.h
index 666ef50..2d6c7437 100644
--- a/chrome/browser/web_applications/app_registrar_observer.h
+++ b/chrome/browser/web_applications/app_registrar_observer.h
@@ -63,6 +63,9 @@
   // this event is also fired during browser startup after the policy has been
   // applied.
   virtual void OnWebAppSettingsPolicyChanged() {}
+
+  virtual void OnAlwaysShowToolbarInFullscreenChanged(const AppId& app_id,
+                                                      bool show) {}
 };
 
 }  // namespace web_app
diff --git a/chrome/browser/web_applications/proto/web_app.proto b/chrome/browser/web_applications/proto/web_app.proto
index 639216e50..aa83652 100644
--- a/chrome/browser/web_applications/proto/web_app.proto
+++ b/chrome/browser/web_applications/proto/web_app.proto
@@ -346,4 +346,8 @@
   // Contains customisations to the web app tab strip. Only present when the
   // display_mode is tabbed.
   optional proto.TabStrip tab_strip = 57;
+
+  // Only used on Mac OS, stores whether the app should always show the
+  // toolbar when in fullscreen mode.
+  optional bool always_show_toolbar_in_fullscreen = 58;
 }
diff --git a/chrome/browser/web_applications/test/web_app_test_utils.cc b/chrome/browser/web_applications/test/web_app_test_utils.cc
index fdf1b2a8..369a0a4 100644
--- a/chrome/browser/web_applications/test/web_app_test_utils.cc
+++ b/chrome/browser/web_applications/test/web_app_test_utils.cc
@@ -593,6 +593,8 @@
     app->SetTabStrip(std::move(tab_strip));
   }
 
+  app->SetAlwaysShowToolbarInFullscreen(random.next_bool());
+
   return app;
 }
 
diff --git a/chrome/browser/web_applications/web_app.cc b/chrome/browser/web_applications/web_app.cc
index 0784091..df17573 100644
--- a/chrome/browser/web_applications/web_app.cc
+++ b/chrome/browser/web_applications/web_app.cc
@@ -434,6 +434,10 @@
   return removed;
 }
 
+void WebApp::SetAlwaysShowToolbarInFullscreen(bool show) {
+  always_show_toolbar_in_fullscreen_ = show;
+}
+
 WebApp::ClientData::ClientData() = default;
 
 WebApp::ClientData::~ClientData() = default;
@@ -558,7 +562,8 @@
         app.app_size_in_bytes_,
         app.data_size_in_bytes_,
         app.management_to_external_config_map_,
-        app.tab_strip_
+        app.tab_strip_,
+        app.always_show_toolbar_in_fullscreen_
         // clang-format on
     );
   };
@@ -869,6 +874,9 @@
     root.SetKey("tab_strip", base::Value());
   }
 
+  root.SetBoolKey("always_show_toolbar_in_fullscreen",
+                  always_show_toolbar_in_fullscreen_);
+
   return root;
 }
 
diff --git a/chrome/browser/web_applications/web_app.h b/chrome/browser/web_applications/web_app.h
index 931ebe2f..e3597b8 100644
--- a/chrome/browser/web_applications/web_app.h
+++ b/chrome/browser/web_applications/web_app.h
@@ -296,6 +296,11 @@
     return tab_strip_;
   }
 
+  // Only used on Mac.
+  bool always_show_toolbar_in_fullscreen() const {
+    return always_show_toolbar_in_fullscreen_;
+  }
+
   // A Web App can be installed from multiple sources simultaneously. Installs
   // add a source to the app. Uninstalls remove a source from the app.
   void AddSource(WebAppManagement::Type source);
@@ -398,6 +403,10 @@
 
   bool RemoveInstallUrlForSource(WebAppManagement::Type type, GURL install_url);
 
+  // Only used on Mac, determines if the toolbar should be permanently shown
+  // when in fullscreen.
+  void SetAlwaysShowToolbarInFullscreen(bool show);
+
   // For logging and debug purposes.
   bool operator==(const WebApp&) const;
   bool operator!=(const WebApp&) const;
@@ -493,12 +502,18 @@
 
   absl::optional<blink::Manifest::TabStrip> tab_strip_;
 
+  // Only used on Mac.
+  bool always_show_toolbar_in_fullscreen_ = true;
+
   // New fields must be added to:
   //  - |operator==|
   //  - AsDebugValue()
   //  - WebAppDatabase::CreateWebApp()
   //  - WebAppDatabase::CreateWebAppProto()
   //  - CreateRandomWebApp()
+  //  - WebAppTest.EmptyAppAsDebugValue
+  //  - WebAppTest.SampleAppAsDebugValue
+  //  - web_app.proto
   // If parsed from manifest, also add to:
   //  - ManifestUpdateTask::IsUpdateNeededForManifest()
   //  - SetWebAppManifestFields()
diff --git a/chrome/browser/web_applications/web_app_database.cc b/chrome/browser/web_applications/web_app_database.cc
index f778a60e..d1250a3 100644
--- a/chrome/browser/web_applications/web_app_database.cc
+++ b/chrome/browser/web_applications/web_app_database.cc
@@ -698,6 +698,9 @@
   if (web_app.data_size_in_bytes().has_value())
     local_data->set_data_size_in_bytes(web_app.data_size_in_bytes().value());
 
+  local_data->set_always_show_toolbar_in_fullscreen(
+      web_app.always_show_toolbar_in_fullscreen());
+
   return local_data;
 }
 
@@ -1296,6 +1299,11 @@
     web_app->SetDataSizeInBytes(local_data.data_size_in_bytes());
   }
 
+  if (local_data.has_always_show_toolbar_in_fullscreen()) {
+    web_app->SetAlwaysShowToolbarInFullscreen(
+        local_data.always_show_toolbar_in_fullscreen());
+  }
+
   return web_app;
 }
 
diff --git a/chrome/browser/web_applications/web_app_registrar.cc b/chrome/browser/web_applications/web_app_registrar.cc
index 56d163e..41c8609 100644
--- a/chrome/browser/web_applications/web_app_registrar.cc
+++ b/chrome/browser/web_applications/web_app_registrar.cc
@@ -493,6 +493,20 @@
   return GetAppStartUrl(app_id);
 }
 
+#if BUILDFLAG(IS_MAC)
+bool WebAppRegistrar::AlwaysShowToolbarInFullscreen(const AppId& app_id) const {
+  auto* web_app = GetAppById(app_id);
+  return web_app ? web_app->always_show_toolbar_in_fullscreen() : true;
+}
+
+void WebAppRegistrar::NotifyAlwaysShowToolbarInFullscreenChanged(
+    const AppId& app_id,
+    bool show) {
+  for (AppRegistrarObserver& observer : observers_)
+    observer.OnAlwaysShowToolbarInFullscreenChanged(app_id, show);
+}
+#endif
+
 const WebApp* WebAppRegistrar::GetAppById(const AppId& app_id) const {
   if (registry_profile_being_deleted_)
     return nullptr;
diff --git a/chrome/browser/web_applications/web_app_registrar.h b/chrome/browser/web_applications/web_app_registrar.h
index c072f015..07b84eb 100644
--- a/chrome/browser/web_applications/web_app_registrar.h
+++ b/chrome/browser/web_applications/web_app_registrar.h
@@ -323,6 +323,12 @@
 
   GURL GetAppNewTabUrl(const AppId& app_id) const;
 
+#if BUILDFLAG(IS_MAC)
+  bool AlwaysShowToolbarInFullscreen(const AppId& app_id) const;
+  void NotifyAlwaysShowToolbarInFullscreenChanged(const AppId& app_id,
+                                                  bool show);
+#endif
+
   void AddObserver(AppRegistrarObserver* observer);
   void RemoveObserver(AppRegistrarObserver* observer);
 
diff --git a/chrome/browser/web_applications/web_app_sync_bridge.cc b/chrome/browser/web_applications/web_app_sync_bridge.cc
index 52d4041..685235f 100644
--- a/chrome/browser/web_applications/web_app_sync_bridge.cc
+++ b/chrome/browser/web_applications/web_app_sync_bridge.cc
@@ -420,6 +420,20 @@
   registrar_->NotifyWebAppProtocolSettingsChanged();
 }
 
+#if BUILDFLAG(IS_MAC)
+void WebAppSyncBridge::SetAlwaysShowToolbarInFullscreen(const AppId& app_id,
+                                                        bool show) {
+  if (!registrar_->IsInstalled(app_id))
+    return;
+  {
+    ScopedRegistryUpdate(this)
+        ->UpdateApp(app_id)
+        ->SetAlwaysShowToolbarInFullscreen(show);
+  }
+  registrar_->NotifyAlwaysShowToolbarInFullscreenChanged(app_id, show);
+}
+#endif
+
 void WebAppSyncBridge::SetAppFileHandlerApprovalState(const AppId& app_id,
                                                       ApiApprovalState state) {
   {
diff --git a/chrome/browser/web_applications/web_app_sync_bridge.h b/chrome/browser/web_applications/web_app_sync_bridge.h
index 0e35bdb..d270da46 100644
--- a/chrome/browser/web_applications/web_app_sync_bridge.h
+++ b/chrome/browser/web_applications/web_app_sync_bridge.h
@@ -10,6 +10,7 @@
 #include "base/callback_forward.h"
 #include "base/memory/raw_ptr.h"
 #include "base/memory/weak_ptr.h"
+#include "build/build_config.h"
 #include "chrome/browser/web_applications/user_display_mode.h"
 #include "chrome/browser/web_applications/web_app.h"
 #include "chrome/browser/web_applications/web_app_constants.h"
@@ -131,6 +132,10 @@
   void RemoveDisallowedLaunchProtocol(const AppId& app_id,
                                       const std::string& protocol_scheme);
 
+#if BUILDFLAG(IS_MAC)
+  void SetAlwaysShowToolbarInFullscreen(const AppId& app_id, bool show);
+#endif
+
   // An access to read-only registry. Does an upcast to read-only type.
   const WebAppRegistrar& registrar() const { return *registrar_; }
 
diff --git a/chrome/browser/web_applications/web_app_unittest.cc b/chrome/browser/web_applications/web_app_unittest.cc
index 9c11d821..7906302 100644
--- a/chrome/browser/web_applications/web_app_unittest.cc
+++ b/chrome/browser/web_applications/web_app_unittest.cc
@@ -172,6 +172,7 @@
    "!name": "",
    "additional_search_terms": [  ],
    "allowed_launch_protocols": [  ],
+   "always_show_toolbar_in_fullscreen": true,
    "app_service_icon_url": "chrome://app-icon/empty_app/32",
    "app_size_in_bytes": "",
    "background_color": "none",
@@ -249,6 +250,7 @@
    "!name": "Name1234",
    "additional_search_terms": [ "Foo_1234_0" ],
    "allowed_launch_protocols": [ "web+test_1234_0", "web+test_1234_1" ],
+   "always_show_toolbar_in_fullscreen": false,
    "app_service_icon_url": "chrome://app-icon/eajjdjobhihlgobdfaehiiheinneagde/32",
    "app_size_in_bytes": "4226285750",
    "background_color": "rgba(77,188,194,0.9686274509803922)",
diff --git a/chrome/browser/webauthn/chrome_authenticator_request_delegate.cc b/chrome/browser/webauthn/chrome_authenticator_request_delegate.cc
index ed46050d..e70f11f8 100644
--- a/chrome/browser/webauthn/chrome_authenticator_request_delegate.cc
+++ b/chrome/browser/webauthn/chrome_authenticator_request_delegate.cc
@@ -601,6 +601,10 @@
     const device::FidoAuthenticator* authenticator,
     bool is_enterprise_attestation,
     base::OnceCallback<void(bool)> callback) {
+  if (disable_ui_ && IsVirtualEnvironmentEnabled()) {
+    std::move(callback).Run(true);
+    return;
+  }
   if (IsWebAuthnRPIDListedInSecurityKeyPermitAttestationPolicy(
           GetBrowserContext(), relying_party_id)) {
     // Enterprise attestations should have been approved already and not reach
diff --git a/chrome/browser/webauthn/chrome_authenticator_request_delegate_unittest.cc b/chrome/browser/webauthn/chrome_authenticator_request_delegate_unittest.cc
index 904538e..e4c1c006 100644
--- a/chrome/browser/webauthn/chrome_authenticator_request_delegate_unittest.cc
+++ b/chrome/browser/webauthn/chrome_authenticator_request_delegate_unittest.cc
@@ -7,7 +7,6 @@
 #include <algorithm>
 #include <utility>
 
-#include "base/logging.h"
 #include "base/memory/raw_ptr.h"
 #include "base/test/scoped_command_line.h"
 #include "base/test/scoped_feature_list.h"
@@ -20,15 +19,15 @@
 #include "components/prefs/pref_service.h"
 #include "content/public/browser/authenticator_request_client_delegate.h"
 #include "content/public/browser/browser_context.h"
-#include "content/public/test/web_contents_tester.h"
 #include "device/fido/cable/cable_discovery_data.h"
 #include "device/fido/features.h"
 #include "device/fido/fido_constants.h"
-#include "device/fido/fido_device_authenticator.h"
 #include "device/fido/fido_discovery_factory.h"
 #include "device/fido/fido_request_handler_base.h"
 #include "device/fido/fido_transport_protocol.h"
 #include "device/fido/test_callback_receiver.h"
+#include "device/fido/virtual_ctap2_device.h"
+#include "device/fido/virtual_fido_device_authenticator.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 #if BUILDFLAG(IS_WIN)
@@ -43,6 +42,8 @@
 
 namespace {
 
+static constexpr char kRelyingPartyID[] = "example.com";
+
 class ChromeAuthenticatorRequestDelegateTest
     : public ChromeRenderViewHostTestHarness {};
 
@@ -369,6 +370,22 @@
   }
 }
 
+// Tests that attestation is returned if the virtual environment is enabled and
+// the UI is disabled.
+// Regression test for crbug.com/1342458
+TEST_F(ChromeAuthenticatorRequestDelegateTest, VirtualEnvironmentAttestation) {
+  ChromeAuthenticatorRequestDelegate delegate(main_rfh());
+  delegate.DisableUI();
+  delegate.SetVirtualEnvironment(true);
+  device::VirtualFidoDeviceAuthenticator authenticator(
+      std::make_unique<device::VirtualCtap2Device>());
+  device::test::ValueCallbackReceiver<bool> cb;
+  delegate.ShouldReturnAttestation(kRelyingPartyID, &authenticator,
+                                   /*is_enterprise_attestation=*/false,
+                                   cb.callback());
+  EXPECT_TRUE(cb.value());
+}
+
 #if BUILDFLAG(IS_MAC)
 std::string TouchIdMetadataSecret(ChromeWebAuthenticationDelegate& delegate,
                                   content::BrowserContext* browser_context) {
@@ -409,8 +426,6 @@
 
 #if BUILDFLAG(IS_WIN)
 
-static constexpr char kRelyingPartyID[] = "example.com";
-
 // Tests that ShouldReturnAttestation() returns with true if |authenticator|
 // is the Windows native WebAuthn API with WEBAUTHN_API_VERSION_2 or higher,
 // where Windows prompts for attestation in its own native UI.
diff --git a/chrome/build/linux.pgo.txt b/chrome/build/linux.pgo.txt
index 1981635c..481e60d 100644
--- a/chrome/build/linux.pgo.txt
+++ b/chrome/build/linux.pgo.txt
@@ -1 +1 @@
-chrome-linux-main-1659095877-4d884c7a5eab80e1488eaffa87e6dfe2615f1cc7.profdata
+chrome-linux-main-1659138473-38b692a07c6ce1a9e17725128ff5ea0b6878b671.profdata
diff --git a/chrome/build/mac-arm.pgo.txt b/chrome/build/mac-arm.pgo.txt
index 5e06170..a06cb03 100644
--- a/chrome/build/mac-arm.pgo.txt
+++ b/chrome/build/mac-arm.pgo.txt
@@ -1 +1 @@
-chrome-mac-arm-main-1659074243-b61fd94a7aa8173d343b06d6f45f007cad6197a4.profdata
+chrome-mac-arm-main-1659138473-cc16a80f4ba8de55cf98b5d0f85d79799bf59297.profdata
diff --git a/chrome/build/mac.pgo.txt b/chrome/build/mac.pgo.txt
index b05a0fe..8041ece 100644
--- a/chrome/build/mac.pgo.txt
+++ b/chrome/build/mac.pgo.txt
@@ -1 +1 @@
-chrome-mac-main-1659095877-bda05a504638beafc29bcc2993195129fc7b488a.profdata
+chrome-mac-main-1659153757-7ebdf5bcf7cc89e6723a56a54d19838510cac59a.profdata
diff --git a/chrome/build/win32.pgo.txt b/chrome/build/win32.pgo.txt
index b0c514d..b852cd8 100644
--- a/chrome/build/win32.pgo.txt
+++ b/chrome/build/win32.pgo.txt
@@ -1 +1 @@
-chrome-win32-main-1659074243-dc49b02459ad7d6f98749c7a63f6c9be35c49af0.profdata
+chrome-win32-main-1659128176-7637cbce5b324d6da821e88d23e87130667082a8.profdata
diff --git a/chrome/build/win64.pgo.txt b/chrome/build/win64.pgo.txt
index 2f3343a0..8c90204b 100644
--- a/chrome/build/win64.pgo.txt
+++ b/chrome/build/win64.pgo.txt
@@ -1 +1 @@
-chrome-win64-main-1659106219-0c823a7058975bf5514fd2661071251279ca40fa.profdata
+chrome-win64-main-1659138473-dbaa68631543e89de158ce4b4fa4f91274b41ced.profdata
diff --git a/chrome/common/chrome_features.cc b/chrome/common/chrome_features.cc
index e515e65..7a5b212 100644
--- a/chrome/common/chrome_features.cc
+++ b/chrome/common/chrome_features.cc
@@ -269,10 +269,6 @@
 const base::Feature kDesktopPWAsEnforceWebAppSettingsPolicy{
     "DesktopPWAsEnforceWebAppSettingsPolicy", base::FEATURE_ENABLED_BY_DEFAULT};
 
-// Enables showing a detailed install dialog for user installs.
-const base::Feature kDesktopPWAsDetailedInstallDialog{
-    "DesktopPWAsDetailedInstallDialog", base::FEATURE_DISABLED_BY_DEFAULT};
-
 // Enables or disables Desktop PWAs to be auto-started on OS login.
 const base::Feature kDesktopPWAsRunOnOsLogin {
   "DesktopPWAsRunOnOsLogin",
diff --git a/chrome/common/chrome_features.h b/chrome/common/chrome_features.h
index d15760e..6472420 100644
--- a/chrome/common/chrome_features.h
+++ b/chrome/common/chrome_features.h
@@ -195,9 +195,6 @@
 extern const base::Feature kDesktopPWAsFlashAppNameInsteadOfOrigin;
 
 COMPONENT_EXPORT(CHROME_FEATURES)
-extern const base::Feature kDesktopPWAsDetailedInstallDialog;
-
-COMPONENT_EXPORT(CHROME_FEATURES)
 extern const base::Feature kDesktopPWAsRunOnOsLogin;
 
 COMPONENT_EXPORT(CHROME_FEATURES)
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index edc01fd..dfff7a4a 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -31,6 +31,7 @@
 import("//components/optimization_guide/features.gni")
 import("//components/os_crypt/features.gni")
 import("//components/safe_browsing/buildflags.gni")
+import("//components/services/screen_ai/buildflags/features.gni")
 import("//components/signin/features.gni")
 import("//components/soda/buildflags.gni")
 import("//components/spellcheck/spellcheck_build_features.gni")
@@ -2341,6 +2342,18 @@
       sources += [ "../browser/net/reporting_browsertest.cc" ]
     }
 
+    if (enable_screen_ai_service) {
+      sources += [
+        "../browser/accessibility/ax_screen_ai_annotator.cc",
+        "../browser/accessibility/ax_screen_ai_annotator.h",
+        "../browser/accessibility/screen_ai_service_browsertest.cc",
+      ]
+      deps += [
+        "//components/services/screen_ai/public/cpp:screen_ai_service_router_factory",
+        "//components/services/screen_ai/public/mojom",
+      ]
+    }
+
     if (!is_chromeos_lacros) {
       sources += [
         # crbug.com/1230268 These tests need to be fixed for Lacros.
@@ -3823,6 +3836,7 @@
         "../browser/ash/policy/login/signin_profile_extensions_policy_browsertest.cc",
         "../browser/ash/policy/networking/network_policy_application_browsertest.cc",
         "../browser/ash/policy/networking/policy_certs_browsertest.cc",
+        "../browser/ash/policy/reporting/user_added_removed/user_added_removed_reporter_browsertest.cc",
         "../browser/ash/policy/status_collector/child_status_collector_browsertest.cc",
         "../browser/ash/policy/status_collector/device_status_collector_browsertest.cc",
         "../browser/ash/policy/status_collector/legacy_device_status_collector_browsertest.cc",
@@ -4139,6 +4153,7 @@
         "//chrome/browser/ash/system_web_apps/types:types",
         "//chrome/browser/ash/wilco_dtc_supportd:mojo_utils",
         "//chrome/browser/chromeos",
+        "//chrome/browser/chromeos:add_remove_user_event_proto",
         "//chrome/browser/chromeos:dlp_policy_event_proto",
         "//chrome/browser/chromeos:key_permissions_proto",
         "//chrome/browser/chromeos:test_support",
diff --git a/chrome/test/base/OWNERS b/chrome/test/base/OWNERS
index e339a22..f0bdc0e 100644
--- a/chrome/test/base/OWNERS
+++ b/chrome/test/base/OWNERS
@@ -20,3 +20,5 @@
 per-file interactive_test_utils_mac.mm=tapted@chromium.org
 
 per-file v8sh.py=file:v8/v8:main:/INFRA_OWNERS
+
+per-file *devtools*=file://chrome/browser/devtools/OWNERS
diff --git a/chrome/test/base/devtools_listener_browsertest.cc b/chrome/test/base/devtools_listener_browsertest.cc
index 8bba0ef..89e1ba9 100644
--- a/chrome/test/base/devtools_listener_browsertest.cc
+++ b/chrome/test/base/devtools_listener_browsertest.cc
@@ -42,6 +42,10 @@
   bool ShouldForceDevToolsAgentHostCreation() override { return true; }
 
   void DevToolsAgentHostCreated(content::DevToolsAgentHost* host) override {
+    if (host->GetType() != content::DevToolsAgentHost::kTypePage &&
+        host->GetType() != content::DevToolsAgentHost::kTypeFrame) {
+      return;
+    }
     CHECK(devtools_agent_.find(host) == devtools_agent_.end());
     devtools_agent_[host] =
         std::make_unique<DevToolsListener>(host, process_id_);
diff --git a/chrome/test/data/banners/manifest_bottom_sheet_install.json b/chrome/test/data/banners/manifest_with_only_narrow_screenshots.json
similarity index 68%
copy from chrome/test/data/banners/manifest_bottom_sheet_install.json
copy to chrome/test/data/banners/manifest_with_only_narrow_screenshots.json
index 7095b85..f76a96c 100644
--- a/chrome/test/data/banners/manifest_bottom_sheet_install.json
+++ b/chrome/test/data/banners/manifest_with_only_narrow_screenshots.json
@@ -1,16 +1,12 @@
 {
-  "name": "PWA Bottom Sheet",
+  "name": "PWA with only narrow screenshots",
   "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
   "screenshots": [
     {
-      "src": "screenshot1.jpeg",
+      "src": "screenshot1-portrait.jpeg",
       "type": "image/jpeg",
-      "sizes": "728x409"
-    },
-    {
-      "src": "screenshot2.jpeg",
-      "type": "image/jpeg",
-      "sizes": "551x541"
+      "sizes": "409x728",
+      "platform": "narrow"
     }
   ],
   "icons": [
diff --git a/chrome/test/data/banners/manifest_bottom_sheet_install.json b/chrome/test/data/banners/manifest_with_only_wide_screenshots.json
similarity index 73%
copy from chrome/test/data/banners/manifest_bottom_sheet_install.json
copy to chrome/test/data/banners/manifest_with_only_wide_screenshots.json
index 7095b85..2b434de 100644
--- a/chrome/test/data/banners/manifest_bottom_sheet_install.json
+++ b/chrome/test/data/banners/manifest_with_only_wide_screenshots.json
@@ -1,16 +1,12 @@
 {
-  "name": "PWA Bottom Sheet",
+  "name": "PWA with only wide screenshots",
   "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
   "screenshots": [
     {
       "src": "screenshot1.jpeg",
       "type": "image/jpeg",
-      "sizes": "728x409"
-    },
-    {
-      "src": "screenshot2.jpeg",
-      "type": "image/jpeg",
-      "sizes": "551x541"
+      "sizes": "728x409",
+      "platform": "wide"
     }
   ],
   "icons": [
diff --git a/chrome/test/data/banners/manifest_bottom_sheet_install.json b/chrome/test/data/banners/manifest_with_screenshots.json
similarity index 75%
rename from chrome/test/data/banners/manifest_bottom_sheet_install.json
rename to chrome/test/data/banners/manifest_with_screenshots.json
index 7095b85..a850181 100644
--- a/chrome/test/data/banners/manifest_bottom_sheet_install.json
+++ b/chrome/test/data/banners/manifest_with_screenshots.json
@@ -3,9 +3,16 @@
   "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
   "screenshots": [
     {
+      "src": "screenshot1-portrait.jpeg",
+      "type": "image/jpeg",
+      "sizes": "409x728",
+      "platform": "narrow"
+    },
+    {
       "src": "screenshot1.jpeg",
       "type": "image/jpeg",
-      "sizes": "728x409"
+      "sizes": "728x409",
+      "platform": "wide"
     },
     {
       "src": "screenshot2.jpeg",
diff --git a/chrome/test/data/banners/manifest_with_too_many_screenshots.json b/chrome/test/data/banners/manifest_with_too_many_screenshots.json
index 6b227ce..a1f15643 100644
--- a/chrome/test/data/banners/manifest_with_too_many_screenshots.json
+++ b/chrome/test/data/banners/manifest_with_too_many_screenshots.json
@@ -1,5 +1,5 @@
 {
-  "name": "PWA Bottom Sheet",
+  "name": "PWA with too many screenshots",
   "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
   "screenshots": [
     {
diff --git a/chrome/test/data/banners/screenshot1-portrait.jpeg b/chrome/test/data/banners/screenshot1-portrait.jpeg
new file mode 100644
index 0000000..6941c3a
--- /dev/null
+++ b/chrome/test/data/banners/screenshot1-portrait.jpeg
Binary files differ
diff --git a/chrome/test/data/safe_browsing/malware4.html b/chrome/test/data/safe_browsing/malware4.html
new file mode 100644
index 0000000..7006427
--- /dev/null
+++ b/chrome/test/data/safe_browsing/malware4.html
@@ -0,0 +1,9 @@
+<html>
+<body>
+<div foo=1>
+<div bar=1>
+<embed src="malware_iframe.html"></iframe>
+</div>
+</div>
+</body>
+</html>
diff --git a/chrome/test/data/webui/cr_components/help_bubble_mixin_test.ts b/chrome/test/data/webui/cr_components/help_bubble_mixin_test.ts
index 2ce3a78..ce99e3bf 100644
--- a/chrome/test/data/webui/cr_components/help_bubble_mixin_test.ts
+++ b/chrome/test/data/webui/cr_components/help_bubble_mixin_test.ts
@@ -369,18 +369,18 @@
     assertTrue(isVisible(bubble));
   });
 
-  const secondParams: HelpBubbleParams = new HelpBubbleParams();
-  secondParams.nativeIdentifier = TITLE_NATIVE_ID;
-  secondParams.closeButtonAltText = CLOSE_BUTTON_ALT_TEXT;
-  secondParams.position = HelpBubblePosition.BELOW;
-  secondParams.bodyText = 'This is another help bubble.';
-  secondParams.titleText = 'This is a title';
-  secondParams.buttons = [];
+  const paramsWithTitle: HelpBubbleParams = new HelpBubbleParams();
+  paramsWithTitle.nativeIdentifier = TITLE_NATIVE_ID;
+  paramsWithTitle.closeButtonAltText = CLOSE_BUTTON_ALT_TEXT;
+  paramsWithTitle.position = HelpBubblePosition.BELOW;
+  paramsWithTitle.bodyText = 'This is another help bubble.';
+  paramsWithTitle.titleText = 'This is a title';
+  paramsWithTitle.buttons = [];
 
   test('help bubble mixin shows multiple bubbles', async () => {
     testProxy.getCallbackRouterRemote().showHelpBubble(defaultParams);
     await waitAfterNextRender(container);
-    testProxy.getCallbackRouterRemote().showHelpBubble(secondParams);
+    testProxy.getCallbackRouterRemote().showHelpBubble(paramsWithTitle);
     await waitAfterNextRender(container);
     assertTrue(container.isHelpBubbleShowing());
     const bubble1 = container.getHelpBubbleFor('title');
@@ -396,7 +396,7 @@
   test('help bubble mixin shows bubbles with and without title', async () => {
     testProxy.getCallbackRouterRemote().showHelpBubble(defaultParams);
     await waitAfterNextRender(container);
-    testProxy.getCallbackRouterRemote().showHelpBubble(secondParams);
+    testProxy.getCallbackRouterRemote().showHelpBubble(paramsWithTitle);
     await waitAfterNextRender(container);
     assertTrue(container.isHelpBubbleShowing());
     const titleBubble = container.getHelpBubbleFor('title')!;
@@ -405,13 +405,37 @@
     // correctly is present in help_bubble_test.ts, so it is sufficient to
     // verify that the property is set correctly.
     assertEquals('', paragraphBubble.titleText);
-    assertEquals(secondParams.titleText, titleBubble.titleText);
+    assertEquals(paramsWithTitle.titleText, titleBubble.titleText);
   });
 
+  const paramsWithProgress: HelpBubbleParams = new HelpBubbleParams();
+  paramsWithProgress.nativeIdentifier = LIST_NATIVE_ID;
+  paramsWithProgress.closeButtonAltText = CLOSE_BUTTON_ALT_TEXT;
+  paramsWithProgress.position = HelpBubblePosition.BELOW;
+  paramsWithProgress.bodyText = 'This is another help bubble.';
+  paramsWithProgress.progress = {current: 1, total: 3};
+  paramsWithProgress.buttons = [];
+
+  test(
+      'help bubble mixin shows bubbles with and without progress', async () => {
+        testProxy.getCallbackRouterRemote().showHelpBubble(defaultParams);
+        await waitAfterNextRender(container);
+        testProxy.getCallbackRouterRemote().showHelpBubble(paramsWithProgress);
+        await waitAfterNextRender(container);
+        assertTrue(container.isHelpBubbleShowing());
+        const paragraphBubble = container.getHelpBubbleFor('p1')!;
+        const progressBubble = container.getHelpBubbleFor('bulletList')!;
+        // Testing that setting `progress` will cause the progress to display
+        // correctly is present in help_bubble_test.ts, so it is sufficient to
+        // verify that the property is set correctly.
+        assertFalse(!!paragraphBubble.progress);
+        assertDeepEquals({current: 1, total: 3}, progressBubble.progress);
+      });
+
   test('help bubble mixin hides multiple bubbles', async () => {
     testProxy.getCallbackRouterRemote().showHelpBubble(defaultParams);
     await waitAfterNextRender(container);
-    testProxy.getCallbackRouterRemote().showHelpBubble(secondParams);
+    testProxy.getCallbackRouterRemote().showHelpBubble(paramsWithTitle);
     await waitAfterNextRender(container);
 
     testProxy.getCallbackRouterRemote().hideHelpBubble(
@@ -424,7 +448,7 @@
     assertEquals(null, container.getHelpBubbleFor('p1'));
 
     testProxy.getCallbackRouterRemote().hideHelpBubble(
-        secondParams.nativeIdentifier);
+        paramsWithTitle.nativeIdentifier);
     await waitAfterNextRender(container);
     assertFalse(container.isHelpBubbleShowing());
     assertEquals(null, container.getHelpBubbleFor('title'));
@@ -463,6 +487,7 @@
 
   test('help bubble mixin sends action button clicked event', async () => {
     container.showHelpBubble('p1', buttonParams);
+    await waitAfterNextRender(container);
 
     // Click one of the action buttons.
     const button =
diff --git a/chrome/test/data/webui/cr_components/help_bubble_test.ts b/chrome/test/data/webui/cr_components/help_bubble_test.ts
index b08643c..490ba49c3 100644
--- a/chrome/test/data/webui/cr_components/help_bubble_test.ts
+++ b/chrome/test/data/webui/cr_components/help_bubble_test.ts
@@ -7,13 +7,44 @@
 
 import {CrButtonElement} from '//resources/cr_elements/cr_button/cr_button.m.js';
 import {HELP_BUBBLE_DISMISSED_EVENT, HelpBubbleDismissedEvent, HelpBubbleElement} from 'chrome://resources/cr_components/help_bubble/help_bubble.js';
-import {HelpBubblePosition} from 'chrome://resources/cr_components/help_bubble/help_bubble.mojom-webui.js';
+import {HelpBubbleButtonParams, HelpBubblePosition} from 'chrome://resources/cr_components/help_bubble/help_bubble.mojom-webui.js';
 import {assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';
-import {isVisible} from 'chrome://webui-test/test_util.js';
+import {isVisible, waitAfterNextRender} from 'chrome://webui-test/test_util.js';
 
 suite('CrComponentsHelpBubbleTest', () => {
   let helpBubble: HelpBubbleElement;
 
+  function getNumButtons() {
+    return helpBubble.$.buttons.querySelectorAll('[id*="action-button"').length;
+  }
+
+  function getProgressIndicators() {
+    return helpBubble.$.progress.querySelectorAll('[class*="progress"]');
+  }
+
+  /**
+   * Finds and returns an element with class `name` that can be in the
+   * topContainer or in the main container of the HelpBubbleElement. If
+   * `inTopContainer` is true, will locate the element in the top container and
+   * fail the test if the element is (also) visible in the main body. If
+   * `inTopContainer` is false, the reverse applies.
+   */
+  function getMovableElement(
+      name: string, inTopContainer: boolean): HTMLElement {
+    const query = `.${name}`;
+    const headerEl =
+        helpBubble.$.topContainer.querySelector<HTMLElement>(query);
+    const mainEl = helpBubble.$.main.querySelector<HTMLElement>(query);
+    assertTrue(!!headerEl);
+    assertTrue(!!mainEl);
+    if (inTopContainer) {
+      assertTrue(mainEl.hidden);
+      return headerEl;
+    }
+    assertTrue(headerEl.hidden);
+    return mainEl;
+  }
+
   setup(() => {
     document.body.innerHTML = `
     <div id='container'>
@@ -47,7 +78,9 @@
     assertEquals(
         document.querySelector<HTMLElement>('#p1'),
         helpBubble.getAnchorElement());
-    assertEquals(HELP_BUBBLE_BODY, helpBubble.$.body.textContent);
+    const bodyElement = getMovableElement('body', true);
+    assertFalse(bodyElement.hidden);
+    assertEquals(HELP_BUBBLE_BODY, bodyElement.textContent!.trim());
     assertTrue(isVisible(helpBubble));
   });
 
@@ -59,8 +92,9 @@
     helpBubble.show();
 
     assertTrue(isVisible(helpBubble));
-    const titleElement = helpBubble.$.title;
-    assertEquals(HELP_BUBBLE_TITLE, titleElement.textContent);
+    const titleElement = getMovableElement('title', true);
+    assertFalse(titleElement.hidden);
+    assertEquals(HELP_BUBBLE_TITLE, titleElement.textContent!.trim());
     assertTrue(isVisible(titleElement));
   });
 
@@ -71,8 +105,8 @@
     helpBubble.show();
 
     assertTrue(isVisible(helpBubble));
-    const titleElement = helpBubble.$.title;
-    assertFalse(isVisible(titleElement));
+    const titleElement = getMovableElement('title', true);
+    assertTrue(titleElement.hidden);
   });
 
   test('help bubble closes', () => {
@@ -100,7 +134,9 @@
     assertEquals(
         document.querySelector<HTMLElement>('#title'),
         helpBubble.getAnchorElement());
-    assertEquals(HELP_BUBBLE_BODY, helpBubble.$.body.textContent);
+    const bodyElement = getMovableElement('body', true);
+    assertFalse(bodyElement.hidden);
+    assertEquals(HELP_BUBBLE_BODY, bodyElement.textContent!.trim());
     assertTrue(isVisible(helpBubble));
   });
 
@@ -110,7 +146,7 @@
     assertEquals(CLOSE_TEXT, helpBubble.$.close.getAttribute('aria-label'));
   });
 
-  test('help bubble click close button generates event', () => {
+  test('help bubble click close button generates event', async () => {
     let clicked: number = 0;
     const callback = (e: HelpBubbleDismissedEvent) => {
       assertEquals('title', e.detail.anchorId);
@@ -122,60 +158,73 @@
     helpBubble.position = HelpBubblePosition.BELOW;
     helpBubble.bodyText = HELP_BUBBLE_BODY;
     helpBubble.show();
+    await waitAfterNextRender(helpBubble);
     const closeButton = helpBubble.$.close;
     assertEquals(0, clicked);
     closeButton.click();
     assertEquals(1, clicked);
   });
 
-  test('help bubble adds one button', () => {
+  test('help bubble adds one button', async () => {
     helpBubble.anchorId = 'title';
     helpBubble.position = HelpBubblePosition.BELOW;
     helpBubble.bodyText = HELP_BUBBLE_BODY;
-    helpBubble.buttons = ['button1'];
+    helpBubble.buttons = [{text: 'button1', isDefault: false}];
     helpBubble.show();
-    assertEquals(1, helpBubble.$.buttons.children.length);
+    await waitAfterNextRender(helpBubble);
+    assertEquals(1, getNumButtons());
     const button = helpBubble.getButtonForTesting(0);
     assertTrue(!!button);
-    assertEquals(helpBubble.buttons[0], button.textContent);
+    assertEquals(helpBubble.buttons[0]!.text, button.textContent);
     assertFalse(button.classList.contains('default-button'));
   });
 
-  test('help bubble adds several buttons', () => {
+  test('help bubble adds several buttons', async () => {
     helpBubble.anchorId = 'title';
     helpBubble.position = HelpBubblePosition.BELOW;
     helpBubble.bodyText = HELP_BUBBLE_BODY;
-    helpBubble.buttons = ['button1', 'button2', 'button3'];
+    helpBubble.buttons = [
+      {text: 'button1', isDefault: false},
+      {text: 'button2', isDefault: false},
+      {text: 'button3', isDefault: false},
+    ];
     helpBubble.show();
-    assertEquals(3, helpBubble.$.buttons.children.length);
+    await waitAfterNextRender(helpBubble);
+    assertEquals(3, getNumButtons());
     for (let i: number = 0; i < 3; ++i) {
       const button = helpBubble.getButtonForTesting(i);
       assertTrue(!!button);
-      assertEquals(helpBubble.buttons[i], button.textContent);
+      assertEquals(helpBubble.buttons[i]!.text, button.textContent);
       assertFalse(button.classList.contains('default-button'));
     }
   });
 
-  test('help bubble adds default button', () => {
+  test('help bubble adds default button', async () => {
     helpBubble.anchorId = 'title';
     helpBubble.position = HelpBubblePosition.BELOW;
     helpBubble.bodyText = HELP_BUBBLE_BODY;
-    helpBubble.buttons = ['button1'];
-    helpBubble.defaultButtonIndex = 0;
+    helpBubble.buttons = [{text: 'button1', isDefault: true}];
     helpBubble.show();
+    await waitAfterNextRender(helpBubble);
     const button = helpBubble.getButtonForTesting(0);
     assertTrue(!!button);
     assertTrue(button.classList.contains('default-button'));
   });
 
-  test('help bubble adds default button among several', () => {
+  const THREE_BUTTONS_MIDDLE_DEFAULT: HelpBubbleButtonParams[] = [
+    {text: 'button1', isDefault: false},
+    {text: 'button2', isDefault: true},
+    {text: 'button3', isDefault: false},
+  ];
+
+  test('help bubble adds default button among several', async () => {
     helpBubble.anchorId = 'title';
     helpBubble.position = HelpBubblePosition.BELOW;
     helpBubble.bodyText = HELP_BUBBLE_BODY;
-    helpBubble.buttons = ['button1', 'button2', 'button3'];
-    helpBubble.defaultButtonIndex = 1;
+    helpBubble.buttons = THREE_BUTTONS_MIDDLE_DEFAULT;
     helpBubble.show();
-    assertEquals(3, helpBubble.$.buttons.children.length);
+    await waitAfterNextRender(helpBubble);
+    assertEquals(3, getNumButtons());
 
     // Make sure all buttons were created as expected, including the default
     // button.
@@ -183,8 +232,8 @@
     for (let i: number = 0; i < 3; ++i) {
       const button = helpBubble.getButtonForTesting(i);
       assertTrue(!!button);
-      assertEquals(helpBubble.buttons[i], button.textContent);
-      const isDefault = (i === helpBubble.defaultButtonIndex);
+      assertEquals(helpBubble.buttons[i]!.text, button.textContent);
+      const isDefault = helpBubble.buttons[i]!.isDefault;
       assertEquals(isDefault, button.classList.contains('default-button'));
       if (isDefault) {
         defaultButton = button;
@@ -198,7 +247,7 @@
         defaultButton, helpBubble.$.buttons.children.item(expectedIndex));
   });
 
-  test('help bubble click action button generates event', () => {
+  test('help bubble click action button generates event', async () => {
     let clicked: boolean;
     let buttonIndex: number;
     const callback = (e: HelpBubbleDismissedEvent) => {
@@ -212,13 +261,13 @@
     helpBubble.anchorId = 'title';
     helpBubble.position = HelpBubblePosition.BELOW;
     helpBubble.bodyText = HELP_BUBBLE_BODY;
-    helpBubble.buttons = ['button1', 'button2', 'button3'];
-    helpBubble.defaultButtonIndex = 1;
+    helpBubble.buttons = THREE_BUTTONS_MIDDLE_DEFAULT;
 
     for (let i: number = 0; i < 3; ++i) {
       clicked = false;
       buttonIndex = -1;
       helpBubble.show();
+      await waitAfterNextRender(helpBubble);
       const button = helpBubble.getButtonForTesting(i);
       assertTrue(!!button);
       button.click();
@@ -227,4 +276,103 @@
       helpBubble.hide();
     }
   });
+
+  test('help bubble with no progress doesn\'t show progress', async () => {
+    helpBubble.anchorId = 'title';
+    helpBubble.position = HelpBubblePosition.BELOW;
+    helpBubble.bodyText = HELP_BUBBLE_BODY;
+    helpBubble.buttons = THREE_BUTTONS_MIDDLE_DEFAULT;
+
+    helpBubble.show();
+    await waitAfterNextRender(helpBubble);
+
+    assertEquals(0, getProgressIndicators().length);
+    const bodyElement = getMovableElement('body', true);
+    assertFalse(bodyElement.hidden);
+  });
+
+  test(
+      'help bubble with no progress and title doesn\'t show progress',
+      async () => {
+        helpBubble.anchorId = 'title';
+        helpBubble.position = HelpBubblePosition.BELOW;
+        helpBubble.bodyText = HELP_BUBBLE_BODY;
+        helpBubble.titleText = HELP_BUBBLE_TITLE;
+        helpBubble.buttons = THREE_BUTTONS_MIDDLE_DEFAULT;
+
+        helpBubble.show();
+        await waitAfterNextRender(helpBubble);
+
+        assertEquals(0, getProgressIndicators().length);
+        assertFalse(getMovableElement('title', true).hidden);
+        assertFalse(getMovableElement('body', false).hidden);
+      });
+
+  test('help bubble with progress shows progress', async () => {
+    helpBubble.anchorId = 'title';
+    helpBubble.position = HelpBubblePosition.BELOW;
+    helpBubble.bodyText = HELP_BUBBLE_BODY;
+    helpBubble.progress = {current: 1, total: 3};
+    helpBubble.buttons = THREE_BUTTONS_MIDDLE_DEFAULT;
+
+    helpBubble.show();
+    await waitAfterNextRender(helpBubble);
+
+    const elements = getProgressIndicators();
+    assertEquals(3, elements.length);
+    assertTrue(elements.item(0)!.classList.contains('current-progress'));
+    assertTrue(elements.item(1)!.classList.contains('total-progress'));
+    assertTrue(elements.item(2)!.classList.contains('total-progress'));
+    assertFalse(getMovableElement('body', false).hidden);
+  });
+
+  test('help bubble with progress and title shows progress', async () => {
+    helpBubble.anchorId = 'title';
+    helpBubble.position = HelpBubblePosition.BELOW;
+    helpBubble.bodyText = HELP_BUBBLE_BODY;
+    helpBubble.titleText = HELP_BUBBLE_TITLE;
+    helpBubble.progress = {current: 1, total: 2};
+    helpBubble.buttons = THREE_BUTTONS_MIDDLE_DEFAULT;
+
+    helpBubble.show();
+    await waitAfterNextRender(helpBubble);
+
+    const elements = getProgressIndicators();
+    assertEquals(2, elements.length);
+    assertTrue(elements.item(0)!.classList.contains('current-progress'));
+    assertTrue(elements.item(1)!.classList.contains('total-progress'));
+
+    assertFalse(getMovableElement('title', false).hidden);
+    assertFalse(getMovableElement('body', false).hidden);
+  });
+
+  test('help bubble with full progress', async () => {
+    helpBubble.anchorId = 'title';
+    helpBubble.position = HelpBubblePosition.BELOW;
+    helpBubble.bodyText = HELP_BUBBLE_BODY;
+    helpBubble.progress = {current: 2, total: 2};
+
+    helpBubble.show();
+    await waitAfterNextRender(helpBubble);
+
+    const elements = getProgressIndicators();
+    assertEquals(2, elements.length);
+    assertTrue(elements.item(0)!.classList.contains('current-progress'));
+    assertTrue(elements.item(1)!.classList.contains('current-progress'));
+  });
+
+  test('help bubble with empty progress', async () => {
+    helpBubble.anchorId = 'title';
+    helpBubble.position = HelpBubblePosition.BELOW;
+    helpBubble.bodyText = HELP_BUBBLE_BODY;
+    helpBubble.progress = {current: 0, total: 2};
+
+    helpBubble.show();
+    await waitAfterNextRender(helpBubble);
+
+    const elements = getProgressIndicators();
+    assertEquals(2, elements.length);
+    assertTrue(elements.item(0)!.classList.contains('total-progress'));
+    assertTrue(elements.item(1)!.classList.contains('total-progress'));
+  });
 });
diff --git a/chrome/test/data/webui/settings/chromeos/device_page_tests.js b/chrome/test/data/webui/settings/chromeos/device_page_tests.js
index d01fff9..fd01ab3 100644
--- a/chrome/test/data/webui/settings/chromeos/device_page_tests.js
+++ b/chrome/test/data/webui/settings/chromeos/device_page_tests.js
@@ -715,13 +715,6 @@
     let crosAudioConfig;
 
     // Static test audio system properties.
-    const mutedByPolicyFakeAudioSystemProperties = {
-      outputVolumePercent: 75,
-
-      /** @type {!MuteState} */
-      outputMuteState: crosAudioConfigMojomWebui.MuteState.kMutedByPolicy,
-    };
-
     const maxVolumePercentFakeAudioSystemProperties = {
       outputVolumePercent: 100,
 
@@ -736,6 +729,27 @@
       outputMuteState: crosAudioConfigMojomWebui.MuteState.kNotMuted,
     };
 
+    const notMutedFakeAudioSystemProperties = {
+      outputVolumePercent: 75,
+
+      /** @type {!MuteState} */
+      outputMuteState: crosAudioConfigMojomWebui.MuteState.kNotMuted,
+    };
+
+    const mutedByUserFakeAudioSystemProperties = {
+      outputVolumePercent: 75,
+
+      /** @type {!MuteState} */
+      outputMuteState: crosAudioConfigMojomWebui.MuteState.kMutedByUser,
+    };
+
+    const mutedByPolicyFakeAudioSystemProperties = {
+      outputVolumePercent: 75,
+
+      /** @type {!MuteState} */
+      outputMuteState: crosAudioConfigMojomWebui.MuteState.kMutedByPolicy,
+    };
+
     setup(async function() {
       loadTimeData.overrideValues({
         enableAudioSettingsPage: true,
@@ -752,9 +766,7 @@
           });
     });
 
-    test('output slider mojo test', async function() {
-      await flushTasks();
-
+    test('output volume mojo test', async function() {
       const outputVolumeSlider =
           audioPage.shadowRoot.querySelector('#outputVolumeSlider');
 
@@ -762,7 +774,6 @@
       assertEquals(
           defaultFakeAudioSystemProperties.outputVolumePercent,
           outputVolumeSlider.value);
-      assertFalse(outputVolumeSlider.disabled);
 
       // Test max volume case.
       crosAudioConfig.setAudioSystemProperties(
@@ -771,7 +782,6 @@
       assertEquals(
           maxVolumePercentFakeAudioSystemProperties.outputVolumePercent,
           outputVolumeSlider.value);
-      assertFalse(outputVolumeSlider.disabled);
 
       // Test min volume case.
       crosAudioConfig.setAudioSystemProperties(
@@ -780,15 +790,28 @@
       assertEquals(
           minVolumePercentFakeAudioSystemProperties.outputVolumePercent,
           outputVolumeSlider.value);
+    });
+
+    test('output mute mojo test', async function() {
+      const outputVolumeSlider =
+          audioPage.shadowRoot.querySelector('#outputVolumeSlider');
+
+      // Test default properties.
+      assertFalse(audioPage.getIsOutputMutedForTest());
       assertFalse(outputVolumeSlider.disabled);
 
-      // Test kMutedByPolicy case.
+      // Test muted by user case.
+      crosAudioConfig.setAudioSystemProperties(
+          mutedByUserFakeAudioSystemProperties);
+      await flushTasks();
+      assertTrue(audioPage.getIsOutputMutedForTest());
+      assertFalse(outputVolumeSlider.disabled);
+
+      // Test muted by policy case.
       crosAudioConfig.setAudioSystemProperties(
           mutedByPolicyFakeAudioSystemProperties);
       await flushTasks();
-      assertEquals(
-          mutedByPolicyFakeAudioSystemProperties.outputVolumePercent,
-          outputVolumeSlider.value);
+      assertTrue(audioPage.getIsOutputMutedForTest());
       assertTrue(outputVolumeSlider.disabled);
     });
   });
diff --git a/chrome/test/data/webui/settings/chromeos/multidevice_permissions_setup_dialog_tests.js b/chrome/test/data/webui/settings/chromeos/multidevice_permissions_setup_dialog_tests.js
index 214b5b7..ad6f84d 100644
--- a/chrome/test/data/webui/settings/chromeos/multidevice_permissions_setup_dialog_tests.js
+++ b/chrome/test/data/webui/settings/chromeos/multidevice_permissions_setup_dialog_tests.js
@@ -847,6 +847,72 @@
       });
 
   test(
+      'Test Camera Roll, Notifications setup flow, notifications rejected',
+      async () => {
+        permissionsSetupDialog.setProperties({
+          showCameraRoll: true,
+          showNotifications: true,
+          showAppStreaming: false,
+          combinedSetupSupported: true,
+        });
+        flush();
+
+        assertTrue(!!dialogBody.querySelector('#start-setup-description'));
+        assertTrue(!!buttonContainer.querySelector('#learnMore'));
+        assertTrue(!!buttonContainer.querySelector('#cancelButton'));
+        assertTrue(!!buttonContainer.querySelector('#getStartedButton'));
+        assertFalse(!!buttonContainer.querySelector('#doneButton'));
+        assertFalse(!!buttonContainer.querySelector('#tryAgainButton'));
+        buttonContainer.querySelector('#getStartedButton').click();
+        assertEquals(
+            browserProxy.getCallCount('attemptCombinedFeatureSetup'), 1);
+        assertArrayEquals(
+            [true, true],
+            browserProxy.getArgs('attemptCombinedFeatureSetup')[0]);
+        assertTrue(
+            isExpectedFlowState(SetupFlowStatus.WAIT_FOR_PHONE_COMBINED));
+
+        simulateCombinedStatusChanged(
+            PermissionsSetupStatus
+                .SENT_MESSAGE_TO_PHONE_AND_WAITING_FOR_RESPONSE);
+        assertFalse(!!dialogBody.querySelector('#start-setup-description'));
+        assertTrue(!!buttonContainer.querySelector('#learnMore'));
+        assertTrue(!!buttonContainer.querySelector('#cancelButton'));
+        assertFalse(!!buttonContainer.querySelector('#getStartedButton'));
+        assertTrue(!!buttonContainer.querySelector('#doneButton'));
+        assertTrue(buttonContainer.querySelector('#doneButton').disabled);
+        assertFalse(!!buttonContainer.querySelector('#tryAgainButton'));
+        assertEquals(browserProxy.getCallCount('setFeatureEnabledState'), 0);
+
+        permissionsSetupDialog.setProperties({
+          showCameraRoll: false,
+          showNotifications: true,
+          showAppStreaming: false,
+          combinedSetupSupported: true,
+        });
+        flush();
+
+        simulateCombinedStatusChanged(
+            PermissionsSetupStatus.CAMERA_ROLL_GRANTED_NOTIFICATION_REJECTED);
+        assertFalse(!!dialogBody.querySelector('#start-setup-description'));
+        assertFalse(!!buttonContainer.querySelector('#learnMore'));
+        assertFalse(!!buttonContainer.querySelector('#cancelButton'));
+        assertFalse(!!buttonContainer.querySelector('#getStartedButton'));
+        assertTrue(!!buttonContainer.querySelector('#doneButton'));
+        assertFalse(buttonContainer.querySelector('#doneButton').disabled);
+        assertFalse(!!buttonContainer.querySelector('#tryAgainButton'));
+
+        // Only Camera Roll is enabled.
+        assertEquals(browserProxy.getCallCount('setFeatureEnabledState'), 1);
+
+        assertTrue(
+            permissionsSetupDialog.shadowRoot.querySelector('#dialog').open);
+        buttonContainer.querySelector('#doneButton').click();
+        assertFalse(
+            permissionsSetupDialog.shadowRoot.querySelector('#dialog').open);
+      });
+
+  test(
       'Test Camera Roll, Notifications and apps setup success flow',
       async () => {
         // Simulate all features are granted by the user
@@ -1125,6 +1191,99 @@
             permissionsSetupDialog.shadowRoot.querySelector('#dialog').open);
       });
 
+  test(
+      'Test all setup flow user granted others rejected cameraRoll permission',
+      async () => {
+        // Simulate user grants app permission only
+        permissionsSetupDialog.setProperties({
+          showCameraRoll: true,
+          showNotifications: true,
+          showAppStreaming: true,
+          combinedSetupSupported: true,
+        });
+        flush();
+
+        assertTrue(!!dialogBody.querySelector('#start-setup-description'));
+        assertTrue(!!buttonContainer.querySelector('#learnMore'));
+        assertTrue(!!buttonContainer.querySelector('#cancelButton'));
+        assertTrue(!!buttonContainer.querySelector('#getStartedButton'));
+        assertFalse(!!buttonContainer.querySelector('#doneButton'));
+        assertFalse(!!buttonContainer.querySelector('#tryAgainButton'));
+        buttonContainer.querySelector('#getStartedButton').click();
+        assertEquals(
+            browserProxy.getCallCount('attemptCombinedFeatureSetup'), 1);
+        assertArrayEquals(
+            [true, true],
+            browserProxy.getArgs('attemptCombinedFeatureSetup')[0]);
+        assertTrue(
+            isExpectedFlowState(SetupFlowStatus.WAIT_FOR_PHONE_COMBINED));
+
+        simulateCombinedStatusChanged(
+            PermissionsSetupStatus
+                .SENT_MESSAGE_TO_PHONE_AND_WAITING_FOR_RESPONSE);
+        assertFalse(!!dialogBody.querySelector('#start-setup-description'));
+        assertTrue(!!buttonContainer.querySelector('#learnMore'));
+        assertTrue(!!buttonContainer.querySelector('#cancelButton'));
+        assertFalse(!!buttonContainer.querySelector('#getStartedButton'));
+        assertTrue(!!buttonContainer.querySelector('#doneButton'));
+        assertTrue(buttonContainer.querySelector('#doneButton').disabled);
+        assertFalse(!!buttonContainer.querySelector('#tryAgainButton'));
+        assertEquals(browserProxy.getCallCount('setFeatureEnabledState'), 0);
+
+        // Because user rejected, Camera Roll and Notification permissions not
+        // granted.
+        permissionsSetupDialog.setProperties({
+          showCameraRoll: true,
+          showNotifications: false,
+          showAppStreaming: true,
+          combinedSetupSupported: true,
+        });
+        flush();
+
+        simulateCombinedStatusChanged(
+            PermissionsSetupStatus.CAMERA_ROLL_REJECTED_NOTIFICATION_GRANTED);
+        assertFalse(!!dialogBody.querySelector('#start-setup-description'));
+        assertFalse(!!buttonContainer.querySelector('#learnMore'));
+        assertTrue(!!buttonContainer.querySelector('#cancelButton'));
+        assertFalse(!!buttonContainer.querySelector('#getStartedButton'));
+        assertFalse(!!buttonContainer.querySelector('#doneButton'));
+        assertFalse(!!buttonContainer.querySelector('#tryAgainButton'));
+
+        // Should not enabled phone hub camera roll and notification feature
+        assertEquals(browserProxy.getCallCount('setFeatureEnabledState'), 1);
+
+        assertEquals(browserProxy.getCallCount('attemptAppsSetup'), 1);
+        assertTrue(isExpectedFlowState(SetupFlowStatus.WAIT_FOR_PHONE_APPS));
+
+        permissionsSetupDialog.setProperties({
+          showCameraRoll: true,
+          showNotifications: false,
+          showAppStreaming: false,
+          combinedSetupSupported: true,
+        });
+        flush();
+
+        simulateAppsStatusChanged(
+            PermissionsSetupStatus.COMPLETED_SUCCESSFULLY);
+        assertFalse(!!dialogBody.querySelector('#start-setup-description'));
+        assertFalse(!!buttonContainer.querySelector('#learnMore'));
+        assertFalse(!!buttonContainer.querySelector('#cancelButton'));
+        assertFalse(!!buttonContainer.querySelector('#getStartedButton'));
+        assertTrue(!!buttonContainer.querySelector('#doneButton'));
+        assertFalse(buttonContainer.querySelector('#doneButton').disabled);
+        assertFalse(!!buttonContainer.querySelector('#tryAgainButton'));
+
+        // The apps feature become enabled when the status becomes
+        // PermissionsSetupStatus.COMPLETED_SUCCESSFULLY.
+        assertEquals(browserProxy.getCallCount('setFeatureEnabledState'), 2);
+
+        assertTrue(
+            permissionsSetupDialog.shadowRoot.querySelector('#dialog').open);
+        buttonContainer.querySelector('#doneButton').click();
+        assertFalse(
+            permissionsSetupDialog.shadowRoot.querySelector('#dialog').open);
+      });
+
   test('Test all setup flow, operation failed or cancelled', async () => {
     permissionsSetupDialog.setProperties({
       showCameraRoll: true,
diff --git a/chromecast/browser/android/apk/src/org/chromium/chromecast/shell/CastContentWindowAndroid.java b/chromecast/browser/android/apk/src/org/chromium/chromecast/shell/CastContentWindowAndroid.java
index 489130a4..1653dc8c 100644
--- a/chromecast/browser/android/apk/src/org/chromium/chromecast/shell/CastContentWindowAndroid.java
+++ b/chromecast/browser/android/apk/src/org/chromium/chromecast/shell/CastContentWindowAndroid.java
@@ -111,16 +111,6 @@
         mComponent.stop(mContext);
     }
 
-    @SuppressWarnings("unused")
-    @CalledByNative
-    private void setHostContext(int interactionId, String conversationId) {
-        if (DEBUG) {
-            Log.d(TAG, "setInteractionid interactionId=%s; conversationID=%s", interactionId,
-                    conversationId);
-        }
-        mComponent.setHostContext(interactionId, conversationId);
-    }
-
     @Override
     public void onComponentClosed() {
         if (DEBUG) Log.d(TAG, "onComponentClosed");
diff --git a/chromecast/browser/android/apk/src/org/chromium/chromecast/shell/CastWebContentsComponent.java b/chromecast/browser/android/apk/src/org/chromium/chromecast/shell/CastWebContentsComponent.java
index b8e8360..2abb4fbe 100644
--- a/chromecast/browser/android/apk/src/org/chromium/chromecast/shell/CastWebContentsComponent.java
+++ b/chromecast/browser/android/apk/src/org/chromium/chromecast/shell/CastWebContentsComponent.java
@@ -267,15 +267,6 @@
         sendIntentSync(CastWebContentsIntentUtils.enableTouchInput(mSessionId, enabled));
     }
 
-    public void setHostContext(int interactionId, String conversationId) {
-        if (DEBUG) {
-            Log.d(TAG, "setInteractionid interactionId=%s; conversationID=%s", interactionId,
-                    conversationId);
-        }
-        sendIntentSync(CastWebContentsIntentUtils.setHostContext(
-                mSessionId, interactionId, conversationId));
-    }
-
     public static void onComponentClosed(String sessionId) {
         if (DEBUG) Log.d(TAG, "onComponentClosed");
         sendIntentSync(CastWebContentsIntentUtils.onActivityStopped(sessionId));
diff --git a/chromecast/browser/android/apk/src/org/chromium/chromecast/shell/CastWebContentsIntentUtils.java b/chromecast/browser/android/apk/src/org/chromium/chromecast/shell/CastWebContentsIntentUtils.java
index 1431f7c..b9ed112 100644
--- a/chromecast/browser/android/apk/src/org/chromium/chromecast/shell/CastWebContentsIntentUtils.java
+++ b/chromecast/browser/android/apk/src/org/chromium/chromecast/shell/CastWebContentsIntentUtils.java
@@ -42,32 +42,12 @@
             "com.google.android.apps.castshell.intent.action.ON_VISIBILITY_CHANGE";
 
     /**
-     * Action type of intent from cast app to android to request for a visibility priority change.
-     */
-    public static final String ACTION_REQUEST_VISIBILITY_PRIORITY =
-            "com.google.android.apps.castshell.intent.action.REQUEST_VISIBILITY_PRIORITY";
-
-    /**
-     * Action type of intent from cast app to android to request to move the cast view out of
-     * screen.
-     */
-    public static final String ACTION_REQUEST_MOVE_OUT =
-            "com.google.android.apps.castshell.intent.action.REQUEST_MOVE_OUT";
-
-    /**
      * Action type of intent from CastWebContentsComponent to notify CastWebContentsActivity that
      * touch should be enabled.
      */
     public static final String ACTION_ENABLE_TOUCH_INPUT =
             "com.google.android.apps.castshell.intent.action.ENABLE_TOUCH_INPUT";
 
-    /**
-     * Action type of intent from CastWebContentsComponent to set interaction ID and conversation ID
-     * to cast window host.
-     */
-    public static final String ACTION_SET_HOST_CONTEXT =
-            "com.google.android.apps.castshell.intent.action.SET_HOST_CONTEXT";
-
     /** Key of extra value in an intent, the value is a URI of cast://webcontents/<instanceId> */
     static final String INTENT_EXTRA_URI = "content_uri";
 
@@ -113,20 +93,6 @@
     private static final String INTENT_EXTRA_VISIBILITY_TYPE =
             "com.google.android.apps.castshell.intent.extra.VISIBILITY_TYPE";
 
-    /**
-     * Key of extra value of the intent ACTION_SET_HOST_CONTEXT, value is an int of
-     * interaction ID.
-     */
-    private static final String INTENT_EXTRA_INTERACTION_ID =
-            "com.google.android.apps.castshell.intent.extra.INTERACTION_ID";
-
-    /**
-     * Key of extra value of the intent ACTION_SET_HOST_CONTEXT, value is a string of
-     * conversation ID.
-     */
-    private static final String INTENT_EXTRA_CONVERSATION_ID =
-            "com.google.android.apps.castshell.intent.extra.CONVERSATION_ID";
-
     @VisibilityType
     static final int VISIBITY_TYPE_UNKNOWN = VisibilityType.UNKNOWN;
     @VisibilityType
@@ -164,16 +130,6 @@
         return in.getIntExtra(INTENT_EXTRA_VISIBILITY_TYPE, 0);
     }
 
-    // Used by intent of ACTION_SET_HOST_CONTEXT
-    public static int getInteractionId(Intent in) {
-        return in.getIntExtra(INTENT_EXTRA_INTERACTION_ID, 0);
-    }
-
-    // Used by intent of ACTION_SET_HOST_CONTEXT
-    public static String getConversationId(Intent in) {
-        return in.getStringExtra(INTENT_EXTRA_CONVERSATION_ID);
-    }
-
     public static boolean isIntentOfActivityStopped(Intent in) {
         return in.getAction().equals(ACTION_ACTIVITY_STOPPED);
     }
@@ -288,20 +244,6 @@
         return intent;
     }
 
-    // CastWebContentsComponent -> CastWindowManager
-    public static Intent setHostContext(
-            String instanceId, int interactionId, String conversationId) {
-        if (DEBUG) {
-            Log.d(TAG, "setInteractionid interactionId=%s; conversationID=%s", interactionId,
-                    conversationId);
-        }
-        Intent intent = new Intent(ACTION_SET_HOST_CONTEXT);
-        intent.putExtra(INTENT_EXTRA_URI, getInstanceUri(instanceId).toString());
-        intent.putExtra(INTENT_EXTRA_INTERACTION_ID, interactionId);
-        intent.putExtra(INTENT_EXTRA_CONVERSATION_ID, conversationId);
-        return intent;
-    }
-
     public static Uri getInstanceUri(String instanceId) {
         Uri instanceUri = new Uri.Builder()
                                   .scheme(ACTION_DATA_SCHEME)
diff --git a/chromecast/browser/android/cast_content_window_android.cc b/chromecast/browser/android/cast_content_window_android.cc
index 0ef159f9..02fd17b 100644
--- a/chromecast/browser/android/cast_content_window_android.cc
+++ b/chromecast/browser/android/cast_content_window_android.cc
@@ -9,7 +9,6 @@
 #include "base/android/jni_android.h"
 #include "base/android/jni_string.h"
 #include "base/android/scoped_java_ref.h"
-#include "base/logging.h"
 #include "chromecast/browser/jni_headers/CastContentWindowAndroid_jni.h"
 #include "content/public/browser/web_contents.h"
 
@@ -31,9 +30,6 @@
       turn_on_screen, ConvertUTF8ToJavaString(env, session_id));
 }
 
-constexpr char kContextInteractionId[] = "interactionId";
-constexpr char kContextConversationId[] = "conversationId";
-
 }  // namespace
 
 CastContentWindowAndroid::CastContentWindowAndroid(
@@ -98,20 +94,7 @@
 void CastContentWindowAndroid::SetActivityContext(
     base::Value activity_context) {}
 
-void CastContentWindowAndroid::SetHostContext(base::Value host_context) {
-  auto* found_interaction_id = host_context.FindKey(kContextInteractionId);
-  auto* found_conversation_id = host_context.FindKey(kContextConversationId);
-  if (found_interaction_id && found_conversation_id) {
-    int interaction_id = found_interaction_id->GetInt();
-    std::string& conversation_id = found_conversation_id->GetString();
-    JNIEnv* env = base::android::AttachCurrentThread();
-    Java_CastContentWindowAndroid_setHostContext(
-        env, java_window_, static_cast<int>(interaction_id),
-        ConvertUTF8ToJavaString(env, conversation_id));
-  } else {
-    LOG(ERROR) << "Interaction ID or Conversation ID is not found";
-  }
-}
+void CastContentWindowAndroid::SetHostContext(base::Value host_context) {}
 
 void CastContentWindowAndroid::RequestMoveOut() {}
 
diff --git a/chromecast/browser/android/junit/src/org/chromium/chromecast/shell/CastWebContentsIntentUtilsTest.java b/chromecast/browser/android/junit/src/org/chromium/chromecast/shell/CastWebContentsIntentUtilsTest.java
index 3050da1..781a4352 100644
--- a/chromecast/browser/android/junit/src/org/chromium/chromecast/shell/CastWebContentsIntentUtilsTest.java
+++ b/chromecast/browser/android/junit/src/org/chromium/chromecast/shell/CastWebContentsIntentUtilsTest.java
@@ -139,15 +139,4 @@
         Assert.assertNotNull(uri);
         Assert.assertEquals(EXPECTED_URI, uri);
     }
-
-    @Test
-    public void testSetHostContext() {
-        Intent in = CastWebContentsIntentUtils.setHostContext(SESSION_ID, 123, "foo");
-        String uri = CastWebContentsIntentUtils.getUriString(in);
-        Assert.assertNotNull(uri);
-        Assert.assertEquals(EXPECTED_URI, uri);
-        Assert.assertEquals(CastWebContentsIntentUtils.ACTION_SET_HOST_CONTEXT, in.getAction());
-        Assert.assertEquals(CastWebContentsIntentUtils.getInteractionId(in), 123);
-        Assert.assertEquals(CastWebContentsIntentUtils.getConversationId(in), "foo");
-    }
 }
diff --git a/chromeos/ash/components/dbus/update_engine/fake_update_engine_client.cc b/chromeos/ash/components/dbus/update_engine/fake_update_engine_client.cc
index 447c094..20d6054 100644
--- a/chromeos/ash/components/dbus/update_engine/fake_update_engine_client.cc
+++ b/chromeos/ash/components/dbus/update_engine/fake_update_engine_client.cc
@@ -122,7 +122,9 @@
 }
 
 void FakeUpdateEngineClient::ApplyDeferredUpdate(
-    base::OnceClosure failure_callback) {}
+    base::OnceClosure failure_callback) {
+  apply_deferred_update_count_++;
+}
 
 void FakeUpdateEngineClient::set_default_status(
     const update_engine::StatusResult& status) {
diff --git a/chromeos/ash/components/dbus/update_engine/fake_update_engine_client.h b/chromeos/ash/components/dbus/update_engine/fake_update_engine_client.h
index fc9e378..2c19558f 100644
--- a/chromeos/ash/components/dbus/update_engine/fake_update_engine_client.h
+++ b/chromeos/ash/components/dbus/update_engine/fake_update_engine_client.h
@@ -128,6 +128,11 @@
   // Returns how many times |IsFeatureEnabled()| is called.
   int is_feature_enabled_count() const { return is_feature_enabled_count_; }
 
+  // Returns how many times |ApplyDeferredUpdate()| is called.
+  int apply_deferred_update_count() const {
+    return apply_deferred_update_count_;
+  }
+
   void SetToggleFeature(const std::string& feature,
                         absl::optional<bool> opt_enabled);
 
@@ -147,6 +152,7 @@
   int update_over_cellular_one_time_permission_count_ = 0;
   int toggle_feature_count_ = 0;
   int is_feature_enabled_count_ = 0;
+  int apply_deferred_update_count_ = 0;
   std::map<std::string, absl::optional<bool>> features_;
   base::Time eol_date_;
 };
diff --git a/components/exo/shell_surface_base.cc b/components/exo/shell_surface_base.cc
index d036c7e..0495942 100644
--- a/components/exo/shell_surface_base.cc
+++ b/components/exo/shell_surface_base.cc
@@ -1350,8 +1350,12 @@
 
   // Start tracking changes to window bounds and window state.
   window->AddObserver(this);
-  // `window_state` can be null when the window type is menu.
-  if (!is_menu_)
+  ash::WindowState* window_state = ash::WindowState::Get(window);
+  // Skip initializing window state when it is menu.
+  // TODO(crbug.com/1338597): Remove `window_state` condition when tooltip fix
+  // is done. Without the fix, window_state can be null when  it is tooltip and
+  // the parent window is menu, so add null check of `window_state` here.
+  if (!is_menu_ && window_state)
     InitializeWindowState(ash::WindowState::Get(window));
 
   SetShellUseImmersiveForFullscreen(window, immersive_implied_by_fullscreen_);
diff --git a/components/lookalikes/core/lookalike_url_util.cc b/components/lookalikes/core/lookalike_url_util.cc
index dd5f853e..3f4e73f3 100644
--- a/components/lookalikes/core/lookalike_url_util.cc
+++ b/components/lookalikes/core/lookalike_url_util.cc
@@ -543,6 +543,59 @@
   return 0;
 }
 
+// Brand names with length of 4 or less should not be checked in domains for
+// Combo Squatting. Short brand names can cause false positives in results.
+bool IsComboSquattingCandidate(const std::string& brand) {
+  return brand.size() > kMinBrandNameLengthForComboSquatting;
+}
+
+// Extract brand names from engaged sites to be checked for Combo Squatting, if
+// the brand is not one of the hard coded brand names.
+std::vector<std::string> GetBrandNamesFromEngagedSites(
+    const std::vector<DomainInfo>& engaged_sites) {
+  std::vector<std::string> output;
+
+  for (const DomainInfo& engaged_site : engaged_sites) {
+    std::string domain_without_registry = engaged_site.domain_without_registry;
+    if (IsComboSquattingCandidate(domain_without_registry)) {
+      output.emplace_back(domain_without_registry);
+    }
+  }
+  return output;
+}
+
+// Returns true if the navigated_domain is flagged as Combo Squatting.
+// matched_domain is the suggested domain that will be shown to the user
+// instead of the navigated_domain in the warning UI.
+bool IsComboSquatting(const std::vector<std::string>& brand_names,
+                      const ComboSquattingParams& combo_squatting_params,
+                      const DomainInfo& navigated_domain,
+                      std::string* matched_domain) {
+  // Check if the domain has any brand name and any popular keyword.
+  for (auto& brand : brand_names) {
+    DCHECK(IsComboSquattingCandidate(brand));
+
+    if (navigated_domain.domain_without_registry.size() == brand.size() ||
+        navigated_domain.domain_without_registry.find(brand) ==
+            std::string::npos) {
+      continue;
+    }
+
+    for (size_t j = 0; j < combo_squatting_params.num_popular_keywords; j++) {
+      auto* const keyword = combo_squatting_params.popular_keywords[j];
+      if (navigated_domain.domain_without_registry.find(keyword) !=
+              std::string::npos &&
+          std::string(brand).find(keyword) == std::string::npos &&
+          std::string(keyword).find(brand) == std::string::npos) {
+        // TODO(crbug.com/1341320): Compute a better matched_domain.
+        *matched_domain = std::string(brand) + ".com";
+        return true;
+      }
+    }
+  }
+  return false;
+}
+
 }  // namespace
 
 DomainInfo::DomainInfo(const std::string& arg_hostname,
@@ -842,13 +895,20 @@
   }
 
   // If none of the previous heuristics work, check it for Combo Squatting.
-  if (IsComboSquatting(navigated_domain, matched_domain)) {
+  ComboSquattingType combo_squatting_type =
+      GetComboSquattingType(navigated_domain, engaged_sites, matched_domain);
+  if (combo_squatting_type == ComboSquattingType::kHardCoded) {
     *match_type = LookalikeUrlMatchType::kComboSquatting;
     DCHECK(!matched_domain->empty());
     return true;
+  } else if (combo_squatting_type == ComboSquattingType::kSiteEngagement) {
+    *match_type = LookalikeUrlMatchType::kComboSquattingSiteEngagement;
+    DCHECK(!matched_domain->empty());
+    return true;
   }
 
   DCHECK(embedding_type == TargetEmbeddingType::kNone);
+  DCHECK(combo_squatting_type == ComboSquattingType::kNone);
   return false;
 }
 
@@ -888,6 +948,9 @@
     case LookalikeUrlMatchType::kComboSquatting:
       RecordEvent(NavigationSuggestionEvent::kComboSquatting);
       break;
+    case LookalikeUrlMatchType::kComboSquattingSiteEngagement:
+      RecordEvent(NavigationSuggestionEvent::kComboSquattingSiteEngagement);
+      break;
     case LookalikeUrlMatchType::kNone:
       break;
   }
@@ -1224,34 +1287,29 @@
              kPopularKeywordsforCSQ, std::size(kPopularKeywordsforCSQ)};
 }
 
-bool IsComboSquatting(const DomainInfo& navigated_domain,
-                      std::string* matched_domain) {
-  // TODO(crbug.com/1341023): We should check the domain in allowlist once we
-  // start getting metrics in future iterations.
-  ComboSquattingParams* combo_squatting_params = GetComboSquattingParams();
-  // Check if the domain has any brand name and any popular keyword.
+ComboSquattingType GetComboSquattingType(
+    const DomainInfo& navigated_domain,
+    const std::vector<DomainInfo>& engaged_sites,
+    std::string* matched_domain) {
+  const ComboSquattingParams* combo_squatting_params =
+      GetComboSquattingParams();
+
+  // First check Combo Squatting with hard coded brand names.
+  std::vector<std::string> brand_names;
   for (size_t i = 0; i < combo_squatting_params->num_brand_names; i++) {
-    auto* const brand = combo_squatting_params->brand_names[i];
-    DCHECK(std::string(brand).size() > kMinBrandNameLengthForComboSquatting);
-
-    if (!(navigated_domain.domain_without_registry.find(brand) !=
-              std::string::npos &&
-          navigated_domain.domain_without_registry.size() != strlen(brand))) {
-      continue;
-    }
-
-    for (size_t j = 0; j < combo_squatting_params->num_popular_keywords; j++) {
-      auto* const keyword = combo_squatting_params->popular_keywords[j];
-      if (navigated_domain.domain_without_registry.find(keyword) !=
-              std::string::npos &&
-          std::string(brand).find(keyword) == std::string::npos &&
-          std::string(keyword).find(brand) == std::string::npos) {
-        // TODO(crbug.com/1341320): In future cls we will compute a better
-        // suggestion for each domain.
-        *matched_domain = std::string(brand) + ".com";
-        return true;
-      }
-    }
+    brand_names.emplace_back(combo_squatting_params->brand_names[i]);
   }
-  return false;
+  if (IsComboSquatting(brand_names, *combo_squatting_params, navigated_domain,
+                       matched_domain)) {
+    return ComboSquattingType::kHardCoded;
+  }
+
+  // Then check Combo Squatting with brand names in engaged sites.
+  brand_names = GetBrandNamesFromEngagedSites(engaged_sites);
+  if (IsComboSquatting(brand_names, *combo_squatting_params, navigated_domain,
+                       matched_domain)) {
+    return ComboSquattingType::kSiteEngagement;
+  }
+
+  return ComboSquattingType::kNone;
 }
diff --git a/components/lookalikes/core/lookalike_url_util.h b/components/lookalikes/core/lookalike_url_util.h
index facc8f2..e4be994 100644
--- a/components/lookalikes/core/lookalike_url_util.h
+++ b/components/lookalikes/core/lookalike_url_util.h
@@ -43,6 +43,15 @@
   kSafetyTip = 2,
 };
 
+// Used for |GetComboSquattingType| return value.
+// It shows if the brand name in the flagged domain
+// comes from the hard-coded brand names or from site engagements.
+enum class ComboSquattingType {
+  kNone = 0,
+  kHardCoded = 1,
+  kSiteEngagement = 2,
+};
+
 // Used for UKM. There is only a single LookalikeUrlMatchType per navigation.
 enum class LookalikeUrlMatchType {
   kNone = 0,
@@ -64,15 +73,17 @@
   kCharacterSwapSiteEngagement = 10,
   kCharacterSwapTop500 = 11,
 
-  // In contrast to other heuristics that use
-  // Top500 and SiteEngagement domains, Combo Squatting uses manually
-  // curated lists of brand names and keywords that are hardcoded as
-  // kBrandNamesforCSQ and kPopularKeywordsforCSQ in lookalike_url_util.cc.
+  // Combo Squatting uses manually
+  // curated lists of hard-coded keywords (kPopularKeywordsforCSQ in
+  // lookalike_url_util.cc) and both manually curated hard-coded brand names
+  // (kBrandNamesforCSQ in lookalike_url_util.cc) and brand names from
+  // SiteEngagement to flag domains.
   kComboSquatting = 12,
+  kComboSquattingSiteEngagement = 13,
 
   // Append new items to the end of the list above; do not modify or replace
   // existing values. Comment out obsolete items.
-  kMaxValue = kComboSquatting,
+  kMaxValue = kComboSquattingSiteEngagement,
 };
 
 // Used for UKM. There is only a single LookalikeUrlBlockingPageUserAction per
@@ -107,10 +118,11 @@
   kMatchCharacterSwapSiteEngagement = 12,
   kMatchCharacterSwapTop500 = 13,
   kComboSquatting = 14,
+  kComboSquattingSiteEngagement = 15,
 
   // Append new items to the end of the list above; do not modify or
   // replace existing values. Comment out obsolete items.
-  kMaxValue = kComboSquatting,
+  kMaxValue = kComboSquattingSiteEngagement,
 };
 
 struct Top500DomainsParams {
@@ -277,10 +289,13 @@
 // Reset brand names and keywords after testing Combo Squatting heuristic.
 void ResetComboSquattingParamsForTesting();
 
-// Returns true if the navigated_domain is flagged as Combo Squatting.
-// matched_domain is the suggested domain that will be shown to the user
-// instead of the navigated_domain in the warning UI.
-bool IsComboSquatting(const DomainInfo& navigated_domain,
-                      std::string* matched_domain);
+// Check if |navigated_domain| is Combo Squatting lookalike.
+// It gets |engaged_sites| to use its brand names in addition to hard coded
+// brand names. The function sets |matched_domain| to suggest to the user
+// instead of the Combo Squatting domain.
+ComboSquattingType GetComboSquattingType(
+    const DomainInfo& navigated_domain,
+    const std::vector<DomainInfo>& engaged_sites,
+    std::string* matched_domain);
 
 #endif  // COMPONENTS_LOOKALIKES_CORE_LOOKALIKE_URL_UTIL_H_
diff --git a/components/lookalikes/core/lookalike_url_util_unittest.cc b/components/lookalikes/core/lookalike_url_util_unittest.cc
index 028e1b6b..104c23762 100644
--- a/components/lookalikes/core/lookalike_url_util_unittest.cc
+++ b/components/lookalikes/core/lookalike_url_util_unittest.cc
@@ -611,66 +611,86 @@
 
 // Test for Combo Squatting check of domains.
 TEST_F(ComboSquattingTest, IsComboSquatting) {
+  const std::vector<DomainInfo> kEngagedSites = {
+      // An engaged site which is not in the hard coded brand names.
+      GetDomainInfo(GURL("https://engagedsite.com")),
+      // An engaged site which is duplicate with a hard coded brand name.
+      GetDomainInfo(GURL("https://subdomain.google.com")),
+      // An engaged site with length less than threshold (4) for
+      // consideration.
+      GetDomainInfo(GURL("https://len.com")),
+  };
   const struct TestCase {
     const char* domain;
     const char* expected_suggested_domain;
-    bool expected_result;
+    const ComboSquattingType expected_type;
+    ;
   } kTestCases[] = {
       // Not Combo Squatting (CSQ).
-      {"google.com", "", false},
-      {"youtube.ca", "", false},
+      {"google.com", "", ComboSquattingType::kNone},
+      {"youtube.ca", "", ComboSquattingType::kNone},
 
       // Not CSQ, contains subdomains.
-      {"login.google.com", "", false},
+      {"login.google.com", "", ComboSquattingType::kNone},
 
       // Not CSQ, non registrable domains.
-      {"google-login.test", "", false},
+      {"google-login.test", "", ComboSquattingType::kNone},
 
       // CSQ with "-".
-      {"google-online.com", "google.com", true},
+      {"google-online.com", "google.com", ComboSquattingType::kHardCoded},
 
       // CSQ with more than one keyword (login, online) with "-".
-      {"google-login-online.com", "google.com", true},
+      {"google-login-online.com", "google.com", ComboSquattingType::kHardCoded},
 
       // CSQ with one keyword (online) and one random word (one) with "-".
-      {"one-sample-online.com", "sample.com", true},
+      {"one-sample-online.com", "sample.com", ComboSquattingType::kHardCoded},
 
       // Not CSQ, with a keyword (test) as TLD.
-      {"www.example.test", "", false},
+      {"www.example.test", "", ComboSquattingType::kNone},
 
       // CSQ with more than one brand (google, youtube) with "-".
-      {"google-youtube-account.com", "google.com", true},
+      {"google-youtube-account.com", "google.com",
+       ComboSquattingType::kHardCoded},
 
       // CSQ without separator.
-      {"loginsample.com", "sample.com", true},
+      {"loginsample.com", "sample.com", ComboSquattingType::kHardCoded},
 
       // Not CSQ with a keyword (ample) inside brand name (sample).
-      {"sample.com", "", false},
+      {"sample.com", "", ComboSquattingType::kNone},
 
       // Current version of the heuristic cannot flag this kind of CSQ
       // with a keyword (ample) inside brand name (sample) and as an added
       // keyword to the domain.
-      {"sample-ample.com", "", false},
+      {"sample-ample.com", "", ComboSquattingType::kNone},
 
       // CSQ with more than one keyword (account, online) without separator.
-      {"accountexampleonline.com", "example.com", true},
+      {"accountexampleonline.com", "example.com",
+       ComboSquattingType::kHardCoded},
 
       // CSQ with one keyword (login) and one random word (one) without "-".
-      {"oneyoutubelogin.com", "youtube.com", true},
+      {"oneyoutubelogin.com", "youtube.com", ComboSquattingType::kHardCoded},
 
       // Not CSQ, google is a public TLD.
-      {"online.google", "", false},
+      {"online.google", "", ComboSquattingType::kNone},
 
       // Not CSQ, brand name (vice) is part of keyword (service).
-      {"keyservices.com", "", false},
+      {"keyservices.com", "", ComboSquattingType::kNone},
+
+      // CSQ, brand name (engagedsite) is from engaged sites list.
+      {"engagedsite-login.com", "engagedsite.com",
+       ComboSquattingType::kSiteEngagement},
+
+      // Not CSQ, brand name (len) is from engaged sites list but it is short.
+      {"len-online.com", "", ComboSquattingType::kNone},
   };
   for (const TestCase& test_case : kTestCases) {
     auto navigated =
         GetDomainInfo(GURL(std::string(url::kHttpsScheme) +
                            url::kStandardSchemeSeparator + test_case.domain));
     std::string matched_domain;
-    bool result = IsComboSquatting(navigated, &matched_domain);
+    ComboSquattingType type =
+        GetComboSquattingType(navigated, kEngagedSites, &matched_domain);
     EXPECT_EQ(std::string(test_case.expected_suggested_domain), matched_domain);
-    EXPECT_EQ(test_case.expected_result, result);
+    EXPECT_EQ(test_case.expected_type, type);
   }
 }
\ No newline at end of file
diff --git a/components/omnibox/browser/omnibox_field_trial.cc b/components/omnibox/browser/omnibox_field_trial.cc
index c4712a2..c9e15056 100644
--- a/components/omnibox/browser/omnibox_field_trial.cc
+++ b/components/omnibox/browser/omnibox_field_trial.cc
@@ -907,6 +907,11 @@
     "RichAutocompletionAutocompleteShowAdditionalText",
     true);
 
+const base::FeatureParam<bool> kRichAutocompletionAdditionalTextWithParenthesis(
+    &omnibox::kRichAutocompletion,
+    "RichAutocompletionAdditionalTextWithParenthesis",
+    false);
+
 const base::FeatureParam<bool> kRichAutocompletionAutocompleteShortcutText(
     &omnibox::kRichAutocompletion,
     "RichAutocompletionAutocompleteShortcutText",
diff --git a/components/omnibox/browser/omnibox_field_trial.h b/components/omnibox/browser/omnibox_field_trial.h
index 7c7509e..918ae7f 100644
--- a/components/omnibox/browser/omnibox_field_trial.h
+++ b/components/omnibox/browser/omnibox_field_trial.h
@@ -563,6 +563,8 @@
     kRichAutocompletionAutocompleteNonPrefixMinChar;
 extern const base::FeatureParam<bool> kRichAutocompletionShowAdditionalText;
 extern const base::FeatureParam<bool>
+    kRichAutocompletionAdditionalTextWithParenthesis;
+extern const base::FeatureParam<bool>
     kRichAutocompletionAutocompleteShortcutText;
 extern const base::FeatureParam<int>
     kRichAutocompletionAutocompleteShortcutTextMinChar;
diff --git a/components/optimization_guide/core/hints_processing_util.cc b/components/optimization_guide/core/hints_processing_util.cc
index 0f62f13..2c0cae1 100644
--- a/components/optimization_guide/core/hints_processing_util.cc
+++ b/components/optimization_guide/core/hints_processing_util.cc
@@ -67,6 +67,8 @@
       return "HistoryClusters";
     case proto::OptimizationType::THANK_CREATOR_ELIGIBLE:
       return "ThankCreatorEligible";
+    case proto::OptimizationType::IBAN_AUTOFILL_BLOCKED:
+      return "IBANAutofillBlocked";
   }
 
   // The returned string is used to record histograms for the optimization type.
diff --git a/components/optimization_guide/proto/hints.proto b/components/optimization_guide/proto/hints.proto
index ffef327..5a0ece2d 100644
--- a/components/optimization_guide/proto/hints.proto
+++ b/components/optimization_guide/proto/hints.proto
@@ -163,6 +163,9 @@
   HISTORY_CLUSTERS = 23;
   // Determines if a page is eligible for 'Thank creator' functionality.
   THANK_CREATOR_ELIGIBLE = 24;
+  // This optimization provides information about hosts that are blocked for
+  // IBAN autofill.
+  IBAN_AUTOFILL_BLOCKED = 25;
 }
 
 // Presents semantics for how page load URLs should be matched.
diff --git a/components/policy/resources/policy_templates.json b/components/policy/resources/policy_templates.json
index e31a8c47..d5b2027 100644
--- a/components/policy/resources/policy_templates.json
+++ b/components/policy/resources/policy_templates.json
@@ -10254,6 +10254,16 @@
         'dynamic_refresh': True,
         'per_profile': True,
       },
+      'items': [
+        {
+          'value': True,
+          'caption': 'Enable policy atomic groups',
+        },
+        {
+          'value': False,
+          'caption': 'Disable policy atomic groups',
+        },
+      ],
       'example_value': True,
       'id': 584,
       'caption': '''Enables the concept of policy atomic groups''',
@@ -10707,6 +10717,16 @@
         'dynamic_refresh': True,
         'per_profile': True,
       },
+      'items': [
+        {
+          'value': True,
+          'caption': 'Enable submission of documents to <ph name="CLOUD_PRINT_NAME">Google Cloud Print</ph>',
+        },
+        {
+          'value': False,
+          'caption': 'Disable submission of documents to <ph name="CLOUD_PRINT_NAME">Google Cloud Print</ph>',
+        },
+      ],
       'deprecated': True,
       'example_value': True,
       'id': 109,
@@ -10762,6 +10782,16 @@
         'dynamic_refresh': False,
         'per_profile': True,
       },
+      'items': [
+        {
+          'value': True,
+          'caption': 'Disable print preview',
+        },
+        {
+          'value': False,
+          'caption': 'Enable print preview',
+        },
+      ],
       'example_value': False,
       'id': 117,
       'caption': '''Disable Print Preview''',
@@ -10781,6 +10811,21 @@
         'dynamic_refresh': True,
         'per_profile': True,
       },
+      'items': [
+        {
+          'value': True,
+          'caption': 'Show headers and footers in print preview',
+        },
+        {
+          'value': False,
+          'caption': 'Hide headers and footers in print preview',
+        },
+        {
+          'value': None,
+          'caption': 'Allow the user to decide',
+        },
+      ],
+      'default': None,
       'example_value': False,
       'id': 480,
       'caption': '''Print Headers and Footers''',
@@ -10859,6 +10904,16 @@
         'dynamic_refresh': True,
         'per_profile': False,
       },
+      'items': [
+        {
+          'value': True,
+          'caption': 'Allow online <ph name="OCSP_CRL_LABEL">OCSP/CRL</ph> checks to be performed',
+        },
+        {
+          'value': False,
+          'caption': 'Prevent online <ph name="OCSP_CRL_LABEL">OCSP/CRL</ph> checks from being performed',
+        },
+      ],
       'example_value': False,
       'id': 129,
       'caption': '''Enable online OCSP/CRL checks''',
@@ -10908,6 +10963,16 @@
         'dynamic_refresh': True,
         'per_profile': False,
       },
+      'items': [
+        {
+          'value': True,
+          'caption': 'Allow SHA-1 signed certificates issued by local trust anchors',
+        },
+        {
+          'value': False,
+          'caption': 'Disallow SHA-1 signed certificates',
+        },
+      ],
       'deprecated': True,
       'example_value': False,
       'id': 340,
@@ -10929,6 +10994,16 @@
         'dynamic_refresh': True,
         'per_profile': False,
       },
+      'items': [
+        {
+          'value': True,
+          'caption': 'Allow certificates lacking a subjectAlternativeName extension when issued by local trust anchors',
+        },
+        {
+          'value': False,
+          'caption': 'Disallow certificates lacking a subjectAlternativeName extension',
+        },
+      ],
       'deprecated': True,
       'example_value': False,
       'id': 366,
diff --git a/components/reporting/client/report_queue_impl.cc b/components/reporting/client/report_queue_impl.cc
index 9f564c8..d39544c 100644
--- a/components/reporting/client/report_queue_impl.cc
+++ b/components/reporting/client/report_queue_impl.cc
@@ -11,18 +11,15 @@
 
 #include "base/bind.h"
 #include "base/callback.h"
-#include "base/json/json_writer.h"
+#include "base/check.h"
 #include "base/memory/ptr_util.h"
-#include "base/memory/ref_counted.h"
 #include "base/memory/scoped_refptr.h"
 #include "base/notreached.h"
 #include "base/sequence_checker.h"
-#include "base/strings/strcat.h"
 #include "base/task/bind_post_task.h"
 #include "base/task/task_traits.h"
 #include "base/task/thread_pool.h"
 #include "base/time/time.h"
-#include "base/values.h"
 #include "components/reporting/client/report_queue_configuration.h"
 #include "components/reporting/proto/synced/record.pb.h"
 #include "components/reporting/proto/synced/record_constants.pb.h"
@@ -179,12 +176,18 @@
               return;
             }
             DCHECK_CALLED_ON_VALID_SEQUENCE(self->sequence_checker_);
-            if (!self->report_queue_) {
+            if (!self->status_or_report_queue_.has_value()) {
               std::move(callback).Run(Status(error::FAILED_PRECONDITION,
                                              "ReportQueue is not ready yet."));
               return;
             }
-            self->report_queue_->Flush(priority, std::move(callback));
+            if (!self->status_or_report_queue_->ok()) {
+              std::move(callback).Run(self->status_or_report_queue_->status());
+              return;
+            }
+            const std::unique_ptr<ReportQueue>& report_queue =
+                self->status_or_report_queue_->ValueOrDie();
+            report_queue->Flush(priority, std::move(callback));
           },
           priority, std::move(callback), weak_ptr_factory_.GetWeakPtr()));
 }
@@ -207,18 +210,23 @@
     EnqueueCallback callback,
     RecordProducer record_producer) const {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  if (!report_queue_) {
-    // Queue is not ready yet, store the record in the memory
-    // queue.
+  if (!status_or_report_queue_.has_value()) {
+    // Queue is not ready yet, store the record in the memory queue.
     pending_record_producers_.emplace(std::move(record_producer), priority);
     std::move(callback).Run(Status::StatusOK());
     return;
   }
-  // Queue is ready. If memory queue is empty, just forward the
-  // record.
+  if (!status_or_report_queue_->ok()) {
+    // Queue creation failed.
+    std::move(callback).Run(status_or_report_queue_->status());
+    return;
+  }
+  // Queue is ready. If memory queue is empty, just forward the record.
   if (pending_record_producers_.empty()) {
-    report_queue_->AddProducedRecord(std::move(record_producer), priority,
-                                     std::move(callback));
+    const std::unique_ptr<ReportQueue>& report_queue =
+        status_or_report_queue_->ValueOrDie();
+    report_queue->AddProducedRecord(std::move(record_producer), priority,
+                                    std::move(callback));
     return;
   }
   // If memory queue is not empty, attach the new record at the
@@ -230,21 +238,24 @@
 void SpeculativeReportQueueImpl::EnqueuePendingRecordProducers(
     EnqueueCallback callback) const {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  DCHECK(report_queue_);
+  DCHECK(status_or_report_queue_.has_value());
+  DCHECK(status_or_report_queue_->ok());
   if (pending_record_producers_.empty()) {
     std::move(callback).Run(Status::StatusOK());
     return;
   }
+  const std::unique_ptr<ReportQueue>& report_queue =
+      status_or_report_queue_->ValueOrDie();
 
   auto head = std::move(pending_record_producers_.front());
   pending_record_producers_.pop();
   if (pending_record_producers_.empty()) {
     // Last of the pending records.
-    report_queue_->AddProducedRecord(std::move(head.record_producer),
-                                     head.record_priority, std::move(callback));
+    report_queue->AddProducedRecord(std::move(head.record_producer),
+                                    head.record_priority, std::move(callback));
     return;
   }
-  report_queue_->AddProducedRecord(
+  report_queue->AddProducedRecord(
       std::move(head.record_producer), head.record_priority,
       base::BindPostTask(
           sequenced_task_runner_,
@@ -278,34 +289,36 @@
             if (!speculative_queue) {
               return;  // Speculative queue was destructed in a meantime.
             }
-            if (!actual_queue_result.ok()) {
-              return;  // Actual queue creation failed.
-            }
             // Set actual queue for the speculative queue to use
             // (asynchronously).
             speculative_queue->AttachActualQueue(
-                std::move(actual_queue_result.ValueOrDie()));
+                std::move(std::move(actual_queue_result)));
           },
           weak_ptr_factory_.GetWeakPtr()));
 }
 
 void SpeculativeReportQueueImpl::AttachActualQueue(
-    std::unique_ptr<ReportQueue> actual_queue) {
+    StatusOr<std::unique_ptr<ReportQueue>> status_or_actual_queue) {
   sequenced_task_runner_->PostTask(
       FROM_HERE,
       base::BindOnce(
           [](base::WeakPtr<SpeculativeReportQueueImpl> self,
-             std::unique_ptr<ReportQueue> actual_queue) {
+             StatusOr<std::unique_ptr<ReportQueue>> status_or_actual_queue) {
             if (!self) {
               return;
             }
             DCHECK_CALLED_ON_VALID_SEQUENCE(self->sequence_checker_);
-            if (self->report_queue_) {
+            if (self->status_or_report_queue_.has_value()) {
               // Already attached, do nothing.
               return;
             }
-            self->report_queue_ = std::move(actual_queue);
-            if (!self->pending_record_producers_.empty()) {
+            self->status_or_report_queue_ = std::move(status_or_actual_queue);
+            // TODO(b/239583016): remove the ok status check once the enqueue
+            // callbacks are stored along with the records in a pending queue
+            // instead of only the records, and run the callbacks with the
+            // failure status if creation failed.
+            if (self->status_or_report_queue_->ok() &&
+                !self->pending_record_producers_.empty()) {
               self->EnqueuePendingRecordProducers(
                   base::BindOnce([](Status enqueue_status) {
                     if (!enqueue_status.ok()) {
@@ -315,7 +328,7 @@
                   }));
             }
           },
-          weak_ptr_factory_.GetWeakPtr(), std::move(actual_queue)));
+          weak_ptr_factory_.GetWeakPtr(), std::move(status_or_actual_queue)));
 }
 
 }  // namespace reporting
diff --git a/components/reporting/client/report_queue_impl.h b/components/reporting/client/report_queue_impl.h
index 3cee147..47950e7 100644
--- a/components/reporting/client/report_queue_impl.h
+++ b/components/reporting/client/report_queue_impl.h
@@ -11,12 +11,10 @@
 #include <utility>
 
 #include "base/callback.h"
-#include "base/memory/ref_counted.h"
 #include "base/memory/scoped_refptr.h"
 #include "base/memory/weak_ptr.h"
 #include "base/sequence_checker.h"
 #include "base/task/sequenced_task_runner.h"
-#include "base/values.h"
 #include "components/reporting/client/report_queue.h"
 #include "components/reporting/client/report_queue_configuration.h"
 #include "components/reporting/proto/synced/record.pb.h"
@@ -24,7 +22,7 @@
 #include "components/reporting/storage/storage_module_interface.h"
 #include "components/reporting/util/status.h"
 #include "components/reporting/util/statusor.h"
-#include "third_party/protobuf/src/google/protobuf/message_lite.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
 
 namespace reporting {
 
@@ -92,7 +90,8 @@
 
   // Substitutes actual queue to the speculative, when ready.
   // Initiates processesing of all pending records.
-  void AttachActualQueue(std::unique_ptr<ReportQueue> actual_queue);
+  void AttachActualQueue(
+      StatusOr<std::unique_ptr<ReportQueue>> status_or_actual_queue);
 
  private:
   // Moveable, non-copyable struct holding a pending record producer for the
@@ -132,8 +131,9 @@
   const scoped_refptr<base::SequencedTaskRunner> sequenced_task_runner_;
   SEQUENCE_CHECKER(sequence_checker_);
 
-  // Actual |ReportQueue|, once created.
-  std::unique_ptr<ReportQueue> report_queue_
+  // Creation status of |ReportQueue| and actual |ReportQueue| if successfully
+  // created.
+  absl::optional<StatusOr<std::unique_ptr<ReportQueue>>> status_or_report_queue_
       GUARDED_BY_CONTEXT(sequence_checker_);
 
   // Queue of the pending record producers, collected before actual queue has
diff --git a/components/reporting/client/report_queue_impl_unittest.cc b/components/reporting/client/report_queue_impl_unittest.cc
index d369f228..7bfa8b29 100644
--- a/components/reporting/client/report_queue_impl_unittest.cc
+++ b/components/reporting/client/report_queue_impl_unittest.cc
@@ -257,6 +257,48 @@
   EXPECT_EQ(test_storage_module()->record().data(), kTestString);
 }
 
+TEST_F(ReportQueueImplTest, SpeculativeQueueMultipleRecordsAfterCreation) {
+  constexpr char kTestString1[] = "record1";
+  constexpr char kTestString2[] = "record2";
+  auto speculative_report_queue = SpeculativeReportQueueImpl::Create();
+
+  speculative_report_queue->AttachActualQueue(std::move(report_queue_));
+  // Let everything ongoing to finish.
+  task_environment_.RunUntilIdle();
+
+  test::TestEvent<Status> test_event1;
+  speculative_report_queue->Enqueue(kTestString1, Priority::IMMEDIATE,
+                                    test_event1.cb());
+  const auto result1 = test_event1.result();
+  ASSERT_TRUE(result1.ok());
+  EXPECT_EQ(test_storage_module()->priority(), Priority::IMMEDIATE);
+  EXPECT_EQ(test_storage_module()->record().data(), kTestString1);
+
+  test::TestEvent<Status> test_event2;
+  speculative_report_queue->Enqueue(kTestString2, Priority::SLOW_BATCH,
+                                    test_event2.cb());
+  const auto result2 = test_event2.result();
+  ASSERT_TRUE(result2.ok());
+  EXPECT_EQ(test_storage_module()->priority(), Priority::SLOW_BATCH);
+  EXPECT_EQ(test_storage_module()->record().data(), kTestString2);
+}
+
+TEST_F(ReportQueueImplTest, SpeculativeQueueCreationFailed) {
+  constexpr char kTestString[] = "record";
+  auto speculative_report_queue = SpeculativeReportQueueImpl::Create();
+
+  auto attach_cb = speculative_report_queue->PrepareToAttachActualQueue();
+  std::move(attach_cb).Run(Status(error::UNKNOWN, "error msg"));
+  task_environment_.RunUntilIdle();
+
+  test::TestEvent<Status> test_event;
+  speculative_report_queue->Enqueue(kTestString, Priority::IMMEDIATE,
+                                    test_event.cb());
+  const auto result = test_event.result();
+  ASSERT_FALSE(result.ok());
+  EXPECT_EQ(result.code(), error::UNKNOWN);
+}
+
 TEST_F(ReportQueueImplTest, OverlappingStringRecords) {
   constexpr char kTestString1[] = "record1";
   constexpr char kTestString2[] = "record2";
@@ -358,6 +400,21 @@
   EXPECT_EQ(result.error_code(), error::FAILED_PRECONDITION);
 }
 
+TEST_F(ReportQueueImplTest, FlushFailedSpeculativeReportQueue) {
+  test::TestEvent<Status> event;
+
+  auto speculative_report_queue = SpeculativeReportQueueImpl::Create();
+  auto attach_cb = speculative_report_queue->PrepareToAttachActualQueue();
+  std::move(attach_cb).Run(Status(error::UNKNOWN, "error msg"));
+  task_environment_.RunUntilIdle();
+
+  speculative_report_queue->Flush(priority_, event.cb());
+
+  const auto result = event.result();
+  ASSERT_FALSE(result.ok());
+  EXPECT_EQ(result.error_code(), error::UNKNOWN);
+}
+
 TEST_F(ReportQueueImplTest, AsyncProcessingReportQueue) {
   auto mock_queue = std::make_unique<MockReportQueue>();
   EXPECT_CALL(*mock_queue, AddProducedRecord)
diff --git a/components/safe_browsing/content/browser/base_ui_manager.cc b/components/safe_browsing/content/browser/base_ui_manager.cc
index bee0584..f19a46f 100644
--- a/components/safe_browsing/content/browser/base_ui_manager.cc
+++ b/components/safe_browsing/content/browser/base_ui_manager.cc
@@ -21,6 +21,7 @@
 #include "content/public/browser/navigation_entry.h"
 #include "content/public/browser/web_contents.h"
 #include "content/public/browser/web_contents_user_data.h"
+#include "services/network/public/mojom/fetch_api.mojom-shared.h"
 
 using content::BrowserThread;
 using content::NavigationEntry;
@@ -207,12 +208,19 @@
 
 void BaseUIManager::DisplayBlockingPage(const UnsafeResource& resource) {
   DCHECK_CURRENTLY_ON(BrowserThread::UI);
-  if (resource.is_subresource && !resource.is_subframe) {
+  bool is_frame = resource.is_subframe ||
+                  resource.request_destination ==
+                      network::mojom::RequestDestination::kEmbed ||
+                  resource.request_destination ==
+                      network::mojom::RequestDestination::kObject;
+  if (resource.is_subresource && !is_frame) {
     // Sites tagged as serving Unwanted Software should only show a warning for
-    // main-frame or sub-frame resource. Similar warning restrictions should be
-    // applied to malware sites tagged as "landing sites" (see "Types of
-    // Malware sites" under
-    // https://developers.google.com/safe-browsing/developers_guide_v3#UserWarnings).
+    // main-frame or frame-like (subframe, embed, object) resource. Similar
+    // warning restrictions should be applied to malware sites tagged as
+    // "landing sites" (see "Types of Malware sites" under
+    // https://developers.google.com/safe-browsing/v4/metadata#malware-sites).
+    // This is to avoid false positives on benign sites that load resources
+    // from landing sites.
     if (resource.threat_type == SB_THREAT_TYPE_URL_UNWANTED ||
         (resource.threat_type == SB_THREAT_TYPE_URL_MALWARE &&
          resource.threat_metadata.threat_pattern_type ==
diff --git a/components/safe_browsing/core/browser/db/BUILD.gn b/components/safe_browsing/core/browser/db/BUILD.gn
index 9a8e6b8..b66f4347 100644
--- a/components/safe_browsing/core/browser/db/BUILD.gn
+++ b/components/safe_browsing/core/browser/db/BUILD.gn
@@ -79,6 +79,7 @@
   ]
   deps = [
     ":database_manager",
+    ":util",
     ":v4_protocol_manager_util",
     "//base:base",
     "//net",
diff --git a/components/safe_browsing/core/browser/db/fake_database_manager.cc b/components/safe_browsing/core/browser/db/fake_database_manager.cc
index 24fab78..f0ca9d9 100644
--- a/components/safe_browsing/core/browser/db/fake_database_manager.cc
+++ b/components/safe_browsing/core/browser/db/fake_database_manager.cc
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 #include "components/safe_browsing/core/browser/db/fake_database_manager.h"
+#include "components/safe_browsing/core/browser/db/util.h"
 
 namespace safe_browsing {
 
@@ -20,6 +21,12 @@
   dangerous_urls_[dangerous_url] = threat_type;
 }
 
+void FakeSafeBrowsingDatabaseManager::AddDangerousUrlPattern(
+    const GURL& dangerous_url,
+    ThreatPatternType pattern_type) {
+  dangerous_patterns_[dangerous_url] = pattern_type;
+}
+
 void FakeSafeBrowsingDatabaseManager::ClearDangerousUrl(
     const GURL& dangerous_url) {
   dangerous_urls_.erase(dangerous_url);
@@ -46,10 +53,16 @@
   if (result_threat_type == SB_THREAT_TYPE_SAFE)
     return true;
 
+  ThreatPatternType pattern_type = ThreatPatternType::NONE;
+  const auto it1 = dangerous_patterns_.find(url);
+  if (it1 != dangerous_patterns_.end()) {
+    pattern_type = it1->second;
+  }
+
   io_task_runner()->PostTask(
       FROM_HERE,
       base::BindOnce(&FakeSafeBrowsingDatabaseManager::CheckBrowseURLAsync, url,
-                     result_threat_type, client));
+                     result_threat_type, pattern_type, client));
   return false;
 }
 
@@ -98,9 +111,11 @@
 void FakeSafeBrowsingDatabaseManager::CheckBrowseURLAsync(
     GURL url,
     SBThreatType result_threat_type,
+    ThreatPatternType pattern_type,
     Client* client) {
-  client->OnCheckBrowseUrlResult(url, result_threat_type,
-                                 safe_browsing::ThreatMetadata());
+  ThreatMetadata metadata;
+  metadata.threat_pattern_type = pattern_type;
+  client->OnCheckBrowseUrlResult(url, result_threat_type, metadata);
 }
 
 // static
diff --git a/components/safe_browsing/core/browser/db/fake_database_manager.h b/components/safe_browsing/core/browser/db/fake_database_manager.h
index cca57095..83e2ddee 100644
--- a/components/safe_browsing/core/browser/db/fake_database_manager.h
+++ b/components/safe_browsing/core/browser/db/fake_database_manager.h
@@ -7,6 +7,7 @@
 
 #include "base/containers/flat_map.h"
 #include "components/safe_browsing/core/browser/db/test_database_manager.h"
+#include "components/safe_browsing/core/browser/db/util.h"
 #include "url/gurl.h"
 
 namespace safe_browsing {
@@ -20,6 +21,8 @@
       scoped_refptr<base::SequencedTaskRunner> io_task_runner);
 
   void AddDangerousUrl(const GURL& dangerous_url, SBThreatType threat_type);
+  void AddDangerousUrlPattern(const GURL& dangerous_url,
+                              ThreatPatternType pattern_type);
   void ClearDangerousUrl(const GURL& dangerous_url);
 
   // TestSafeBrowsingDatabaseManager implementation:
@@ -42,12 +45,14 @@
 
   static void CheckBrowseURLAsync(GURL url,
                                   SBThreatType result_threat_type,
+                                  ThreatPatternType pattern_type,
                                   Client* client);
   static void CheckDownloadURLAsync(const std::vector<GURL>& url_chain,
                                     SBThreatType result_threat_type,
                                     Client* client);
 
   base::flat_map<GURL, SBThreatType> dangerous_urls_;
+  base::flat_map<GURL, ThreatPatternType> dangerous_patterns_;
 };
 
 }  // namespace safe_browsing
diff --git a/components/user_education/common/tutorial_description.h b/components/user_education/common/tutorial_description.h
index bbecc1aa..3dbc494 100644
--- a/components/user_education/common/tutorial_description.h
+++ b/components/user_education/common/tutorial_description.h
@@ -40,6 +40,10 @@
   // Records whether, when an IPH offered the tutorial, the user opted into
   // seeing the tutorial or not.
   virtual void RecordIphLinkClicked(bool value) = 0;
+
+  // Records whether, when an IPH offered the tutorial, the user opted into
+  // seeing the tutorial or not.
+  virtual void RecordStartedFromWhatsNewPage(bool value) = 0;
 };
 
 namespace internal {
@@ -55,8 +59,10 @@
                         ".Completion"),
         aborted_name_(kTutorialHistogramPrefix + histogram_name_ +
                       ".AbortStep"),
-        link_clicked_name_(kTutorialHistogramPrefix + histogram_name_ +
-                           ".IPHLinkClicked"),
+        iph_link_clicked_name_(kTutorialHistogramPrefix + histogram_name_ +
+                               ".IPHLinkClicked"),
+        whats_new_page_name_(kTutorialHistogramPrefix + histogram_name_ +
+                             ".StartedFromWhatsNewPage"),
         max_steps_(max_steps) {}
   ~TutorialHistogramsImpl() override = default;
 
@@ -70,14 +76,19 @@
   }
 
   void RecordIphLinkClicked(bool value) override {
-    UMA_HISTOGRAM_BOOLEAN(link_clicked_name_, value);
+    UMA_HISTOGRAM_BOOLEAN(iph_link_clicked_name_, value);
+  }
+
+  void RecordStartedFromWhatsNewPage(bool value) override {
+    UMA_HISTOGRAM_BOOLEAN(whats_new_page_name_, value);
   }
 
  private:
   const std::string histogram_name_;
   const std::string completed_name_;
   const std::string aborted_name_;
-  const std::string link_clicked_name_;
+  const std::string iph_link_clicked_name_;
+  const std::string whats_new_page_name_;
   const int max_steps_;
 };
 
diff --git a/components/user_education/common/tutorial_service.cc b/components/user_education/common/tutorial_service.cc
index 50a839e..7ed0b6b 100644
--- a/components/user_education/common/tutorial_service.cc
+++ b/components/user_education/common/tutorial_service.cc
@@ -77,6 +77,16 @@
     description->histograms->RecordIphLinkClicked(iph_link_was_clicked);
 }
 
+void TutorialService::LogStartedFromWhatsNewPage(TutorialIdentifier id,
+                                                 bool success) {
+  TutorialDescription* description =
+      tutorial_registry_->GetTutorialDescription(id);
+  CHECK(description);
+
+  if (description->histograms)
+    description->histograms->RecordStartedFromWhatsNewPage(success);
+}
+
 bool TutorialService::RestartTutorial() {
   DCHECK(running_tutorial_ && running_tutorial_creation_params_);
   base::AutoReset<bool> resetter(&is_restarting_, true);
diff --git a/components/user_education/common/tutorial_service.h b/components/user_education/common/tutorial_service.h
index 5e8d84c..86e8097 100644
--- a/components/user_education/common/tutorial_service.h
+++ b/components/user_education/common/tutorial_service.h
@@ -55,6 +55,8 @@
       AbortedCallback aborted_callback = base::DoNothing());
 
   void LogIPHLinkClicked(TutorialIdentifier id, bool iph_link_was_clicked);
+  virtual void LogStartedFromWhatsNewPage(TutorialIdentifier id,
+                                          bool iph_link_was_clicked);
 
   // Uses the stored tutorial creation params to restart a tutorial. Replaces
   // the current_tutorial with a newly generated tutorial.
diff --git a/components/user_education/webui/help_bubble_handler.cc b/components/user_education/webui/help_bubble_handler.cc
index 2dd59490..2ff377a 100644
--- a/components/user_education/webui/help_bubble_handler.cc
+++ b/components/user_education/webui/help_bubble_handler.cc
@@ -126,6 +126,11 @@
   mojom_params->close_button_alt_text =
       base::UTF16ToUTF8(data.params->close_button_alt_text);
   mojom_params->position = HelpBubbleArrowToPosition(data.params->arrow);
+  if (data.params->progress) {
+    mojom_params->progress = help_bubble::mojom::Progress::New();
+    mojom_params->progress->current = data.params->progress->first;
+    mojom_params->progress->total = data.params->progress->second;
+  }
   if (!data.params->title_text.empty()) {
     mojom_params->title_text = base::UTF16ToUTF8(data.params->title_text);
   }
diff --git a/components/user_education/webui/help_bubble_handler_unittest.cc b/components/user_education/webui/help_bubble_handler_unittest.cc
index 3d9c9a4d..f55c2db 100644
--- a/components/user_education/webui/help_bubble_handler_unittest.cc
+++ b/components/user_education/webui/help_bubble_handler_unittest.cc
@@ -92,6 +92,11 @@
   EXPECT_EQ(expected->native_identifier, arg->native_identifier);
   EXPECT_EQ(expected->position, arg->position);
   EXPECT_EQ(expected->title_text, arg->title_text);
+  EXPECT_EQ(!!expected->progress, !!arg->progress);
+  if (expected->progress && arg->progress) {
+    EXPECT_EQ(expected->progress->current, arg->progress->current);
+    EXPECT_EQ(expected->progress->total, arg->progress->total);
+  }
 
   EXPECT_EQ(expected->buttons.size(), arg->buttons.size());
   if (expected->buttons.size() == arg->buttons.size()) {
@@ -239,6 +244,59 @@
   EXPECT_FALSE(help_bubble->is_open());
 }
 
+TEST_F(HelpBubbleHandlerTest, ShowHelpBubbleWithButtonsAndProgress) {
+  handler()->HelpBubbleAnchorVisibilityChanged(
+      kHelpBubbleHandlerTestElementIdentifier.GetName(), true);
+  auto* const element =
+      ui::ElementTracker::GetElementTracker()->GetUniqueElement(
+          kHelpBubbleHandlerTestElementIdentifier, test_handler_->context());
+  ASSERT_NE(nullptr, element);
+  HelpBubbleParams params;
+  params.body_text = u"Help bubble body.";
+  params.close_button_alt_text = u"Close button alt text.";
+  params.arrow = HelpBubbleArrow::kTopCenter;
+  params.progress = std::make_pair(1, 3);
+
+  HelpBubbleButtonParams button;
+  button.is_default = true;
+  button.text = u"button1";
+  params.buttons.emplace_back(std::move(button));
+
+  // Check the parameters passed to the ShowHelpBubble mojo method.
+  help_bubble::mojom::HelpBubbleParamsPtr expected =
+      help_bubble::mojom::HelpBubbleParams::New();
+  expected->native_identifier = element->identifier().GetName();
+  expected->body_text = base::UTF16ToUTF8(params.body_text);
+  expected->close_button_alt_text =
+      base::UTF16ToUTF8(params.close_button_alt_text);
+  expected->position = help_bubble::mojom::HelpBubblePosition::BELOW;
+
+  auto expected_button = help_bubble::mojom::HelpBubbleButtonParams::New();
+  expected_button->text = "button1";
+  expected_button->is_default = true;
+  expected->buttons.emplace_back(std::move(expected_button));
+
+  auto expected_progress = help_bubble::mojom::Progress::New();
+  expected_progress->current = 1;
+  expected_progress->total = 3;
+  expected->progress = std::move(expected_progress);
+
+  EXPECT_CALL(test_handler_->mock(),
+              ShowHelpBubble(MatchesHelpBubbleParams(expected.get())));
+  auto help_bubble = help_bubble_factory_registry_.CreateHelpBubble(
+      element, std::move(params));
+
+  EXPECT_TRUE(help_bubble);
+  EXPECT_TRUE(help_bubble->is_open());
+
+  EXPECT_CALL(
+      test_handler_->mock(),
+      HideHelpBubble(kHelpBubbleHandlerTestElementIdentifier.GetName()));
+  EXPECT_TRUE(help_bubble->Close());
+
+  EXPECT_FALSE(help_bubble->is_open());
+}
+
 TEST_F(HelpBubbleHandlerTest, FocusHelpBubble) {
   handler()->HelpBubbleAnchorVisibilityChanged(
       kHelpBubbleHandlerTestElementIdentifier.GetName(), true);
diff --git a/components/webapps/browser/android/shortcut_info.cc b/components/webapps/browser/android/shortcut_info.cc
index 5dbb3fd..7acb828d 100644
--- a/components/webapps/browser/android/shortcut_info.cc
+++ b/components/webapps/browser/android/shortcut_info.cc
@@ -149,7 +149,7 @@
   // Set the screenshots urls based on the screenshots in the manifest, if any.
   screenshot_urls.clear();
   for (const auto& screenshot : manifest.screenshots)
-    screenshot_urls.push_back(screenshot.src);
+    screenshot_urls.push_back(screenshot->image.src);
 
   if (manifest.share_target) {
     share_target = ShareTarget();
diff --git a/components/webapps/browser/features.cc b/components/webapps/browser/features.cc
index 4216d25..dd89664 100644
--- a/components/webapps/browser/features.cc
+++ b/components/webapps/browser/features.cc
@@ -53,6 +53,10 @@
 const base::Feature kSkipServiceWorkerCheckInstallOnly{
     "SkipServiceWorkerCheckInstallOnly", base::FEATURE_DISABLED_BY_DEFAULT};
 
+// Enables showing a detailed install dialog for user installs.
+const base::Feature kDesktopPWAsDetailedInstallDialog{
+    "DesktopPWAsDetailedInstallDialog", base::FEATURE_DISABLED_BY_DEFAULT};
+
 bool SkipBannerServiceWorkerCheck() {
   return base::FeatureList::IsEnabled(kSkipServiceWorkerCheckAll);
 }
diff --git a/components/webapps/browser/features.h b/components/webapps/browser/features.h
index 0304beb..04edc6a 100644
--- a/components/webapps/browser/features.h
+++ b/components/webapps/browser/features.h
@@ -27,6 +27,7 @@
 extern const base::Feature kCreateShortcutIgnoresManifest;
 extern const base::Feature kSkipServiceWorkerCheckAll;
 extern const base::Feature kSkipServiceWorkerCheckInstallOnly;
+extern const base::Feature kDesktopPWAsDetailedInstallDialog;
 
 bool SkipBannerServiceWorkerCheck();
 bool SkipInstallServiceWorkerCheck();
diff --git a/components/webapps/browser/installable/installable_manager.cc b/components/webapps/browser/installable/installable_manager.cc
index 50db3ef..8af8c3d6 100644
--- a/components/webapps/browser/installable/installable_manager.cc
+++ b/components/webapps/browser/installable/installable_manager.cc
@@ -16,6 +16,7 @@
 #include "base/threading/sequenced_task_runner_handle.h"
 #include "build/build_config.h"
 #include "components/security_state/core/security_state.h"
+#include "components/webapps/browser/features.h"
 #include "components/webapps/browser/installable/installable_metrics.h"
 #include "components/webapps/browser/webapps_client.h"
 #include "components/webapps/common/constants.h"
@@ -36,6 +37,7 @@
 #include "third_party/blink/public/common/manifest/manifest_util.h"
 #include "third_party/blink/public/common/storage_key/storage_key.h"
 #include "third_party/blink/public/mojom/manifest/display_mode.mojom.h"
+#include "third_party/skia/include/core/SkBitmap.h"
 #include "url/origin.h"
 
 #if BUILDFLAG(IS_ANDROID)
@@ -186,7 +188,7 @@
            (display_mode == blink::mojom::DisplayMode::kBorderless &&
             base::FeatureList::IsEnabled(blink::features::kWebAppBorderless)) ||
            (display_mode == blink::mojom::DisplayMode::kTabbed &&
-            base::FeatureList::IsEnabled(features::kDesktopPWAsTabStrip)));
+            base::FeatureList::IsEnabled(::features::kDesktopPWAsTabStrip)));
 }
 
 void OnDidCompleteGetAllErrors(
@@ -602,7 +604,12 @@
                           IconUsage::kPrimary);
   } else if (params.fetch_screenshots && !screenshots_downloading_ &&
              !is_screenshots_fetch_complete_) {
-    CheckAndFetchScreenshots();
+    if (base::FeatureList::IsEnabled(
+            webapps::features::kDesktopPWAsDetailedInstallDialog)) {
+      CheckAndFetchScreenshots();
+    } else {
+      CheckAndFetchScreenshots(/*check_platform=*/false);
+    }
   } else if (params.has_worker && !worker_->fetched) {
     CheckServiceWorker();
   } else if (params.valid_splash_icon && params.prefer_maskable_icon &&
@@ -887,7 +894,7 @@
   WorkOnTask();
 }
 
-void InstallableManager::CheckAndFetchScreenshots() {
+void InstallableManager::CheckAndFetchScreenshots(bool check_platform) {
   DCHECK(!blink::IsEmptyManifest(manifest()));
   DCHECK(!is_screenshots_fetch_complete_);
 
@@ -896,32 +903,50 @@
   int num_of_screenshots = 0;
 
   for (const auto& url : manifest().screenshots) {
-    if (++num_of_screenshots > kMaximumNumOfScreenshots)
-      break;
-    // A screenshot URL that's in the map is already taken care of.
-    if (downloaded_screenshots_.count(url.src) > 0)
+#if BUILDFLAG(IS_ANDROID)
+    auto reject_platform = blink::mojom::ManifestScreenshot::Platform::kWide;
+#else
+    auto reject_platform = blink::mojom::ManifestScreenshot::Platform::kNarrow;
+#endif  // BUILDFLAG(IS_ANDROID)
+    if (check_platform && url->platform == reject_platform)
       continue;
 
-    int ideal_size_in_px = url.sizes.empty() ? kMinimumScreenshotSizeInPx
-                                             : std::max(url.sizes[0].width(),
-                                                        url.sizes[0].height());
+    if (++num_of_screenshots > kMaximumNumOfScreenshots)
+      break;
+
+    // A screenshot URL that's in the map is already taken care of.
+    if (downloaded_screenshots_.count(url->image.src) > 0)
+      continue;
+
+    int ideal_size_in_px = url->image.sizes.empty()
+                               ? kMinimumScreenshotSizeInPx
+                               : std::max(url->image.sizes[0].width(),
+                                          url->image.sizes[0].height());
     // Do not pass in a maximum icon size so that screenshots larger than
     // kMaximumScreenshotSizeInPx are not downscaled to the maximum size by
     // `ManifestIconDownloader::Download`. Screenshots with size larger than
     // kMaximumScreenshotSizeInPx get filtered out by OnScreenshotFetched.
     bool can_download = content::ManifestIconDownloader::Download(
-        GetWebContents(), url.src, ideal_size_in_px, kMinimumScreenshotSizeInPx,
+        GetWebContents(), url->image.src, ideal_size_in_px,
+        kMinimumScreenshotSizeInPx,
         /*maximum_icon_size_in_px=*/0,
         base::BindOnce(&InstallableManager::OnScreenshotFetched,
-                       weak_factory_.GetWeakPtr(), url.src),
+                       weak_factory_.GetWeakPtr(), url->image.src),
         /*square_only=*/false);
     if (can_download)
       ++screenshots_downloading_;
   }
 
   if (!screenshots_downloading_) {
-    is_screenshots_fetch_complete_ = true;
-    WorkOnTask();
+    // If there is no screenshot that matches all the criteria, populate again
+    // without checking platform to fallback to screenshots with mismatching
+    // platform instead of showing nothing.
+    if (screenshots_.size() == 0 && check_platform) {
+      CheckAndFetchScreenshots(/*check_platform=*/false);
+    } else {
+      is_screenshots_fetch_complete_ = true;
+      WorkOnTask();
+    }
   }
 }
 
@@ -943,7 +968,7 @@
       if (++num_of_screenshots > kMaximumNumOfScreenshots)
         break;
 
-      auto iter = downloaded_screenshots_.find(url.src);
+      auto iter = downloaded_screenshots_.find(url->image.src);
       if (iter == downloaded_screenshots_.end())
         continue;
 
@@ -953,7 +978,6 @@
         continue;
       }
 
-      // TODO(crbug.com/1146450): Filter out screenshots by platform.
       // Screenshots must have the same aspect ratio. Cross-multiplying
       // dimensions checks portrait vs landscape mode (1:2 vs 2:1 for instance).
       if (screenshots_.size() &&
@@ -971,6 +995,7 @@
 
       screenshots_.push_back(screenshot);
     }
+
     downloaded_screenshots_.clear();
     is_screenshots_fetch_complete_ = true;
 
diff --git a/components/webapps/browser/installable/installable_manager.h b/components/webapps/browser/installable/installable_manager.h
index 8788b853..12f21edc 100644
--- a/components/webapps/browser/installable/installable_manager.h
+++ b/components/webapps/browser/installable/installable_manager.h
@@ -241,8 +241,10 @@
                              IconUsage usage);
   void OnIconFetched(GURL icon_url, IconUsage usage, const SkBitmap& bitmap);
 
-  void CheckAndFetchScreenshots();
+  void CheckAndFetchScreenshots(bool check_platform = true);
+
   void OnScreenshotFetched(GURL screenshot_url, const SkBitmap& bitmap);
+  void PopulateScreenshots(bool check_platform);
 
   // content::ServiceWorkerContextObserver overrides
   void OnRegistrationCompleted(const GURL& pattern) override;
diff --git a/content/browser/BUILD.gn b/content/browser/BUILD.gn
index b0e3eb97..917b7f696 100644
--- a/content/browser/BUILD.gn
+++ b/content/browser/BUILD.gn
@@ -845,6 +845,8 @@
     "devtools/shared_worker_devtools_agent_host.h",
     "devtools/shared_worker_devtools_manager.cc",
     "devtools/shared_worker_devtools_manager.h",
+    "devtools/web_contents_devtools_agent_host.cc",
+    "devtools/web_contents_devtools_agent_host.h",
     "devtools/worker_devtools_agent_host.cc",
     "devtools/worker_devtools_agent_host.h",
     "devtools/worker_devtools_manager.cc",
diff --git a/content/browser/back_forward_cache_internal_browsertest.cc b/content/browser/back_forward_cache_internal_browsertest.cc
index 83429ec..e02c20a 100644
--- a/content/browser/back_forward_cache_internal_browsertest.cc
+++ b/content/browser/back_forward_cache_internal_browsertest.cc
@@ -3555,8 +3555,14 @@
 
 // Verify that the page will be evicted upon accessibility events if the
 // flag to evict on ax events is off, and evicted otherwise.
+#if BUILDFLAG(IS_WIN)
+#define MAYBE_EvictOnAccessibilityEventsOrNot \
+  DISABLED_EvictOnAccessibilityEventsOrNot
+#else
+#define MAYBE_EvictOnAccessibilityEventsOrNot EvictOnAccessibilityEventsOrNot
+#endif
 IN_PROC_BROWSER_TEST_P(BackForwardCacheBrowserTestWithFlagForAXEvents,
-                       EvictOnAccessibilityEventsOrNot) {
+                       MAYBE_EvictOnAccessibilityEventsOrNot) {
   ASSERT_TRUE(embedded_test_server()->Start());
   GURL url_a(embedded_test_server()->GetURL("a.com", "/title1.html"));
   GURL url_b(embedded_test_server()->GetURL("b.com", "/title1.html"));
diff --git a/content/browser/devtools/devtools_agent_host_impl.cc b/content/browser/devtools/devtools_agent_host_impl.cc
index e6c47fdd..55c8eb91 100644
--- a/content/browser/devtools/devtools_agent_host_impl.cc
+++ b/content/browser/devtools/devtools_agent_host_impl.cc
@@ -25,6 +25,7 @@
 #include "content/browser/devtools/service_worker_devtools_manager.h"
 #include "content/browser/devtools/shared_worker_devtools_agent_host.h"
 #include "content/browser/devtools/shared_worker_devtools_manager.h"
+#include "content/browser/devtools/web_contents_devtools_agent_host.h"
 #include "content/public/browser/browser_thread.h"
 #include "content/public/browser/content_browser_client.h"
 #include "content/public/browser/devtools_external_agent_proxy_delegate.h"
@@ -61,6 +62,7 @@
 
 }  // namespace
 
+const char DevToolsAgentHost::kTypeTab[] = "tab";
 const char DevToolsAgentHost::kTypePage[] = "page";
 const char DevToolsAgentHost::kTypeFrame[] = "iframe";
 const char DevToolsAgentHost::kTypeDedicatedWorker[] = "worker";
@@ -101,6 +103,7 @@
   // TODO(dgozman): we should add dedicated workers here, but clients are not
   // ready.
   RenderFrameDevToolsAgentHost::AddAllAgentHosts(&result);
+  WebContentsDevToolsAgentHost::AddAllAgentHosts(&result);
 
   AuctionWorkletDevToolsAgentHostManager::GetInstance().GetAll(&result);
 
diff --git a/content/browser/devtools/devtools_http_handler.cc b/content/browser/devtools/devtools_http_handler.cc
index b75e19e7..f096136 100644
--- a/content/browser/devtools/devtools_http_handler.cc
+++ b/content/browser/devtools/devtools_http_handler.cc
@@ -714,8 +714,12 @@
   DevToolsAgentHost::List agent_hosts = std::move(hosts);
   std::sort(agent_hosts.begin(), agent_hosts.end(), TimeComparator);
   base::ListValue list_value;
-  for (auto& agent_host : agent_hosts)
-    list_value.Append(SerializeDescriptor(agent_host, host));
+  for (auto& agent_host : agent_hosts) {
+    // TODO(caseq): figure out if it makes sense exposing tab target to
+    // HTTP clients and potentially compatibility risks involved.
+    if (agent_host->GetType() != DevToolsAgentHost::kTypeTab)
+      list_value.Append(SerializeDescriptor(agent_host, host));
+  }
   SendJson(connection_id, net::HTTP_OK, &list_value, std::string());
 }
 
diff --git a/content/browser/devtools/protocol/target_handler.cc b/content/browser/devtools/protocol/target_handler.cc
index 9899aff..06507a1 100644
--- a/content/browser/devtools/protocol/target_handler.cc
+++ b/content/browser/devtools/protocol/target_handler.cc
@@ -614,6 +614,11 @@
                                  .SetExclude(true)
                                  .SetType(DevToolsAgentHost::kTypeBrowser)
                                  .Build());
+    // - Exclude `tab`.
+    default_filter.push_back(protocol::Target::FilterEntry::Create()
+                                 .SetExclude(true)
+                                 .SetType(DevToolsAgentHost::kTypeTab)
+                                 .Build());
     // - Allow everything else.
     default_filter.push_back(protocol::Target::FilterEntry::Create().Build());
     return base::WrapUnique(new TargetFilter(std::move(default_filter)));
diff --git a/content/browser/devtools/site_per_process_devtools_browsertest.cc b/content/browser/devtools/site_per_process_devtools_browsertest.cc
index d502798e..540abd8e 100644
--- a/content/browser/devtools/site_per_process_devtools_browsertest.cc
+++ b/content/browser/devtools/site_per_process_devtools_browsertest.cc
@@ -60,6 +60,18 @@
   bool waiting_for_reply_;
 };
 
+DevToolsAgentHost::List ExtractPageOrFrameTargets(
+    DevToolsAgentHost::List list) {
+  DevToolsAgentHost::List result;
+  for (auto& entry : list) {
+    if (entry->GetType() == DevToolsAgentHost::kTypePage ||
+        entry->GetType() == DevToolsAgentHost::kTypeFrame) {
+      result.push_back(std::move(entry));
+    }
+  }
+  return result;
+}
+
 // Fails on Android, http://crbug.com/464993.
 #if BUILDFLAG(IS_ANDROID)
 #define MAYBE_CrossSiteIframeAgentHost DISABLED_CrossSiteIframeAgentHost
@@ -77,7 +89,7 @@
                             ->GetPrimaryFrameTree()
                             .root();
 
-  list = DevToolsAgentHost::GetOrCreateAll();
+  list = ExtractPageOrFrameTargets(DevToolsAgentHost::GetOrCreateAll());
   EXPECT_EQ(1U, list.size());
   EXPECT_EQ(DevToolsAgentHost::kTypePage, list[0]->GetType());
   EXPECT_EQ(main_url.spec(), list[0]->GetURL().spec());
@@ -87,7 +99,7 @@
   GURL http_url(embedded_test_server()->GetURL("/title1.html"));
   EXPECT_TRUE(NavigateToURLFromRenderer(child, http_url));
 
-  list = DevToolsAgentHost::GetOrCreateAll();
+  list = ExtractPageOrFrameTargets(DevToolsAgentHost::GetOrCreateAll());
   EXPECT_EQ(1U, list.size());
   EXPECT_EQ(DevToolsAgentHost::kTypePage, list[0]->GetType());
   EXPECT_EQ(main_url.spec(), list[0]->GetURL().spec());
@@ -99,7 +111,7 @@
   cross_site_url = cross_site_url.ReplaceComponents(replace_host);
   EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), cross_site_url));
 
-  list = DevToolsAgentHost::GetOrCreateAll();
+  list = ExtractPageOrFrameTargets(DevToolsAgentHost::GetOrCreateAll());
   EXPECT_EQ(2U, list.size());
   EXPECT_EQ(DevToolsAgentHost::kTypePage, list[0]->GetType());
   EXPECT_EQ(main_url.spec(), list[0]->GetURL().spec());
@@ -128,7 +140,7 @@
   // Load back same-site page into iframe.
   EXPECT_TRUE(NavigateToURLFromRenderer(root->child_at(0), http_url));
 
-  list = DevToolsAgentHost::GetOrCreateAll();
+  list = ExtractPageOrFrameTargets(DevToolsAgentHost::GetOrCreateAll());
   EXPECT_EQ(1U, list.size());
   EXPECT_EQ(DevToolsAgentHost::kTypePage, list[0]->GetType());
   EXPECT_EQ(main_url.spec(), list[0]->GetURL().spec());
diff --git a/content/browser/devtools/web_contents_devtools_agent_host.cc b/content/browser/devtools/web_contents_devtools_agent_host.cc
new file mode 100644
index 0000000..9f1270f4
--- /dev/null
+++ b/content/browser/devtools/web_contents_devtools_agent_host.cc
@@ -0,0 +1,247 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/browser/devtools/web_contents_devtools_agent_host.h"
+
+#include "base/unguessable_token.h"
+#include "content/browser/devtools/protocol/target_auto_attacher.h"
+#include "content/browser/devtools/protocol/target_handler.h"
+#include "content/browser/devtools/render_frame_devtools_agent_host.h"
+#include "content/browser/web_contents/web_contents_impl.h"
+
+namespace content {
+
+namespace {
+using WebContentsDevToolsMap =
+    std::map<WebContents*, WebContentsDevToolsAgentHost*>;
+base::LazyInstance<WebContentsDevToolsMap>::Leaky g_agent_host_instances =
+    LAZY_INSTANCE_INITIALIZER;
+
+WebContentsDevToolsAgentHost* FindAgentHost(WebContents* wc) {
+  if (!g_agent_host_instances.IsCreated())
+    return nullptr;
+  auto it = g_agent_host_instances.Get().find(wc);
+  return it == g_agent_host_instances.Get().end() ? nullptr : it->second;
+}
+
+bool ShouldCreateDevToolsAgentHost(WebContents* wc) {
+  return wc == wc->GetResponsibleWebContents();
+}
+}  // namespace
+
+class WebContentsDevToolsAgentHost::AutoAttacher
+    : public protocol::TargetAutoAttacher {
+ public:
+  explicit AutoAttacher(WebContents* web_contents)
+      : web_contents_(web_contents) {}
+
+ private:
+  void UpdateAutoAttach(base::OnceClosure callback) override {
+    if (auto_attach())
+      UpdateAssociatedFrames();
+    protocol::TargetAutoAttacher::UpdateAutoAttach(std::move(callback));
+  }
+
+  void UpdateAssociatedFrames() {
+    // TODO: This needs to cover:
+    // - portals
+    // - pre-renders
+    // - BF-cache
+    DevToolsAgentHost::List hosts;
+    FrameTreeNode* primary_root = static_cast<WebContentsImpl*>(web_contents_)
+                                      ->GetPrimaryFrameTree()
+                                      .root();
+    hosts.push_back(RenderFrameDevToolsAgentHost::GetOrCreateFor(primary_root));
+    DispatchSetAttachedTargetsOfType(hosts, DevToolsAgentHost::kTypePage);
+  }
+
+  WebContents* const web_contents_;
+};
+
+// static
+WebContentsDevToolsAgentHost* WebContentsDevToolsAgentHost::GetFor(
+    WebContents* web_contents) {
+  return FindAgentHost(web_contents->GetResponsibleWebContents());
+}
+
+// static
+WebContentsDevToolsAgentHost* WebContentsDevToolsAgentHost::GetOrCreateFor(
+    WebContents* web_contents) {
+  web_contents = web_contents->GetResponsibleWebContents();
+  if (auto* host = FindAgentHost(web_contents))
+    return host;
+  return new WebContentsDevToolsAgentHost(web_contents);
+}
+
+// static
+void WebContentsDevToolsAgentHost::AddAllAgentHosts(
+    DevToolsAgentHost::List* result) {
+  for (WebContentsImpl* wc : WebContentsImpl::GetAllWebContents()) {
+    if (ShouldCreateDevToolsAgentHost(wc))
+      result->push_back(GetOrCreateFor(wc));
+  }
+}
+
+WebContentsDevToolsAgentHost::WebContentsDevToolsAgentHost(WebContents* wc)
+    : DevToolsAgentHostImpl(base::UnguessableToken::Create().ToString()),
+      WebContentsObserver(wc),
+      auto_attacher_(std::make_unique<AutoAttacher>(wc)) {
+  DCHECK(web_contents());
+  bool inserted =
+      g_agent_host_instances.Get().insert(std::make_pair(wc, this)).second;
+  DCHECK(inserted);
+  // Once created, persist till underlying WC is destroyed, so that
+  // the target id is retained.
+  AddRef();
+  NotifyCreated();
+}
+
+WebContentsDevToolsAgentHost::~WebContentsDevToolsAgentHost() {
+  DCHECK(!web_contents());
+}
+
+void WebContentsDevToolsAgentHost::DisconnectWebContents() {
+  NOTREACHED();
+}
+
+void WebContentsDevToolsAgentHost::ConnectWebContents(
+    WebContents* web_contents) {
+  NOTREACHED();
+}
+
+BrowserContext* WebContentsDevToolsAgentHost::GetBrowserContext() {
+  return web_contents()->GetBrowserContext();
+}
+
+WebContents* WebContentsDevToolsAgentHost::GetWebContents() {
+  return web_contents();
+}
+
+std::string WebContentsDevToolsAgentHost::GetParentId() {
+  return std::string();
+}
+
+std::string WebContentsDevToolsAgentHost::GetOpenerId() {
+  if (DevToolsAgentHost* host = GetPrimaryFrameAgent())
+    return host->GetOpenerId();
+  return "";
+};
+
+std::string WebContentsDevToolsAgentHost::GetOpenerFrameId() {
+  if (DevToolsAgentHost* host = GetPrimaryFrameAgent())
+    return host->GetOpenerFrameId();
+  return "";
+}
+
+bool WebContentsDevToolsAgentHost::CanAccessOpener() {
+  if (DevToolsAgentHost* host = GetPrimaryFrameAgent())
+    return host->CanAccessOpener();
+  return false;
+}
+
+std::string WebContentsDevToolsAgentHost::GetType() {
+  return kTypeTab;
+}
+
+std::string WebContentsDevToolsAgentHost::GetTitle() {
+  if (DevToolsAgentHost* host = GetPrimaryFrameAgent())
+    return host->GetTitle();
+  return "";
+}
+
+std::string WebContentsDevToolsAgentHost::GetDescription() {
+  if (DevToolsAgentHost* host = GetPrimaryFrameAgent())
+    return host->GetDescription();
+  return "";
+}
+
+GURL WebContentsDevToolsAgentHost::GetURL() {
+  if (DevToolsAgentHost* host = GetPrimaryFrameAgent())
+    return host->GetURL();
+  return GURL();
+}
+
+GURL WebContentsDevToolsAgentHost::GetFaviconURL() {
+  if (DevToolsAgentHost* host = GetPrimaryFrameAgent())
+    return host->GetFaviconURL();
+  return GURL();
+}
+
+bool WebContentsDevToolsAgentHost::Activate() {
+  if (DevToolsAgentHost* host = GetPrimaryFrameAgent())
+    return host->Activate();
+  return false;
+}
+
+void WebContentsDevToolsAgentHost::Reload() {
+  if (DevToolsAgentHost* host = GetPrimaryFrameAgent())
+    host->Reload();
+}
+
+bool WebContentsDevToolsAgentHost::Close() {
+  if (DevToolsAgentHost* host = GetPrimaryFrameAgent())
+    return host->Close();
+  return false;
+}
+
+base::TimeTicks WebContentsDevToolsAgentHost::GetLastActivityTime() {
+  if (DevToolsAgentHost* host = GetPrimaryFrameAgent())
+    return host->GetLastActivityTime();
+  return base::TimeTicks();
+}
+
+absl::optional<network::CrossOriginEmbedderPolicy>
+WebContentsDevToolsAgentHost::cross_origin_embedder_policy(
+    const std::string& id) {
+  NOTREACHED();
+  return absl::nullopt;
+}
+
+absl::optional<network::CrossOriginOpenerPolicy>
+WebContentsDevToolsAgentHost::cross_origin_opener_policy(
+    const std::string& id) {
+  NOTREACHED();
+  return absl::nullopt;
+}
+
+DevToolsAgentHostImpl* WebContentsDevToolsAgentHost::GetPrimaryFrameAgent() {
+  if (WebContents* wc = web_contents()) {
+    return RenderFrameDevToolsAgentHost::GetFor(
+        static_cast<RenderFrameHostImpl*>(wc->GetPrimaryMainFrame()));
+  }
+  return nullptr;
+}
+
+void WebContentsDevToolsAgentHost::WebContentsDestroyed() {
+  DCHECK_EQ(this, FindAgentHost(web_contents()));
+  ForceDetachAllSessions();
+  auto_attacher_.reset();
+  g_agent_host_instances.Get().erase(web_contents());
+  Observe(nullptr);
+  // We may or may not be destruced here, depending on embedders
+  // potentially retaining references.
+  Release();
+}
+
+// DevToolsAgentHostImpl overrides.
+bool WebContentsDevToolsAgentHost::AttachSession(DevToolsSession* session,
+                                                 bool acquire_wake_lock) {
+  // TODO(caseq): figure out if this can be a CHECK().
+  if (!web_contents())
+    return false;
+  const bool may_attach_to_brower = session->GetClient()->IsTrusted();
+  session->CreateAndAddHandler<protocol::TargetHandler>(
+      may_attach_to_brower
+          ? protocol::TargetHandler::AccessMode::kRegular
+          : protocol::TargetHandler::AccessMode::kAutoAttachOnly,
+      GetId(), auto_attacher_.get(), session->GetRootSession());
+  return true;
+}
+
+protocol::TargetAutoAttacher* WebContentsDevToolsAgentHost::auto_attacher() {
+  DCHECK(!!auto_attacher_ == !!web_contents());
+  return auto_attacher_.get();
+}
+
+}  // namespace content
diff --git a/content/browser/devtools/web_contents_devtools_agent_host.h b/content/browser/devtools/web_contents_devtools_agent_host.h
new file mode 100644
index 0000000..278ac79
--- /dev/null
+++ b/content/browser/devtools/web_contents_devtools_agent_host.h
@@ -0,0 +1,77 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CONTENT_BROWSER_DEVTOOLS_WEB_CONTENTS_DEVTOOLS_AGENT_HOST_H_
+#define CONTENT_BROWSER_DEVTOOLS_WEB_CONTENTS_DEVTOOLS_AGENT_HOST_H_
+
+#include "content/browser/devtools/devtools_agent_host_impl.h"
+#include "content/common/content_export.h"
+#include "content/public/browser/render_process_host_observer.h"
+#include "content/public/browser/web_contents_observer.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+
+namespace content {
+
+class CONTENT_EXPORT WebContentsDevToolsAgentHost
+    : public DevToolsAgentHostImpl,
+      public WebContentsObserver {
+ public:
+  // Returns appropriate agent host for given Web Contents
+  static WebContentsDevToolsAgentHost* GetFor(WebContents* web_contents);
+  // Similar to GetFor(), but creates a host if it doesn't exist yet.
+  static WebContentsDevToolsAgentHost* GetOrCreateFor(
+      WebContents* web_contents);
+
+  WebContentsDevToolsAgentHost(const WebContentsDevToolsAgentHost&) = delete;
+  WebContentsDevToolsAgentHost& operator=(const WebContentsDevToolsAgentHost&) =
+      delete;
+
+  static void AddAllAgentHosts(DevToolsAgentHost::List* result);
+
+ private:
+  class AutoAttacher;
+
+  explicit WebContentsDevToolsAgentHost(WebContents* wc);
+  ~WebContentsDevToolsAgentHost() override;
+
+  // DevToolsAgentHost overrides.
+  void DisconnectWebContents() override;
+  void ConnectWebContents(WebContents* web_contents) override;
+  BrowserContext* GetBrowserContext() override;
+  WebContents* GetWebContents() override;
+  std::string GetParentId() override;
+  std::string GetOpenerId() override;
+  std::string GetOpenerFrameId() override;
+  bool CanAccessOpener() override;
+  std::string GetType() override;
+  std::string GetTitle() override;
+  std::string GetDescription() override;
+  GURL GetURL() override;
+  GURL GetFaviconURL() override;
+  bool Activate() override;
+  void Reload() override;
+
+  bool Close() override;
+  base::TimeTicks GetLastActivityTime() override;
+
+  absl::optional<network::CrossOriginEmbedderPolicy>
+  cross_origin_embedder_policy(const std::string& id) override;
+  absl::optional<network::CrossOriginOpenerPolicy> cross_origin_opener_policy(
+      const std::string& id) override;
+
+  // DevToolsAgentHostImpl overrides.
+  bool AttachSession(DevToolsSession* session, bool acquire_wake_lock) override;
+  protocol::TargetAutoAttacher* auto_attacher() override;
+
+  // WebContentsObserver overrides.
+  void WebContentsDestroyed() override;
+
+  DevToolsAgentHostImpl* GetPrimaryFrameAgent();
+
+  std::unique_ptr<AutoAttacher> auto_attacher_;
+};
+
+}  // namespace content
+
+#endif  // CONTENT_BROWSER_DEVTOOLS_WEB_CONTENTS_DEVTOOLS_AGENT_HOST_H_
diff --git a/content/public/browser/devtools_agent_host.h b/content/public/browser/devtools_agent_host.h
index daf9aa2f..96c4d77 100644
--- a/content/public/browser/devtools_agent_host.h
+++ b/content/public/browser/devtools_agent_host.h
@@ -45,6 +45,7 @@
 class CONTENT_EXPORT DevToolsAgentHost
     : public base::RefCounted<DevToolsAgentHost> {
  public:
+  static const char kTypeTab[];
   static const char kTypePage[];
   static const char kTypeFrame[];
   static const char kTypeDedicatedWorker[];
diff --git a/infra/config/generated/luci/commit-queue.cfg b/infra/config/generated/luci/commit-queue.cfg
index c62ec07..3a4f88623 100644
--- a/infra/config/generated/luci/commit-queue.cfg
+++ b/infra/config/generated/luci/commit-queue.cfg
@@ -276,6 +276,10 @@
         includable_only: true
       }
       builders {
+        name: "chromium/codesearch/gen-webview-try"
+        includable_only: true
+      }
+      builders {
         name: "chromium/codesearch/gen-win-try"
         includable_only: true
       }
diff --git a/infra/config/generated/luci/cr-buildbucket.cfg b/infra/config/generated/luci/cr-buildbucket.cfg
index 197ccff..c422cd6 100644
--- a/infra/config/generated/luci/cr-buildbucket.cfg
+++ b/infra/config/generated/luci/cr-buildbucket.cfg
@@ -44455,6 +44455,83 @@
       }
     }
     builders {
+      name: "gen-webview-try"
+      swarming_host: "chromium-swarm.appspot.com"
+      dimensions: "builderless:1"
+      dimensions: "cores:8"
+      dimensions: "cpu:x86-64"
+      dimensions: "os:Ubuntu-18.04"
+      dimensions: "pool:luci.chromium.try"
+      dimensions: "ssd:0"
+      exe {
+        cipd_package: "infra/recipe_bundles/chromium.googlesource.com/chromium/tools/build"
+        cipd_version: "refs/heads/main"
+        cmd: "luciexe"
+      }
+      properties:
+        '{'
+        '  "$build/goma": {'
+        '    "enable_ats": true,'
+        '    "rpc_extra_params": "?prod",'
+        '    "server_host": "goma.chromium.org",'
+        '    "use_luci_auth": true'
+        '  },'
+        '  "$recipe_engine/resultdb/test_presentation": {'
+        '    "column_keys": [],'
+        '    "grouping_keys": ['
+        '      "status",'
+        '      "v.test_suite"'
+        '    ]'
+        '  },'
+        '  "builder_group": "tryserver.chromium.codesearch",'
+        '  "recipe": "chromium_codesearch"'
+        '}'
+      execution_timeout_secs: 32400
+      expiration_secs: 7200
+      caches {
+        name: "win_toolchain"
+        path: "win_toolchain"
+      }
+      build_numbers: YES
+      service_account: "chromium-try-builder@chops-service-accounts.iam.gserviceaccount.com"
+      experiments {
+        key: "luci.recipes.use_python3"
+        value: 100
+      }
+      resultdb {
+        enable: true
+        bq_exports {
+          project: "chrome-luci-data"
+          dataset: "chromium"
+          table: "try_test_results"
+          test_results {}
+        }
+        bq_exports {
+          project: "chrome-luci-data"
+          dataset: "chromium"
+          table: "gpu_try_test_results"
+          test_results {
+            predicate {
+              test_id_regexp: "ninja://chrome/test:telemetry_gpu_integration_test[^/]*/.+"
+            }
+          }
+        }
+        bq_exports {
+          project: "chrome-luci-data"
+          dataset: "chromium"
+          table: "blink_web_tests_try_test_results"
+          test_results {
+            predicate {
+              test_id_regexp: "ninja://[^/]*blink_web_tests/.+"
+            }
+          }
+        }
+        history_options {
+          use_invocation_timestamp: true
+        }
+      }
+    }
+    builders {
       name: "gen-win-try"
       swarming_host: "chromium-swarm.appspot.com"
       dimensions: "builderless:1"
@@ -52431,7 +52508,7 @@
       }
       experiments {
         key: "enable_weetbix_queries"
-        value: 50
+        value: 100
       }
       experiments {
         key: "luci.recipes.use_python3"
diff --git a/infra/config/generated/luci/luci-milo.cfg b/infra/config/generated/luci/luci-milo.cfg
index 0929794d..863a919 100644
--- a/infra/config/generated/luci/luci-milo.cfg
+++ b/infra/config/generated/luci/luci-milo.cfg
@@ -17482,6 +17482,9 @@
     name: "buildbucket/luci.chromium.codesearch/gen-mac-try"
   }
   builders {
+    name: "buildbucket/luci.chromium.codesearch/gen-webview-try"
+  }
+  builders {
     name: "buildbucket/luci.chromium.codesearch/gen-win-try"
   }
   builder_view_only: true
diff --git a/infra/config/subprojects/chromium/try/tryserver.chromium.android.star b/infra/config/subprojects/chromium/try/tryserver.chromium.android.star
index 434851d..aeaa0e6 100644
--- a/infra/config/subprojects/chromium/try/tryserver.chromium.android.star
+++ b/infra/config/subprojects/chromium/try/tryserver.chromium.android.star
@@ -286,7 +286,7 @@
     coverage_test_types = ["unit", "overall"],
     tryjob = try_.job(),
     experiments = {
-        "enable_weetbix_queries": 50,
+        "enable_weetbix_queries": 100,
     },
 )
 
diff --git a/infra/config/subprojects/codesearch/codesearch.star b/infra/config/subprojects/codesearch/codesearch.star
index a051259..567d85c 100644
--- a/infra/config/subprojects/codesearch/codesearch.star
+++ b/infra/config/subprojects/codesearch/codesearch.star
@@ -76,6 +76,10 @@
 )
 
 try_.builder(
+    name = "gen-webview-try",
+)
+
+try_.builder(
     name = "gen-win-try",
     os = os.WINDOWS_10,
 )
diff --git a/infra/orchestrator/BUILD.gn b/infra/orchestrator/BUILD.gn
index de3dada..b007180f 100644
--- a/infra/orchestrator/BUILD.gn
+++ b/infra/orchestrator/BUILD.gn
@@ -18,6 +18,10 @@
     ":standard_isolated_script_merge_py",
   ]
 
+  if (is_android) {
+    data_deps += [ "//build/android:test_result_presentations_py" ]
+  }
+
   data = [
     # Various merge/collect scripts will likely need a venv specified in
     # the root vpython spec files.
@@ -35,10 +39,7 @@
 
   if (use_jacoco_coverage) {
     data += [ "//third_party/jacoco/lib/jacococli.jar" ]
-    data_deps += [
-      "//build/android:test_result_presentations_py",
-      "//third_party/jdk:java_data",
-    ]
+    data_deps += [ "//third_party/jdk:java_data" ]
   }
   write_runtime_deps = "$root_out_dir/orchestrator_all.runtime_deps"
 }
diff --git a/ios/chrome/app/BUILD.gn b/ios/chrome/app/BUILD.gn
index 3bd1481..052ebaa 100644
--- a/ios/chrome/app/BUILD.gn
+++ b/ios/chrome/app/BUILD.gn
@@ -279,7 +279,10 @@
     "//ios/chrome/browser/discover_feed:discover_feed_factory",
     "//ios/chrome/browser/ntp:features",
   ]
-  frameworks = [ "BackgroundTasks.framework" ]
+  frameworks = [
+    "BackgroundTasks.framework",
+    "UserNotifications.framework",
+  ]
 }
 
 source_set("app_internal") {
diff --git a/ios/chrome/app/feed_app_agent.mm b/ios/chrome/app/feed_app_agent.mm
index 5b26efc..b223b10 100644
--- a/ios/chrome/app/feed_app_agent.mm
+++ b/ios/chrome/app/feed_app_agent.mm
@@ -5,6 +5,7 @@
 #import "ios/chrome/app/feed_app_agent.h"
 
 #import <BackgroundTasks/BackgroundTasks.h>
+#import <UserNotifications/UserNotifications.h>
 
 #import "ios/chrome/app/application_delegate/app_state.h"
 #import "ios/chrome/browser/discover_feed/discover_feed_service.h"
@@ -22,26 +23,26 @@
 
 - (void)appState:(AppState*)appState
     didTransitionFromInitStage:(InitStage)previousInitStage {
-  if (IsFeedBackgroundRefreshEnabled()) {
-    if (appState.initStage == InitStageBrowserBasic) {
-      // Apple docs say that background tasks must be registered before the
-      // end of `application:didFinishLaunchingWithOptions:`.
-      // InitStageBrowserBasic fulfills that requirement.
-      [self registerBackgroundRefreshTask];
-    } else if (appState.initStage ==
-               InitStageBrowserObjectsForBackgroundHandlers) {
-      // Save the value of the feature flag now since 'base::FeatureList' was
-      // not available in `InitStageBrowserBasic`.
-      // IsFeedBackgroundRefreshEnabled() simply reads the saved value saved by
-      // SaveFeedBackgroundRefreshEnabledForNextColdStart(). Do not wrap this in
-      // IsFeedBackgroundRefreshEnabled() -- in this case, a new value would
-      // never be saved again once we save NO, since the NO codepath would not
-      // execute saving a new value.
-      SaveFeedBackgroundRefreshEnabledForNextColdStart();
-    }
-  }
-
-  if (appState.initStage == InitStageNormalUI && IsWebChannelsEnabled()) {
+  if (appState.initStage == InitStageBrowserBasic) {
+    // Apple docs say that background tasks must be registered before the
+    // end of `application:didFinishLaunchingWithOptions:`.
+    // InitStageBrowserBasic fulfills that requirement.
+    [self maybeRegisterBackgroundRefreshTask];
+    // This is a provisional permission, which does not prompt the user at this
+    // point.
+    [self maybeRequestUserNotificationPermissions];
+  } else if (appState.initStage ==
+             InitStageBrowserObjectsForBackgroundHandlers) {
+    // Save the value of the feature flag now since 'base::FeatureList' was
+    // not available in `InitStageBrowserBasic`.
+    // IsFeedBackgroundRefreshEnabled() simply reads the saved value saved by
+    // SaveFeedBackgroundRefreshEnabledForNextColdStart(). Do not wrap this in
+    // IsFeedBackgroundRefreshEnabled() -- in this case, a new value would
+    // never be saved again once we save NO, since the NO codepath would not
+    // execute saving a new value.
+    SaveFeedBackgroundRefreshEnabledForNextColdStart();
+  } else if (appState.initStage == InitStageNormalUI &&
+             IsWebChannelsEnabled()) {
     // Starting the DiscoverFeedService is required before users are able to
     // interact with any tab because following a web channel (part of the
     // Following Feed feature which depends on the DiscoverFeedService) is
@@ -86,7 +87,7 @@
 // Registers handler for the background refresh task. According to
 // documentation, this must complete before the end of
 // `applicationDidFinishLaunching`.
-- (void)registerBackgroundRefreshTask {
+- (void)maybeRegisterBackgroundRefreshTask {
   if (!IsFeedBackgroundRefreshEnabled()) {
     return;
   }
@@ -115,15 +116,30 @@
   }
   BGAppRefreshTaskRequest* request = [[BGAppRefreshTaskRequest alloc]
       initWithIdentifier:kFeedBackgroundRefreshTaskIdentifier];
-  // This is expected to crash if FeedService is not available.
-  request.earliestBeginDate =
-      [self feedService]->GetEarliestBackgroundRefreshBeginDate();
+  request.earliestBeginDate = [self earliestBackgroundRefreshBeginDate];
   // Error in scheduling is intentionally not handled since the fallback is that
   // the user will just refresh in the foreground.
   // TODO(crbug.com/1343695): Consider logging error in histogram.
   [BGTaskScheduler.sharedScheduler submitTaskRequest:request error:nil];
 }
 
+// Returns the earliest begin date to set on the refresh task. Either returns a
+// date from DiscoverFeedService or an override date created with the override
+// interval in Experimental Settings.
+- (NSDate*)earliestBackgroundRefreshBeginDate {
+  NSDate* earliestBeginDate = nil;
+  NSTimeInterval intervalOverride =
+      GetBackgroundRefreshIntervalOverrideInSeconds();
+  if (intervalOverride > 0) {
+    earliestBeginDate = [NSDate dateWithTimeIntervalSinceNow:intervalOverride];
+  } else {
+    // This is expected to crash if FeedService is not available.
+    earliestBeginDate =
+        [self feedService]->GetEarliestBackgroundRefreshBeginDate();
+  }
+  return earliestBeginDate;
+}
+
 // This method is called when the app is in the background.
 - (void)handleBackgroundRefreshTask:(BGTask*)task {
   // Do not DCHECK IsFeedBackgroundRefreshEnabled() because the value could have
@@ -137,11 +153,83 @@
   task.expirationHandler = ^{
     // This is expected to crash if FeedService is not available.
     [self feedService]->HandleBackgroundRefreshTaskExpiration();
+    [self maybeNotifyRefreshSuccess:NO];
   };
-  // This is expected to crash if FeedService is not available.
-  [self feedService]->PerformBackgroundRefreshes(^(BOOL success) {
-    [task setTaskCompletedWithSuccess:success];
-  });
+  if (IsAttemptFeedBackgroundRefreshEnabled()) {
+    // This is expected to crash if FeedService is not available.
+    [self feedService]->PerformBackgroundRefreshes(^(BOOL success) {
+      [self maybeNotifyRefreshSuccess:success];
+      [task setTaskCompletedWithSuccess:success];
+    });
+  } else {
+    [self maybeNotifyFetchTriggered];
+    [task setTaskCompletedWithSuccess:YES];
+  }
+}
+
+#pragma mark - Non-release only - Refresh Completion Notifications
+
+// Request provisional permission, which does not explicitly prompt the user for
+// permission. Instead, the OS delivers provisional notifications quietly and
+// are visible in the notification center's history. For active debugging, the
+// tester can go to the Settings App and turn on full permissions for banners
+// and sounds.
+- (void)maybeRequestUserNotificationPermissions {
+  if (!IsFeedBackgroundRefreshCompletedNotificationEnabled()) {
+    return;
+  }
+  UNUserNotificationCenter* center =
+      UNUserNotificationCenter.currentNotificationCenter;
+  [center requestAuthorizationWithOptions:(UNAuthorizationOptionProvisional |
+                                           UNAuthorizationOptionAlert |
+                                           UNAuthorizationOptionSound)
+                        completionHandler:^(BOOL granted, NSError* error){
+                        }];
+}
+
+// Requests OS to send a local user notification with `title`.
+- (void)maybeRequestNotification:(NSString*)title {
+  if (!IsFeedBackgroundRefreshCompletedNotificationEnabled()) {
+    return;
+  }
+  UNMutableNotificationContent* content =
+      [[UNMutableNotificationContent alloc] init];
+  content.title = title;
+  content.body = @"This is compiled only into non-release versions.";
+  UNTimeIntervalNotificationTrigger* trigger =
+      [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:(1)
+                                                         repeats:NO];
+  UNNotificationRequest* request =
+      [UNNotificationRequest requestWithIdentifier:[[NSUUID UUID] UUIDString]
+                                           content:content
+                                           trigger:trigger];
+  UNUserNotificationCenter* center =
+      UNUserNotificationCenter.currentNotificationCenter;
+  [center addNotificationRequest:request withCompletionHandler:nil];
+}
+
+// Requests OS to send a local user notification when a background fetch is
+// triggered, but an actual feed refresh is not attempted.
+- (void)maybeNotifyFetchTriggered {
+  // Disable triggering notification if actual refresh attempt is enabled so
+  // that user doesn't get unnecessary notifications.
+  if (IsAttemptFeedBackgroundRefreshEnabled()) {
+    return;
+  }
+  [self maybeRequestNotification:@"Background Fetch Triggered"];
+}
+
+// Requests OS to send a local user notification that a feed refresh has been
+// attempted in the background. The notification title says 'success' or
+// 'failure' based on `success`.
+- (void)maybeNotifyRefreshSuccess:(BOOL)success {
+  NSString* title = nil;
+  if (success) {
+    title = @"Feed Bg Refresh Success";
+  } else {
+    title = @"Feed Bg Refresh Failure";
+  }
+  [self maybeRequestNotification:title];
 }
 
 @end
diff --git a/ios/chrome/browser/infobars/overlays/permissions_overlay_tab_helper_unittest.mm b/ios/chrome/browser/infobars/overlays/permissions_overlay_tab_helper_unittest.mm
index 83c6f19..9ddfd64 100644
--- a/ios/chrome/browser/infobars/overlays/permissions_overlay_tab_helper_unittest.mm
+++ b/ios/chrome/browser/infobars/overlays/permissions_overlay_tab_helper_unittest.mm
@@ -4,7 +4,6 @@
 
 #import "ios/chrome/browser/infobars/overlays/permissions_overlay_tab_helper.h"
 
-#include "base/test/scoped_feature_list.h"
 #include "base/test/task_environment.h"
 #include "base/threading/platform_thread.h"
 #include "base/time/time.h"
@@ -41,7 +40,6 @@
 
   ~PermissionsOverlayTabHelperTest() override {
     InfoBarManagerImpl::FromWebState(&web_state_)->ShutDown();
-    // Observer should be removed before |scoped_feature_list_| is reset.
     web_state_.RemoveObserver(
         PermissionsOverlayTabHelper::FromWebState(&web_state_));
   }
@@ -70,7 +68,6 @@
   }
 
   base::test::TaskEnvironment task_environment_;
-  base::test::ScopedFeatureList scoped_feature_list_;
   web::FakeWebState web_state_;
 };
 
diff --git a/ios/chrome/browser/ntp/features.h b/ios/chrome/browser/ntp/features.h
index 97a77ff..c9b36f0 100644
--- a/ios/chrome/browser/ntp/features.h
+++ b/ios/chrome/browser/ntp/features.h
@@ -48,10 +48,20 @@
 // buildflag is not defined.
 bool IsFeedBackgroundRefreshEnabled();
 
+// Whether feed background refresh is attempted, if background refresh is
+// enabled. Disabling this value allows for testing the background fetch
+// capability independent of the feed background refresh codepaths.
+bool IsAttemptFeedBackgroundRefreshEnabled();
+
 // Saves the current value for feature `kEnableFeedBackgroundRefresh`. This call
 // DCHECKs on the availability of `base::FeatureList`.
 void SaveFeedBackgroundRefreshEnabledForNextColdStart();
 
+// Returns true if the user should receive a local notification when a feed
+// background refresh is completed. Background refresh completion notifications
+// are only compiled into non-release versions.
+bool IsFeedBackgroundRefreshCompletedNotificationEnabled();
+
 // Whether the Following feed should also be refreshed in the background.
 bool IsFollowingFeedBackgroundRefreshEnabled();
 
@@ -62,9 +72,14 @@
 // background refresh.
 bool IsRecurringBackgroundRefreshScheduleEnabled();
 
-// The earliest interval to refresh if server value is not used.
+// The earliest interval to refresh if server value is not used. This value is
+// an input into the DiscoverFeedService.
 double GetBackgroundRefreshIntervalInSeconds();
 
+// If greater than zero, this value should be used to completely override the
+// earliest begin date provided by the DiscoverFeedService.
+double GetBackgroundRefreshIntervalOverrideInSeconds();
+
 // Returns the background refresh max age in seconds.
 double GetBackgroundRefreshMaxAgeInSeconds();
 
diff --git a/ios/chrome/browser/ntp/features.mm b/ios/chrome/browser/ntp/features.mm
index 893b85f..a983d58 100644
--- a/ios/chrome/browser/ntp/features.mm
+++ b/ios/chrome/browser/ntp/features.mm
@@ -35,7 +35,8 @@
 
 const char kEnableFollowingFeedBackgroundRefresh[] =
     "EnableFollowingFeedBackgroundRefresh";
-
+const char kEnableAttemptFeedBackgroundRefresh[] =
+    "EnableAttemptFeedBackgroundRefresh";
 const char kEnableServerDrivenBackgroundRefreshSchedule[] =
     "EnableServerDrivenBackgroundRefreshSchedule";
 const char kEnableRecurringBackgroundRefreshSchedule[] =
@@ -60,6 +61,15 @@
 #endif  // BUILDFLAG(IOS_BACKGROUND_MODE_ENABLED)
 }
 
+bool IsAttemptFeedBackgroundRefreshEnabled() {
+  if (!IsFeedBackgroundRefreshEnabled()) {
+    return false;
+  }
+  return base::GetFieldTrialParamByFeatureAsBool(
+      kEnableFeedBackgroundRefresh, kEnableAttemptFeedBackgroundRefresh,
+      /*default=*/false);
+}
+
 void SaveFeedBackgroundRefreshEnabledForNextColdStart() {
   DCHECK(base::FeatureList::GetInstance());
   [[NSUserDefaults standardUserDefaults]
@@ -67,6 +77,11 @@
        forKey:kEnableFeedBackgroundRefreshForNextColdStart];
 }
 
+bool IsFeedBackgroundRefreshCompletedNotificationEnabled() {
+  return IsFeedBackgroundRefreshEnabled() &&
+         experimental_flags::IsRefreshCompletedNotificationEnabled();
+}
+
 bool IsFollowingFeedBackgroundRefreshEnabled() {
   if (experimental_flags::IsForceBackgroundRefreshForFollowingFeedEnabled()) {
     return true;
@@ -94,6 +109,10 @@
       /*default=*/60 * 60);
 }
 
+double GetBackgroundRefreshIntervalOverrideInSeconds() {
+  return experimental_flags::GetBackgroundRefreshIntervalOverrideInSeconds();
+}
+
 double GetBackgroundRefreshMaxAgeInSeconds() {
   if (experimental_flags::GetBackgroundRefreshMaxAgeInSeconds() > 0) {
     return experimental_flags::GetBackgroundRefreshMaxAgeInSeconds();
diff --git a/ios/chrome/browser/resources/Settings.bundle/Experimental.plist b/ios/chrome/browser/resources/Settings.bundle/Experimental.plist
index 652a1ab..eb303b1 100644
--- a/ios/chrome/browser/resources/Settings.bundle/Experimental.plist
+++ b/ios/chrome/browser/resources/Settings.bundle/Experimental.plist
@@ -74,6 +74,16 @@
 			<key>Type</key>
 			<string>PSToggleSwitchSpecifier</string>
 			<key>Title</key>
+			<string>Refresh Completed Notification Enabled</string>
+			<key>Key</key>
+			<string>RefreshCompletedNotificationEnabled</string>
+			<key>DefaultValue</key>
+			<false/>
+		</dict>
+		<dict>
+			<key>Type</key>
+			<string>PSToggleSwitchSpecifier</string>
+			<key>Title</key>
 			<string>Force Background Refresh for Following Feed</string>
 			<key>Key</key>
 			<string>ForceBackgroundRefreshForFollowingFeedEnabled</string>
@@ -84,6 +94,18 @@
 			<key>Type</key>
 			<string>PSTextFieldSpecifier</string>
 			<key>Title</key>
+			<string>Refresh Interval Override In Seconds</string>
+			<key>Key</key>
+			<string>BackgroundRefreshIntervalOverrideInSeconds</string>
+			<key>DefaultValue</key>
+			<string></string>
+			<key>KeyboardType</key>
+			<string>NumberPad</string>
+		</dict>
+		<dict>
+			<key>Type</key>
+			<string>PSTextFieldSpecifier</string>
+			<key>Title</key>
 			<string>Feed Max Age In Seconds</string>
 			<key>Key</key>
 			<string>BackgroundRefreshMaxAgeInSeconds</string>
diff --git a/ios/chrome/browser/system_flags.h b/ios/chrome/browser/system_flags.h
index fb16fb98..e7bfbf9 100644
--- a/ios/chrome/browser/system_flags.h
+++ b/ios/chrome/browser/system_flags.h
@@ -64,10 +64,19 @@
 // TODO(crbug.com/1340154): Remove after launch.
 bool ShouldAlwaysShowFollowIPH();
 
+// Returns true if the user should receive a local notification when a
+// background refresh is completed.
+bool IsRefreshCompletedNotificationEnabled();
+
 // Returns true if background refresh should also be used for the Following
 // feed. If false, the default value or finch feature flag value should be used.
 bool IsForceBackgroundRefreshForFollowingFeedEnabled();
 
+// Returns an interval that can be used to set the background refresh earliest
+// begin date. Any value greater than zero should be used to override the
+// earliest begin date provided by the DiscoverFeedService.
+double GetBackgroundRefreshIntervalOverrideInSeconds();
+
 // The maximum age a response can be before it is refreshed in the background.
 // This check is done when the background task is executed. The default value of
 // 0 means the age check is ignored.
diff --git a/ios/chrome/browser/system_flags.mm b/ios/chrome/browser/system_flags.mm
index 2bc2ff5..507fe494 100644
--- a/ios/chrome/browser/system_flags.mm
+++ b/ios/chrome/browser/system_flags.mm
@@ -37,8 +37,12 @@
 const base::Feature kEnableThirdPartyKeyboardWorkaround{
     "EnableThirdPartyKeyboardWorkaround", base::FEATURE_ENABLED_BY_DEFAULT};
 
+NSString* const kRefreshCompletedNotificationEnabled =
+    @"RefreshCompletedNotificationEnabled";
 NSString* const kForceBackgroundRefreshForFollowingFeedEnabled =
     @"ForceBackgroundRefreshForFollowingFeedEnabled";
+NSString* const kBackgroundRefreshIntervalOverrideInSeconds =
+    @"BackgroundRefreshIntervalOverrideInSeconds";
 NSString* const kBackgroundRefreshMaxAgeInSeconds =
     @"BackgroundRefreshMaxAgeInSeconds";
 
@@ -97,11 +101,21 @@
       [[NSUserDefaults standardUserDefaults] boolForKey:@"AlwaysShowFollowIPH"];
 }
 
+bool IsRefreshCompletedNotificationEnabled() {
+  return [[NSUserDefaults standardUserDefaults]
+      boolForKey:kRefreshCompletedNotificationEnabled];
+}
+
 bool IsForceBackgroundRefreshForFollowingFeedEnabled() {
   return [[NSUserDefaults standardUserDefaults]
       boolForKey:kForceBackgroundRefreshForFollowingFeedEnabled];
 }
 
+double GetBackgroundRefreshIntervalOverrideInSeconds() {
+  return [[NSUserDefaults standardUserDefaults]
+      doubleForKey:kBackgroundRefreshIntervalOverrideInSeconds];
+}
+
 double GetBackgroundRefreshMaxAgeInSeconds() {
   return [[NSUserDefaults standardUserDefaults]
       doubleForKey:kBackgroundRefreshMaxAgeInSeconds];
diff --git a/ios/chrome/browser/ui/page_info/page_info_permissions_mediator_unittest.mm b/ios/chrome/browser/ui/page_info/page_info_permissions_mediator_unittest.mm
index d00c6a9d..a7fd81240 100644
--- a/ios/chrome/browser/ui/page_info/page_info_permissions_mediator_unittest.mm
+++ b/ios/chrome/browser/ui/page_info/page_info_permissions_mediator_unittest.mm
@@ -4,7 +4,6 @@
 
 #import "ios/chrome/browser/ui/page_info/page_info_permissions_mediator.h"
 
-#include "base/test/scoped_feature_list.h"
 #import "ios/chrome/browser/ui/permissions/permission_info.h"
 #import "ios/web/public/permissions/permissions.h"
 #import "ios/web/public/test/fakes/fake_web_state.h"
@@ -50,7 +49,6 @@
   web::WebState* web_state() { return fake_web_state_.get(); }
 
  private:
-  base::test::ScopedFeatureList feature_list_;
   std::unique_ptr<web::FakeWebState> fake_web_state_;
   PageInfoPermissionsMediator* mediator_ API_AVAILABLE(ios(15.0));
 };
diff --git a/ios/google_internal/frameworks/chrome_internal_dynamic_framework.ios.zip.sha1 b/ios/google_internal/frameworks/chrome_internal_dynamic_framework.ios.zip.sha1
index d83721c..e43c3af 100644
--- a/ios/google_internal/frameworks/chrome_internal_dynamic_framework.ios.zip.sha1
+++ b/ios/google_internal/frameworks/chrome_internal_dynamic_framework.ios.zip.sha1
@@ -1 +1 @@
-7c4d5c6e9f1182f9ad9734019de71b014d87f3e6
\ No newline at end of file
+3a2733efb290c5e61feab6553cc8b3d99c6f88a9
\ No newline at end of file
diff --git a/ios/google_internal/frameworks/chrome_internal_dynamic_framework.iossimulator.zip.sha1 b/ios/google_internal/frameworks/chrome_internal_dynamic_framework.iossimulator.zip.sha1
index ffbca6ed..bb094e5 100644
--- a/ios/google_internal/frameworks/chrome_internal_dynamic_framework.iossimulator.zip.sha1
+++ b/ios/google_internal/frameworks/chrome_internal_dynamic_framework.iossimulator.zip.sha1
@@ -1 +1 @@
-78e5b229ec9ee45fe9872db9209d7e57d3cd041f
\ No newline at end of file
+24a733a5131e008997b1fd843b7c8252f4b27034
\ No newline at end of file
diff --git a/ios/google_internal/frameworks/chrome_sso_internal_dynamic_framework.ios.zip.sha1 b/ios/google_internal/frameworks/chrome_sso_internal_dynamic_framework.ios.zip.sha1
index 9e8130c2..3cb04b6 100644
--- a/ios/google_internal/frameworks/chrome_sso_internal_dynamic_framework.ios.zip.sha1
+++ b/ios/google_internal/frameworks/chrome_sso_internal_dynamic_framework.ios.zip.sha1
@@ -1 +1 @@
-075a43a3d70fd1898d3195bc370ed52872fe9ee5
\ No newline at end of file
+d781d3ba036a437dcf69ed5ed36a2584b225ff4e
\ No newline at end of file
diff --git a/ios/google_internal/frameworks/chrome_sso_internal_dynamic_framework.iossimulator.zip.sha1 b/ios/google_internal/frameworks/chrome_sso_internal_dynamic_framework.iossimulator.zip.sha1
index 5ca08da..e5e12056 100644
--- a/ios/google_internal/frameworks/chrome_sso_internal_dynamic_framework.iossimulator.zip.sha1
+++ b/ios/google_internal/frameworks/chrome_sso_internal_dynamic_framework.iossimulator.zip.sha1
@@ -1 +1 @@
-b03b01724e36e134fe7ec37c85d47b36fb5814c6
\ No newline at end of file
+09856a0ba2de799aa988c44fc92e6c4f7eea6779
\ No newline at end of file
diff --git a/ios/google_internal/frameworks/remoting_dogfood_internal_dynamic_framework.ios.zip.sha1 b/ios/google_internal/frameworks/remoting_dogfood_internal_dynamic_framework.ios.zip.sha1
index 79a9e9ce..829f117 100644
--- a/ios/google_internal/frameworks/remoting_dogfood_internal_dynamic_framework.ios.zip.sha1
+++ b/ios/google_internal/frameworks/remoting_dogfood_internal_dynamic_framework.ios.zip.sha1
@@ -1 +1 @@
-6b6a82c1810376b3ce8891b68ea7ae7a50b1a978
\ No newline at end of file
+5a34a1b729ae1a2418009c76f568926f45edfc6a
\ No newline at end of file
diff --git a/ios/google_internal/frameworks/remoting_dogfood_internal_dynamic_framework.iossimulator.zip.sha1 b/ios/google_internal/frameworks/remoting_dogfood_internal_dynamic_framework.iossimulator.zip.sha1
index 2b9a7ae..2562cf8 100644
--- a/ios/google_internal/frameworks/remoting_dogfood_internal_dynamic_framework.iossimulator.zip.sha1
+++ b/ios/google_internal/frameworks/remoting_dogfood_internal_dynamic_framework.iossimulator.zip.sha1
@@ -1 +1 @@
-cfd0f19b181d39cfe0b45c6dc898da7e2d342602
\ No newline at end of file
+4014c0ced2e51ec21ea4bd3e5f80d8aebabd7278
\ No newline at end of file
diff --git a/ios/google_internal/frameworks/remoting_internal_dynamic_framework.ios.zip.sha1 b/ios/google_internal/frameworks/remoting_internal_dynamic_framework.ios.zip.sha1
index 4902416..1415951a 100644
--- a/ios/google_internal/frameworks/remoting_internal_dynamic_framework.ios.zip.sha1
+++ b/ios/google_internal/frameworks/remoting_internal_dynamic_framework.ios.zip.sha1
@@ -1 +1 @@
-0fdff4b920cbf6c4bb7ed2c1bfe14099bac6b4e4
\ No newline at end of file
+a231280d77561372f93503182f3468eb63dd861b
\ No newline at end of file
diff --git a/ios/google_internal/frameworks/remoting_internal_dynamic_framework.iossimulator.zip.sha1 b/ios/google_internal/frameworks/remoting_internal_dynamic_framework.iossimulator.zip.sha1
index d892d31..abe8b0f 100644
--- a/ios/google_internal/frameworks/remoting_internal_dynamic_framework.iossimulator.zip.sha1
+++ b/ios/google_internal/frameworks/remoting_internal_dynamic_framework.iossimulator.zip.sha1
@@ -1 +1 @@
-aa062a1a45d3e43d11838423b843a834583c942e
\ No newline at end of file
+616eb7482a752fda61a47e26342d9d2f5a76b47c
\ No newline at end of file
diff --git a/ios/google_internal/frameworks/web_view_shell_internal_dynamic_framework.ios.zip.sha1 b/ios/google_internal/frameworks/web_view_shell_internal_dynamic_framework.ios.zip.sha1
index df668ada..0fefa9e 100644
--- a/ios/google_internal/frameworks/web_view_shell_internal_dynamic_framework.ios.zip.sha1
+++ b/ios/google_internal/frameworks/web_view_shell_internal_dynamic_framework.ios.zip.sha1
@@ -1 +1 @@
-02099026bfbc43b621b22506c3ce5b2f5d7b6377
\ No newline at end of file
+d5ee80f988e539019e812e7ae31c095d79a1b6db
\ No newline at end of file
diff --git a/ios/google_internal/frameworks/web_view_shell_internal_dynamic_framework.iossimulator.zip.sha1 b/ios/google_internal/frameworks/web_view_shell_internal_dynamic_framework.iossimulator.zip.sha1
index 38256c33..6f209aa4 100644
--- a/ios/google_internal/frameworks/web_view_shell_internal_dynamic_framework.iossimulator.zip.sha1
+++ b/ios/google_internal/frameworks/web_view_shell_internal_dynamic_framework.iossimulator.zip.sha1
@@ -1 +1 @@
-289fedfab2f388899eae6fbeeddaa8daf70bf41e
\ No newline at end of file
+4e59880b1d969ec7d89bae72265d6639c1763d96
\ No newline at end of file
diff --git a/ios/web/find_in_page/resources/find_in_page.ts b/ios/web/find_in_page/resources/find_in_page.ts
index 28d0d41..8e3f86a 100644
--- a/ios/web/find_in_page/resources/find_in_page.ts
+++ b/ios/web/find_in_page/resources/find_in_page.ts
@@ -3,6 +3,179 @@
 // found in the LICENSE file.
 
 /**
+ * Class name of CSS element that selects a highlighted match with orange.
+ * @type {string}
+ */
+const CSS_CLASS_NAME_SELECT = 'find_selected';
+
+/**
+ * Returns the width of the document.body.  Sometimes though the body lies to
+ * try to make the page not break rails, so attempt to find those as well.
+ * An example: wikipedia pages for the ipad.
+ * @return {number} Width of the document body.
+ */
+function getBodyWidth_(): number {
+  let body = document.body;
+  let documentElement = document.documentElement;
+  return Math.max(
+      body.scrollWidth, documentElement.scrollWidth, body.offsetWidth,
+      documentElement.offsetWidth, body.clientWidth,
+      documentElement.clientWidth);
+};
+
+/**
+ * Returns the height of the document.body.  Sometimes though the body lies to
+ * try to make the page not break rails, so attempt to find those as well.
+ * An example: wikipedia pages for the ipad.
+ * @return {number} Height of the document body.
+ */
+function getBodyHeight_(): number {
+  let body = document.body;
+  let documentElement = document.documentElement;
+  return Math.max(
+      body.scrollHeight, documentElement.scrollHeight, body.offsetHeight,
+      documentElement.offsetHeight, body.clientHeight,
+      documentElement.clientHeight);
+};
+
+/**
+ * Helper function that determines if an element is visible.
+ * @param {Element} elem Element to check.
+ * @return {boolean} Whether elem is visible or not.
+ */
+function isElementVisible_(elem: HTMLElement): boolean {
+  if (!elem) {
+    return false;
+  }
+  let top = 0;
+  let left = 0;
+  let bottom = Infinity;
+  let right = Infinity;
+
+  let originalElement = elem;
+  let nextOffsetParent = originalElement.offsetParent;
+
+  // We are currently handling all scrolling through the app, which means we can
+  // only scroll the window, not any scrollable containers in the DOM itself. So
+  // for now this function returns false if the element is scrolled outside the
+  // viewable area of its ancestors.
+  // TODO(crbug.com/915357): handle scrolling within the DOM.
+  let bodyHeight = getBodyHeight_();
+  let bodyWidth = getBodyWidth_();
+
+  while (elem && elem.nodeName.toUpperCase() !== 'BODY') {
+    if (elem.style.display === 'none' || elem.style.visibility === 'hidden') {
+      return false;
+    }
+
+    // Check that there is a value set before converting to a Number, otherwise
+    // and empty string will convert to opacity zero and a visible item will be
+    // assumed hidden.
+    if (elem.style.opacity.length) {
+      const opacity = Number(elem.style.opacity);
+      if (!isNaN(opacity) && opacity === 0) {
+        return false;
+      }
+    }
+
+    if (elem.ownerDocument && elem.ownerDocument.defaultView) {
+      const computedStyle =
+          elem.ownerDocument.defaultView.getComputedStyle(elem, null);
+      if (computedStyle.display === 'none' ||
+          computedStyle.visibility === 'hidden') {
+        return false;
+      }
+
+      // Check that there is a value set before converting to a Number,
+      // otherwise and empty string will convert to opacity zero and a visible
+      // item will be assumed hidden.
+      if (computedStyle.opacity.length) {
+        const opacity = Number(computedStyle.opacity);
+        if (!isNaN(opacity) && opacity === 0) {
+          return false;
+        }
+      }
+    }
+
+    // For the original element and all ancestor offsetParents, trim down the
+    // visible area of the original element.
+    if (elem.isSameNode(originalElement) || elem.isSameNode(nextOffsetParent)) {
+      let visible = elem.getBoundingClientRect();
+      if (elem.style.overflow === 'hidden' &&
+          (visible.width === 0 || visible.height === 0))
+        return false;
+
+      top = Math.max(top, visible.top + window.pageYOffset);
+      bottom = Math.min(bottom, visible.bottom + window.pageYOffset);
+      left = Math.max(left, visible.left + window.pageXOffset);
+      right = Math.min(right, visible.right + window.pageXOffset);
+
+      // The element is not within the original viewport.
+      let notWithinViewport = top < 0 || left < 0;
+
+      // The element is flowing off the boundary of the page. Note this is
+      // not comparing to the size of the window, but the calculated offset
+      // size of the document body. This can happen if the element is within
+      // a scrollable container in the page.
+      let offPage = right > bodyWidth || bottom > bodyHeight;
+      if (notWithinViewport || offPage) {
+        return false;
+      }
+      nextOffsetParent = elem.offsetParent;
+    }
+
+    if (!(elem.parentNode instanceof HTMLElement)) {
+      break;
+    }
+
+    elem = elem.parentNode;
+  }
+  return true;
+};
+
+
+/**
+ * A Match represents a match result in the document. |this.nodes| stores all
+ * the <chrome_find> Nodes created for highlighting the matched text. If it
+ * contains only one Node, it means the match is found within one HTML TEXT
+ * Node, otherwise the match involves multiple HTML TEXT Nodes.
+ */
+class Match {
+  nodes: HTMLElement[] = [];
+
+  /**
+   * Returns if all <chrome_find> Nodes of this match are visible.
+   * @return {Boolean} If the Match is visible.
+   */
+  visible(): boolean {
+    for (const node of this.nodes) {
+      if (!isElementVisible_(node))
+        return false;
+    }
+    return true;
+  }
+
+  /**
+   * Adds orange color highlight for "selected match result", over the yellow
+   * color highlight for "normal match result".
+   */
+  addSelectHighlight(): void {
+    for (const node of this.nodes) {
+      node.classList.add(CSS_CLASS_NAME_SELECT);
+    }
+  }
+
+  /**
+   * Clears the orange color highlight.
+   */
+  removeSelectHighlight(): void {
+    for (const node of this.nodes) {
+      node.classList.remove(CSS_CLASS_NAME_SELECT);
+    }
+  }
+}
+
+/**
  * A Section contains the info of one TEXT node in the |allText_|. The node's
  * textContent is [begin, end) of |allText_|.
  */
@@ -23,4 +196,4 @@
   }
 }
 
-export {Section}
+export {CSS_CLASS_NAME_SELECT, Match, Section}
diff --git a/ios/web/find_in_page/resources/find_in_page_native_api.js b/ios/web/find_in_page/resources/find_in_page_native_api.js
index dfb5c753..881e913 100644
--- a/ios/web/find_in_page/resources/find_in_page_native_api.js
+++ b/ios/web/find_in_page/resources/find_in_page_native_api.js
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import {Section} from '//ios/web/find_in_page/resources/find_in_page.js';
+import {CSS_CLASS_NAME_SELECT, Match, Section} from '//ios/web/find_in_page/resources/find_in_page.js';
 
 /**
  * Based on code from the Google iOS app.
@@ -63,51 +63,6 @@
 }
 
 /**
- * A Match represents a match result in the document. |this.nodes| stores all
- * the <chrome_find> Nodes created for highlighting the matched text. If it
- * contains only one Node, it means the match is found within one HTML TEXT
- * Node, otherwise the match involves multiple HTML TEXT Nodes.
- */
-class Match {
-  constructor() {
-    this.nodes = [];
-  }
-
-  /**
-   * Returns if all <chrome_find> Nodes of this match are visible.
-   * @return {Boolean} If the Match is visible.
-   */
-  visible() {
-    for (let i = 0; i < this.nodes.length; ++i) {
-      if (!isElementVisible_(this.nodes[i]))
-        return false;
-    }
-    return true;
-  }
-
-  /**
-   * Adds orange color highlight for "selected match result", over the yellow
-   * color highlight for "normal match result".
-   * @return {undefined}
-   */
-  addSelectHighlight() {
-    for (let i = 0; i < this.nodes.length; ++i) {
-      this.nodes[i].classList.add(CSS_CLASS_NAME_SELECT);
-    }
-  }
-
-  /**
-   * Clears the orange color highlight.
-   * @return {undefined}
-   */
-  removeSelectHighlight() {
-    for (let i = 0; i < this.nodes.length; ++i) {
-      this.nodes[i].classList.remove(CSS_CLASS_NAME_SELECT);
-    }
-  }
-}
-
-/**
  * The list of all the matches in current page.
  * @type {Array<Match>}
  */
@@ -326,12 +281,6 @@
 const CSS_CLASS_NAME = 'find_in_page';
 
 /**
- * Class name of CSS element that selects a highlighted match with orange.
- * @type {string}
- */
-const CSS_CLASS_NAME_SELECT = 'find_selected';
-
-/**
  * ID of CSS style.
  * @type {string}
  */
@@ -770,103 +719,6 @@
 };
 
 /**
- * Returns the width of the document.body.  Sometimes though the body lies to
- * try to make the page not break rails, so attempt to find those as well.
- * An example: wikipedia pages for the ipad.
- * @return {number} Width of the document body.
- */
-function getBodyWidth_() {
-  let body = document.body;
-  let documentElement = document.documentElement;
-  return Math.max(
-      body.scrollWidth, documentElement.scrollWidth, body.offsetWidth,
-      documentElement.offsetWidth, body.clientWidth,
-      documentElement.clientWidth);
-};
-
-/**
- * Returns the height of the document.body.  Sometimes though the body lies to
- * try to make the page not break rails, so attempt to find those as well.
- * An example: wikipedia pages for the ipad.
- * @return {number} Height of the document body.
- */
-function getBodyHeight_() {
-  let body = document.body;
-  let documentElement = document.documentElement;
-  return Math.max(
-      body.scrollHeight, documentElement.scrollHeight, body.offsetHeight,
-      documentElement.offsetHeight, body.clientHeight,
-      documentElement.clientHeight);
-};
-
-/**
- * Helper function that determines if an element is visible.
- * @param {Element} elem Element to check.
- * @return {boolean} Whether elem is visible or not.
- */
-function isElementVisible_(elem) {
-  if (!elem) {
-    return false;
-  }
-  let top = 0;
-  let left = 0;
-  let bottom = Infinity;
-  let right = Infinity;
-
-  let originalElement = elem;
-  let nextOffsetParent = originalElement.offsetParent;
-
-  // We are currently handling all scrolling through the app, which means we can
-  // only scroll the window, not any scrollable containers in the DOM itself. So
-  // for now this function returns false if the element is scrolled outside the
-  // viewable area of its ancestors.
-  // TODO(crbug.com/915357): handle scrolling within the DOM.
-  let bodyHeight = getBodyHeight_();
-  let bodyWidth = getBodyWidth_();
-
-  while (elem && elem.nodeName.toUpperCase() != 'BODY') {
-    let computedStyle =
-        elem.ownerDocument.defaultView.getComputedStyle(elem, null);
-
-    if (elem.style.display === 'none' || elem.style.visibility === 'hidden' ||
-        elem.style.opacity === 0 || computedStyle.display === 'none' ||
-        computedStyle.visibility === 'hidden' || computedStyle.opacity === 0) {
-      return false;
-    }
-
-    // For the original element and all ancestor offsetParents, trim down the
-    // visible area of the original element.
-    if (elem.isSameNode(originalElement) || elem.isSameNode(nextOffsetParent)) {
-      let visible = elem.getBoundingClientRect();
-      if (elem.style.overflow === 'hidden' &&
-          (visible.width === 0 || visible.height === 0))
-        return false;
-
-      top = Math.max(top, visible.top + window.pageYOffset);
-      bottom = Math.min(bottom, visible.bottom + window.pageYOffset);
-      left = Math.max(left, visible.left + window.pageXOffset);
-      right = Math.min(right, visible.right + window.pageXOffset);
-
-      // The element is not within the original viewport.
-      let notWithinViewport = top < 0 || left < 0;
-
-      // The element is flowing off the boundary of the page. Note this is
-      // not comparing to the size of the window, but the calculated offset
-      // size of the document body. This can happen if the element is within
-      // a scrollable container in the page.
-      let offPage = right > bodyWidth || bottom > bodyHeight;
-      if (notWithinViewport || offPage) {
-        return false;
-      }
-      nextOffsetParent = elem.offsetParent;
-    }
-
-    elem = elem.parentNode;
-  }
-  return true;
-};
-
-/**
  * Helper function to find the absolute position of an element on the page.
  * @param {Element} elem Element to check.
  * @return {Array<number>} [x, y] positions.
diff --git a/ios/web/web_state/permissions_inttest.mm b/ios/web/web_state/permissions_inttest.mm
index 0cbcdef..1118dbc 100644
--- a/ios/web/web_state/permissions_inttest.mm
+++ b/ios/web/web_state/permissions_inttest.mm
@@ -3,7 +3,6 @@
 // found in the LICENSE file.
 
 #import "base/test/ios/wait_util.h"
-#include "base/test/scoped_feature_list.h"
 #include "ios/testing/scoped_block_swizzler.h"
 #import "ios/web/public/navigation/navigation_manager.h"
 #include "ios/web/public/navigation/reload_type.h"
@@ -117,7 +116,6 @@
   }
 
  protected:
-  base::test::ScopedFeatureList scoped_feature_list_;
   std::unique_ptr<ScopedBlockSwizzler> swizzler_;
   std::unique_ptr<net::EmbeddedTestServer> test_server_;
   testing::NiceMock<WebStateObserverMock> observer_;
diff --git a/media/cdm/cdm_paths_unittest.cc b/media/cdm/cdm_paths_unittest.cc
index 5113620..3a7d9d9b3 100644
--- a/media/cdm/cdm_paths_unittest.cc
+++ b/media/cdm/cdm_paths_unittest.cc
@@ -4,8 +4,11 @@
 
 #include "media/cdm/cdm_paths.h"
 
-#include "base/strings/string_split.h"
+#include <string>
+
+#include "base/files/file_path.h"
 #include "base/strings/string_util.h"
+#include "base/strings/stringprintf.h"
 #include "base/strings/utf_string_conversions.h"
 #include "build/build_config.h"
 #include "build/chromeos_buildflags.h"
@@ -16,9 +19,6 @@
 
 namespace {
 
-// Special path used in chrome components.
-const char kPlatformSpecific[] = "_platform_specific";
-
 // Name of the component platform.
 const char kComponentPlatform[] =
 #if BUILDFLAG(IS_MAC)
@@ -47,24 +47,10 @@
     "unsupported_arch";
 #endif
 
-base::FilePath GetExpectedPlatformSpecificDirectory(
-    const std::string& base_path) {
-  base::FilePath path;
-  const std::string kPlatformArch =
-      std::string(kComponentPlatform) + "_" + kComponentArch;
-  return path.AppendASCII(base_path)
-      .AppendASCII(kPlatformSpecific)
-      .AppendASCII(kPlatformArch);
-}
-
-std::string GetFlag() {
-  return BUILDFLAG(CDM_PLATFORM_SPECIFIC_PATH);
-}
-
 }  // namespace
 
 TEST(CdmPathsTest, FlagSpecified) {
-  EXPECT_FALSE(GetFlag().empty());
+  EXPECT_FALSE(std::string(BUILDFLAG(CDM_PLATFORM_SPECIFIC_PATH)).empty());
 }
 
 TEST(CdmPathsTest, Prefix) {
@@ -76,9 +62,17 @@
 }
 
 TEST(CdmPathsTest, Expected) {
-  const char kPrefix[] = "cdm";
-  EXPECT_EQ(GetExpectedPlatformSpecificDirectory(kPrefix),
-            GetPlatformSpecificDirectory(kPrefix));
+  // The same prefix that will be passed to the function under test followed by
+  // the special path used by Chrome's component updater.
+  std::string expected_path = base::StringPrintf(
+      "SomeCdm/_platform_specific/%s_%s", kComponentPlatform, kComponentArch);
+
+#if BUILDFLAG(IS_WIN)
+  EXPECT_TRUE(base::ReplaceChars(expected_path, "/", "\\", &expected_path));
+#endif
+
+  EXPECT_EQ(base::FilePath::FromUTF8Unsafe(expected_path),
+            GetPlatformSpecificDirectory("SomeCdm"));
 }
 
 }  // namespace media
diff --git a/media/gpu/v4l2/test/av1_decoder.cc b/media/gpu/v4l2/test/av1_decoder.cc
index 76e459d..09f128953 100644
--- a/media/gpu/v4l2/test/av1_decoder.cc
+++ b/media/gpu/v4l2/test/av1_decoder.cc
@@ -114,6 +114,172 @@
   v4l2_seq_params->max_frame_height_minus_1 = seq_header->max_frame_height - 1;
 }
 
+// 5.9.2. Uncompressed header syntax
+void FillFrameParams(struct v4l2_ctrl_av1_frame_header* v4l2_frame_params,
+                     const libgav1::ObuFrameHeader& frm_header) {
+  conditionally_set_u32_flags(&v4l2_frame_params->flags, frm_header.show_frame,
+                              V4L2_AV1_FRAME_HEADER_FLAG_SHOW_FRAME);
+  conditionally_set_u32_flags(&v4l2_frame_params->flags,
+                              frm_header.showable_frame,
+                              V4L2_AV1_FRAME_HEADER_FLAG_SHOWABLE_FRAME);
+  conditionally_set_u32_flags(&v4l2_frame_params->flags,
+                              frm_header.error_resilient_mode,
+                              V4L2_AV1_FRAME_HEADER_FLAG_ERROR_RESILIENT_MODE);
+  // libgav1 header has |enable_cdf_update| instead of |disable_cdf_update|.
+  conditionally_set_u32_flags(&v4l2_frame_params->flags,
+                              !frm_header.enable_cdf_update,
+                              V4L2_AV1_FRAME_HEADER_FLAG_DISABLE_CDF_UPDATE);
+  conditionally_set_u32_flags(
+      &v4l2_frame_params->flags, frm_header.allow_screen_content_tools,
+      V4L2_AV1_FRAME_HEADER_FLAG_ALLOW_SCREEN_CONTENT_TOOLS);
+  conditionally_set_u32_flags(&v4l2_frame_params->flags,
+                              frm_header.force_integer_mv,
+                              V4L2_AV1_FRAME_HEADER_FLAG_FORCE_INTEGER_MV);
+  conditionally_set_u32_flags(&v4l2_frame_params->flags,
+                              frm_header.allow_intrabc,
+                              V4L2_AV1_FRAME_HEADER_FLAG_ALLOW_INTRABC);
+  conditionally_set_u32_flags(&v4l2_frame_params->flags,
+                              frm_header.use_superres,
+                              V4L2_AV1_FRAME_HEADER_FLAG_USE_SUPERRES);
+  conditionally_set_u32_flags(
+      &v4l2_frame_params->flags, frm_header.allow_high_precision_mv,
+      V4L2_AV1_FRAME_HEADER_FLAG_ALLOW_HIGH_PRECISION_MV);
+  conditionally_set_u32_flags(
+      &v4l2_frame_params->flags, frm_header.is_motion_mode_switchable,
+      V4L2_AV1_FRAME_HEADER_FLAG_IS_MOTION_MODE_SWITCHABLE);
+  conditionally_set_u32_flags(&v4l2_frame_params->flags,
+                              frm_header.use_ref_frame_mvs,
+                              V4L2_AV1_FRAME_HEADER_FLAG_USE_REF_FRAME_MVS);
+  // libgav1 header has |enable_frame_end_update_cdf| instead.
+  conditionally_set_u32_flags(
+      &v4l2_frame_params->flags, !frm_header.enable_frame_end_update_cdf,
+      V4L2_AV1_FRAME_HEADER_FLAG_DISABLE_FRAME_END_UPDATE_CDF);
+  conditionally_set_u32_flags(&v4l2_frame_params->flags,
+                              frm_header.tile_info.uniform_spacing,
+                              V4L2_AV1_FRAME_HEADER_FLAG_UNIFORM_TILE_SPACING);
+  conditionally_set_u32_flags(&v4l2_frame_params->flags,
+                              frm_header.allow_warped_motion,
+                              V4L2_AV1_FRAME_HEADER_FLAG_ALLOW_WARPED_MOTION);
+  conditionally_set_u32_flags(&v4l2_frame_params->flags,
+                              frm_header.reference_mode_select,
+                              V4L2_AV1_FRAME_HEADER_FLAG_REFERENCE_SELECT);
+  conditionally_set_u32_flags(&v4l2_frame_params->flags,
+                              frm_header.reduced_tx_set,
+                              V4L2_AV1_FRAME_HEADER_FLAG_REDUCED_TX_SET);
+  conditionally_set_u32_flags(&v4l2_frame_params->flags,
+                              frm_header.skip_mode_frame[0] > 0,
+                              V4L2_AV1_FRAME_HEADER_FLAG_SKIP_MODE_ALLOWED);
+  conditionally_set_u32_flags(&v4l2_frame_params->flags,
+                              frm_header.skip_mode_present,
+                              V4L2_AV1_FRAME_HEADER_FLAG_SKIP_MODE_PRESENT);
+  conditionally_set_u32_flags(&v4l2_frame_params->flags,
+                              frm_header.frame_size_override_flag,
+                              V4L2_AV1_FRAME_HEADER_FLAG_FRAME_SIZE_OVERRIDE);
+  // libgav1 header doesn't have |buffer_removal_time_present_flag|.
+  conditionally_set_u32_flags(
+      &v4l2_frame_params->flags, frm_header.buffer_removal_time[0] > 0,
+      V4L2_AV1_FRAME_HEADER_FLAG_BUFFER_REMOVAL_TIME_PRESENT);
+  conditionally_set_u32_flags(
+      &v4l2_frame_params->flags, frm_header.frame_refs_short_signaling,
+      V4L2_AV1_FRAME_HEADER_FLAG_FRAME_REFS_SHORT_SIGNALING);
+
+  switch (frm_header.frame_type) {
+    case libgav1::kFrameKey:
+      v4l2_frame_params->frame_type = V4L2_AV1_KEY_FRAME;
+      break;
+    case libgav1::kFrameInter:
+      v4l2_frame_params->frame_type = V4L2_AV1_INTER_FRAME;
+      break;
+    case libgav1::kFrameIntraOnly:
+      v4l2_frame_params->frame_type = V4L2_AV1_INTRA_ONLY_FRAME;
+      break;
+    case libgav1::kFrameSwitch:
+      v4l2_frame_params->frame_type = V4L2_AV1_SWITCH_FRAME;
+      break;
+    default:
+      NOTREACHED() << "Invalid frame type, " << frm_header.frame_type;
+  }
+
+  v4l2_frame_params->order_hint = frm_header.order_hint;
+  v4l2_frame_params->superres_denom = frm_header.superres_scale_denominator;
+  v4l2_frame_params->upscaled_width = frm_header.upscaled_width;
+
+  switch (frm_header.interpolation_filter) {
+    case libgav1::kInterpolationFilterEightTap:
+      v4l2_frame_params->interpolation_filter =
+          V4L2_AV1_INTERPOLATION_FILTER_EIGHTTAP;
+      break;
+    case libgav1::kInterpolationFilterEightTapSmooth:
+      v4l2_frame_params->interpolation_filter =
+          V4L2_AV1_INTERPOLATION_FILTER_EIGHTTAP_SMOOTH;
+      break;
+    case libgav1::kInterpolationFilterEightTapSharp:
+      v4l2_frame_params->interpolation_filter =
+          V4L2_AV1_INTERPOLATION_FILTER_EIGHTTAP_SHARP;
+      break;
+    case libgav1::kInterpolationFilterBilinear:
+      v4l2_frame_params->interpolation_filter =
+          V4L2_AV1_INTERPOLATION_FILTER_BILINEAR;
+      break;
+    case libgav1::kInterpolationFilterSwitchable:
+      v4l2_frame_params->interpolation_filter =
+          V4L2_AV1_INTERPOLATION_FILTER_SWITCHABLE;
+      break;
+    default:
+      NOTREACHED() << "Invalid interpolation filter, "
+                   << frm_header.interpolation_filter;
+  }
+
+  switch (frm_header.tx_mode) {
+    case libgav1::kTxModeOnly4x4:
+      v4l2_frame_params->tx_mode = V4L2_AV1_TX_MODE_ONLY_4X4;
+      break;
+    case libgav1::kTxModeLargest:
+      v4l2_frame_params->tx_mode = V4L2_AV1_TX_MODE_LARGEST;
+      break;
+    case libgav1::kTxModeSelect:
+      v4l2_frame_params->tx_mode = V4L2_AV1_TX_MODE_SELECT;
+      break;
+    default:
+      NOTREACHED() << "Invalid tx mode, " << frm_header.tx_mode;
+  }
+
+  v4l2_frame_params->frame_width_minus_1 = frm_header.width - 1;
+  v4l2_frame_params->frame_height_minus_1 = frm_header.height - 1;
+  v4l2_frame_params->render_width_minus_1 = frm_header.render_width - 1;
+  v4l2_frame_params->render_height_minus_1 = frm_header.render_height - 1;
+
+  v4l2_frame_params->current_frame_id = frm_header.current_frame_id;
+  v4l2_frame_params->primary_ref_frame = frm_header.primary_reference_frame;
+  SafeArrayMemcpy(v4l2_frame_params->buffer_removal_time,
+                  frm_header.buffer_removal_time);
+  v4l2_frame_params->refresh_frame_flags = frm_header.refresh_frame_flags;
+
+  static_assert(std::size(decltype(v4l2_frame_params->order_hints){}) ==
+                    libgav1::kNumReferenceFrameTypes,
+                "Invalid size of |order_hints| array");
+  for (size_t i = 0; i < libgav1::kNumReferenceFrameTypes; i++)
+    v4l2_frame_params->order_hints[i] =
+        base::checked_cast<__u32>(frm_header.reference_order_hint[i]);
+
+  v4l2_frame_params->last_frame_idx =
+      frm_header.reference_frame_index[libgav1::kReferenceFrameLast];
+  v4l2_frame_params->gold_frame_idx =
+      frm_header.reference_frame_index[libgav1::kReferenceFrameGolden];
+
+  static_assert(std::size(decltype(v4l2_frame_params->ref_frame_idx){}) ==
+                    libgav1::kNumInterReferenceFrameTypes,
+                "Invalid size of |ref_frame_idx| array");
+  for (size_t i = 0; i < libgav1::kNumInterReferenceFrameTypes; i++)
+    v4l2_frame_params->ref_frame_idx[i] =
+        base::checked_cast<__u64>(frm_header.reference_frame_index[i]);
+
+  v4l2_frame_params->skip_mode_frame[0] =
+      base::checked_cast<__u8>(frm_header.skip_mode_frame[0]);
+  v4l2_frame_params->skip_mode_frame[1] =
+      base::checked_cast<__u8>(frm_header.skip_mode_frame[1]);
+}
+
 // Section 5.9.11. Loop filter params syntax.
 // Note that |update_ref_delta| and |update_mode_delta| flags in the spec
 // are not needed for V4L2 AV1 API.
@@ -733,6 +899,8 @@
   FillGlobalMotionParams(&v4l2_frame_params.global_motion,
                          current_frame_header.global_motion);
 
+  FillFrameParams(&v4l2_frame_params, current_frame_header);
+
   // TODO(stevecho): V4L2_CID_STATELESS_AV1_FRAME_HEADER is trending to be
   // changed to V4L2_CID_STATELESS_AV1_FRAME
   ext_ctrl_vectors.push_back({.id = V4L2_CID_STATELESS_AV1_FRAME_HEADER,
diff --git a/net/cookies/cookie_access_delegate.h b/net/cookies/cookie_access_delegate.h
index 71878fad..76b4d59 100644
--- a/net/cookies/cookie_access_delegate.h
+++ b/net/cookies/cookie_access_delegate.h
@@ -108,19 +108,6 @@
       const CookieAccessDelegate* delegate,
       const CookiePartitionKey& cookie_partition_key,
       base::OnceCallback<void(CookiePartitionKey)> callback);
-
-  // Computes the First-Party Sets.
-  //
-  // This may return a result synchronously, or asynchronously invoke `callback`
-  // with the result. The callback will be invoked iff the return value is
-  // nullopt; i.e. a result will be provided via return value or callback, but
-  // not both, and not neither.
-  [[nodiscard]] virtual absl::optional<
-      base::flat_map<net::SchemefulSite, std::set<net::SchemefulSite>>>
-  RetrieveFirstPartySets(
-      base::OnceCallback<void(
-          base::flat_map<net::SchemefulSite, std::set<net::SchemefulSite>>)>
-          callback) const = 0;
 };
 
 }  // namespace net
diff --git a/net/cookies/cookie_monster.cc b/net/cookies/cookie_monster.cc
index 644e5fc..b44e3eba 100644
--- a/net/cookies/cookie_monster.cc
+++ b/net/cookies/cookie_monster.cc
@@ -59,6 +59,7 @@
 #include "base/metrics/histogram_functions.h"
 #include "base/metrics/histogram_macros.h"
 #include "base/ranges/algorithm.h"
+#include "base/strings/strcat.h"
 #include "base/strings/string_piece.h"
 #include "base/strings/string_util.h"
 #include "base/strings/stringprintf.h"
@@ -82,6 +83,7 @@
 #include "url/origin.h"
 #include "url/third_party/mozilla/url_parse.h"
 #include "url/url_canon.h"
+#include "url/url_constants.h"
 
 using base::Time;
 using base::TimeTicks;
@@ -2238,8 +2240,20 @@
   base::UmaHistogramCounts100000("Cookie.Count2", cookies_.size());
 
   if (cookie_access_delegate()) {
-    absl::optional<base::flat_map<SchemefulSite, std::set<SchemefulSite>>>
-        maybe_sets = cookie_access_delegate()->RetrieveFirstPartySets(
+    std::vector<SchemefulSite> sites;
+    for (const auto& entry : cookies_) {
+      sites.emplace_back(
+          GURL(base::StrCat({url::kHttpsScheme, "://", entry.first})));
+    }
+    for (const auto& [partition_key, cookie_map] : partitioned_cookies_) {
+      for (const auto& [domain, unused_cookie] : *cookie_map) {
+        sites.emplace_back(
+            GURL(base::StrCat({url::kHttpsScheme, "://", domain})));
+      }
+    }
+    absl::optional<base::flat_map<SchemefulSite, SchemefulSite>> maybe_sets =
+        cookie_access_delegate()->FindFirstPartySetOwners(
+            sites,
             base::BindOnce(&CookieMonster::RecordPeriodicFirstPartySetsStats,
                            weak_ptr_factory_.GetWeakPtr()));
     if (maybe_sets.has_value())
@@ -2264,8 +2278,12 @@
 }
 
 void CookieMonster::RecordPeriodicFirstPartySetsStats(
-    base::flat_map<SchemefulSite, std::set<SchemefulSite>> sets) const {
-  for (const auto& set : sets) {
+    base::flat_map<SchemefulSite, SchemefulSite> sets) const {
+  base::flat_map<SchemefulSite, std::set<SchemefulSite>> grouped_by_owner;
+  for (const auto& [site, owner] : sets) {
+    grouped_by_owner[owner].insert(site);
+  }
+  for (const auto& set : grouped_by_owner) {
     int sample = std::accumulate(
         set.second.begin(), set.second.end(), 0,
         [this](int acc, const net::SchemefulSite& site) -> int {
diff --git a/net/cookies/cookie_monster.h b/net/cookies/cookie_monster.h
index d2cfebf..278c842 100644
--- a/net/cookies/cookie_monster.h
+++ b/net/cookies/cookie_monster.h
@@ -688,10 +688,8 @@
   // First-Party Sets presents a potentially asynchronous interface, these stats
   // may be collected asynchronously w.r.t. the rest of the stats collected by
   // `RecordPeriodicStats`.
-  // TODO(https://crbug.com/1266014): don't assume that the sets can all fit in
-  // memory at once.
   void RecordPeriodicFirstPartySetsStats(
-      base::flat_map<SchemefulSite, std::set<SchemefulSite>> sets) const;
+      base::flat_map<SchemefulSite, SchemefulSite> sets) const;
 
   // Defers the callback until the full coookie database has been loaded. If
   // it's already been loaded, runs the callback synchronously.
diff --git a/net/cookies/cookie_monster_unittest.cc b/net/cookies/cookie_monster_unittest.cc
index 2d0595e1..2fd523d6 100644
--- a/net/cookies/cookie_monster_unittest.cc
+++ b/net/cookies/cookie_monster_unittest.cc
@@ -5337,8 +5337,11 @@
   base::HistogramTester histogram_tester;
   EXPECT_TRUE(cm()->DoRecordPeriodicStatsForTesting());
   EXPECT_THAT(histogram_tester.GetAllSamples("Cookie.PerFirstPartySetCount"),
-              testing::ElementsAre(base::Bucket(2 /* min */, 1 /* samples */),
-                                   base::Bucket(3 /* min */, 1 /* samples */)));
+              testing::ElementsAre(  //
+                                     // owner2.test & member3.test
+                  base::Bucket(2 /* min */, 1 /* samples */),
+                  // owner1.test, member1.test, & member2.test
+                  base::Bucket(3 /* min */, 1 /* samples */)));
 }
 
 TEST_F(CookieMonsterTest, GetAllCookiesForURLNonce) {
diff --git a/net/cookies/test_cookie_access_delegate.cc b/net/cookies/test_cookie_access_delegate.cc
index 5b751f26..1bf7de1d 100644
--- a/net/cookies/test_cookie_access_delegate.cc
+++ b/net/cookies/test_cookie_access_delegate.cc
@@ -101,14 +101,6 @@
       mapping, std::move(callback));
 }
 
-absl::optional<base::flat_map<SchemefulSite, std::set<SchemefulSite>>>
-TestCookieAccessDelegate::RetrieveFirstPartySets(
-    base::OnceCallback<
-        void(base::flat_map<SchemefulSite, std::set<SchemefulSite>>)> callback)
-    const {
-  return RunMaybeAsync(first_party_sets_, std::move(callback));
-}
-
 template <class T>
 absl::optional<T> TestCookieAccessDelegate::RunMaybeAsync(
     T result,
diff --git a/net/cookies/test_cookie_access_delegate.h b/net/cookies/test_cookie_access_delegate.h
index 969447f..5293a416 100644
--- a/net/cookies/test_cookie_access_delegate.h
+++ b/net/cookies/test_cookie_access_delegate.h
@@ -55,11 +55,6 @@
       const base::flat_set<SchemefulSite>& sites,
       base::OnceCallback<void(base::flat_map<SchemefulSite, SchemefulSite>)>
           callback) const override;
-  absl::optional<base::flat_map<SchemefulSite, std::set<SchemefulSite>>>
-  RetrieveFirstPartySets(
-      base::OnceCallback<void(
-          base::flat_map<SchemefulSite, std::set<SchemefulSite>>)> callback)
-      const override;
 
   // Sets the expected return value for any cookie whose Domain
   // matches |cookie_domain|. Pass the value of |cookie.Domain()| and any
diff --git a/services/network/cookie_access_delegate_impl.cc b/services/network/cookie_access_delegate_impl.cc
index e58d9a8..18acdee 100644
--- a/services/network/cookie_access_delegate_impl.cc
+++ b/services/network/cookie_access_delegate_impl.cc
@@ -96,13 +96,4 @@
                                                        std::move(callback));
 }
 
-absl::optional<FirstPartySetsAccessDelegate::SetsByOwner>
-CookieAccessDelegateImpl::RetrieveFirstPartySets(
-    base::OnceCallback<void(FirstPartySetsAccessDelegate::SetsByOwner)>
-        callback) const {
-  if (!first_party_sets_access_delegate_)
-    return {{}};
-  return first_party_sets_access_delegate_->Sets(std::move(callback));
-}
-
 }  // namespace network
diff --git a/services/network/cookie_access_delegate_impl.h b/services/network/cookie_access_delegate_impl.h
index 15461367..022a370 100644
--- a/services/network/cookie_access_delegate_impl.h
+++ b/services/network/cookie_access_delegate_impl.h
@@ -72,10 +72,6 @@
       const base::flat_set<net::SchemefulSite>& sites,
       base::OnceCallback<void(FirstPartySetsAccessDelegate::OwnersResult)>
           callback) const override;
-  [[nodiscard]] absl::optional<FirstPartySetsAccessDelegate::SetsByOwner>
-  RetrieveFirstPartySets(
-      base::OnceCallback<void(FirstPartySetsAccessDelegate::SetsByOwner)>
-          callback) const override;
 
  private:
   const mojom::CookieAccessDelegateType type_;
diff --git a/services/network/cookie_access_delegate_impl_unittest.cc b/services/network/cookie_access_delegate_impl_unittest.cc
index 55262b2..8c0c9405a 100644
--- a/services/network/cookie_access_delegate_impl_unittest.cc
+++ b/services/network/cookie_access_delegate_impl_unittest.cc
@@ -63,10 +63,6 @@
           {site},
           base::BindOnce([](FirstPartySetsManager::OwnersResult) { FAIL(); })),
       Optional(IsEmpty()));
-
-  EXPECT_THAT(delegate().RetrieveFirstPartySets(base::BindOnce(
-                  [](FirstPartySetsManager::SetsByOwner) { FAIL(); })),
-              Optional(IsEmpty()));
 }
 
 }  // namespace
diff --git a/services/network/first_party_sets/first_party_sets_access_delegate.cc b/services/network/first_party_sets/first_party_sets_access_delegate.cc
index a5e9bd7..5354341 100644
--- a/services/network/first_party_sets/first_party_sets_access_delegate.cc
+++ b/services/network/first_party_sets/first_party_sets_access_delegate.cc
@@ -65,24 +65,6 @@
                                    context_config_, std::move(callback));
 }
 
-absl::optional<FirstPartySetsAccessDelegate::SetsByOwner>
-FirstPartySetsAccessDelegate::Sets(
-    base::OnceCallback<void(FirstPartySetsAccessDelegate::SetsByOwner)>
-        callback) {
-  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  if (pending_queries_) {
-    // base::Unretained() is safe because `this` owns `pending_queries_` and
-    // `pending_queries_` will not run the enqueued callbacks after `this` is
-    // destroyed.
-    EnqueuePendingQuery(
-        base::BindOnce(&FirstPartySetsAccessDelegate::SetsAndInvoke,
-                       base::Unretained(this), std::move(callback)));
-    return absl::nullopt;
-  }
-
-  return manager_->Sets(context_config_, std::move(callback));
-}
-
 absl::optional<FirstPartySetsAccessDelegate::OwnerResult>
 FirstPartySetsAccessDelegate::FindOwner(
     const net::SchemefulSite& site,
@@ -140,21 +122,6 @@
     std::move(callbacks.second).Run(std::move(sync_result.value()));
 }
 
-void FirstPartySetsAccessDelegate::SetsAndInvoke(
-    base::OnceCallback<void(FirstPartySetsAccessDelegate::SetsByOwner)>
-        callback) const {
-  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  std::pair<base::OnceCallback<void(FirstPartySetsAccessDelegate::SetsByOwner)>,
-            base::OnceCallback<void(FirstPartySetsAccessDelegate::SetsByOwner)>>
-      callbacks = base::SplitOnceCallback(std::move(callback));
-
-  absl::optional<FirstPartySetsAccessDelegate::SetsByOwner> sync_result =
-      manager_->Sets(context_config_, std::move(callbacks.first));
-
-  if (sync_result.has_value())
-    std::move(callbacks.second).Run(std::move(sync_result.value()));
-}
-
 void FirstPartySetsAccessDelegate::FindOwnerAndInvoke(
     const net::SchemefulSite& site,
     base::OnceCallback<void(FirstPartySetsAccessDelegate::OwnerResult)>
diff --git a/services/network/first_party_sets/first_party_sets_access_delegate.h b/services/network/first_party_sets/first_party_sets_access_delegate.h
index ed68355..91a1fd4 100644
--- a/services/network/first_party_sets/first_party_sets_access_delegate.h
+++ b/services/network/first_party_sets/first_party_sets_access_delegate.h
@@ -30,8 +30,6 @@
 class FirstPartySetsAccessDelegate
     : public mojom::FirstPartySetsAccessDelegate {
  public:
-  using SetsByOwner =
-      base::flat_map<net::SchemefulSite, std::set<net::SchemefulSite>>;
   using OwnerResult = absl::optional<net::SchemefulSite>;
   using OwnersResult = base::flat_map<net::SchemefulSite, net::SchemefulSite>;
   using FlattenedSets = base::flat_map<net::SchemefulSite, net::SchemefulSite>;
@@ -70,16 +68,6 @@
       const std::set<net::SchemefulSite>& party_context,
       base::OnceCallback<void(net::FirstPartySetMetadata)> callback);
 
-  // Computes a mapping from owner to set members. For convenience of iteration,
-  // the members of the set includes the owner.
-  //
-  // This may return a result synchronously, or asynchronously invoke `callback`
-  // with the result. The callback will be invoked iff the return value is
-  // nullopt; i.e. a result will be provided via return value or callback, but
-  // not both, and not neither.
-  [[nodiscard]] absl::optional<SetsByOwner> Sets(
-      base::OnceCallback<void(SetsByOwner)> callback);
-
   // Returns optional(nullopt) if First-Party Sets is disabled or if the input
   // is not in a nontrivial set.
   // If FPS is enabled and the input site is in a nontrivial set, then this
@@ -121,10 +109,6 @@
       const std::set<net::SchemefulSite>& party_context,
       base::OnceCallback<void(net::FirstPartySetMetadata)> callback) const;
 
-  // Same as `Sets`, but plumbs the result into the callback. Must only be
-  // called once the instance is fully initialized.
-  void SetsAndInvoke(base::OnceCallback<void(SetsByOwner)> callback) const;
-
   // Same as `FindOwner`, but plumbs the result into the callback. Must only be
   // called once the instance is fully initialized.
   void FindOwnerAndInvoke(const net::SchemefulSite& site,
diff --git a/services/network/first_party_sets/first_party_sets_access_delegate_unittest.cc b/services/network/first_party_sets/first_party_sets_access_delegate_unittest.cc
index b0120385..de20ff54 100644
--- a/services/network/first_party_sets/first_party_sets_access_delegate_unittest.cc
+++ b/services/network/first_party_sets/first_party_sets_access_delegate_unittest.cc
@@ -32,13 +32,13 @@
 
 namespace {
 
-const net::SchemefulSite kSet1Owner(GURL("https://example.test"));
-const net::SchemefulSite kSet1Member1(GURL("https://member1.test"));
-const net::SchemefulSite kSet1Member2(GURL("https://member3.test"));
-const net::SchemefulSite kSet2Owner(GURL("https://foo.test"));
-const net::SchemefulSite kSet2Member1(GURL("https://member2.test"));
-const net::SchemefulSite kSet3Owner(GURL("https://bar.test"));
-const net::SchemefulSite kSet3Member1(GURL("https://member4.test"));
+const net::SchemefulSite kSet1Owner(GURL("https://set1owner.test"));
+const net::SchemefulSite kSet1Member1(GURL("https://set1member1.test"));
+const net::SchemefulSite kSet1Member2(GURL("https://set1member2.test"));
+const net::SchemefulSite kSet2Owner(GURL("https://set2owner.test"));
+const net::SchemefulSite kSet2Member1(GURL("https://set2member1.test"));
+const net::SchemefulSite kSet3Owner(GURL("https://set3owner.test"));
+const net::SchemefulSite kSet3Member1(GURL("https://set3member1.test"));
 
 mojom::FirstPartySetsAccessDelegateParamsPtr
 CreateFirstPartySetsAccessDelegateParams(bool enabled) {
@@ -95,14 +95,6 @@
       net::SamePartyContext(Type::kSameParty));
 }
 
-TEST_F(NoopFirstPartySetsAccessDelegateTest, Sets) {
-  EXPECT_THAT(delegate().Sets(base::NullCallback()),
-              FirstPartySetsAccessDelegate::SetsByOwner({
-                  {kSet1Owner, {kSet1Owner, kSet1Member1, kSet1Member2}},
-                  {kSet2Owner, {kSet2Owner, kSet2Member1}},
-              }));
-}
-
 TEST_F(NoopFirstPartySetsAccessDelegateTest, FindOwner) {
   EXPECT_THAT(delegate().FindOwner(kSet1Owner, base::NullCallback()),
               absl::make_optional(kSet1Owner));
@@ -146,13 +138,6 @@
     return result.has_value() ? std::move(result).value() : future.Take();
   }
 
-  FirstPartySetsAccessDelegate::SetsByOwner SetsAndWait() {
-    base::test::TestFuture<FirstPartySetsAccessDelegate::SetsByOwner> future;
-    absl::optional<FirstPartySetsAccessDelegate::SetsByOwner> result =
-        delegate_.Sets(future.GetCallback());
-    return result.has_value() ? result.value() : future.Get();
-  }
-
   FirstPartySetsAccessDelegate::OwnerResult FindOwnerAndWait(
       const net::SchemefulSite& site) {
     base::test::TestFuture<FirstPartySetsAccessDelegate::OwnerResult> future;
@@ -197,10 +182,6 @@
                                     Type::kSameParty));
 }
 
-TEST_F(FirstPartySetsAccessDelegateDisabledTest, Sets_IsEmpty) {
-  EXPECT_THAT(SetsAndWait(), IsEmpty());
-}
-
 TEST_F(FirstPartySetsAccessDelegateDisabledTest, FindOwner) {
   EXPECT_FALSE(FindOwnerAndWait(kSet1Owner));
   EXPECT_FALSE(FindOwnerAndWait(kSet1Member1));
@@ -236,19 +217,6 @@
                                        &kSet1Owner, &kSet1Owner));
 }
 
-TEST_F(AsyncFirstPartySetsAccessDelegateTest, QueryBeforeReady_Sets) {
-  base::test::TestFuture<FirstPartySetsAccessDelegate::SetsByOwner> future;
-  EXPECT_FALSE(delegate().Sets(future.GetCallback()));
-
-  delegate_remote()->NotifyReady(mojom::FirstPartySetsReadyEvent::New());
-
-  EXPECT_THAT(future.Get(),
-              FirstPartySetsAccessDelegate::SetsByOwner({
-                  {kSet1Owner, {kSet1Owner, kSet1Member1, kSet1Member2}},
-                  {kSet2Owner, {kSet2Owner, kSet2Member1}},
-              }));
-}
-
 TEST_F(AsyncFirstPartySetsAccessDelegateTest, QueryBeforeReady_FindOwner) {
   base::test::TestFuture<FirstPartySetsAccessDelegate::OwnerResult> future;
   EXPECT_FALSE(delegate().FindOwner(kSet1Member1, future.GetCallback()));
@@ -294,9 +262,19 @@
                                        &kSet3Owner, &kSet3Owner));
 }
 
-TEST_F(AsyncFirstPartySetsAccessDelegateTest, OverrideSets_Sets) {
-  base::test::TestFuture<FirstPartySetsAccessDelegate::SetsByOwner> future;
-  EXPECT_FALSE(delegate().Sets(future.GetCallback()));
+TEST_F(AsyncFirstPartySetsAccessDelegateTest, OverrideSets_MemberIsOwner) {
+  base::test::TestFuture<FirstPartySetsAccessDelegate::OwnersResult> future;
+  EXPECT_FALSE(delegate().FindOwners(
+      {
+          kSet1Member1,
+          kSet1Member2,
+          kSet1Owner,
+          kSet2Member1,
+          kSet2Owner,
+          kSet3Member1,
+          kSet3Owner,
+      },
+      future.GetCallback()));
   // The member of an override set is also an owner of an existing set as an
   // addition.
   delegate_remote()->NotifyReady(CreateFirstPartySetsReadyEvent({
@@ -307,13 +285,16 @@
       {kSet3Owner, {kSet3Owner}},
   }));
 
-  EXPECT_THAT(
-      future.Get(),
-      FirstPartySetsAccessDelegate::SetsByOwner({
-          {kSet2Owner, {kSet2Owner, kSet2Member1}},
-          {kSet3Owner,
-           {kSet3Owner, kSet3Member1, kSet1Owner, kSet1Member1, kSet1Member2}},
-      }));
+  EXPECT_EQ(future.Get(), FirstPartySetsAccessDelegate::OwnersResult({
+                              {kSet2Owner, kSet2Owner},
+                              {kSet2Member1, kSet2Owner},
+                              {kSet3Owner, kSet3Owner},
+                              {kSet2Member1, kSet3Owner},
+                              {kSet1Owner, kSet3Owner},
+                              {kSet1Member1, kSet3Owner},
+                              {kSet1Member2, kSet3Owner},
+                              {kSet3Member1, kSet3Owner},
+                          }));
 }
 
 TEST_F(AsyncFirstPartySetsAccessDelegateTest, OverrideSets_FindOwner) {
@@ -366,15 +347,6 @@
                                        &kSet1Owner, &kSet1Owner));
 }
 
-TEST_F(SyncFirstPartySetsAccessDelegateTest, Sets) {
-  EXPECT_THAT(SetsAndWait(),
-              FirstPartySetsAccessDelegate::SetsByOwner({
-                  {kSet1Owner, {kSet1Owner, kSet1Member1, kSet1Member2}},
-                  {kSet2Owner, {kSet2Owner, kSet2Member1}},
-                  {kSet3Owner, {kSet3Owner, kSet3Member1}},
-              }));
-}
-
 TEST_F(SyncFirstPartySetsAccessDelegateTest, FindOwner) {
   EXPECT_THAT(FindOwnerAndWait(kSet1Member1), absl::make_optional(kSet1Owner));
 }
diff --git a/services/network/first_party_sets/first_party_sets_manager.cc b/services/network/first_party_sets/first_party_sets_manager.cc
index 9e7acd48..4f16dcb 100644
--- a/services/network/first_party_sets/first_party_sets_manager.cc
+++ b/services/network/first_party_sets/first_party_sets_manager.cc
@@ -12,7 +12,6 @@
 
 #include "base/check.h"
 #include "base/containers/circular_deque.h"
-#include "base/containers/contains.h"
 #include "base/containers/flat_map.h"
 #include "base/metrics/histogram_functions.h"
 #include "base/metrics/histogram_macros.h"
@@ -278,68 +277,6 @@
   return sites_to_owners;
 }
 
-absl::optional<FirstPartySetsManager::SetsByOwner> FirstPartySetsManager::Sets(
-    const FirstPartySetsContextConfig& fps_context_config,
-    base::OnceCallback<void(FirstPartySetsManager::SetsByOwner)> callback) {
-  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-
-  if (!sets_.has_value()) {
-    EnqueuePendingQuery(base::BindOnce(
-        &FirstPartySetsManager::SetsAndInvoke, weak_factory_.GetWeakPtr(),
-        fps_context_config, std::move(callback), base::TimeTicks::Now()));
-    return absl::nullopt;
-  }
-
-  return SetsInternal(fps_context_config);
-}
-
-void FirstPartySetsManager::SetsAndInvoke(
-    const FirstPartySetsContextConfig& fps_context_config,
-    base::OnceCallback<void(FirstPartySetsManager::SetsByOwner)> callback,
-    base::TimeTicks enqueued_at) const {
-  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  DCHECK(sets_.has_value());
-
-  UMA_HISTOGRAM_TIMES("Cookie.FirstPartySets.EnqueueingDelay.Sets",
-                      base::TimeTicks::Now() - enqueued_at);
-
-  std::move(callback).Run(SetsInternal(fps_context_config));
-}
-
-FirstPartySetsManager::SetsByOwner FirstPartySetsManager::SetsInternal(
-    const FirstPartySetsContextConfig& fps_context_config) const {
-  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  DCHECK(sets_.has_value());
-
-  if (!fps_context_config.is_enabled() || !is_enabled())
-    return {};
-
-  FirstPartySetsManager::SetsByOwner sets;
-  // Go over `sets_` to add entries that are not modified by the customizations.
-  for (const auto& pair : *sets_) {
-    const net::SchemefulSite& member = pair.first;
-    const net::SchemefulSite& owner = pair.second;
-    if (!base::Contains(fps_context_config.customizations(), member)) {
-      auto set = sets.emplace(owner, std::set<net::SchemefulSite>()).first;
-      set->second.insert(member);
-    }
-  }
-
-  // Then go over the customizations to add entries that are not deleted.
-  for (const auto& pair : fps_context_config.customizations()) {
-    const net::SchemefulSite& member = pair.first;
-    const absl::optional<net::SchemefulSite>& owner = pair.second;
-    if (owner.has_value()) {
-      auto set =
-          sets.emplace(std::move(owner.value()), std::set<net::SchemefulSite>())
-              .first;
-      set->second.insert(member);
-    }
-  }
-
-  return sets;
-}
-
 void FirstPartySetsManager::InvokePendingQueries() {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   DCHECK(sets_.has_value());
diff --git a/services/network/first_party_sets/first_party_sets_manager.h b/services/network/first_party_sets/first_party_sets_manager.h
index 8be54191..600b306 100644
--- a/services/network/first_party_sets/first_party_sets_manager.h
+++ b/services/network/first_party_sets/first_party_sets_manager.h
@@ -28,8 +28,6 @@
 // answers queries about First-Party Sets after they've been loaded.
 class FirstPartySetsManager {
  public:
-  using SetsByOwner =
-      base::flat_map<net::SchemefulSite, std::set<net::SchemefulSite>>;
   using OwnerResult = absl::optional<net::SchemefulSite>;
   using OwnersResult = base::flat_map<net::SchemefulSite, net::SchemefulSite>;
   using FlattenedSets = base::flat_map<net::SchemefulSite, net::SchemefulSite>;
@@ -58,17 +56,6 @@
       const FirstPartySetsContextConfig& fps_context_config,
       base::OnceCallback<void(net::FirstPartySetMetadata)> callback);
 
-  // Computes a mapping from owner to set members. For convenience of iteration,
-  // the members of the set includes the owner.
-  //
-  // This may return a result synchronously, or asynchronously invoke `callback`
-  // with the result. The callback will be invoked iff the return value is
-  // nullopt; i.e. a result will be provided via return value or callback, but
-  // not both, and not neither.
-  [[nodiscard]] absl::optional<SetsByOwner> Sets(
-      const FirstPartySetsContextConfig& fps_context_config,
-      base::OnceCallback<void(SetsByOwner)> callback);
-
   // Stores the First-Party Sets data.
   //
   // Only the first call to SetCompleteSets can have any effect; subsequent
@@ -177,17 +164,6 @@
       const base::flat_set<net::SchemefulSite>& sites,
       const FirstPartySetsContextConfig& fps_context_config) const;
 
-  // Same as `Sets`, but plumbs the result into the callback. Must only be
-  // called once the instance is fully initialized.
-  void SetsAndInvoke(const FirstPartySetsContextConfig& fps_context_config,
-                     base::OnceCallback<void(SetsByOwner)> callback,
-                     base::TimeTicks enqueued_at) const;
-
-  // Synchronous version of `Sets`, to be run only once the instance is
-  // initialized.
-  SetsByOwner SetsInternal(
-      const FirstPartySetsContextConfig& fps_context_config) const;
-
   // Enqueues a query to be answered once the instance is fully initialized.
   void EnqueuePendingQuery(base::OnceClosure run_query);
 
diff --git a/services/network/first_party_sets/first_party_sets_manager_unittest.cc b/services/network/first_party_sets/first_party_sets_manager_unittest.cc
index 46e7fef..762f026b 100644
--- a/services/network/first_party_sets/first_party_sets_manager_unittest.cc
+++ b/services/network/first_party_sets/first_party_sets_manager_unittest.cc
@@ -11,7 +11,6 @@
 #include "base/test/task_environment.h"
 #include "base/test/test_future.h"
 #include "net/base/schemeful_site.h"
-#include "net/base/test_completion_callback.h"
 #include "net/cookies/cookie_constants.h"
 #include "net/cookies/first_party_set_metadata.h"
 #include "net/cookies/same_party_context.h"
@@ -47,13 +46,6 @@
     manager_.SetCompleteSets(content);
   }
 
-  FirstPartySetsManager::SetsByOwner SetsAndWait() {
-    base::test::TestFuture<FirstPartySetsManager::SetsByOwner> future;
-    absl::optional<FirstPartySetsManager::SetsByOwner> result =
-        manager_.Sets(fps_context_config_, future.GetCallback());
-    return result.has_value() ? result.value() : future.Get();
-  }
-
   net::FirstPartySetMetadata ComputeMetadataAndWait(
       const net::SchemefulSite& site,
       const net::SchemefulSite* top_frame_site,
@@ -115,7 +107,11 @@
                    {net::SchemefulSite(GURL("https://example.test")),
                     net::SchemefulSite(GURL("https://example.test"))}});
 
-  EXPECT_THAT(SetsAndWait(), IsEmpty());
+  EXPECT_THAT(FindOwnersAndWait({
+                  net::SchemefulSite(GURL("https://aaaa.test")),
+                  net::SchemefulSite(GURL("https://example.test")),
+              }),
+              IsEmpty());
 }
 
 TEST_F(FirstPartySetsManagerDisabledTest, FindOwners) {
@@ -188,27 +184,12 @@
       FindOwnerAndWait(net::SchemefulSite(GURL("https://member.test"))));
 }
 
-TEST_F(FirstPartySetsManagerDisabledTest, Sets_IsEmpty) {
-  SetFirstPartySetsContextConfig(
-      true, {{net::SchemefulSite(GURL("https://aaaa.test")),
-              {net::SchemefulSite(GURL("https://example.test"))}},
-             {net::SchemefulSite(GURL("https://example.test")),
-              {net::SchemefulSite(GURL("https://example.test"))}}});
-
-  EXPECT_THAT(SetsAndWait(), IsEmpty());
-}
-
 class FirstPartySetsEnabledTest : public FirstPartySetsManagerTest {
  public:
   FirstPartySetsEnabledTest()
       : FirstPartySetsManagerTest(/*enabled=*/true, /*context_enabled=*/true) {}
 };
 
-TEST_F(FirstPartySetsEnabledTest, Sets_IsEmpty) {
-  SetCompleteSets({});
-  EXPECT_THAT(SetsAndWait(), IsEmpty());
-}
-
 TEST_F(FirstPartySetsEnabledTest, SetCompleteSets) {
   SetCompleteSets(base::flat_map<net::SchemefulSite, net::SchemefulSite>(
       {{net::SchemefulSite(GURL("https://aaaa.test")),
@@ -216,23 +197,30 @@
        {net::SchemefulSite(GURL("https://example.test")),
         net::SchemefulSite(GURL("https://example.test"))}}));
 
-  EXPECT_THAT(SetsAndWait(),
-              UnorderedElementsAre(Pair(
-                  SerializesTo("https://example.test"),
-                  UnorderedElementsAre(SerializesTo("https://example.test"),
-                                       SerializesTo("https://aaaa.test")))));
+  EXPECT_THAT(FindOwnersAndWait({
+                  net::SchemefulSite(GURL("https://aaaa.test")),
+                  net::SchemefulSite(GURL("https://example.test")),
+              }),
+              UnorderedElementsAre(Pair(SerializesTo("https://example.test"),
+                                        SerializesTo("https://example.test")),
+                                   Pair(SerializesTo("https://aaaa.test"),
+                                        SerializesTo("https://example.test"))));
 }
 
 TEST_F(FirstPartySetsEnabledTest, SetCompleteSets_Idempotent) {
   SetCompleteSets({});
-  EXPECT_THAT(SetsAndWait(), IsEmpty());
+  EXPECT_THAT(FindOwnersAndWait({}), IsEmpty());
 
   // The second call to SetCompleteSets should have no effect.
   SetCompleteSets({{net::SchemefulSite(GURL("https://aaaa.test")),
                     net::SchemefulSite(GURL("https://example.test"))},
                    {net::SchemefulSite(GURL("https://example.test")),
                     net::SchemefulSite(GURL("https://example.test"))}});
-  EXPECT_THAT(SetsAndWait(), IsEmpty());
+  EXPECT_THAT(FindOwnersAndWait({
+                  net::SchemefulSite(GURL("https://aaaa.test")),
+                  net::SchemefulSite(GURL("https://example.test")),
+              }),
+              IsEmpty());
 }
 
 // Test fixture that allows precise control over when the instance gets FPS
@@ -328,24 +316,6 @@
                                         SerializesTo("https://foo.test"))));
 }
 
-TEST_F(AsyncPopulatedFirstPartySetsManagerTest, QueryBeforeReady_Sets) {
-  base::test::TestFuture<FirstPartySetsManager::SetsByOwner> future;
-  EXPECT_FALSE(manager().Sets(fps_context_config(), future.GetCallback()));
-
-  Populate();
-
-  EXPECT_THAT(
-      future.Get(),
-      UnorderedElementsAre(
-          Pair(SerializesTo("https://example.test"),
-               UnorderedElementsAre(SerializesTo("https://example.test"),
-                                    SerializesTo("https://member1.test"),
-                                    SerializesTo("https://member3.test"))),
-          Pair(SerializesTo("https://foo.test"),
-               UnorderedElementsAre(SerializesTo("https://foo.test"),
-                                    SerializesTo("https://member2.test")))));
-}
-
 class PopulatedFirstPartySetsManagerTest
     : public AsyncPopulatedFirstPartySetsManagerTest {
  public:
@@ -936,19 +906,6 @@
                                         SerializesTo("https://foo.test"))));
 }
 
-TEST_F(PopulatedFirstPartySetsManagerTest, Sets_NonEmpty) {
-  EXPECT_THAT(
-      SetsAndWait(),
-      UnorderedElementsAre(
-          Pair(SerializesTo("https://example.test"),
-               UnorderedElementsAre(SerializesTo("https://example.test"),
-                                    SerializesTo("https://member1.test"),
-                                    SerializesTo("https://member3.test"))),
-          Pair(SerializesTo("https://foo.test"),
-               UnorderedElementsAre(SerializesTo("https://foo.test"),
-                                    SerializesTo("https://member2.test")))));
-}
-
 class DisabledContextFirstPartySetsManagerTest
     : public PopulatedFirstPartySetsManagerTest {
  public:
@@ -979,10 +936,6 @@
       FindOwnerAndWait(net::SchemefulSite(GURL("https://member.test"))));
 }
 
-TEST_F(DisabledContextFirstPartySetsManagerTest, Sets_IsEmpty) {
-  EXPECT_THAT(SetsAndWait(), IsEmpty());
-}
-
 TEST_F(DisabledContextFirstPartySetsManagerTest, ComputeMetadata) {
   net::SchemefulSite member(GURL("https://member1.test"));
   net::SchemefulSite example(GURL("https://example.test"));
@@ -1180,7 +1133,7 @@
             net::SchemefulSite(GURL("https://foo.test")));
 }
 
-TEST_F(OverrideSetsFirstPartySetsManagerTest, Sets_NoIntersection) {
+TEST_F(OverrideSetsFirstPartySetsManagerTest, NoIntersection) {
   SetFirstPartySetsContextConfig(
       true, {
                 {net::SchemefulSite(GURL("https://member3.test")),
@@ -1190,21 +1143,28 @@
                  {net::SchemefulSite(GURL("https://foo.test"))}},
             });
 
-  EXPECT_THAT(
-      SetsAndWait(),
-      UnorderedElementsAre(
-          Pair(SerializesTo("https://example.test"),
-               UnorderedElementsAre(SerializesTo("https://example.test"),
-                                    SerializesTo("https://member1.test"),
-                                    SerializesTo("https://member2.test"))),
-          Pair(SerializesTo("https://foo.test"),
-               UnorderedElementsAre(SerializesTo("https://foo.test"),
-                                    SerializesTo("https://member3.test")))));
+  EXPECT_THAT(FindOwnersAndWait({
+                  net::SchemefulSite(GURL("https://foo.test")),
+                  net::SchemefulSite(GURL("https://example.test")),
+                  net::SchemefulSite(GURL("https://member1.test")),
+                  net::SchemefulSite(GURL("https://member2.test")),
+                  net::SchemefulSite(GURL("https://member3.test")),
+              }),
+              UnorderedElementsAre(Pair(SerializesTo("https://foo.test"),
+                                        SerializesTo("https://foo.test")),
+                                   Pair(SerializesTo("https://example.test"),
+                                        SerializesTo("https://example.test")),
+                                   Pair(SerializesTo("https://member1.test"),
+                                        SerializesTo("https://example.test")),
+                                   Pair(SerializesTo("https://member2.test"),
+                                        SerializesTo("https://example.test")),
+                                   Pair(SerializesTo("https://member3.test"),
+                                        SerializesTo("https://foo.test"))));
 }
 
 // The member of a override set is also a member of an existing set as a
 // replacement.
-TEST_F(OverrideSetsFirstPartySetsManagerTest, Sets_ReplacesExistingMember) {
+TEST_F(OverrideSetsFirstPartySetsManagerTest, ReplacesExistingMember) {
   // The owner of the existing set is mapped to nullopt since it gets removed
   // after its member is replaced to an override set and it becomes a singleton.
   SetFirstPartySetsContextConfig(
@@ -1219,16 +1179,21 @@
            {net::SchemefulSite(GURL("https://foo.test"))}},
       });
 
-  EXPECT_THAT(SetsAndWait(),
-              UnorderedElementsAre(Pair(
-                  SerializesTo("https://foo.test"),
-                  UnorderedElementsAre(SerializesTo("https://foo.test"),
-                                       SerializesTo("https://member1.test")))));
+  EXPECT_THAT(FindOwnersAndWait({
+                  net::SchemefulSite(GURL("https://foo.test")),
+                  net::SchemefulSite(GURL("https://example.test")),
+                  net::SchemefulSite(GURL("https://member1.test")),
+                  net::SchemefulSite(GURL("https://member2.test")),
+              }),
+              UnorderedElementsAre(Pair(SerializesTo("https://foo.test"),
+                                        SerializesTo("https://foo.test")),
+                                   Pair(SerializesTo("https://member1.test"),
+                                        SerializesTo("https://foo.test"))));
 }
 
 // The owner of a override set is also an owner of an existing set as a
 // replacement.
-TEST_F(OverrideSetsFirstPartySetsManagerTest, Sets_ReplacesExistingOwner) {
+TEST_F(OverrideSetsFirstPartySetsManagerTest, ReplacesExistingOwner) {
   // The member of the existing set is mapped to nullopt since it gets removed
   // after its owner is replaced to an override set and it becomes a singleton.
   SetFirstPartySetsContextConfig(
@@ -1243,16 +1208,21 @@
            {net::SchemefulSite(GURL("https://example.test"))}},
       });
 
-  EXPECT_THAT(SetsAndWait(),
-              UnorderedElementsAre(Pair(
-                  SerializesTo("https://example.test"),
-                  UnorderedElementsAre(SerializesTo("https://example.test"),
-                                       SerializesTo("https://member3.test")))));
+  EXPECT_THAT(FindOwnersAndWait({
+                  net::SchemefulSite(GURL("https://example.test")),
+                  net::SchemefulSite(GURL("https://member1.test")),
+                  net::SchemefulSite(GURL("https://member2.test")),
+                  net::SchemefulSite(GURL("https://member3.test")),
+              }),
+              UnorderedElementsAre(Pair(SerializesTo("https://example.test"),
+                                        SerializesTo("https://example.test")),
+                                   Pair(SerializesTo("https://member3.test"),
+                                        SerializesTo("https://example.test"))));
 }
 
 // The owner of an override set is also an owner of an existing set as an
 // addition.
-TEST_F(OverrideSetsFirstPartySetsManagerTest, Sets_AdditionMutualOwner) {
+TEST_F(OverrideSetsFirstPartySetsManagerTest, AdditionMutualOwner) {
   SetFirstPartySetsContextConfig(
       true, {
                 {net::SchemefulSite(GURL("https://member3.test")),
@@ -1262,17 +1232,24 @@
                  {net::SchemefulSite(GURL("https://example.test"))}},
             });
 
-  EXPECT_THAT(SetsAndWait(),
-              UnorderedElementsAre(Pair(
-                  SerializesTo("https://example.test"),
-                  UnorderedElementsAre(SerializesTo("https://example.test"),
-                                       SerializesTo("https://member1.test"),
-                                       SerializesTo("https://member2.test"),
-                                       SerializesTo("https://member3.test")))));
+  EXPECT_THAT(FindOwnersAndWait({
+                  net::SchemefulSite(GURL("https://example.test")),
+                  net::SchemefulSite(GURL("https://member1.test")),
+                  net::SchemefulSite(GURL("https://member2.test")),
+                  net::SchemefulSite(GURL("https://member3.test")),
+              }),
+              UnorderedElementsAre(Pair(SerializesTo("https://example.test"),
+                                        SerializesTo("https://example.test")),
+                                   Pair(SerializesTo("https://member1.test"),
+                                        SerializesTo("https://example.test")),
+                                   Pair(SerializesTo("https://member2.test"),
+                                        SerializesTo("https://example.test")),
+                                   Pair(SerializesTo("https://member3.test"),
+                                        SerializesTo("https://example.test"))));
 }
 
 // The owner of a override set is a member of an existing set as an addition.
-TEST_F(OverrideSetsFirstPartySetsManagerTest, Sets_AdditionOwnerIsMember) {
+TEST_F(OverrideSetsFirstPartySetsManagerTest, AdditionOwnerIsMember) {
   // All the sites in the existing set are reparented to the new owner.
   SetFirstPartySetsContextConfig(
       true, {
@@ -1287,18 +1264,25 @@
                  {net::SchemefulSite(GURL("https://member1.test"))}},
             });
 
-  EXPECT_THAT(SetsAndWait(),
-              UnorderedElementsAre(Pair(
-                  SerializesTo("https://member1.test"),
-                  UnorderedElementsAre(SerializesTo("https://member1.test"),
-                                       SerializesTo("https://example.test"),
-                                       SerializesTo("https://member2.test"),
-                                       SerializesTo("https://member3.test")))));
+  EXPECT_THAT(FindOwnersAndWait({
+                  net::SchemefulSite(GURL("https://example.test")),
+                  net::SchemefulSite(GURL("https://member1.test")),
+                  net::SchemefulSite(GURL("https://member2.test")),
+                  net::SchemefulSite(GURL("https://member3.test")),
+              }),
+              UnorderedElementsAre(Pair(SerializesTo("https://example.test"),
+                                        SerializesTo("https://member1.test")),
+                                   Pair(SerializesTo("https://member1.test"),
+                                        SerializesTo("https://member1.test")),
+                                   Pair(SerializesTo("https://member2.test"),
+                                        SerializesTo("https://member1.test")),
+                                   Pair(SerializesTo("https://member3.test"),
+                                        SerializesTo("https://member1.test"))));
 }
 
 // The member of a override set is also an owner of an existing set as an
 // addition.
-TEST_F(OverrideSetsFirstPartySetsManagerTest, Sets_AdditionMemberIsOwner) {
+TEST_F(OverrideSetsFirstPartySetsManagerTest, AdditionMemberIsOwner) {
   // The member of the existing set for that owner is reparented to the new
   // owner as addition.
   SetFirstPartySetsContextConfig(
@@ -1314,13 +1298,20 @@
                  {net::SchemefulSite(GURL("https://foo.test"))}},
             });
 
-  EXPECT_THAT(SetsAndWait(),
-              UnorderedElementsAre(Pair(
-                  SerializesTo("https://foo.test"),
-                  UnorderedElementsAre(SerializesTo("https://foo.test"),
-                                       SerializesTo("https://example.test"),
-                                       SerializesTo("https://member1.test"),
-                                       SerializesTo("https://member2.test")))));
+  EXPECT_THAT(FindOwnersAndWait({
+                  net::SchemefulSite(GURL("https://foo.test")),
+                  net::SchemefulSite(GURL("https://example.test")),
+                  net::SchemefulSite(GURL("https://member1.test")),
+                  net::SchemefulSite(GURL("https://member2.test")),
+              }),
+              UnorderedElementsAre(Pair(SerializesTo("https://foo.test"),
+                                        SerializesTo("https://foo.test")),
+                                   Pair(SerializesTo("https://example.test"),
+                                        SerializesTo("https://foo.test")),
+                                   Pair(SerializesTo("https://member1.test"),
+                                        SerializesTo("https://foo.test")),
+                                   Pair(SerializesTo("https://member2.test"),
+                                        SerializesTo("https://foo.test"))));
 }
 
 }  // namespace network
\ No newline at end of file
diff --git a/services/network/network_context.cc b/services/network/network_context.cc
index cb42918..33264a4 100644
--- a/services/network/network_context.cc
+++ b/services/network/network_context.cc
@@ -1583,6 +1583,14 @@
 
 void NetworkContext::CanSendSCTAuditingReport(
     base::OnceCallback<void(bool)> callback) {
+  // If the NetworkContextClient hasn't been set yet or has disconnected for
+  // some reason, just return `false`. (One case where this could occur is when
+  // restarting SCTAuditingReporter instances loaded form disk at startup -- see
+  // crbug.com/1347180 for more details on that case.)
+  if (!client_) {
+    std::move(callback).Run(false);
+    return;
+  }
   client_->OnCanSendSCTAuditingReport(std::move(callback));
 }
 
diff --git a/services/network/sct_auditing/sct_auditing_handler.cc b/services/network/sct_auditing/sct_auditing_handler.cc
index 5afcf31..489379d 100644
--- a/services/network/sct_auditing/sct_auditing_handler.cc
+++ b/services/network/sct_auditing/sct_auditing_handler.cc
@@ -55,6 +55,7 @@
 const char kBackoffEntryKey[] = "backoff_entry";
 const char kReportKey[] = "report";
 const char kSCTHashdanceMetadataKey[] = "sct_metadata";
+const char kAlreadyCountedKey[] = "counted_towards_report_limit";
 
 void RecordPopularSCTSkippedMetrics(bool popular_sct_skipped) {
   base::UmaHistogramBoolean("Security.SCTAuditing.OptOut.PopularSCTSkipped",
@@ -214,6 +215,8 @@
         net::BackoffEntrySerializer::SerializeToValue(
             *reporter->backoff_entry(), base::Time::Now());
     report_entry.Set(kBackoffEntryKey, std::move(backoff_entry_value));
+    report_entry.Set(kAlreadyCountedKey,
+                     reporter->counted_towards_report_limit());
 
     std::string serialized_report;
     reporter->report()->SerializeToString(&serialized_report);
@@ -248,6 +251,8 @@
     const absl::optional<base::Value> sct_metadata_value =
         entry_dict->Extract(kSCTHashdanceMetadataKey);
     const base::Value* backoff_entry_value = entry_dict->Find(kBackoffEntryKey);
+    const absl::optional<bool> counted_towards_report_limit =
+        entry_dict->FindBool(kAlreadyCountedKey);
 
     if (!reporter_key_string || !report_string || !backoff_entry_value) {
       continue;
@@ -296,7 +301,8 @@
     }
 
     AddReporter(cache_key, std::move(audit_report), std::move(sct_metadata),
-                std::move(backoff_entry));
+                std::move(backoff_entry),
+                counted_towards_report_limit.value_or(false));
     ++num_reporters_deserialized;
   }
   base::UmaHistogramCounts100("Security.SCTAuditing.NumPersistedReportsLoaded",
@@ -318,7 +324,8 @@
     net::HashValue reporter_key,
     std::unique_ptr<sct_auditing::SCTClientReport> report,
     absl::optional<SCTAuditingReporter::SCTHashdanceMetadata> sct_metadata,
-    std::unique_ptr<net::BackoffEntry> backoff_entry) {
+    std::unique_ptr<net::BackoffEntry> backoff_entry,
+    bool already_counted) {
   DCHECK(foreground_runner_->RunsTasksInCurrentSequence());
   if (mode_ == mojom::SCTAuditingMode::kDisabled) {
     return;
@@ -334,7 +341,7 @@
       base::BindRepeating(&SCTAuditingHandler::OnReporterStateUpdated,
                           GetWeakPtr()),
       base::BindOnce(&SCTAuditingHandler::OnReporterFinished, GetWeakPtr()),
-      std::move(backoff_entry));
+      std::move(backoff_entry), already_counted);
   reporter->Start();
   pending_reporters_.Put(reporter->key(), std::move(reporter));
 
diff --git a/services/network/sct_auditing/sct_auditing_handler.h b/services/network/sct_auditing/sct_auditing_handler.h
index 9a34f7f..0b44a6c 100644
--- a/services/network/sct_auditing/sct_auditing_handler.h
+++ b/services/network/sct_auditing/sct_auditing_handler.h
@@ -88,13 +88,15 @@
   // this will call SCTAuditingReporter::Start() to initiate sending the report.
   // If the report is a hashdance report, |leaf_hash| should be set to the
   // Merkle tree leaf hash of a randomly selected SCT.
-  // Optionally takes in a BackoffEntry for recreating reporter state from
-  // persisted storage.
+  // Optionally takes in a BackoffEntry and a bool for whether the report has
+  // already been counted towards the max-reports limit, for recreating reporter
+  // state from persisted storage.
   void AddReporter(
       net::HashValue reporter_key,
       std::unique_ptr<sct_auditing::SCTClientReport> report,
       absl::optional<SCTAuditingReporter::SCTHashdanceMetadata> sct_metadata,
-      std::unique_ptr<net::BackoffEntry> backoff_entry = nullptr);
+      std::unique_ptr<net::BackoffEntry> backoff_entry = nullptr,
+      bool already_counted = false);
 
   // Loads serialized reports from `serialized` and creates a new
   // SCTAuditingReporter for each (if a reporter for that report does not yet
diff --git a/services/network/sct_auditing/sct_auditing_handler_unittest.cc b/services/network/sct_auditing/sct_auditing_handler_unittest.cc
index a24c9ce..7af3722 100644
--- a/services/network/sct_auditing/sct_auditing_handler_unittest.cc
+++ b/services/network/sct_auditing/sct_auditing_handler_unittest.cc
@@ -1056,6 +1056,102 @@
       "sha256/qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqo="));
 }
 
+// Test that the "counted_towards_report_limit" flag is correctly reapplied when
+// deserialize a persisted reporter. See crbug.com/1348313.
+TEST_F(SCTAuditingHandlerTest, PersistedDataWithReportAlreadyCounted) {
+  // Set up previously persisted data on disk:
+  // - Default-initialized net::HashValue(net::HASH_VALUE_SHA256)
+  // - Empty SCTClientReport for origin "example.test:443".
+  // - A simple BackoffEntry.
+  // - A simple SCTHashdanceMetadata value.
+  // - The "already counted toward report limit" flag set to `true`.
+  std::string persisted_report =
+      R"(
+        [{
+          "reporter_key":
+            "sha256/qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqo=",
+          "report": "EhUKExIRCgxleGFtcGxlLnRlc3QQuwM=",
+          "backoff_entry": [2,0,"30000000","11644578625551798"],
+          "sct_metadata": {
+            "leaf_hash": "ZmFrZS1sZWFmLWhhc2g=",
+            "issued": "1659045681000000",
+            "log_id": "ZmFrZS1sb2ctaWQ=",
+            "log_mmd": "86400000000",
+            "cert_expiry": "1661724081000000"
+          },
+          "counted_towards_report_limit": true
+        }]
+      )";
+  ASSERT_TRUE(base::WriteFile(persistence_path_, persisted_report));
+
+  mojo::PendingRemote<network::mojom::URLLoaderFactory> factory_remote;
+  url_loader_factory_.Clone(factory_remote.InitWithNewPipeAndPassReceiver());
+
+  SCTAuditingHandler handler(network_context_.get(), persistence_path_);
+  handler.SetMode(mojom::SCTAuditingMode::kHashdance);
+  handler.SetURLLoaderFactoryForTesting(std::move(factory_remote));
+
+  // Wait for a lookup query request to be sent to ensure the persisted report
+  // has been deserialized and a new SCTAuditingReporter created.
+  WaitForRequests(1u);
+
+  auto* pending_reporters = handler.GetPendingReportersForTesting();
+  ASSERT_EQ(pending_reporters->size(), 1u);
+  // Reporter should have the `counted_toward_report_limit` flag set to `true`.
+  for (const auto& reporter : *pending_reporters) {
+    EXPECT_TRUE(reporter.second->counted_towards_report_limit());
+  }
+}
+
+// Test that when a persisted reporter is deserialized that does not have the
+// "counted_towards_report_limit" flag set, it gets defaulted to `false` in the
+// newly created SCTAuditingReporter. (This covers the case for existing
+// serialized data from versions before the flag was added.)
+// See crbug.com/1348313.
+TEST_F(SCTAuditingHandlerTest, PersistedDataWithoutReportAlreadyCounted) {
+  // Set up previously persisted data on disk:
+  // - Default-initialized net::HashValue(net::HASH_VALUE_SHA256)
+  // - Empty SCTClientReport for origin "example.test:443".
+  // - A simple BackoffEntry.
+  // - A simple SCTHashdanceMetadata value.
+  // - The "already counted toward report limit" not set.
+  std::string persisted_report =
+      R"(
+        [{
+          "reporter_key":
+            "sha256/qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqo=",
+          "report": "EhUKExIRCgxleGFtcGxlLnRlc3QQuwM=",
+          "backoff_entry": [2,0,"30000000","11644578625551798"],
+          "sct_metadata": {
+            "leaf_hash": "ZmFrZS1sZWFmLWhhc2g=",
+            "issued": "1659045681000000",
+            "log_id": "ZmFrZS1sb2ctaWQ=",
+            "log_mmd": "86400000000",
+            "cert_expiry": "1661724081000000"
+          }
+        }]
+      )";
+  ASSERT_TRUE(base::WriteFile(persistence_path_, persisted_report));
+
+  mojo::PendingRemote<network::mojom::URLLoaderFactory> factory_remote;
+  url_loader_factory_.Clone(factory_remote.InitWithNewPipeAndPassReceiver());
+
+  SCTAuditingHandler handler(network_context_.get(), persistence_path_);
+  handler.SetMode(mojom::SCTAuditingMode::kHashdance);
+  handler.SetURLLoaderFactoryForTesting(std::move(factory_remote));
+
+  // Wait for a lookup query request to be sent to ensure the persisted report
+  // has been deserialized and a new SCTAuditingReporter created.
+  WaitForRequests(1u);
+
+  auto* pending_reporters = handler.GetPendingReportersForTesting();
+  ASSERT_EQ(pending_reporters->size(), 1u);
+  // Reporter should have the `counted_toward_report_limit` flag set to `false`.
+  for (const auto& reporter : *pending_reporters) {
+    EXPECT_FALSE(reporter.second->counted_towards_report_limit());
+  }
+}
+
 class NoPersistenceSCTAuditingHandlerTest : public SCTAuditingHandlerTest {
  public:
   void SetUp() override {
diff --git a/services/network/sct_auditing/sct_auditing_reporter.cc b/services/network/sct_auditing/sct_auditing_reporter.cc
index b2ea41d..3ba7eca 100644
--- a/services/network/sct_auditing/sct_auditing_reporter.cc
+++ b/services/network/sct_auditing/sct_auditing_reporter.cc
@@ -214,7 +214,8 @@
     mojom::URLLoaderFactory* url_loader_factory,
     ReporterUpdatedCallback update_callback,
     ReporterDoneCallback done_callback,
-    std::unique_ptr<net::BackoffEntry> persisted_backoff_entry)
+    std::unique_ptr<net::BackoffEntry> persisted_backoff_entry,
+    bool counted_towards_report_limit)
     : owner_network_context_(owner_network_context),
       reporter_key_(reporter_key),
       report_(std::move(report)),
@@ -223,7 +224,8 @@
       configuration_(std::move(configuration)),
       update_callback_(std::move(update_callback)),
       done_callback_(std::move(done_callback)),
-      max_retries_(kMaxRetries) {
+      max_retries_(kMaxRetries),
+      counted_towards_report_limit_(counted_towards_report_limit) {
   // Clone the URLLoaderFactory to avoid any dependencies on its lifetime. The
   // Reporter instance can maintain its own copy.
   // Relatively few Reporters are expected to exist at a time (due to sampling
@@ -266,16 +268,17 @@
   }
 
   // Entrypoint for checking whether the max-reports limit has been reached.
-  // This should only get called once for the lifetime of the Reporter.
-  // TODO(crbug.com/1144205): Once reports are persisted to disk, the Reporter
-  // state should include whether it has been "counted" yet, otherwise if a
-  // Reporter gets persisted and restored many times it would cause the report
-  // cap to trigger. This can likely just be a boolean flag on the Reporter and
-  // the persisted state -- if `true`, this check (and incrementing the report
-  // count) can be skipped.
-  owner_network_context_->CanSendSCTAuditingReport(
-      base::BindOnce(&SCTAuditingReporter::OnCheckReportAllowedStatusComplete,
-                     weak_factory_.GetWeakPtr()));
+  // This should only get called once for the lifetime of the Reporter. If the
+  // report has already been counted towards the max-reports limit, we skip the
+  // check (and don't increment the report count later when trying to send the
+  // full report).
+  if (counted_towards_report_limit_) {
+    OnCheckReportAllowedStatusComplete(true);
+  } else {
+    owner_network_context_->CanSendSCTAuditingReport(
+        base::BindOnce(&SCTAuditingReporter::OnCheckReportAllowedStatusComplete,
+                       weak_factory_.GetWeakPtr()));
+  }
 }
 
 void SCTAuditingReporter::OnCheckReportAllowedStatusComplete(bool allowed) {
@@ -495,8 +498,16 @@
   }
 
   // The server does not know about this SCT, and it should. Notify the
-  // embedder and start sending the full report.
-  owner_network_context_->OnNewSCTAuditingReportSent();
+  // embedder (ensuring we only do this once per report) and then start sending
+  // the full report.
+  if (!counted_towards_report_limit_) {
+    owner_network_context_->OnNewSCTAuditingReportSent();
+    // Mark this reporter as already counted towards the max report limit. This
+    // prevents counting the same report multiple times in case of retries (and
+    // retries across browser restarts with persisted reports).
+    counted_towards_report_limit_ = true;
+  }
+
   RecordLookupQueryResult(LookupQueryResult::kSCTSuffixNotFound);
   SendReport();
 }
diff --git a/services/network/sct_auditing/sct_auditing_reporter.h b/services/network/sct_auditing/sct_auditing_reporter.h
index e5b51727..eb97a9c 100644
--- a/services/network/sct_auditing/sct_auditing_reporter.h
+++ b/services/network/sct_auditing/sct_auditing_reporter.h
@@ -143,7 +143,8 @@
       mojom::URLLoaderFactory* url_loader_factory,
       ReporterUpdatedCallback update_callback,
       ReporterDoneCallback done_callback,
-      std::unique_ptr<net::BackoffEntry> backoff_entry = nullptr);
+      std::unique_ptr<net::BackoffEntry> backoff_entry = nullptr,
+      bool counted_towards_report_limit = false);
   ~SCTAuditingReporter();
 
   SCTAuditingReporter(const SCTAuditingReporter&) = delete;
@@ -159,6 +160,7 @@
   const absl::optional<SCTHashdanceMetadata>& sct_hashdance_metadata() {
     return sct_hashdance_metadata_;
   }
+  bool counted_towards_report_limit() { return counted_towards_report_limit_; }
 
   static void SetRetryDelayForTesting(absl::optional<base::TimeDelta> delay);
 
@@ -195,6 +197,13 @@
 
   int max_retries_;
 
+  // Whether the report has been counted towards the max-reports limit. This is
+  // used to determine whether to notify the embedder that a new report is being
+  // sent by the client, to avoid overcounting how many unique reports have been
+  // sent. (Without this flag, this could happen on retries if the hashdance
+  // lookup query succeeds but then the full report upload fails.)
+  bool counted_towards_report_limit_;
+
   base::WeakPtrFactory<SCTAuditingReporter> weak_factory_{this};
 };
 
diff --git a/testing/variations/fieldtrial_testing_config.json b/testing/variations/fieldtrial_testing_config.json
index 9b6c2edb..312cfb7 100644
--- a/testing/variations/fieldtrial_testing_config.json
+++ b/testing/variations/fieldtrial_testing_config.json
@@ -10250,6 +10250,25 @@
             ]
         }
     ],
+    "WebUIBubblePersistentRendererStudy": [
+        {
+            "platforms": [
+                "chromeos",
+                "chromeos_lacros",
+                "linux",
+                "mac",
+                "windows"
+            ],
+            "experiments": [
+                {
+                    "name": "Enabled",
+                    "enable_features": [
+                        "WebUIBubblePerProfilePersistence"
+                    ]
+                }
+            ]
+        }
+    ],
     "WebUICodeCache": [
         {
             "platforms": [
diff --git a/third_party/blink/public/devtools_protocol/browser_protocol.pdl b/third_party/blink/public/devtools_protocol/browser_protocol.pdl
index a541e0e..28fcb0c 100644
--- a/third_party/blink/public/devtools_protocol/browser_protocol.pdl
+++ b/third_party/blink/public/devtools_protocol/browser_protocol.pdl
@@ -9346,13 +9346,16 @@
   experimental type FilterEntry extends object
     properties
       # If set, causes exclusion of mathcing targets from the list.
-      # The remainder of filter entries in the filter arrat are ignored.
       optional boolean exclude
       # If not present, matches any type.
       optional string type
 
-  # If filter is not specified, the one assumed is [{type: "browser", exclude: true}, {}]
-  # (i.e. include everything but browser).
+  # The entries in TargetFilter are matched sequentially against targets and
+  # the first entry that matches determines if the target is included or not,
+  # depending on the value of `exclude` field in the entry.
+  # If filter is not specified, the one assumed is
+  # [{type: "browser", exclude: true}, {type: "tab", exclude: true}, {}]
+  # (i.e. include everything but `browser` and `tab`).
   experimental type TargetFilter extends array of FilterEntry
 
   experimental type RemoteLocation extends object
diff --git a/third_party/blink/public/mojom/manifest/manifest.mojom b/third_party/blink/public/mojom/manifest/manifest.mojom
index 2014b31b..be336ec 100644
--- a/third_party/blink/public/mojom/manifest/manifest.mojom
+++ b/third_party/blink/public/mojom/manifest/manifest.mojom
@@ -40,7 +40,7 @@
 
   array<ManifestImageResource> icons;
 
-  array<ManifestImageResource> screenshots;
+  array<ManifestScreenshot> screenshots;
 
   array<ManifestShortcutItem> shortcuts;
 
@@ -171,6 +171,22 @@
   array<Purpose> purpose;
 };
 
+// Structure representing a screenshot as per the Manifest specification, see:
+// https://www.w3.org/TR/manifest-app-info/#screenshots-member. This includes
+// a ImageResource with some additional members.
+struct ManifestScreenshot {
+  enum Platform {
+    kUnknown = 0,
+    kWide,
+    kNarrow,
+  };
+
+  ManifestImageResource image;
+
+  // The distribution platform for which a given screenshot applies.
+  Platform platform;
+};
+
 // Structure representing a share target file.
 struct ManifestFileFilter {
   mojo_base.mojom.String16? name;
diff --git a/third_party/blink/renderer/bindings/generated_in_core.gni b/third_party/blink/renderer/bindings/generated_in_core.gni
index 48bf8d98..1074ce7c 100644
--- a/third_party/blink/renderer/bindings/generated_in_core.gni
+++ b/third_party/blink/renderer/bindings/generated_in_core.gni
@@ -203,6 +203,8 @@
   "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_get_root_node_options.h",
   "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_hash_change_event_init.cc",
   "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_hash_change_event_init.h",
+  "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_highlight_pointer_event_init.cc",
+  "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_highlight_pointer_event_init.h",
   "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_idle_request_options.cc",
   "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_idle_request_options.h",
   "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_image_bitmap_options.cc",
@@ -829,6 +831,8 @@
   "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_headers.h",
   "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_highlight.cc",
   "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_highlight.h",
+  "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_highlight_pointer_event.cc",
+  "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_highlight_pointer_event.h",
   "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_highlight_registry.cc",
   "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_highlight_registry.h",
   "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_history.cc",
diff --git a/third_party/blink/renderer/bindings/idl_in_core.gni b/third_party/blink/renderer/bindings/idl_in_core.gni
index a8ba436f0..eababe1 100644
--- a/third_party/blink/renderer/bindings/idl_in_core.gni
+++ b/third_party/blink/renderer/bindings/idl_in_core.gni
@@ -334,6 +334,7 @@
           "//third_party/blink/renderer/core/geometry/dom_rect_read_only.idl",
           "//third_party/blink/renderer/core/highlight/css_highlight_registry.idl",
           "//third_party/blink/renderer/core/highlight/highlight.idl",
+          "//third_party/blink/renderer/core/highlight/highlight_pointer_event.idl",
           "//third_party/blink/renderer/core/highlight/highlight_registry.idl",
           "//third_party/blink/renderer/core/html/assigned_nodes_options.idl",
           "//third_party/blink/renderer/core/html/canvas/baselines.idl",
diff --git a/third_party/blink/renderer/core/animation/animation.cc b/third_party/blink/renderer/core/animation/animation.cc
index a460769..84b7de1 100644
--- a/third_party/blink/renderer/core/animation/animation.cc
+++ b/third_party/blink/renderer/core/animation/animation.cc
@@ -726,8 +726,8 @@
     // matches the specification for composite ordering
     if (priority1 == PseudoPriority::kOther && pseudo1 != pseudo2) {
       return CodeUnitCompareLessThan(
-          PseudoElement::PseudoElementNameForEvents(pseudo1),
-          PseudoElement::PseudoElementNameForEvents(pseudo2));
+          PseudoElement::PseudoElementNameForEvents(owning_element1),
+          PseudoElement::PseudoElementNameForEvents(owning_element2));
     }
     if (anim_priority1 == kCssAnimationPriority) {
       // When comparing two CSSAnimations with the same owning element, we sort
diff --git a/third_party/blink/renderer/core/animation/css/css_animations.cc b/third_party/blink/renderer/core/animation/css/css_animations.cc
index 2fcbea1d..1d4bb5e6 100644
--- a/third_party/blink/renderer/core/animation/css/css_animations.cc
+++ b/third_party/blink/renderer/core/animation/css/css_animations.cc
@@ -1774,8 +1774,8 @@
     const AtomicString& event_name,
     const AnimationTimeDelta& elapsed_time) {
   if (animation_target_->GetDocument().HasListenerType(listener_type)) {
-    String pseudo_element_name = PseudoElement::PseudoElementNameForEvents(
-        animation_target_->GetPseudoId());
+    String pseudo_element_name =
+        PseudoElement::PseudoElementNameForEvents(animation_target_);
     AnimationEvent* event = AnimationEvent::Create(
         event_name, name_, elapsed_time, pseudo_element_name);
     event->SetTarget(GetEventTarget());
@@ -1950,7 +1950,7 @@
           ? property_.CustomPropertyName()
           : property_.GetCSSProperty().GetPropertyNameString();
   String pseudo_element =
-      PseudoElement::PseudoElementNameForEvents(GetPseudoId());
+      PseudoElement::PseudoElementNameForEvents(transition_target_);
   TransitionEvent* event = TransitionEvent::Create(
       type, property_name, elapsed_time, pseudo_element);
   event->SetTarget(GetEventTarget());
diff --git a/third_party/blink/renderer/core/animation/css/css_animations.h b/third_party/blink/renderer/core/animation/css/css_animations.h
index 2161793..9b8d35df 100644
--- a/third_party/blink/renderer/core/animation/css/css_animations.h
+++ b/third_party/blink/renderer/core/animation/css/css_animations.h
@@ -303,7 +303,6 @@
 
     const Element& TransitionTarget() const { return *transition_target_; }
     EventTarget* GetEventTarget() const;
-    PseudoId GetPseudoId() const { return transition_target_->GetPseudoId(); }
     Document& GetDocument() const { return transition_target_->GetDocument(); }
 
     Member<Element> transition_target_;
diff --git a/third_party/blink/renderer/core/dom/events/event.cc b/third_party/blink/renderer/core/dom/events/event.cc
index 1821c3a7..707d8c3e 100644
--- a/third_party/blink/renderer/core/dom/events/event.cc
+++ b/third_party/blink/renderer/core/dom/events/event.cc
@@ -195,6 +195,10 @@
   return false;
 }
 
+bool Event::IsHighlightPointerEvent() const {
+  return false;
+}
+
 bool Event::IsInputEvent() const {
   return false;
 }
diff --git a/third_party/blink/renderer/core/dom/events/event.h b/third_party/blink/renderer/core/dom/events/event.h
index 96fd605..d0b7670c 100644
--- a/third_party/blink/renderer/core/dom/events/event.h
+++ b/third_party/blink/renderer/core/dom/events/event.h
@@ -220,6 +220,7 @@
   virtual bool IsGestureEvent() const;
   virtual bool IsWheelEvent() const;
   virtual bool IsPointerEvent() const;
+  virtual bool IsHighlightPointerEvent() const;
   virtual bool IsInputEvent() const;
   virtual bool IsCompositionEvent() const;
 
diff --git a/third_party/blink/renderer/core/dom/pseudo_element.cc b/third_party/blink/renderer/core/dom/pseudo_element.cc
index 5c065db..c7196ff 100644
--- a/third_party/blink/renderer/core/dom/pseudo_element.cc
+++ b/third_party/blink/renderer/core/dom/pseudo_element.cc
@@ -133,12 +133,29 @@
   return name;
 }
 
-const AtomicString& PseudoElement::PseudoElementNameForEvents(
-    PseudoId pseudo_id) {
-  if (pseudo_id == kPseudoIdNone)
-    return g_null_atom;
-  else
-    return PseudoElementTagName(pseudo_id).LocalName();
+AtomicString PseudoElement::PseudoElementNameForEvents(Element* element) {
+  DCHECK(element);
+  auto pseudo_id = element->GetPseudoId();
+  switch (pseudo_id) {
+    case kPseudoIdNone:
+      return g_null_atom;
+    case kPseudoIdPageTransitionContainer:
+    case kPseudoIdPageTransitionImageWrapper:
+    case kPseudoIdPageTransitionIncomingImage:
+    case kPseudoIdPageTransitionOutgoingImage: {
+      auto* pseudo = To<PseudoElement>(element);
+      DCHECK(pseudo);
+      StringBuilder builder;
+      builder.Append(PseudoElementTagName(pseudo_id).LocalName());
+      builder.Append("(");
+      builder.Append(pseudo->document_transition_tag());
+      builder.Append(")");
+      return AtomicString(builder.ReleaseString());
+    }
+    default:
+      break;
+  }
+  return PseudoElementTagName(pseudo_id).LocalName();
 }
 
 bool PseudoElement::IsWebExposed(PseudoId pseudo_id, const Node* parent) {
@@ -147,14 +164,6 @@
       if (parent && parent->IsPseudoElement())
         return RuntimeEnabledFeatures::CSSMarkerNestedPseudoElementEnabled();
       return true;
-    case kPseudoIdPageTransition:
-    case kPseudoIdPageTransitionContainer:
-    case kPseudoIdPageTransitionImageWrapper:
-    case kPseudoIdPageTransitionIncomingImage:
-    case kPseudoIdPageTransitionOutgoingImage:
-      // These elements are generated only when DocumentTransition feature is
-      // enabled.
-      return true;
     default:
       return true;
   }
diff --git a/third_party/blink/renderer/core/dom/pseudo_element.h b/third_party/blink/renderer/core/dom/pseudo_element.h
index 268a4d7..9716e3b 100644
--- a/third_party/blink/renderer/core/dom/pseudo_element.h
+++ b/third_party/blink/renderer/core/dom/pseudo_element.h
@@ -66,7 +66,7 @@
   scoped_refptr<ComputedStyle> LayoutStyleForDisplayContents(
       const ComputedStyle&);
 
-  static const AtomicString& PseudoElementNameForEvents(PseudoId);
+  static AtomicString PseudoElementNameForEvents(Element*);
   static bool IsWebExposed(PseudoId, const Node*);
 
   // Pseudo element are not allowed to be the inner node for hit testing. Find
diff --git a/third_party/blink/renderer/core/frame/local_frame_view.cc b/third_party/blink/renderer/core/frame/local_frame_view.cc
index 5d02678..1bd0569 100644
--- a/third_party/blink/renderer/core/frame/local_frame_view.cc
+++ b/third_party/blink/renderer/core/frame/local_frame_view.cc
@@ -288,17 +288,7 @@
       unique_id_(NewUniqueObjectId()),
       layout_shift_tracker_(MakeGarbageCollected<LayoutShiftTracker>(this)),
       paint_timing_detector_(MakeGarbageCollected<PaintTimingDetector>(this)),
-      mobile_friendliness_checker_(
-          // Only run the mobile friendliness checker for the outermost main
-          // frame. The checker will iterate through all local frames in the
-          // current blink::Page. Also skip the mobile friendliness checks for
-          // "non-ordinary" pages by checking IsLocalFrameClientImpl(), since
-          // it's not useful to generate mobile friendliness metrics for
-          // devtools, svg, etc.
-          GetFrame().Client()->IsLocalFrameClientImpl() &&
-                  GetFrame().IsOutermostMainFrame()
-              ? MakeGarbageCollected<MobileFriendlinessChecker>(*this)
-              : nullptr)
+      mobile_friendliness_checker_(MobileFriendlinessChecker::Create(*this))
 #if DCHECK_IS_ON()
       ,
       is_updating_descendant_dependent_flags_(false),
diff --git a/third_party/blink/renderer/core/highlight/build.gni b/third_party/blink/renderer/core/highlight/build.gni
index 8d22062..4bd4883 100644
--- a/third_party/blink/renderer/core/highlight/build.gni
+++ b/third_party/blink/renderer/core/highlight/build.gni
@@ -7,6 +7,8 @@
   "css_highlight_registry.h",
   "highlight.cc",
   "highlight.h",
+  "highlight_pointer_event.cc",
+  "highlight_pointer_event.h",
   "highlight_registry.cc",
   "highlight_registry.h",
   "highlight_registry_map_entry.h",
diff --git a/third_party/blink/renderer/core/highlight/highlight.cc b/third_party/blink/renderer/core/highlight/highlight.cc
index 4c019d3d..54d7b33 100644
--- a/third_party/blink/renderer/core/highlight/highlight.cc
+++ b/third_party/blink/renderer/core/highlight/highlight.cc
@@ -25,7 +25,7 @@
 void Highlight::Trace(blink::Visitor* visitor) const {
   visitor->Trace(highlight_ranges_);
   visitor->Trace(containing_highlight_registries_);
-  ScriptWrappable::Trace(visitor);
+  EventTargetWithInlineData::Trace(visitor);
 }
 
 void Highlight::ScheduleRepaintsInContainingHighlightRegistries() const {
@@ -80,6 +80,18 @@
   return highlight_ranges_.Contains(range);
 }
 
+const AtomicString& Highlight::InterfaceName() const {
+  // TODO(crbug.com/1346693)
+  NOTIMPLEMENTED();
+  return g_null_atom;
+}
+
+ExecutionContext* Highlight::GetExecutionContext() const {
+  // TODO(crbug.com/1346693)
+  NOTIMPLEMENTED();
+  return nullptr;
+}
+
 void Highlight::RegisterIn(HighlightRegistry* highlight_registry) {
   auto map_iterator = containing_highlight_registries_.find(highlight_registry);
   if (map_iterator == containing_highlight_registries_.end()) {
diff --git a/third_party/blink/renderer/core/highlight/highlight.h b/third_party/blink/renderer/core/highlight/highlight.h
index ff23ac8f..bb4a1b0a 100644
--- a/third_party/blink/renderer/core/highlight/highlight.h
+++ b/third_party/blink/renderer/core/highlight/highlight.h
@@ -8,7 +8,7 @@
 #include "third_party/blink/renderer/bindings/core/v8/iterable.h"
 #include "third_party/blink/renderer/core/core_export.h"
 #include "third_party/blink/renderer/core/dom/abstract_range.h"
-#include "third_party/blink/renderer/platform/bindings/script_wrappable.h"
+#include "third_party/blink/renderer/core/dom/events/event_target.h"
 #include "third_party/blink/renderer/platform/heap/collection_support/heap_hash_map.h"
 #include "third_party/blink/renderer/platform/heap/collection_support/heap_linked_hash_set.h"
 #include "third_party/blink/renderer/platform/heap/collection_support/heap_vector.h"
@@ -20,7 +20,7 @@
     SetlikeIterable<Member<AbstractRange>, AbstractRange>;
 class HighlightRegistry;
 
-class CORE_EXPORT Highlight : public ScriptWrappable,
+class CORE_EXPORT Highlight : public EventTargetWithInlineData,
                               public HighlightSetIterable {
   DEFINE_WRAPPERTYPEINFO();
 
@@ -46,6 +46,11 @@
 
   bool Contains(AbstractRange*) const;
 
+  // EventTarget
+  const AtomicString& InterfaceName() const override;
+  ExecutionContext* GetExecutionContext() const override;
+
+  // HighlightSetIterable
   class IterationSource final : public HighlightSetIterable::IterationSource {
    public:
     explicit IterationSource(const Highlight& highlight);
diff --git a/third_party/blink/renderer/core/highlight/highlight.idl b/third_party/blink/renderer/core/highlight/highlight.idl
index 2272698..1111d21 100644
--- a/third_party/blink/renderer/core/highlight/highlight.idl
+++ b/third_party/blink/renderer/core/highlight/highlight.idl
@@ -16,4 +16,8 @@
   setlike<AbstractRange>;
   attribute long priority;
   attribute HighlightType type;
+  // TODO(crbug.com/1344319): Inherit from EventTarget
+  [RuntimeEnabled = HighlightPointerEvents] void addEventListener(DOMString type, EventListener? listener, optional (AddEventListenerOptions or boolean) options);
+  [RuntimeEnabled = HighlightPointerEvents] void removeEventListener(DOMString type, EventListener? listener, optional (EventListenerOptions or boolean) options);
+  [ImplementedAs=dispatchEventForBindings, RaisesException, RuntimeCallStatsCounter=EventTargetDispatchEvent, RuntimeEnabled = HighlightPointerEvents] boolean dispatchEvent(Event event);
 };
diff --git a/third_party/blink/renderer/core/highlight/highlight_pointer_event.cc b/third_party/blink/renderer/core/highlight/highlight_pointer_event.cc
new file mode 100644
index 0000000..6ec023d
--- /dev/null
+++ b/third_party/blink/renderer/core/highlight/highlight_pointer_event.cc
@@ -0,0 +1,32 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/highlight/highlight_pointer_event.h"
+
+#include "third_party/blink/renderer/bindings/core/v8/v8_highlight_pointer_event_init.h"
+
+namespace blink {
+
+HighlightPointerEvent::HighlightPointerEvent(
+    const AtomicString& type,
+    const HighlightPointerEventInit* initializer,
+    base::TimeTicks platform_time_stamp,
+    MouseEvent::SyntheticEventType synthetic_event_type,
+    WebMenuSourceType menu_source_type)
+    : PointerEvent(type,
+                   initializer,
+                   platform_time_stamp,
+                   synthetic_event_type,
+                   menu_source_type) {}
+
+bool HighlightPointerEvent::IsHighlightPointerEvent() const {
+  return true;
+}
+
+void HighlightPointerEvent::Trace(blink::Visitor* visitor) const {
+  visitor->Trace(range_);
+  PointerEvent::Trace(visitor);
+}
+
+}  // namespace blink
diff --git a/third_party/blink/renderer/core/highlight/highlight_pointer_event.h b/third_party/blink/renderer/core/highlight/highlight_pointer_event.h
new file mode 100644
index 0000000..7c6718f
--- /dev/null
+++ b/third_party/blink/renderer/core/highlight/highlight_pointer_event.h
@@ -0,0 +1,57 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_HIGHLIGHT_HIGHLIGHT_POINTER_EVENT_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_HIGHLIGHT_HIGHLIGHT_POINTER_EVENT_H_
+
+#include "third_party/blink/renderer/core/dom/range.h"
+#include "third_party/blink/renderer/core/events/pointer_event.h"
+
+namespace blink {
+
+class HighlightPointerEventInit;
+
+class CORE_EXPORT HighlightPointerEvent : public PointerEvent {
+  DEFINE_WRAPPERTYPEINFO();
+
+ public:
+  static HighlightPointerEvent* Create(
+      const AtomicString& type,
+      const HighlightPointerEventInit* initializer,
+      base::TimeTicks platform_time_stamp = base::TimeTicks::Now(),
+      MouseEvent::SyntheticEventType synthetic_event_type =
+          kRealOrIndistinguishable,
+      WebMenuSourceType menu_source_type = kMenuSourceNone) {
+    return MakeGarbageCollected<HighlightPointerEvent>(
+        type, initializer, platform_time_stamp, synthetic_event_type,
+        menu_source_type);
+  }
+
+  explicit HighlightPointerEvent(
+      const AtomicString&,
+      const HighlightPointerEventInit*,
+      base::TimeTicks platform_time_stamp,
+      MouseEvent::SyntheticEventType synthetic_event_type,
+      WebMenuSourceType menu_source_type = kMenuSourceNone);
+
+  Range* range() const { return range_; }
+
+  bool IsHighlightPointerEvent() const override;
+
+  void Trace(blink::Visitor*) const override;
+
+ private:
+  Member<Range> range_;
+};
+
+template <>
+struct DowncastTraits<HighlightPointerEvent> {
+  static bool AllowFrom(const Event& event) {
+    return event.IsHighlightPointerEvent();
+  }
+};
+
+}  // namespace blink
+
+#endif  // THIRD_PARTY_BLINK_RENDERER_CORE_HIGHLIGHT_HIGHLIGHT_POINTER_EVENT_H_
diff --git a/third_party/blink/renderer/core/highlight/highlight_pointer_event.idl b/third_party/blink/renderer/core/highlight/highlight_pointer_event.idl
new file mode 100644
index 0000000..85a119b6
--- /dev/null
+++ b/third_party/blink/renderer/core/highlight/highlight_pointer_event.idl
@@ -0,0 +1,14 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+dictionary HighlightPointerEventInit : PointerEventInit {
+  Range? range = null;
+};
+
+[
+  RuntimeEnabled = HighlightPointerEvents,
+  Exposed = Window
+] interface HighlightPointerEvent : PointerEvent {
+  readonly attribute Range? range;
+};
diff --git a/third_party/blink/renderer/core/layout/ng/ng_simplified_layout_algorithm.cc b/third_party/blink/renderer/core/layout/ng/ng_simplified_layout_algorithm.cc
index 01377d6..0c24f1d 100644
--- a/third_party/blink/renderer/core/layout/ng/ng_simplified_layout_algorithm.cc
+++ b/third_party/blink/renderer/core/layout/ng/ng_simplified_layout_algorithm.cc
@@ -134,6 +134,14 @@
           std::make_unique<NGTableFragmentData::CollapsedBordersGeometry>(
               *table_collapsed_borders_geometry));
     }
+  } else if (physical_fragment.IsTableNGSection()) {
+    if (const auto section_start_row_index =
+            physical_fragment.TableSectionStartRowIndex()) {
+      Vector<LayoutUnit> section_row_offsets =
+          *physical_fragment.TableSectionRowOffsets();
+      container_builder_.SetTableSectionCollapsedBordersGeometry(
+          *section_start_row_index, std::move(section_row_offsets));
+    }
   }
 
   if (physical_fragment.IsGridNG()) {
diff --git a/third_party/blink/renderer/core/mobile_metrics/mobile_friendliness_checker.cc b/third_party/blink/renderer/core/mobile_metrics/mobile_friendliness_checker.cc
index ddad2c1..3847efb2 100644
--- a/third_party/blink/renderer/core/mobile_metrics/mobile_friendliness_checker.cc
+++ b/third_party/blink/renderer/core/mobile_metrics/mobile_friendliness_checker.cc
@@ -15,6 +15,7 @@
 #include "third_party/blink/renderer/core/frame/local_frame_view.h"
 #include "third_party/blink/renderer/core/frame/page_scale_constraints_set.h"
 #include "third_party/blink/renderer/core/frame/root_frame_viewport.h"
+#include "third_party/blink/renderer/core/frame/settings.h"
 #include "third_party/blink/renderer/core/frame/visual_viewport.h"
 #include "third_party/blink/renderer/core/html/forms/html_form_control_element.h"
 #include "third_party/blink/renderer/core/html/html_anchor_element.h"
@@ -399,6 +400,23 @@
 
 }  // namespace
 
+MobileFriendlinessChecker* MobileFriendlinessChecker::Create(
+    LocalFrameView& frame_view) {
+  // Only run the mobile friendliness checker for the outermost main
+  // frame. The checker will iterate through all local frames in the
+  // current blink::Page. Also skip the mobile friendliness checks for
+  // "non-ordinary" pages by checking IsLocalFrameClientImpl(), since
+  // it's not useful to generate mobile friendliness metrics for
+  // devtools, svg, etc.
+  if (!frame_view.GetFrame().Client()->IsLocalFrameClientImpl() ||
+      !frame_view.GetFrame().IsOutermostMainFrame() ||
+      !frame_view.GetPage()->GetSettings().GetViewportEnabled() ||
+      !frame_view.GetPage()->GetSettings().GetViewportMetaEnabled()) {
+    return nullptr;
+  }
+  return MakeGarbageCollected<MobileFriendlinessChecker>(frame_view);
+}
+
 MobileFriendlinessChecker* MobileFriendlinessChecker::From(
     const Document& document) {
   DCHECK(document.GetFrame());
diff --git a/third_party/blink/renderer/core/mobile_metrics/mobile_friendliness_checker.h b/third_party/blink/renderer/core/mobile_metrics/mobile_friendliness_checker.h
index 933fa67..d309be4 100644
--- a/third_party/blink/renderer/core/mobile_metrics/mobile_friendliness_checker.h
+++ b/third_party/blink/renderer/core/mobile_metrics/mobile_friendliness_checker.h
@@ -29,6 +29,7 @@
  public:
   explicit MobileFriendlinessChecker(LocalFrameView& frame_view);
   virtual ~MobileFriendlinessChecker();
+  static MobileFriendlinessChecker* Create(LocalFrameView& frame_view);
   static MobileFriendlinessChecker* From(const Document&);
 
   // LocalFrameView::LifecycleNotificationObserver implementation
diff --git a/third_party/blink/renderer/core/paint/scoped_paint_state.cc b/third_party/blink/renderer/core/paint/scoped_paint_state.cc
index 8d151d8a..04e3a902 100644
--- a/third_party/blink/renderer/core/paint/scoped_paint_state.cc
+++ b/third_party/blink/renderer/core/paint/scoped_paint_state.cc
@@ -145,15 +145,16 @@
   if (input_paint_info_.phase == PaintPhase::kForeground) {
     // We treat horizontal-scrollable scrollers like replaced objects.
     if (auto* scrollable_area = box.GetScrollableArea()) {
-      if (scrollable_area->HasHorizontalScrollbar()) {
-        if (auto* mf_checker =
-                MobileFriendlinessChecker::From(box.GetDocument())) {
-          PhysicalRect content_rect = box.LocalVisualRect();
-          content_rect.Move(paint_offset_);
-          content_rect.Intersect(
-              PhysicalRect(input_paint_info_.GetCullRect().Rect()));
-          mf_checker->NotifyPaintReplaced(content_rect);
-          mf_ignore_scope_.emplace(*mf_checker);
+      if (scrollable_area->MaximumScrollOffset().x() > 0) {
+        if (!box.IsLayoutView()) {
+          if (auto* mf_checker =
+                  MobileFriendlinessChecker::From(box.GetDocument())) {
+            PhysicalRect content_rect = box.OverflowClipRect(paint_offset_);
+            content_rect.Intersect(
+                PhysicalRect(input_paint_info_.GetCullRect().Rect()));
+            mf_checker->NotifyPaintReplaced(content_rect);
+            mf_ignore_scope_.emplace(*mf_checker);
+          }
         }
       }
     }
diff --git a/third_party/blink/renderer/modules/manifest/manifest_parser.cc b/third_party/blink/renderer/modules/manifest/manifest_parser.cc
index 87d653a..708add0 100644
--- a/third_party/blink/renderer/modules/manifest/manifest_parser.cc
+++ b/third_party/blink/renderer/modules/manifest/manifest_parser.cc
@@ -17,6 +17,7 @@
 #include "third_party/blink/public/common/permissions_policy/permissions_policy.h"
 #include "third_party/blink/public/common/security/protocol_handler_security_level.h"
 #include "third_party/blink/public/mojom/manifest/manifest.mojom-blink-forward.h"
+#include "third_party/blink/public/mojom/manifest/manifest.mojom-blink.h"
 #include "third_party/blink/public/mojom/permissions_policy/permissions_policy.mojom-blink.h"
 #include "third_party/blink/public/platform/url_conversion.h"
 #include "third_party/blink/public/platform/web_icon_sizes_parser.h"
@@ -26,6 +27,7 @@
 #include "third_party/blink/renderer/modules/manifest/manifest_uma_util.h"
 #include "third_party/blink/renderer/modules/navigatorcontentutils/navigator_content_utils.h"
 #include "third_party/blink/renderer/platform/json/json_parser.h"
+#include "third_party/blink/renderer/platform/json/json_values.h"
 #include "third_party/blink/renderer/platform/runtime_enabled_features.h"
 #include "third_party/blink/renderer/platform/weborigin/kurl.h"
 #include "third_party/blink/renderer/platform/weborigin/security_origin.h"
@@ -661,19 +663,69 @@
   return purposes;
 }
 
-Vector<mojom::blink::ManifestImageResourcePtr> ManifestParser::ParseIcons(
-    const JSONObject* object) {
-  return ParseImageResource("icons", object);
+mojom::blink::ManifestScreenshot::Platform
+ManifestParser::ParseScreenshotPlatform(const JSONObject* screenshot) {
+  absl::optional<String> platform_str =
+      ParseString(screenshot, "platform", Trim(false));
+
+  if (!platform_str.has_value()) {
+    return mojom::blink::ManifestScreenshot::Platform::kUnknown;
+  }
+
+  String platform = platform_str.value();
+
+  if (EqualIgnoringASCIICase(platform, "wide")) {
+    return mojom::blink::ManifestScreenshot::Platform::kWide;
+  } else if (EqualIgnoringASCIICase(platform, "narrow")) {
+    return mojom::blink::ManifestScreenshot::Platform::kNarrow;
+  }
+
+  AddErrorInfo(
+      "property 'platform' on screenshots has an invalid value, ignoring it.");
+
+  return mojom::blink::ManifestScreenshot::Platform::kUnknown;
 }
 
-Vector<mojom::blink::ManifestImageResourcePtr> ManifestParser::ParseScreenshots(
+Vector<mojom::blink::ManifestImageResourcePtr> ManifestParser::ParseIcons(
     const JSONObject* object) {
-  return ParseImageResource("screenshots", object);
+  return ParseImageResourceArray("icons", object);
+}
+
+Vector<mojom::blink::ManifestScreenshotPtr> ManifestParser::ParseScreenshots(
+    const JSONObject* object) {
+  Vector<mojom::blink::ManifestScreenshotPtr> screenshots;
+  JSONValue* json_value = object->Get("screenshots");
+  if (!json_value)
+    return screenshots;
+
+  JSONArray* screenshots_list = object->GetArray("screenshots");
+  if (!screenshots_list) {
+    AddErrorInfo("property 'screenshots' ignored, type array expected.");
+    return screenshots;
+  }
+
+  for (wtf_size_t i = 0; i < screenshots_list->size(); ++i) {
+    JSONObject* screenshot_object = JSONObject::Cast(screenshots_list->at(i));
+    if (!screenshot_object)
+      continue;
+
+    auto screenshot = mojom::blink::ManifestScreenshot::New();
+    auto image = ParseImageResource(screenshot_object);
+    if (!image.has_value())
+      continue;
+
+    screenshot->image = std::move(*image);
+    screenshot->platform = ParseScreenshotPlatform(screenshot_object);
+
+    screenshots.push_back(std::move(screenshot));
+  }
+
+  return screenshots;
 }
 
 Vector<mojom::blink::ManifestImageResourcePtr>
-ManifestParser::ParseImageResource(const String& key,
-                                   const JSONObject* object) {
+ManifestParser::ParseImageResourceArray(const String& key,
+                                        const JSONObject* object) {
   Vector<mojom::blink::ManifestImageResourcePtr> icons;
   JSONValue* json_value = object->Get(key);
   if (!json_value)
@@ -686,30 +738,36 @@
   }
 
   for (wtf_size_t i = 0; i < icons_list->size(); ++i) {
-    JSONObject* icon_object = JSONObject::Cast(icons_list->at(i));
-    if (!icon_object)
-      continue;
-
-    auto icon = mojom::blink::ManifestImageResource::New();
-    icon->src = ParseIconSrc(icon_object);
-    // An icon MUST have a valid src. If it does not, it MUST be ignored.
-    if (!icon->src.IsValid())
-      continue;
-
-    icon->type = ParseIconType(icon_object);
-    icon->sizes = ParseIconSizes(icon_object);
-    auto purpose = ParseIconPurpose(icon_object);
-    if (!purpose)
-      continue;
-
-    icon->purpose = std::move(*purpose);
-
-    icons.push_back(std::move(icon));
+    auto icon = ParseImageResource(icons_list->at(i));
+    if (icon.has_value())
+      icons.push_back(std::move(*icon));
   }
 
   return icons;
 }
 
+absl::optional<mojom::blink::ManifestImageResourcePtr>
+ManifestParser::ParseImageResource(const JSONValue* object) {
+  const JSONObject* icon_object = JSONObject::Cast(object);
+  if (!icon_object)
+    return absl::nullopt;
+
+  auto icon = mojom::blink::ManifestImageResource::New();
+  icon->src = ParseIconSrc(icon_object);
+  // An icon MUST have a valid src. If it does not, it MUST be ignored.
+  if (!icon->src.IsValid())
+    return absl::nullopt;
+
+  icon->type = ParseIconType(icon_object);
+  icon->sizes = ParseIconSizes(icon_object);
+  auto purpose = ParseIconPurpose(icon_object);
+  if (!purpose)
+    return absl::nullopt;
+
+  icon->purpose = std::move(*purpose);
+  return icon;
+}
+
 String ManifestParser::ParseShortcutName(const JSONObject* shortcut) {
   absl::optional<String> name =
       ParseStringForMember(shortcut, "shortcut", "name", true, Trim(true));
diff --git a/third_party/blink/renderer/modules/manifest/manifest_parser.h b/third_party/blink/renderer/modules/manifest/manifest_parser.h
index 1ca975f..92e786cc 100644
--- a/third_party/blink/renderer/modules/manifest/manifest_parser.h
+++ b/third_party/blink/renderer/modules/manifest/manifest_parser.h
@@ -10,6 +10,7 @@
 #include "base/types/strong_alias.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
 #include "third_party/blink/public/common/permissions_policy/permissions_policy.h"
+#include "third_party/blink/public/mojom/manifest/manifest.mojom-blink-forward.h"
 #include "third_party/blink/public/mojom/manifest/manifest.mojom-blink.h"
 #include "third_party/blink/public/mojom/permissions_policy/permissions_policy.mojom-blink.h"
 #include "third_party/blink/renderer/core/execution_context/execution_context.h"
@@ -216,17 +217,25 @@
       const JSONObject* object);
 
   // Parses the 'screenshots' field of a Manifest, as defined in:
-  // https://w3c.github.io/manifest/#screenshots-member
+  // https://www.w3.org/TR/manifest-app-info/#screenshots-member
   // Returns a vector of ManifestImageResourcePtr with the successfully parsed
   // screenshots, if any. An empty vector if the field was not present or empty.
-  Vector<mojom::blink::ManifestImageResourcePtr> ParseScreenshots(
+  Vector<mojom::blink::ManifestScreenshotPtr> ParseScreenshots(
       const JSONObject* object);
 
+  // Parse the 'platform' field of 'screenshots' as defined in:
+  // https://www.w3.org/TR/manifest-app-info/#platform-member
+  mojom::blink::ManifestScreenshot::Platform ParseScreenshotPlatform(
+      const JSONObject* screenshot);
+
   // A helper function for parsing ImageResources under |key| in the manifest.
-  Vector<mojom::blink::ManifestImageResourcePtr> ParseImageResource(
+  Vector<mojom::blink::ManifestImageResourcePtr> ParseImageResourceArray(
       const String& key,
       const JSONObject* object);
 
+  absl::optional<mojom::blink::ManifestImageResourcePtr> ParseImageResource(
+      const JSONValue* object);
+
   // Parses the 'name' field of a shortcut, as defined in:
   // https://w3c.github.io/manifest/#shortcuts-member
   // Returns the parsed string if any, a null string if the parsing failed.
diff --git a/third_party/blink/renderer/modules/manifest/manifest_parser_unittest.cc b/third_party/blink/renderer/modules/manifest/manifest_parser_unittest.cc
index 40d9d85..ac634104 100644
--- a/third_party/blink/renderer/modules/manifest/manifest_parser_unittest.cc
+++ b/third_party/blink/renderer/modules/manifest/manifest_parser_unittest.cc
@@ -13,6 +13,7 @@
 #include "third_party/abseil-cpp/absl/types/optional.h"
 #include "third_party/blink/public/common/features.h"
 #include "third_party/blink/public/common/permissions_policy/permissions_policy.h"
+#include "third_party/blink/public/mojom/manifest/manifest.mojom-blink.h"
 #include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
 #include "third_party/blink/renderer/platform/weborigin/kurl.h"
 #include "third_party/blink/renderer/platform/weborigin/security_origin.h"
@@ -1147,7 +1148,8 @@
 
     auto& screenshots = manifest->screenshots;
     EXPECT_EQ(screenshots.size(), 1u);
-    EXPECT_EQ(screenshots[0]->src.GetString(), "http://foo.com/manifest.json");
+    EXPECT_EQ(screenshots[0]->image->src.GetString(),
+              "http://foo.com/manifest.json");
     EXPECT_FALSE(IsManifestEmpty(manifest));
     EXPECT_EQ(0u, GetErrorCount());
   }
@@ -1160,12 +1162,70 @@
 
     auto& screenshots = manifest->screenshots;
     EXPECT_EQ(screenshots.size(), 1u);
-    EXPECT_EQ(screenshots[0]->src.GetString(), "http://foo.com/foo.jpg");
+    EXPECT_EQ(screenshots[0]->image->src.GetString(), "http://foo.com/foo.jpg");
     EXPECT_FALSE(IsManifestEmpty(manifest));
     EXPECT_EQ(0u, GetErrorCount());
   }
 }
 
+TEST_F(ManifestParserTest, ScreenshotPlatformParseRules) {
+  // Smoke test.
+  {
+    auto& manifest = ParseManifest(
+        R"({ "screenshots": [{ "src": "foo.jpg", "platform": "narrow" }] })");
+    EXPECT_FALSE(manifest->screenshots.IsEmpty());
+
+    auto& screenshots = manifest->screenshots;
+    EXPECT_EQ(screenshots.size(), 1u);
+    EXPECT_EQ(screenshots[0]->platform,
+              mojom::blink::ManifestScreenshot::Platform::kNarrow);
+    EXPECT_FALSE(IsManifestEmpty(manifest));
+    EXPECT_EQ(0u, GetErrorCount());
+  }
+
+  // Unspecified.
+  {
+    auto& manifest =
+        ParseManifest(R"({ "screenshots": [{ "src": "foo.jpg"}] })");
+    EXPECT_FALSE(manifest->screenshots.IsEmpty());
+
+    auto& screenshots = manifest->screenshots;
+    EXPECT_EQ(screenshots.size(), 1u);
+    EXPECT_EQ(screenshots[0]->platform,
+              mojom::blink::ManifestScreenshot::Platform::kUnknown);
+    EXPECT_FALSE(IsManifestEmpty(manifest));
+    EXPECT_EQ(0u, GetErrorCount());
+  }
+
+  // Invalid type.
+  {
+    auto& manifest = ParseManifest(
+        R"({ "screenshots": [{ "src": "foo.jpg", "platform": 1}] })");
+    EXPECT_FALSE(manifest->screenshots.IsEmpty());
+
+    auto& screenshots = manifest->screenshots;
+    EXPECT_EQ(screenshots.size(), 1u);
+    EXPECT_EQ(screenshots[0]->platform,
+              mojom::blink::ManifestScreenshot::Platform::kUnknown);
+    EXPECT_FALSE(IsManifestEmpty(manifest));
+    EXPECT_EQ(1u, GetErrorCount());
+  }
+
+  // Unrecognized string.
+  {
+    auto& manifest = ParseManifest(
+        R"({ "screenshots": [{ "src": "foo.jpg", "platform": "windows"}] })");
+    EXPECT_FALSE(manifest->screenshots.IsEmpty());
+
+    auto& screenshots = manifest->screenshots;
+    EXPECT_EQ(screenshots.size(), 1u);
+    EXPECT_EQ(screenshots[0]->platform,
+              mojom::blink::ManifestScreenshot::Platform::kUnknown);
+    EXPECT_FALSE(IsManifestEmpty(manifest));
+    EXPECT_EQ(1u, GetErrorCount());
+  }
+}
+
 TEST_F(ManifestParserTest, IconSrcParseRules) {
   // Smoke test.
   {
diff --git a/third_party/blink/renderer/platform/graphics/paint/geometry_mapper_transform_cache.cc b/third_party/blink/renderer/platform/graphics/paint/geometry_mapper_transform_cache.cc
index b8661f7..61ee04e0 100644
--- a/third_party/blink/renderer/platform/graphics/paint/geometry_mapper_transform_cache.cc
+++ b/third_party/blink/renderer/platform/graphics/paint/geometry_mapper_transform_cache.cc
@@ -47,6 +47,9 @@
   has_sticky_ =
       node.RequiresCompositingForStickyPosition() || parent.has_sticky_;
 
+  is_backface_hidden_ =
+      node.IsBackfaceHiddenInternal(parent.is_backface_hidden_);
+
   nearest_scroll_translation_ =
       node.ScrollNode() ? &node : parent.nearest_scroll_translation_;
 
diff --git a/third_party/blink/renderer/platform/graphics/paint/geometry_mapper_transform_cache.h b/third_party/blink/renderer/platform/graphics/paint/geometry_mapper_transform_cache.h
index 23d5dfe..ac26d1a 100644
--- a/third_party/blink/renderer/platform/graphics/paint/geometry_mapper_transform_cache.h
+++ b/third_party/blink/renderer/platform/graphics/paint/geometry_mapper_transform_cache.h
@@ -118,6 +118,8 @@
   bool has_fixed() const { return has_fixed_; }
   bool has_sticky() const { return has_sticky_; }
 
+  bool is_backface_hidden() const { return is_backface_hidden_; }
+
   const TransformPaintPropertyNode& nearest_scroll_translation() const {
     DCHECK(nearest_scroll_translation_);
     return *nearest_scroll_translation_;
@@ -224,6 +226,8 @@
   // Whether or not there is a sticky translation to the root.
   bool has_sticky_ = false;
 
+  bool is_backface_hidden_ = false;
+
   const TransformPaintPropertyNode* nearest_scroll_translation_ = nullptr;
   const TransformPaintPropertyNode* nearest_directly_composited_ancestor_ =
       nullptr;
diff --git a/third_party/blink/renderer/platform/graphics/paint/transform_paint_property_node.h b/third_party/blink/renderer/platform/graphics/paint/transform_paint_property_node.h
index d001b15..0717a03c 100644
--- a/third_party/blink/renderer/platform/graphics/paint/transform_paint_property_node.h
+++ b/third_party/blink/renderer/platform/graphics/paint/transform_paint_property_node.h
@@ -320,6 +320,10 @@
     return state_.backface_visibility;
   }
 
+  bool IsBackfaceHidden() const {
+    return GetTransformCache().is_backface_hidden();
+  }
+
   // Returns true if the backface visibility for this node is the same as that
   // of its parent. This will be true for the Root node.
   bool BackfaceVisibilitySameAsParent() const {
@@ -342,19 +346,6 @@
            Parent()->Unalias().state_.flags.flattens_inherited_transform;
   }
 
-  // Returns the first non-inherited BackefaceVisibility value along the
-  // transform node ancestor chain, including this node's value if it is
-  // non-inherited. TODO(wangxianzhu): Let PaintPropertyTreeBuilder calculate
-  // the value instead of walking up the tree.
-  bool IsBackfaceHidden() const {
-    const auto* node = this;
-    while (node &&
-           node->state_.backface_visibility == BackfaceVisibility::kInherited)
-      node = node->UnaliasedParent();
-    return node &&
-           node->state_.backface_visibility == BackfaceVisibility::kHidden;
-  }
-
   bool HasDirectCompositingReasons() const {
     return DirectCompositingReasons() != CompositingReason::kNone;
   }
@@ -452,6 +443,12 @@
     return state_.direct_compositing_reasons;
   }
 
+  bool IsBackfaceHiddenInternal(bool parent_backface_hidden) const {
+    if (state_.backface_visibility == BackfaceVisibility::kInherited)
+      return parent_backface_hidden;
+    return state_.backface_visibility == BackfaceVisibility::kHidden;
+  }
+
   void Validate() const {
 #if DCHECK_IS_ON()
     if (state_.scroll) {
diff --git a/third_party/blink/renderer/platform/runtime_enabled_features.json5 b/third_party/blink/renderer/platform/runtime_enabled_features.json5
index 374f441..8843256 100644
--- a/third_party/blink/renderer/platform/runtime_enabled_features.json5
+++ b/third_party/blink/renderer/platform/runtime_enabled_features.json5
@@ -1171,6 +1171,10 @@
       status: "stable",
     },
     {
+      name: "HighlightPointerEvents",
+      depends_on: ["HighlightAPI"],
+    },
+    {
       name: "HrefTranslate",
       depends_on: ["TranslateService"],
       origin_trial_feature_name: "HrefTranslate",
diff --git a/third_party/blink/tools/build_wpt_metadata.py b/third_party/blink/tools/build_wpt_metadata.py
index ff70a9e..da54577 100755
--- a/third_party/blink/tools/build_wpt_metadata.py
+++ b/third_party/blink/tools/build_wpt_metadata.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env vpython
+#!/usr/bin/env vpython3
 # Copyright 2019 The Chromium Authors. All rights reserved.
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
diff --git a/third_party/blink/tools/check_testharness_expected_pass.py b/third_party/blink/tools/check_testharness_expected_pass.py
index fc64df7..7664046 100755
--- a/third_party/blink/tools/check_testharness_expected_pass.py
+++ b/third_party/blink/tools/check_testharness_expected_pass.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env vpython
+#!/usr/bin/env vpython3
 #
 # Copyright 2014 The Chromium Authors. All rights reserved.
 # Use of this source code is governed by a BSD-style license that can be
diff --git a/third_party/blink/tools/compile_devtools_frontend.py b/third_party/blink/tools/compile_devtools_frontend.py
index 5589d451..bc3a9b97 100755
--- a/third_party/blink/tools/compile_devtools_frontend.py
+++ b/third_party/blink/tools/compile_devtools_frontend.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env vpython
+#!/usr/bin/env vpython3
 # Copyright 2017 The Chromium Authors. All rights reserved.
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
diff --git a/third_party/blink/tools/diff_wpt_results.py b/third_party/blink/tools/diff_wpt_results.py
index 2f9a366..6ad5187d 100755
--- a/third_party/blink/tools/diff_wpt_results.py
+++ b/third_party/blink/tools/diff_wpt_results.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env vpython
+#!/usr/bin/env vpython3
 
 # Copyright (C) 2021 Google Inc.  All rights reserved.
 #
diff --git a/third_party/blink/tools/diff_wpt_results_unittest.py b/third_party/blink/tools/diff_wpt_results_unittest.py
index 48a3e7d..0d46473 100755
--- a/third_party/blink/tools/diff_wpt_results_unittest.py
+++ b/third_party/blink/tools/diff_wpt_results_unittest.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env vpython
+#!/usr/bin/env vpython3
 
 # Copyright (C) 2021 Google Inc.  All rights reserved.
 #
diff --git a/third_party/blink/tools/merge_web_test_results.py b/third_party/blink/tools/merge_web_test_results.py
index c9f542b..0702d67 100755
--- a/third_party/blink/tools/merge_web_test_results.py
+++ b/third_party/blink/tools/merge_web_test_results.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env vpython
+#!/usr/bin/env vpython3
 #
 # Copyright 2017 The Chromium Authors. All rights reserved.
 # Use of this source code is governed by a BSD-style license that can be
diff --git a/third_party/blink/tools/print_stale_test_expectations_entries.py b/third_party/blink/tools/print_stale_test_expectations_entries.py
index 9870a4a..141184e 100755
--- a/third_party/blink/tools/print_stale_test_expectations_entries.py
+++ b/third_party/blink/tools/print_stale_test_expectations_entries.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env vpython
+#!/usr/bin/env vpython3
 #
 # Copyright (C) 2013 Google Inc. All rights reserved.
 #
@@ -33,9 +33,9 @@
 import datetime
 import json
 import optparse
-import StringIO
+from six import StringIO
 import sys
-import urllib2
+from six.moves import urllib
 
 from blinkpy.common.host import Host
 from blinkpy.web_tests.models.test_expectations import TestExpectationParser
@@ -105,22 +105,23 @@
                     self.populate_bug_info(bug_link, test_name)
                 # Return the stale bug's information.
                 if all(self.is_stale(bug_link) for bug_link in bug_links):
-                    print line.original_string.strip()
+                    print(line.original_string.strip())
                     return [
                         bug_links[0], self.bug_info[bug_links[0]].filename,
                         self.bug_info[bug_links[0]].days_since_last_update,
                         self.bug_info[bug_links[0]].owner,
                         self.bug_info[bug_links[0]].status
                     ]
-        except urllib2.HTTPError as error:
+        except urllib.error.HTTPError as error:
             if error.code == 404:
                 message = 'got 404, bug does not exist.'
             elif error.code == 403:
                 message = 'got 403, not accessible. Not able to tell if it\'s stale.'
             else:
                 message = str(error)
-            print >> sys.stderr, 'Error when checking %s: %s' % (
-                ','.join(bug_links), message)
+            print(
+                'Error when checking %s: %s' % (','.join(bug_links), message),
+                sys.stderr)
         return None
 
     def populate_bug_info(self, bug_link, test_name):
@@ -129,7 +130,7 @@
         # In case there's an error in the request, don't make the same request again.
         bug_number = bug_link.strip(CRBUG_PREFIX)
         url = GOOGLE_CODE_URL % bug_number
-        response = urllib2.urlopen(url)
+        response = urllib.urlopen(url)
         parsed = json.loads(response.read())
         parsed_time = datetime.datetime.strptime(
             parsed['updated'].split(".")[0] + "UTC", "%Y-%m-%dT%H:%M:%S%Z")
diff --git a/third_party/blink/tools/read_checksum_from_png.py b/third_party/blink/tools/read_checksum_from_png.py
index fe972610..448a542 100755
--- a/third_party/blink/tools/read_checksum_from_png.py
+++ b/third_party/blink/tools/read_checksum_from_png.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env vpython
+#!/usr/bin/env vpython3
 # Copyright (c) 2011 Google Inc. All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -34,6 +34,6 @@
 
 if '__main__' == __name__:
     for filename in sys.argv[1:]:
-        with open(filename, 'r') as filehandle:
-            print "%s: %s" % (read_checksum_from_png.read_checksum(filehandle),
-                              filename)
+        with open(filename, 'rb') as filehandle:
+            print("%s: %s" %
+                  (read_checksum_from_png.read_checksum(filehandle), filename))
diff --git a/third_party/blink/tools/run_bindings_tests.py b/third_party/blink/tools/run_bindings_tests.py
index 003d6d4..3dcc27d 100755
--- a/third_party/blink/tools/run_bindings_tests.py
+++ b/third_party/blink/tools/run_bindings_tests.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env vpython
+#!/usr/bin/env vpython3
 # Copyright (C) 2010 Google Inc.  All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/third_party/blink/tools/run_blink_httpd.py b/third_party/blink/tools/run_blink_httpd.py
index eae1ddb7..f3610de 100755
--- a/third_party/blink/tools/run_blink_httpd.py
+++ b/third_party/blink/tools/run_blink_httpd.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env vpython
+#!/usr/bin/env vpython3
 # Copyright (C) 2010 Google Inc. All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/third_party/blink/tools/run_blink_websocketserver.py b/third_party/blink/tools/run_blink_websocketserver.py
index ba0d9f7..25a9f614 100755
--- a/third_party/blink/tools/run_blink_websocketserver.py
+++ b/third_party/blink/tools/run_blink_websocketserver.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env vpython
+#!/usr/bin/env vpython3
 # Copyright (C) 2012 Google Inc. All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
diff --git a/third_party/blink/tools/try_flag.py b/third_party/blink/tools/try_flag.py
index fa2594a8..b6a1411 100755
--- a/third_party/blink/tools/try_flag.py
+++ b/third_party/blink/tools/try_flag.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env vpython
+#!/usr/bin/env vpython3
 # Copyright 2017 The Chromium Authors. All rights reserved.
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
diff --git a/third_party/blink/tools/update_expectations.py b/third_party/blink/tools/update_expectations.py
index 9294f8acd..69e9b3c 100755
--- a/third_party/blink/tools/update_expectations.py
+++ b/third_party/blink/tools/update_expectations.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env vpython
+#!/usr/bin/env vpython3
 #
 # Copyright 2016 The Chromium Authors. All rights reserved.
 # Use of this source code is governed by a BSD-style license that can be
diff --git a/third_party/blink/tools/wpt_cleanup.py b/third_party/blink/tools/wpt_cleanup.py
index d00af8e6..d0b7c4c 100755
--- a/third_party/blink/tools/wpt_cleanup.py
+++ b/third_party/blink/tools/wpt_cleanup.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env vpython
+#!/usr/bin/env vpython3
 # Copyright 2017 The Chromium Authors. All rights reserved.
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
diff --git a/third_party/blink/tools/wpt_update_expectations.py b/third_party/blink/tools/wpt_update_expectations.py
index 3acac28d..0db81d83 100755
--- a/third_party/blink/tools/wpt_update_expectations.py
+++ b/third_party/blink/tools/wpt_update_expectations.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env vpython
+#!/usr/bin/env vpython3
 # Copyright 2016 The Chromium Authors. All rights reserved.
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
diff --git a/third_party/blink/web_tests/FlagExpectations/disable-site-isolation-trials b/third_party/blink/web_tests/FlagExpectations/disable-site-isolation-trials
index 25ff7619..f385e117 100644
--- a/third_party/blink/web_tests/FlagExpectations/disable-site-isolation-trials
+++ b/third_party/blink/web_tests/FlagExpectations/disable-site-isolation-trials
@@ -33,6 +33,7 @@
 http/tests/inspector-protocol/target/target-setAutoAttach-oopif-multisession-wait.js [ Skip ]
 http/tests/inspector-protocol/target/auto-attach-sub-sub-frame.js [ Skip ]
 http/tests/inspector-protocol/target/message-to-detached-session.js [ Skip ]
+http/tests/inspector-protocol/target/tab-target.js [ Skip ]
 http/tests/inspector-protocol/target/target-filter.js [ Skip ]
 virtual/fenced-frame-mparch/http/tests/inspector-protocol/fenced-frame/fenced-frame-in-oopif-auto-attach.js [ Skip ]
 
diff --git a/third_party/blink/web_tests/external/wpt/css/css-tables/border-collapse-dynamic-oof.html b/third_party/blink/web_tests/external/wpt/css/css-tables/border-collapse-dynamic-oof.html
new file mode 100644
index 0000000..6699fba
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-tables/border-collapse-dynamic-oof.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1348154">
+<link rel="match" href="../reference/ref-filled-green-100px-square.xht">
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div style="width: 100px; height: 100px; background: red;">
+  <div style="display: table; border-collapse: collapse; border: solid green 50px; width: 0; height: 0;">
+    <div style="display: table-cell; position: relative;">
+      <div id="target" style="position: absolute;"></div>
+    </div>
+  </div>
+</div>
+<script>
+document.body.offsetTop;
+document.getElementById('target').style.top = '10px';
+</script>
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/resources/inspector-protocol-test.js b/third_party/blink/web_tests/http/tests/inspector-protocol/resources/inspector-protocol-test.js
index 6ee6693..832fff629 100644
--- a/third_party/blink/web_tests/http/tests/inspector-protocol/resources/inspector-protocol-test.js
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/resources/inspector-protocol-test.js
@@ -185,6 +185,10 @@
     })
   };
 
+  browserSession() {
+    return this._browserSession;
+  }
+
   browserP() {
     return this._browserSession.protocol;
   }
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/target/tab-target.js b/third_party/blink/web_tests/http/tests/inspector-protocol/target/tab-target.js
new file mode 100644
index 0000000..d25a6a73
--- /dev/null
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/target/tab-target.js
@@ -0,0 +1,45 @@
+(async function(testRunner) {
+  const pageURL = 'http://127.0.0.1:8000/inspector-protocol/resources/inspector-protocol-page.html';
+  const {session, dp} = await testRunner.startURL(pageURL,
+    'Tests basic functionality of tab target.');
+
+  const bp = testRunner.browserP();
+  const {targetInfos} = (await bp.Target.getTargets({filter: [{type: "tab"}]})).result;
+  const tabTargets = targetInfos.sort((a, b) => a.url.localeCompare(b.url));
+  testRunner.log(tabTargets);
+  const targetUnderTest = tabTargets.find(target => target.url === pageURL);
+
+  const tabSessionId = (await bp.Target.attachToTarget({targetId: targetUnderTest.targetId, flatten: true})).result.sessionId;
+  const tabSession = testRunner.browserSession().createChild(tabSessionId);
+  const tp = tabSession.protocol;
+  const tabTargetInfo = (await tp.Target.getTargetInfo());
+  testRunner.log(tabTargetInfo, "Tab target info, as obtained from target");
+
+  const autoAttachCompletePromise = tp.Target.setAutoAttach({autoAttach: true, waitForDebuggerOnStart: false, flatten: true});
+  const autoAttachedTargets = [];
+  tp.Target.onAttachedToTarget(target => {
+    autoAttachedTargets.push(target.params);
+  });
+  // All auto-attaches should be complete before we return.
+  await autoAttachCompletePromise;
+  testRunner.log(autoAttachedTargets, "Auto-attached targets (there should be exactly 1): ");
+  if (autoAttachedTargets.length !== 1) {
+    testRunner.completeTest();
+    return;
+  }
+  const frameSession = tabSession.createChild(autoAttachedTargets[0].sessionId);
+  testRunner.log(`Attached to page ${await frameSession.evaluate('location.href')}`);
+  // Now create a cross-process subframe and make sure it only gets attached to the
+  // frame target, not to the tab one.
+  await frameSession.protocol.Target.setAutoAttach({autoAttach: true, waitForDebuggerOnStart: false, flatten: true});
+  const subframePromise = frameSession.protocol.Target.onceAttachedToTarget();
+  await frameSession.evaluateAsync(`new Promise(resolve => {
+    const frame = document.createElement('iframe');
+    frame.src = 'http://devtools.oopif-a.test:8000/inspector-protocol/resources/inspector-protocol-page.html';
+    frame.onload = resolve;
+    document.body.appendChild(frame);
+  })`);
+  testRunner.log(await subframePromise, `Auto-attached subframe target`);
+  testRunner.log(`Number of auto-attached tab sessions (should be 1): ${autoAttachedTargets.length}`);
+  testRunner.completeTest();
+});
diff --git a/third_party/blink/web_tests/platform/generic/http/tests/inspector-protocol/target/tab-target-expected.txt b/third_party/blink/web_tests/platform/generic/http/tests/inspector-protocol/target/tab-target-expected.txt
new file mode 100644
index 0000000..04cb1f5
--- /dev/null
+++ b/third_party/blink/web_tests/platform/generic/http/tests/inspector-protocol/target/tab-target-expected.txt
@@ -0,0 +1,71 @@
+Tests basic functionality of tab target.
+[
+    [0] : {
+        attached : false
+        browserContextId : <string>
+        canAccessOpener : false
+        targetId : <string>
+        title : 127.0.0.1:8000/inspector-protocol/resources/inspector-protocol-page.html
+        type : tab
+        url : http://127.0.0.1:8000/inspector-protocol/resources/inspector-protocol-page.html
+    }
+    [1] : {
+        attached : false
+        browserContextId : <string>
+        canAccessOpener : false
+        targetId : <string>
+        title : 127.0.0.1:8000/inspector-protocol/resources/inspector-protocol-test.html?test=http://127.0.0.1:8000/inspector-protocol/target/tab-target.js
+        type : tab
+        url : http://127.0.0.1:8000/inspector-protocol/resources/inspector-protocol-test.html?test=http://127.0.0.1:8000/inspector-protocol/target/tab-target.js
+    }
+]
+Tab target info, as obtained from target{
+    id : <number>
+    result : {
+        targetInfo : {
+            attached : true
+            browserContextId : <string>
+            canAccessOpener : false
+            targetId : <string>
+            title : 127.0.0.1:8000/inspector-protocol/resources/inspector-protocol-page.html
+            type : tab
+            url : http://127.0.0.1:8000/inspector-protocol/resources/inspector-protocol-page.html
+        }
+    }
+    sessionId : <string>
+}
+Auto-attached targets (there should be exactly 1): [
+    [0] : {
+        sessionId : <string>
+        targetInfo : {
+            attached : true
+            browserContextId : <string>
+            canAccessOpener : false
+            targetId : <string>
+            title : 127.0.0.1:8000/inspector-protocol/resources/inspector-protocol-page.html
+            type : page
+            url : http://127.0.0.1:8000/inspector-protocol/resources/inspector-protocol-page.html
+        }
+        waitingForDebugger : false
+    }
+]
+Attached to page http://127.0.0.1:8000/inspector-protocol/resources/inspector-protocol-page.html
+Auto-attached subframe target{
+    method : Target.attachedToTarget
+    params : {
+        sessionId : <string>
+        targetInfo : {
+            attached : true
+            browserContextId : <string>
+            canAccessOpener : false
+            targetId : <string>
+            title : 
+            type : iframe
+            url : 
+        }
+        waitingForDebugger : false
+    }
+    sessionId : <string>
+}
+Number of auto-attached tab sessions (should be 1): 1
+
diff --git a/third_party/blink/web_tests/platform/generic/http/tests/inspector-protocol/target/target-filter-expected.txt b/third_party/blink/web_tests/platform/generic/http/tests/inspector-protocol/target/target-filter-expected.txt
index d2a9b2c..0f2e55c 100644
--- a/third_party/blink/web_tests/platform/generic/http/tests/inspector-protocol/target/target-filter-expected.txt
+++ b/third_party/blink/web_tests/platform/generic/http/tests/inspector-protocol/target/target-filter-expected.txt
@@ -78,6 +78,24 @@
         type : shared_worker
         url : http://127.0.0.1:8000/inspector-protocol/fetch/resources/shared-worker.js
     }
+    [4] : {
+        attached : false
+        browserContextId : <string>
+        canAccessOpener : false
+        targetId : <string>
+        title : 127.0.0.1:8000/inspector-protocol/resources/inspector-protocol-test.html?test=http://127.0.0.1:8000/inspector-protocol/target/target-filter.js
+        type : tab
+        url : http://127.0.0.1:8000/inspector-protocol/resources/inspector-protocol-test.html?test=http://127.0.0.1:8000/inspector-protocol/target/target-filter.js
+    }
+    [5] : {
+        attached : false
+        browserContextId : <string>
+        canAccessOpener : false
+        targetId : <string>
+        title : 127.0.0.1:8000/inspector-protocol/resources/inspector-protocol-page.html
+        type : tab
+        url : http://127.0.0.1:8000/inspector-protocol/resources/inspector-protocol-page.html
+    }
 ]
 default filter [
     [0] : {
@@ -172,6 +190,24 @@
         type : shared_worker
         url : http://127.0.0.1:8000/inspector-protocol/fetch/resources/shared-worker.js
     }
+    [5] : {
+        attached : false
+        browserContextId : <string>
+        canAccessOpener : false
+        targetId : <string>
+        title : 127.0.0.1:8000/inspector-protocol/resources/inspector-protocol-test.html?test=http://127.0.0.1:8000/inspector-protocol/target/target-filter.js
+        type : tab
+        url : http://127.0.0.1:8000/inspector-protocol/resources/inspector-protocol-test.html?test=http://127.0.0.1:8000/inspector-protocol/target/target-filter.js
+    }
+    [6] : {
+        attached : false
+        browserContextId : <string>
+        canAccessOpener : false
+        targetId : <string>
+        title : 127.0.0.1:8000/inspector-protocol/resources/inspector-protocol-page.html
+        type : tab
+        url : http://127.0.0.1:8000/inspector-protocol/resources/inspector-protocol-page.html
+    }
 ]
 Discovered targets with type = shared_worker [
     [0] : {
diff --git a/third_party/blink/web_tests/wpt_internal/document-transition/event-pseudo-name.html b/third_party/blink/web_tests/wpt_internal/document-transition/event-pseudo-name.html
new file mode 100644
index 0000000..325cfcc5
--- /dev/null
+++ b/third_party/blink/web_tests/wpt_internal/document-transition/event-pseudo-name.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<title>Shared transitions: event pseudo name</title>
+<link rel="help" href="https://github.com/WICG/shared-element-transitions">
+<link rel="author" href="mailto:vmpstr@chromium.org">
+<link rel="match" href="web-animations-api-ref.html">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<style>
+:root { page-transition-tag: none; }
+#first {
+  background: blue;
+  width: 100px;
+  height: 100px;
+  contain:  paint;
+  page-transition-tag: shared;
+}
+
+html::page-transition-container(*),
+html::page-transition-image-wrapper(*),
+html::page-transition-incoming-image(*),
+html::page-transition-outgoing-image(*) {
+  animation-duration: 600s;
+}
+
+@keyframes fade-in {
+  from { opacity: 0; }
+}
+html::page-transition-image-wrapper(*) {
+  animation: fade-in 600s both;
+}
+
+</style>
+<div id=first></div>
+<script>
+async_test(t => {
+  let names = [];
+  document.documentElement.addEventListener("animationstart", (e) => {
+    names.push(e.pseudoElement);
+    if (names.length == 4) {
+      t.step(() => assert_true(names.includes("::page-transition-container(shared)")));
+      t.step(() => assert_true(names.includes("::page-transition-image-wrapper(shared)")));
+      t.step(() => assert_true(names.includes("::page-transition-incoming-image(shared)")));
+      t.step(() => assert_true(names.includes("::page-transition-outgoing-image(shared)")));
+      t.done();
+    }
+  });
+  document.createDocumentTransition().start();
+}, "verifies pseudo name includes a tag");
+
+</script>
+
diff --git a/third_party/nearby/README.chromium b/third_party/nearby/README.chromium
index e2e0e09..47871ffe 100644
--- a/third_party/nearby/README.chromium
+++ b/third_party/nearby/README.chromium
@@ -1,7 +1,7 @@
 Name: Nearby Connections Library
 Short Name: Nearby
 URL: https://github.com/google/nearby
-Version: a37a7b7a839b1f3c189662af6720b94e3eaa3280
+Version: 0d4964da7babe0a0ae01cd4950c5215dbd7dd8d1
 License: Apache 2.0
 License File: LICENSE
 Security Critical: yes
diff --git a/third_party/widevine/cdm/BUILD.gn b/third_party/widevine/cdm/BUILD.gn
index be63a99..94d5d07 100644
--- a/third_party/widevine/cdm/BUILD.gn
+++ b/third_party/widevine/cdm/BUILD.gn
@@ -12,6 +12,8 @@
 import("//media/media_options.gni")
 import("//third_party/widevine/cdm/widevine.gni")
 
+assert(!bundle_widevine_cdm || (enable_widevine && enable_library_cdms))
+
 buildflag_header("buildflags") {
   header = "buildflags.h"
 
@@ -23,13 +25,9 @@
   ]
 }
 
-# No branding, use the default one.
-widevine_cdm_version_h_file = "widevine_cdm_version.h"
-widevine_cdm_binary_files = []
-widevine_cdm_manifest_and_license_files = []
-
 # TODO(xhwang): widevine_cdm_version.h is only used in few places. Clean this up
 # so we don't need to copy it in most cases.
+# Also, merge the bundle_widevine_cdm blocks as much as possible.
 if (bundle_widevine_cdm) {
   widevine_arch = target_cpu
 
@@ -47,6 +45,9 @@
     assert(is_win || is_mac)
     widevine_cdm_binary_files += [ "${widevine_cdm_root}/${cdm_file_name}.sig" ]
   }
+} else {
+  # The CDM is not bundled. Use the default file.
+  widevine_cdm_version_h_file = "widevine_cdm_version.h"
 }
 
 copy("version_h") {
@@ -69,18 +70,12 @@
   ]
 }
 
-if (widevine_cdm_manifest_and_license_files != []) {
+if (bundle_widevine_cdm) {
   copy("widevine_cdm_manifest_and_license") {
     sources = widevine_cdm_manifest_and_license_files
     outputs = [ "${root_out_dir}/WidevineCdm/{{source_file_part}}" ]
   }
-} else {
-  group("widevine_cdm_manifest_and_license") {
-    # NOP
-  }
-}
 
-if (widevine_cdm_binary_files != []) {
   copy("widevine_cdm_binary") {
     sources = widevine_cdm_binary_files
     outputs = [ "${root_out_dir}/${widevine_cdm_path}/{{source_file_part}}" ]
@@ -88,27 +83,27 @@
     # TODO(jrummell)
     # 'COPY_PHASE_STRIP': 'NO',
   }
+
+  group("cdm") {
+    # Needed at run time by tests, e.g. swarming tests to generate isolate.
+    # See https://crbug.com/824493 for context.
+    data_deps = [
+      ":widevine_cdm_binary",
+      ":widevine_cdm_manifest_and_license",
+    ]
+
+    # Needed at build time e.g. for mac bundle (//chrome:chrome_framework).
+    public_deps = [
+      ":widevine_cdm_binary",
+      ":widevine_cdm_manifest_and_license",
+    ]
+  }
 } else {
-  group("widevine_cdm_binary") {
+  group("cdm") {
     # NOP
   }
 }
 
-group("cdm") {
-  # Needed at run time by tests, e.g. swarming tests to generate isolate.
-  # See https://crbug.com/824493 for context.
-  data_deps = [
-    ":widevine_cdm_binary",
-    ":widevine_cdm_manifest_and_license",
-  ]
-
-  # Needed at build time e.g. for mac bundle (//chrome:chrome_framework).
-  public_deps = [
-    ":widevine_cdm_binary",
-    ":widevine_cdm_manifest_and_license",
-  ]
-}
-
 # This target exists for tests to depend on that pulls in a runtime dependency
 # on the license server.
 group("widevine_test_license_server") {
diff --git a/tools/mb/mb_config.pyl b/tools/mb/mb_config.pyl
index 638ce7a..03454f5 100644
--- a/tools/mb/mb_config.pyl
+++ b/tools/mb/mb_config.pyl
@@ -585,6 +585,7 @@
       'codesearch-gen-chromium-lacros': 'codesearch_gen_chromium_lacros_bot',
       'codesearch-gen-chromium-linux': 'codesearch_gen_chromium_bot',
       'codesearch-gen-chromium-mac': 'codesearch_gen_chromium_mac_bot',
+      'codesearch-gen-chromium-webview': 'codesearch_gen_chromium_webview_bot',
       'codesearch-gen-chromium-win': 'codesearch_gen_chromium_bot',
     },
 
@@ -1076,6 +1077,7 @@
       'gen-lacros-try': 'codesearch_gen_chromium_lacros_bot',
       'gen-linux-try': 'codesearch_gen_chromium_bot',
       'gen-mac-try': 'codesearch_gen_chromium_mac_bot',
+      'gen-webview-try': 'codesearch_gen_chromium_webview_bot',
       'gen-win-try': 'codesearch_gen_chromium_bot',
     },
 
@@ -2385,6 +2387,10 @@
       'codesearch', 'mac',
     ],
 
+    'codesearch_gen_chromium_webview_bot': [
+      'codesearch', 'android_without_codecs', 'static', 'goma',
+    ],
+
     'dawn_tests_asan_release_bot_dcheck_always_on_reclient': [
       'dawn_tests', 'asan', 'release_trybot_minimal_symbols_reclient',
     ],
diff --git a/tools/mb/mb_config_expectations/chromium.infra.codesearch.json b/tools/mb/mb_config_expectations/chromium.infra.codesearch.json
index 4c62306..6f8190a 100644
--- a/tools/mb/mb_config_expectations/chromium.infra.codesearch.json
+++ b/tools/mb/mb_config_expectations/chromium.infra.codesearch.json
@@ -76,6 +76,19 @@
       "use_goma": true
     }
   },
+  "codesearch-gen-chromium-webview": {
+    "gn_args": {
+      "blink_enable_generated_code_formatting": true,
+      "clang_use_chrome_plugins": false,
+      "enable_kythe_annotations": true,
+      "is_clang": true,
+      "is_component_build": false,
+      "is_debug": true,
+      "symbol_level": 1,
+      "target_os": "android",
+      "use_goma": true
+    }
+  },
   "codesearch-gen-chromium-win": {
     "gn_args": {
       "blink_enable_generated_code_formatting": true,
diff --git a/tools/mb/mb_config_expectations/tryserver.chromium.codesearch.json b/tools/mb/mb_config_expectations/tryserver.chromium.codesearch.json
index 621a59fc7b..a31fb47 100644
--- a/tools/mb/mb_config_expectations/tryserver.chromium.codesearch.json
+++ b/tools/mb/mb_config_expectations/tryserver.chromium.codesearch.json
@@ -76,6 +76,19 @@
       "use_goma": true
     }
   },
+  "gen-webview-try": {
+    "gn_args": {
+      "blink_enable_generated_code_formatting": true,
+      "clang_use_chrome_plugins": false,
+      "enable_kythe_annotations": true,
+      "is_clang": true,
+      "is_component_build": false,
+      "is_debug": true,
+      "symbol_level": 1,
+      "target_os": "android",
+      "use_goma": true
+    }
+  },
   "gen-win-try": {
     "gn_args": {
       "blink_enable_generated_code_formatting": true,
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index 74795981..901f2fc 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -72890,6 +72890,7 @@
   <int value="22" label="PAGE_ENTITIES"/>
   <int value="23" label="HISTORY_CLUSTERS"/>
   <int value="24" label="THANK_CREATOR_ELIGIBLE"/>
+  <int value="25" label="IBAN_AUTOFILL_BLOCKED"/>
 </enum>
 
 <enum name="OptOutBlacklistReason">
@@ -73026,6 +73027,7 @@
   <int value="103" label="Bluetooth: Pair Device"/>
   <int value="104" label="Bluetooth: Unpair Device"/>
   <int value="105" label="Bluetooth: Fast Pair On/Off"/>
+  <int value="106" label="Bluetooth: Fast Pair Saved Devices"/>
   <int value="200" label="Set Up MultiDevice"/>
   <int value="201" label="Verify MultiDevice Setup"/>
   <int value="202" label="MultiDevice On/Off"/>
diff --git a/tools/metrics/histograms/metadata/feature_engagement/histograms.xml b/tools/metrics/histograms/metadata/feature_engagement/histograms.xml
index 1ae8ac7..ba94cdb 100644
--- a/tools/metrics/histograms/metadata/feature_engagement/histograms.xml
+++ b/tools/metrics/histograms/metadata/feature_engagement/histograms.xml
@@ -465,6 +465,18 @@
   <token key="TutorialID" variants="TutorialID"/>
 </histogram>
 
+<histogram name="Tutorial{TutorialID}.StartedFromWhatsNewPage"
+    enum="BooleanSuccess" expires_after="2022-12-31">
+  <owner>dpenning@chromium.org</owner>
+  <owner>dfried@chromium.org</owner>
+  <summary>
+    The count of successful starts of the tutorial when {TutorialID} button is
+    clicked on the whats new page (called by the Tutorial BrowserCommand). A
+    false value correlates to a failure to start the tutorial.
+  </summary>
+  <token key="TutorialID" variants="TutorialID"/>
+</histogram>
+
 </histograms>
 
 </histogram-configuration>
diff --git a/tools/metrics/histograms/metadata/fingerprint/histograms.xml b/tools/metrics/histograms/metadata/fingerprint/histograms.xml
index 3c7103d..ae961496 100644
--- a/tools/metrics/histograms/metadata/fingerprint/histograms.xml
+++ b/tools/metrics/histograms/metadata/fingerprint/histograms.xml
@@ -245,6 +245,18 @@
   </summary>
 </histogram>
 
+<histogram name="Fingerprint.Unlock.RecentAttemptsCountBeforeSuccess"
+    units="attempts" expires_after="2023-04-05">
+  <owner>agrandi@google.com</owner>
+  <owner>chromeos-fingerprint@google.com</owner>
+  <summary>
+    Counts the number of recent fingerprint attempts until successful screen
+    unlock. Recent attempts are defined as happening within 3 seconds from each
+    others. The goal is to count intentional attempt to unlock the device and
+    exclude incidental touches of the fingerprint sensor.
+  </summary>
+</histogram>
+
 <histogram name="Fingerprint.Unlock.RecordFormatVersion"
     enum="FingerprintRecordFormatVersion" expires_after="2023-04-05">
   <owner>hesling@chromium.org</owner>
diff --git a/tools/metrics/histograms/metadata/optimization/histograms.xml b/tools/metrics/histograms/metadata/optimization/histograms.xml
index 9317286b..a60a770 100644
--- a/tools/metrics/histograms/metadata/optimization/histograms.xml
+++ b/tools/metrics/histograms/metadata/optimization/histograms.xml
@@ -68,6 +68,9 @@
   <variant name="HistoryClusters"
       summary="This optimization provides information for whether a page
                should be included in a history cluster."/>
+  <variant name="IBANAutofillBlocked"
+      summary="This optimization provides information for whether a page
+               should be blocked for IBAN autofill feature."/>
   <variant name="LinkPerformance"
       summary="Provides aggregated performance information for links on the
                page"/>
diff --git a/tools/metrics/histograms/metadata/safe_browsing/histograms.xml b/tools/metrics/histograms/metadata/safe_browsing/histograms.xml
index d8facc1..b82e151 100644
--- a/tools/metrics/histograms/metadata/safe_browsing/histograms.xml
+++ b/tools/metrics/histograms/metadata/safe_browsing/histograms.xml
@@ -1989,6 +1989,26 @@
     name="SafeBrowsing.TailoredSecurity.SyncPromptEnabledNotificationResult"
     enum="SafeBrowsingTailoredSecurityNotificationResult"
     expires_after="2022-12-11">
+  <obsolete>
+    This metric was useful until 2022-07-29, when the experiment for
+    TailoredSecurityDesktopNotice began and values for NoBrowserAvailable and
+    NoWebContentsAvailable could also contain results from showing the disabled
+    modal (due to crbug/1348654).
+  </obsolete>
+  <owner>jacastro@chromium.org</owner>
+  <owner>chrome-counter-abuse-alerts@google.com</owner>
+  <summary>
+    Records the result of trying to show the Chrome enhanced protection
+    opt-in/enabled notification to sync users. It is logged once each time
+    Chrome is informed that the account level enhanced protection bit has been
+    enabled.
+  </summary>
+</histogram>
+
+<histogram
+    name="SafeBrowsing.TailoredSecurity.SyncPromptEnabledNotificationResult2"
+    enum="SafeBrowsingTailoredSecurityNotificationResult"
+    expires_after="2022-12-11">
   <owner>jacastro@chromium.org</owner>
   <owner>chrome-counter-abuse-alerts@google.com</owner>
   <summary>
diff --git a/tools/perf/chrome-health-presets.yaml b/tools/perf/chrome-health-presets.yaml
index 7d1690c..3d0c0c5 100644
--- a/tools/perf/chrome-health-presets.yaml
+++ b/tools/perf/chrome-health-presets.yaml
@@ -138,6 +138,17 @@
           - android-pixel4-perf
         stories:
           - Speedometer2
+  speedometer2_pgo:
+    telemetry_batch_experiment:
+      - benchmark: speedometer2-chrome-health
+        configs:
+          - mac-laptop_low_end-perf-pgo
+          - mac-m1_mini_2020-perf-pgo
+          - linux-perf-pgo
+          - win-10-perf-pgo
+          - android-pixel4-perf
+        stories:
+          - Speedometer2
   motionmark:
     telemetry_batch_experiment:
       - benchmark: rendering.desktop
@@ -182,6 +193,50 @@
           - android-pixel4-perf
         stories:
           - motionmark_ramp_composite
+  motionmark_pgo:
+    telemetry_batch_experiment:
+      - benchmark: rendering.desktop
+        configs:
+          - mac-laptop_low_end-perf-pgo
+          - mac-m1_mini_2020-perf-pgo
+          - linux-perf-pgo
+          - win-10-perf-pgo
+        stories:
+          - motionmark_ramp_canvas_arcs
+          - motionmark_ramp_canvas_lines
+          - motionmark_ramp_design
+          - motionmark_ramp_focus
+          - motionmark_ramp_images
+          - motionmark_ramp_leaves
+          - motionmark_ramp_multiply
+          - motionmark_ramp_paths
+          - motionmark_ramp_suits
+      - benchmark: rendering.mobile
+        configs:
+          - android-pixel4-perf
+        stories:
+          - motionmark_ramp_canvas_arcs
+          - motionmark_ramp_canvas_lines
+          - motionmark_ramp_design
+          - motionmark_ramp_focus
+          - motionmark_ramp_images
+          - motionmark_ramp_leaves
+          - motionmark_ramp_multiply
+          - motionmark_ramp_paths
+          - motionmark_ramp_suits
+      - benchmark: rendering.desktop.notracing
+        configs:
+          - mac-laptop_low_end-perf-pgo
+          - mac-m1_mini_2020-perf-pgo
+          - linux-perf-pgo
+          - win-10-perf-pgo
+        stories:
+          - motionmark_ramp_composite
+      - benchmark: rendering.mobile.notracing
+        configs:
+          - android-pixel4-perf
+        stories:
+          - motionmark_ramp_composite
   jetstream2:
     telemetry_batch_experiment:
       - benchmark: jetstream2
@@ -193,7 +248,7 @@
           - android-pixel4-perf
         stories:
           - JetStream2
-  jetstream2-pgo:
+  jetstream2_pgo:
     telemetry_batch_experiment:
       - benchmark: jetstream2
         configs:
diff --git a/tools/perf/chrome-health-run-daily.sh b/tools/perf/chrome-health-run-daily.sh
index a9616bb6..54f4be29b 100644
--- a/tools/perf/chrome-health-run-daily.sh
+++ b/tools/perf/chrome-health-run-daily.sh
@@ -28,6 +28,6 @@
 headOfMain=`git whatchanged --grep="Updating trunk VERSION" --format="%H" -1 | head -n 1`
 
 # M vs. M-1
-~/depot_tools/pinpoint experiment-telemetry-start --base-commit=$pinnedReleaseMinusOne --exp-commit=$headOfRelease --presets-file ~/chromium/src/tools/perf/chrome-health-presets.yaml --preset=chrome_health --attempts=40
+~/depot_tools/pinpoint experiment-telemetry-start --base-commit=$pinnedReleaseMinusOne --exp-commit=$headOfRelease --presets-file ~/chromium/src/tools/perf/chrome-health-presets.yaml --preset=chrome_health_pgo --attempts=40
 # Main
 ~/depot_tools/pinpoint experiment-telemetry-start --base-commit=$pinnedMain --exp-commit=$headOfMain --presets-file ~/chromium/src/tools/perf/chrome-health-presets.yaml --preset=chrome_health_pgo --attempts=40
\ No newline at end of file
diff --git a/tools/perf/chrome-health-run-weekly.sh b/tools/perf/chrome-health-run-weekly.sh
new file mode 100644
index 0000000..3b536241
--- /dev/null
+++ b/tools/perf/chrome-health-run-weekly.sh
@@ -0,0 +1,29 @@
+# Copyright 2022 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+#!/bin/sh
+
+# BEFORE YOU RUN THIS - In your ~ directory, execute the following. Note - this is named health_chromium so that cd chr<tab> works.
+# mkdir health_chromium
+# cd health_chromium
+# fetch --nohooks chromium
+
+# Script to run weekly A/A tests
+
+releaseBranchNo=5112 #M104
+
+cd ~/health_chromium/src
+git fetch
+
+# Current release branch
+git checkout -b branch_$releaseBranchNo branch-heads/$releaseBranchNo
+git checkout -f branch_$releaseBranchNo
+git pull
+headOfRelease=`git whatchanged --grep="Incrementing VERSION" --format="%H" -1 | head -n 1`
+echo $headOfRelease
+
+# M vs. M-1
+for i in {1..200}
+do
+  ~/depot_tools/pinpoint experiment-telemetry-start --base-commit=$headOfRelease --exp-commit=$headOfRelease --presets-file ~/chromium/src/tools/perf/chrome-health-presets.yaml --preset=speedometer2_pgo --attempts=40;
+done
\ No newline at end of file
diff --git a/ui/accessibility/platform/ax_platform_node_win.cc b/ui/accessibility/platform/ax_platform_node_win.cc
index 50c41ed4..d59e1fb 100644
--- a/ui/accessibility/platform/ax_platform_node_win.cc
+++ b/ui/accessibility/platform/ax_platform_node_win.cc
@@ -1023,8 +1023,8 @@
               L"document"};
 
     case ax::mojom::Role::kGraphicsObject:
-      return {UIALocalizationStrategy::kSupply, UIA_PaneControlTypeId,
-              L"region"};
+      return {UIALocalizationStrategy::kSupply, UIA_GroupControlTypeId,
+              L"group"};
 
     case ax::mojom::Role::kGraphicsSymbol:
       return {UIALocalizationStrategy::kSupply, UIA_ImageControlTypeId, L"img"};
diff --git a/ui/accessibility/platform/ax_platform_node_win_unittest.cc b/ui/accessibility/platform/ax_platform_node_win_unittest.cc
index b4675e5..906bf06 100644
--- a/ui/accessibility/platform/ax_platform_node_win_unittest.cc
+++ b/ui/accessibility/platform/ax_platform_node_win_unittest.cc
@@ -5806,7 +5806,12 @@
   child6.role = ax::mojom::Role::kDialog;
   root.child_ids.push_back(child6.id);
 
-  Init(root, child1, child2, child3, child4, child5, child6);
+  AXNodeData child7;
+  child7.id = 8;
+  child7.role = ax::mojom::Role::kGraphicsObject;
+  root.child_ids.push_back(child7.id);
+
+  Init(root, child1, child2, child3, child4, child5, child6, child7);
 
   EXPECT_UIA_INT_EQ(
       QueryInterfaceFromNodeId<IRawElementProviderSimple>(child1.id),
@@ -5826,6 +5831,9 @@
   EXPECT_UIA_INT_EQ(
       QueryInterfaceFromNodeId<IRawElementProviderSimple>(child6.id),
       UIA_ControlTypePropertyId, int{UIA_WindowControlTypeId});
+  EXPECT_UIA_INT_EQ(
+      QueryInterfaceFromNodeId<IRawElementProviderSimple>(child7.id),
+      UIA_ControlTypePropertyId, int{UIA_GroupControlTypeId});
 }
 
 TEST_F(AXPlatformNodeWinTest, UIALandmarkType) {
diff --git a/ui/message_center/views/notification_header_view.cc b/ui/message_center/views/notification_header_view.cc
index 40de6f8..0f7fd0c 100644
--- a/ui/message_center/views/notification_header_view.cc
+++ b/ui/message_center/views/notification_header_view.cc
@@ -276,20 +276,20 @@
   summary_text_view_->SetText(l10n_util::GetStringFUTF16Int(
       IDS_MESSAGE_CENTER_NOTIFICATION_PROGRESS_PERCENTAGE, progress));
   has_progress_ = true;
-  UpdateSummaryTextVisibility();
+  UpdateSummaryTextAndTimestampVisibility();
 }
 
 void NotificationHeaderView::SetSummaryText(const std::u16string& text) {
   summary_text_view_->SetText(text);
   has_progress_ = false;
-  UpdateSummaryTextVisibility();
+  UpdateSummaryTextAndTimestampVisibility();
 }
 
 void NotificationHeaderView::SetOverflowIndicator(int count) {
   summary_text_view_->SetText(l10n_util::GetStringFUTF16Int(
       IDS_MESSAGE_CENTER_LIST_NOTIFICATION_HEADER_OVERFLOW_INDICATOR, count));
   has_progress_ = false;
-  UpdateSummaryTextVisibility();
+  UpdateSummaryTextAndTimestampVisibility();
 }
 
 void NotificationHeaderView::GetAccessibleNodeData(ui::AXNodeData* node_data) {
@@ -317,7 +317,7 @@
 
   timestamp_view_->SetText(relative_time);
   timestamp_ = timestamp;
-  UpdateSummaryTextVisibility();
+  UpdateSummaryTextAndTimestampVisibility();
 
   // Unretained is safe as the timer cancels the task on destruction.
   timestamp_update_timer_.Start(
@@ -334,7 +334,7 @@
   else
     timestamp_update_timer_.Stop();
 
-  UpdateSummaryTextVisibility();
+  UpdateSummaryTextAndTimestampVisibility();
 }
 
 void NotificationHeaderView::SetExpandButtonEnabled(bool enabled) {
@@ -381,7 +381,7 @@
 }
 
 void NotificationHeaderView::SetTimestampVisible(bool visible) {
-  timestamp_divider_->SetVisible(visible);
+  timestamp_divider_->SetVisible(!is_in_group_child_notification_ && visible);
   timestamp_view_->SetVisible(visible);
 }
 
@@ -398,6 +398,20 @@
   SetPreferredSize(gfx::Size(kNotificationWidth, kHeaderHeightInAsh));
 }
 
+void NotificationHeaderView::SetIsInGroupChildNotification(
+    bool is_in_group_child_notification) {
+  if (is_in_group_child_notification_ == is_in_group_child_notification)
+    return;
+  is_in_group_child_notification_ = is_in_group_child_notification;
+
+  app_name_view_->SetVisible(!is_in_group_child_notification_);
+  app_icon_view_->SetVisible(!is_in_ash_notification_ &&
+                             !is_in_group_child_notification_);
+  expand_button_->SetVisible(!is_in_ash_notification_ &&
+                             !is_in_group_child_notification_);
+  UpdateSummaryTextAndTimestampVisibility();
+}
+
 const std::u16string& NotificationHeaderView::app_name_for_testing() const {
   return app_name_view_->GetText();
 }
@@ -406,14 +420,14 @@
   return app_icon_view_->GetImage();
 }
 
-void NotificationHeaderView::UpdateSummaryTextVisibility() {
-  const bool summary_visible = !summary_text_view_->GetText().empty();
+void NotificationHeaderView::UpdateSummaryTextAndTimestampVisibility() {
+  const bool summary_visible = !is_in_group_child_notification_ &&
+                               !summary_text_view_->GetText().empty();
   summary_text_divider_->SetVisible(summary_visible);
   summary_text_view_->SetVisible(summary_visible);
 
   const bool timestamp_visible = !has_progress_ && timestamp_;
-  timestamp_divider_->SetVisible(timestamp_visible);
-  timestamp_view_->SetVisible(timestamp_visible);
+  SetTimestampVisible(timestamp_visible);
 
   // TODO(crbug.com/991492): this should not be necessary.
   detail_views_->InvalidateLayout();
diff --git a/ui/message_center/views/notification_header_view.h b/ui/message_center/views/notification_header_view.h
index cb413a96..0d967a0c 100644
--- a/ui/message_center/views/notification_header_view.h
+++ b/ui/message_center/views/notification_header_view.h
@@ -79,6 +79,9 @@
 
   void SetIsInAshNotificationView(bool is_in_ash_notification);
 
+  // The header only shows timestamp if it is in a group child notification.
+  void SetIsInGroupChildNotification(bool is_in_group_child_notification);
+
   // views::View:
   void GetAccessibleNodeData(ui::AXNodeData* node_data) override;
   void OnThemeChanged() override;
@@ -105,9 +108,11 @@
 
  private:
   FRIEND_TEST_ALL_PREFIXES(NotificationHeaderViewTest, SettingsMode);
+  FRIEND_TEST_ALL_PREFIXES(NotificationHeaderViewTest,
+                           GroupChildNotificationVisibility);
 
   // Update visibility for both |summary_text_view_| and |timestamp_view_|.
-  void UpdateSummaryTextVisibility();
+  void UpdateSummaryTextAndTimestampVisibility();
 
   void UpdateColors();
 
@@ -134,6 +139,9 @@
 
   // Whether this view is used for an ash notification view.
   bool is_in_ash_notification_ = false;
+
+  // Whether this view is used for a group child notification.
+  bool is_in_group_child_notification_ = false;
 };
 
 BEGIN_VIEW_BUILDER(MESSAGE_CENTER_EXPORT, NotificationHeaderView, views::Button)
diff --git a/ui/message_center/views/notification_header_view_unittest.cc b/ui/message_center/views/notification_header_view_unittest.cc
index 935dbb4..c1c61017 100644
--- a/ui/message_center/views/notification_header_view_unittest.cc
+++ b/ui/message_center/views/notification_header_view_unittest.cc
@@ -230,6 +230,48 @@
   EXPECT_FALSE(notification_header_view->expand_button()->GetVisible());
 }
 
+TEST_F(NotificationHeaderViewTest, GroupChildNotificationVisibility) {
+  notification_header_view_->SetSummaryText(u"summary");
+  notification_header_view_->SetTimestamp(base::Time::Now());
+
+  EXPECT_TRUE(
+      notification_header_view_->app_icon_view_for_testing()->GetVisible());
+  EXPECT_TRUE(notification_header_view_->expand_button()->GetVisible());
+  EXPECT_TRUE(
+      notification_header_view_->summary_text_for_testing()->GetVisible());
+  EXPECT_TRUE(notification_header_view_->summary_text_divider_->GetVisible());
+  EXPECT_TRUE(
+      notification_header_view_->timestamp_view_for_testing()->GetVisible());
+  EXPECT_TRUE(notification_header_view_->timestamp_divider_->GetVisible());
+
+  // For group child notification, all the views except `timestamp_view_` should
+  // not be visible.
+  notification_header_view_->SetIsInGroupChildNotification(
+      /*is_in_group_child_notification=*/true);
+  EXPECT_FALSE(
+      notification_header_view_->app_icon_view_for_testing()->GetVisible());
+  EXPECT_FALSE(notification_header_view_->expand_button()->GetVisible());
+  EXPECT_FALSE(
+      notification_header_view_->summary_text_for_testing()->GetVisible());
+  EXPECT_FALSE(notification_header_view_->summary_text_divider_->GetVisible());
+  EXPECT_FALSE(notification_header_view_->timestamp_divider_->GetVisible());
+  EXPECT_TRUE(
+      notification_header_view_->timestamp_view_for_testing()->GetVisible());
+
+  // Switching back.
+  notification_header_view_->SetIsInGroupChildNotification(
+      /*is_in_group_child_notification=*/false);
+  EXPECT_TRUE(
+      notification_header_view_->app_icon_view_for_testing()->GetVisible());
+  EXPECT_TRUE(notification_header_view_->expand_button()->GetVisible());
+  EXPECT_TRUE(
+      notification_header_view_->summary_text_for_testing()->GetVisible());
+  EXPECT_TRUE(notification_header_view_->summary_text_divider_->GetVisible());
+  EXPECT_TRUE(
+      notification_header_view_->timestamp_view_for_testing()->GetVisible());
+  EXPECT_TRUE(notification_header_view_->timestamp_divider_->GetVisible());
+}
+
 TEST_F(NotificationHeaderViewTest, MetadataTest) {
   views::test::TestViewMetadata(notification_header_view_);
 }
diff --git a/ui/webui/resources/cr_components/help_bubble/help_bubble.html b/ui/webui/resources/cr_components/help_bubble/help_bubble.html
index a9e48532..4faa986 100644
--- a/ui/webui/resources/cr_components/help_bubble/help_bubble.html
+++ b/ui/webui/resources/cr_components/help_bubble/help_bubble.html
@@ -1,48 +1,70 @@
-<style>
+<style include="cr-hidden-style">
   :host {
     position: absolute;
     z-index: 1;
   }
 
-  .arrow {
+  /* HelpBubblePosition.ABOVE */
+  :host([position='0']) {
+    transform: translateY(-100%);
+  }
+
+  /* HelpBubblePosition.BELOW */
+  :host([position='1']) {
+    transform: none;
+  }
+
+  /* HelpBubblePosition.LEFT */
+  :host([position='2']) {
+    transform: translate(-100%, -50%);
+  }
+
+  /* HelpBubblePosition.RIGHT */
+  :host([position='3']) {
+    transform: translateY(-50%);
+  }
+
+  #arrow {
+    --help-bubble-arrow-size: 16px;
+    --help-bubble-arrow-offset: calc(var(--help-bubble-arrow-size) / 2);
     background-color: var(--help-bubble-background);
-    height: 16px;
+    height: var(--help-bubble-arrow-size);
     position: absolute;
     transform: rotate(-45deg);
-    width: 16px;
+    width: var(--help-bubble-arrow-size);
     z-index: -1;
   }
 
   /* Turns the arrow direction downwards, when the bubble is placed above
    * the anchor element */
-  :host([position=above]) .arrow {
+  #arrow.above {
     border-bottom-left-radius: 2px;
-    bottom: -8px;
-    left: calc(50% - 8px);
+    bottom: calc(0 - var(--help-bubble-arrow-offset));
+    left: calc(50% - var(--help-bubble-arrow-offset));
   }
 
   /* Turns the arrow direction upwards, when the bubble is placed below
    * the anchor element */
-  :host([position=below]) .arrow {
+  #arrow.below {
     border-top-right-radius: 2px;
-    left: calc(50% - 8px);
-    top: -8px;
+    left: calc(50% - var(--help-bubble-arrow-offset));
+    top: calc(0 - var(--help-bubble-arrow-offset));
   }
 
   /* Turns the arrow direction to the right, when the bubble is placed to the
    * left of the anchor element */
-  :host([position=left]) .arrow {
+  #arrow.left {
     border-bottom-right-radius: 2px;
-    right: -8px;
-    top: calc(50% - 8px);
+    right: calc(0 - var(--help-bubble-arrow-offset));
+    top: calc(50% - var(--help-bubble-arrow-offset));
   }
 
   /* Turns the arrow direction to the left, when the bubble is placed to the
    * right of the anchor element */
-  :host([position=right]) .arrow {
+  #arrow.right {
     border-top-left-radius: 2px;
-    left: -8px;
-    top: calc(50% - 8px);
+    left: calc(0 - var(--help-bubble-arrow-offset));
+    top: calc(50% - var(--help-bubble-arrow-offset));
   }
 
   #topContainer {
@@ -50,24 +72,50 @@
     display: flex;
   }
 
-  #body {
+  #progress {
+    display: inline-block;
+    flex: auto;
+  }
+
+  #progress div {
+    --help-bubble-progress-size: 8px;
+    border: 1px solid var(--help-bubble-text-color);
+    border-radius: 50%;
+    display: inline-block;
+    height: var(--help-bubble-progress-size);
+    margin-bottom: var(--help-bubble-element-spacing);
+    margin-inline-end: var(--help-bubble-element-spacing);
+    margin-top: 4px;
+    width: var(--help-bubble-progress-size);
+  }
+
+  .total-progress {
+    background-color: var(--help-bubble-text-color);
+  }
+
+  div.body {
     flex: auto;
     font-size: 14px;
   }
 
-  #title {
+  div.title {
     flex: auto;
     font-size: 16px;
     font-weight: bold;
     margin-bottom: 8px;
   }
 
+  div.hidden {
+    display: none;
+  }
+
   /* Note: help bubbles have the same color treatment in both light and dark
    * themes, which is why the values below do not change based on theme
    * preference. */
 
    .help-bubble {
     --help-bubble-background: var(--google-blue-700);
+    --help-bubble-element-spacing: 8px;
     --help-bubble-text-color: var(--google-grey-200);
     background-color: var(--help-bubble-background);
     border-radius: 8px;
@@ -103,8 +151,8 @@
   cr-button {
     --text-color: var(--help-bubble-text-color);
     border-color: var(--help-bubble-text-color);
-    margin-inline-start: 8px;
-    margin-top: 8px;
+    margin-inline-start: var(--help-bubble-element-spacing);
+    margin-top: var(--help-bubble-element-spacing);
   }
 
   cr-button:focus {
@@ -127,12 +175,39 @@
 <div class="help-bubble">
   <div id="topContainer">
     <cr-icon-button id="close" iron-icon="cr:close"
-        aria-label="[[closeText]]" on-click="dismiss_">
+        aria-label$="[[closeText]]" on-click="dismiss_">
     </cr-icon-button>
-    <div id="title"></div>
-    <div id="body"></div>
+    <div id="progress" hidden$="[[!progress]]">
+      <template is="dom-repeat" items="[[progressData_]]">
+        <div class$="[[getProgressClass_(index)]]"></div>
+      </template>
+    </div>
+    <div class="title"
+         hidden$="[[!shouldShowTitleInTopContainer_(progress, titleText)]]">
+      [[titleText]]
+    </div>
+    <div class="body"
+         hidden$="[[!shouldShowBodyInTopContainer_(progress, titleText)]]">
+      [[bodyText]]
+    </div>
   </div>
-  <div id="main"></div>
-  <div id="buttons"></div>
-  <div class="arrow"></div>
+  <div id="main">
+    <div class="title"
+         hidden$="[[!shouldShowTitleInMain_(progress, titleText)]]">
+      [[titleText]]
+    </div>
+    <div class="body"
+         hidden$="[[!shouldShowBodyInMain_(progress, titleText)]]">
+      [[bodyText]]
+    </div>
+  </div>
+  <div id="buttons" hidden$="[[!buttons.length]]">
+    <template is="dom-repeat" items="[[buttons]]" sort="buttonSortFunc_">
+      <cr-button id$="[[getButtonId_(itemsIndex)]]"
+          tabindex$="[[getButtonTabIndex_(itemsIndex, item.isDefault)]]"
+          class$="[[getButtonClass_(item.isDefault)]]"
+          on-click="onButtonClick_">[[item.text]]</cr-button>
+    </template>
+  </div>
+  <div id="arrow" class$="[[getArrowClass_(position)]]"></div>
 </div>
diff --git a/ui/webui/resources/cr_components/help_bubble/help_bubble.mojom b/ui/webui/resources/cr_components/help_bubble/help_bubble.mojom
index c749af0..40c2f18 100644
--- a/ui/webui/resources/cr_components/help_bubble/help_bubble.mojom
+++ b/ui/webui/resources/cr_components/help_bubble/help_bubble.mojom
@@ -28,6 +28,12 @@
   bool is_default = false;
 };
 
+// Progress indicator for tutorial bubbles.
+struct Progress {
+  uint8 current;
+  uint8 total;
+};
+
 // Simplified version of user_education::HelpBubbleParams.
 struct HelpBubbleParams {
   // Holds the name of the ElementIdentifier used to identify the help bubble's
@@ -39,6 +45,7 @@
   string? title_text;
   string body_text;
   string close_button_alt_text;
+  Progress? progress;
   array<HelpBubbleButtonParams> buttons;
 };
 
diff --git a/ui/webui/resources/cr_components/help_bubble/help_bubble.ts b/ui/webui/resources/cr_components/help_bubble/help_bubble.ts
index bc71291..b22443d 100644
--- a/ui/webui/resources/cr_components/help_bubble/help_bubble.ts
+++ b/ui/webui/resources/cr_components/help_bubble/help_bubble.ts
@@ -10,17 +10,17 @@
  */
 import 'chrome://resources/cr_elements/cr_button/cr_button.m.js';
 import 'chrome://resources/cr_elements/cr_icon_button/cr_icon_button.m.js';
+import 'chrome://resources/cr_elements/hidden_style_css.m.js';
 import 'chrome://resources/cr_elements/icons.m.js';
 
 import {CrButtonElement} from '//resources/cr_elements/cr_button/cr_button.m.js';
 import {CrIconButtonElement} from '//resources/cr_elements/cr_icon_button/cr_icon_button.m.js';
 import {assert, assertNotReached} from '//resources/js/assert_ts.js';
 import {isWindows} from '//resources/js/cr.m.js';
-import {PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
-import {EventTracker} from 'chrome://resources/js/event_tracker.m.js';
+import {DomRepeatEvent, PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 
 import {getTemplate} from './help_bubble.html.js';
-import {HelpBubblePosition} from './help_bubble.mojom-webui.js';
+import {HelpBubbleButtonParams, HelpBubblePosition, Progress} from './help_bubble.mojom-webui.js';
 
 const ANCHOR_HIGHLIGHT_CLASS = 'help-anchor-highlight';
 
@@ -36,11 +36,11 @@
 
 export interface HelpBubbleElement {
   $: {
-    body: HTMLElement,
+    arrow: HTMLElement,
     buttons: HTMLElement,
     close: CrIconButtonElement,
     main: HTMLElement,
-    title: HTMLElement,
+    progress: HTMLElement,
     topContainer: HTMLElement,
   };
 }
@@ -61,7 +61,14 @@
         value: '',
         reflectToAttribute: true,
       },
+
       closeText: String,
+
+      position: {
+        type: HelpBubblePosition,
+        value: HelpBubblePosition.BELOW,
+        reflectToAttribute: true,
+      },
     };
   }
 
@@ -70,33 +77,30 @@
   titleText: string;
   closeText: string;
   position: HelpBubblePosition;
-  buttons: string[] = [];
-  defaultButtonIndex: number;
+  buttons: HelpBubbleButtonParams[] = [];
+  progress: Progress|null = null;
 
   /**
    * HTMLElement corresponding to |this.anchorId|.
    */
   private anchorElement_: HTMLElement|null = null;
 
-  private buttonEventTracker_: EventTracker = new EventTracker();
+  /**
+   * Backing data for the dom-repeat that generates progress indicators.
+   * The elements are placeholders only.
+   */
+  private progressData_: void[] = [];
 
   /**
    * Shows the bubble.
    */
   show() {
-    // If there is no title, the body element should be in the top container
-    // with the close button, else it should be in the main container.
-    if (this.titleText) {
-      this.$.title.style.display = 'block';
-      this.$.title.innerText = this.titleText;
-      this.$.main.appendChild(this.$.body);
+    // Set up the progress track.
+    if (this.progress) {
+      this.progressData_ = new Array(this.progress.total);
     } else {
-      this.$.title.style.display = 'none';
-      this.$.topContainer.appendChild(this.$.body);
+      this.progressData_ = [];
     }
-    this.$.body.innerText = this.bodyText;
-
-    this.addButtons_();
 
     this.anchorElement_ =
         this.parentElement!.querySelector<HTMLElement>(`#${this.anchorId}`)!;
@@ -122,7 +126,6 @@
    * bubble will go away on hide.
    */
   hide() {
-    this.removeButtons_();
     this.style.display = 'none';
     this.setAttribute('aria-hidden', 'true');
     this.setAnchorHighlight_(false);
@@ -141,12 +144,8 @@
    * Returns the button with the given `buttonIndex`, or null if not found.
    */
   getButtonForTesting(buttonIndex: number): CrButtonElement|null {
-    for (const button of this.$.buttons.children) {
-      if (button.id === ACTION_BUTTON_ID_PREFIX + buttonIndex) {
-        return button as CrButtonElement;
-      }
-    }
-    return null;
+    return this.$.buttons.querySelector<CrButtonElement>(
+        `[id="${ACTION_BUTTON_ID_PREFIX + buttonIndex}"]`);
   }
 
   /**
@@ -167,71 +166,85 @@
     }));
   }
 
-  private onButtonClicked_(buttonIndex: number, _e: Event) {
+  private getProgressClass_(index: number): string {
+    return index < this.progress!.current ? 'current-progress' :
+                                            'total-progress';
+  }
+
+  private shouldShowTitleInTopContainer_(
+      progress: Progress|null, titleText: string): boolean {
+    return !!titleText && !progress;
+  }
+
+  private shouldShowTitleInMain_(progress: Progress|null, titleText: string):
+      boolean {
+    return !!titleText && !!progress;
+  }
+
+  private shouldShowBodyInTopContainer_(
+      progress: Progress|null, titleText: string): boolean {
+    return !progress && !titleText;
+  }
+
+  private shouldShowBodyInMain_(progress: Progress|null, titleText: string):
+      boolean {
+    return !!progress || !!titleText;
+  }
+
+  private onButtonClick_(e: DomRepeatEvent<HelpBubbleButtonParams>) {
     assert(
         this.anchorId,
         'Action button clicked: expected help bubble to have an anchor.');
+    // There is no access to the model index here due to limitations of
+    // dom-repeat. However, the index is stored in the node's identifier.
+    const index: number = parseInt(
+        (e.target as Element).id.substring(ACTION_BUTTON_ID_PREFIX.length));
     this.dispatchEvent(new CustomEvent(HELP_BUBBLE_DISMISSED_EVENT, {
       detail: {
         anchorId: this.anchorId,
         fromActionButton: true,
-        buttonIndex: buttonIndex,
+        buttonIndex: index,
       },
     }));
   }
 
-  /**
-   * Removes button elements and listeners, if any are present.
-   */
-  private removeButtons_() {
-    while (this.$.buttons.firstChild) {
-      this.buttonEventTracker_.remove(this.$.buttons.firstChild, 'click');
-      this.$.buttons.removeChild(this.$.buttons.firstChild);
-    }
+  private getButtonId_(index: number): string {
+    return ACTION_BUTTON_ID_PREFIX + index;
   }
 
-  /**
-   * Adds any buttons required by `this.buttons` with their on-click listeners.
-   */
-  private addButtons_() {
-    assert(
-        !this.$.buttons.firstChild,
-        'Add buttons: expected button list to be empty.');
+  private getButtonClass_(isDefault: boolean): string {
+    return isDefault ? 'default-button' : '';
+  }
 
-    // If there are no buttons to add, hide the container and return.
-    if (!this.buttons.length) {
-      return;
+  private getButtonTabIndex_(index: number, isDefault: boolean): number {
+    return isDefault ? 1 : index + 2;
+  }
+
+  private buttonSortFunc_(
+      button1: HelpBubbleButtonParams,
+      button2: HelpBubbleButtonParams): number {
+    // Default button is leading on Windows, trailing on other platforms.
+    if (button1.isDefault) {
+      return isWindows ? -1 : 1;
     }
-
-    let defaultButton: HTMLElement|null = null;
-    for (let i: number = 0; i < this.buttons.length; ++i) {
-      const button = document.createElement('cr-button');
-      button.innerText = this.buttons[i];
-      button.id = ACTION_BUTTON_ID_PREFIX + i;
-      this.buttonEventTracker_.add(
-          button, 'click', this.onButtonClicked_.bind(this, i));
-      if (i === this.defaultButtonIndex) {
-        defaultButton = button;
-        // Default button should always be first in tab order.
-        button.tabIndex = 1;
-        button.classList.add('default-button');
-      } else {
-        // Tab index for non-default buttons starts at 2, since default button
-        // gets 1.
-        button.tabIndex = i + 2;
-        this.$.buttons.appendChild(button);
-      }
+    if (button2.isDefault) {
+      return isWindows ? 1 : -1;
     }
+    return 0;
+  }
 
-    // Place the default button in the correct order; either leading or
-    // trailing based on platform.
-    if (defaultButton) {
-      if (HelpBubbleElement.isDefaultButtonLeading() &&
-          this.$.buttons.firstChild) {
-        this.$.buttons.insertBefore(defaultButton, this.$.buttons.firstChild);
-      } else {
-        this.$.buttons.appendChild(defaultButton);
-      }
+  private getArrowClass_(position: HelpBubblePosition): string {
+    switch (position) {
+      case HelpBubblePosition.ABOVE:
+        return 'above';
+      case HelpBubblePosition.BELOW:
+        return 'below';
+      case HelpBubblePosition.LEFT:
+        return 'left';
+      case HelpBubblePosition.RIGHT:
+        return 'right';
+      default:
+        assertNotReached('Unknown help bubble position: ' + position);
     }
   }
 
@@ -244,44 +257,44 @@
         this.anchorElement_, 'Update position: expected valid anchor element.');
 
     // Inclusive of 8px visible arrow and 8px margin.
-    const offset = 16;
     const parentRect = this.offsetParent!.getBoundingClientRect();
     const anchorRect = this.anchorElement_.getBoundingClientRect();
     const anchorLeft = anchorRect.left - parentRect.left;
+    const anchorHorizontalCenter = anchorLeft + anchorRect.width / 2;
     const anchorTop = anchorRect.top - parentRect.top;
+    const ARROW_OFFSET = 16;
+    const LEFT_MARGIN = 8;
+    const HELP_BUBBLE_WIDTH = 362;
 
     let helpLeft: string = '';
     let helpTop: string = '';
-    let helpTransform: string = '';
 
     switch (this.position) {
       case HelpBubblePosition.ABOVE:
         // Anchor the help bubble to the top center of the anchor element.
-        helpTop = `${anchorTop - offset}px`;
-        helpLeft = `${anchorLeft + anchorRect.width / 2}px`;
-        // Horizontally center the help bubble.
-        helpTransform = 'translate(-50%, -100%)';
+        helpTop = `${anchorTop - ARROW_OFFSET}px`;
+        helpLeft = `${
+            Math.max(
+                LEFT_MARGIN,
+                anchorHorizontalCenter - HELP_BUBBLE_WIDTH / 2)}px`;
         break;
       case HelpBubblePosition.BELOW:
         // Anchor the help bubble to the bottom center of the anchor element.
-        helpTop = `${anchorTop + anchorRect.height + offset}px`;
-        helpLeft = `${anchorLeft + anchorRect.width / 2}px`;
-        // Horizontally center the help bubble.
-        helpTransform = 'translateX(-50%)';
+        helpTop = `${anchorTop + anchorRect.height + ARROW_OFFSET}px`;
+        helpLeft = `${
+            Math.max(
+                LEFT_MARGIN,
+                anchorHorizontalCenter - HELP_BUBBLE_WIDTH / 2)}px`;
         break;
       case HelpBubblePosition.LEFT:
         // Anchor the help bubble to the center left of the anchor element.
         helpTop = `${anchorTop + anchorRect.height / 2}px`;
-        helpLeft = `${anchorLeft - offset}px`;
-        // Vertically center the help bubble.
-        helpTransform = 'translate(-100%, -50%)';
+        helpLeft = `${anchorLeft - ARROW_OFFSET}px`;
         break;
       case HelpBubblePosition.RIGHT:
         // Anchor the help bubble to the center right of the anchor element.
         helpTop = `${anchorTop + anchorRect.height / 2}px`;
-        helpLeft = `${anchorLeft + anchorRect.width + offset}px`;
-        // Vertically center the help bubble.
-        helpTransform = 'translateY(-50%)';
+        helpLeft = `${anchorLeft + anchorRect.width + ARROW_OFFSET}px`;
         break;
       default:
         assertNotReached();
@@ -289,7 +302,6 @@
 
     this.style.top = helpTop;
     this.style.left = helpLeft;
-    this.style.transform = helpTransform;
   }
 
   /**
diff --git a/ui/webui/resources/cr_components/help_bubble/help_bubble_mixin.ts b/ui/webui/resources/cr_components/help_bubble/help_bubble_mixin.ts
index eae87cea..9aa24dfe1 100644
--- a/ui/webui/resources/cr_components/help_bubble/help_bubble_mixin.ts
+++ b/ui/webui/resources/cr_components/help_bubble/help_bubble_mixin.ts
@@ -170,14 +170,11 @@
           bubble.position = params.position;
           bubble.bodyText = params.bodyText;
           bubble.titleText = params.titleText || '';
-          bubble.buttons = [];
-          bubble.defaultButtonIndex = -1;
-          for (let i: number = 0; i < params.buttons.length; ++i) {
-            bubble.buttons.push(params.buttons[i].text);
-            if (params.buttons[i].isDefault) {
-              bubble.defaultButtonIndex = i;
-            }
-          }
+          bubble.progress = params.progress || null;
+          assert(
+              !bubble.progress ||
+              bubble.progress.total >= bubble.progress.current);
+          bubble.buttons = params.buttons;
           bubble.show();
           anchor!.focus();
         }
diff --git a/ui/webui/resources/cr_elements/BUILD.gn b/ui/webui/resources/cr_elements/BUILD.gn
index a4e6150..d23e67e 100644
--- a/ui/webui/resources/cr_elements/BUILD.gn
+++ b/ui/webui/resources/cr_elements/BUILD.gn
@@ -193,7 +193,6 @@
       "cr_input:closure_compile",
       "cr_lottie:closure_compile",
       "cr_radio_button:closure_compile",
-      "cr_radio_group:closure_compile",
       "cr_toggle:closure_compile",
       "policy:closure_compile",
 
@@ -218,6 +217,7 @@
       deps += [
         "cr_button:closure_compile",
         "cr_expand_button:closure_compile",
+        "cr_radio_group:closure_compile",
       ]
     }
   }
diff --git a/ui/webui/resources/cr_elements/cr_radio_group/BUILD.gn b/ui/webui/resources/cr_elements/cr_radio_group/BUILD.gn
index b1805b2..6b44fc4 100644
--- a/ui/webui/resources/cr_elements/cr_radio_group/BUILD.gn
+++ b/ui/webui/resources/cr_elements/cr_radio_group/BUILD.gn
@@ -2,19 +2,22 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+import("//build/config/chromeos/ui_mode.gni")
 import("//third_party/closure_compiler/compile_js.gni")
 import("//tools/polymer/polymer.gni")
 
-js_type_check("closure_compile") {
-  uses_legacy_modules = true
-  deps = [ ":cr_radio_group" ]
-}
+if (is_chromeos_ash) {
+  js_type_check("closure_compile") {
+    uses_legacy_modules = true
+    deps = [ ":cr_radio_group" ]
+  }
 
-js_library("cr_radio_group") {
-  deps = [
-    "//ui/webui/resources/cr_elements/cr_radio_button:cr_radio_button",
-    "//ui/webui/resources/js:event_tracker",
-  ]
+  js_library("cr_radio_group") {
+    deps = [
+      "//ui/webui/resources/cr_elements/cr_radio_button:cr_radio_button",
+      "//ui/webui/resources/js:event_tracker",
+    ]
+  }
 }
 
 # Targets for auto-generating and typechecking Polymer 3 JS modules
diff --git a/ui/webui/resources/js/BUILD.gn b/ui/webui/resources/js/BUILD.gn
index da422c5..233d69a1 100644
--- a/ui/webui/resources/js/BUILD.gn
+++ b/ui/webui/resources/js/BUILD.gn
@@ -164,31 +164,34 @@
 js_type_check("js_resources") {
   uses_legacy_modules = true
   deps = [
-    ":action_link",
     ":assert",
     ":cr",
-    ":event_tracker",
-    ":i18n_template_no_process",
     ":load_time_data",
-    ":parse_html_subset",
     ":promise_resolver",
     ":util",
     ":webui_resource_test",
   ]
+
   if (is_ios) {
     deps += [ "ios:web_ui" ]
   }
 
   if (is_chromeos_ash) {
     deps += [
+      ":action_link",
+      ":event_tracker",
       ":i18n_behavior",
+      ":i18n_template_no_process",
       ":list_property_update_behavior",
+      ":parse_html_subset",
       ":web_ui_listener_behavior",
     ]
   }
 }
 
-js_library("action_link") {
+if (is_chromeos_ash) {
+  js_library("action_link") {
+  }
 }
 
 js_library("assert") {
@@ -202,15 +205,14 @@
   externs_list = [ "$externs_path/chrome_send.js" ]
 }
 
-js_library("event_tracker") {
-  deps = [ ":cr" ]
-}
-
-js_library("i18n_template_no_process") {
-  deps = [ ":load_time_data" ]
-}
-
 if (is_chromeos_ash) {
+  js_library("event_tracker") {
+    deps = [ ":cr" ]
+  }
+
+  js_library("i18n_template_no_process") {
+    deps = [ ":load_time_data" ]
+  }
   js_library("i18n_behavior") {
     deps = [
       ":load_time_data",
@@ -227,7 +229,9 @@
   extra_deps = [ ":unmodulize" ]
 }
 
-js_library("parse_html_subset") {
+if (is_chromeos_ash) {
+  js_library("parse_html_subset") {
+  }
 }
 
 js_library("promise_resolver") {