diff --git a/AUTHORS b/AUTHORS
index fa1e9e1..8f704e3 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -421,6 +421,7 @@
 Jihwan Marc Kim <bluewhale.marc@gmail.com>
 Jin Yang <jin.a.yang@intel.com>
 Jincheol Jo <jincheol.jo@navercorp.com>
+Jing Zhao <zhaojing7@xiaomi.com>
 Jingwei Liu <kingweiliu@gmail.com>
 Jingyi Wei <wjywbs@gmail.com>
 Jinho Bang <jinho.bang@samsung.com>
diff --git a/DEPS b/DEPS
index 728f77f..c6a8388 100644
--- a/DEPS
+++ b/DEPS
@@ -133,11 +133,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': 'c6568afa0b634d47596557960ccf756962b9aab3',
+  'skia_revision': 'e5b3e400b6e960814e55cd0e737dce14cb5c199a',
   # 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': '09be7f2ffcd2aab7221a97235793c8e8ea7a2293',
+  'v8_revision': '92b8322b89a02a88c867f43e7b6770c779ff6d62',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling swarming_client
   # and whatever else without interference from each other.
@@ -145,11 +145,11 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling ANGLE
   # and whatever else without interference from each other.
-  'angle_revision': '06de90c66c18f0cb32980606925f39ea63c29b6c',
+  'angle_revision': 'cafd7736490e2cfeca38397301853e76dfce91c4',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling SwiftShader
   # and whatever else without interference from each other.
-  'swiftshader_revision': 'ab1e2b49fe88bc2067a9e0961bc20a550facdcb7',
+  'swiftshader_revision': '0559bc475569285c4499c46af575b3d8ef19b346',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling PDFium
   # and whatever else without interference from each other.
@@ -196,7 +196,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': 'fd64d5d2d4a77c909aa24986a25b43f685d4504b',
+  'catapult_revision': 'b1d937f4218ece4f194f35e0d2475f51ce4493c7',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling libFuzzer
   # and whatever else without interference from each other.
@@ -272,7 +272,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.
-  'quiche_revision': '601f67857b99302f77270911ef9610be18e0c577',
+  'quiche_revision': '63d0bc4ae8dcc67502f30c320cdc074c44ea4d89',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling ios_webkit
   # and whatever else without interference from each other.
@@ -802,7 +802,7 @@
 
   # Build tools for Chrome OS. Note: This depends on third_party/pyelftools.
   'src/third_party/chromite': {
-      'url': Var('chromium_git') + '/chromiumos/chromite.git' + '@' + '867bf9d35a9f22f928777573b4ebbe50b7d37845',
+      'url': Var('chromium_git') + '/chromiumos/chromite.git' + '@' + '23e25e0bb4dc715701aa4477e66ce908649999e6',
       'condition': 'checkout_linux',
   },
 
@@ -817,7 +817,7 @@
 
   # For Linux and Chromium OS.
   'src/third_party/cros_system_api': {
-      'url': Var('chromium_git') + '/chromiumos/platform2/system_api.git' + '@' + '8ee930c15e1af5426105115969b0e568ce606e57',
+      'url': Var('chromium_git') + '/chromiumos/platform2/system_api.git' + '@' + '48dde549792d89486d7d92ce033bfac0e594b7e7',
       'condition': 'checkout_linux',
   },
 
@@ -827,7 +827,7 @@
   },
 
   'src/third_party/depot_tools':
-    Var('chromium_git') + '/chromium/tools/depot_tools.git' + '@' + '5737f025b55d6ee2b4469d244f77f275a5e9f1a2',
+    Var('chromium_git') + '/chromium/tools/depot_tools.git' + '@' + '0e405d1ac6adc90fcb7082270363d9e11758e28c',
 
   'src/third_party/devtools-node-modules':
     Var('chromium_git') + '/external/github.com/ChromeDevTools/devtools-node-modules' + '@' + Var('devtools_node_modules_revision'),
@@ -1177,7 +1177,7 @@
   },
 
   'src/third_party/perfetto':
-    Var('android_git') + '/platform/external/perfetto.git' + '@' +  '7cac1ef36965995c9e4c4bc5f924934548fd1ecb',
+    Var('android_git') + '/platform/external/perfetto.git' + '@' +  'b0f423a33189049776b359044a863b837bcc4d8f',
 
   'src/third_party/perl': {
       'url': Var('chromium_git') + '/chromium/deps/perl.git' + '@' + 'ac0d98b5cee6c024b0cffeb4f8f45b6fc5ccdb78',
@@ -1345,7 +1345,7 @@
     Var('chromium_git') + '/external/khronosgroup/webgl.git' + '@' + '6f0b34abee8dba611c253738d955c59f703c147a',
 
   'src/third_party/webrtc':
-    Var('webrtc_git') + '/src.git' + '@' + 'fe57f6252fbd4d56fffbeef72dc677b11540af9e',
+    Var('webrtc_git') + '/src.git' + '@' + '519d74a5fcd869478f2bd8cc3a38eca7dfb608e6',
 
   'src/third_party/xdg-utils': {
       'url': Var('chromium_git') + '/chromium/deps/xdg-utils.git' + '@' + 'd80274d5869b17b8c9067a1022e4416ee7ed5e0d',
@@ -1386,7 +1386,7 @@
     Var('chromium_git') + '/v8/v8.git' + '@' +  Var('v8_revision'),
 
   'src-internal': {
-    'url': 'https://chrome-internal.googlesource.com/chrome/src-internal.git@e8401c6b878f4d30cea4a57e3e749fed8ab60a89',
+    'url': 'https://chrome-internal.googlesource.com/chrome/src-internal.git@c05031d35df81197b8d4c21138acb045b3de88fe',
     'condition': 'checkout_src_internal',
   },
 
diff --git a/android_webview/BUILD.gn b/android_webview/BUILD.gn
index 26705bf..ca90423 100644
--- a/android_webview/BUILD.gn
+++ b/android_webview/BUILD.gn
@@ -843,6 +843,7 @@
     "java/src/org/chromium/android_webview/AwFirebaseConfig.java",
     "java/src/org/chromium/android_webview/AwFormDatabase.java",
     "java/src/org/chromium/android_webview/AwGeolocationPermissions.java",
+    "java/src/org/chromium/android_webview/AwHistogramRecorder.java",
     "java/src/org/chromium/android_webview/AwHttpAuthHandler.java",
     "java/src/org/chromium/android_webview/AwLayoutSizer.java",
     "java/src/org/chromium/android_webview/AwMetricsLogUploader.java",
diff --git a/android_webview/browser/gfx/aw_gl_surface.cc b/android_webview/browser/gfx/aw_gl_surface.cc
index be75655..d64b2ba 100644
--- a/android_webview/browser/gfx/aw_gl_surface.cc
+++ b/android_webview/browser/gfx/aw_gl_surface.cc
@@ -31,10 +31,6 @@
   return gfx::SwapResult::SWAP_ACK;
 }
 
-bool AwGLSurface::SupportsPresentationCallback() {
-  return true;
-}
-
 gfx::Size AwGLSurface::GetSize() {
   return size_;
 }
diff --git a/android_webview/browser/gfx/aw_gl_surface.h b/android_webview/browser/gfx/aw_gl_surface.h
index bf19955..142e678 100644
--- a/android_webview/browser/gfx/aw_gl_surface.h
+++ b/android_webview/browser/gfx/aw_gl_surface.h
@@ -22,7 +22,6 @@
   bool IsOffscreen() override;
   unsigned int GetBackingFramebufferObject() override;
   gfx::SwapResult SwapBuffers(PresentationCallback callback) override;
-  bool SupportsPresentationCallback() override;
   gfx::Size GetSize() override;
   void* GetHandle() override;
   void* GetDisplay() override;
diff --git a/android_webview/java/src/org/chromium/android_webview/AwContents.java b/android_webview/java/src/org/chromium/android_webview/AwContents.java
index d00d5f84..ee15998 100644
--- a/android_webview/java/src/org/chromium/android_webview/AwContents.java
+++ b/android_webview/java/src/org/chromium/android_webview/AwContents.java
@@ -3074,6 +3074,9 @@
     @CalledByNative
     private void onReceivedHttpAuthRequest(AwHttpAuthHandler handler, String host, String realm) {
         mContentsClient.onReceivedHttpAuthRequest(handler, host, realm);
+
+        AwHistogramRecorder.recordCallbackInvocation(
+                AwHistogramRecorder.WebViewCallbackType.ON_RECEIVED_HTTP_AUTH_REQUEST);
     }
 
     public AwGeolocationPermissions getGeolocationPermissions() {
diff --git a/android_webview/java/src/org/chromium/android_webview/AwContentsClientBridge.java b/android_webview/java/src/org/chromium/android_webview/AwContentsClientBridge.java
index 9f395a6..b134a9d 100644
--- a/android_webview/java/src/org/chromium/android_webview/AwContentsClientBridge.java
+++ b/android_webview/java/src/org/chromium/android_webview/AwContentsClientBridge.java
@@ -212,6 +212,10 @@
         final ClientCertificateRequestCallback callback =
                 new ClientCertificateRequestCallback(id, host, port);
         mClient.onReceivedClientCertRequest(callback, keyTypes, principals, host, port);
+
+        // Record UMA for onReceivedClientCertRequest.
+        AwHistogramRecorder.recordCallbackInvocation(
+                AwHistogramRecorder.WebViewCallbackType.ON_RECEIVED_CLIENT_CERT_REQUEST);
     }
 
     @CalledByNative
@@ -268,11 +272,19 @@
             String mimeType, long contentLength) {
         mClient.getCallbackHelper().postOnDownloadStart(
                 url, userAgent, contentDisposition, mimeType, contentLength);
+
+        // Record UMA for onDownloadStart.
+        AwHistogramRecorder.recordCallbackInvocation(
+                AwHistogramRecorder.WebViewCallbackType.ON_DOWNLOAD_START);
     }
 
     @CalledByNative
     private void newLoginRequest(String realm, String account, String args) {
         mClient.getCallbackHelper().postOnReceivedLoginRequest(realm, account, args);
+
+        // Record UMA for onReceivedLoginRequest.
+        AwHistogramRecorder.recordCallbackInvocation(
+                AwHistogramRecorder.WebViewCallbackType.ON_RECEIVED_LOGIN_REQUEST);
     }
 
     @CalledByNative
diff --git a/android_webview/java/src/org/chromium/android_webview/AwHistogramRecorder.java b/android_webview/java/src/org/chromium/android_webview/AwHistogramRecorder.java
new file mode 100644
index 0000000..17b44fc5
--- /dev/null
+++ b/android_webview/java/src/org/chromium/android_webview/AwHistogramRecorder.java
@@ -0,0 +1,40 @@
+// 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.
+
+package org.chromium.android_webview;
+
+import android.support.annotation.IntDef;
+
+import org.chromium.base.metrics.RecordHistogram;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Collect information about callbacks in Android WebView.
+ */
+public class AwHistogramRecorder {
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({WebViewCallbackType.ON_RECEIVED_LOGIN_REQUEST,
+            WebViewCallbackType.ON_RECEIVED_CLIENT_CERT_REQUEST,
+            WebViewCallbackType.ON_RECEIVED_HTTP_AUTH_REQUEST,
+            WebViewCallbackType.ON_DOWNLOAD_START})
+    @interface WebViewCallbackType {
+        // These values are used for UMA. Don't reuse or reorder values.
+        // If you add something, update NUM_ENTRIES.
+        int ON_RECEIVED_LOGIN_REQUEST = 0;
+        int ON_RECEIVED_CLIENT_CERT_REQUEST = 1;
+        int ON_RECEIVED_HTTP_AUTH_REQUEST = 2;
+        int ON_DOWNLOAD_START = 3;
+        int NUM_ENTRIES = 4;
+    }
+
+    // not meant to be instantiated
+    private AwHistogramRecorder() {}
+
+    public static void recordCallbackInvocation(@WebViewCallbackType int result) {
+        RecordHistogram.recordEnumeratedHistogram(
+                "Android.WebView.Callback.Counts", result, WebViewCallbackType.NUM_ENTRIES);
+    }
+}
diff --git a/ash/shell_unittest.cc b/ash/shell_unittest.cc
index 33c4b6c..4482b739 100644
--- a/ash/shell_unittest.cc
+++ b/ash/shell_unittest.cc
@@ -580,12 +580,8 @@
 };
 
 TEST_F(ShellTest2, DontCrashWhenWindowDeleted) {
-  // This test explicitly uses aura::Env::GetInstance() rather than
-  // Shell->aura_env() as the Window outlives the Shell. In order for a Window
-  // to outlive Shell the Window must be created outside of Ash, which uses
-  // aura::Env::GetInstance() as the Env.
-  window_ = std::make_unique<aura::Window>(
-      nullptr, aura::client::WINDOW_TYPE_UNKNOWN, aura::Env::GetInstance());
+  window_ = std::make_unique<aura::Window>(nullptr,
+                                           aura::client::WINDOW_TYPE_UNKNOWN);
   window_->Init(ui::LAYER_NOT_DRAWN);
 }
 
diff --git a/ash/system/message_center/arc/arc_notification_view_unittest.cc b/ash/system/message_center/arc/arc_notification_view_unittest.cc
index 780ee97..3354527 100644
--- a/ash/system/message_center/arc/arc_notification_view_unittest.cc
+++ b/ash/system/message_center/arc/arc_notification_view_unittest.cc
@@ -82,8 +82,8 @@
     notification_view_.reset(static_cast<ArcNotificationView*>(
         message_center::MessageViewFactory::Create(*notification)));
     notification_view_->set_owned_by_client();
-    surface_ = std::make_unique<MockArcNotificationSurface>(
-        kDefaultNotificationKey, Shell::Get()->aura_env());
+    surface_ =
+        std::make_unique<MockArcNotificationSurface>(kDefaultNotificationKey);
     notification_view_->content_view_->SetSurface(surface_.get());
     UpdateNotificationViews(*notification);
 
diff --git a/ash/system/message_center/arc/mock_arc_notification_surface.cc b/ash/system/message_center/arc/mock_arc_notification_surface.cc
index 65c7c83..3550ef1 100644
--- a/ash/system/message_center/arc/mock_arc_notification_surface.cc
+++ b/ash/system/message_center/arc/mock_arc_notification_surface.cc
@@ -9,18 +9,16 @@
 namespace ash {
 
 MockArcNotificationSurface::MockArcNotificationSurface(
-    const std::string& notification_key,
-    aura::Env* aura_env)
+    const std::string& notification_key)
     : notification_key_(notification_key),
       ax_tree_id_(ui::AXTreeIDUnknown()),
       native_view_host_(nullptr),
-      window_(std::make_unique<aura::Window>(nullptr,
-                                             aura::client::WINDOW_TYPE_UNKNOWN,
-                                             aura_env)),
+      window_(
+          std::make_unique<aura::Window>(nullptr,
+                                         aura::client::WINDOW_TYPE_UNKNOWN)),
       content_window_(
           std::make_unique<aura::Window>(nullptr,
-                                         aura::client::WINDOW_TYPE_UNKNOWN,
-                                         aura_env)) {
+                                         aura::client::WINDOW_TYPE_UNKNOWN)) {
   window_->Init(ui::LAYER_NOT_DRAWN);
   content_window_->Init(ui::LAYER_NOT_DRAWN);
 }
diff --git a/ash/system/message_center/arc/mock_arc_notification_surface.h b/ash/system/message_center/arc/mock_arc_notification_surface.h
index 7d008af..8c61892 100644
--- a/ash/system/message_center/arc/mock_arc_notification_surface.h
+++ b/ash/system/message_center/arc/mock_arc_notification_surface.h
@@ -7,16 +7,11 @@
 
 #include "ash/system/message_center/arc/arc_notification_surface.h"
 
-namespace aura {
-class Env;
-}
-
 namespace ash {
 
 class MockArcNotificationSurface : public ArcNotificationSurface {
  public:
-  explicit MockArcNotificationSurface(const std::string& notification_key,
-                                      aura::Env* aura_env = nullptr);
+  explicit MockArcNotificationSurface(const std::string& notification_key);
   ~MockArcNotificationSurface() override;
 
   gfx::Size GetSize() const override;
diff --git a/ash/test/ash_test_helper.cc b/ash/test/ash_test_helper.cc
index bdf2394..9baea85e 100644
--- a/ash/test/ash_test_helper.cc
+++ b/ash/test/ash_test_helper.cc
@@ -135,8 +135,6 @@
   aura::test::EnvTestHelper env_helper(Shell::Get()->aura_env());
   env_helper.ResetEnvForTesting();
 
-  aura::test::SetEnvForTestWindows(Shell::Get()->aura_env());
-
   env_helper.SetInputStateLookup(std::unique_ptr<aura::InputStateLookup>());
 
   Shell* shell = Shell::Get();
@@ -185,8 +183,6 @@
   test_keyboard_controller_observer_.reset();
   app_list_test_helper_.reset();
 
-  aura::test::SetEnvForTestWindows(nullptr);
-
   Shell::DeleteInstance();
 
   // Suspend the tear down until all resources are returned via
diff --git a/ash/window_factory.cc b/ash/window_factory.cc
index a276464..b8526cf 100644
--- a/ash/window_factory.cc
+++ b/ash/window_factory.cc
@@ -4,7 +4,6 @@
 
 #include "ash/window_factory.h"
 
-#include "ash/shell.h"
 #include "ui/aura/window.h"
 
 namespace ash {
@@ -12,8 +11,7 @@
 
 std::unique_ptr<aura::Window> NewWindow(aura::WindowDelegate* delegate,
                                         aura::client::WindowType type) {
-  return std::make_unique<aura::Window>(delegate, type,
-                                        Shell::Get()->aura_env());
+  return std::make_unique<aura::Window>(delegate, type);
 }
 
 }  // namespace window_factory
diff --git a/ash/wm/overview/overview_controller.cc b/ash/wm/overview/overview_controller.cc
index f22285b..4936e46 100644
--- a/ash/wm/overview/overview_controller.cc
+++ b/ash/wm/overview/overview_controller.cc
@@ -513,8 +513,7 @@
 
   reset_pauser_task_.Cancel();
   occlusion_tracker_pauser_ =
-      std::make_unique<aura::WindowOcclusionTracker::ScopedPause>(
-          Shell::Get()->aura_env());
+      std::make_unique<aura::WindowOcclusionTracker::ScopedPause>();
 }
 
 void OverviewController::UnpauseOcclusionTracker(int delay) {
diff --git a/ash/wm/tablet_mode/tablet_mode_controller.cc b/ash/wm/tablet_mode/tablet_mode_controller.cc
index fb128d0..8ccb240 100644
--- a/ash/wm/tablet_mode/tablet_mode_controller.cc
+++ b/ash/wm/tablet_mode/tablet_mode_controller.cc
@@ -870,8 +870,7 @@
 void TabletModeController::SuspendOcclusionTracker() {
   occlusion_tracker_reset_timer_.Stop();
   occlusion_tracker_pauser_ =
-      std::make_unique<aura::WindowOcclusionTracker::ScopedPause>(
-          Shell::Get()->aura_env());
+      std::make_unique<aura::WindowOcclusionTracker::ScopedPause>();
   occlusion_tracker_reset_timer_.Start(FROM_HERE, kOcclusionTrackerTimeout,
                                        this,
                                        &TabletModeController::ResetPauser);
diff --git a/base/memory/shared_memory_mapping.h b/base/memory/shared_memory_mapping.h
index d9569af..782b150 100644
--- a/base/memory/shared_memory_mapping.h
+++ b/base/memory/shared_memory_mapping.h
@@ -8,7 +8,6 @@
 #include <cstddef>
 #include <type_traits>
 
-#include "base/containers/buffer_iterator.h"
 #include "base/containers/span.h"
 #include "base/macros.h"
 #include "base/unguessable_token.h"
@@ -146,12 +145,6 @@
     return span<const T>(static_cast<const T*>(raw_memory_ptr()), count);
   }
 
-  // Returns a BufferIterator of const T.
-  template <typename T>
-  BufferIterator<const T> GetMemoryAsBufferIterator() const {
-    return BufferIterator<const T>(GetMemoryAsSpan<T>());
-  }
-
  private:
   friend class ReadOnlySharedMemoryRegion;
   ReadOnlySharedMemoryMapping(void* address,
@@ -223,12 +216,6 @@
     return span<T>(static_cast<T*>(raw_memory_ptr()), count);
   }
 
-  // Returns a BufferIterator of T.
-  template <typename T>
-  BufferIterator<T> GetMemoryAsBufferIterator() {
-    return BufferIterator<T>(GetMemoryAsSpan<T>());
-  }
-
  private:
   friend WritableSharedMemoryMapping MapAtForTesting(
       subtle::PlatformSharedMemoryRegion* region,
diff --git a/build/config/ios/Module-Info.plist b/build/config/ios/Module-Info.plist
index 13b67c4..d1bf77f 100644
--- a/build/config/ios/Module-Info.plist
+++ b/build/config/ios/Module-Info.plist
@@ -20,5 +20,7 @@
   <string>????</string>
   <key>CFBundleVersion</key>
   <string>1</string>
+  <key>NSPrincipalClass</key>
+  <string>${XCTEST_BUNDLE_PRINCIPAL_CLASS}</string>
 </dict>
 </plist>
diff --git a/build/config/ios/rules.gni b/build/config/ios/rules.gni
index 6415e0f..383207d 100644
--- a/build/config/ios/rules.gni
+++ b/build/config/ios/rules.gni
@@ -1660,14 +1660,23 @@
       info_plist = "//build/config/ios/Module-Info.plist"
       executable_name = _output_name
 
+      if (defined(invoker.xctest_bundle_principal_class)) {
+        _principal_class = invoker.xctest_bundle_principal_class
+      } else {
+        # Fall back to a reasonable default value.
+        _principal_class = "NSObject"
+      }
+      extra_substitutions =
+          [ "XCTEST_BUNDLE_PRINCIPAL_CLASS=${_principal_class}" ]
+
       if (ios_automatically_manage_certs) {
         # Use a fixed bundle identifier for EarlGrey tests when using Xcode to
         # manage the certificates as the number of free certs is limited.
-        extra_substitutions = [
+        extra_substitutions += [
           "MODULE_BUNDLE_ID=gtest.${ios_generic_test_bundle_id_suffix}-module",
         ]
       } else {
-        extra_substitutions = [ "MODULE_BUNDLE_ID=gtest.$_output_name" ]
+        extra_substitutions += [ "MODULE_BUNDLE_ID=gtest.$_output_name" ]
       }
     }
 
@@ -2027,7 +2036,11 @@
 
   _xcuitest_module_output = _xcuitest_target
   ios_xctest_bundle(_xcuitest_module_target) {
-    forward_variables_from(invoker, [ "xcode_test_application_name" ])
+    forward_variables_from(invoker,
+                           [
+                             "xcode_test_application_name",
+                             "xctest_bundle_principal_class",
+                           ])
 
     product_type = _ios_xcode_xcuitest_bundle_id
     host_target = _xcuitest_runner_target
diff --git a/build/fuchsia/linux.sdk.sha1 b/build/fuchsia/linux.sdk.sha1
index e80a142..55cd25d 100644
--- a/build/fuchsia/linux.sdk.sha1
+++ b/build/fuchsia/linux.sdk.sha1
@@ -1 +1 @@
-8913272851295197632
\ No newline at end of file
+8913214486540616192
\ No newline at end of file
diff --git a/build/fuchsia/mac.sdk.sha1 b/build/fuchsia/mac.sdk.sha1
index adb5a7d..bdb2ba0 100644
--- a/build/fuchsia/mac.sdk.sha1
+++ b/build/fuchsia/mac.sdk.sha1
@@ -1 +1 @@
-8913299131746549040
\ No newline at end of file
+8913219563458029600
\ No newline at end of file
diff --git a/chrome/android/chrome_java_sources.gni b/chrome/android/chrome_java_sources.gni
index 1f90f33..290a307 100644
--- a/chrome/android/chrome_java_sources.gni
+++ b/chrome/android/chrome_java_sources.gni
@@ -1347,6 +1347,7 @@
   "java/src/org/chromium/chrome/browser/send_tab_to_self/SendTabToSelfAndroidBridge.java",
   "java/src/org/chromium/chrome/browser/send_tab_to_self/SendTabToSelfEntry.java",
   "java/src/org/chromium/chrome/browser/send_tab_to_self/SendTabToSelfInfoBar.java",
+  "java/src/org/chromium/chrome/browser/send_tab_to_self/SendTabToSelfInfoBarController.java",
   "java/src/org/chromium/chrome/browser/send_tab_to_self/SendTabToSelfShareActivity.java",
   "java/src/org/chromium/chrome/browser/services/AccountsChangedReceiver.java",
   "java/src/org/chromium/chrome/browser/services/AndroidEduAndChildAccountHelper.java",
diff --git a/chrome/android/features/autofill_assistant/BUILD.gn b/chrome/android/features/autofill_assistant/BUILD.gn
index dcdd1355..b324f20 100644
--- a/chrome/android/features/autofill_assistant/BUILD.gn
+++ b/chrome/android/features/autofill_assistant/BUILD.gn
@@ -37,9 +37,9 @@
   classpath_deps = [
     "//base:base_java",
     "//chrome/android:chrome_java",
+    "//components/policy/android:policy_java",
     "//components/signin/core/browser/android:java",
     "//components/url_formatter/android:url_formatter_java",
-    "//components/policy/android:policy_java",
     "//content/public/android:content_java",
     "//mojo/public/java:bindings_java",
     "//third_party/android_deps:android_arch_lifecycle_common_java",
@@ -57,11 +57,9 @@
   ]
 
   if (enable_chrome_android_internal) {
-    # TODO(crbug/951489): Change below to 'deps' once crbug/951489 is fixed.
-    classpath_deps +=
-        [ "//clank/features/autofill_assistant:animated_poodle_java" ]
+    deps += [ "//clank/features/autofill_assistant:animated_poodle_java" ]
   } else {
-    classpath_deps += [ ":animated_poodle_java" ]
+    deps += [ ":animated_poodle_java" ]
   }
 
   java_files = [
@@ -157,7 +155,6 @@
   ]
 
   deps = [
-    ":animated_poodle_java",
     ":java",
     "//base:base_java",
     "//base:base_java_test_support",
diff --git a/chrome/android/features/autofill_assistant/autofill_assistant_module_tmpl.gni b/chrome/android/features/autofill_assistant/autofill_assistant_module_tmpl.gni
index 52964d5..be350a2 100644
--- a/chrome/android/features/autofill_assistant/autofill_assistant_module_tmpl.gni
+++ b/chrome/android/features/autofill_assistant/autofill_assistant_module_tmpl.gni
@@ -31,7 +31,6 @@
     package_name = "autofill_assistant"
     package_name_to_id_mapping = resource_packages_id_mapping
     deps = [
-      "//chrome/android/features/autofill_assistant:animated_poodle_java",
       "//chrome/android/features/autofill_assistant:java",
     ]
   }
diff --git a/chrome/android/features/autofill_assistant/java/res/layout/autofill_assistant_form_selection_input.xml b/chrome/android/features/autofill_assistant/java/res/layout/autofill_assistant_form_selection_input.xml
index 22b241f..db97496 100644
--- a/chrome/android/features/autofill_assistant/java/res/layout/autofill_assistant_form_selection_input.xml
+++ b/chrome/android/features/autofill_assistant/java/res/layout/autofill_assistant_form_selection_input.xml
@@ -5,11 +5,24 @@
 
 <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:orientation="vertical">
     <TextView
         android:id="@+id/label"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"/>
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="4dp"
+        android:textAppearance="@style/TextAppearance.BlackTitle2"/>
+
+    <org.chromium.chrome.browser.autofill_assistant.payment.AssistantChoiceList
+        android:id="@+id/choice_list"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        app:can_add_items="false"
+        app:row_spacing="0dp"
+        app:column_spacing="8dp">
+        <!-- Choices are created in code. -->
+    </org.chromium.chrome.browser.autofill_assistant.payment.AssistantChoiceList>
 </LinearLayout>
diff --git a/chrome/android/features/autofill_assistant/java/src/org/chromium/chrome/browser/autofill_assistant/form/AssistantFormSelectionInput.java b/chrome/android/features/autofill_assistant/java/src/org/chromium/chrome/browser/autofill_assistant/form/AssistantFormSelectionInput.java
index 72ed63b..8285484 100644
--- a/chrome/android/features/autofill_assistant/java/src/org/chromium/chrome/browser/autofill_assistant/form/AssistantFormSelectionInput.java
+++ b/chrome/android/features/autofill_assistant/java/src/org/chromium/chrome/browser/autofill_assistant/form/AssistantFormSelectionInput.java
@@ -8,13 +8,12 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.CheckBox;
-import android.widget.CompoundButton;
-import android.widget.LinearLayout;
-import android.widget.RadioButton;
-import android.widget.RadioGroup;
 import android.widget.TextView;
 
+import org.chromium.base.ApiCompatibilityUtils;
+import org.chromium.chrome.autofill_assistant.R;
+import org.chromium.chrome.browser.autofill_assistant.payment.AssistantChoiceList;
+
 import java.util.List;
 
 /** A form input that allows to choose between multiple choices. */
@@ -39,9 +38,8 @@
     @Override
     public View createView(Context context, ViewGroup parent) {
         ViewGroup root = (ViewGroup) LayoutInflater.from(context).inflate(
-                org.chromium.chrome.autofill_assistant.R.layout
-                        .autofill_assistant_form_selection_input,
-                parent, /* attachToRoot= */ false);
+                R.layout.autofill_assistant_form_selection_input, parent,
+                /* attachToRoot= */ false);
         TextView label = root.findViewById(org.chromium.chrome.autofill_assistant.R.id.label);
         if (mLabel.isEmpty()) {
             label.setVisibility(View.GONE);
@@ -49,44 +47,25 @@
             label.setText(mLabel);
         }
 
-        LinearLayout container;
-        if (mAllowMultipleChoices) {
-            container = new LinearLayout(context);
-            for (int i = 0; i < mChoices.size(); i++) {
-                CheckBox checkBox = new CheckBox(context);
-                initChoiceView(container, checkBox, i);
-                checkBox.setChecked(mChoices.get(i).isInitiallySelected());
-            }
-        } else {
-            // If only one choice can be selected, we need the parent to be a RadioGroup to make
-            // sure there is always at most one selected choice.
-            RadioGroup radioGroup = new RadioGroup(context);
-            container = radioGroup;
-            for (int i = 0; i < mChoices.size(); i++) {
-                RadioButton radioButton = new RadioButton(context);
-                initChoiceView(container, radioButton, i);
+        AssistantChoiceList choiceList = root.findViewById(R.id.choice_list);
+        choiceList.setAllowMultipleChoices(mAllowMultipleChoices);
+        for (int i = 0; i < mChoices.size(); i++) {
+            AssistantFormSelectionChoice choice = mChoices.get(i);
 
-                if (mChoices.get(i).isInitiallySelected()) {
-                    // We can't use the CompoundButton#setChecked method for radio buttons as this
-                    // will not update the state of the parent RadioGroup, so we need to use the
-                    // RadioGroup#check method instead.
-                    radioGroup.check(radioButton.getId());
-                }
-            }
+            // Set the same style as Payment Request T&C.
+            TextView choiceView = new TextView(context);
+            ApiCompatibilityUtils.setTextAppearance(
+                    choiceView, org.chromium.chrome.R.style.TextAppearance_BlackCaption);
+            choiceView.setText(choice.getLabel());
+
+            int index = i; // needed for the lambda.
+            choiceList.addItem(choiceView, /* hasEditButton= */ false,
+                    (isChecked)
+                            -> mDelegate.onChoiceSelectionChanged(index, isChecked),
+                    /* itemEditedListener= */ null);
+
+            choiceList.setChecked(choiceView, choice.isInitiallySelected());
         }
-
-        container.setOrientation(LinearLayout.VERTICAL);
-        container.setLayoutParams(new ViewGroup.LayoutParams(
-                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
-        root.addView(container);
         return root;
     }
-
-    private void initChoiceView(LinearLayout container, CompoundButton view, int index) {
-        container.addView(view);
-        AssistantFormSelectionChoice choice = mChoices.get(index);
-        view.setText(choice.getLabel());
-        view.setOnCheckedChangeListener(
-                (unusedView, isChecked) -> mDelegate.onChoiceSelectionChanged(index, isChecked));
-    }
 }
diff --git a/chrome/android/features/autofill_assistant/java/src/org/chromium/chrome/browser/autofill_assistant/payment/AssistantChoiceList.java b/chrome/android/features/autofill_assistant/java/src/org/chromium/chrome/browser/autofill_assistant/payment/AssistantChoiceList.java
index b0886b2..bc9458d 100644
--- a/chrome/android/features/autofill_assistant/java/src/org/chromium/chrome/browser/autofill_assistant/payment/AssistantChoiceList.java
+++ b/chrome/android/features/autofill_assistant/java/src/org/chromium/chrome/browser/autofill_assistant/payment/AssistantChoiceList.java
@@ -12,6 +12,8 @@
 import android.view.Gravity;
 import android.view.View;
 import android.view.ViewGroup;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
 import android.widget.ImageView;
 import android.widget.RadioButton;
 import android.widget.Space;
@@ -46,15 +48,15 @@
      * Represents a single choice with a radio button, customizable content and an edit button.
      */
     private class Item {
-        final RadioButton mRadioButton;
+        final CompoundButton mCompoundButton;
         final Callback<Boolean> mOnSelectedListener;
         final View mContent;
         final View mEditButton;
         final View mSpacer;
 
-        Item(@Nullable RadioButton radioButton, @Nullable Callback onSelectedListener, View content,
-                @Nullable View editButton, @Nullable View spacer) {
-            this.mRadioButton = radioButton;
+        Item(@Nullable CompoundButton compoundButton, @Nullable Callback onSelectedListener,
+                View content, @Nullable View editButton, @Nullable View spacer) {
+            this.mCompoundButton = compoundButton;
             this.mOnSelectedListener = onSelectedListener;
             this.mContent = content;
             this.mEditButton = editButton;
@@ -76,6 +78,7 @@
     private final int mColumnSpacing;
     private final int mAddButtonSpacing;
     private final List<Item> mItems = new ArrayList<>();
+    private boolean mAllowMultipleChoices;
     private Runnable mAddButtonListener;
 
     public AssistantChoiceList(Context context, AttributeSet attrs) {
@@ -116,6 +119,20 @@
     }
 
     /**
+     * Set whether this list allows multiple choices to be selected at the same time. This method
+     * can only be called when no items have been added, otherwise it will throw an exception.
+     */
+    public void setAllowMultipleChoices(boolean allowMultipleChoices) {
+        if (!mItems.isEmpty()) {
+            throw new UnsupportedOperationException(
+                    "Calling #setAllowMultipleChoices is not allowed when items have already been "
+                    + "added.");
+        }
+
+        mAllowMultipleChoices = allowMultipleChoices;
+    }
+
+    /**
      * Children of this container are automatically added as selectable items to the list.
      *
      * This method is automatically called by layout inflaters and xml files. In code, you usually
@@ -148,7 +165,8 @@
     public void addItem(View view, boolean hasEditButton,
             @Nullable Callback<Boolean> itemSelectedListener,
             @Nullable Runnable itemEditedListener) {
-        RadioButton radioButton = new RadioButton(getContext());
+        CompoundButton radioButton =
+                mAllowMultipleChoices ? new CheckBox(getContext()) : new RadioButton(getContext());
         // Insert at end, before the `add' button (if any).
         int viewIndex = mCanAddItems ? indexOfChild(mAddButton) : getChildCount();
         addViewInternal(radioButton, viewIndex++, createRadioButtonLayoutParams());
@@ -172,8 +190,14 @@
         }
 
         Item item = new Item(radioButton, itemSelectedListener, view, editButton, spacer);
-        radioButton.setOnClickListener(unusedView -> { setCheckedItem(view); });
-        view.setOnClickListener(unusedView -> { setCheckedItem(view); });
+
+        // When clicking a checkbox, invert its checked value. A radio button will always be
+        // selected when clicked.
+        View.OnClickListener clickListener = unusedView
+                -> setChecked(
+                        view, mAllowMultipleChoices ? !item.mCompoundButton.isChecked() : true);
+        radioButton.setOnClickListener(clickListener);
+        view.setOnClickListener(clickListener);
         mItems.add(item);
 
         // Need to adjust button margins after first item was inserted.
@@ -189,7 +213,7 @@
         for (int i = 0; i < mItems.size(); i++) {
             Item item = mItems.get(i);
             removeView(item.mContent);
-            removeView(item.mRadioButton);
+            removeView(item.mCompoundButton);
             if (item.mEditButton != null) {
                 removeView(item.mEditButton);
             }
@@ -213,17 +237,46 @@
     }
 
     /**
-     * Selects the specified item and de-selects all other items in the UI.
+     * Selects the specified item. If this choice list does not allow checking multiple choice, this
+     * will also deselect all other items.
      *
      * @param content The content view to select, as specified in |addItem|. Can be null to indicate
      * that all items should be de-selected.
      */
     public void setCheckedItem(@Nullable View content) {
-        for (int i = 0; i < mItems.size(); i++) {
-            Item item = mItems.get(i);
-            item.mRadioButton.setChecked(item.mContent == content);
-            if (item.mOnSelectedListener != null) {
-                item.mOnSelectedListener.onResult(item.mContent == content);
+        if (content == null) {
+            for (Item item : mItems) {
+                item.mCompoundButton.setChecked(false);
+                if (item.mOnSelectedListener != null) {
+                    item.mOnSelectedListener.onResult(false);
+                }
+            }
+            return;
+        }
+
+        setChecked(content, true);
+    }
+
+    /**
+     * Sets whether the specified item is checked or not. If this choice list does not allow
+     * checking multiple choice and {@code checked} is true, this will also deselect all other
+     * items.
+     *
+     * @param content The content view to (un)select, as specified in |addItem|.
+     */
+    public void setChecked(View content, boolean checked) {
+        for (Item item : mItems) {
+            boolean notifyListener = false;
+            if (item.mContent == content) {
+                item.mCompoundButton.setChecked(checked);
+                notifyListener = true;
+            } else if (checked && !mAllowMultipleChoices) {
+                item.mCompoundButton.setChecked(false);
+                notifyListener = true;
+            }
+
+            if (notifyListener && item.mOnSelectedListener != null) {
+                item.mOnSelectedListener.onResult(item.mCompoundButton.isChecked());
             }
         }
     }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/download/DownloadManagerService.java b/chrome/android/java/src/org/chromium/chrome/browser/download/DownloadManagerService.java
index 0ff04e9f..3d4fcf8 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/download/DownloadManagerService.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/download/DownloadManagerService.java
@@ -143,6 +143,9 @@
 
     private final Handler mHandler;
 
+    // The first download that is triggered in background mode.
+    private String mFirstBackgroundDownloadId;
+
     /** Generic interface for notifying external UI components about downloads and their states. */
     public interface DownloadObserver extends DownloadSharedPreferenceHelper.Observer {
         /** Called in response to {@link DownloadManagerService#getAllDownloads(boolean)}. */
@@ -1974,6 +1977,11 @@
         DownloadNotificationUmaHelper.recordBackgroundDownloadHistogram(
                 UmaBackgroundDownload.STARTED);
         sBackgroundDownloadIds.add(downloadGuid);
+        if (mFirstBackgroundDownloadId == null) {
+            mFirstBackgroundDownloadId = downloadGuid;
+            DownloadNotificationUmaHelper.recordFirstBackgroundDownloadHistogram(
+                    UmaBackgroundDownload.STARTED);
+        }
     }
 
     /**
@@ -1986,6 +1994,9 @@
         if (sBackgroundDownloadIds.remove(downloadGuid)) {
             DownloadNotificationUmaHelper.recordBackgroundDownloadHistogram(event);
         }
+        if (downloadGuid.equals(mFirstBackgroundDownloadId)) {
+            DownloadNotificationUmaHelper.recordFirstBackgroundDownloadHistogram(event);
+        }
     }
 
     /**
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/download/DownloadNotificationUmaHelper.java b/chrome/android/java/src/org/chromium/chrome/browser/download/DownloadNotificationUmaHelper.java
index 5444cb3a..e8b91f3 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/download/DownloadNotificationUmaHelper.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/download/DownloadNotificationUmaHelper.java
@@ -214,4 +214,13 @@
         RecordHistogram.recordEnumeratedHistogram(
                 "MobileDownload.Background", type, UmaBackgroundDownload.NUM_ENTRIES);
     }
+
+    /**
+     * Helper method to record the first background download resumption UMA.
+     * @param type UMA type to be recorded.
+     */
+    static void recordFirstBackgroundDownloadHistogram(@UmaBackgroundDownload int type) {
+        RecordHistogram.recordEnumeratedHistogram(
+                "MobileDownload.Background.FirstDownload", type, UmaBackgroundDownload.NUM_ENTRIES);
+    }
 }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/send_tab_to_self/SendTabToSelfAndroidBridge.java b/chrome/android/java/src/org/chromium/chrome/browser/send_tab_to_self/SendTabToSelfAndroidBridge.java
index 4f02c98..da4708c 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/send_tab_to_self/SendTabToSelfAndroidBridge.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/send_tab_to_self/SendTabToSelfAndroidBridge.java
@@ -48,7 +48,9 @@
         allGuids.add(newGuid);
     }
 
-    /** Deletes all SendTabToSelf entries. This is called when the user disables sync. */
+    /**
+     * Deletes all SendTabToSelf entries. This is called when the user disables sync.
+     */
     public static void deleteAllEntries(Profile profile) {
         // TODO(https://crbug.com/942549): Add this assertion back in once the code to load is in
         // place. assert mIsNativeSendTabToSelfModelLoaded;
@@ -117,6 +119,16 @@
         return SendTabToSelfAndroidBridgeJni.get().isFeatureAvailable(webContents);
     }
 
+    /**
+     * Shows an infobar for the webcontents passed in.
+     * @param entry Contains the URL to open when the user taps on the infobar.
+     * @param webContents Where to create the infobar.
+     */
+    public static void showInfoBar(SendTabToSelfEntry entry, WebContents webContents) {
+        SendTabToSelfAndroidBridgeJni.get().showInfoBar(
+                webContents, entry.guid, entry.url, entry.targetDeviceSyncCacheGuid);
+    }
+
     @NativeMethods
     interface Natives {
         SendTabToSelfEntry addEntry(Profile profile, String url, String title, long navigationTime,
@@ -133,5 +145,8 @@
         SendTabToSelfEntry getEntryByGUID(Profile profile, String guid);
 
         boolean isFeatureAvailable(WebContents webContents);
+
+        void showInfoBar(
+                WebContents webContents, String guid, String url, String targetDeviceSyncCacheGuid);
     }
 }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/send_tab_to_self/SendTabToSelfInfoBar.java b/chrome/android/java/src/org/chromium/chrome/browser/send_tab_to_self/SendTabToSelfInfoBar.java
index a42bf43..f216ad1 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/send_tab_to_self/SendTabToSelfInfoBar.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/send_tab_to_self/SendTabToSelfInfoBar.java
@@ -27,9 +27,8 @@
     @Override
     protected void createCompactLayoutContent(InfoBarCompactLayout layout) {
         new InfoBarCompactLayout.MessageBuilder(layout)
-                .withText("Tab shared")
-                // TODO(crbug.com/949233): Add the link in
-                // .withLink(textResId, onTapCallback)
+                .withText(R.string.send_tab_to_self_infobar_message)
+                .withLink(R.string.send_tab_to_self_infobar_message_url, view -> onLinkClicked())
                 .buildAndInsert();
     }
 
@@ -37,4 +36,10 @@
     private static SendTabToSelfInfoBar create() {
         return new SendTabToSelfInfoBar();
     }
+
+    @Override
+    public void onLinkClicked() {
+        // TODO(crbug.com/944602): Add support for opening the link. Figure out
+        // whether the logic should live here or in the delegate.
+    }
 }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/send_tab_to_self/SendTabToSelfInfoBarController.java b/chrome/android/java/src/org/chromium/chrome/browser/send_tab_to_self/SendTabToSelfInfoBarController.java
new file mode 100644
index 0000000..774bc61
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/send_tab_to_self/SendTabToSelfInfoBarController.java
@@ -0,0 +1,62 @@
+// 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.
+
+package org.chromium.chrome.browser.send_tab_to_self;
+
+import org.chromium.chrome.browser.ChromeActivity;
+import org.chromium.chrome.browser.lifecycle.Destroyable;
+import org.chromium.chrome.browser.tab.Tab;
+
+/**
+ * This class is responsible for listening for new SendTabToSelfEntries and showing an infobar
+ * to the user if the user does not have notifications enabled.
+ */
+public class SendTabToSelfInfoBarController implements Destroyable {
+    private ChromeActivity mActivity;
+
+    /**
+     * Creates a SendTabToSelfInfoBarController for the activity passed in.
+     *
+     * TODO(crbug.com/949233):
+     *     - Add observers to listen for SendTabToSelfModel changes. When a new entry is observed,
+     *       check to see if notifications are enabled. If not, display an infobar.
+     *     - If a new entry comes in and the user is not on a tab, listen for a new tab and show
+     *       an infobar then.
+     *     - If an infobar is already being displayed and the user pushes another tab, replace
+     *       the existing infobar with a new one.
+     *
+     * @param activity A {@link ChromeActivity} instance the infobars will be
+     *            shown in.
+     * @return A new instance of {@link SendTabToSelfInfoBarController}.
+     */
+    public SendTabToSelfInfoBarController(ChromeActivity activity) {
+        mActivity = activity;
+        mActivity.getLifecycleDispatcher().register(this);
+    }
+
+    // Destroyable implementation.
+    @Override
+    public void destroy() {
+        mActivity.getLifecycleDispatcher().unregister(this);
+        mActivity = null;
+    }
+
+    /**
+     * Shows an infobar corresponding to the entry passed in if the user is on a tab.
+     * @param entry The entry to display the infobar for.
+     */
+    public void showInfobarForEntry(SendTabToSelfEntry entry) {
+        if (mActivity == null) {
+            return;
+        }
+        Tab tab = mActivity.getActivityTabProvider().get();
+
+        // TODO(crbug.com/949233): Listen for when the user opens a tab next and show
+        // an infobar then.
+        if (tab == null) {
+            return;
+        }
+        SendTabToSelfAndroidBridge.showInfoBar(entry, tab.getWebContents());
+    }
+}
diff --git a/chrome/android/java/strings/android_chrome_strings.grd b/chrome/android/java/strings/android_chrome_strings.grd
index 1870f55e..ba01e30 100644
--- a/chrome/android/java/strings/android_chrome_strings.grd
+++ b/chrome/android/java/strings/android_chrome_strings.grd
@@ -3940,6 +3940,12 @@
       <message name="IDS_SEND_TAB_TO_SELF_NOTIFICATION_CONTEXT_TEXT" desc="Text displayed as the second line of a notification indicating the domain and the device the tab is shared from.">
         <ph name="DOMAIN">%1$s<ex>www.google.com</ex></ph> - Sent from <ph name="DEVICE_NAME">%2$s<ex>Tanya's Pixel 2XL</ex></ph>
       </message>
+      <message name="IDS_SEND_TAB_TO_SELF_INFOBAR_MESSAGE" desc="The message text for the infobar when a user receives a shared tab from another device.">
+        Tab received
+      </message>
+      <message name="IDS_SEND_TAB_TO_SELF_INFOBAR_MESSAGE_URL" desc="Clickable text displayed as part of a URL in the inforbar displayed when a user receives a shared tab from another device.">
+        Open
+      </message>
 
       <!-- Chrome Duet -->
       <message name="IDS_IPH_DUET_TITLE" desc="This string appears in an overlay that explains a new UI to users. 'Search' refers to searching the web, and 'explore' refers to Chrome's suggested content.">
diff --git a/chrome/app/chromeos_strings.grdp b/chrome/app/chromeos_strings.grdp
index 5e84ec1..066ae8f 100644
--- a/chrome/app/chromeos_strings.grdp
+++ b/chrome/app/chromeos_strings.grdp
@@ -1269,6 +1269,12 @@
   <message name="IDS_SIGNIN_ERROR_SECONDARY_ACCOUNT_BUBBLE_VIEW_MESSAGE" desc="Message in the sign-in error bubble view for Chrome OS Secondary Accounts.">
     Your Google Account(s) need attention
   </message>
+  <message name="IDS_SIGNIN_ERROR_SECONDARY_ACCOUNT_MIGRATION_BUBBLE_VIEW_TITLE" desc="Title in the sign-in error bubble view/notification for Chrome OS Secondary Accounts that have not been migrated to Chrome OS Account Manager.">
+    Sign-in has changed
+  </message>
+  <message name="IDS_SIGNIN_ERROR_SECONDARY_ACCOUNT_MIGRATION_BUBBLE_VIEW_MESSAGE" desc="Message in the sign-in error bubble view for Chrome OS Secondary Accounts that have not been migrated to Chrome OS Account Manager.">
+    Account update required
+  </message>
 
   <!-- Strings used in <cr-network-list>, used in Settings and OOBE -->
   <message name="IDS_NETWORK_LIST_INITIALIZING" desc="Text in the network list element when a Cellular network is initiailizing.">
@@ -3867,12 +3873,6 @@
   <message name="IDS_PLUGIN_VM_LAUNCHER_IMPORTING_MESSAGE" desc="Text of the Plugin VM installer while configuring the VM.">
     Configuring the virtual machine. This may take a few minutes.
   </message>
-  <message name="IDS_PLUGIN_VM_LAUNCHER_UNZIPPING_MESSAGE" desc="Text of the Plugin VM installer while configuring the VM." translateable="false">
-    Configuring the virtual machine. This may take a few minutes.
-  </message>
-  <message name="IDS_PLUGIN_VM_LAUNCHER_REGISTERING_MESSAGE" desc="Text of the Plugin VM installer shortly before completion." translateable="false">
-    Putting on final touches...
-  </message>
   <message name="IDS_PLUGIN_VM_LAUNCHER_FINISHED_MESSAGE" desc="Text of the Plugin VM installer after successful installation.">
     Click or tap Launch to use Plugin VM. In the future, you can start Plugin VM by selecting the icon in the Launcher.
   </message>
@@ -3900,11 +3900,24 @@
 
   <!-- Strings for Account Manager welcome screen -->
   <message name="IDS_ACCOUNT_MANAGER_WELCOME_TITLE" desc="Title for the Chrome OS Account Manager Welcome screen.">
-    Manage your Google Accounts in one place
+    Sign-in has changed
   </message>
   <message name="IDS_ACCOUNT_MANAGER_WELCOME_TEXT" desc="Text body for the Chrome OS Account Manager Welcome screen.">
-    Switch accounts quickly and sign in to apps and websites all at once.
-    Apps and sites can ask you for permission to use some of your Google Account info. <ph name="LINK_BEGIN">&lt;a target="_blank" href="$1<ex>https://support.google.com/chromebook/?p=google_accounts</ex>"&gt;</ph>Learn more<ph name="LINK_END">&lt;/a&gt;</ph>
+    You can now manage all of your Google Accounts in one place. Access and permissions you've granted to apps, websites, and extensions in Chrome and Google Play may now apply to all of your signed-in accounts. <ph name="LINK_BEGIN">&lt;a target="_blank" href="$1<ex>https://support.google.com/chromebook/?p=google_accounts</ex>"&gt;</ph>Learn more<ph name="LINK_END">&lt;/a&gt;</ph>
+  </message>
+  <message name="IDS_ACCOUNT_MANAGER_WELCOME_BUTTON" desc="Label for the button to view accounts on the Chrome OS Account Manager Welcome screen.">
+    View accounts
+  </message>
+
+  <!-- Strings for per-account Migration welcome screen -->
+  <message name="IDS_ACCOUNT_MIGRATION_WELCOME_TITLE" desc="Title for the Account Migration Welcome screen.">
+    Sign in again to update <ph name="USER_EMAIL">$1<ex>john.doe@example.com</ex></ph>
+  </message>
+  <message name="IDS_ACCOUNT_MIGRATION_WELCOME_TEXT" desc="Text body for the Account Migration Welcome screen.">
+    Please sign in again to confirm that your account <ph name="USER_EMAIL">$1<ex>john.doe@example.com</ex></ph> can be used with websites, apps, and extensions in Chrome and Google Play. You may also remove this account.
+  </message>
+  <message name="IDS_ACCOUNT_MIGRATION_UPDATE_BUTTON" desc="Label for the button in Account Migration dialog to migrate the account.">
+    Update account
   </message>
 
   <!-- TPM firmware auto-update notifications -->
diff --git a/chrome/app/chromeos_strings_grdp/IDS_ACCOUNT_MANAGER_WELCOME_BUTTON.png.sha1 b/chrome/app/chromeos_strings_grdp/IDS_ACCOUNT_MANAGER_WELCOME_BUTTON.png.sha1
new file mode 100644
index 0000000..65c04fa
--- /dev/null
+++ b/chrome/app/chromeos_strings_grdp/IDS_ACCOUNT_MANAGER_WELCOME_BUTTON.png.sha1
@@ -0,0 +1 @@
+6ed62dc96bff7ef0b0a7eddc3552dde101bdf11d
\ No newline at end of file
diff --git a/chrome/app/chromeos_strings_grdp/IDS_ACCOUNT_MANAGER_WELCOME_TEXT.png.sha1 b/chrome/app/chromeos_strings_grdp/IDS_ACCOUNT_MANAGER_WELCOME_TEXT.png.sha1
index af26923..65c04fa 100644
--- a/chrome/app/chromeos_strings_grdp/IDS_ACCOUNT_MANAGER_WELCOME_TEXT.png.sha1
+++ b/chrome/app/chromeos_strings_grdp/IDS_ACCOUNT_MANAGER_WELCOME_TEXT.png.sha1
@@ -1 +1 @@
-19ffbc3968fe63257627287ffa342e424c7c1cfc
\ No newline at end of file
+6ed62dc96bff7ef0b0a7eddc3552dde101bdf11d
\ No newline at end of file
diff --git a/chrome/app/chromeos_strings_grdp/IDS_ACCOUNT_MANAGER_WELCOME_TITLE.png.sha1 b/chrome/app/chromeos_strings_grdp/IDS_ACCOUNT_MANAGER_WELCOME_TITLE.png.sha1
index 226cbec..65c04fa 100644
--- a/chrome/app/chromeos_strings_grdp/IDS_ACCOUNT_MANAGER_WELCOME_TITLE.png.sha1
+++ b/chrome/app/chromeos_strings_grdp/IDS_ACCOUNT_MANAGER_WELCOME_TITLE.png.sha1
@@ -1 +1 @@
-43b13f274f906c3b0e2aca792cc286b883b73c05
\ No newline at end of file
+6ed62dc96bff7ef0b0a7eddc3552dde101bdf11d
\ No newline at end of file
diff --git a/chrome/app/chromeos_strings_grdp/IDS_ACCOUNT_MIGRATION_UPDATE_BUTTON.png.sha1 b/chrome/app/chromeos_strings_grdp/IDS_ACCOUNT_MIGRATION_UPDATE_BUTTON.png.sha1
new file mode 100644
index 0000000..8d3043f
--- /dev/null
+++ b/chrome/app/chromeos_strings_grdp/IDS_ACCOUNT_MIGRATION_UPDATE_BUTTON.png.sha1
@@ -0,0 +1 @@
+6e74dee3836a86d0391c1c5dd87c54baf8b9521c
\ No newline at end of file
diff --git a/chrome/app/chromeos_strings_grdp/IDS_ACCOUNT_MIGRATION_WELCOME_TEXT.png.sha1 b/chrome/app/chromeos_strings_grdp/IDS_ACCOUNT_MIGRATION_WELCOME_TEXT.png.sha1
new file mode 100644
index 0000000..8d3043f
--- /dev/null
+++ b/chrome/app/chromeos_strings_grdp/IDS_ACCOUNT_MIGRATION_WELCOME_TEXT.png.sha1
@@ -0,0 +1 @@
+6e74dee3836a86d0391c1c5dd87c54baf8b9521c
\ No newline at end of file
diff --git a/chrome/app/chromeos_strings_grdp/IDS_ACCOUNT_MIGRATION_WELCOME_TITLE.png.sha1 b/chrome/app/chromeos_strings_grdp/IDS_ACCOUNT_MIGRATION_WELCOME_TITLE.png.sha1
new file mode 100644
index 0000000..8d3043f
--- /dev/null
+++ b/chrome/app/chromeos_strings_grdp/IDS_ACCOUNT_MIGRATION_WELCOME_TITLE.png.sha1
@@ -0,0 +1 @@
+6e74dee3836a86d0391c1c5dd87c54baf8b9521c
\ No newline at end of file
diff --git a/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_DOWNLOADING_MESSAGE.png.sha1 b/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_DOWNLOADING_MESSAGE.png.sha1
new file mode 100644
index 0000000..2e16724
--- /dev/null
+++ b/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_DOWNLOADING_MESSAGE.png.sha1
@@ -0,0 +1 @@
+35a7f67d6407ee1a3f9295e6537b569982a3bf82
\ No newline at end of file
diff --git a/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_DOWNLOAD_PROGRESS_MESSAGE.png.sha1 b/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_DOWNLOAD_PROGRESS_MESSAGE.png.sha1
new file mode 100644
index 0000000..2e16724
--- /dev/null
+++ b/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_DOWNLOAD_PROGRESS_MESSAGE.png.sha1
@@ -0,0 +1 @@
+35a7f67d6407ee1a3f9295e6537b569982a3bf82
\ No newline at end of file
diff --git a/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_DOWNLOAD_PROGRESS_WITHOUT_DOWNLOAD_SIZE_MESSAGE.png.sha1 b/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_DOWNLOAD_PROGRESS_WITHOUT_DOWNLOAD_SIZE_MESSAGE.png.sha1
new file mode 100644
index 0000000..341ba07
--- /dev/null
+++ b/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_DOWNLOAD_PROGRESS_WITHOUT_DOWNLOAD_SIZE_MESSAGE.png.sha1
@@ -0,0 +1 @@
+1a375495b162b5d13993a3cbcbcc7ad38085c420
\ No newline at end of file
diff --git a/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_ENVIRONMENT_SETTING_TITLE.png.sha1 b/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_ENVIRONMENT_SETTING_TITLE.png.sha1
new file mode 100644
index 0000000..46a1238
--- /dev/null
+++ b/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_ENVIRONMENT_SETTING_TITLE.png.sha1
@@ -0,0 +1 @@
+fbf9fe15c79321a16168a1ca22f77e09ef5e2e5d
\ No newline at end of file
diff --git a/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_ERROR_MESSAGE.png.sha1 b/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_ERROR_MESSAGE.png.sha1
new file mode 100644
index 0000000..6d1ff04
--- /dev/null
+++ b/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_ERROR_MESSAGE.png.sha1
@@ -0,0 +1 @@
+2f7b77c25f84e15d99a3355c9c03e5004a876a8c
\ No newline at end of file
diff --git a/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_ERROR_TITLE.png.sha1 b/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_ERROR_TITLE.png.sha1
new file mode 100644
index 0000000..6d1ff04
--- /dev/null
+++ b/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_ERROR_TITLE.png.sha1
@@ -0,0 +1 @@
+2f7b77c25f84e15d99a3355c9c03e5004a876a8c
\ No newline at end of file
diff --git a/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_FINISHED_MESSAGE.png.sha1 b/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_FINISHED_MESSAGE.png.sha1
new file mode 100644
index 0000000..041fae7
--- /dev/null
+++ b/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_FINISHED_MESSAGE.png.sha1
@@ -0,0 +1 @@
+90635bbbb7c6cda1709096f278d4479dbfc9794c
\ No newline at end of file
diff --git a/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_FINISHED_TITLE.png.sha1 b/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_FINISHED_TITLE.png.sha1
new file mode 100644
index 0000000..041fae7
--- /dev/null
+++ b/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_FINISHED_TITLE.png.sha1
@@ -0,0 +1 @@
+90635bbbb7c6cda1709096f278d4479dbfc9794c
\ No newline at end of file
diff --git a/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_IMPORTING_MESSAGE.png.sha1 b/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_IMPORTING_MESSAGE.png.sha1
new file mode 100644
index 0000000..af01bd0
--- /dev/null
+++ b/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_IMPORTING_MESSAGE.png.sha1
@@ -0,0 +1 @@
+502a528436ae6953073d008f5eef3b54f7a6e16c
\ No newline at end of file
diff --git a/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_LAUNCH_BUTTON.png.sha1 b/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_LAUNCH_BUTTON.png.sha1
new file mode 100644
index 0000000..041fae7
--- /dev/null
+++ b/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_LAUNCH_BUTTON.png.sha1
@@ -0,0 +1 @@
+90635bbbb7c6cda1709096f278d4479dbfc9794c
\ No newline at end of file
diff --git a/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_NOT_ALLOWED_MESSAGE.png.sha1 b/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_NOT_ALLOWED_MESSAGE.png.sha1
new file mode 100644
index 0000000..d079467
--- /dev/null
+++ b/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_NOT_ALLOWED_MESSAGE.png.sha1
@@ -0,0 +1 @@
+c2812c4f692dc0568d1353b5c4c58eced1b4a5ce
\ No newline at end of file
diff --git a/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_RETRY_BUTTON.png.sha1 b/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_RETRY_BUTTON.png.sha1
new file mode 100644
index 0000000..6d1ff04
--- /dev/null
+++ b/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_RETRY_BUTTON.png.sha1
@@ -0,0 +1 @@
+2f7b77c25f84e15d99a3355c9c03e5004a876a8c
\ No newline at end of file
diff --git a/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_START_DOWNLOADING_MESSAGE.png.sha1 b/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_START_DOWNLOADING_MESSAGE.png.sha1
new file mode 100644
index 0000000..46a1238
--- /dev/null
+++ b/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_START_DOWNLOADING_MESSAGE.png.sha1
@@ -0,0 +1 @@
+fbf9fe15c79321a16168a1ca22f77e09ef5e2e5d
\ No newline at end of file
diff --git a/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_TIME_LEFT_MESSAGE.png.sha1 b/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_TIME_LEFT_MESSAGE.png.sha1
new file mode 100644
index 0000000..2e16724
--- /dev/null
+++ b/chrome/app/chromeos_strings_grdp/IDS_PLUGIN_VM_LAUNCHER_TIME_LEFT_MESSAGE.png.sha1
@@ -0,0 +1 @@
+35a7f67d6407ee1a3f9295e6537b569982a3bf82
\ No newline at end of file
diff --git a/chrome/app/framework.order b/chrome/app/framework.order
index 91fba12..c24c630 100644
--- a/chrome/app/framework.order
+++ b/chrome/app/framework.order
@@ -19,7 +19,7 @@
 ___asan_default_options
 
 # Entry point from the app mode loader.
-_ChromeAppModeStart_v5
+_ChromeAppModeStart_v6
 
 # _ChromeMain must be listed last.  That's the whole point of this file.
 _ChromeMain
diff --git a/chrome/app/settings_strings.grdp b/chrome/app/settings_strings.grdp
index fec07a4..359f239 100644
--- a/chrome/app/settings_strings.grdp
+++ b/chrome/app/settings_strings.grdp
@@ -3612,7 +3612,7 @@
       Add account
     </message>
     <message name="IDS_SETTINGS_ACCOUNT_MANAGER_DESCRIPTION" desc="Description of the Account Manager Settings page. Shown just below the title of the page.">
-      All of your signed in Google Accounts from apps and websites can be managed here. Apps and websites that have your permission can access the account information they need to work properly.
+      Manage your signed-in Google Accounts. Websites, apps, and extensions in Chrome and Google Play may use these accounts to customize your experience, depending on permissions.
     </message>
     <message name="IDS_SETTINGS_ACCOUNT_MANAGER_LIST_HEADER" desc="List header for Account List in Account Manager Settings page.">
       Accounts
@@ -3626,6 +3626,12 @@
     <message name="IDS_SETTINGS_ACCOUNT_MANAGER_SIGNED_OUT_ACCOUNT_PLACEHOLDER" desc="Placeholder text for the full name of a signed out account in Account Manager.">
       Sign in again
     </message>
+    <message name="IDS_SETTINGS_ACCOUNT_MANAGER_UNMIGRATED_ACCOUNT_PLACEHOLDER" desc="Placeholder text for the unmigrated account in Account Manager.">
+      Not updated yet
+    </message>
+    <message name="IDS_SETTINGS_ACCOUNT_MANAGER_MIGRATION_LABEL" desc="Label of the migration button on Account Manager Settings page.">
+      Update account
+    </message>
     <message name="IDS_SETTINGS_ACCOUNT_MANAGER_REAUTHENTICATION_LABEL" desc="Label of the re-authentication button on Account Manager Settings page.">
       Sign in
     </message>
diff --git a/chrome/app/settings_strings_grdp/IDS_SETTINGS_ACCOUNT_MANAGER_DESCRIPTION.png.sha1 b/chrome/app/settings_strings_grdp/IDS_SETTINGS_ACCOUNT_MANAGER_DESCRIPTION.png.sha1
index 53fce49..12dbf1d 100644
--- a/chrome/app/settings_strings_grdp/IDS_SETTINGS_ACCOUNT_MANAGER_DESCRIPTION.png.sha1
+++ b/chrome/app/settings_strings_grdp/IDS_SETTINGS_ACCOUNT_MANAGER_DESCRIPTION.png.sha1
@@ -1 +1 @@
-09b51bcbf5f2b73810d6ae32252fa08a1a3936e0
\ No newline at end of file
+13150cd24a0c5a68defce4422aa8c1bce98966d6
\ No newline at end of file
diff --git a/chrome/app/settings_strings_grdp/IDS_SETTINGS_ACCOUNT_MANAGER_MIGRATION_LABEL.png.sha1 b/chrome/app/settings_strings_grdp/IDS_SETTINGS_ACCOUNT_MANAGER_MIGRATION_LABEL.png.sha1
new file mode 100644
index 0000000..c47b3e00
--- /dev/null
+++ b/chrome/app/settings_strings_grdp/IDS_SETTINGS_ACCOUNT_MANAGER_MIGRATION_LABEL.png.sha1
@@ -0,0 +1 @@
+92ba90eaa3d94731b388337b83084f101c67477f
\ No newline at end of file
diff --git a/chrome/app/settings_strings_grdp/IDS_SETTINGS_ACCOUNT_MANAGER_UNMIGRATED_ACCOUNT_PLACEHOLDER.png.sha1 b/chrome/app/settings_strings_grdp/IDS_SETTINGS_ACCOUNT_MANAGER_UNMIGRATED_ACCOUNT_PLACEHOLDER.png.sha1
new file mode 100644
index 0000000..c47b3e00
--- /dev/null
+++ b/chrome/app/settings_strings_grdp/IDS_SETTINGS_ACCOUNT_MANAGER_UNMIGRATED_ACCOUNT_PLACEHOLDER.png.sha1
@@ -0,0 +1 @@
+92ba90eaa3d94731b388337b83084f101c67477f
\ No newline at end of file
diff --git a/chrome/app_shim/app_mode_loader_mac.mm b/chrome/app_shim/app_mode_loader_mac.mm
index ad9d253..a9751cc 100644
--- a/chrome/app_shim/app_mode_loader_mac.mm
+++ b/chrome/app_shim/app_mode_loader_mac.mm
@@ -31,12 +31,6 @@
 
 typedef int (*StartFun)(const app_mode::ChromeAppModeInfo*);
 
-// The name of the entry point in the Framework. This name is dynamically
-// queried at shim launch to allow the shim to connect and run.
-// The function is versioned in case we need to obsolete and rebuild the shim
-// before it loads, e.g. see https://crbug.com/561205.
-const char kStartFunName[] = "ChromeAppModeStart_v5";
-
 int LoadFrameworkAndStart(int argc, char** argv) {
   using base::SysNSStringToUTF8;
   using base::SysNSStringToUTF16;
@@ -101,13 +95,11 @@
 
   // ** 3: Read information from the Chrome bundle.
   base::FilePath executable_path;
-  base::FilePath version_path;
-  base::FilePath framework_shlib_path;
-  if (!app_mode::GetChromeBundleInfo(cr_bundle_path,
-                                     cr_version_str.value(),
-                                     &executable_path,
-                                     &version_path,
-                                     &framework_shlib_path)) {
+  base::FilePath framework_path;
+  base::FilePath framework_dylib_path;
+  if (!app_mode::GetChromeBundleInfo(cr_bundle_path, cr_version_str.value(),
+                                     &executable_path, &framework_path,
+                                     &framework_dylib_path)) {
     LOG(FATAL) << "Couldn't ready Chrome bundle info";
   }
   base::FilePath app_mode_bundle_path =
@@ -136,10 +128,11 @@
 
   // ** 5: Open the framework.
   StartFun ChromeAppModeStart = NULL;
-  void* cr_dylib = dlopen(framework_shlib_path.value().c_str(), RTLD_LAZY);
+  void* cr_dylib = dlopen(framework_dylib_path.value().c_str(), RTLD_LAZY);
   if (cr_dylib) {
     // Find the entry point.
-    ChromeAppModeStart = (StartFun)dlsym(cr_dylib, kStartFunName);
+    ChromeAppModeStart =
+        (StartFun)dlsym(cr_dylib, APP_SHIM_ENTRY_POINT_NAME_STRING);
     if (!ChromeAppModeStart)
       LOG(ERROR) << "Couldn't get entry point: " << dlerror();
   } else {
@@ -149,7 +142,7 @@
   // ** 6: Fill in ChromeAppModeInfo and call into Chrome's framework.
   if (ChromeAppModeStart) {
     // Ensure that the strings pointed to by |info| outlive |info|.
-    const std::string version_path_utf8 = version_path.AsUTF8Unsafe();
+    const std::string framework_path_utf8 = framework_path.AsUTF8Unsafe();
     const std::string cr_bundle_path_utf8 = cr_bundle_path.AsUTF8Unsafe();
     const std::string app_mode_bundle_path_utf8 =
         app_mode_bundle_path.AsUTF8Unsafe();
@@ -157,11 +150,9 @@
         plist_user_data_dir.AsUTF8Unsafe();
     const std::string profile_dir_utf8 = profile_dir.AsUTF8Unsafe();
     app_mode::ChromeAppModeInfo info;
-    info.major_version = app_mode::kCurrentChromeAppModeInfoMajorVersion;
-    info.minor_version = app_mode::kCurrentChromeAppModeInfoMinorVersion;
     info.argc = argc;
     info.argv = argv;
-    info.chrome_versioned_path = version_path_utf8.c_str();
+    info.chrome_framework_path = framework_path_utf8.c_str();
     info.chrome_outer_bundle_path = cr_bundle_path_utf8.c_str();
     info.app_mode_bundle_path = app_mode_bundle_path_utf8.c_str();
     info.app_mode_id = app_mode_id.c_str();
diff --git a/chrome/app_shim/chrome_main_app_mode_mac.mm b/chrome/app_shim/chrome_main_app_mode_mac.mm
index 1d3939b..a5c4d15 100644
--- a/chrome/app_shim/chrome_main_app_mode_mac.mm
+++ b/chrome/app_shim/chrome_main_app_mode_mac.mm
@@ -30,6 +30,7 @@
 #include "chrome/common/chrome_constants.h"
 #include "chrome/common/chrome_content_client.h"
 #include "chrome/common/chrome_paths.h"
+#include "chrome/common/chrome_paths_internal.h"
 #include "chrome/common/mac/app_mode_common.h"
 #include "components/crash/content/app/crashpad.h"
 #include "mojo/core/embedder/embedder.h"
@@ -56,7 +57,7 @@
 // upgrade them; the old shim will not be able to dyload the new
 // ChromeAppModeStart, so it will fall back to the upgrade path. See
 // https://crbug.com/561205.
-__attribute__((visibility("default"))) int ChromeAppModeStart_v5(
+__attribute__((visibility("default"))) int APP_SHIM_ENTRY_POINT_NAME(
     const app_mode::ChromeAppModeInfo* info);
 
 }  // extern "C"
@@ -67,28 +68,18 @@
       base::TimeDelta::FromDays(1));
 }
 
-int ChromeAppModeStart_v5(const app_mode::ChromeAppModeInfo* info) {
+int APP_SHIM_ENTRY_POINT_NAME(const app_mode::ChromeAppModeInfo* info) {
   base::CommandLine::Init(info->argc, info->argv);
 
   base::mac::ScopedNSAutoreleasePool scoped_pool;
   base::AtExitManager exit_manager;
   chrome::RegisterPathProvider();
 
-  if (info->major_version < app_mode::kCurrentChromeAppModeInfoMajorVersion) {
-    RAW_LOG(ERROR, "App Mode Loader too old.");
-    return 1;
-  }
-  if (info->major_version > app_mode::kCurrentChromeAppModeInfoMajorVersion) {
-    RAW_LOG(ERROR, "Browser Framework too old to load App Shortcut.");
-    return 1;
-  }
-
   // Set bundle paths. This loads the bundles.
   base::mac::SetOverrideOuterBundlePath(
       base::FilePath(info->chrome_outer_bundle_path));
   base::mac::SetOverrideFrameworkBundlePath(
-      base::FilePath(info->chrome_versioned_path)
-          .Append(chrome::kFrameworkName));
+      base::FilePath(info->chrome_framework_path));
 
   ChromeCrashReporterClient::Create();
   crash_reporter::InitializeCrashpad(true, "app_shim");
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
index 43967437..50d90a7 100644
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -5268,8 +5268,6 @@
     "search_engines/template_url_service_factory_test_util.h",
     "search_engines/template_url_service_test_util.cc",
     "search_engines/template_url_service_test_util.h",
-    "signin/account_consistency_mode_manager_test_util.cc",
-    "signin/account_consistency_mode_manager_test_util.h",
     "signin/chrome_signin_client_test_util.cc",
     "signin/chrome_signin_client_test_util.h",
     "signin/identity_test_environment_profile_adaptor.cc",
diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
index 404c981..087fb4c 100644
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -1017,10 +1017,6 @@
     {"enable-new-photo-picker", flag_descriptions::kNewPhotoPickerName,
      flag_descriptions::kNewPhotoPickerDescription, kOsAndroid,
      FEATURE_VALUE_TYPE(chrome::android::kNewPhotoPicker)},
-    {"enable-usermedia-screen-capturing",
-     flag_descriptions::kMediaScreenCaptureName,
-     flag_descriptions::kMediaScreenCaptureDescription, kOsAndroid,
-     FEATURE_VALUE_TYPE(chrome::android::kUserMediaScreenCapturing)},
     {"enable-surfacecontrol", flag_descriptions::kAndroidSurfaceControl,
      flag_descriptions::kAndroidSurfaceControlDescription, kOsAndroid,
      FEATURE_VALUE_TYPE(features::kAndroidSurfaceControl)},
@@ -3819,14 +3815,6 @@
       channel == version_info::Channel::STABLE) {
     return true;
   }
-
-  // Don't expose in-session password change on stable and beta channel.
-  if (!strcmp("in-session-password-change", entry.internal_name) &&
-      channel != version_info::Channel::DEV &&
-      channel != version_info::Channel::CANARY &&
-      channel != version_info::Channel::UNKNOWN) {
-    return true;
-  }
 #endif  // defined(OS_CHROMEOS)
 
   // data-reduction-proxy-lo-fi and enable-data-reduction-proxy-lite-page
diff --git a/chrome/browser/android/send_tab_to_self/send_tab_to_self_android_bridge.cc b/chrome/browser/android/send_tab_to_self/send_tab_to_self_android_bridge.cc
index 1f207b9..cb8cf81 100644
--- a/chrome/browser/android/send_tab_to_self/send_tab_to_self_android_bridge.cc
+++ b/chrome/browser/android/send_tab_to_self/send_tab_to_self_android_bridge.cc
@@ -9,6 +9,7 @@
 #include "base/android/jni_string.h"
 #include "base/metrics/histogram_macros.h"
 #include "base/time/time.h"
+#include "chrome/browser/android/send_tab_to_self/send_tab_to_self_infobar.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/profiles/profile_android.h"
 #include "chrome/browser/send_tab_to_self/send_tab_to_self_client_service.h"
@@ -16,6 +17,7 @@
 #include "chrome/browser/send_tab_to_self/send_tab_to_self_util.h"
 #include "chrome/browser/sync/send_tab_to_self_sync_service_factory.h"
 #include "components/send_tab_to_self/send_tab_to_self_entry.h"
+#include "components/send_tab_to_self/send_tab_to_self_infobar_delegate.h"
 #include "components/send_tab_to_self/send_tab_to_self_model.h"
 #include "components/send_tab_to_self/send_tab_to_self_sync_service.h"
 #include "content/public/browser/web_contents.h"
@@ -178,4 +180,32 @@
   return ShouldOfferFeature(web_contents);
 }
 
+static void JNI_SendTabToSelfAndroidBridge_ShowInfoBar(
+    JNIEnv* env,
+    const JavaParamRef<jobject>& j_web_contents,
+    const JavaParamRef<jstring>& j_guid,
+    const JavaParamRef<jstring>& j_url,
+    const JavaParamRef<jstring>& j_target_device_sync_cache_guid) {
+  content::WebContents* web_contents =
+      content::WebContents::FromJavaWebContents(j_web_contents);
+  const std::string guid = ConvertJavaStringToUTF8(env, j_guid);
+  const std::string url = ConvertJavaStringToUTF8(env, j_url);
+  const std::string target_device_sync_cache_guid =
+      ConvertJavaStringToUTF8(env, j_target_device_sync_cache_guid);
+
+  std::unique_ptr<SendTabToSelfEntry> entry =
+      SendTabToSelfEntry::FromRequiredFields(guid, GURL(url),
+                                             target_device_sync_cache_guid);
+
+  // The entry fields were malformed so don't show an infobar. Theoretically,
+  // this should never happen because a malformed entry can not be synced
+  // to the server but it doesn't hurt to check.
+  if (!entry) {
+    return;
+  }
+  std::unique_ptr<SendTabToSelfInfoBarDelegate> delegate =
+      SendTabToSelfInfoBarDelegate::Create(web_contents, entry.release());
+  SendTabToSelfInfoBar::ShowInfoBar(web_contents, std::move(delegate));
+}
+
 }  // namespace send_tab_to_self
diff --git a/chrome/browser/android/send_tab_to_self/send_tab_to_self_infobar.cc b/chrome/browser/android/send_tab_to_self/send_tab_to_self_infobar.cc
index e833fd6..9280a9c 100644
--- a/chrome/browser/android/send_tab_to_self/send_tab_to_self_infobar.cc
+++ b/chrome/browser/android/send_tab_to_self/send_tab_to_self_infobar.cc
@@ -43,9 +43,12 @@
 }
 
 // static
-void SendTabToSelfInfoBar::ShowInfoBar(content::WebContents* web_contents,
-                                       SendTabToSelfInfoBarDelegate* delegate) {
-  NOTIMPLEMENTED();
+void SendTabToSelfInfoBar::ShowInfoBar(
+    content::WebContents* web_contents,
+    std::unique_ptr<SendTabToSelfInfoBarDelegate> delegate) {
+  InfoBarService* service = InfoBarService::FromWebContents(web_contents);
+  service->AddInfoBar(
+      base::WrapUnique(new SendTabToSelfInfoBar(std::move(delegate))));
 }
 
 }  // namespace send_tab_to_self
diff --git a/chrome/browser/android/send_tab_to_self/send_tab_to_self_infobar.h b/chrome/browser/android/send_tab_to_self/send_tab_to_self_infobar.h
index ad81556..7cb5fb0 100644
--- a/chrome/browser/android/send_tab_to_self/send_tab_to_self_infobar.h
+++ b/chrome/browser/android/send_tab_to_self/send_tab_to_self_infobar.h
@@ -23,8 +23,9 @@
  public:
   ~SendTabToSelfInfoBar() override;
   // |delegate| must remain alive while showing this info bar.
-  static void ShowInfoBar(content::WebContents* web_contents,
-                          SendTabToSelfInfoBarDelegate* delegate);
+  static void ShowInfoBar(
+      content::WebContents* web_contents,
+      std::unique_ptr<SendTabToSelfInfoBarDelegate> delegate);
 
  private:
   explicit SendTabToSelfInfoBar(
diff --git a/chrome/browser/apps/platform_apps/shortcut_manager.cc b/chrome/browser/apps/platform_apps/shortcut_manager.cc
index e675b62..cc115a7 100644
--- a/chrome/browser/apps/platform_apps/shortcut_manager.cc
+++ b/chrome/browser/apps/platform_apps/shortcut_manager.cc
@@ -30,6 +30,10 @@
 #include "extensions/browser/extension_system.h"
 #include "extensions/common/extension_set.h"
 
+#if defined(OS_MACOSX)
+#include "chrome/common/mac/app_mode_common.h"
+#endif
+
 using extensions::Extension;
 
 namespace {
@@ -38,7 +42,7 @@
 // need to be recreated. This might happen when we change various aspects of app
 // shortcuts like command-line flags or associated icons, binaries, etc.
 #if defined(OS_MACOSX)
-const int kCurrentAppShortcutsVersion = 5;
+const int kCurrentAppShortcutsVersion = APP_SHIM_VERSION_NUMBER;
 #else
 const int kCurrentAppShortcutsVersion = 0;
 #endif
diff --git a/chrome/browser/chromeos/OWNERS b/chrome/browser/chromeos/OWNERS
index 808c3cf..3f4dcc4 100644
--- a/chrome/browser/chromeos/OWNERS
+++ b/chrome/browser/chromeos/OWNERS
@@ -7,7 +7,6 @@
 derat@chromium.org
 jamescook@chromium.org
 oshima@chromium.org
-rkc@chromium.org
 satorux@chromium.org
 stevenjb@chromium.org
 xiyuan@chromium.org
diff --git a/chrome/browser/chromeos/accessibility/select_to_speak_browsertest.cc b/chrome/browser/chromeos/accessibility/select_to_speak_browsertest.cc
index 223f31d..be85c3b 100644
--- a/chrome/browser/chromeos/accessibility/select_to_speak_browsertest.cc
+++ b/chrome/browser/chromeos/accessibility/select_to_speak_browsertest.cc
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 #include <memory>
+#include <utility>
 #include <vector>
 
 #include "ash/accessibility/accessibility_focus_ring_controller.h"
@@ -16,6 +17,7 @@
 #include "ash/system/status_area_widget.h"
 #include "ash/system/unified/unified_system_tray.h"
 #include "base/bind.h"
+#include "base/command_line.h"
 #include "base/memory/weak_ptr.h"
 #include "base/strings/pattern.h"
 #include "chrome/browser/chromeos/accessibility/accessibility_manager.h"
@@ -34,6 +36,7 @@
 #include "extensions/browser/notification_types.h"
 #include "extensions/browser/process_manager.h"
 #include "services/service_manager/public/cpp/connector.h"
+#include "ui/accessibility/accessibility_switches.h"
 #include "ui/events/test/event_generator.h"
 #include "url/url_constants.h"
 
@@ -158,6 +161,16 @@
   DISALLOW_COPY_AND_ASSIGN(SelectToSpeakTest);
 };
 
+/* Test fixture enabling experimental accessibility language detection switch */
+class SelectToSpeakTestWithLanguageDetection : public SelectToSpeakTest {
+ protected:
+  void SetUpCommandLine(base::CommandLine* command_line) override {
+    SelectToSpeakTest::SetUpCommandLine(command_line);
+    command_line->AppendSwitch(
+        ::switches::kEnableExperimentalAccessibilityLanguageDetection);
+  }
+};
+
 IN_PROC_BROWSER_TEST_F(SelectToSpeakTest, SpeakStatusTray) {
   gfx::Rect tray_bounds = ash::Shell::Get()
                               ->GetPrimaryRootWindowController()
@@ -281,6 +294,37 @@
                                  "Second paragraph*"));
 }
 
+IN_PROC_BROWSER_TEST_F(SelectToSpeakTest, LanguageBoundsIgnoredByDefault) {
+  // Splitting at language bounds is behind a feature flag, test the default
+  // behaviour doesn't introduce a regression.
+  ActivateSelectToSpeakInWindowBounds(
+      "data:text/html;charset=utf-8,<div>"
+      "<span lang='en-US'>The first paragraph</span>"
+      "<span lang='fr-FR'>la deuxième paragraphe</span></div>");
+
+  EXPECT_TRUE(
+      base::MatchPattern(speech_monitor_.GetNextUtterance(),
+                         "The first paragraph* la deuxième paragraphe*"));
+}
+
+IN_PROC_BROWSER_TEST_F(SelectToSpeakTestWithLanguageDetection,
+                       BreaksAtLanguageBounds) {
+  ActivateSelectToSpeakInWindowBounds(
+      "data:text/html;charset=utf-8,<div>"
+      "<span lang='en-US'>The first paragraph</span>"
+      "<span lang='fr-FR'>la deuxième paragraphe</span></div>");
+
+  std::pair<std::string, std::string> result1 =
+      speech_monitor_.GetNextUtteranceWithLanguage();
+  EXPECT_TRUE(base::MatchPattern(result1.first, "The first paragraph*"));
+  EXPECT_EQ("en-US", result1.second);
+
+  std::pair<std::string, std::string> result2 =
+      speech_monitor_.GetNextUtteranceWithLanguage();
+  EXPECT_TRUE(base::MatchPattern(result2.first, "la deuxième paragraphe*"));
+  EXPECT_EQ("fr-FR", result2.second);
+}
+
 // Flaky test. https://crbug.com/950049
 IN_PROC_BROWSER_TEST_F(SelectToSpeakTest, DISABLED_FocusRingMovesWithMouse) {
   // Create a callback for the focus ring observer.
diff --git a/chrome/browser/chromeos/accessibility/speech_monitor.cc b/chrome/browser/chromeos/accessibility/speech_monitor.cc
index fcd6b7f..0ae533d 100644
--- a/chrome/browser/chromeos/accessibility/speech_monitor.cc
+++ b/chrome/browser/chromeos/accessibility/speech_monitor.cc
@@ -25,12 +25,17 @@
 }
 
 std::string SpeechMonitor::GetNextUtterance() {
+  return GetNextUtteranceWithLanguage().first;
+}
+
+std::pair<std::string, std::string>
+SpeechMonitor::GetNextUtteranceWithLanguage() {
   if (utterance_queue_.empty()) {
     loop_runner_ = new content::MessageLoopRunner();
     loop_runner_->Run();
     loop_runner_ = NULL;
   }
-  std::string result = utterance_queue_.front();
+  std::pair<std::string, std::string> result = utterance_queue_.front();
   utterance_queue_.pop_front();
   return result;
 }
@@ -58,9 +63,9 @@
       loop_runner_->Run();
       loop_runner_ = NULL;
     }
-    std::string result = utterance_queue_.front();
+    std::pair<std::string, std::string> result = utterance_queue_.front();
     utterance_queue_.pop_front();
-    if (result == message)
+    if (result.first == message)
       return true;
   }
   return false;
@@ -112,7 +117,8 @@
     return;
 
   VLOG(0) << "Speaking " << utterance->GetText();
-  utterance_queue_.push_back(utterance->GetText());
+  utterance_queue_.push_back(
+      std::make_pair(utterance->GetText(), utterance->GetLang()));
   if (loop_runner_.get())
     loop_runner_->Quit();
 }
diff --git a/chrome/browser/chromeos/accessibility/speech_monitor.h b/chrome/browser/chromeos/accessibility/speech_monitor.h
index c6f30b6..9c28ec7 100644
--- a/chrome/browser/chromeos/accessibility/speech_monitor.h
+++ b/chrome/browser/chromeos/accessibility/speech_monitor.h
@@ -5,6 +5,8 @@
 #ifndef CHROME_BROWSER_CHROMEOS_ACCESSIBILITY_SPEECH_MONITOR_H_
 #define CHROME_BROWSER_CHROMEOS_ACCESSIBILITY_SPEECH_MONITOR_H_
 
+#include <utility>
+
 #include "base/containers/circular_deque.h"
 #include "base/macros.h"
 #include "base/memory/ref_counted.h"
@@ -25,6 +27,8 @@
 
   // Blocks until the next utterance is spoken, and returns its text.
   std::string GetNextUtterance();
+  // Blocks until the next utterance is spoken, and returns its text.
+  std::pair<std::string, std::string> GetNextUtteranceWithLanguage();
 
   // Wait for next utterance and return true if next utterance is ChromeVox
   // enabled message.
@@ -60,7 +64,8 @@
   void SetError(const std::string& error) override;
 
   scoped_refptr<content::MessageLoopRunner> loop_runner_;
-  base::circular_deque<std::string> utterance_queue_;
+  // Our list of utterances and specified language.
+  base::circular_deque<std::pair<std::string, std::string>> utterance_queue_;
   bool did_stop_ = false;
   std::string error_;
 
diff --git a/chrome/browser/chromeos/login/easy_unlock/easy_unlock_service.h b/chrome/browser/chromeos/login/easy_unlock/easy_unlock_service.h
index 2272924..2807987 100644
--- a/chrome/browser/chromeos/login/easy_unlock/easy_unlock_service.h
+++ b/chrome/browser/chromeos/login/easy_unlock/easy_unlock_service.h
@@ -85,9 +85,10 @@
   // Gets/Sets/Clears the permit access for the local device.
   virtual void ClearPermitAccess() = 0;
 
-  // Gets/Sets the remote devices list.
+  // Retrieve the stored remote devices list:
+  //   * If in regular context, device list is retrieved from prefs.
+  //   * If in sign-in context, device list is retrieved from TPM.
   virtual const base::ListValue* GetRemoteDevices() const = 0;
-  virtual void SetRemoteDevices(const base::ListValue& devices) = 0;
 
   // Gets the challenge bytes for the user currently associated with the
   // service.
diff --git a/chrome/browser/chromeos/login/easy_unlock/easy_unlock_service_regular.cc b/chrome/browser/chromeos/login/easy_unlock/easy_unlock_service_regular.cc
index ac69064..5c2740e 100644
--- a/chrome/browser/chromeos/login/easy_unlock/easy_unlock_service_regular.cc
+++ b/chrome/browser/chromeos/login/easy_unlock/easy_unlock_service_regular.cc
@@ -242,8 +242,24 @@
     device_list->Append(std::move(dict));
   }
 
-  // TODO(tengs): Rename this function after the easy_unlock app is replaced.
-  SetRemoteDevices(*device_list);
+  SetStoredRemoteDevices(*device_list);
+}
+
+void EasyUnlockServiceRegular::SetStoredRemoteDevices(
+    const base::ListValue& devices) {
+  std::string remote_devices_json;
+  JSONStringValueSerializer serializer(&remote_devices_json);
+  serializer.Serialize(devices);
+  PA_LOG(VERBOSE) << "Setting RemoteDevices:\n  " << remote_devices_json;
+
+  DictionaryPrefUpdate pairing_update(profile()->GetPrefs(),
+                                      prefs::kEasyUnlockPairing);
+  if (devices.empty())
+    pairing_update->RemoveWithoutPathExpansion(kKeyDevices, NULL);
+  else
+    pairing_update->SetKey(kKeyDevices, devices.Clone());
+
+  RefreshCryptohomeKeysIfPossible();
 }
 
 proximity_auth::ProximityAuthPrefManager*
@@ -296,23 +312,6 @@
   return NULL;
 }
 
-void EasyUnlockServiceRegular::SetRemoteDevices(
-    const base::ListValue& devices) {
-  std::string remote_devices_json;
-  JSONStringValueSerializer serializer(&remote_devices_json);
-  serializer.Serialize(devices);
-  PA_LOG(VERBOSE) << "Setting RemoteDevices:\n  " << remote_devices_json;
-
-  DictionaryPrefUpdate pairing_update(profile()->GetPrefs(),
-                                      prefs::kEasyUnlockPairing);
-  if (devices.empty())
-    pairing_update->RemoveWithoutPathExpansion(kKeyDevices, NULL);
-  else
-    pairing_update->SetKey(kKeyDevices, devices.Clone());
-
-  RefreshCryptohomeKeysIfPossible();
-}
-
 std::string EasyUnlockServiceRegular::GetChallenge() const {
   return std::string();
 }
diff --git a/chrome/browser/chromeos/login/easy_unlock/easy_unlock_service_regular.h b/chrome/browser/chromeos/login/easy_unlock/easy_unlock_service_regular.h
index f03dd78..fee4245 100644
--- a/chrome/browser/chromeos/login/easy_unlock/easy_unlock_service_regular.h
+++ b/chrome/browser/chromeos/login/easy_unlock/easy_unlock_service_regular.h
@@ -72,6 +72,11 @@
   void UseLoadedRemoteDevices(
       const multidevice::RemoteDeviceRefList& remote_devices);
 
+  // Persists Smart Lock host and local device to prefs, and then informs
+  // the base class to potentially update Smart Lock host and local device
+  // stored in the TPM.
+  void SetStoredRemoteDevices(const base::ListValue& devices);
+
   // EasyUnlockService implementation:
   proximity_auth::ProximityAuthPrefManager* GetProximityAuthPrefManager()
       override;
@@ -79,7 +84,6 @@
   AccountId GetAccountId() const override;
   void ClearPermitAccess() override;
   const base::ListValue* GetRemoteDevices() const override;
-  void SetRemoteDevices(const base::ListValue& devices) override;
   std::string GetChallenge() const override;
   std::string GetWrappedSecret() const override;
   void RecordEasySignInOutcome(const AccountId& account_id,
diff --git a/chrome/browser/chromeos/login/easy_unlock/easy_unlock_service_signin_chromeos.cc b/chrome/browser/chromeos/login/easy_unlock/easy_unlock_service_signin_chromeos.cc
index 403ec45..59bda93 100644
--- a/chrome/browser/chromeos/login/easy_unlock/easy_unlock_service_signin_chromeos.cc
+++ b/chrome/browser/chromeos/login/easy_unlock/easy_unlock_service_signin_chromeos.cc
@@ -239,10 +239,6 @@
   return &data->remote_devices_value;
 }
 
-void EasyUnlockServiceSignin::SetRemoteDevices(const base::ListValue& devices) {
-  NOTREACHED();
-}
-
 std::string EasyUnlockServiceSignin::GetChallenge() const {
   const UserData* data = FindLoadedDataForCurrentUser();
   if (!data)
diff --git a/chrome/browser/chromeos/login/easy_unlock/easy_unlock_service_signin_chromeos.h b/chrome/browser/chromeos/login/easy_unlock/easy_unlock_service_signin_chromeos.h
index 20e080c..15a04e7 100644
--- a/chrome/browser/chromeos/login/easy_unlock/easy_unlock_service_signin_chromeos.h
+++ b/chrome/browser/chromeos/login/easy_unlock/easy_unlock_service_signin_chromeos.h
@@ -92,7 +92,6 @@
   AccountId GetAccountId() const override;
   void ClearPermitAccess() override;
   const base::ListValue* GetRemoteDevices() const override;
-  void SetRemoteDevices(const base::ListValue& devices) override;
   std::string GetChallenge() const override;
   std::string GetWrappedSecret() const override;
   void RecordEasySignInOutcome(const AccountId& account_id,
diff --git a/chrome/browser/chromeos/login/kiosk_browsertest.cc b/chrome/browser/chromeos/login/kiosk_browsertest.cc
index 3efe605..9a1bdbc 100644
--- a/chrome/browser/chromeos/login/kiosk_browsertest.cc
+++ b/chrome/browser/chromeos/login/kiosk_browsertest.cc
@@ -1033,7 +1033,8 @@
   WaitForAppLaunchSuccess();
 }
 
-IN_PROC_BROWSER_TEST_F(KioskTest, LaunchAppUserCancel) {
+// TODO(https://crbug.com/964333): Flakily seg faults.
+IN_PROC_BROWSER_TEST_F(KioskTest, DISABLED_LaunchAppUserCancel) {
   // Make fake_cws_ return empty update response.
   set_test_app_version("");
   OobeScreenWaiter splash_waiter(AppLaunchSplashScreenView::kScreenId);
diff --git a/chrome/browser/chromeos/plugin_vm/plugin_vm_image_manager.cc b/chrome/browser/chromeos/plugin_vm/plugin_vm_image_manager.cc
index 5f4ab10..ac48b3b 100644
--- a/chrome/browser/chromeos/plugin_vm/plugin_vm_image_manager.cc
+++ b/chrome/browser/chromeos/plugin_vm/plugin_vm_image_manager.cc
@@ -10,35 +10,45 @@
 #include "base/bind.h"
 #include "base/files/file.h"
 #include "base/files/file_util.h"
+#include "base/files/scoped_file.h"
 #include "base/guid.h"
 #include "base/strings/string_util.h"
 #include "base/task/post_task.h"
 #include "base/task/task_traits.h"
+#include "chrome/browser/chromeos/plugin_vm/plugin_vm_manager.h"
 #include "chrome/browser/chromeos/plugin_vm/plugin_vm_metrics_util.h"
 #include "chrome/browser/chromeos/plugin_vm/plugin_vm_pref_names.h"
 #include "chrome/browser/chromeos/plugin_vm/plugin_vm_util.h"
+#include "chrome/browser/chromeos/profiles/profile_helper.h"
 #include "chrome/browser/download/download_service_factory.h"
 #include "chrome/browser/profiles/profile.h"
+#include "chromeos/dbus/dbus_thread_manager.h"
+#include "chromeos/dbus/debug_daemon_client.h"
 #include "components/download/public/background_service/download_metadata.h"
 #include "components/download/public/background_service/download_service.h"
 #include "components/prefs/pref_service.h"
 #include "content/public/browser/browser_task_traits.h"
 #include "content/public/browser/browser_thread.h"
 #include "net/traffic_annotation/network_traffic_annotation.h"
-#include "third_party/zlib/google/zip.h"
 
 namespace {
 
-int64_t GetSpeed(base::TimeTicks start_tick, int64_t bytes_proceeded) {
+chromeos::ConciergeClient* GetConciergeClient() {
+  return chromeos::DBusThreadManager::Get()->GetConciergeClient();
+}
+
+int64_t GetSpeed(base::TimeTicks start_tick, int64_t units_processed) {
   const base::TimeDelta diff = base::TimeTicks::Now() - start_tick;
   const int64_t diff_ms = diff.InMilliseconds();
-  return diff_ms == 0 ? 0 : bytes_proceeded * 1000 / diff_ms;
+  return diff_ms == 0 ? 0 : units_processed * 1000 / diff_ms;
 }
 
 }  // namespace
 
 namespace plugin_vm {
 
+PluginVmImageManager::~PluginVmImageManager() = default;
+
 bool PluginVmImageManager::IsProcessingImage() {
   return State::NOT_STARTED < state_ && state_ < State::CONFIGURED;
 }
@@ -76,57 +86,6 @@
   download_service_->CancelDownload(current_download_guid_);
 }
 
-void PluginVmImageManager::StartUnzipping() {
-  if (state_ != State::DOWNLOADED) {
-    LOG(ERROR) << "Unzipping of PluginVm image couldn't proceed as current "
-               << "state is " << GetStateName(state_) << " not "
-               << GetStateName(State::DOWNLOADED);
-    OnUnzipped(false);
-    return;
-  }
-
-  state_ = State::UNZIPPING;
-  base::PostTaskWithTraitsAndReplyWithResult(
-      FROM_HERE, {base::TaskPriority::USER_VISIBLE, base::MayBlock()},
-      base::BindOnce(&PluginVmImageManager::UnzipDownloadedPluginVmImageArchive,
-                     base::Unretained(this)),
-      base::BindOnce(&PluginVmImageManager::OnUnzipped,
-                     weak_ptr_factory_.GetWeakPtr()));
-}
-
-void PluginVmImageManager::CancelUnzipping() {
-  state_ = State::UNZIPPING_CANCELLED;
-}
-
-void PluginVmImageManager::StartRegistration() {
-  if (state_ != State::UNZIPPED) {
-    LOG(ERROR) << "Registration of PluginVm image couldn't proceed as current "
-               << "state is " << GetStateName(state_) << " not "
-               << GetStateName(State::UNZIPPED);
-    OnRegistered(false);
-    return;
-  }
-
-  state_ = State::REGISTERING;
-  // TODO(https://crbug.com/947014): Add call to register PluginVm image.
-  base::PostTaskWithTraits(
-      FROM_HERE, {content::BrowserThread::UI},
-      base::BindOnce(&PluginVmImageManager::OnRegistered,
-                     weak_ptr_factory_.GetWeakPtr(), true /* success */));
-}
-
-void PluginVmImageManager::CancelRegistration() {
-  state_ = State::REGISTRATION_CANCELLED;
-}
-
-void PluginVmImageManager::SetObserver(Observer* observer) {
-  observer_ = observer;
-}
-
-void PluginVmImageManager::RemoveObserver() {
-  observer_ = nullptr;
-}
-
 void PluginVmImageManager::OnDownloadStarted() {
   download_start_tick_ = base::TimeTicks::Now();
   if (observer_)
@@ -145,6 +104,7 @@
 void PluginVmImageManager::OnDownloadCompleted(
     const download::CompletionInfo& info) {
   downloaded_plugin_vm_image_archive_ = info.path;
+  downloaded_plugin_vm_image_size_ = info.bytes_downloaded;
   current_download_guid_.clear();
 
   if (!VerifyDownload(info.hash256)) {
@@ -179,57 +139,222 @@
     observer_->OnDownloadFailed();
 }
 
-void PluginVmImageManager::OnUnzippingProgressUpdated(int new_unzipped_bytes) {
-  plugin_vm_image_bytes_unzipped_ += new_unzipped_bytes;
-  if (observer_) {
-    observer_->OnUnzippingProgressUpdated(
-        plugin_vm_image_bytes_unzipped_, plugin_vm_image_size_,
-        GetSpeed(unzipping_start_tick_, plugin_vm_image_bytes_unzipped_));
+void PluginVmImageManager::StartImport() {
+  if (state_ != State::DOWNLOADED) {
+    LOG(ERROR) << "Importing of PluginVm image couldn't proceed as current "
+               << "state is " << GetStateName(state_) << " not "
+               << GetStateName(State::DOWNLOADED);
+    OnImported(false);
+    return;
+  }
+
+  state_ = State::IMPORTING;
+
+  VLOG(1) << "Starting PluginVm dispatcher service";
+  chromeos::DBusThreadManager::Get()
+      ->GetDebugDaemonClient()
+      ->StartPluginVmDispatcher(
+          base::BindOnce(&PluginVmImageManager::OnPluginVmDispatcherStarted,
+                         weak_ptr_factory_.GetWeakPtr()));
+}
+
+void PluginVmImageManager::OnPluginVmDispatcherStarted(bool success) {
+  if (!success) {
+    LOG(ERROR) << "Failed to start PluginVm dispatcher service";
+    OnImported(false);
+    return;
+  }
+  GetConciergeClient()->WaitForServiceToBeAvailable(
+      base::BindOnce(&PluginVmImageManager::OnConciergeAvailable,
+                     weak_ptr_factory_.GetWeakPtr()));
+}
+
+void PluginVmImageManager::OnConciergeAvailable(bool success) {
+  if (!success) {
+    LOG(ERROR) << "Concierge did not become available";
+    OnImported(false);
+    return;
+  }
+  if (!GetConciergeClient()->IsDiskImageProgressSignalConnected()) {
+    LOG(ERROR) << "Disk image progress signal is not connected";
+    OnImported(false);
+    return;
+  }
+  VLOG(1) << "Plugin VM dispatcher service has been started and disk image "
+             "signals are connected";
+  GetConciergeClient()->AddDiskImageObserver(this);
+
+  base::PostTaskWithTraitsAndReplyWithResult(
+      FROM_HERE, {base::TaskPriority::USER_VISIBLE, base::MayBlock()},
+      base::BindOnce(&PluginVmImageManager::PrepareFD, base::Unretained(this)),
+      base::BindOnce(&PluginVmImageManager::OnFDPrepared,
+                     weak_ptr_factory_.GetWeakPtr()));
+}
+
+base::Optional<base::ScopedFD> PluginVmImageManager::PrepareFD() {
+  // TODO(aoldemeier): do we need to close this?
+  base::File file(downloaded_plugin_vm_image_archive_,
+                  base::File::FLAG_OPEN | base::File::FLAG_READ);
+  if (!file.IsValid()) {
+    LOG(ERROR) << "Failed to open "
+               << downloaded_plugin_vm_image_archive_.value();
+    return {};
+  }
+  base::ScopedFD fd(file.TakePlatformFile());
+  return fd;
+}
+
+void PluginVmImageManager::OnFDPrepared(
+    base::Optional<base::ScopedFD> maybeFd) {
+  if (!maybeFd.has_value()) {
+    LOG(ERROR) << "Could not open downloaded image archive";
+    OnImported(false);
+    return;
+  }
+
+  vm_tools::concierge::ImportDiskImageRequest request;
+  request.set_cryptohome_id(
+      chromeos::ProfileHelper::GetUserIdHashFromProfile(profile_));
+  request.set_disk_path(kPluginVmDefaultName);
+  request.set_storage_location(
+      vm_tools::concierge::STORAGE_CRYPTOHOME_PLUGINVM);
+  request.set_source_size(downloaded_plugin_vm_image_size_);
+
+  VLOG(1) << "Making call to concierge to import disk image";
+
+  GetConciergeClient()->ImportDiskImage(
+      std::move(maybeFd.value()), request,
+      base::BindOnce(&PluginVmImageManager::OnImportDiskImage,
+                     weak_ptr_factory_.GetWeakPtr()));
+}
+
+void PluginVmImageManager::OnImportDiskImage(
+    base::Optional<vm_tools::concierge::ImportDiskImageResponse> reply) {
+  if (!reply.has_value()) {
+    LOG(ERROR) << "Could not retrieve response from ImportDiskImage call to "
+                  "concierge";
+    OnImported(false);
+    return;
+  }
+
+  vm_tools::concierge::ImportDiskImageResponse response = reply.value();
+
+  // TODO(aoldemeier, okalitova): handle cases where this jumps straight to
+  // completed?
+  if (response.status() !=
+      vm_tools::concierge::DiskImageStatus::DISK_STATUS_IN_PROGRESS) {
+    LOG(ERROR) << "Disk image is not in progress. Status: " << response.status()
+               << "," << response.failure_reason();
+    OnImported(false);
+    return;
+  }
+
+  VLOG(1) << "Disk image import is now in progress";
+  import_start_tick_ = base::TimeTicks::Now();
+  current_import_command_uuid_ = response.command_uuid();
+  // Image in progress. Waiting for progress signals...
+  // TODO(aoldemeier, okalitova): think about adding a timeout here,
+  //   i.e. what happens if concierge dies and does not report any signal
+  //   back, not even an error signal. Right now, the user would see
+  //   the "Configuring Plugin VM" screen forever. Maybe that's OK
+  //   at this stage though.
+}
+
+void PluginVmImageManager::OnDiskImageProgress(
+    const vm_tools::concierge::DiskImageStatusResponse& signal) {
+  if (signal.command_uuid() != current_import_command_uuid_)
+    return;
+
+  const uint64_t percent_completed = signal.progress();
+  const vm_tools::concierge::DiskImageStatus status = signal.status();
+
+  switch (status) {
+    case vm_tools::concierge::DiskImageStatus::DISK_STATUS_CREATED:
+      VLOG(1) << "Disk image status indicates that importing is done.";
+      RequestFinalStatus();
+      return;
+    case vm_tools::concierge::DiskImageStatus::DISK_STATUS_IN_PROGRESS:
+      if (observer_) {
+        observer_->OnImportProgressUpdated(
+            percent_completed, GetSpeed(import_start_tick_, percent_completed));
+      }
+      return;
+    default:
+      LOG(ERROR) << "Disk image status signal has status: " << status
+                 << " with error message: " << signal.failure_reason()
+                 << " and current progress: " << percent_completed;
+      OnImported(false);
+      return;
   }
 }
 
-void PluginVmImageManager::OnUnzipped(bool success) {
-  plugin_vm_image_size_ = -1;
-  plugin_vm_image_bytes_unzipped_ = 0;
+void PluginVmImageManager::RequestFinalStatus() {
+  vm_tools::concierge::DiskImageStatusRequest status_request;
+  status_request.set_command_uuid(current_import_command_uuid_);
+  GetConciergeClient()->DiskImageStatus(
+      status_request,
+      base::BindOnce(&PluginVmImageManager::OnFinalDiskImageStatus,
+                     weak_ptr_factory_.GetWeakPtr()));
+}
+
+void PluginVmImageManager::OnFinalDiskImageStatus(
+    base::Optional<vm_tools::concierge::DiskImageStatusResponse> reply) {
+  if (!reply.has_value()) {
+    LOG(ERROR) << "Could not retrieve response from DiskImageStatus call to "
+                  "concierge";
+    OnImported(false);
+    return;
+  }
+
+  vm_tools::concierge::DiskImageStatusResponse response = reply.value();
+  DCHECK(response.command_uuid() == current_import_command_uuid_);
+  if (response.status() !=
+      vm_tools::concierge::DiskImageStatus::DISK_STATUS_CREATED) {
+    LOG(ERROR) << "Disk image is not created. Status: " << response.status()
+               << ", " << response.failure_reason();
+    OnImported(false);
+    return;
+  }
+
+  OnImported(true);
+}
+
+void PluginVmImageManager::OnImported(bool success) {
+  GetConciergeClient()->RemoveDiskImageObserver(this);
   RemoveTemporaryPluginVmImageArchiveIfExists();
+  current_import_command_uuid_.clear();
 
   if (!success) {
-    state_ = State::UNZIPPING_FAILED;
+    LOG(ERROR) << "Image import failed";
+    state_ = State::IMPORTING_FAILED;
     if (observer_)
-      observer_->OnUnzippingFailed();
-    RemovePluginVmImageDirectoryIfExists();
+      observer_->OnImportFailed();
+
     return;
   }
 
-  state_ = State::UNZIPPED;
+  profile_->GetPrefs()->SetBoolean(plugin_vm::prefs::kPluginVmImageExists,
+                                   true);
   if (observer_)
-    observer_->OnUnzipped();
-}
-
-void PluginVmImageManager::OnRegistered(bool success) {
-  // If image registration has been canceled registration call result is just
-  // not being proceeded.
-  if (state_ == State::REGISTRATION_CANCELLED) {
-    RemovePluginVmImageDirectoryIfExists();
-    state_ = State::NOT_STARTED;
-    return;
-  }
-
-  if (!success) {
-    state_ = State::REGISTRATION_FAILED;
-    if (observer_)
-      observer_->OnRegistrationFailed();
-    RemovePluginVmImageDirectoryIfExists();
-    return;
-  }
-
-  state_ = State::REGISTERED;
-  if (observer_)
-    observer_->OnRegistered();
+    observer_->OnImported();
 
   state_ = State::CONFIGURED;
 }
 
+void PluginVmImageManager::CancelImport() {
+  VLOG(1) << "Cancelling import with command_uuid: "
+          << current_import_command_uuid_;
+  // TODO(aoldemeier,okalitova) Make D-Bus call and set/handle state.
+}
+
+void PluginVmImageManager::SetObserver(Observer* observer) {
+  observer_ = observer;
+}
+
+void PluginVmImageManager::RemoveObserver() {
+  observer_ = nullptr;
+}
+
 void PluginVmImageManager::SetDownloadServiceForTesting(
     download::DownloadService* download_service) {
   download_service_ = download_service;
@@ -248,7 +373,6 @@
     : profile_(profile),
       download_service_(DownloadServiceFactory::GetForBrowserContext(profile)),
       weak_ptr_factory_(this) {}
-PluginVmImageManager::~PluginVmImageManager() = default;
 
 GURL PluginVmImageManager::GetPluginVmImageDownloadUrl() {
   const base::Value* url_ptr =
@@ -272,26 +396,16 @@
       return "DOWNLOAD_CANCELLED";
     case State::DOWNLOADED:
       return "DOWNLOADED";
-    case State::UNZIPPING:
-      return "UNZIPPING";
-    case State::UNZIPPING_CANCELLED:
-      return "UNZIPPING_CANCELLED";
-    case State::UNZIPPED:
-      return "UNZIPPED";
-    case State::REGISTERING:
-      return "REGISTERING";
-    case State::REGISTRATION_CANCELLED:
-      return "REGISTRATION_CANCELLED";
-    case State::REGISTERED:
-      return "REGISTERED";
+    case State::IMPORTING:
+      return "IMPORTING";
+    case State::IMPORTING_CANCELLED:
+      return "IMPORTING_CANCELLED";
     case State::CONFIGURED:
       return "CONFIGURED";
     case State::DOWNLOAD_FAILED:
       return "DOWNLOAD_FAILED";
-    case State::UNZIPPING_FAILED:
-      return "UNZIPPING_FAILED";
-    case State::REGISTRATION_FAILED:
-      return "REGISTRATION_FAILED";
+    case State::IMPORTING_FAILED:
+      return "IMPORTING_FAILED";
   }
 }
 
@@ -354,138 +468,6 @@
                                           downloaded_archive_hash);
 }
 
-void PluginVmImageManager::CalculatePluginVmImageSize() {
-  plugin_vm_image_size_ = 0;
-
-  zip::ZipReader reader;
-  if (!reader.Open(downloaded_plugin_vm_image_archive_)) {
-    LOG(ERROR) << downloaded_plugin_vm_image_archive_.value()
-               << " cannot be opened by ZipReader";
-    plugin_vm_image_size_ = -1;
-    return;
-  }
-
-  while (reader.HasMore()) {
-    if (!reader.OpenCurrentEntryInZip()) {
-      LOG(ERROR) << "One of zip entries cannot be opened";
-      plugin_vm_image_size_ = -1;
-      return;
-    }
-    plugin_vm_image_size_ += reader.current_entry_info()->original_size();
-    if (!reader.AdvanceToNextEntry()) {
-      LOG(ERROR) << "ZipReader failed to advance to the next entry";
-      plugin_vm_image_size_ = -1;
-      return;
-    }
-  }
-}
-
-bool PluginVmImageManager::UnzipDownloadedPluginVmImageArchive() {
-  if (!EnsureDirectoryForPluginVmImageIsPresent() ||
-      !EnsureDownloadedPluginVmImageArchiveIsPresent()) {
-    LOG(ERROR) << "Unzipping of PluginVm image couldn't be proceeded";
-    return false;
-  }
-
-  CalculatePluginVmImageSize();
-
-  base::File file(downloaded_plugin_vm_image_archive_,
-                  base::File::FLAG_OPEN | base::File::FLAG_READ);
-  if (!file.IsValid()) {
-    LOG(ERROR) << "Failed to open "
-               << downloaded_plugin_vm_image_archive_.value();
-    return false;
-  }
-
-  unzipping_start_tick_ = base::TimeTicks::Now();
-  plugin_vm_image_bytes_unzipped_ = 0;
-  bool success = zip::UnzipWithFilterAndWriters(
-      file.GetPlatformFile(),
-      base::BindRepeating(
-          &PluginVmImageManager::CreatePluginVmImageWriterDelegate,
-          base::Unretained(this)),
-      base::BindRepeating(&PluginVmImageManager::CreateDirectory,
-                          base::Unretained(this)),
-      base::BindRepeating(
-          &PluginVmImageManager::FilterFilesInPluginVmImageArchive,
-          base::Unretained(this)),
-      true /* log_skipped_files */);
-  return success;
-}
-
-bool PluginVmImageManager::IsUnzippingCancelled() {
-  return state_ == State::UNZIPPING_CANCELLED;
-}
-
-PluginVmImageManager::PluginVmImageWriterDelegate::PluginVmImageWriterDelegate(
-    PluginVmImageManager* manager,
-    const base::FilePath& output_file_path)
-    : manager_(manager), output_file_path_(output_file_path) {}
-
-bool PluginVmImageManager::PluginVmImageWriterDelegate::PrepareOutput() {
-  // We can't rely on parent directory entries being specified in the
-  // zip, so we make sure they are created.
-  if (!base::CreateDirectory(output_file_path_.DirName()))
-    return false;
-
-  output_file_.Initialize(output_file_path_, base::File::FLAG_CREATE_ALWAYS |
-                                                 base::File::FLAG_WRITE);
-  return output_file_.IsValid();
-}
-
-bool PluginVmImageManager::PluginVmImageWriterDelegate::WriteBytes(
-    const char* data,
-    int num_bytes) {
-  bool success = num_bytes == output_file_.WriteAtCurrentPos(data, num_bytes);
-  if (success) {
-    base::PostTaskWithTraits(
-        FROM_HERE, {content::BrowserThread::UI},
-        base::BindOnce(&PluginVmImageManager::OnUnzippingProgressUpdated,
-                       base::Unretained(manager_), num_bytes));
-  }
-  return !manager_->IsUnzippingCancelled() && success;
-}
-
-void PluginVmImageManager::PluginVmImageWriterDelegate::SetTimeModified(
-    const base::Time& time) {
-  output_file_.Close();
-  base::TouchFile(output_file_path_, base::Time::Now(), time);
-}
-
-std::unique_ptr<zip::WriterDelegate>
-PluginVmImageManager::CreatePluginVmImageWriterDelegate(
-    const base::FilePath& entry_path) {
-  return std::make_unique<PluginVmImageWriterDelegate>(
-      this, plugin_vm_image_dir_.Append(entry_path));
-}
-
-bool PluginVmImageManager::CreateDirectory(const base::FilePath& entry_path) {
-  return base::CreateDirectory(plugin_vm_image_dir_.Append(entry_path));
-}
-
-bool PluginVmImageManager::FilterFilesInPluginVmImageArchive(
-    const base::FilePath& file) {
-  return true;
-}
-
-bool PluginVmImageManager::EnsureDownloadedPluginVmImageArchiveIsPresent() {
-  return !downloaded_plugin_vm_image_archive_.empty();
-}
-
-bool PluginVmImageManager::EnsureDirectoryForPluginVmImageIsPresent() {
-  plugin_vm_image_dir_ = profile_->GetPath()
-                             .AppendASCII(kCrosvmDir)
-                             .AppendASCII(kPvmDir)
-                             .AppendASCII(kPluginVmImageDir);
-  if (!base::CreateDirectory(plugin_vm_image_dir_)) {
-    LOG(ERROR) << "Directory " << plugin_vm_image_dir_.value()
-               << " failed to be created";
-    plugin_vm_image_dir_.clear();
-    return false;
-  }
-  return true;
-}
-
 void PluginVmImageManager::RemoveTemporaryPluginVmImageArchiveIfExists() {
   if (!downloaded_plugin_vm_image_archive_.empty()) {
     base::PostTaskWithTraitsAndReplyWithResult(
@@ -506,27 +488,8 @@
                << " failed to be deleted";
     return;
   }
+  downloaded_plugin_vm_image_size_ = -1;
   downloaded_plugin_vm_image_archive_.clear();
 }
 
-void PluginVmImageManager::RemovePluginVmImageDirectoryIfExists() {
-  if (!plugin_vm_image_dir_.empty()) {
-    base::PostTaskWithTraitsAndReplyWithResult(
-        FROM_HERE, {base::TaskPriority::USER_VISIBLE, base::MayBlock()},
-        base::BindOnce(&base::DeleteFile, plugin_vm_image_dir_,
-                       true /* recursive */),
-        base::BindOnce(&PluginVmImageManager::OnPluginVmImageDirectoryRemoved,
-                       weak_ptr_factory_.GetWeakPtr()));
-  }
-}
-
-void PluginVmImageManager::OnPluginVmImageDirectoryRemoved(bool success) {
-  if (!success) {
-    LOG(ERROR) << "Directory with PluginVm image "
-               << plugin_vm_image_dir_.value() << " failed to be deleted";
-    return;
-  }
-  plugin_vm_image_dir_.clear();
-}
-
 }  // namespace plugin_vm
diff --git a/chrome/browser/chromeos/plugin_vm/plugin_vm_image_manager.h b/chrome/browser/chromeos/plugin_vm/plugin_vm_image_manager.h
index 5260b8b..b0fc2bd 100644
--- a/chrome/browser/chromeos/plugin_vm/plugin_vm_image_manager.h
+++ b/chrome/browser/chromeos/plugin_vm/plugin_vm_image_manager.h
@@ -11,9 +11,10 @@
 #include "base/macros.h"
 #include "base/memory/weak_ptr.h"
 #include "base/time/time.h"
+#include "chromeos/dbus/concierge/service.pb.h"
+#include "chromeos/dbus/concierge_client.h"
 #include "components/download/public/background_service/download_params.h"
 #include "components/keyed_service/core/keyed_service.h"
-#include "third_party/zlib/google/zip_reader.h"
 
 namespace download {
 class DownloadService;
@@ -24,23 +25,20 @@
 
 namespace plugin_vm {
 
-constexpr char kCrosvmDir[] = "crosvm";
-constexpr char kPvmDir[] = "pvm";
-constexpr char kPluginVmImageDir[] = "default";
-
 // PluginVmImageManager is responsible for management of PluginVm image
 // including downloading this image from url specified by the user policy,
-// unzipping downloaded image archive to the specified location and registering
-// final image.
+// and importing the downloaded image archive using concierge D-Bus services.
 //
 // Only one PluginVm image at a time is allowed to be processed.
-// Methods StartDownload(), StartUnzipping() and StartRegistration() should be
-// called according to this order, image processing might be interrupted by
-// calling according cancel methods. If one of the methods mentioned is called
-// not in the correct order or before previous state is finished then associated
-// fail method will be called by manager and image processing will be
-// interrupted.
-class PluginVmImageManager : public KeyedService {
+// Methods StartDownload() and StartImport() should be
+// called in this order. Image processing might be interrupted by
+// calling the corresponding cancel methods. If one of the methods mentioned is
+// called not in the correct order or before the previous state is finished then
+// associated fail method will be called by the manager and image processing
+// will be interrupted.
+class PluginVmImageManager
+    : public KeyedService,
+      public chromeos::ConciergeClient::DiskImageObserver {
  public:
   // Observer class for the PluginVm image related events.
   class Observer {
@@ -54,14 +52,10 @@
     virtual void OnDownloadCancelled() = 0;
     // TODO(https://crbug.com/904851): Add failure reasons.
     virtual void OnDownloadFailed() = 0;
-    virtual void OnUnzippingProgressUpdated(
-        int64_t bytes_unzipped,
-        int64_t plugin_vm_image_size,
-        int64_t unzipping_bytes_per_sec) = 0;
-    virtual void OnUnzipped() = 0;
-    virtual void OnUnzippingFailed() = 0;
-    virtual void OnRegistered() = 0;
-    virtual void OnRegistrationFailed() = 0;
+    virtual void OnImportProgressUpdated(uint64_t percent_completed,
+                                         int64_t import_percent_per_sec) = 0;
+    virtual void OnImported() = 0;
+    virtual void OnImportFailed() = 0;
   };
 
   explicit PluginVmImageManager(Profile* profile);
@@ -73,23 +67,13 @@
   // Downloaded PluginVm image archive is being deleted.
   void CancelDownload();
 
+  // Proceed with importing (unzipping and registering) of the VM image.
   // Should be called when download of PluginVm image is successfully completed.
-  // If called in other cases - unzipping is not started and
-  // OnUnzipped(false /* success */) is called.
-  void StartUnzipping();
-  // Sets flag that indicates that unzipping is cancelled. This flag is further
-  // checked in PluginVmImageWriterDelegate and in cases it is set to true
-  // OnUnzipped(false /* success */) called to interrupt unzipping and
-  // remove PluginVm image.
-  void CancelUnzipping();
-
-  // Should be called when download and unzipping of PluginVm are successfully
-  // completed. If called in other cases - registration is not started and
-  // OnRegistered(false /* success */) is called.
-  void StartRegistration();
-  // Sets flag that indicates that registration is cancelled. This flag is
-  // further checked when OnRegistered(bool success) is called.
-  void CancelRegistration();
+  // If called in other cases - importing is not started and
+  // OnImported(false /* success */) is called.
+  void StartImport();
+  // Makes a call to concierge to cancel the import.
+  void CancelImport();
 
   void SetObserver(Observer* observer);
   void RemoveObserver();
@@ -103,15 +87,13 @@
   void OnDownloadCancelled();
   void OnDownloadFailed();
 
-  // Called by PluginVmImageWriterDelegate, are not supposed to be used by other
-  // classes.
-  void OnUnzippingProgressUpdated(int new_bytes_unzipped);
-  // Deletes downloaded PluginVm image archive. In case |success| is false also
-  // deletes PluginVm image.
-  void OnUnzipped(bool success);
+  // ConciergeClient::DiskImageObserver:
+  void OnDiskImageProgress(
+      const vm_tools::concierge::DiskImageStatusResponse& signal) override;
 
-  // Returns true in case downloaded PluginVm image archive passes verification
-  // and false otherwise. Public for testing purposes.
+  // Helper function that returns true in case downloaded PluginVm image
+  // archive passes hash verification and false otherwise.
+  // Public for testing purposes.
   bool VerifyDownload(const std::string& downloaded_archive_hash);
 
   void SetDownloadServiceForTesting(
@@ -126,16 +108,11 @@
     DOWNLOADING,
     DOWNLOAD_CANCELLED,
     DOWNLOADED,
-    UNZIPPING,
-    UNZIPPING_CANCELLED,
-    UNZIPPED,
-    REGISTERING,
-    REGISTRATION_CANCELLED,
-    REGISTERED,
+    IMPORTING,
+    IMPORTING_CANCELLED,
     CONFIGURED,
     DOWNLOAD_FAILED,
-    UNZIPPING_FAILED,
-    REGISTRATION_FAILED,
+    IMPORTING_FAILED,
   };
 
   Profile* profile_ = nullptr;
@@ -144,32 +121,12 @@
   State state_ = State::NOT_STARTED;
   std::string current_download_guid_;
   base::FilePath downloaded_plugin_vm_image_archive_;
-  base::FilePath plugin_vm_image_dir_;
+  // Used to identify our running import with concierge:
+  std::string current_import_command_uuid_;
   // -1 when is not yet determined.
-  int64_t plugin_vm_image_size_ = -1;
+  int64_t downloaded_plugin_vm_image_size_ = -1;
   base::TimeTicks download_start_tick_;
-  int64_t plugin_vm_image_bytes_unzipped_ = 0;
-  base::TimeTicks unzipping_start_tick_;
-
-  class PluginVmImageWriterDelegate : public zip::WriterDelegate {
-   public:
-    explicit PluginVmImageWriterDelegate(
-        PluginVmImageManager* manager,
-        const base::FilePath& output_file_path);
-
-    // zip::WriterDelegate implementation.
-    bool PrepareOutput() override;
-    // If unzipping is cancelled returns false so unzipping would fail.
-    bool WriteBytes(const char* data, int num_bytes) override;
-    void SetTimeModified(const base::Time& time) override;
-
-   private:
-    PluginVmImageManager* manager_;
-    base::FilePath output_file_path_;
-    base::File output_file_;
-
-    DISALLOW_COPY_AND_ASSIGN(PluginVmImageWriterDelegate);
-  };
+  base::TimeTicks import_start_tick_;
 
   ~PluginVmImageManager() override;
 
@@ -182,27 +139,42 @@
   void OnStartDownload(const std::string& download_guid,
                        download::DownloadParams::StartResult start_result);
 
-  void CalculatePluginVmImageSize();
-  bool UnzipDownloadedPluginVmImageArchive();
-  bool IsUnzippingCancelled();
-  // Callback arguments for unzipping function.
-  std::unique_ptr<zip::WriterDelegate> CreatePluginVmImageWriterDelegate(
-      const base::FilePath& entry_path);
-  bool CreateDirectory(const base::FilePath& entry_path);
-  bool FilterFilesInPluginVmImageArchive(const base::FilePath& file);
+  // Callback when PluginVm dispatcher is started (together with supporting
+  // services such as concierge). This will then make the call to concierge's
+  // ImportDiskImage.
+  void OnPluginVmDispatcherStarted(bool success);
+
+  // Callback which is called once we know if concierge is available.
+  void OnConciergeAvailable(bool success);
+
+  // Ran as a blocking task preparing the FD for the ImportDiskImage call.
+  base::Optional<base::ScopedFD> PrepareFD();
+
+  // Callback when the FD is prepared. Makes the call to ImportDiskImage.
+  void OnFDPrepared(base::Optional<base::ScopedFD> maybeFd);
+
+  // Callback for the concierge DiskImageImport call.
+  void OnImportDiskImage(
+      base::Optional<vm_tools::concierge::ImportDiskImageResponse> reply);
+
+  // After we get a signal that the import is finished successfully, we
+  // make one final call to concierge's DiskImageStatus method to get a
+  // final resolution.
+  void RequestFinalStatus();
+
+  // Callback for the final call to concierge's DiskImageStatus to
+  // get the final result of the disk import operation. This moves
+  // the manager to a finishing state, depending on the result of the
+  // query. Called when the signal for the command indicates that we
+  // are done with importing.
+  void OnFinalDiskImageStatus(
+      base::Optional<vm_tools::concierge::DiskImageStatusResponse> reply);
 
   // Finishes the processing of PluginVm image.
-  void OnRegistered(bool success);
+  void OnImported(bool success);
 
-  bool EnsureDownloadedPluginVmImageArchiveIsPresent();
-  // Creates directory for PluginVm image if one doesn't exists.
-  // Returns true in case directory has already existed or was successfully
-  // created and false otherwise.
-  bool EnsureDirectoryForPluginVmImageIsPresent();
   void RemoveTemporaryPluginVmImageArchiveIfExists();
   void OnTemporaryPluginVmImageArchiveRemoved(bool success);
-  void RemovePluginVmImageDirectoryIfExists();
-  void OnPluginVmImageDirectoryRemoved(bool success);
 
   base::WeakPtrFactory<PluginVmImageManager> weak_ptr_factory_;
 
diff --git a/chrome/browser/chromeos/plugin_vm/plugin_vm_image_manager_unittest.cc b/chrome/browser/chromeos/plugin_vm/plugin_vm_image_manager_unittest.cc
index 0f34328..b41e0dc 100644
--- a/chrome/browser/chromeos/plugin_vm/plugin_vm_image_manager_unittest.cc
+++ b/chrome/browser/chromeos/plugin_vm/plugin_vm_image_manager_unittest.cc
@@ -17,10 +17,13 @@
 #include "chrome/browser/chromeos/plugin_vm/plugin_vm_image_manager_factory.h"
 #include "chrome/browser/chromeos/plugin_vm/plugin_vm_metrics_util.h"
 #include "chrome/browser/chromeos/plugin_vm/plugin_vm_pref_names.h"
+#include "chrome/browser/chromeos/plugin_vm/plugin_vm_test_helper.h"
 #include "chrome/browser/chromeos/profiles/profile_helper.h"
 #include "chrome/browser/chromeos/settings/scoped_cros_settings_test_helper.h"
 #include "chrome/browser/prefs/browser_prefs.h"
 #include "chrome/test/base/testing_profile.h"
+#include "chromeos/dbus/dbus_thread_manager.h"
+#include "chromeos/dbus/fake_concierge_client.h"
 #include "components/account_id/account_id.h"
 #include "components/download/public/background_service/test/test_download_service.h"
 #include "components/prefs/scoped_user_pref_update.h"
@@ -28,28 +31,23 @@
 #include "content/public/test/test_browser_thread_bundle.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
-#include "third_party/zlib/google/zip.h"
 
 namespace plugin_vm {
 
 namespace {
 
+using ::testing::_;
+
 const char kProfileName[] = "p1";
 const char kUrl[] = "http://example.com";
+const char kPluginVmImageFile[] = "plugin_vm_image_file_1.zip";
+const char kContent[] = "This is zipped content.";
 const char kHash[] =
     "842841a4c75a55ad050d686f4ea5f77e83ae059877fe9b6946aa63d3d057ed32";
 const char kHashUppercase[] =
     "842841A4C75A55AD050D686F4EA5F77E83AE059877FE9B6946AA63D3D057ED32";
 const char kHash2[] =
     "02f06421ae27144aacdc598aebcd345a5e2e634405e8578300173628fe1574bd";
-const char kPluginVmImageUnzipped[] = "plugin_vm_image_unzipped";
-const char kPluginVmImageFile1[] = "plugin_vm_image_file_1";
-const char kContent1[] = "This is content #1.";
-const int kContent1Size = strlen(kContent1);
-const char kPluginVmImageFile2[] = "plugin_vm_image_file_2";
-const char kContent2[] = "This is content #2.";
-const int kContent2Size = strlen(kContent2);
-const int kContentSize = kContent1Size + kContent2Size;
 const char kLicenseKey[] = "LICENSE_KEY";
 // File size set in test_download_service.
 const int kDownloadedPluginVmImageSizeInMb = 123456789u / (1024 * 1024);
@@ -66,20 +64,25 @@
   MOCK_METHOD0(OnDownloadCompleted, void());
   MOCK_METHOD0(OnDownloadCancelled, void());
   MOCK_METHOD0(OnDownloadFailed, void());
-  MOCK_METHOD3(OnUnzippingProgressUpdated,
-               void(int64_t bytes_unzipped,
-                    int64_t plugin_vm_image_size,
-                    int64_t unzipping_bytes_per_sec));
-  MOCK_METHOD0(OnUnzipped, void());
-  MOCK_METHOD0(OnUnzippingFailed, void());
-  MOCK_METHOD0(OnRegistered, void());
-  MOCK_METHOD0(OnRegistrationFailed, void());
+  MOCK_METHOD2(OnImportProgressUpdated,
+               void(uint64_t percent_completed,
+                    int64_t import_percent_per_second));
+  MOCK_METHOD0(OnImported, void());
+  MOCK_METHOD0(OnImportFailed, void());
 };
 
 class PluginVmImageManagerTest : public testing::Test {
  public:
   PluginVmImageManagerTest()
-      : download_service_(new download::test::TestDownloadService()) {}
+      : download_service_(new download::test::TestDownloadService()) {
+    chromeos::DBusThreadManager::Initialize();
+    fake_concierge_client_ = static_cast<chromeos::FakeConciergeClient*>(
+        chromeos::DBusThreadManager::Get()->GetConciergeClient());
+  }
+
+  ~PluginVmImageManagerTest() override {
+    chromeos::DBusThreadManager::Shutdown();
+  }
 
  protected:
   chromeos::MockUserManager user_manager_;
@@ -91,6 +94,8 @@
   std::unique_ptr<MockObserver> observer_;
   base::FilePath fake_downloaded_plugin_vm_image_archive_;
   std::unique_ptr<base::HistogramTester> histogram_tester_;
+  // Owned by chromeos::DBusThreadManager
+  chromeos::FakeConciergeClient* fake_concierge_client_;
 
   void SetUp() override {
     ASSERT_TRUE(profiles_dir_.CreateUniqueTempDir());
@@ -142,56 +147,26 @@
     plugin_vm_image->SetKey("hash", base::Value(hash));
   }
 
-  void ProcessImageUntilUnzipping() {
+  void ProcessImageUntilImporting() {
     manager_->StartDownload();
     test_browser_thread_bundle_.RunUntilIdle();
   }
 
-  void ProcessImageUntilRegistration() {
-    ProcessImageUntilUnzipping();
+  void ProcessImageUntilConfigured() {
+    ProcessImageUntilImporting();
 
     // Faking downloaded file for testing.
     manager_->SetDownloadedPluginVmImageArchiveForTesting(
         fake_downloaded_plugin_vm_image_archive_);
-    manager_->StartUnzipping();
+    manager_->StartImport();
     test_browser_thread_bundle_.RunUntilIdle();
   }
 
-  void ProcessImageUntilConfigured() {
-    ProcessImageUntilRegistration();
-
-    manager_->StartRegistration();
-    test_browser_thread_bundle_.RunUntilIdle();
-  }
-
-  void EnsurePluginVmImageIsRemoved() {
-    base::FilePath plugin_vm_image_unzipped =
-        profile_->GetPath()
-            .AppendASCII(kCrosvmDir)
-            .AppendASCII(kPvmDir)
-            .AppendASCII(kPluginVmImageDir);
-    EXPECT_FALSE(base::DirectoryExists(plugin_vm_image_unzipped));
-  }
-
   base::FilePath CreateZipFile() {
-    base::FilePath src_dir = profile_->GetPath().AppendASCII("src");
-    base::CreateDirectory(src_dir);
-    base::FilePath dest_dir = profile_->GetPath().AppendASCII("dest");
-    base::CreateDirectory(dest_dir);
-    base::FilePath zip_file = dest_dir.Append("out");
-
-    base::FilePath plugin_vm_image_unzipped =
-        src_dir.Append(kPluginVmImageUnzipped);
-    base::CreateDirectory(plugin_vm_image_unzipped);
-    base::FilePath plugin_vm_image_file_1 =
-        plugin_vm_image_unzipped.Append(kPluginVmImageFile1);
-    base::FilePath plugin_vm_image_file_2 =
-        plugin_vm_image_unzipped.Append(kPluginVmImageFile2);
-    base::WriteFile(plugin_vm_image_file_1, kContent1, strlen(kContent1));
-    base::WriteFile(plugin_vm_image_file_2, kContent2, strlen(kContent2));
-
-    zip::Zip(src_dir, zip_file, true /* include_hidden_files */);
-    return zip_file;
+    base::FilePath zip_file_path =
+        profile_->GetPath().AppendASCII(kPluginVmImageFile);
+    base::WriteFile(zip_file_path, kContent, strlen(kContent));
+    return zip_file_path;
   }
 
  private:
@@ -212,13 +187,11 @@
 };
 
 TEST_F(PluginVmImageManagerTest, DownloadPluginVmImageParamsTest) {
+  SetupConciergeForSuccessfulDiskImageImport(fake_concierge_client_);
+
   EXPECT_CALL(*observer_, OnDownloadCompleted());
-  EXPECT_CALL(*observer_, OnUnzippingProgressUpdated(kContent1Size,
-                                                     kContentSize, testing::_));
-  EXPECT_CALL(*observer_, OnUnzippingProgressUpdated(kContentSize, kContentSize,
-                                                     testing::_));
-  EXPECT_CALL(*observer_, OnUnzipped());
-  EXPECT_CALL(*observer_, OnRegistered());
+  EXPECT_CALL(*observer_, OnImportProgressUpdated(50.0, _));
+  EXPECT_CALL(*observer_, OnImported());
 
   manager_->StartDownload();
 
@@ -235,20 +208,16 @@
   // Faking downloaded file for testing.
   manager_->SetDownloadedPluginVmImageArchiveForTesting(
       fake_downloaded_plugin_vm_image_archive_);
-  manager_->StartUnzipping();
-  test_browser_thread_bundle_.RunUntilIdle();
-  manager_->StartRegistration();
+  manager_->StartImport();
   test_browser_thread_bundle_.RunUntilIdle();
 }
 
 TEST_F(PluginVmImageManagerTest, OnlyOneImageIsProcessedTest) {
+  SetupConciergeForSuccessfulDiskImageImport(fake_concierge_client_);
+
   EXPECT_CALL(*observer_, OnDownloadCompleted());
-  EXPECT_CALL(*observer_, OnUnzippingProgressUpdated(kContent1Size,
-                                                     kContentSize, testing::_));
-  EXPECT_CALL(*observer_, OnUnzippingProgressUpdated(kContentSize, kContentSize,
-                                                     testing::_));
-  EXPECT_CALL(*observer_, OnUnzipped());
-  EXPECT_CALL(*observer_, OnRegistered());
+  EXPECT_CALL(*observer_, OnImportProgressUpdated(50.0, _));
+  EXPECT_CALL(*observer_, OnImported());
 
   manager_->StartDownload();
 
@@ -261,15 +230,7 @@
 
   EXPECT_TRUE(manager_->IsProcessingImage());
 
-  manager_->StartUnzipping();
-
-  EXPECT_TRUE(manager_->IsProcessingImage());
-
-  test_browser_thread_bundle_.RunUntilIdle();
-
-  EXPECT_TRUE(manager_->IsProcessingImage());
-
-  manager_->StartRegistration();
+  manager_->StartImport();
 
   EXPECT_TRUE(manager_->IsProcessingImage());
 
@@ -282,21 +243,17 @@
 }
 
 TEST_F(PluginVmImageManagerTest, CanProceedWithANewImageWhenSucceededTest) {
+  SetupConciergeForSuccessfulDiskImageImport(fake_concierge_client_);
+
   EXPECT_CALL(*observer_, OnDownloadCompleted()).Times(2);
-  EXPECT_CALL(*observer_, OnUnzippingProgressUpdated(kContent1Size,
-                                                     kContentSize, testing::_))
-      .Times(2);
-  EXPECT_CALL(*observer_, OnUnzippingProgressUpdated(kContentSize, kContentSize,
-                                                     testing::_))
-      .Times(2);
-  EXPECT_CALL(*observer_, OnUnzipped()).Times(2);
-  EXPECT_CALL(*observer_, OnRegistered()).Times(2);
+  EXPECT_CALL(*observer_, OnImportProgressUpdated(50.0, _)).Times(2);
+  EXPECT_CALL(*observer_, OnImported()).Times(2);
 
   ProcessImageUntilConfigured();
 
   EXPECT_FALSE(manager_->IsProcessingImage());
 
-  // As it is deleted after successful unzipping.
+  // As it is deleted after successful importing.
   fake_downloaded_plugin_vm_image_archive_ = CreateZipFile();
   ProcessImageUntilConfigured();
 
@@ -305,14 +262,12 @@
 }
 
 TEST_F(PluginVmImageManagerTest, CanProceedWithANewImageWhenFailedTest) {
+  SetupConciergeForSuccessfulDiskImageImport(fake_concierge_client_);
+
   EXPECT_CALL(*observer_, OnDownloadFailed());
   EXPECT_CALL(*observer_, OnDownloadCompleted());
-  EXPECT_CALL(*observer_, OnUnzippingProgressUpdated(kContent1Size,
-                                                     kContentSize, testing::_));
-  EXPECT_CALL(*observer_, OnUnzippingProgressUpdated(kContentSize, kContentSize,
-                                                     testing::_));
-  EXPECT_CALL(*observer_, OnUnzipped());
-  EXPECT_CALL(*observer_, OnRegistered());
+  EXPECT_CALL(*observer_, OnImportProgressUpdated(50.0, _));
+  EXPECT_CALL(*observer_, OnImported());
 
   manager_->StartDownload();
   std::string guid = manager_->GetCurrentDownloadGuidForTesting();
@@ -340,105 +295,22 @@
   histogram_tester_->ExpectTotalCount(kPluginVmImageDownloadedSize, 0);
 }
 
-TEST_F(PluginVmImageManagerTest, UnzipDownloadedImageTest) {
+TEST_F(PluginVmImageManagerTest, ImportNonExistingImageTest) {
+  SetupConciergeForSuccessfulDiskImageImport(fake_concierge_client_);
+
   EXPECT_CALL(*observer_, OnDownloadCompleted());
-  EXPECT_CALL(*observer_, OnUnzippingProgressUpdated(kContent1Size,
-                                                     kContentSize, testing::_));
-  EXPECT_CALL(*observer_, OnUnzippingProgressUpdated(kContentSize, kContentSize,
-                                                     testing::_));
-  EXPECT_CALL(*observer_, OnUnzipped());
-  EXPECT_CALL(*observer_, OnRegistered());
+  EXPECT_CALL(*observer_, OnImportFailed());
 
-  ProcessImageUntilConfigured();
-
-  // Checking that all files are in place.
-  base::FilePath plugin_vm_image_unzipped =
-      profile_->GetPath()
-          .AppendASCII(kCrosvmDir)
-          .AppendASCII(kPvmDir)
-          .AppendASCII(kPluginVmImageDir)
-          .AppendASCII(kPluginVmImageUnzipped);
-  EXPECT_TRUE(base::DirectoryExists(plugin_vm_image_unzipped));
-  base::FilePath plugin_vm_image_file_1 =
-      plugin_vm_image_unzipped.AppendASCII(kPluginVmImageFile1);
-  EXPECT_TRUE(base::PathExists(plugin_vm_image_file_1));
-  EXPECT_FALSE(base::DirectoryExists(plugin_vm_image_file_1));
-  std::string plugin_vm_image_file_1_content;
-  EXPECT_TRUE(base::ReadFileToString(plugin_vm_image_file_1,
-                                     &plugin_vm_image_file_1_content));
-  EXPECT_EQ(kContent1, plugin_vm_image_file_1_content);
-  base::FilePath plugin_vm_image_file_2 =
-      plugin_vm_image_unzipped.AppendASCII(kPluginVmImageFile2);
-  EXPECT_TRUE(base::PathExists(plugin_vm_image_file_2));
-  EXPECT_FALSE(base::DirectoryExists(plugin_vm_image_file_2));
-  std::string plugin_vm_image_file_2_content;
-  EXPECT_TRUE(base::ReadFileToString(plugin_vm_image_file_2,
-                                     &plugin_vm_image_file_2_content));
-  EXPECT_EQ(kContent2, plugin_vm_image_file_2_content);
-}
-
-TEST_F(PluginVmImageManagerTest, UnzipNonExistingImageTest) {
-  EXPECT_CALL(*observer_, OnDownloadCompleted());
-  EXPECT_CALL(*observer_, OnUnzippingFailed());
-
-  ProcessImageUntilUnzipping();
+  ProcessImageUntilImporting();
   // Should fail as fake downloaded file isn't set.
-  manager_->StartUnzipping();
+  manager_->StartImport();
   test_browser_thread_bundle_.RunUntilIdle();
-
-  EnsurePluginVmImageIsRemoved();
-}
-
-TEST_F(PluginVmImageManagerTest, CancelUnzippingTest) {
-  EXPECT_CALL(*observer_, OnDownloadCompleted());
-  EXPECT_CALL(*observer_, OnUnzippingProgressUpdated(kContent1Size,
-                                                     kContentSize, testing::_));
-  EXPECT_CALL(*observer_, OnUnzippingFailed());
-
-  ProcessImageUntilUnzipping();
-
-  // Faking downloaded file for testing.
-  manager_->SetDownloadedPluginVmImageArchiveForTesting(
-      fake_downloaded_plugin_vm_image_archive_);
-  manager_->StartUnzipping();
-  manager_->CancelUnzipping();
-  test_browser_thread_bundle_.RunUntilIdle();
-
-  EnsurePluginVmImageIsRemoved();
-
-  histogram_tester_->ExpectUniqueSample(kPluginVmImageDownloadedSize,
-                                        kDownloadedPluginVmImageSizeInMb, 1);
-}
-
-TEST_F(PluginVmImageManagerTest, CancelRegistrationTest) {
-  EXPECT_CALL(*observer_, OnDownloadCompleted());
-  EXPECT_CALL(*observer_, OnUnzippingProgressUpdated(kContent1Size,
-                                                     kContentSize, testing::_));
-  EXPECT_CALL(*observer_, OnUnzippingProgressUpdated(kContentSize, kContentSize,
-                                                     testing::_));
-  EXPECT_CALL(*observer_, OnUnzipped());
-  EXPECT_CALL(*observer_, OnRegistered()).Times(0);
-  EXPECT_CALL(*observer_, OnRegistrationFailed()).Times(0);
-
-  ProcessImageUntilRegistration();
-
-  manager_->StartRegistration();
-  manager_->CancelRegistration();
-  test_browser_thread_bundle_.RunUntilIdle();
-
-  EnsurePluginVmImageIsRemoved();
-
-  histogram_tester_->ExpectUniqueSample(kPluginVmImageDownloadedSize,
-                                        kDownloadedPluginVmImageSizeInMb, 1);
 }
 
 TEST_F(PluginVmImageManagerTest, EmptyPluginVmImageUrlTest) {
   SetPluginVmImagePref("", kHash);
-
   EXPECT_CALL(*observer_, OnDownloadFailed());
-
-  ProcessImageUntilUnzipping();
-
+  ProcessImageUntilImporting();
   histogram_tester_->ExpectTotalCount(kPluginVmImageDownloadedSize, 0);
 }
 
@@ -452,7 +324,7 @@
 TEST_F(PluginVmImageManagerTest, CannotStartDownloadIfPluginVmGetsDisabled) {
   settings_helper_.SetBoolean(chromeos::kPluginVmAllowed, false);
   EXPECT_CALL(*observer_, OnDownloadFailed());
-  ProcessImageUntilUnzipping();
+  ProcessImageUntilImporting();
 }
 
 }  // namespace plugin_vm
diff --git a/chrome/browser/chromeos/plugin_vm/plugin_vm_manager.cc b/chrome/browser/chromeos/plugin_vm/plugin_vm_manager.cc
index ea41b0c..1035970 100644
--- a/chrome/browser/chromeos/plugin_vm/plugin_vm_manager.cc
+++ b/chrome/browser/chromeos/plugin_vm/plugin_vm_manager.cc
@@ -16,8 +16,6 @@
 
 namespace {
 
-constexpr char kPluginVmDefaultName[] = "PvmDefault";
-
 class PluginVmManagerFactory : public BrowserContextKeyedServiceFactory {
  public:
   static PluginVmManager* GetForProfile(Profile* profile) {
@@ -50,6 +48,8 @@
 
 }  // namespace
 
+const char kPluginVmDefaultName[] = "PvmDefault";
+
 PluginVmManager* PluginVmManager::GetForProfile(Profile* profile) {
   return PluginVmManagerFactory::GetForProfile(profile);
 }
diff --git a/chrome/browser/chromeos/plugin_vm/plugin_vm_manager.h b/chrome/browser/chromeos/plugin_vm/plugin_vm_manager.h
index 1ed5d50..cea291c 100644
--- a/chrome/browser/chromeos/plugin_vm/plugin_vm_manager.h
+++ b/chrome/browser/chromeos/plugin_vm/plugin_vm_manager.h
@@ -16,6 +16,8 @@
 
 namespace plugin_vm {
 
+extern const char kPluginVmDefaultName[];
+
 // The PluginVmManager is responsible for connecting to the D-Bus services to
 // manage the Plugin Vm.
 class PluginVmManager : public KeyedService {
diff --git a/chrome/browser/chromeos/plugin_vm/plugin_vm_test_helper.cc b/chrome/browser/chromeos/plugin_vm/plugin_vm_test_helper.cc
index 828e238..2318b20 100644
--- a/chrome/browser/chromeos/plugin_vm/plugin_vm_test_helper.cc
+++ b/chrome/browser/chromeos/plugin_vm/plugin_vm_test_helper.cc
@@ -16,6 +16,44 @@
 
 namespace plugin_vm {
 
+namespace {
+const char kDiskImageImportCommandUuid[] = "3922722bd7394acf85bf4d5a330d4a47";
+}  // namespace
+
+void SetupConciergeForSuccessfulDiskImageImport(
+    chromeos::FakeConciergeClient* fake_concierge_client_) {
+  // Set immediate response for the ImportDiskImage call: will be that "image is
+  // in progress":
+  vm_tools::concierge::ImportDiskImageResponse import_disk_image_response;
+  import_disk_image_response.set_status(
+      vm_tools::concierge::DISK_STATUS_IN_PROGRESS);
+  import_disk_image_response.set_command_uuid(kDiskImageImportCommandUuid);
+  fake_concierge_client_->set_import_disk_image_response(
+      import_disk_image_response);
+
+  // Set a series of signals: one at 50% (in progress) and one at 100%
+  // (created):
+  std::vector<vm_tools::concierge::DiskImageStatusResponse> signals;
+  vm_tools::concierge::DiskImageStatusResponse signal1;
+  signal1.set_status(vm_tools::concierge::DISK_STATUS_IN_PROGRESS);
+  signal1.set_progress(50);
+  signal1.set_command_uuid(kDiskImageImportCommandUuid);
+  vm_tools::concierge::DiskImageStatusResponse signal2;
+  signal2.set_status(vm_tools::concierge::DISK_STATUS_CREATED);
+  signal2.set_progress(100);
+  signal2.set_command_uuid(kDiskImageImportCommandUuid);
+  fake_concierge_client_->set_disk_image_status_signals({signal1, signal2});
+
+  // Finally, set a success response for any eventual final call to
+  // DiskImageStatus:
+  vm_tools::concierge::DiskImageStatusResponse disk_image_status_response;
+  disk_image_status_response.set_status(
+      vm_tools::concierge::DISK_STATUS_CREATED);
+  disk_image_status_response.set_command_uuid(kDiskImageImportCommandUuid);
+  fake_concierge_client_->set_disk_image_status_response(
+      disk_image_status_response);
+}
+
 PluginVmTestHelper::PluginVmTestHelper(TestingProfile* testing_profile)
     : testing_profile_(testing_profile) {
   testing_profile_->ScopedCrosSettingsTestHelper()
diff --git a/chrome/browser/chromeos/plugin_vm/plugin_vm_test_helper.h b/chrome/browser/chromeos/plugin_vm/plugin_vm_test_helper.h
index 76100b2..35572e36 100644
--- a/chrome/browser/chromeos/plugin_vm/plugin_vm_test_helper.h
+++ b/chrome/browser/chromeos/plugin_vm/plugin_vm_test_helper.h
@@ -6,11 +6,15 @@
 #define CHROME_BROWSER_CHROMEOS_PLUGIN_VM_PLUGIN_VM_TEST_HELPER_H_
 
 #include "chrome/browser/chromeos/login/users/mock_user_manager.h"
+#include "chromeos/dbus/fake_concierge_client.h"
 
 class TestingProfile;
 
 namespace plugin_vm {
 
+void SetupConciergeForSuccessfulDiskImageImport(
+    chromeos::FakeConciergeClient* fake_concierge_client_);
+
 // A helper class for enabling Plugin VM in unit tests.
 class PluginVmTestHelper {
  public:
diff --git a/chrome/browser/chromeos/system_logs/OWNERS b/chrome/browser/chromeos/system_logs/OWNERS
index 806a1ce..9fe2069 100644
--- a/chrome/browser/chromeos/system_logs/OWNERS
+++ b/chrome/browser/chromeos/system_logs/OWNERS
@@ -1,4 +1,3 @@
 afakhry@chromium.org
-rkc@chromium.org
 
 # COMPONENT: Platform>Apps>Feedback
diff --git a/chrome/browser/content_settings/host_content_settings_map_factory.cc b/chrome/browser/content_settings/host_content_settings_map_factory.cc
index 9f8d478..1e31094 100644
--- a/chrome/browser/content_settings/host_content_settings_map_factory.cc
+++ b/chrome/browser/content_settings/host_content_settings_map_factory.cc
@@ -82,7 +82,8 @@
   }
 
   scoped_refptr<HostContentSettingsMap> settings_map(new HostContentSettingsMap(
-      profile->GetPrefs(), profile->IsIncognito(), profile->IsGuestSession(),
+      profile->GetPrefs(), profile->IsIncognito(),
+      profile->GetProfileType() == Profile::GUEST_PROFILE,
       /*store_last_modified=*/true,
       base::FeatureList::IsEnabled(features::kPermissionDelegation)));
 
diff --git a/chrome/browser/extensions/api/bluetooth_low_energy/OWNERS b/chrome/browser/extensions/api/bluetooth_low_energy/OWNERS
index 6a2cb03..9591b06 100644
--- a/chrome/browser/extensions/api/bluetooth_low_energy/OWNERS
+++ b/chrome/browser/extensions/api/bluetooth_low_energy/OWNERS
@@ -1 +1 @@
-rkc@chromium.org
+ortuno@chromium.org
diff --git a/chrome/browser/extensions/api/feedback_private/OWNERS b/chrome/browser/extensions/api/feedback_private/OWNERS
index c32d770..bd30e9f 100644
--- a/chrome/browser/extensions/api/feedback_private/OWNERS
+++ b/chrome/browser/extensions/api/feedback_private/OWNERS
@@ -1,5 +1,4 @@
 afakhry@chromium.org
-rkc@chromium.org
 jkardatzke@chromium.org
 
 # COMPONENT: Platform>Apps>Feedback
diff --git a/chrome/browser/extensions/api/identity/identity_api_unittest.cc b/chrome/browser/extensions/api/identity/identity_api_unittest.cc
index 00f22ff..06ba8e1 100644
--- a/chrome/browser/extensions/api/identity/identity_api_unittest.cc
+++ b/chrome/browser/extensions/api/identity/identity_api_unittest.cc
@@ -4,11 +4,9 @@
 
 #include "chrome/browser/extensions/api/identity/identity_api.h"
 
-#include <memory>
-
 #include "base/test/scoped_feature_list.h"
 #include "build/build_config.h"
-#include "chrome/browser/signin/account_consistency_mode_manager_test_util.h"
+#include "chrome/browser/signin/scoped_account_consistency.h"
 #include "chrome/test/base/testing_profile.h"
 #include "components/signin/core/browser/signin_buildflags.h"
 #include "content/public/test/test_browser_thread_bundle.h"
@@ -24,7 +22,7 @@
   feature_list.InitAndEnableFeature(kExtensionsAllAccountsFeature);
 
   {
-    // Dice is enabled by default on new profiles.
+    ScopedAccountConsistencyDice scoped_dice;
     TestingProfile profile;
     IdentityAPI api(&profile);
     EXPECT_FALSE(api.AreExtensionsRestrictedToPrimaryAccount());
@@ -32,8 +30,12 @@
   }
 
   {
-    std::unique_ptr<TestingProfile> pre_dice_profile = BuildPreDiceProfile();
-    IdentityAPI api(pre_dice_profile.get());
+    ScopedAccountConsistencyDiceMigration scoped_dice_migration;
+    TestingProfile::Builder profile_builder;
+    // The profile is not a new profile to prevent automatic migration.
+    profile_builder.OverrideIsNewProfile(false);
+    std::unique_ptr<TestingProfile> profile = profile_builder.Build();
+    IdentityAPI api(profile.get());
     EXPECT_TRUE(api.AreExtensionsRestrictedToPrimaryAccount());
     api.Shutdown();
   }
diff --git a/chrome/browser/extensions/default_apps.cc b/chrome/browser/extensions/default_apps.cc
index b5fe700..46b80da 100644
--- a/chrome/browser/extensions/default_apps.cc
+++ b/chrome/browser/extensions/default_apps.cc
@@ -14,7 +14,6 @@
 #include "base/stl_util.h"
 #include "base/strings/string_util.h"
 #include "chrome/browser/browser_process.h"
-#include "chrome/browser/first_run/first_run.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/common/chrome_switches.h"
 #include "chrome/common/extensions/extension_constants.h"
@@ -77,13 +76,9 @@
 
   switch (state) {
     case kUnknown: {
-      // Only new installations and profiles get default apps. In theory the
-      // new profile checks should catch new installations, but that is not
-      // always the case (http:/crbug.com/145351).
       bool is_new_profile = profile_->WasCreatedByVersionOrLater(
           version_info::GetVersionNumber());
-      bool is_first_run = first_run::IsChromeFirstRun();
-      if (!is_first_run && !is_new_profile)
+      if (!is_new_profile)
         install_apps = false;
       break;
     }
diff --git a/chrome/browser/feedback/OWNERS b/chrome/browser/feedback/OWNERS
index 54caa5f..d10d080 100644
--- a/chrome/browser/feedback/OWNERS
+++ b/chrome/browser/feedback/OWNERS
@@ -1,5 +1,4 @@
 afakhry@chromium.org
-rkc@chromium.org
 zork@chromium.org
 
 # COMPONENT: Platform>Apps>Feedback
diff --git a/chrome/browser/flag-metadata.json b/chrome/browser/flag-metadata.json
index 31c253c..f24c991 100644
--- a/chrome/browser/flag-metadata.json
+++ b/chrome/browser/flag-metadata.json
@@ -1553,8 +1553,8 @@
   },
   {
     "name": "enable-suggestions-with-substring-match",
-    // "owners": [ "your-team" ],
-    "expiry_milestone": 76
+    "owners": [ "tmartino" ],
+    "expiry_milestone": 77
   },
   {
     "name": "enable-surfacecontrol",
@@ -1711,11 +1711,6 @@
     "expiry_milestone": 80
   },
   {
-    "name": "enable-usermedia-screen-capturing",
-    // "owners": [ "your-team" ],
-    "expiry_milestone": 76
-  },
-  {
     "name": "enable-vaapi-jpeg-image-decode-acceleration",
     "owners": [ "chromeos-camera-eng@google.com" ],
     "expiry_milestone": 76
@@ -1813,8 +1808,8 @@
   },
   {
     "name": "enable-webrtc-hide-local-ips-with-mdns",
-    // "owners": [ "your-team" ],
-    "expiry_milestone": 76
+    "owners": [ "qingsi", "jeroendb" ],
+    "expiry_milestone": 78
   },
   {
     "name": "enable-webrtc-hw-h264-encoding",
@@ -1858,8 +1853,8 @@
   },
   {
     "name": "enable-webrtc-stun-origin",
-    // "owners": [ "your-team" ],
-    "expiry_milestone": 76
+    "owners": [ "qingsi", "jeroendb" ],
+    "expiry_milestone": 78
   },
   {
     "name": "enable-webvr",
@@ -2113,11 +2108,6 @@
     "expiry_milestone": -1
   },
   {
-    "name": "in-session-password-change",
-    "owners": [ "rsorokin" ],
-    "expiry_milestone": 77
-  },
-  {
     "name": "incognito-strings",
     // "owners": [ "your-team" ],
     "expiry_milestone": 76
diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc
index 338f805..ec07d37 100644
--- a/chrome/browser/flag_descriptions.cc
+++ b/chrome/browser/flag_descriptions.cc
@@ -2301,10 +2301,6 @@
     "Whether Chrome should offer users the option to manually request to "
     "generate passwords on Android.";
 
-const char kMediaScreenCaptureName[] = "Experimental ScreenCapture.";
-const char kMediaScreenCaptureDescription[] =
-    "Enable this option for experimental ScreenCapture feature on Android.";
-
 const char kNewNetErrorPageUIName[] = "Enable new UI for net-error page";
 const char kNewNetErrorPageUIDescription[] =
     "Enables showing available offline content on the net-error (Dino) page.";
@@ -3257,13 +3253,6 @@
     "Enables the redesigned notification stacking bar UI with a \"Clear all\" "
     "button.";
 
-extern const char kInSessionPasswordChangeName[] =
-    "Enable user password change in the session";
-
-extern const char kInSessionPasswordChangeDescription[] =
-    "Let user change both their IdP password and Chromebook password on "
-    "chrome://password-change page.";
-
 #endif  // defined(OS_CHROMEOS)
 
 // All views-based platforms --------------------------------------------------
diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h
index 4c32335..2837f15 100644
--- a/chrome/browser/flag_descriptions.h
+++ b/chrome/browser/flag_descriptions.h
@@ -1376,9 +1376,6 @@
 extern const char kManualPasswordGenerationAndroidName[];
 extern const char kManualPasswordGenerationAndroidDescription[];
 
-extern const char kMediaScreenCaptureName[];
-extern const char kMediaScreenCaptureDescription[];
-
 extern const char kNewNetErrorPageUIName[];
 extern const char kNewNetErrorPageUIDescription[];
 
@@ -1963,9 +1960,6 @@
 extern const char kAshNotificationStackingBarRedesignName[];
 extern const char kAshNotificationStackingBarRedesignDescription[];
 
-extern const char kInSessionPasswordChangeName[];
-extern const char kInSessionPasswordChangeDescription[];
-
 #endif  // #if defined(OS_CHROMEOS)
 
 // All views-based platforms --------------------------------------------------
diff --git a/chrome/browser/media/media_engagement_autoplay_browsertest.cc b/chrome/browser/media/media_engagement_autoplay_browsertest.cc
index 01375f2..b9920a0 100644
--- a/chrome/browser/media/media_engagement_autoplay_browsertest.cc
+++ b/chrome/browser/media/media_engagement_autoplay_browsertest.cc
@@ -367,6 +367,16 @@
   ExpectAutoplayAllowedIfEnabled();
 }
 
+IN_PROC_BROWSER_TEST_P(MediaEngagementAutoplayBrowserTest,
+                       BypassAutoplayHighEngagement_HTTPSOnly) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(media::kMediaEngagementHTTPSOnly);
+
+  SetScores(PrimaryOrigin(), 20, 20);
+  LoadTestPage("engagement_autoplay_test.html");
+  ExpectAutoplayDenied();
+}
+
 INSTANTIATE_TEST_SUITE_P(/* no prefix */,
                          MediaEngagementAutoplayBrowserTest,
                          testing::Bool());
diff --git a/chrome/browser/media/media_engagement_contents_observer.cc b/chrome/browser/media/media_engagement_contents_observer.cc
index 0814fc7..d637ecd 100644
--- a/chrome/browser/media/media_engagement_contents_observer.cc
+++ b/chrome/browser/media/media_engagement_contents_observer.cc
@@ -581,6 +581,9 @@
   MediaEngagementScore score = service_->CreateEngagementScore(origin);
   bool has_high_engagement = score.high_score();
 
+  if (base::FeatureList::IsEnabled(media::kMediaEngagementHTTPSOnly))
+    DCHECK(!has_high_engagement || (origin.scheme() == url::kHttpsScheme));
+
   // If the preloaded feature flag is enabled and the number of visits is less
   // than the number of visits required to have an MEI score we should check the
   // global data.
diff --git a/chrome/browser/media/media_engagement_score.cc b/chrome/browser/media/media_engagement_score.cc
index 6b889bc..b7ecfac 100644
--- a/chrome/browser/media/media_engagement_score.cc
+++ b/chrome/browser/media/media_engagement_score.cc
@@ -109,6 +109,13 @@
   if (!score_dict_)
     return;
 
+  // This is to prevent using previously saved data to mark an HTTP website as
+  // allowed to autoplay.
+  if (base::FeatureList::IsEnabled(media::kMediaEngagementHTTPSOnly) &&
+      origin_.scheme() != url::kHttpsScheme) {
+    return;
+  }
+
   GetIntegerFromScore(score_dict_.get(), kVisitsKey, &visits_);
   GetIntegerFromScore(score_dict_.get(), kMediaPlaybacksKey, &media_playbacks_);
   GetIntegerFromScore(score_dict_.get(), kAudiblePlaybacksKey,
@@ -209,6 +216,12 @@
   if (!score_dict_)
     return false;
 
+  // This is to prevent saving data that we would otherwise not use.
+  if (base::FeatureList::IsEnabled(media::kMediaEngagementHTTPSOnly) &&
+      origin_.scheme() != url::kHttpsScheme) {
+    return false;
+  }
+
   if (base::Value* value = score_dict_->FindKeyOfType(
           kHasHighScoreKey, base::Value::Type::BOOLEAN)) {
     is_high = value->GetBool();
diff --git a/chrome/browser/media/media_engagement_score_details.mojom b/chrome/browser/media/media_engagement_score_details.mojom
index 2f1ec13..342c65c 100644
--- a/chrome/browser/media/media_engagement_score_details.mojom
+++ b/chrome/browser/media/media_engagement_score_details.mojom
@@ -46,6 +46,7 @@
   bool feature_record_data;
   bool feature_bypass_autoplay;
   bool feature_preload_data;
+  bool feature_https_only;
   bool feature_autoplay_disable_settings;
   bool feature_autoplay_whitelist_settings;
   bool pref_disable_unified_autoplay;
diff --git a/chrome/browser/media/media_engagement_score_unittest.cc b/chrome/browser/media/media_engagement_score_unittest.cc
index a20c41e..0e85003 100644
--- a/chrome/browser/media/media_engagement_score_unittest.cc
+++ b/chrome/browser/media/media_engagement_score_unittest.cc
@@ -108,7 +108,8 @@
       int visits_with_media_tag,
       int high_score_changes,
       int media_element_playbacks,
-      int audio_context_playbacks) {
+      int audio_context_playbacks,
+      bool update_score_expectation) {
     MediaEngagementScore* initial_score =
         new MediaEngagementScore(&test_clock, url::Origin(),
                                  std::move(score_dict), nullptr /* settings */);
@@ -124,7 +125,7 @@
 
     // Increment the scores and check that the values were stored correctly.
     UpdateScore(initial_score);
-    EXPECT_TRUE(initial_score->UpdateScoreDict());
+    EXPECT_EQ(update_score_expectation, initial_score->UpdateScoreDict());
     delete initial_score;
   }
 
@@ -201,7 +202,7 @@
 TEST_F(MediaEngagementScoreTest, EmptyDictionary) {
   std::unique_ptr<base::DictionaryValue> dict(new base::DictionaryValue());
   TestScoreInitializesAndUpdates(std::move(dict), 0, 0, base::Time(), false, 0,
-                                 0, 0, 0, 0, 0);
+                                 0, 0, 0, 0, 0, true);
 }
 
 // Test that scores are read / written correctly from / to partially empty
@@ -211,7 +212,7 @@
   dict->SetInteger(MediaEngagementScore::kVisitsKey, 2);
 
   TestScoreInitializesAndUpdates(std::move(dict), 2, 0, base::Time(), false, 0,
-                                 0, 0, 0, 0, 0);
+                                 0, 0, 0, 0, 0, true);
 }
 
 // Test that scores are read / written correctly from / to populated score
@@ -232,7 +233,7 @@
                    2);
 
   TestScoreInitializesAndUpdates(std::move(dict), 20, 12, test_clock.Now(),
-                                 true, 2, 4, 6, 3, 1, 2);
+                                 true, 2, 4, 6, 3, 1, 2, true);
 }
 
 // Test getting and commiting the score works correctly with different
@@ -722,3 +723,27 @@
     EXPECT_EQ(media_element_playbacks, stored_media_element_playbacks);
   }
 }
+
+// Test that scores are read / written correctly from / to populated score
+// dictionaries.
+TEST_F(MediaEngagementScoreTest, PopulatedDictionary_HTTPSOnly) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(media::kMediaEngagementHTTPSOnly);
+
+  std::unique_ptr<base::DictionaryValue> dict(new base::DictionaryValue());
+  dict->SetInteger(MediaEngagementScore::kVisitsKey, 20);
+  dict->SetInteger(MediaEngagementScore::kMediaPlaybacksKey, 12);
+  dict->SetDouble(MediaEngagementScore::kLastMediaPlaybackTimeKey,
+                  test_clock.Now().ToDeltaSinceWindowsEpoch().InMicroseconds());
+  dict->SetBoolean(MediaEngagementScore::kHasHighScoreKey, true);
+  dict->SetInteger(MediaEngagementScore::kAudiblePlaybacksKey, 2);
+  dict->SetInteger(MediaEngagementScore::kSignificantPlaybacksKey, 4);
+  dict->SetInteger(MediaEngagementScore::kVisitsWithMediaTagKey, 6);
+  dict->SetInteger(MediaEngagementScore::kHighScoreChanges, 3);
+  dict->SetInteger(MediaEngagementScore::kSignificantMediaPlaybacksKey, 1);
+  dict->SetInteger(MediaEngagementScore::kSignificantAudioContextPlaybacksKey,
+                   2);
+
+  TestScoreInitializesAndUpdates(std::move(dict), 0, 0, base::Time(), false, 0,
+                                 0, 0, 0, 0, 0, false);
+}
diff --git a/chrome/browser/media/media_engagement_service.cc b/chrome/browser/media/media_engagement_service.cc
index 3c2a0a6..7383f18 100644
--- a/chrome/browser/media/media_engagement_service.cc
+++ b/chrome/browser/media/media_engagement_service.cc
@@ -330,6 +330,9 @@
 
 bool MediaEngagementService::ShouldRecordEngagement(
     const url::Origin& origin) const {
+  if (base::FeatureList::IsEnabled(media::kMediaEngagementHTTPSOnly))
+    return origin.scheme() == url::kHttpsScheme;
+
   return (origin.scheme() == url::kHttpsScheme ||
           origin.scheme() == url::kHttpScheme);
 }
@@ -358,6 +361,11 @@
       continue;
     }
 
+    if (base::FeatureList::IsEnabled(media::kMediaEngagementHTTPSOnly) &&
+        origin.scheme() != url::kHttpsScheme) {
+      continue;
+    }
+
     const auto& result = filtered_results.find(origin);
     if (result != filtered_results.end()) {
       DCHECK(result->second->incognito && !site.incognito);
diff --git a/chrome/browser/media/media_engagement_service_unittest.cc b/chrome/browser/media/media_engagement_service_unittest.cc
index d2e5797..9c67806 100644
--- a/chrome/browser/media/media_engagement_service_unittest.cc
+++ b/chrome/browser/media/media_engagement_service_unittest.cc
@@ -113,13 +113,23 @@
 
 }  // namespace
 
-class MediaEngagementServiceTest : public ChromeRenderViewHostTestHarness {
+class MediaEngagementServiceTest : public ChromeRenderViewHostTestHarness,
+                                   public testing::WithParamInterface<bool> {
  public:
   void SetUp() override {
     mock_time_task_runner_ =
         base::MakeRefCounted<base::TestMockTimeTaskRunner>();
-    scoped_feature_list_.InitAndEnableFeature(
-        media::kRecordMediaEngagementScores);
+
+    if (GetParam()) {
+      scoped_feature_list_.InitWithFeatures(
+          {media::kRecordMediaEngagementScores,
+           media::kMediaEngagementHTTPSOnly},
+          {});
+    } else {
+      scoped_feature_list_.InitWithFeatures(
+          {media::kRecordMediaEngagementScores},
+          {media::kMediaEngagementHTTPSOnly});
+    }
     ChromeRenderViewHostTestHarness::SetUp();
 
     ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
@@ -279,34 +289,40 @@
   base::test::ScopedFeatureList scoped_feature_list_;
 };
 
-TEST_F(MediaEngagementServiceTest, MojoSerialization) {
+TEST_P(MediaEngagementServiceTest, MojoSerialization) {
   EXPECT_EQ(0u, GetAllScoreDetails().size());
 
   RecordVisitAndPlaybackAndAdvanceClock(
-      url::Origin::Create(GURL("https://www.google.com")));
-  EXPECT_EQ(1u, GetAllScoreDetails().size());
+      url::Origin::Create(GURL("http://www.example.com")));
+  RecordVisitAndPlaybackAndAdvanceClock(
+      url::Origin::Create(GURL("https://www.example.com")));
+
+  EXPECT_EQ(GetParam() ? 1u : 2u, GetAllScoreDetails().size());
 }
 
-TEST_F(MediaEngagementServiceTest, RestrictedToHTTPAndHTTPS) {
-  url::Origin origin1 = url::Origin::Create(GURL("ftp://www.google.com/"));
-  url::Origin origin2 = url::Origin::Create(GURL("file://blah"));
-  url::Origin origin3 = url::Origin::Create(GURL("chrome://"));
-  url::Origin origin4 = url::Origin::Create(GURL("about://config"));
+TEST_P(MediaEngagementServiceTest, RestrictedToHTTPAndHTTPS) {
+  std::vector<url::Origin> origins = {
+      url::Origin::Create(GURL("ftp://www.google.com/")),
+      url::Origin::Create(GURL("file://blah")),
+      url::Origin::Create(GURL("chrome://")),
+      url::Origin::Create(GURL("about://config")),
+      url::Origin::Create(GURL("http://example.com")),
+      url::Origin::Create(GURL("https://example.com")),
+  };
 
-  RecordVisitAndPlaybackAndAdvanceClock(origin1);
-  ExpectScores(origin1, 0.0, 0, 0, TimeNotSet());
+  for (const url::Origin& origin : origins) {
+    RecordVisitAndPlaybackAndAdvanceClock(origin);
 
-  RecordVisitAndPlaybackAndAdvanceClock(origin2);
-  ExpectScores(origin2, 0.0, 0, 0, TimeNotSet());
-
-  RecordVisitAndPlaybackAndAdvanceClock(origin4);
-  ExpectScores(origin4, 0.0, 0, 0, TimeNotSet());
-
-  RecordVisitAndPlaybackAndAdvanceClock(origin4);
-  ExpectScores(origin4, 0.0, 0, 0, TimeNotSet());
+    if (origin.scheme() == url::kHttpsScheme ||
+        (origin.scheme() == url::kHttpScheme && !GetParam())) {
+      ExpectScores(origin, 0.05, 1, 1, Now());
+    } else {
+      ExpectScores(origin, 0.0, 0, 0, TimeNotSet());
+    }
+  }
 }
 
-TEST_F(MediaEngagementServiceTest,
+TEST_P(MediaEngagementServiceTest,
        HandleRecordVisitAndPlaybackAndAdvanceClockion) {
   url::Origin origin1 = url::Origin::Create(GURL("https://www.google.com"));
   ExpectScores(origin1, 0.0, 0, 0, TimeNotSet());
@@ -326,8 +342,8 @@
   ExpectScores(origin1, 0.1, 2, 2, origin1_time);
 }
 
-TEST_F(MediaEngagementServiceTest, IncognitoEngagementService) {
-  url::Origin origin1 = url::Origin::Create(GURL("http://www.google.com/"));
+TEST_P(MediaEngagementServiceTest, IncognitoEngagementService) {
+  url::Origin origin1 = url::Origin::Create(GURL("https://www.google.fr/"));
   url::Origin origin2 = url::Origin::Create(GURL("https://www.google.com/"));
   url::Origin origin3 = url::Origin::Create(GURL("https://drive.google.com/"));
   url::Origin origin4 = url::Origin::Create(GURL("https://maps.google.com/"));
@@ -360,7 +376,7 @@
   ExpectScores(origin4, 0.05, 1, 1, Now());
 }
 
-TEST_F(MediaEngagementServiceTest, IncognitoOverrideRegularProfile) {
+TEST_P(MediaEngagementServiceTest, IncognitoOverrideRegularProfile) {
   const url::Origin kOrigin1 = url::Origin::Create(GURL("https://example.org"));
   const url::Origin kOrigin2 = url::Origin::Create(GURL("https://example.com"));
 
@@ -436,15 +452,15 @@
   }
 }
 
-TEST_F(MediaEngagementServiceTest, CleanupOriginsOnHistoryDeletion) {
-  url::Origin origin1 = url::Origin::Create(GURL("http://www.google.com/"));
+TEST_P(MediaEngagementServiceTest, CleanupOriginsOnHistoryDeletion) {
+  url::Origin origin1 = url::Origin::Create(GURL("https://www.google.com/"));
   url::Origin origin2 = url::Origin::Create(GURL("https://drive.google.com/"));
-  url::Origin origin3 = url::Origin::Create(GURL("http://deleted.com/"));
-  url::Origin origin4 = url::Origin::Create(GURL("http://notdeleted.com"));
+  url::Origin origin3 = url::Origin::Create(GURL("https://deleted.com/"));
+  url::Origin origin4 = url::Origin::Create(GURL("https://notdeleted.com"));
 
-  GURL url1a = GURL("http://www.google.com/search?q=asdf");
-  GURL url1b = GURL("http://www.google.com/maps/search?q=asdf");
-  GURL url3a = GURL("http://deleted.com/test");
+  GURL url1a = GURL("https://www.google.com/search?q=asdf");
+  GURL url1b = GURL("https://www.google.com/maps/search?q=asdf");
+  GURL url3a = GURL("https://deleted.com/test");
 
   // origin1 will have a score that is high enough to not return zero
   // and we will ensure it has the same score. origin2 will have a score
@@ -596,7 +612,7 @@
   }
 }
 
-TEST_F(MediaEngagementServiceTest, CleanUpDatabaseWhenHistoryIsExpired) {
+TEST_P(MediaEngagementServiceTest, CleanUpDatabaseWhenHistoryIsExpired) {
   base::HistogramTester histogram_tester;
 
   // |origin1| will have history that is before the expiry threshold and should
@@ -605,7 +621,7 @@
   // threshold and should be deleted.
   url::Origin origin1 = url::Origin::Create(GURL("https://www.google.com"));
   url::Origin origin2 = url::Origin::Create(GURL("https://drive.google.com"));
-  url::Origin origin3 = url::Origin::Create(GURL("http://deleted.com"));
+  url::Origin origin3 = url::Origin::Create(GURL("https://deleted.com"));
 
   // Populate test MEI data.
   SetScores(origin1, 20, 20);
@@ -647,15 +663,15 @@
       MediaEngagementService::kHistogramClearName, 4, 1);
 }
 
-TEST_F(MediaEngagementServiceTest, CleanUpDatabaseWhenHistoryIsDeleted) {
-  url::Origin origin1 = url::Origin::Create(GURL("http://www.google.com/"));
+TEST_P(MediaEngagementServiceTest, CleanUpDatabaseWhenHistoryIsDeleted) {
+  url::Origin origin1 = url::Origin::Create(GURL("https://www.google.com/"));
   url::Origin origin2 = url::Origin::Create(GURL("https://drive.google.com/"));
-  url::Origin origin3 = url::Origin::Create(GURL("http://deleted.com/"));
-  url::Origin origin4 = url::Origin::Create(GURL("http://notdeleted.com"));
+  url::Origin origin3 = url::Origin::Create(GURL("https://deleted.com/"));
+  url::Origin origin4 = url::Origin::Create(GURL("https://notdeleted.com"));
 
-  GURL url1a = GURL("http://www.google.com/search?q=asdf");
-  GURL url1b = GURL("http://www.google.com/maps/search?q=asdf");
-  GURL url3a = GURL("http://deleted.com/test");
+  GURL url1a = GURL("https://www.google.com/search?q=asdf");
+  GURL url1b = GURL("https://www.google.com/maps/search?q=asdf");
+  GURL url3a = GURL("https://deleted.com/test");
 
   // origin1 will have a score that is high enough to not return zero
   // and we will ensure it has the same score. origin2 will have a score
@@ -730,15 +746,15 @@
   }
 }
 
-TEST_F(MediaEngagementServiceTest, HistoryExpirationIsNoOp) {
-  url::Origin origin1 = url::Origin::Create(GURL("http://www.google.com/"));
+TEST_P(MediaEngagementServiceTest, HistoryExpirationIsNoOp) {
+  url::Origin origin1 = url::Origin::Create(GURL("https://www.google.com/"));
   url::Origin origin2 = url::Origin::Create(GURL("https://drive.google.com/"));
-  url::Origin origin3 = url::Origin::Create(GURL("http://deleted.com/"));
-  url::Origin origin4 = url::Origin::Create(GURL("http://notdeleted.com"));
+  url::Origin origin3 = url::Origin::Create(GURL("https://deleted.com/"));
+  url::Origin origin4 = url::Origin::Create(GURL("https://notdeleted.com"));
 
-  GURL url1a = GURL("http://www.google.com/search?q=asdf");
-  GURL url1b = GURL("http://www.google.com/maps/search?q=asdf");
-  GURL url3a = GURL("http://deleted.com/test");
+  GURL url1a = GURL("https://www.google.com/search?q=asdf");
+  GURL url1b = GURL("https://www.google.com/maps/search?q=asdf");
+  GURL url3a = GURL("https://deleted.com/test");
 
   SetScores(origin1, MediaEngagementScore::GetScoreMinVisits() + 2, 14);
   SetScores(origin2, 2, 1);
@@ -787,7 +803,7 @@
   }
 }
 
-TEST_F(MediaEngagementServiceTest,
+TEST_P(MediaEngagementServiceTest,
        CleanupDataOnSiteDataCleanup_OutsideBoundary) {
   url::Origin origin = url::Origin::Create(GURL("https://www.google.com"));
   base::HistogramTester histogram_tester;
@@ -808,7 +824,7 @@
       MediaEngagementService::kHistogramClearName, 1, 1);
 }
 
-TEST_F(MediaEngagementServiceTest,
+TEST_P(MediaEngagementServiceTest,
        CleanupDataOnSiteDataCleanup_WithinBoundary) {
   url::Origin origin1 = url::Origin::Create(GURL("https://www.google.com"));
   url::Origin origin2 = url::Origin::Create(GURL("https://www.google.co.uk"));
@@ -834,7 +850,7 @@
       MediaEngagementService::kHistogramClearName, 1, 1);
 }
 
-TEST_F(MediaEngagementServiceTest, CleanupDataOnSiteDataCleanup_NoTimeSet) {
+TEST_P(MediaEngagementServiceTest, CleanupDataOnSiteDataCleanup_NoTimeSet) {
   url::Origin origin = url::Origin::Create(GURL("https://www.google.com"));
   base::HistogramTester histogram_tester;
 
@@ -853,7 +869,7 @@
       MediaEngagementService::kHistogramClearName, 1, 1);
 }
 
-TEST_F(MediaEngagementServiceTest, CleanupDataOnSiteDataCleanup_All) {
+TEST_P(MediaEngagementServiceTest, CleanupDataOnSiteDataCleanup_All) {
   url::Origin origin1 = url::Origin::Create(GURL("https://www.google.com"));
   url::Origin origin2 = url::Origin::Create(GURL("https://www.google.co.uk"));
   base::HistogramTester histogram_tester;
@@ -878,7 +894,7 @@
       MediaEngagementService::kHistogramClearName, 0, 1);
 }
 
-TEST_F(MediaEngagementServiceTest, HasHighEngagement) {
+TEST_P(MediaEngagementServiceTest, HasHighEngagement) {
   url::Origin origin1 = url::Origin::Create(GURL("https://www.google.com"));
   url::Origin origin2 = url::Origin::Create(GURL("https://www.google.co.uk"));
   url::Origin origin3 = url::Origin::Create(GURL("https://www.example.com"));
@@ -891,7 +907,7 @@
   EXPECT_FALSE(HasHighEngagement(origin3));
 }
 
-TEST_F(MediaEngagementServiceTest, SchemaVersion_Changed) {
+TEST_P(MediaEngagementServiceTest, SchemaVersion_Changed) {
   url::Origin origin = url::Origin::Create(GURL("https://www.google.com"));
   SetScores(origin, 1, 2);
 
@@ -904,7 +920,7 @@
   new_service->Shutdown();
 }
 
-TEST_F(MediaEngagementServiceTest, SchemaVersion_Same) {
+TEST_P(MediaEngagementServiceTest, SchemaVersion_Same) {
   url::Origin origin = url::Origin::Create(GURL("https://www.google.com"));
   SetScores(origin, 1, 2);
 
@@ -916,6 +932,8 @@
   new_service->Shutdown();
 }
 
+INSTANTIATE_TEST_SUITE_P(, MediaEngagementServiceTest, ::testing::Bool());
+
 class MediaEngagementServiceEnabledTest
     : public ChromeRenderViewHostTestHarness {};
 
diff --git a/chrome/browser/net/variations_http_headers_browsertest.cc b/chrome/browser/net/variations_http_headers_browsertest.cc
index c0d93fb..c96ed96 100644
--- a/chrome/browser/net/variations_http_headers_browsertest.cc
+++ b/chrome/browser/net/variations_http_headers_browsertest.cc
@@ -12,6 +12,7 @@
 #include "base/run_loop.h"
 #include "base/strings/strcat.h"
 #include "base/threading/thread_task_runner_handle.h"
+#include "build/build_config.h"
 #include "chrome/browser/browser_process.h"
 #include "chrome/browser/chrome_browser_main.h"
 #include "chrome/browser/chrome_browser_main_extra_parts.h"
@@ -324,9 +325,17 @@
   EXPECT_FALSE(HasReceivedHeader(GetExampleUrl(), "X-Client-Data"));
 }
 
+#if defined(OS_CHROMEOS)
+// See https://crbug.com/964338
+#define MAYBE_TestStrippingHeadersFromRequestUsingSimpleURLLoaderWithProfileNetworkContext \
+  DISABLED_TestStrippingHeadersFromRequestUsingSimpleURLLoaderWithProfileNetworkContext
+#else
+#define MAYBE_TestStrippingHeadersFromRequestUsingSimpleURLLoaderWithProfileNetworkContext \
+  TestStrippingHeadersFromRequestUsingSimpleURLLoaderWithProfileNetworkContext
+#endif
 IN_PROC_BROWSER_TEST_F(
     VariationsHttpHeadersBrowserTest,
-    TestStrippingHeadersFromRequestUsingSimpleURLLoaderWithProfileNetworkContext) {
+    MAYBE_TestStrippingHeadersFromRequestUsingSimpleURLLoaderWithProfileNetworkContext) {
   GURL url = GetGoogleRedirectUrl1();
 
   auto resource_request = std::make_unique<network::ResourceRequest>();
diff --git a/chrome/browser/offline_pages/offline_page_request_handler_unittest.cc b/chrome/browser/offline_pages/offline_page_request_handler_unittest.cc
index a60ff364..ba22012 100644
--- a/chrome/browser/offline_pages/offline_page_request_handler_unittest.cc
+++ b/chrome/browser/offline_pages/offline_page_request_handler_unittest.cc
@@ -22,6 +22,7 @@
 #include "base/test/metrics/histogram_tester.h"
 #include "base/threading/thread_task_runner_handle.h"
 #include "base/time/default_clock.h"
+#include "build/build_config.h"
 #include "chrome/browser/offline_pages/offline_page_model_factory.h"
 #include "chrome/browser/offline_pages/offline_page_request_interceptor.h"
 #include "chrome/browser/offline_pages/offline_page_tab_helper.h"
@@ -58,6 +59,11 @@
 #include "testing/gtest/include/gtest/gtest.h"
 #include "url/gurl.h"
 
+#if defined(OS_ANDROID)
+#include "components/gcm_driver/instance_id/instance_id_android.h"
+#include "components/gcm_driver/instance_id/scoped_use_fake_instance_id_android.h"
+#endif  // OS_ANDROID
+
 namespace offline_pages {
 
 namespace {
@@ -547,6 +553,18 @@
   OfflinePageItem page_;
   OfflinePageHeader offline_page_header_;
 
+#if defined(OS_ANDROID)
+  // OfflinePageTabHelper instantiates PrefetchService which in turn requests a
+  // fresh GCM token automatically. These two lines mock out InstanceID (the
+  // component which actually requests the token from play services). Without
+  // this, each test takes an extra 30s waiting on the token (because
+  // content::TestBrowserThreadBundle tries to finish all pending tasks before
+  // ending the test).
+  instance_id::InstanceIDAndroid::ScopedBlockOnAsyncTasksForTesting
+      block_async_;
+  instance_id::ScopedUseFakeInstanceIDAndroid use_fake_;
+#endif  // OS_ANDROID
+
   // These are not thread-safe. But they can be used in the pattern that
   // setting the state is done first from one thread and reading this state
   // can be from any other thread later.
diff --git a/chrome/browser/offline_pages/offline_page_utils_unittest.cc b/chrome/browser/offline_pages/offline_page_utils_unittest.cc
index fb35003..cd4e214 100644
--- a/chrome/browser/offline_pages/offline_page_utils_unittest.cc
+++ b/chrome/browser/offline_pages/offline_page_utils_unittest.cc
@@ -45,6 +45,8 @@
 
 #if defined(OS_ANDROID)
 #include "chrome/browser/android/download/mock_download_controller.h"
+#include "components/gcm_driver/instance_id/instance_id_android.h"
+#include "components/gcm_driver/instance_id/scoped_use_fake_instance_id_android.h"
 #endif
 
 namespace offline_pages {
@@ -133,6 +135,15 @@
   int64_t last_cache_size_;
 #if defined(OS_ANDROID)
   chrome::android::MockDownloadController download_controller_;
+  // OfflinePageTabHelper instantiates PrefetchService which in turn requests a
+  // fresh GCM token automatically. These two lines mock out InstanceID (the
+  // component which actually requests the token from play services). Without
+  // this, each test takes an extra 30s waiting on the token (because
+  // content::TestBrowserThreadBundle tries to finish all pending tasks before
+  // ending the test).
+  instance_id::InstanceIDAndroid::ScopedBlockOnAsyncTasksForTesting
+      block_async_;
+  instance_id::ScopedUseFakeInstanceIDAndroid use_fake_;
 #endif
 };
 
diff --git a/chrome/browser/offline_pages/prefetch/prefetch_service_factory.cc b/chrome/browser/offline_pages/prefetch/prefetch_service_factory.cc
index a2c852f..486805f 100644
--- a/chrome/browser/offline_pages/prefetch/prefetch_service_factory.cc
+++ b/chrome/browser/offline_pages/prefetch/prefetch_service_factory.cc
@@ -7,10 +7,12 @@
 #include <memory>
 #include <utility>
 
+#include "base/bind_helpers.h"
 #include "base/files/file_path.h"
 #include "base/memory/singleton.h"
-#include "base/sequenced_task_runner.h"
 #include "base/task/post_task.h"
+#include "base/task/task_traits.h"
+#include "base/threading/sequenced_task_runner_handle.h"
 #include "chrome/browser/chrome_content_browser_client.h"
 #include "chrome/browser/download/download_service_factory.h"
 #include "chrome/browser/image_fetcher/image_fetcher_service_factory.h"
@@ -22,6 +24,7 @@
 #include "chrome/browser/offline_pages/prefetch/prefetch_instance_id_proxy.h"
 #include "chrome/browser/offline_pages/prefetch/thumbnail_fetcher_impl.h"
 #include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/transition_manager/full_browser_transition_manager.h"
 #include "chrome/common/channel_info.h"
 #include "chrome/common/chrome_constants.h"
 #include "components/feed/feed_feature_list.h"
@@ -29,6 +32,7 @@
 #include "components/image_fetcher/core/image_fetcher_impl.h"
 #include "components/image_fetcher/core/image_fetcher_service.h"
 #include "components/keyed_service/content/browser_context_dependency_manager.h"
+#include "components/offline_pages/core/offline_page_feature.h"
 #include "components/offline_pages/core/prefetch/prefetch_dispatcher_impl.h"
 #include "components/offline_pages/core/prefetch/prefetch_downloader_impl.h"
 #include "components/offline_pages/core/prefetch/prefetch_gcm_app_handler.h"
@@ -38,10 +42,35 @@
 #include "components/offline_pages/core/prefetch/store/prefetch_store.h"
 #include "components/offline_pages/core/prefetch/suggested_articles_observer.h"
 #include "content/public/browser/browser_context.h"
+#include "content/public/browser/browser_task_traits.h"
 #include "services/network/public/cpp/shared_url_loader_factory.h"
 
 namespace offline_pages {
 
+namespace {
+
+void OnProfileCreated(PrefetchServiceImpl* service, Profile* profile) {
+  auto gcm_app_handler = std::make_unique<PrefetchGCMAppHandler>(
+      std::make_unique<PrefetchInstanceIDProxy>(kPrefetchingOfflinePagesAppId,
+                                                profile));
+  service->SetPrefetchGCMHandler(std::move(gcm_app_handler));
+  if (IsPrefetchingOfflinePagesEnabled()) {
+    // Trigger an update of the cached GCM token. This needs to be post tasked
+    // because otherwise leads to circular dependency between
+    // PrefetchServiceFactory and GCMProfileServiceFactory. See
+    // https://crbug.com/944952
+    // Update is not a priority so make sure it happens after the critical
+    // startup path.
+    base::PostTaskWithTraits(
+        FROM_HERE,
+        {content::BrowserThread::UI, base::TaskPriority::BEST_EFFORT},
+        base::BindOnce(&PrefetchServiceImpl::GetGCMToken, service->GetWeakPtr(),
+                       base::DoNothing::Once<const std::string&>()));
+  }
+}
+
+}  // namespace
+
 PrefetchServiceFactory::PrefetchServiceFactory()
     : BrowserContextKeyedServiceFactory(
           "OfflinePagePrefetchService",
@@ -91,10 +120,6 @@
     url_loader_factory = nullptr;
   }
 
-  auto prefetch_gcm_app_handler = std::make_unique<PrefetchGCMAppHandler>(
-      std::make_unique<PrefetchInstanceIDProxy>(kPrefetchingOfflinePagesAppId,
-                                                context));
-
   auto prefetch_network_request_factory =
       std::make_unique<PrefetchNetworkRequestFactoryImpl>(
           url_loader_factory, chrome::GetChannel(), GetUserAgent(),
@@ -135,14 +160,19 @@
   auto prefetch_background_task_handler =
       std::make_unique<PrefetchBackgroundTaskHandlerImpl>(profile->GetPrefs());
 
-  return new PrefetchServiceImpl(
+  auto* service = new PrefetchServiceImpl(
       std::move(offline_metrics_collector), std::move(prefetch_dispatcher),
-      std::move(prefetch_gcm_app_handler),
       std::move(prefetch_network_request_factory), offline_page_model,
       std::move(prefetch_store), std::move(suggested_articles_observer),
       std::move(prefetch_downloader), std::move(prefetch_importer),
       std::move(prefetch_background_task_handler), std::move(thumbnail_fetcher),
       image_fetcher);
+
+  auto callback = base::BindOnce(&OnProfileCreated, service);
+  FullBrowserTransitionManager::Get()->RegisterCallbackOnProfileCreation(
+      profile->GetProfileKey(), std::move(callback));
+
+  return service;
 }
 
 }  // namespace offline_pages
diff --git a/chrome/browser/profile_resetter/triggered_profile_resetter_win_unittest.cc b/chrome/browser/profile_resetter/triggered_profile_resetter_win_unittest.cc
index fe3b654..3c17536d 100644
--- a/chrome/browser/profile_resetter/triggered_profile_resetter_win_unittest.cc
+++ b/chrome/browser/profile_resetter/triggered_profile_resetter_win_unittest.cc
@@ -26,6 +26,10 @@
 class TriggeredProfileResetterTest : public testing::Test {
  protected:
   void SetUp() override {
+    TestingProfile::Builder profile_builder;
+    profile_builder.OverrideIsNewProfile(false);
+    profile_ = profile_builder.Build();
+
     ASSERT_NO_FATAL_FAILURE(
         override_manager_.OverrideRegistry(HKEY_CURRENT_USER));
 
@@ -38,7 +42,7 @@
   }
 
   content::TestBrowserThreadBundle thread_bundle_;
-  TestingProfile profile_;
+  std::unique_ptr<TestingProfile> profile_;
 
   void SetRegTimestampAndToolName(const base::string16& toolname,
                                   FILETIME* file_time) {
@@ -62,7 +66,7 @@
 
 TEST_F(TriggeredProfileResetterTest, HasResetTriggerAndClear) {
   SetRegTimestampAndToolName(base::string16(), nullptr);
-  TriggeredProfileResetter triggered_profile_resetter(&profile_);
+  TriggeredProfileResetter triggered_profile_resetter(profile_.get());
   triggered_profile_resetter.Activate();
   EXPECT_TRUE(triggered_profile_resetter.HasResetTrigger());
   triggered_profile_resetter.ClearResetTrigger();
@@ -72,10 +76,10 @@
 TEST_F(TriggeredProfileResetterTest, HasDuplicateResetTrigger) {
   FILETIME ft = {};
   SetRegTimestampAndToolName(base::string16(), &ft);
-  profile_.GetPrefs()->SetInt64(prefs::kLastProfileResetTimestamp,
-                                bit_cast<int64_t, FILETIME>(ft));
+  profile_->GetPrefs()->SetInt64(prefs::kLastProfileResetTimestamp,
+                                 bit_cast<int64_t, FILETIME>(ft));
 
-  TriggeredProfileResetter triggered_profile_resetter(&profile_);
+  TriggeredProfileResetter triggered_profile_resetter(profile_.get());
   triggered_profile_resetter.Activate();
   EXPECT_FALSE(triggered_profile_resetter.HasResetTrigger());
 }
@@ -83,7 +87,7 @@
 TEST_F(TriggeredProfileResetterTest, HasToolName) {
   const wchar_t kToolName[] = L"ToolyMcTool";
   SetRegTimestampAndToolName(kToolName, nullptr);
-  TriggeredProfileResetter triggered_profile_resetter(&profile_);
+  TriggeredProfileResetter triggered_profile_resetter(profile_.get());
   triggered_profile_resetter.Activate();
   EXPECT_TRUE(triggered_profile_resetter.HasResetTrigger());
   EXPECT_STREQ(kToolName,
@@ -99,7 +103,7 @@
       L"ToolMcToolToolMcToolToolMcToolToolMcToolToolMcToolToolMcToolToolMcTool"
       L"ToolMcToolToolMcToolToolMcTool";
   SetRegTimestampAndToolName(kLongToolName, nullptr);
-  TriggeredProfileResetter triggered_profile_resetter(&profile_);
+  TriggeredProfileResetter triggered_profile_resetter(profile_.get());
   triggered_profile_resetter.Activate();
   EXPECT_TRUE(triggered_profile_resetter.HasResetTrigger());
   EXPECT_STREQ(kExpectedToolName,
diff --git a/chrome/browser/profiles/profile.cc b/chrome/browser/profiles/profile.cc
index ea80faa..aa8b4d51 100644
--- a/chrome/browser/profiles/profile.cc
+++ b/chrome/browser/profiles/profile.cc
@@ -10,7 +10,6 @@
 #include "build/build_config.h"
 #include "chrome/browser/browsing_data/chrome_browsing_data_remover_delegate.h"
 #include "chrome/browser/chrome_notification_types.h"
-#include "chrome/browser/first_run/first_run.h"
 #include "chrome/browser/net/profile_network_context_service.h"
 #include "chrome/browser/net/profile_network_context_service_factory.h"
 #include "chrome/browser/profiles/profile_manager.h"
@@ -40,6 +39,7 @@
 #endif
 
 #if !defined(OS_ANDROID)
+#include "chrome/browser/first_run/first_run.h"
 #include "content/public/browser/host_zoom_map.h"
 #endif
 
@@ -268,12 +268,16 @@
 }
 
 bool Profile::IsNewProfile() {
-  // The profile has been shut down if the prefs were loaded from disk, unless
-  // first-run autoimport wrote them and reloaded the pref service.
-  // TODO(crbug.com/660346): revisit this when crbug.com/22142 (unifying the
-  // profile import code) is fixed.
+#if !defined(OS_ANDROID)
+  // The profile is new if the preference files has just been created, except on
+  // first run, because the installer may create a preference file. See
+  // https://crbug.com/728402
+  if (first_run::IsChromeFirstRun())
+    return true;
+#endif
+
   return GetOriginalProfile()->GetPrefs()->GetInitializationStatus() ==
-      PrefService::INITIALIZATION_STATUS_CREATED_NEW_PREF_STORE;
+         PrefService::INITIALIZATION_STATUS_CREATED_NEW_PREF_STORE;
 }
 
 bool Profile::IsSyncAllowed() {
diff --git a/chrome/browser/profiles/profile_impl.cc b/chrome/browser/profiles/profile_impl.cc
index c3226565..7e53f73 100644
--- a/chrome/browser/profiles/profile_impl.cc
+++ b/chrome/browser/profiles/profile_impl.cc
@@ -981,16 +981,13 @@
 
   FullBrowserTransitionManager::Get()->OnProfileCreated(this);
 
-  // The version must be set before the keyed services are created, so
-  // that they can call WasCreatedByVersionOrLater in their constructor.
-  ChromeVersionService::OnProfileLoaded(prefs_.get(), IsNewProfile());
-
   {
     SCOPED_UMA_HISTOGRAM_TIMER("Profile.CreateBrowserContextServicesTime");
     BrowserContextDependencyManager::GetInstance()
         ->CreateBrowserContextServices(this);
   }
 
+  ChromeVersionService::OnProfileLoaded(prefs_.get(), IsNewProfile());
   DoFinalInit();
 }
 
diff --git a/chrome/browser/resources/chromeos/camera/BUILD.gn b/chrome/browser/resources/chromeos/camera/BUILD.gn
index c5994ea..2cc13dfd 100644
--- a/chrome/browser/resources/chromeos/camera/BUILD.gn
+++ b/chrome/browser/resources/chromeos/camera/BUILD.gn
@@ -88,6 +88,7 @@
     "src/images/settings_feedback.svg",
     "src/images/settings_grid_type.svg",
     "src/images/settings_help.svg",
+    "src/images/settings_resolution.svg",
     "src/images/settings_timer_duration.svg",
     "src/images/spinner.svg",
   ]
@@ -105,6 +106,7 @@
     "src/js/main.js",
     "src/js/metrics.js",
     "src/js/nav.js",
+    "src/js/resolution_event_broker.js",
     "src/js/scrollbar.js",
     "src/js/sound.js",
     "src/js/state.js",
diff --git a/chrome/browser/resources/chromeos/camera/Makefile b/chrome/browser/resources/chromeos/camera/Makefile
index 25b82ad..4c5e7c7 100644
--- a/chrome/browser/resources/chromeos/camera/Makefile
+++ b/chrome/browser/resources/chromeos/camera/Makefile
@@ -110,6 +110,7 @@
 	src/images/settings_feedback.svg \
 	src/images/settings_grid_type.svg \
 	src/images/settings_help.svg \
+	src/images/settings_resolution.svg \
 	src/images/settings_timer_duration.svg \
 	src/images/spinner.svg \
 	src/js/background.js \
@@ -122,6 +123,7 @@
 	src/js/models/filesystem.js \
 	src/js/mojo/imagecapture.js \
 	src/js/nav.js \
+	src/js/resolution_event_broker.js \
 	src/js/scrollbar.js \
 	src/js/sound.js \
 	src/js/state.js \
diff --git a/chrome/browser/resources/chromeos/camera/src/css/main.css b/chrome/browser/resources/chromeos/camera/src/css/main.css
index bcee2f2..06f339a 100644
--- a/chrome/browser/resources/chromeos/camera/src/css/main.css
+++ b/chrome/browser/resources/chromeos/camera/src/css/main.css
@@ -443,6 +443,9 @@
 #settings,
 #gridsettings,
 #timersettings,
+#resolutionsettings,
+#photoresolutionsettings,
+#videoresolutionsettings,
 #browser,
 #message-dialog,
 #intro-dialog,
@@ -461,6 +464,9 @@
 body.settings #settings,
 body.gridsettings #gridsettings,
 body.timersettings #timersettings,
+body.resolutionsettings #resolutionsettings,
+body.photoresolutionsettings #photoresolutionsettings,
+body.videoresolutionsettings #videoresolutionsettings,
 body.browser #browser,
 body.message-dialog #message-dialog,
 body.intro-dialog #intro-dialog,
@@ -472,6 +478,9 @@
 
 body.gridsettings #gridsettings,
 body.timersettings #timersettings,
+body.resolutionsettings #resolutionsettings,
+body.photoresolutionsettings #photoresolutionsettings,
+body.videoresolutionsettings #videoresolutionsettings,
 body.settings #settings {
   /* Avoid flicking for transition between settings. */
   transition: opacity 0ms;
@@ -481,7 +490,15 @@
 body.gridsettings .left-stripe,
 body.gridsettings #settings,
 body.timersettings .left-stripe,
-body.timersettings #settings {
+body.timersettings #settings,
+body.resolutionsettings .left-stripe,
+body.resolutionsettings #settings,
+body.photoresolutionsettings .left-stripe,
+body.photoresolutionsettings #settings,
+body.photoresolutionsettings #resolutionsettings,
+body.videoresolutionsettings .left-stripe,
+body.videoresolutionsettings #settings,
+body.videoresolutionsettings #resolutionsettings {
   opacity: 0;
 }
 
@@ -903,6 +920,7 @@
   left: 0;
   min-width: 360px;
   opacity: 0.9;
+  overflow-y: scroll;
   position: absolute;
   top: 0;
 }
@@ -921,6 +939,10 @@
   text-align: left;
 }
 
+.menu-item.resol-item {
+  padding-left: 60px;
+}
+
 .menu-header {
   color: white;
   font-size: 20px;
@@ -945,11 +967,21 @@
   display: none;
 }
 
+body:not(.has-front-camera) #settings-front-photores,
+body:not(.has-front-camera) #settings-front-videores,
+body:not(.has-back-camera) #settings-back-photores,
+body:not(.has-back-camera) #settings-back-videores,
+body:not(.has-front-camera):not(.has-back-camera) #builtin-photo-header,
+body:not(.has-front-camera):not(.has-back-camera) #builtin-video-header {
+  display: none;
+}
+
 body._3x3 .description span[i18n-content=label_grid_3x3],
 body._4x4 .description span[i18n-content=label_grid_4x4],
 body.golden .description span[i18n-content=label_grid_golden],
 body._3sec .description span[i18n-content=label_timer_3s],
-body._10sec .description span[i18n-content=label_timer_10s] {
+body._10sec .description span[i18n-content=label_timer_10s],
+.menu-item.resol-item .description span {
   display: inline;
 }
 
@@ -993,6 +1025,10 @@
   background-image: url(../images/settings_timer_duration.svg);
 }
 
+#settings-resolution .icon {
+  background-image: url(../images/settings_resolution.svg);
+}
+
 #settings-feedback .icon {
   background-image: url(../images/settings_feedback.svg);
 }
@@ -1002,7 +1038,9 @@
 }
 
 #settings-gridtype .icon.end,
-#settings-timerdur .icon.end {
+#settings-timerdur .icon.end,
+#settings-resolution .icon.end,
+.resol-item.multi-option .icon.end {
   background-image: url(../images/settings_button_expand.svg);
 }
 
diff --git a/chrome/browser/resources/chromeos/camera/src/images/settings_resolution.svg b/chrome/browser/resources/chromeos/camera/src/images/settings_resolution.svg
new file mode 100644
index 0000000..6ef8936
--- /dev/null
+++ b/chrome/browser/resources/chromeos/camera/src/images/settings_resolution.svg
@@ -0,0 +1 @@
+<svg width="20" height="20" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path d="M0 0h20v20H0z"/><path d="M7 13h3v3H7v-3zm6 0h3v3h-3v-3zM7 7h3v3H7V7zm6 0h3v3h-3V7zm-9 3h3v3H4v-3zm6 0h3v3h-3v-3zM4 4h3v3H4V4zm6 0h3v3h-3V4z" fill="#E8EAED"/></g></svg>
\ No newline at end of file
diff --git a/chrome/browser/resources/chromeos/camera/src/js/main.js b/chrome/browser/resources/chromeos/camera/src/js/main.js
index 398c906..4e0383f 100644
--- a/chrome/browser/resources/chromeos/camera/src/js/main.js
+++ b/chrome/browser/resources/chromeos/camera/src/js/main.js
@@ -32,6 +32,18 @@
    */
   this.browserView_ = new cca.views.Browser(this.model_);
 
+  /**
+   * @type {cca.ResolutionEventBroker}
+   * @private
+   */
+  this.resolBroker_ = new cca.ResolutionEventBroker();
+
+  /**
+   * @type {cca.views.ResolutionSettings}
+   * @private
+   */
+  this.resolSettingsView_ = new cca.views.ResolutionSettings(this.resolBroker_);
+
   // End of properties. Seal the object.
   Object.seal(this);
 
@@ -43,10 +55,13 @@
 
   // Set up views navigation by their DOM z-order.
   cca.nav.setup([
-    new cca.views.Camera(this.model_),
+    new cca.views.Camera(this.model_, this.resolBroker_),
     new cca.views.MasterSettings(),
-    new cca.views.GridSettings(),
-    new cca.views.TimerSettings(),
+    new cca.views.BaseSettings('#gridsettings'),
+    new cca.views.BaseSettings('#timersettings'),
+    this.resolSettingsView_,
+    new cca.views.BaseSettings('#photoresolutionsettings'),
+    new cca.views.BaseSettings('#videoresolutionsettings'),
     this.browserView_,
     new cca.views.Warning(),
     new cca.views.Dialog('#message-dialog'),
diff --git a/chrome/browser/resources/chromeos/camera/src/js/resolution_event_broker.js b/chrome/browser/resources/chromeos/camera/src/js/resolution_event_broker.js
new file mode 100644
index 0000000..dee6fd9
--- /dev/null
+++ b/chrome/browser/resources/chromeos/camera/src/js/resolution_event_broker.js
@@ -0,0 +1,175 @@
+// 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.
+
+'use strict';
+
+/**
+ * Namespace for the Camera app.
+ */
+var cca = cca || {};
+
+/**
+ * A list of resolutions represented as its width and height.
+ * @typedef {Array<[number, number]>} ResolList
+ */
+
+/**
+ * A list of device id and the supported video, photo resolutions of its
+ * corresponding video device.
+ * @typedef {[number, ResolList, ResolList]} DeviceIdResols
+ */
+
+/**
+ * Broker for registering or notifying resolution related events.
+ * @constructor
+ */
+cca.ResolutionEventBroker = function() {
+  /**
+   * Handler for requests of changing user-preferred resolution used in photo
+   * taking.
+   * @type {function(string, number, number)}
+   * @private
+   */
+  this.photoChangeResolHandler_ = () => {};
+
+  /**
+   * Handler for requests of changing user-preferred resolution used in video
+   * recording.
+   * @type {function(string, number, number)}
+   * @private
+   */
+  this.videoChangeResolHandler_ = () => {};
+
+  /**
+   * Listener for changes of resolution currently used in photo taking.
+   * @type {function(string, number, number)}
+   * @private
+   */
+  this.photoResolChangeListener_ = () => {};
+
+  /**
+   * Listener for changes of resolution currently used in video recording.
+   * @type {function(string, number, number)}
+   * @private
+   */
+  this.videoResolChangeListener_ = () => {};
+
+  /**
+   * Listener for changes of currently available device-resolutions of front,
+   * back, external cameras.
+   * @type {function(?DeviceIdResols, ?DeviceIdResols, Array<DeviceIdResols>)}
+   * @private
+   */
+  this.deviceResolListener_ = () => {};
+};
+
+/**
+ * Registers handler for requests of changing user-preferred resolution used in
+ * photo taking.
+ * @param {function(string, number, number)} handler Called with device id of
+ *     video device to be changed and width, height of new resolution.
+ */
+cca.ResolutionEventBroker.prototype.registerChangePhotoResolHandler = function(
+    handler) {
+  this.photoChangeResolHandler_ = handler;
+};
+
+/**
+ * Registers handler for requests of changing user-preferred resolution used in
+ * video recording.
+ * @param {function(string, number, number)} handler Called with device id of
+ *     video device to be changed and width, height of new resolution.
+ */
+cca.ResolutionEventBroker.prototype.registerChangeVideoResolHandler = function(
+    handler) {
+  this.videoChangeResolHandler_ = handler;
+};
+
+/**
+ * Requests for changing user-preferred resolution used in photo taking.
+ * @param {string} deviceId Device id of video device to be changed.
+ * @param {number} width Change to resolution width.
+ * @param {number} height Change to resolution height.
+ */
+cca.ResolutionEventBroker.prototype.requestChangePhotoResol = function(
+    deviceId, width, height) {
+  this.photoChangeResolHandler_(deviceId, width, height);
+};
+
+/**
+ * Requests for changing user-preferred resolution used in video recording.
+ * @param {string} deviceId Device id of video device to be changed.
+ * @param {number} width Change to resolution width.
+ * @param {number} height Change to resolution height.
+ */
+cca.ResolutionEventBroker.prototype.requestChangeVideoResol = function(
+    deviceId, width, height) {
+  this.videoChangeResolHandler_(deviceId, width, height);
+};
+
+/**
+   Adds listener for changes of resolution currently used in photo taking.
+ * @param {function(string, number, number)} listener Called with changed device
+ *     id and new resolution width, height.
+ */
+cca.ResolutionEventBroker.prototype.addPhotoResolChangeListener = function(
+    listener) {
+  this.photoResolChangeListener_ = listener;
+};
+
+/**
+ * Adds listener for changes of resolution currently used in video recording.
+ * @param {function(string, number, number)} listener Called with changed device
+ *     id and new resolution width, height.
+ */
+cca.ResolutionEventBroker.prototype.addVideoResolChangeListener = function(
+    listener) {
+  this.videoResolChangeListener_ = listener;
+};
+
+/**
+ * Notifies the change of resolution currently used in photo taking.
+ * @param {string} deviceId Device id of changed video device.
+ * @param {number} width New resolution width.
+ * @param {number} height New resolution height.
+ */
+cca.ResolutionEventBroker.prototype.notifyPhotoResolChange = function(
+    deviceId, width, height) {
+  this.photoResolChangeListener_(deviceId, width, height);
+};
+
+/**
+ * Notifies the change of resolution currently used in video recording.
+ * @param {string} deviceId Device id of changed video device.
+ * @param {number} width New resolution width.
+ * @param {number} height New resolution height.
+ */
+cca.ResolutionEventBroker.prototype.notifyVideoResolChange = function(
+    deviceId, width, height) {
+  this.videoResolChangeListener_(deviceId, width, height);
+};
+
+/**
+ * Adds listener for changes of currently available device-resolutions of
+ * all cameras.
+ * @param {function(?DeviceIdResols, ?DeviceIdResols, Array<DeviceIdResols>)}
+ *     listener Listener function called with deviceId-resolutions of front,
+ *     back and external cameras.
+ */
+cca.ResolutionEventBroker.prototype.addUpdateDeviceResolutionsListener =
+    function(listener) {
+  this.deviceResolListener_ = listener;
+};
+
+/**
+ * Notifies the change of currently available device-resolutions of all cameras.
+ * @param {?DeviceIdResols} front DeviceId-resolutions of front camera.
+ * @param {?DeviceIdResols} back DeviceId-resolutions of back camera.
+ * @param {Array<DeviceIdResols>} externals DeviceId-resolutions of external
+ *     cameras.
+ */
+cca.ResolutionEventBroker.prototype.notifyUpdateDeviceResolutions = function(
+    front, back, externals) {
+  this.deviceResolListener_(front, back, externals);
+};
diff --git a/chrome/browser/resources/chromeos/camera/src/js/views/camera.js b/chrome/browser/resources/chromeos/camera/src/js/views/camera.js
index a31d243..5804268 100644
--- a/chrome/browser/resources/chromeos/camera/src/js/views/camera.js
+++ b/chrome/browser/resources/chromeos/camera/src/js/views/camera.js
@@ -17,9 +17,10 @@
 /**
  * Creates the camera-view controller.
  * @param {cca.models.Gallery} model Model object.
+ * @param {cca.ResolutionEventBroker} resolBroker
  * @constructor
  */
-cca.views.Camera = function(model) {
+cca.views.Camera = function(model, resolBroker) {
   cca.views.View.call(this, '#camera');
 
   /**
@@ -48,7 +49,8 @@
    * @type {cca.views.camera.Options}
    * @private
    */
-  this.options_ = new cca.views.camera.Options(this.stop_.bind(this));
+  this.options_ =
+      new cca.views.camera.Options(resolBroker, this.stop_.bind(this));
 
   /**
    * Modes for the camera.
@@ -56,7 +58,8 @@
    * @private
    */
   this.modes_ = new cca.views.camera.Modes(
-      this.stop_.bind(this), async (blob, isMotionPicture, filename) => {
+      resolBroker, this.stop_.bind(this), this.stop_.bind(this),
+      async (blob, isMotionPicture, filename) => {
         if (blob) {
           cca.metrics.log(
               cca.metrics.Type.CAPTURE, this.facingMode_, blob.mins);
@@ -214,6 +217,45 @@
 };
 
 /**
+ * Try start stream reconfiguration with specified device id.
+ * @async
+ * @param {string} deviceId
+ * @return {boolean} If found suitable stream and reconfigure successfully.
+ */
+cca.views.Camera.prototype.startWithDevice_ = async function(deviceId) {
+  let supportedModes = null;
+  for (const mode of this.modes_.getModeCandidates()) {
+    const [photoRs, videoRs] =
+        await this.options_.getDeviceResolutions(deviceId);
+    for (const [[width, height], previewCandidates] of this.modes_
+             .getResolutionCandidates(mode, deviceId, photoRs, videoRs)) {
+      for (const constraints of previewCandidates) {
+        try {
+          const stream = await navigator.mediaDevices.getUserMedia(constraints);
+          if (!supportedModes) {
+            supportedModes = await this.modes_.getSupportedModes(stream);
+            if (!supportedModes.includes(mode)) {
+              stream.getTracks()[0].stop();
+              return false;
+            }
+          }
+          await this.preview_.start(stream);
+          this.facingMode_ = this.options_.updateValues(constraints, stream);
+          await this.modes_.updateModeSelectionUI(supportedModes);
+          await this.modes_.updateMode(mode, stream, deviceId, width, height);
+          cca.nav.close('warning', 'no-camera');
+          return true;
+        } catch (e) {
+          this.preview_.stop();
+          console.error(e);
+        }
+      }
+    }
+  }
+  return false;
+};
+
+/**
  * Starts camera if the camera stream was stopped.
  * @private
  */
@@ -223,36 +265,8 @@
       (async () => {
         if (!suspend) {
           for (const id of await this.options_.videoDeviceIds()) {
-            let supportedModes = null;
-            for (const mode of this.modes_.getModeCandidates()) {
-              if (supportedModes && !supportedModes.includes(mode)) {
-                continue;
-              }
-              for (const constraints of this.modes_.getConstraitsCandidates(
-                       id, mode)) {
-                try {
-                  const stream =
-                      await navigator.mediaDevices.getUserMedia(constraints);
-                  if (!supportedModes) {
-                    supportedModes =
-                        await this.modes_.getSupportedModes(stream);
-                    if (!supportedModes.includes(mode)) {
-                      stream.getTracks()[0].stop();
-                      break;
-                    }
-                  }
-                  await this.preview_.start(stream);
-                  await this.modes_.updateModeSelectionUI(supportedModes);
-                  this.facingMode_ =
-                      this.options_.updateValues(constraints, stream);
-                  await this.modes_.updateMode(mode, stream);
-                  cca.nav.close('warning', 'no-camera');
-                  return;
-                } catch (e) {
-                  this.preview_.stop();
-                  console.error(e);
-                }
-              }
+            if (await this.startWithDevice_(id)) {
+              return;
             }
           }
         }
diff --git a/chrome/browser/resources/chromeos/camera/src/js/views/camera/modes.js b/chrome/browser/resources/chromeos/camera/src/js/views/camera/modes.js
index c445901..b48001c 100644
--- a/chrome/browser/resources/chromeos/camera/src/js/views/camera/modes.js
+++ b/chrome/browser/resources/chromeos/camera/src/js/views/camera/modes.js
@@ -21,12 +21,16 @@
 
 /**
  * Mode controller managing capture sequence of different camera mode.
+ * @param {cca.ResolutionEventBroker} resolBroker
+ * @param {function()} doSwitchResolution Callback to trigger resolution
+ *     switching.
  * @param {function()} doSwitchMode Callback to trigger mode switching.
  * @param {function(?Blob, boolean, string): Promise} doSavePicture Callback for
  *     saving picture.
  * @constructor
  */
-cca.views.camera.Modes = function(doSwitchMode, doSavePicture) {
+cca.views.camera.Modes = function(
+    resolBroker, doSwitchResolution, doSwitchMode, doSavePicture) {
   /**
    * @type {function()}
    * @private
@@ -60,6 +64,34 @@
   this.modesGroup_ = document.querySelector('#modes-group');
 
   /**
+   * @type {cca.views.camera.PhotoResolutionConfig}
+   * @private
+   */
+  this.photoResConfig_ = new cca.views.camera.PhotoResolutionConfig(
+      resolBroker, doSwitchResolution);
+
+  /**
+   * @type {cca.views.camera.VideoResolutionConfig}
+   * @private
+   */
+  this.videoResConfig_ = new cca.views.camera.VideoResolutionConfig(
+      resolBroker, doSwitchResolution);
+
+  /**
+   * Captured resolution width.
+   * @type {number}
+   * @private
+   */
+  this.captureWidth_ = 0;
+
+  /**
+   * Captured resolution height.
+   * @type {number}
+   * @private
+   */
+  this.captureHeight_ = 0;
+
+  /**
    * Mode classname and related functions and attributes.
    * @type {Object<string, Object>}
    * @private
@@ -69,26 +101,29 @@
       captureFactory: () =>
           new cca.views.camera.Video(this.stream_, this.doSavePicture_),
       isSupported: async () => true,
-      deviceConstraints: cca.views.camera.Modes.videoConstraits,
+      resolutionConfig: this.videoResConfig_,
       nextMode: 'photo-mode',
     },
     'photo-mode': {
-      captureFactory: () =>
-          new cca.views.camera.Photo(this.stream_, this.doSavePicture_),
+      captureFactory: () => new cca.views.camera.Photo(
+          this.stream_, this.doSavePicture_, this.captureWidth_,
+          this.captureHeight_),
       isSupported: async () => true,
-      deviceConstraints: cca.views.camera.Modes.photoConstraits,
+      resolutionConfig: this.photoResConfig_,
       nextMode: 'square-mode',
     },
     'square-mode': {
-      captureFactory: () =>
-          new cca.views.camera.Square(this.stream_, this.doSavePicture_),
+      captureFactory: () => new cca.views.camera.Square(
+          this.stream_, this.doSavePicture_, this.captureWidth_,
+          this.captureHeight_),
       isSupported: async () => true,
-      deviceConstraints: cca.views.camera.Modes.photoConstraits,
+      resolutionConfig: this.photoResConfig_,
       nextMode: 'portrait-mode',
     },
     'portrait-mode': {
-      captureFactory: () =>
-          new cca.views.camera.Portrait(this.stream_, this.doSavePicture_),
+      captureFactory: () => new cca.views.camera.Portrait(
+          this.stream_, this.doSavePicture_, this.captureWidth_,
+          this.captureHeight_),
       isSupported: async (stream) => {
         try {
           const imageCapture =
@@ -104,7 +139,7 @@
           return false;
         }
       },
-      deviceConstraints: cca.views.camera.Modes.photoConstraits,
+      resolutionConfig: this.photoResConfig_,
       nextMode: 'video-mode',
     },
   };
@@ -149,59 +184,284 @@
 };
 
 /**
- * Returns a set of available video constraints.
- * @param {?string} deviceId Id of video device.
- * @return {Array<Object>} Result of constraints-candidates.
+ * Controller for generating suitable capture and preview resolution
+ * combination.
+ * @param {cca.ResolutionEventBroker} resolBroker
+ * @param {function()} doSwitchResolution
+ * @constructor
  */
-cca.views.camera.Modes.videoConstraits = function(deviceId) {
-  return [
-    {
-      aspectRatio: {ideal: 1.7777777778},
-      width: {min: 1280},
-      frameRate: {min: 24},
-    },
-    {
-      width: {min: 640},
-      frameRate: {min: 24},
-    },
-  ].map((constraint) => {
-    // Each passed-in video-constraint will be modified here.
-    if (deviceId) {
-      constraint.deviceId = {exact: deviceId};
-    } else {
-      // As a default camera use the one which is facing the user.
-      constraint.facingMode = {exact: 'user'};
-    }
-    return {audio: true, video: constraint};
-  });
+cca.views.camera.ModeResolutionConfig = function(
+    resolBroker, doSwitchResolution) {
+  /**
+   * @type {cca.ResolutionEventBroker}
+   * @private
+   */
+  this.resolBroker_ = resolBroker;
+
+  /**
+   * @type {function()}
+   * @private
+   */
+  this.doSwitchResolution_ = doSwitchResolution;
+
+  /**
+   * Object saving resolution preference for each of its key as device id and
+   * value to be preferred width, height of resolution of that video device.
+   * @type {Object<string, {width: number, height: number}>}
+   * @private
+   */
+  this.prefResolution_ = null;
+
+  /**
+   * Device id of currently working video device.
+   * @type {string}
+   * @private
+   */
+  this.deviceId_ = null;
+
+  // End of properties, seal the object.
+  Object.seal(this);
 };
 
 /**
- * Returns a set of available photo constraints.
- * @param {?string} deviceId Id of video device.
- * @return {Array<Object>} Result of constraints-candidates.
+ * Gets all available resolutions candidates for capturing under this controller
+ * and its corresponding preview constraints from all available resolutions of
+ * different format. Returned resolutions and constraints candidates are both
+ * sorted in desired trying order.
+ * @param {string} deviceId
+ * @param {ResolList} photoResolutions
+ * @param {ResolList} videoResolutions
+ * @return {Array<[number, number, Array<Object>]>} Result capture resolution
+ *     width, height and constraints-candidates for its preview.
  */
-cca.views.camera.Modes.photoConstraits = function(deviceId) {
-  return [
-    {
-      aspectRatio: {ideal: 1.3333333333},
-      width: {min: 1280},
-      frameRate: {min: 24},
-    },
-    {
-      width: {min: 640},
-      frameRate: {min: 24},
-    },
-  ].map((constraint) => {
-    // Each passed-in video-constraint will be modified here.
-    if (deviceId) {
-      constraint.deviceId = {exact: deviceId};
-    } else {
-      // As a default camera use the one which is facing the user.
-      constraint.facingMode = {exact: 'user'};
+cca.views.camera.ModeResolutionConfig.prototype.getSortedCandidates = function(
+    deviceId, photoResolutions, videoResolutions) {
+  return null;
+};
+
+/**
+ * Updates selected resolution.
+ * @param {string} deviceId Device id of selected video device.
+ * @param {number} width Width of selected resolution.
+ * @param {number} height Height of selected resolution.
+ */
+cca.views.camera.ModeResolutionConfig.prototype.updateSelectedResolution =
+    function(deviceId, width, height) {};
+
+/**
+ * Controller for generating suitable video recording and preview resolution
+ * combination.
+ * @param {cca.ResolutionEventBroker} resolBroker
+ * @param {function()} doSwitchResolution
+ * @constructor
+ */
+cca.views.camera.VideoResolutionConfig = function(
+    resolBroker, doSwitchResolution) {
+  cca.views.camera.ModeResolutionConfig.call(
+      this, resolBroker, doSwitchResolution);
+
+  // Restore saved preferred video resolution per video device.
+  chrome.storage.local.get(
+      {deviceVideoResolution: {}},
+      (values) => this.prefResolution_ = values.deviceVideoResolution);
+
+  this.resolBroker_.registerChangeVideoResolHandler(
+      (deviceId, width, height) => {
+        this.prefResolution_[deviceId] = {width, height};
+        chrome.storage.local.set({deviceVideoResolution: this.prefResolution_});
+        if (cca.state.get('video-mode') && deviceId == this.deviceId_) {
+          this.doSwitchResolution_();
+        }
+      });
+};
+
+cca.views.camera.VideoResolutionConfig.prototype = {
+  __proto__: cca.views.camera.ModeResolutionConfig.prototype,
+};
+
+/**
+ * @param {string} deviceId
+ * @param {number} width
+ * @param {number} height
+ * @override
+ */
+cca.views.camera.VideoResolutionConfig.prototype.updateSelectedResolution =
+    function(deviceId, width, height) {
+  this.deviceId_ = deviceId;
+  this.prefResolution_[deviceId] = {width, height};
+  chrome.storage.local.set({deviceVideoResolution: this.prefResolution_});
+  this.resolBroker_.notifyVideoResolChange(deviceId, width, height);
+};
+
+/**
+ * @param {string} deviceId
+ * @param {ResolList} photoResolutions
+ * @param {ResolList} videoResolutions
+ * @return {Array<[number, number, Array<Object>]>}
+ * @override
+ */
+cca.views.camera.VideoResolutionConfig.prototype.getSortedCandidates = function(
+    deviceId, photoResolutions, videoResolutions) {
+  // Due to the limitation of MediaStream API, preview stream is used directly
+  // to do video recording.
+  const prefR = this.prefResolution_[deviceId] || {width: 0, height: -1};
+  const preferredOrder = ([w, h], [w2, h2]) => {
+    if (w == w2 && h == h2) {
+      return 0;
     }
-    return {audio: false, video: constraint};
-  });
+    // Exactly the preferred resolution.
+    if (w == prefR.width && h == prefR.height) {
+      return -1;
+    }
+    if (w2 == prefR.width && h2 == prefR.height) {
+      return 1;
+    }
+    // Aspect ratio same as preferred resolution.
+    if (w * h2 != w2 * h) {
+      if (w * prefR.height == prefR.width * h) {
+        return -1;
+      }
+      if (w2 * prefR.height == prefR.width * h2) {
+        return 1;
+      }
+    }
+    return w2 * h2 - w * h;
+  };
+  return videoResolutions.sort(preferredOrder).map(([width, height]) => ([
+                                                     [width, height],
+                                                     [{
+                                                       audio: true,
+                                                       video: {
+                                                         deviceId:
+                                                             {exact: deviceId},
+                                                         frameRate: {min: 24},
+                                                         width,
+                                                         height,
+                                                       },
+                                                     }],
+                                                   ]));
+};
+
+/**
+ * Controller for generating suitable photo taking and preview resolution
+ * combination.
+ * @param {cca.ResolutionEventBroker} resolBroker
+ * @param {function()} doSwitchResolution
+ * @constructor
+ */
+cca.views.camera.PhotoResolutionConfig = function(
+    resolBroker, doSwitchResolution) {
+  cca.views.camera.ModeResolutionConfig.call(
+      this, resolBroker, doSwitchResolution);
+
+  // Restore saved preferred photo resolution per video device.
+  chrome.storage.local.get(
+      {devicePhotoResolution: {}},
+      (values) => this.prefResolution_ = values.devicePhotoResolution);
+
+  this.resolBroker_.registerChangePhotoResolHandler(
+      (deviceId, width, height) => {
+        this.prefResolution_[deviceId] = {width, height};
+        chrome.storage.local.set({devicePhotoResolution: this.prefResolution_});
+        if (!cca.state.get('video-mode') && deviceId == this.deviceId_) {
+          this.doSwitchResolution_();
+        }
+      });
+};
+
+/**
+ * @param {string} deviceId
+ * @param {number} width
+ * @param {number} height
+ * @override
+ */
+cca.views.camera.PhotoResolutionConfig.prototype.updateSelectedResolution =
+    function(deviceId, width, height) {
+  this.deviceId_ = deviceId;
+  this.prefResolution_[deviceId] = {width, height};
+  chrome.storage.local.set({devicePhotoResolution: this.prefResolution_});
+  this.resolBroker_.notifyPhotoResolChange(deviceId, width, height);
+};
+
+/**
+ * Finds and pairs photo resolutions and preview resolutions with the same
+ * aspect ratio.
+ * @param {ResolList} captureResolutions Available photo capturing resolutions.
+ * @param {ResolList} previewResolutions Available preview resolutions.
+ * @return {Array<[ResolList, ResolList]>} Each item of returned array is a pair
+ *     of capture and preview resolutions of same aspect ratio.
+ */
+cca.views.camera.PhotoResolutionConfig.prototype
+    .pairCapturePreviewResolutions_ = function(
+    captureResolutions, previewResolutions) {
+  const toAspectRatio = (w, h) => (w / h).toFixed(4);
+  const previewRatios = previewResolutions.reduce((rs, [w, h]) => {
+    const key = toAspectRatio(w, h);
+    rs[key] = rs[key] || [];
+    rs[key].push([w, h]);
+    return rs;
+  }, {});
+  const captureRatios = captureResolutions.reduce((rs, [w, h]) => {
+    const key = toAspectRatio(w, h);
+    if (key in previewRatios) {
+      rs[key] = rs[key] || [];
+      rs[key].push([w, h]);
+    }
+    return rs;
+  }, {});
+  return Object.entries(captureRatios)
+      .map(([aspectRatio,
+             captureRs]) => [captureRs, previewRatios[aspectRatio]]);
+};
+
+/**
+ * @param {string} deviceId
+ * @param {ResolList} photoResolutions
+ * @param {ResolList} videoResolutions
+ * @return {Array<[number, number, Array<Object>]>}
+ * @override
+ */
+cca.views.camera.PhotoResolutionConfig.prototype.getSortedCandidates = function(
+    deviceId, photoResolutions, videoResolutions) {
+  const prefR = this.prefResolution_[deviceId] || {width: 0, height: -1};
+  return this.pairCapturePreviewResolutions_(photoResolutions, videoResolutions)
+      .map(([captureRs, previewRs]) => {
+        if (captureRs.some(([w, h]) => w == prefR.width && h == prefR.height)) {
+          var captureR = [prefR.width, prefR.height];
+        } else {
+          var captureR = captureRs.reduce(
+              (captureR, r) => (r[0] > captureR[0] ? r : captureR), [0, -1]);
+        }
+
+        const candidates = previewRs.sort(([w, h], [w2, h2]) => w2 - w)
+                               .map(([width, height]) => ({
+                                      audio: false,
+                                      video: {
+                                        deviceId: {exact: deviceId},
+                                        frameRate: {min: 24},
+                                        width,
+                                        height,
+                                      },
+                                    }));
+        // Format of map result:
+        // [
+        //   [[CaptureW 1, CaptureH 1], [CaptureW 2, CaptureH 2], ...],
+        //   [PreviewConstraint 1, PreviewConstraint 2, ...]
+        // ]
+        return [captureR, candidates];
+      })
+      .sort(([[w, h]], [[w2, h2]]) => {
+        if (w == w2 && h == h2) {
+          return 0;
+        }
+        if (w == prefR.width && h == prefR.height) {
+          return -1;
+        }
+        if (w2 == prefR.width && h2 == prefR.height) {
+          return 1;
+        }
+        return w2 * h2 - w * h;
+      });
 };
 
 /**
@@ -236,14 +496,19 @@
 };
 
 /**
- * Gets constraints-candidates for given mode and deviceId.
- * @param {?string} deviceId Request video device id.
- * @param {?string} mode Request mode.
- * @return {Array<Object>} Constraints-candidates for given deviceId and mode.
+ * Gets all available capture resolution and its corresponding preview
+ * constraints for the given mode.
+ * @param {string} mode
+ * @param {string} deviceId
+ * @param {ResolList} photoResolutions
+ * @param {ResolList} videoResolutions
+ * @return {Array<[number, number, Array<Object>]>} Result capture resolution
+ *     width, height and constraints-candidates for its preview.
  */
-cca.views.camera.Modes.prototype.getConstraitsCandidates = function(
-    deviceId, mode) {
-  return this.allModes_[mode].deviceConstraints(deviceId);
+cca.views.camera.Modes.prototype.getResolutionCandidates = function(
+    mode, deviceId, photoResolutions, videoResolutions) {
+  return this.allModes_[mode].resolutionConfig.getSortedCandidates(
+      deviceId, photoResolutions, videoResolutions);
 };
 
 /**
@@ -280,30 +545,34 @@
  * @async
  * @param {string} mode Classname of mode to be updated.
  * @param {MediaStream} stream Stream of the new switching mode.
+ * @param {string} deviceId Device id of currently working video device.
+ * @param {number} captureWidth Capturing resolution width.
+ * @param {number} captureHeight Capturing resolution height.
  */
-cca.views.camera.Modes.prototype.updateMode = async function(mode, stream) {
+cca.views.camera.Modes.prototype.updateMode =
+    async function(mode, stream, deviceId, captureWidth, captureHeight) {
   if (this.current != null) {
     await this.current.stopCapture();
   }
   this.updateModeUI_(mode);
   this.stream_ = stream;
+  this.captureWidth_ = captureWidth;
+  this.captureHeight_ = captureHeight;
   this.current = this.allModes_[mode].captureFactory();
+  this.allModes_[mode].resolutionConfig.updateSelectedResolution(
+      deviceId, captureWidth, captureHeight);
 };
 
 /**
  * Base class for controlling capture sequence in different camera modes.
  * @param {MediaStream} stream
  * @param {function(?Blob, boolean, string): Promise} doSavePicture
+ * @param {number} captureWidth Capturing resolution width.
+ * @param {number} captureHeight Capturing resolution height.
  * @constructor
  */
-cca.views.camera.Mode = function(stream, doSavePicture) {
-  /**
-   * Promise for ongoing capture operation.
-   * @type {?Promise}
-   * @private
-   */
-  this.capture_ = null;
-
+cca.views.camera.Mode = function(
+    stream, doSavePicture, captureWidth, captureHeight) {
   /**
    * Stream of current mode.
    * @type {?Promise}
@@ -317,6 +586,25 @@
    * @protected
    */
   this.doSavePicture_ = doSavePicture;
+
+  /**
+   * @type {number}
+   * @private
+   */
+  this.captureWidth_ = captureWidth;
+
+  /**
+   * @type {number}
+   * @private
+   */
+  this.captureHeight_ = captureHeight;
+
+  /**
+   * Promise for ongoing capture operation.
+   * @type {?Promise}
+   * @private
+   */
+  this.capture_ = null;
 };
 
 /**
@@ -360,7 +648,7 @@
  * @constructor
  */
 cca.views.camera.Video = function(stream, doSavePicture) {
-  cca.views.camera.Mode.call(this, stream, doSavePicture);
+  cca.views.camera.Mode.call(this, stream, doSavePicture, -1, -1);
 
   /**
    * Promise for play start sound delay.
@@ -500,10 +788,14 @@
  * Photo mode capture controller.
  * @param {MediaStream} stream
  * @param {function(?Blob, boolean, string): Promise} doSavePicture
+ * @param {number} captureWidth
+ * @param {number} captureHeight
  * @constructor
  */
-cca.views.camera.Photo = function(stream, doSavePicture) {
-  cca.views.camera.Mode.call(this, stream, doSavePicture);
+cca.views.camera.Photo = function(
+    stream, doSavePicture, captureWidth, captureHeight) {
+  cca.views.camera.Mode.call(
+      this, stream, doSavePicture, captureWidth, captureHeight);
 
   /**
    * ImageCapture object to capture still photos.
@@ -549,12 +841,9 @@
  * @private
  */
 cca.views.camera.Photo.prototype.createPhotoBlob_ = async function() {
-  var photoCapabilities = await this.imageCapture_.getPhotoCapabilities();
-  // Set to take the highest resolution, but the photo to be taken will
-  // still have the same aspect ratio with the preview.
-  var photoSettings = {
-    imageWidth: photoCapabilities.imageWidth.max,
-    imageHeight: photoCapabilities.imageHeight.max,
+  const photoSettings = {
+    imageWidth: this.captureWidth_,
+    imageHeight: this.captureHeight_,
   };
   return await this.imageCapture_.takePhoto(photoSettings);
 };
@@ -563,10 +852,14 @@
  * Square mode capture controller.
  * @param {MediaStream} stream
  * @param {function(?Blob, boolean, string): Promise} doSavePicture
+ * @param {number} captureWidth
+ * @param {number} captureHeight
  * @constructor
  */
-cca.views.camera.Square = function(stream, doSavePicture) {
-  cca.views.camera.Photo.call(this, stream, doSavePicture);
+cca.views.camera.Square = function(
+    stream, doSavePicture, captureWidth, captureHeight) {
+  cca.views.camera.Photo.call(
+      this, stream, doSavePicture, captureWidth, captureHeight);
 
   /**
    * Picture saving callback from parent.
@@ -622,10 +915,14 @@
  * Portrait mode capture controller.
  * @param {MediaStream} stream
  * @param {function(?Blob, boolean): Promise} doSavePicture
+ * @param {number} captureWidth
+ * @param {number} captureHeight
  * @constructor
  */
-cca.views.camera.Portrait = function(stream, doSavePicture) {
-  cca.views.camera.Mode.call(this, stream, doSavePicture);
+cca.views.camera.Portrait = function(
+    stream, doSavePicture, captureWidth, captureHeight) {
+  cca.views.camera.Mode.call(
+      this, stream, doSavePicture, captureWidth, captureHeight);
 
   /**
    * ImageCapture object to capture still photos.
@@ -655,10 +952,9 @@
       throw e;
     }
   }
-  const photoCapabilities = await this.crosImageCapture_.getPhotoCapabilities();
   const photoSettings = {
-    imageWidth: photoCapabilities.imageWidth.max,
-    imageHeight: photoCapabilities.imageHeight.max,
+    imageWidth: this.captureWidth_,
+    imageHeight: this.captureHeight_,
   };
   try {
     var [reference, portrait] = this.crosImageCapture_.takePhoto(
diff --git a/chrome/browser/resources/chromeos/camera/src/js/views/camera/options.js b/chrome/browser/resources/chromeos/camera/src/js/views/camera/options.js
index 27c97d7..bcfdacb 100644
--- a/chrome/browser/resources/chromeos/camera/src/js/views/camera/options.js
+++ b/chrome/browser/resources/chromeos/camera/src/js/views/camera/options.js
@@ -21,10 +21,17 @@
 
 /**
  * Creates a controller for the options of Camera view.
+ * @param {cca.ResolutionEventBroker} resolBroker
  * @param {function()} doSwitchDevice Callback to trigger device switching.
  * @constructor
  */
-cca.views.camera.Options = function(doSwitchDevice) {
+cca.views.camera.Options = function(resolBroker, doSwitchDevice) {
+  /**
+   * @type {cca.ResolutionEventBroker}
+   * @private
+   */
+  this.resolBroker_ = resolBroker;
+
   /**
    * @type {function()}
    * @private
@@ -65,6 +72,14 @@
   this.videoDevices_ = null;
 
   /**
+   * List of available video devices and width, height of its supported video
+   * resolutions and photo resolutions.
+   * @type {Promise<!Array<[MediaDeviceInfo, ResolList, ResolList]>>}
+   * @private
+   */
+  this.deviceResolutions_ = null;
+
+  /**
    * Mirroring set per device.
    * @type {Object}
    * @private
@@ -104,6 +119,27 @@
 };
 
 /**
+ * Label of front facing camera from MediaDeviceInfo.
+ * @type {string}
+ * @const
+ */
+cca.views.camera.Options.FRONT_CAMERA_LABEL = 'Front Camera';
+
+/**
+ * Label of back facing camera from MediaDeviceInfo.
+ * @type {string}
+ * @const
+ */
+cca.views.camera.Options.BACK_CAMERA_LABEL = 'Back Camera';
+
+/**
+ * Label of external facing camera from MediaDeviceInfo.
+ * @type {string}
+ * @const
+ */
+cca.views.camera.Options.EXTERNAL_CAMERA_LABEL = 'External Camera';
+
+/**
  * Switches to the next available camera device.
  * @private
  */
@@ -240,6 +276,40 @@
     cca.state.set('multi-camera', multi);
     this.refreshingVideoDeviceIds_ = false;
   });
+
+  this.deviceResolutions_ = this.videoDevices_.then((devices) => {
+    return Promise.all(devices.map((d) => Promise.all([
+      d,
+      cca.mojo.getPhotoResolutions(d.deviceId),
+      cca.mojo.getVideoConfigs(d.deviceId)
+          .then((v) => v.filter(([, , fps]) => fps >= 24).map(([w,
+                                                                h]) => [w, h])),
+    ])));
+  });
+
+  this.deviceResolutions_.then((deviceResolutions) => {
+    let frontSetting = null;
+    let backSetting = null;
+    let externalSettings = [];
+    deviceResolutions.forEach(([{deviceId, label}, photoRs, videoRs]) => {
+      const setting = [deviceId, photoRs, videoRs];
+      switch (label) {
+        case cca.views.camera.Options.FRONT_CAMERA_LABEL:
+          frontSetting = setting;
+          break;
+        case cca.views.camera.Options.BACK_CAMERA_LABEL:
+          backSetting = setting;
+          break;
+        case cca.views.camera.Options.EXTERNAL_CAMERA_LABEL:
+          externalSettings.push(setting);
+          break;
+        default:
+          console.error(`Ignore device of unknown label: ${label}`);
+      }
+    });
+    this.resolBroker_.notifyUpdateDeviceResolutions(
+        frontSetting, backSetting, externalSettings);
+  });
 };
 
 /**
@@ -252,20 +322,32 @@
       throw new Error('Device list empty.');
     }
     // Put the selected video device id first.
-    var sorted = devices.map((device) => device.deviceId).sort((a, b) => {
-      if (a == b) {
-        return 0;
-      }
-      if (a == this.videoDeviceId_) {
-        return -1;
-      }
-      return 1;
-    });
-    // Prepended 'null' deviceId means the system default camera. Add it only
-    // when the app is launched (no video-device-id set).
-    if (this.videoDeviceId_ == null) {
-      sorted.unshift(null);
-    }
-    return sorted;
+    return devices
+        .sort((a, b) => {
+          if (a.deviceId == b.deviceId) {
+            return 0;
+          }
+          if (this.videoDeviceId_ ?
+                  (a.deviceId == this.videoDeviceId_) :
+                  (a.label == cca.views.camera.Options.FRONT_CAMERA_LABEL)) {
+            return -1;
+          }
+          return 1;
+        })
+        .map(({deviceId}) => deviceId);
   });
 };
+
+/**
+ * Gets supported photo and video resolutions for specified video device.
+ * @async
+ * @param {string} deviceId Device id of the video device.
+ * @return {[ResolList, ResolList]} Supported photo and video resolutions.
+ */
+cca.views.camera.Options.prototype.getDeviceResolutions =
+    async function(deviceId) {
+  const deviceResolutions = await this.deviceResolutions_;
+  const [, photoRs, videoRs] =
+      deviceResolutions.find(([d]) => d.deviceId == deviceId);
+  return [photoRs, videoRs];
+};
diff --git a/chrome/browser/resources/chromeos/camera/src/js/views/settings.js b/chrome/browser/resources/chromeos/camera/src/js/views/settings.js
index f96b1b4..641fad3 100644
--- a/chrome/browser/resources/chromeos/camera/src/js/views/settings.js
+++ b/chrome/browser/resources/chromeos/camera/src/js/views/settings.js
@@ -17,17 +17,14 @@
 /**
  * Creates the base controller of settings view.
  * @param {string} selector Selector text of the view's root element.
- * @param {Object<string|function(Event=)>} itemHandlers Click-handlers
+ * @param {Object<string|function(Event=)>=} itemHandlers Click-handlers
  *     mapped by element ids.
  * @extends {cca.views.View}
  * @constructor
  */
-cca.views.BaseSettings = function(selector, itemHandlers) {
+cca.views.BaseSettings = function(selector, itemHandlers = {}) {
   cca.views.View.call(this, selector, true, true);
 
-  // End of properties, seal the object.
-  Object.seal(this);
-
   this.root.querySelector('.menu-header button').addEventListener(
       'click', () => this.leave());
   this.root.querySelectorAll('.menu-item').forEach((element) => {
@@ -43,35 +40,13 @@
 };
 
 /**
- * Creates the controller of grid settings view.
- * @extends {cca.views.BaseSettings}
- * @constructor
+ * Opens sub-settings.
+ * @param {string} id Settings identifier.
+ * @private
  */
-cca.views.GridSettings = function() {
-  cca.views.BaseSettings.call(this, '#gridsettings', {});
-
-  // End of properties, seal the object.
-  Object.seal(this);
-};
-
-cca.views.GridSettings.prototype = {
-  __proto__: cca.views.BaseSettings.prototype,
-};
-
-/**
- * Creates the controller of timer settings view.
- * @extends {cca.views.BaseSettings}
- * @constructor
- */
-cca.views.TimerSettings = function() {
-  cca.views.BaseSettings.call(this, '#timersettings', {});
-
-  // End of properties, seal the object.
-  Object.seal(this);
-};
-
-cca.views.TimerSettings.prototype = {
-  __proto__: cca.views.BaseSettings.prototype,
+cca.views.BaseSettings.prototype.openSubSettings = function(id) {
+  // Dismiss master-settings if sub-settings was dimissed by background click.
+  cca.nav.open(id).then((cond) => cond && cond.bkgnd && this.leave());
 };
 
 /**
@@ -83,6 +58,7 @@
   cca.views.BaseSettings.call(this, '#settings', {
     'settings-gridtype': () => this.openSubSettings('gridsettings'),
     'settings-timerdur': () => this.openSubSettings('timersettings'),
+    'settings-resolution': () => this.openSubSettings('resolutionsettings'),
     'settings-feedback': () => this.openFeedback(),
     'settings-help': () => this.openHelp_(),
   });
@@ -99,16 +75,6 @@
 };
 
 /**
- * Opens sub-settings.
- * @param {string} id Settings identifier.
- * @private
- */
-cca.views.MasterSettings.prototype.openSubSettings = function(id) {
-  // Dismiss master-settings if sub-settings was dimissed by background click.
-  cca.nav.open(id).then((cond) => cond && cond.bkgnd && this.leave());
-};
-
-/**
  * Opens feedback.
  * @private
  */
@@ -136,3 +102,370 @@
   window.open(
       'https://support.google.com/chromebook/?p=camera_usage_on_chromebook');
 };
+
+/**
+ * Creates the controller of resolution settings view.
+ * @param {cca.ResolutionEventBroker} resolBroker
+ * @extends {cca.views.BaseSettings}
+ * @constructor
+ */
+cca.views.ResolutionSettings = function(resolBroker) {
+  cca.views.BaseSettings.call(this, '#resolutionsettings', {
+    'settings-front-photores': () => {
+      const element = document.querySelector('#settings-front-photores');
+      if (element.classList.contains('multi-option')) {
+        this.openPhotoResSettings_(
+            this.frontSetting_[0], this.frontSetting_[1], element);
+      }
+    },
+    'settings-front-videores': () => {
+      const element = document.querySelector('#settings-front-videores');
+      if (element.classList.contains('multi-option')) {
+        this.openVideoResSettings_(
+            this.frontSetting_[0], this.frontSetting_[2], element);
+      }
+    },
+    'settings-back-photores': () => {
+      const element = document.querySelector('#settings-back-photores');
+      if (element.classList.contains('multi-option')) {
+        this.openPhotoResSettings_(
+            this.backSetting_[0], this.backSetting_[1], element);
+      }
+    },
+    'settings-back-videores': () => {
+      const element = document.querySelector('#settings-back-videores');
+      if (element.classList.contains('multi-option')) {
+        this.openVideoResSettings_(
+            this.backSetting_[0], this.backSetting_[2], element);
+      }
+    },
+  });
+
+  /**
+   * @type {cca.ResolutionEventBroker}
+   * @private
+   */
+  this.resolBroker_ = resolBroker;
+
+  /**
+   * @type {HTMLElement}
+   * @private
+   */
+  this.resMenu_ = document.querySelector('#resolutionsettings>div.menu');
+
+  /**
+   * @type {HTMLElement}
+   * @private
+   */
+  this.videoResMenu_ =
+      document.querySelector('#videoresolutionsettings>div.menu');
+
+  /**
+   * @type {HTMLElement}
+   * @private
+   */
+  this.photoResMenu_ =
+      document.querySelector('#photoresolutionsettings>div.menu');
+
+  /**
+   * @type {HTMLElement}
+   * @private
+   */
+  this.frontPhotoItem_ = document.querySelector('#settings-front-photores');
+
+  /**
+   * @type {HTMLElement}
+   * @private
+   */
+  this.frontVideoItem_ = document.querySelector('#settings-front-videores');
+
+  /**
+   * @type {HTMLElement}
+   * @private
+   */
+  this.backPhotoItem_ = document.querySelector('#settings-back-photores');
+
+  /**
+   * @type {HTMLElement}
+   * @private
+   */
+  this.backVideoItem_ = document.querySelector('#settings-back-videores');
+
+  /**
+   * @type {HTMLTemplateElement}
+   * @private
+   */
+  this.resItemTempl_ = document.querySelector('#resolution-item-template');
+
+  /**
+   * @type {HTMLTemplateElement}
+   * @private
+   */
+  this.extcamItemTempl_ =
+      document.querySelector('#extcam-resolution-item-template');
+
+  /**
+   * Device id and resolutions of front camera. Null for no front camera.
+   * @type {?DeviceIdResols}
+   * @private
+   */
+  this.frontSetting_ = null;
+
+  /**
+   * Device id and resolutions of back camera. Null for no back camera.
+   * @type {?DeviceIdResols}
+   * @private
+   */
+  this.backSetting_ = null;
+
+  /**
+   * Device id and resolutions of external cameras.
+   * @type {Array<DeviceIdResols>}
+   * @private
+   */
+  this.externalSettings_ = [];
+
+  // End of properties, seal the object.
+  Object.seal(this);
+
+  this.resolBroker_.addUpdateDeviceResolutionsListener(
+      this.updateResolutions.bind(this));
+  this.resolBroker_.addPhotoResolChangeListener(
+      this.updateSelectedPhotoResolution_.bind(this));
+  this.resolBroker_.addVideoResolChangeListener(
+      this.updateSelectedVideoResolution_.bind(this));
+};
+
+cca.views.ResolutionSettings.prototype = {
+  __proto__: cca.views.BaseSettings.prototype,
+};
+
+/**
+ * Template for generating option text from photo resolution width and height.
+ * @param {number} w Resolution width.
+ * @param {number} h Resolution height.
+ * @return {string} Text shown on resolution option item.
+ * @private
+ */
+cca.views.ResolutionSettings.prototype.photoOptTextTempl_ = function(w, h) {
+  return `${Math.round(w * h / 100000) / 10} megapixels (${w}:${h})`;
+};
+
+/**
+ * Template for generating option text from video resolution width and height.
+ * @param {number} w Resolution width.
+ * @param {number} h Resolution height.
+ * @return {string} Text shown on resolution option item.
+ * @private
+ */
+cca.views.ResolutionSettings.prototype.videoOptTextTempl_ = function(w, h) {
+  return `HD ${h}p (${w}:${h})`;
+};
+
+/**
+ * Updates resolutions of front, back camera and external cameras.
+ * @param {?DeviceIdResols} frontSetting Resolutions of front camera.
+ * @param {?DeviceIdResols} backSetting Resolutions of back camera.
+ * @param {Array<DeviceIdResols>} externalSettings Resolutions of external
+ *     cameras.
+ */
+cca.views.ResolutionSettings.prototype.updateResolutions = function(
+    frontSetting, backSetting, externalSettings) {
+  const checkMulti = (item, resolutions, optTextTempl) => {
+    // Filter out resolutions of megapixels < 0.1 i.e. megapixels 0.0
+    resolutions = resolutions.filter(([w, h]) => w * h >= 100000);
+    item.classList.toggle('multi-option', resolutions.length > 1);
+    if (resolutions.length == 1) {
+      const [w, h] = resolutions;
+      item.dataset.sWidth = w;
+      item.dataset.sHeight = h;
+      item.querySelector('.description>span').textContent = optTextTempl(w, h);
+    }
+  };
+
+  // Update front camera setting
+  this.frontSetting_ = frontSetting;
+  cca.state.set('has-front-camera', this.frontSetting_);
+  if (this.frontSetting_) {
+    const [deviceId, photoRs, videoRs] = this.frontSetting_;
+    this.frontPhotoItem_.dataset.deviceId =
+        this.frontVideoItem_.dataset.deviceId = deviceId;
+    checkMulti(this.frontPhotoItem_, photoRs, this.photoOptTextTempl_);
+    checkMulti(this.frontVideoItem_, videoRs, this.videoOptTextTempl_);
+  }
+
+  // Update back camera setting
+  this.backSetting_ = backSetting;
+  cca.state.set('has-back-camera', this.backSetting_);
+  if (this.backSetting_) {
+    const [deviceId, photoRs, videoRs] = this.backSetting_;
+    this.backPhotoItem_.dataset.deviceId =
+        this.backVideoItem_.dataset.deviceId = deviceId;
+    checkMulti(this.backPhotoItem_, photoRs, this.photoOptTextTempl_);
+    checkMulti(this.backVideoItem_, videoRs, this.videoOptTextTempl_);
+  }
+
+  // Update external camera settings
+  this.externalSettings_ = externalSettings;
+  this.resMenu_.querySelectorAll('.menu-item.external-camera')
+      .forEach((element) => element.parentNode.removeChild(element));
+
+  externalSettings.forEach(([deviceId, photoRs, videoRs]) => {
+    const extItem = document.importNode(this.extcamItemTempl_.content, true);
+    const [photoItem, videoItem] = extItem.querySelectorAll('.resol-item');
+    photoItem.dataset.deviceId = videoItem.dataset.deviceId = deviceId;
+    checkMulti(photoItem, photoRs, this.photoOptTextTempl_);
+    checkMulti(videoItem, videoRs, this.videoOptTextTempl_);
+
+    photoItem.addEventListener('click', () => {
+      if (photoItem.classList.contains('multi-option')) {
+        this.openPhotoResSettings_(deviceId, photoRs, photoItem);
+      }
+    });
+    videoItem.addEventListener('click', () => {
+      if (videoItem.classList.contains('multi-option')) {
+        this.openVideoResSettings_(deviceId, videoRs, videoItem);
+      }
+    });
+    this.resMenu_.appendChild(extItem);
+  });
+};
+
+/**
+ * Updates current selected photo resolution.
+ * @param {string} deviceId Device id of the selected resolution.
+ * @param {number} width Width of selected resolution.
+ * @param {number} height Height of selected resolution.
+ * @private
+ */
+cca.views.ResolutionSettings.prototype.updateSelectedPhotoResolution_ =
+    function(deviceId, width, height) {
+  const [photoItem] = this.resMenu_.querySelectorAll(
+      `.resol-item[data-device-id="${deviceId}"]`);
+  photoItem.dataset.sWidth = width;
+  photoItem.dataset.sHeight = height;
+  photoItem.querySelector('.description>span').textContent =
+      this.photoOptTextTempl_(width, height);
+
+  // Update setting option if it's opened.
+  if (cca.state.get('photoresolutionsettings')) {
+    this.photoResMenu_
+        .querySelector(`input[data-width="${width}"][data-height="${height}"]`)
+        .checked = true;
+  }
+};
+
+/**
+ * Updates current selected video resolution.
+ * @param {string} deviceId Device id of the selected resolution.
+ * @param {number} width Width of selected resolution.
+ * @param {number} height Height of selected resolution.
+ * @private
+ */
+cca.views.ResolutionSettings.prototype.updateSelectedVideoResolution_ =
+    function(deviceId, width, height) {
+  const [, videoItem] = this.resMenu_.querySelectorAll(
+      `.resol-item[data-device-id="${deviceId}"]`);
+  videoItem.dataset.sWidth = width;
+  videoItem.dataset.sHeight = height;
+  videoItem.querySelector('.description>span').textContent =
+      this.videoOptTextTempl_(width, height);
+
+  // Update setting option if it's opened.
+  if (cca.state.get('videoresolutionsettings')) {
+    this.videoResMenu_
+        .querySelector(`input[data-width="${width}"][data-height="${height}"]`)
+        .checked = true;
+  }
+};
+
+/**
+ * Opens photo resolution setting view.
+ * @param {string} deviceId Device id of the opened view.
+ * @param {ResolList} resolutions Resolutions to be shown in opened view.
+ * @param {HTMLElement} resolItem Dom element from upper layer holding the
+ *     selected resolution width, height.
+ * @private
+ */
+cca.views.ResolutionSettings.prototype.openPhotoResSettings_ = function(
+    deviceId, resolutions, resolItem) {
+  this.updateMenu_(
+      resolItem, this.photoResMenu_, this.photoOptTextTempl_,
+      (w, h) => this.resolBroker_.requestChangePhotoResol(deviceId, w, h),
+      resolutions, parseInt(resolItem.dataset.sWidth),
+      parseInt(resolItem.dataset.sHeight));
+  this.openSubSettings('photoresolutionsettings');
+};
+
+/**
+ * Opens video resolution setting view.
+ * @param {string} deviceId Device id of the opened view.
+ * @param {ResolList} resolutions Resolutions to be shown in opened view.
+ * @param {HTMLElement} resolItem Dom element from upper layer holding the
+ *     selected resolution width, height.
+ * @private
+ */
+cca.views.ResolutionSettings.prototype.openVideoResSettings_ = function(
+    deviceId, resolutions, resolItem) {
+  this.updateMenu_(
+      resolItem, this.videoResMenu_, this.videoOptTextTempl_,
+      (w, h) => this.resolBroker_.requestChangeVideoResol(deviceId, w, h),
+      resolutions, parseInt(resolItem.dataset.sWidth),
+      parseInt(resolItem.dataset.sHeight));
+  this.openSubSettings('videoresolutionsettings');
+};
+
+/**
+ * Updates resolution menu with specified resolutions.
+ * @param {HTMLElement} resolItem DOM element holding selected resolution.
+ * @param {HTMLElement} menu Menu holding all resolution option elements.
+ * @param {function(number, number): string} optTextTempl Template
+ *     generating text content for each resolution option from its
+ *     width and height.
+ * @param {function(number, number)} onChange Called when selected
+ *     option changed with the its width and height
+ * @param {ResolList} resolutions Resolutions of its width and height to be
+ *      updated with.
+ * @param {number} selectedWidth Width of selected resolution.
+ * @param {number} selectedHeight Height of selected resolution.
+ * @private
+ */
+cca.views.ResolutionSettings.prototype.updateMenu_ = function(
+    resolItem, menu, optTextTempl, onChange, resolutions, selectedWidth,
+    selectedHeight) {
+  const captionText = resolItem.querySelector('.description>span');
+  const updateSelection = (w, h) => {
+    resolItem.dataset.sWidth = w;
+    resolItem.dataset.sHeight = h;
+    captionText.textContent = optTextTempl(w, h);
+  };
+  captionText.textContent = '';
+  menu.querySelectorAll('.menu-item')
+      .forEach((element) => element.parentNode.removeChild(element));
+
+  resolutions.forEach(([w, h], index) => {
+    // TODO(inker): i18n contents in optTextTempl
+    const item = document.importNode(this.resItemTempl_.content, true);
+    const inputElement = item.querySelector('input');
+    item.querySelector('span').textContent = optTextTempl(w, h);
+    inputElement.name = menu.dataset.name;
+    inputElement.dataset.width = w;
+    inputElement.dataset.height = h;
+    if (w == selectedWidth && h == selectedHeight) {
+      updateSelection(w, h);
+      inputElement.checked = true;
+    }
+    inputElement.addEventListener('click', (event) => {
+      if (!cca.state.get('streaming') || cca.state.get('taking')) {
+        event.preventDefault();
+      }
+    });
+    inputElement.addEventListener('change', (event) => {
+      if (inputElement.checked) {
+        updateSelection(w, h);
+        onChange(w, h);
+      }
+    });
+    menu.appendChild(item);
+  });
+};
diff --git a/chrome/browser/resources/chromeos/camera/src/views/main.html b/chrome/browser/resources/chromeos/camera/src/views/main.html
index 9cd3328..1cb3f53 100644
--- a/chrome/browser/resources/chromeos/camera/src/views/main.html
+++ b/chrome/browser/resources/chromeos/camera/src/views/main.html
@@ -12,6 +12,7 @@
     <script src="../js/google-analytics-bundle.js"></script>
     <script src="../js/metrics.js"></script>
     <script src="../js/util.js"></script>
+    <script src="../js/resolution_event_broker.js"></script>
     <script src="../js/toast.js"></script>
     <script src="../js/tooltip.js"></script>
     <script src="../js/state.js"></script>
@@ -164,6 +165,17 @@
           </div>
           <div class="icon end"></div>
         </button>
+        <button class="menu-item circle" id="settings-resolution" tabindex="0"
+                aria-describedby="resolution-desc">
+          <div class="icon"></div>
+          <div>
+            <div>Camera resolution</div>
+            <div class="description" id="resolution-desc" aria-hidden="true">
+              <span></span>
+            </div>
+          </div>
+          <div class="icon end"></div>
+        </button>
         <button class="menu-item circle" id="settings-feedback" tabindex="0">
           <div class="icon"></div>
           <div i18n-content="feedback_button"></div>
@@ -216,6 +228,77 @@
         </label>
       </div>
     </div>
+    <div id="resolutionsettings">
+      <div class="menu">
+        <div class="menu-header circle">
+          <button class="icon" tabindex="0" i18n-aria="back_button"></button>
+          <div>Camera resolutions</div>
+        </div>
+        <div id="builtin-photo-header" class="menu-item">Photo</div>
+        <button class="menu-item resol-item circle"
+                id="settings-front-photores" tabindex="0"
+                aria-describedby="front-photores-desc">
+          <div>
+            <div>Front camera</div>
+            <div class="description" id="front-photores-desc"
+                 aria-hidden="true">
+              <span></span>
+            </div>
+          </div>
+          <div class="icon end"></div>
+        </button>
+        <button class="menu-item resol-item circle"
+                id="settings-back-photores" tabindex="0"
+                aria-describedby="back-photores-desc">
+          <div>
+            <div>Back camera</div>
+            <div class="description" id="back-photores-desc" aria-hidden="true">
+              <span></span>
+            </div>
+          </div>
+          <div class="icon end"></div>
+        </button>
+        <div id="builtin-video-header" class="menu-item">Video</div>
+        <button class="menu-item resol-item circle"
+                id="settings-front-videores" tabindex="0"
+                aria-describedby="front-videores-desc">
+          <div>
+            <div>Front camera</div>
+            <div class="description" id="front-videores-desc" aria-hidden="true">
+              <span></span>
+            </div>
+          </div>
+          <div class="icon end"></div>
+        </button>
+        <button class="menu-item resol-item circle"
+                id="settings-back-videores" tabindex="0"
+                aria-describedby="back-videores-desc">
+          <div>
+            <div>Back camera</div>
+            <div class="description" id="back-videores-desc" aria-hidden="true">
+              <span></span>
+            </div>
+          </div>
+          <div class="icon end"></div>
+        </button>
+      </div>
+    </div>
+    <div id="photoresolutionsettings">
+      <div class="menu" data-name="photores">
+        <div class="menu-header circle">
+          <button class="icon" tabindex="0" i18n-aria="back_button"></button>
+          <div>Photo resolution</div>
+        </div>
+      </div>
+    </div>
+    <div id="videoresolutionsettings">
+      <div class="menu" data-name="videores">
+        <div class="menu-header circle">
+          <button class="icon" tabindex="0" i18n-aria="back_button"></button>
+          <div>Video resolution</div>
+        </div>
+      </div>
+    </div>
     <div class="centered-overlay" id="spinner"></div>
     <div id="browser" role="listbox" i18n-aria="gallery_images">
       <div class="padder">
@@ -264,5 +347,34 @@
     <audio id="sound-shutter" src="../sounds/shutter.ogg" data-timeout="350">
     <audio id="sound-rec-start" src="../sounds/record_start.ogg" data-timeout="200">
     <audio id="sound-rec-end" src="../sounds/record_end.ogg" data-timeout="450">
+    <template id="resolution-item-template">
+      <label class="menu-item circle">
+        <input class="icon" type="radio" tabindex="0">
+        <span></span>
+      </label>
+    </template>
+    <template id="extcam-resolution-item-template">
+        <div class="menu-item external-camera">External camera</div>
+        <button class="menu-item resol-item external-camera"
+              tabindex="0">
+          <div>
+            <div>Photo resolution</div>
+            <div class="description" aria-hidden="true">
+              <span></span>
+            </div>
+          </div>
+          <div class="icon end"></div>
+        </button>
+        <button class="menu-item resol-item circle external-camera"
+              tabindex="0">
+          <div>
+            <div>Video resolution</div>
+            <div class="description" aria-hidden="true">
+              <span></span>
+            </div>
+          </div>
+          <div class="icon end"></div>
+        </button>
+    </template>
   </body>
 </html>
diff --git a/chrome/browser/resources/chromeos/login/screen_arc_terms_of_service.js b/chrome/browser/resources/chromeos/login/screen_arc_terms_of_service.js
index d1f45bb..9f5fc2e 100644
--- a/chrome/browser/resources/chromeos/login/screen_arc_terms_of_service.js
+++ b/chrome/browser/resources/chromeos/login/screen_arc_terms_of_service.js
@@ -33,11 +33,14 @@
      * @private
      */
     getCurrentLanguage_: function() {
-      var languageList = loadTimeData.getValue('languageList');
-      if (languageList) {
-        var language = getSelectedValue(languageList);
-        if (language) {
-          return language;
+      const LANGUAGE_LIST_ID = 'languageList';
+      if (loadTimeData.valueExists(LANGUAGE_LIST_ID)) {
+        var languageList = loadTimeData.getValue(LANGUAGE_LIST_ID);
+        if (languageList) {
+          var language = getSelectedValue(languageList);
+          if (language) {
+            return language;
+          }
         }
       }
       return navigator.language;
diff --git a/chrome/browser/resources/chromeos/select_to_speak/paragraph_utils.js b/chrome/browser/resources/chromeos/select_to_speak/paragraph_utils.js
index 6dc98ed..4388583 100644
--- a/chrome/browser/resources/chromeos/select_to_speak/paragraph_utils.js
+++ b/chrome/browser/resources/chromeos/select_to_speak/paragraph_utils.js
@@ -167,14 +167,17 @@
  * representing a paragraph of inline nodes.
  * @param {Array<!AutomationNode>} nodes List of automation nodes to use.
  * @param {number} index The index into nodes at which to start.
+ * @param {boolean} splitOnLanguage flag to determine if we should split nodes
+ *                  up based on language.
  * @return {ParagraphUtils.NodeGroup} info about the node group
  */
-ParagraphUtils.buildNodeGroup = function(nodes, index) {
+ParagraphUtils.buildNodeGroup = function(nodes, index, splitOnLanguage) {
   let node = nodes[index];
   let next = nodes[index + 1];
   let result = new ParagraphUtils.NodeGroup(
       ParagraphUtils.getFirstBlockAncestor(nodes[index]));
   let staticTextParent = null;
+  let currentLanguage = undefined;
   // TODO: Don't skip nodes. Instead, go through every node in
   // this paragraph from the first to the last in the nodes list.
   // This will catch nodes at the edges of the user's selection like
@@ -216,14 +219,41 @@
         result.nodes.push(newNode);
       }
     }
+
+    // Set currentLanguage if we don't have one yet.
+    // We have to do this before we consider stopping otherwise we miss out on
+    // the last language attribute of each NodeGroup which could be important if
+    // this NodeGroup only contains a single node, or if all previous nodes
+    // lacked any language information.
+    if (!currentLanguage) {
+      currentLanguage = node.detectedLanguage;
+    }
+
+    // Stop if any of following is true:
+    //  1. we have no more nodes to process.
+    //  2. the next node is not part of the same paragraph.
     if (index + 1 >= nodes.length ||
         !ParagraphUtils.inSameParagraph(node, next)) {
       break;
     }
+
+    // Stop if the next node would change our currentLanguage (if we have
+    // one). We allow an undefined detectedLanguage to match with any previous
+    // language, so that we will never break up a NodeGroup on an undefined
+    // detectedLanguage.
+    if (splitOnLanguage && currentLanguage && next.detectedLanguage &&
+        currentLanguage !== next.detectedLanguage) {
+      break;
+    }
+
     index += 1;
     node = next;
     next = nodes[index + 1];
   }
+
+  if (splitOnLanguage && currentLanguage) {
+    result.detectedLanguage = currentLanguage;
+  }
   result.endIndex = index;
   return result;
 };
@@ -264,6 +294,12 @@
    * @type {number}
    */
   this.endIndex = -1;
+
+  /**
+   * Language and country code for all nodes within this NodeGroup.
+   * @type {string|undefined}
+   */
+  this.detectedLanguage = undefined;
 };
 
 /**
diff --git a/chrome/browser/resources/chromeos/select_to_speak/paragraph_utils_unittest.gtestjs b/chrome/browser/resources/chromeos/select_to_speak/paragraph_utils_unittest.gtestjs
index 047bb6f4d..8830b8a 100644
--- a/chrome/browser/resources/chromeos/select_to_speak/paragraph_utils_unittest.gtestjs
+++ b/chrome/browser/resources/chromeos/select_to_speak/paragraph_utils_unittest.gtestjs
@@ -167,7 +167,7 @@
       root: root};
   let text3 = {role: 'staticText', parent: paragraph2, name: 'text3',
       root: root};
-  let result = ParagraphUtils.buildNodeGroup([text1, text2, text3], 0);
+  let result = ParagraphUtils.buildNodeGroup([text1, text2, text3], 0, false /* do not split on language */);
   assertEquals('text1 text2 ', result.text);
   assertEquals(1, result.endIndex);
   assertEquals(2, result.nodes.length);
@@ -178,6 +178,144 @@
   assertEquals(paragraph1, result.blockParent);
 });
 
+TEST_F('SelectToSpeakParagraphUnitTest',
+       'BuildNodeGroupStopsAtLanguageBoundary', function() {
+  let splitOnLanguage = true;
+
+  // When the detectedLanguage changes from en-US to fr-FR we expect to break
+  // the NodeGroup.
+  let root = {role: 'rootWebArea'};
+  let text1 = {role: 'staticText', parent: root, name: 'text1',
+      root: root, detectedLanguage: "en-US"};
+  let text2 = {role: 'staticText', parent: root, name: 'text2',
+      root: root, detectedLanguage: "en-US"};
+  let text3 = {role: 'staticText', parent: root, name: 'text3',
+      root: root, detectedLanguage: "fr-FR"};
+
+  let result1 = ParagraphUtils.buildNodeGroup([text1, text2, text3], 0, splitOnLanguage);
+  assertEquals('text1 text2 ', result1.text);
+  assertEquals(1, result1.endIndex);
+  assertEquals(2, result1.nodes.length);
+  assertEquals(0, result1.nodes[0].startChar);
+  assertEquals(text1, result1.nodes[0].node);
+  assertEquals(6, result1.nodes[1].startChar);
+  assertEquals(text2, result1.nodes[1].node);
+  assertEquals("en-US", result1.detectedLanguage);
+
+  let result2 = ParagraphUtils.buildNodeGroup([text1, text2, text3], 2, splitOnLanguage);
+  assertEquals('text3 ', result2.text);
+  assertEquals(2, result2.endIndex);
+  assertEquals(1, result2.nodes.length);
+  assertEquals(0, result2.nodes[0].startChar);
+  assertEquals(text3, result2.nodes[0].node);
+  assertEquals("fr-FR", result2.detectedLanguage);
+});
+
+TEST_F('SelectToSpeakParagraphUnitTest',
+       'BuildNodeGroupStopsAtLanguageBoundaryAllUndefined', function() {
+  let splitOnLanguage = true;
+
+  // If no detectedLanguage is defined then we should not split at all....
+  let root = {role: 'rootWebArea'};
+  let text1 = {role: 'staticText', parent: root, name: 'text1',
+      root: root};
+  let text2 = {role: 'staticText', parent: root, name: 'text2',
+      root: root};
+  let text3 = {role: 'staticText', parent: root, name: 'text3',
+      root: root};
+  let result = ParagraphUtils.buildNodeGroup([text1, text2, text3], 0, splitOnLanguage);
+  assertEquals('text1 text2 text3 ', result.text);
+  assertEquals(2, result.endIndex);
+  assertEquals(3, result.nodes.length);
+  assertEquals(0, result.nodes[0].startChar);
+  assertEquals(text1, result.nodes[0].node);
+  assertEquals(6, result.nodes[1].startChar);
+  assertEquals(text2, result.nodes[1].node);
+  assertEquals(12, result.nodes[2].startChar);
+  assertEquals(text3, result.nodes[2].node);
+  assertEquals(undefined, result.detectedLanguage);
+});
+
+TEST_F('SelectToSpeakParagraphUnitTest',
+       'BuildNodeGroupStopsAtLanguageBoundaryLastNode', function() {
+  let splitOnLanguage = true;
+
+  // our NodeGroup should get the first defined detectedLanguage
+  let root = {role: 'rootWebArea'};
+  let text1 = {role: 'staticText', parent: root, name: 'text1',
+      root: root};
+  let text2 = {role: 'staticText', parent: root, name: 'text2',
+      root: root};
+  let text3 = {role: 'staticText', parent: root, name: 'text3',
+      root: root, detectedLanguage: 'fr-FR'};
+  let result = ParagraphUtils.buildNodeGroup([text1, text2, text3], 0, splitOnLanguage);
+  assertEquals('text1 text2 text3 ', result.text);
+  assertEquals(2, result.endIndex);
+  assertEquals(3, result.nodes.length);
+  assertEquals(0, result.nodes[0].startChar);
+  assertEquals(text1, result.nodes[0].node);
+  assertEquals(6, result.nodes[1].startChar);
+  assertEquals(text2, result.nodes[1].node);
+  assertEquals(12, result.nodes[2].startChar);
+  assertEquals(text3, result.nodes[2].node);
+  assertEquals("fr-FR", result.detectedLanguage);
+});
+
+TEST_F('SelectToSpeakParagraphUnitTest',
+       'BuildNodeGroupSplitOnLanguageDisabled', function() {
+  // Test behaviour with splitOnLanguage disabled. This is to show that we
+  // haven't introduced an obvious regression.
+  let splitOnLanguage = false;
+
+  let root = {role: 'rootWebArea'};
+  let text1 = {role: 'staticText', parent: root, name: 'text1',
+      root: root};
+  let text2 = {role: 'staticText', parent: root, name: 'text2',
+      root: root, detectedLanguage: "en-US"};
+  let text3 = {role: 'staticText', parent: root, name: 'text3',
+      root: root};
+  let text4 = {role: 'staticText', parent: root, name: 'text4',
+      root: root, detectedLanguage: "fr-FR"};
+  let result = ParagraphUtils.buildNodeGroup([text1, text2, text3, text4], 0, splitOnLanguage);
+  assertEquals('text1 text2 text3 text4 ', result.text);
+  assertEquals(3, result.endIndex);
+  assertEquals(4, result.nodes.length);
+  assertEquals(text1, result.nodes[0].node);
+  assertEquals(text4, result.nodes[3].node);
+  assertEquals(undefined, result.detectedLanguage);
+});
+
+TEST_F('SelectToSpeakParagraphUnitTest',
+       'BuildNodeGroupStopsAtLanguageBoundarySomeUndefined', function() {
+  let splitOnLanguage = true;
+
+  // We never want to break up a NodeGroup based on an undefined
+  // detectedLanguage, instead we allow an undefined detectedLanguage to match
+  // any other language.
+  // The language for the NodeGroup will be determined by the first defined
+  // detectedLanguage.
+  let root = {role: 'rootWebArea'};
+  let text1 = {role: 'staticText', parent: root, name: 'text1',
+      root: root};
+  let text2 = {role: 'staticText', parent: root, name: 'text2',
+      root: root, detectedLanguage: "en-US"};
+  let text3 = {role: 'staticText', parent: root, name: 'text3',
+      root: root};
+  let text4 = {role: 'staticText', parent: root, name: 'text4',
+      root: root, detectedLanguage: "fr-FR"};
+  let result = ParagraphUtils.buildNodeGroup([text1, text2, text3, text4], 0, splitOnLanguage);
+  assertEquals('text1 text2 text3 ', result.text);
+  assertEquals(2, result.endIndex);
+  assertEquals(3, result.nodes.length);
+  assertEquals(0, result.nodes[0].startChar);
+  assertEquals(text1, result.nodes[0].node);
+  assertEquals(6, result.nodes[1].startChar);
+  assertEquals(text2, result.nodes[1].node);
+  assertEquals(12, result.nodes[2].startChar);
+  assertEquals(text3, result.nodes[2].node);
+  assertEquals("en-US", result.detectedLanguage);
+});
+
 TEST_F('SelectToSpeakParagraphUnitTest', 'BuildNodeGroupIncludesLinks',
     function() {
   let root = {role: 'rootWebArea'};
@@ -191,7 +329,7 @@
   let link = {role: 'link', parent: paragraph1, root: root};
   let linkText = {role: 'staticText', parent: link, name: 'linkText',
       root: root};
-  let result = ParagraphUtils.buildNodeGroup([text1, text2, linkText], 0);
+  let result = ParagraphUtils.buildNodeGroup([text1, text2, linkText], 0, false /* do not split on language */);
   assertEquals('text1 linkText ', result.text);
   assertEquals(2, result.endIndex);
   assertEquals(2, result.nodes.length);
@@ -213,7 +351,7 @@
 
   // If there is no value, it should use the name.
   searchBar.value = '';
-  result = ParagraphUtils.buildNodeGroup([searchBar], 0);
+  result = ParagraphUtils.buildNodeGroup([searchBar], 0, false /* do not split on language */);
   assertEquals('Address and search bar ', result.text);
 });
 
@@ -227,6 +365,6 @@
   let inline2 = {role: 'inlineTextBox', parent: text2, root: root,
       name: 'world!'};
 
-  let result = ParagraphUtils.buildNodeGroup([inline1, inline2], 0);
+  let result = ParagraphUtils.buildNodeGroup([inline1, inline2], 0, false /* do not split on language */);
   assertEquals('Hello, world! ', result.text);
 });
diff --git a/chrome/browser/resources/chromeos/select_to_speak/prefs_manager.js b/chrome/browser/resources/chromeos/select_to_speak/prefs_manager.js
index 50bde8e..2720b7b 100644
--- a/chrome/browser/resources/chromeos/select_to_speak/prefs_manager.js
+++ b/chrome/browser/resources/chromeos/select_to_speak/prefs_manager.js
@@ -259,7 +259,7 @@
 /**
  * Generates the basic speech options for Select-to-Speak based on user
  * preferences. Call for each chrome.tts.speak.
- * @return {!Object} options The TTS options.
+ * @return {Object} options The TTS options.
  * @public
  */
 PrefsManager.prototype.speechOptions = function() {
diff --git a/chrome/browser/resources/chromeos/select_to_speak/select_to_speak.js b/chrome/browser/resources/chromeos/select_to_speak/select_to_speak.js
index b0a3d48..90427c0 100644
--- a/chrome/browser/resources/chromeos/select_to_speak/select_to_speak.js
+++ b/chrome/browser/resources/chromeos/select_to_speak/select_to_speak.js
@@ -107,6 +107,17 @@
 
   this.runContentScripts_();
   this.setUpEventListeners_();
+
+  /**
+   * Feature flag controlling STS language detection integration.
+   * @type {boolean}
+   */
+  this.enableLanguageDetectionIntegration_ = false;
+  // TODO(chrishall): do we want to (also?) expose this in preferences?
+  chrome.commandLinePrivate.hasSwitch(
+      'enable-experimental-accessibility-language-detection', (result) => {
+        this.enableLanguageDetectionIntegration_ = result;
+      });
 };
 
 /** @const {number} */
@@ -565,7 +576,9 @@
   startSpeechQueue_: function(nodes, opt_startIndex, opt_endIndex) {
     this.prepareForSpeech_();
     for (var i = 0; i < nodes.length; i++) {
-      let nodeGroup = ParagraphUtils.buildNodeGroup(nodes, i);
+      let nodeGroup = ParagraphUtils.buildNodeGroup(
+          nodes, i, this.enableLanguageDetectionIntegration_);
+
       if (i == 0) {
         // We need to start in the middle of a node. Remove all text before
         // the start index so that it is not spoken.
@@ -608,7 +621,14 @@
         continue;
       }
 
-      let options = this.prefsManager_.speechOptions();
+      let options = {};
+      /* Copy options so we can add lang below */
+      Object.assign(options, this.prefsManager_.speechOptions());
+      if (this.enableLanguageDetectionIntegration_ &&
+          nodeGroup.detectedLanguage) {
+        options.lang = nodeGroup.detectedLanguage;
+      }
+
       options.onEvent = (event) => {
         if (event.type == 'start' && nodeGroup.nodes.length > 0) {
           this.onStateChanged_(SelectToSpeakState.SPEAKING);
diff --git a/chrome/browser/resources/feedback/OWNERS b/chrome/browser/resources/feedback/OWNERS
index c32d770..bd30e9f 100644
--- a/chrome/browser/resources/feedback/OWNERS
+++ b/chrome/browser/resources/feedback/OWNERS
@@ -1,5 +1,4 @@
 afakhry@chromium.org
-rkc@chromium.org
 jkardatzke@chromium.org
 
 # COMPONENT: Platform>Apps>Feedback
diff --git a/chrome/browser/resources/media/media_engagement.js b/chrome/browser/resources/media/media_engagement.js
index e1df7c58..5491928 100644
--- a/chrome/browser/resources/media/media_engagement.js
+++ b/chrome/browser/resources/media/media_engagement.js
@@ -136,6 +136,8 @@
   configTableBody.appendChild(createConfigRow(
       'Preload MEI data', formatFeatureFlag(config.featurePreloadData)));
   configTableBody.appendChild(createConfigRow(
+      'MEI for HTTPS only', formatFeatureFlag(config.featureHttpsOnly)));
+  configTableBody.appendChild(createConfigRow(
       'Autoplay disable settings',
       formatFeatureFlag(config.featureAutoplayDisableSettings)));
   configTableBody.appendChild(createConfigRow(
diff --git a/chrome/browser/resources/settings/people_page/account_manager.html b/chrome/browser/resources/settings/people_page/account_manager.html
index 61398272..a4b5f75 100644
--- a/chrome/browser/resources/settings/people_page/account_manager.html
+++ b/chrome/browser/resources/settings/people_page/account_manager.html
@@ -61,7 +61,14 @@
       }
     </style>
 
-    <div class="settings-box first">$i18n{accountManagerDescription}</div>
+    <div class="settings-box first">
+      <span>
+        $i18n{accountManagerDescription}
+        <a href="$i18nRaw{accountManagerLearnMoreUrl}" target="_blank">
+          $i18n{learnMore}
+        </a>
+      </span>
+    </div>
 
     <div class="settings-box first">
       <div id="account-list-header" class="flex">
@@ -99,7 +106,7 @@
               <!-- Else, display a re-authentication message -->
               <template is="dom-if" if="[[!item.isSignedIn]]">
                 <span style="color:red">
-                  $i18n{accountManagerSignedOutAccountName}
+                  [[getAccountManagerSignedOutName_(item.unmigrated)]]
                 </span>
               </template>
 
@@ -111,7 +118,7 @@
               if="[[shouldShowReauthenticationButton_(item)]]">
             <paper-button class="reauth-button"
                 on-click="onReauthenticationTap_">
-              $i18n{accountManagerReauthenticationLabel}
+              [[getAccountManagerSignedOutLabel_(item.unmigrated)]]
             </paper-button>
           </template>
 
@@ -124,7 +131,7 @@
           <!-- Else, display a hamburger menu for removing the account -->
           <template is="dom-if" if="[[!item.isDeviceAccount]]">
             <cr-icon-button class="icon-more-vert" title="$i18n{moreActions}"
-                  on-click="onAccountActionsMenuButtonTap_"></cr-icon-button>
+                on-click="onAccountActionsMenuButtonTap_"></cr-icon-button>
           </template>
         </div>
       </template>
@@ -132,8 +139,7 @@
       <div class="clear settings-box"></div>
 
       <cr-action-menu>
-        <button class="dropdown-item"
-            on-click="onRemoveAccountTap_">
+        <button class="dropdown-item" on-click="onRemoveAccountTap_">
           $i18n{removeAccountLabel}
         </button>
       </cr-action-menu>
diff --git a/chrome/browser/resources/settings/people_page/account_manager.js b/chrome/browser/resources/settings/people_page/account_manager.js
index eb24b96..16e3d27a 100644
--- a/chrome/browser/resources/settings/people_page/account_manager.js
+++ b/chrome/browser/resources/settings/people_page/account_manager.js
@@ -116,6 +116,24 @@
   },
 
   /**
+   * @param {boolean} unmigrated
+   * @private
+   */
+  getAccountManagerSignedOutName_: function(unmigrated) {
+    return this.i18n(unmigrated ? 'accountManagerUnmigratedAccountName'
+                                : 'accountManagerSignedOutAccountName');
+  },
+
+  /**
+   * @param {boolean} unmigrated
+   * @private
+   */
+  getAccountManagerSignedOutLabel_: function(unmigrated) {
+    return this.i18n(unmigrated ? 'accountManagerMigrationLabel'
+                                : 'accountManagerReauthenticationLabel');
+  },
+
+  /**
    * @param {!CustomEvent<!{model: !{item: !settings.Account}}>} event
    * @private
    */
diff --git a/chrome/browser/resources/settings/people_page/account_manager_browser_proxy.js b/chrome/browser/resources/settings/people_page/account_manager_browser_proxy.js
index b0be2bf..6533eb0 100644
--- a/chrome/browser/resources/settings/people_page/account_manager_browser_proxy.js
+++ b/chrome/browser/resources/settings/people_page/account_manager_browser_proxy.js
@@ -16,6 +16,7 @@
  *   accountType: number,
  *   isDeviceAccount: boolean,
  *   isSignedIn: boolean,
+ *   unmigrated: boolean,
  *   fullName: string,
  *   email: string,
  *   pic: string,
diff --git a/chrome/browser/safe_browsing/incident_reporting/incident_reporting_service_unittest.cc b/chrome/browser/safe_browsing/incident_reporting/incident_reporting_service_unittest.cc
index c5a7a2c..5c40203 100644
--- a/chrome/browser/safe_browsing/incident_reporting/incident_reporting_service_unittest.cc
+++ b/chrome/browser/safe_browsing/incident_reporting/incident_reporting_service_unittest.cc
@@ -267,7 +267,8 @@
         profile_name, std::move(prefs), base::ASCIIToUTF16(profile_name),
         0,              // avatar_id (unused)
         std::string(),  // supervised_user_id (unused)
-        TestingProfile::TestingFactories());
+        TestingProfile::TestingFactories(),
+        /*override_new_profile=*/base::Optional<bool>(false));
   }
 
   // Configures a callback to run when the next upload is started that will post
diff --git a/chrome/browser/safe_browsing/incident_reporting/platform_state_store_win_unittest.cc b/chrome/browser/safe_browsing/incident_reporting/platform_state_store_win_unittest.cc
index f874b54..8bf892d 100644
--- a/chrome/browser/safe_browsing/incident_reporting/platform_state_store_win_unittest.cc
+++ b/chrome/browser/safe_browsing/incident_reporting/platform_state_store_win_unittest.cc
@@ -16,7 +16,6 @@
 #include "chrome/test/base/testing_profile_manager.h"
 #include "components/pref_registry/pref_registry_syncable.h"
 #include "components/prefs/pref_notifier_impl.h"
-#include "components/prefs/testing_pref_store.h"
 #include "components/sync_preferences/testing_pref_service_syncable.h"
 #include "content/public/test/test_browser_thread_bundle.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -51,24 +50,13 @@
       profile_manager_.DeleteTestingProfile(kProfileName_);
       profile_ = nullptr;
     }
-    // Create a profile with a user PrefStore that can be manipulated.
-    TestingPrefStore* user_pref_store = new TestingPrefStore();
-    // Profile::IsNewProfile() returns true/false on the basis of the pref
-    // store's read_error property. A profile is considered "New" if it didn't
-    // have a user prefs file.
-    user_pref_store->set_read_error(
-        new_profile ? PersistentPrefStore::PREF_READ_ERROR_NO_FILE
-                    : PersistentPrefStore::PREF_READ_ERROR_NONE);
-    // Ownership of |user_pref_store| is passed to the service.
     std::unique_ptr<sync_preferences::TestingPrefServiceSyncable> prefs(
-        new sync_preferences::TestingPrefServiceSyncable(
-            new TestingPrefStore(), new TestingPrefStore(), user_pref_store,
-            new TestingPrefStore(), new user_prefs::PrefRegistrySyncable(),
-            new PrefNotifierImpl()));
+        new sync_preferences::TestingPrefServiceSyncable);
     RegisterUserProfilePrefs(prefs->registry());
     profile_ = profile_manager_.CreateTestingProfile(
         kProfileName_, std::move(prefs), base::UTF8ToUTF16(kProfileName_), 0,
-        std::string(), TestingProfile::TestingFactories());
+        std::string(), TestingProfile::TestingFactories(),
+        base::Optional<bool>(new_profile));
     if (new_profile)
       ASSERT_TRUE(profile_->IsNewProfile());
     else
diff --git a/chrome/browser/safe_browsing/incident_reporting/state_store_unittest.cc b/chrome/browser/safe_browsing/incident_reporting/state_store_unittest.cc
index 67a4dae..179fba9 100644
--- a/chrome/browser/safe_browsing/incident_reporting/state_store_unittest.cc
+++ b/chrome/browser/safe_browsing/incident_reporting/state_store_unittest.cc
@@ -122,7 +122,8 @@
     profile_ = profile_manager_.CreateTestingProfile(
         kProfileName_, factory.CreateSyncable(pref_registry),
         base::UTF8ToUTF16(kProfileName_), 0, std::string(),
-        TestingProfile::TestingFactories());
+        TestingProfile::TestingFactories(),
+        /*override_new_profile=*/base::Optional<bool>(false));
   }
 
   static const char kProfileName_[];
diff --git a/chrome/browser/signin/account_consistency_mode_manager.cc b/chrome/browser/signin/account_consistency_mode_manager.cc
index ae4cea93..23ac951 100644
--- a/chrome/browser/signin/account_consistency_mode_manager.cc
+++ b/chrome/browser/signin/account_consistency_mode_manager.cc
@@ -68,9 +68,7 @@
   return AccountConsistencyModeManagerFactory::GetForProfile(profile);
 }
 
-AccountConsistencyModeManager::AccountConsistencyModeManager(
-    Profile* profile,
-    bool auto_migrate_to_dice)
+AccountConsistencyModeManager::AccountConsistencyModeManager(Profile* profile)
     : profile_(profile),
       account_consistency_(signin::AccountConsistencyMethod::kDisabled),
       account_consistency_initialized_(false) {
@@ -92,8 +90,7 @@
   account_consistency_ = ComputeAccountConsistencyMethod(profile_);
 
 #if BUILDFLAG(ENABLE_DICE_SUPPORT)
-  bool is_ready_for_dice =
-      IsReadyForDiceMigration(profile_, auto_migrate_to_dice);
+  bool is_ready_for_dice = IsReadyForDiceMigration(profile_);
   if (is_ready_for_dice &&
       signin::DiceMethodGreaterOrEqual(
           account_consistency_, AccountConsistencyMethod::kDiceMigration)) {
@@ -161,11 +158,9 @@
 }
 
 // static
-bool AccountConsistencyModeManager::IsReadyForDiceMigration(
-    Profile* profile,
-    bool auto_migrate_to_dice) {
+bool AccountConsistencyModeManager::IsReadyForDiceMigration(Profile* profile) {
   return ShouldBuildServiceForProfile(profile) &&
-         (auto_migrate_to_dice ||
+         (profile->IsNewProfile() ||
           profile->GetPrefs()->GetBoolean(kDiceMigrationOnStartupPref));
 }
 #endif  // BUILDFLAG(ENABLE_DICE_SUPPORT)
diff --git a/chrome/browser/signin/account_consistency_mode_manager.h b/chrome/browser/signin/account_consistency_mode_manager.h
index 81ce7da..3a898c7 100644
--- a/chrome/browser/signin/account_consistency_mode_manager.h
+++ b/chrome/browser/signin/account_consistency_mode_manager.h
@@ -39,7 +39,7 @@
   // May return nullptr if there is none (e.g. in incognito).
   static AccountConsistencyModeManager* GetForProfile(Profile* profile);
 
-  AccountConsistencyModeManager(Profile* profile, bool auto_migrate_to_dice);
+  explicit AccountConsistencyModeManager(Profile* profile);
   ~AccountConsistencyModeManager() override;
 
   static void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry);
@@ -76,7 +76,7 @@
 
  private:
   FRIEND_TEST_ALL_PREFIXES(AccountConsistencyModeManagerTest,
-                           MigratePreDiceProfileAtCreation);
+                           MigrateAtCreation);
   FRIEND_TEST_ALL_PREFIXES(AccountConsistencyModeManagerTest,
                            SigninAllowedChangesDiceState);
   FRIEND_TEST_ALL_PREFIXES(AccountConsistencyModeManagerTest,
@@ -88,8 +88,7 @@
   static void SetDiceMigrationOnStartup(PrefService* prefs, bool migrate);
 
   // Returns true if migration can happen on the next startup.
-  static bool IsReadyForDiceMigration(Profile* profile,
-                                      bool auto_migrate_to_dice);
+  static bool IsReadyForDiceMigration(Profile* profile);
 #endif
 
   // Returns the account consistency method for the current profile.
diff --git a/chrome/browser/signin/account_consistency_mode_manager_factory.cc b/chrome/browser/signin/account_consistency_mode_manager_factory.cc
index c7b3d09..e739f36 100644
--- a/chrome/browser/signin/account_consistency_mode_manager_factory.cc
+++ b/chrome/browser/signin/account_consistency_mode_manager_factory.cc
@@ -38,11 +38,8 @@
   DCHECK(!context->IsOffTheRecord());
   Profile* profile = Profile::FromBrowserContext(context);
 
-  // New profiles are consistent at creation and can be migrated immediately
-  // without going through full migration (which requires restarting Chrome).
-  bool auto_migrate_to_dice = profile->WasCreatedByVersionOrLater("75.0.0.0");
   return AccountConsistencyModeManager::ShouldBuildServiceForProfile(profile)
-             ? new AccountConsistencyModeManager(profile, auto_migrate_to_dice)
+             ? new AccountConsistencyModeManager(profile)
              : nullptr;
 }
 
diff --git a/chrome/browser/signin/account_consistency_mode_manager_test_util.cc b/chrome/browser/signin/account_consistency_mode_manager_test_util.cc
deleted file mode 100644
index 38e2e3d..0000000
--- a/chrome/browser/signin/account_consistency_mode_manager_test_util.cc
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright 2019 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "chrome/browser/signin/account_consistency_mode_manager_test_util.h"
-
-#include "chrome/browser/signin/account_consistency_mode_manager.h"
-#include "chrome/browser/signin/account_consistency_mode_manager_factory.h"
-#include "chrome/test/base/testing_profile.h"
-
-std::unique_ptr<TestingProfile> BuildPreDiceProfile() {
-  TestingProfile::Builder builder;
-  builder.AddTestingFactory(
-      AccountConsistencyModeManagerFactory::GetInstance(),
-      base::BindRepeating([](content::BrowserContext* context)
-                              -> std::unique_ptr<KeyedService> {
-        return std::make_unique<AccountConsistencyModeManager>(
-            Profile::FromBrowserContext(context),
-            /*auto_migrate_to_dice=*/false);
-      }));
-  return builder.Build();
-}
diff --git a/chrome/browser/signin/account_consistency_mode_manager_test_util.h b/chrome/browser/signin/account_consistency_mode_manager_test_util.h
deleted file mode 100644
index 56e4e30..0000000
--- a/chrome/browser/signin/account_consistency_mode_manager_test_util.h
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright 2019 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef CHROME_BROWSER_SIGNIN_ACCOUNT_CONSISTENCY_MODE_MANAGER_TEST_UTIL_H_
-#define CHROME_BROWSER_SIGNIN_ACCOUNT_CONSISTENCY_MODE_MANAGER_TEST_UTIL_H_
-
-#include <memory>
-
-class TestingProfile;
-
-// Builds a profile in pre-Dice account consistency mode.
-// The profile is in Dice migration but does not migrate automatically because
-// it was created before Dice was launched.
-std::unique_ptr<TestingProfile> BuildPreDiceProfile();
-
-#endif  // CHROME_BROWSER_SIGNIN_ACCOUNT_CONSISTENCY_MODE_MANAGER_TEST_UTIL_H_
diff --git a/chrome/browser/signin/account_consistency_mode_manager_unittest.cc b/chrome/browser/signin/account_consistency_mode_manager_unittest.cc
index c4faf56..d1f3f65 100644
--- a/chrome/browser/signin/account_consistency_mode_manager_unittest.cc
+++ b/chrome/browser/signin/account_consistency_mode_manager_unittest.cc
@@ -5,7 +5,6 @@
 #include "chrome/browser/signin/account_consistency_mode_manager.h"
 
 #include <memory>
-#include <string>
 #include <utility>
 
 #include "base/command_line.h"
@@ -13,8 +12,6 @@
 #include "build/build_config.h"
 #include "build/buildflag.h"
 #include "chrome/browser/prefs/browser_prefs.h"
-#include "chrome/browser/signin/account_consistency_mode_manager_factory.h"
-#include "chrome/browser/signin/account_consistency_mode_manager_test_util.h"
 #include "chrome/browser/signin/scoped_account_consistency.h"
 #include "chrome/browser/supervised_user/supervised_user_constants.h"
 #include "chrome/common/pref_names.h"
@@ -28,24 +25,37 @@
 #include "content/public/test/test_browser_thread_bundle.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
+namespace {
+
+std::unique_ptr<TestingProfile> BuildTestingProfile(bool is_new_profile) {
+  TestingProfile::Builder profile_builder;
+  profile_builder.OverrideIsNewProfile(is_new_profile);
+  std::unique_ptr<TestingProfile> profile = profile_builder.Build();
+  EXPECT_EQ(is_new_profile, profile->IsNewProfile());
+  return profile;
+}
+
+}  // namespace
+
 // Check the default account consistency method.
 TEST(AccountConsistencyModeManagerTest, DefaultValue) {
   content::TestBrowserThreadBundle test_thread_bundle;
-  TestingProfile profile;
+  std::unique_ptr<TestingProfile> profile =
+      BuildTestingProfile(/*is_new_profile=*/false);
 
 #if BUILDFLAG(ENABLE_MIRROR)
   EXPECT_EQ(signin::AccountConsistencyMethod::kMirror,
-            AccountConsistencyModeManager::GetMethodForProfile(&profile));
+            AccountConsistencyModeManager::GetMethodForProfile(profile.get()));
 #elif BUILDFLAG(ENABLE_DICE_SUPPORT)
-  EXPECT_EQ(signin::AccountConsistencyMethod::kDice,
-            AccountConsistencyModeManager::GetMethodForProfile(&profile));
+  EXPECT_EQ(signin::AccountConsistencyMethod::kDiceMigration,
+            AccountConsistencyModeManager::GetMethodForProfile(profile.get()));
 #else
   EXPECT_EQ(signin::AccountConsistencyMethod::kDisabled,
-            AccountConsistencyModeManager::GetMethodForProfile(&profile));
+            AccountConsistencyModeManager::GetMethodForProfile(profile.get()));
   EXPECT_FALSE(
-      AccountConsistencyModeManager::IsMirrorEnabledForProfile(&profile));
+      AccountConsistencyModeManager::IsMirrorEnabledForProfile(profile.get()));
   EXPECT_FALSE(
-      AccountConsistencyModeManager::IsDiceEnabledForProfile(&profile));
+      AccountConsistencyModeManager::IsDiceEnabledForProfile(profile.get()));
 #endif
 }
 
@@ -58,7 +68,7 @@
     bool expect_dice_enabled;
   } test_cases[] = {
 #if BUILDFLAG(ENABLE_DICE_SUPPORT)
-    {signin::AccountConsistencyMethod::kDiceMigration, false, true},
+    {signin::AccountConsistencyMethod::kDiceMigration, false, false},
     {signin::AccountConsistencyMethod::kDice, false, true},
 #else
     {signin::AccountConsistencyMethod::kMirror, true, false}
@@ -67,39 +77,8 @@
 
   for (const TestCase& test_case : test_cases) {
     ScopedAccountConsistency scoped_method(test_case.method);
-    TestingProfile profile;
-
-    if (test_case.expect_dice_enabled) {
-      EXPECT_EQ(signin::AccountConsistencyMethod::kDice,
-                AccountConsistencyModeManager::GetMethodForProfile(&profile));
-    } else if (test_case.expect_mirror_enabled) {
-      EXPECT_EQ(signin::AccountConsistencyMethod::kMirror,
-                AccountConsistencyModeManager::GetMethodForProfile(&profile));
-    }
-    EXPECT_EQ(
-        test_case.expect_mirror_enabled,
-        AccountConsistencyModeManager::IsMirrorEnabledForProfile(&profile));
-    EXPECT_EQ(test_case.expect_dice_enabled,
-              AccountConsistencyModeManager::IsDiceEnabledForProfile(&profile));
-  }
-}
-
-#if BUILDFLAG(ENABLE_DICE_SUPPORT)
-TEST(AccountConsistencyModeManagerTest, BasicPreDice) {
-  content::TestBrowserThreadBundle test_thread_bundle;
-
-  struct TestCase {
-    signin::AccountConsistencyMethod method;
-    bool expect_mirror_enabled;
-    bool expect_dice_enabled;
-  } test_cases[] = {
-      {signin::AccountConsistencyMethod::kDiceMigration, false, false},
-      {signin::AccountConsistencyMethod::kDice, false, true},
-  };
-
-  for (const TestCase& test_case : test_cases) {
-    ScopedAccountConsistency scoped_method(test_case.method);
-    std::unique_ptr<Profile> profile = BuildPreDiceProfile();
+    std::unique_ptr<TestingProfile> profile =
+        BuildTestingProfile(/*is_new_profile=*/false);
 
     EXPECT_EQ(
         test_case.method,
@@ -113,25 +92,26 @@
   }
 }
 
+#if BUILDFLAG(ENABLE_DICE_SUPPORT)
 // Checks that changing the signin-allowed pref changes the Dice state on next
 // startup.
 TEST(AccountConsistencyModeManagerTest, SigninAllowedChangesDiceState) {
   ScopedAccountConsistencyDice scoped_dice;
   content::TestBrowserThreadBundle test_thread_bundle;
-  TestingProfile profile;
+  std::unique_ptr<TestingProfile> profile =
+      BuildTestingProfile(/*is_new_profile=*/false);
 
   {
     // First startup.
-    AccountConsistencyModeManager manager(&profile,
-                                          /*auto_migrate_to_dice=*/true);
-    EXPECT_TRUE(profile.GetPrefs()->GetBoolean(prefs::kSigninAllowed));
+    AccountConsistencyModeManager manager(profile.get());
+    EXPECT_TRUE(profile->GetPrefs()->GetBoolean(prefs::kSigninAllowed));
     EXPECT_TRUE(
-        profile.GetPrefs()->GetBoolean(prefs::kSigninAllowedOnNextStartup));
+        profile->GetPrefs()->GetBoolean(prefs::kSigninAllowedOnNextStartup));
     EXPECT_EQ(signin::AccountConsistencyMethod::kDice,
               manager.GetAccountConsistencyMethod());
 
     // User changes their settings.
-    profile.GetPrefs()->SetBoolean(prefs::kSigninAllowedOnNextStartup, false);
+    profile->GetPrefs()->SetBoolean(prefs::kSigninAllowedOnNextStartup, false);
     // Dice should remain in the same state until restart.
     EXPECT_EQ(signin::AccountConsistencyMethod::kDice,
               manager.GetAccountConsistencyMethod());
@@ -139,12 +119,11 @@
 
   {
     // Second startup.
-    AccountConsistencyModeManager manager(&profile,
-                                          /*auto_migrate_to_dice=*/true);
+    AccountConsistencyModeManager manager(profile.get());
     // The signin-allowed pref should be disabled.
-    EXPECT_FALSE(profile.GetPrefs()->GetBoolean(prefs::kSigninAllowed));
+    EXPECT_FALSE(profile->GetPrefs()->GetBoolean(prefs::kSigninAllowed));
     EXPECT_FALSE(
-        profile.GetPrefs()->GetBoolean(prefs::kSigninAllowedOnNextStartup));
+        profile->GetPrefs()->GetBoolean(prefs::kSigninAllowedOnNextStartup));
     // Dice should be disabled.
     EXPECT_EQ(signin::AccountConsistencyMethod::kDisabled,
               manager.GetAccountConsistencyMethod());
@@ -155,18 +134,18 @@
 TEST(AccountConsistencyModeManagerTest, DisallowSigninSwitch) {
   ScopedAccountConsistencyDice scoped_dice;
   content::TestBrowserThreadBundle test_thread_bundle;
-  TestingProfile profile;
+  std::unique_ptr<TestingProfile> profile =
+      BuildTestingProfile(/*is_new_profile=*/false);
 
   {
     // With the switch, signin is disallowed.
     base::test::ScopedCommandLine scoped_command_line;
     scoped_command_line.GetProcessCommandLine()->AppendSwitch(
         "disallow-signin");
-    AccountConsistencyModeManager manager(&profile,
-                                          /*auto_migrate_to_dice=*/true);
-    EXPECT_FALSE(profile.GetPrefs()->GetBoolean(prefs::kSigninAllowed));
+    AccountConsistencyModeManager manager(profile.get());
+    EXPECT_FALSE(profile->GetPrefs()->GetBoolean(prefs::kSigninAllowed));
     EXPECT_TRUE(
-        profile.GetPrefs()->GetBoolean(prefs::kSigninAllowedOnNextStartup));
+        profile->GetPrefs()->GetBoolean(prefs::kSigninAllowedOnNextStartup));
     // Dice should be disabled.
     EXPECT_EQ(signin::AccountConsistencyMethod::kDisabled,
               manager.GetAccountConsistencyMethod());
@@ -174,11 +153,10 @@
 
   {
     // Remove the switch, signin is allowed again.
-    AccountConsistencyModeManager manager(&profile,
-                                          /*auto_migrate_to_dice=*/true);
-    EXPECT_TRUE(profile.GetPrefs()->GetBoolean(prefs::kSigninAllowed));
+    AccountConsistencyModeManager manager(profile.get());
+    EXPECT_TRUE(profile->GetPrefs()->GetBoolean(prefs::kSigninAllowed));
     EXPECT_TRUE(
-        profile.GetPrefs()->GetBoolean(prefs::kSigninAllowedOnNextStartup));
+        profile->GetPrefs()->GetBoolean(prefs::kSigninAllowedOnNextStartup));
     // Dice should be enabled.
     EXPECT_EQ(signin::AccountConsistencyMethod::kDice,
               manager.GetAccountConsistencyMethod());
@@ -186,18 +164,16 @@
 }
 
 // Checks that Dice migration happens when the reconcilor is created.
-TEST(AccountConsistencyModeManagerTest, MigratePreDiceProfileAtCreation) {
+TEST(AccountConsistencyModeManagerTest, MigrateAtCreation) {
   content::TestBrowserThreadBundle test_thread_bundle;
-  std::unique_ptr<Profile> profile = BuildPreDiceProfile();
+  std::unique_ptr<TestingProfile> profile =
+      BuildTestingProfile(/*is_new_profile=*/false);
 
   {
     // Migration does not happen if SetDiceMigrationOnStartup() is not called.
     ScopedAccountConsistencyDiceMigration scoped_dice_migration;
-    AccountConsistencyModeManager manager(profile.get(),
-                                          /*auto_migrate_to_dice=*/false);
-    EXPECT_FALSE(AccountConsistencyModeManager::IsReadyForDiceMigration(
-        profile.get(),
-        /*auto_migrate_to_dice=*/false));
+    AccountConsistencyModeManager manager(profile.get());
+    EXPECT_FALSE(manager.IsReadyForDiceMigration(profile.get()));
     EXPECT_NE(signin::AccountConsistencyMethod::kDice,
               manager.GetAccountConsistencyMethod());
   }
@@ -207,16 +183,23 @@
   {
     // Migration happens.
     ScopedAccountConsistencyDiceMigration scoped_dice_migration;
-    AccountConsistencyModeManager manager(profile.get(),
-                                          /*auto_migrate_to_dice=*/false);
-    EXPECT_TRUE(AccountConsistencyModeManager::IsReadyForDiceMigration(
-        profile.get(),
-        /*auto_migrate_to_dice=*/false));
+    AccountConsistencyModeManager manager(profile.get());
+    EXPECT_TRUE(manager.IsReadyForDiceMigration(profile.get()));
     EXPECT_EQ(signin::AccountConsistencyMethod::kDice,
               manager.GetAccountConsistencyMethod());
   }
 }
 
+// Checks that new profiles are migrated at creation.
+TEST(AccountConsistencyModeManagerTest, NewProfile) {
+  content::TestBrowserThreadBundle test_thread_bundle;
+  ScopedAccountConsistencyDiceMigration scoped_dice_migration;
+  std::unique_ptr<TestingProfile> profile =
+      BuildTestingProfile(/*is_new_profile=*/true);
+  EXPECT_TRUE(
+      AccountConsistencyModeManager::IsDiceEnabledForProfile(profile.get()));
+}
+
 TEST(AccountConsistencyModeManagerTest, DiceOnlyForRegularProfile) {
   ScopedAccountConsistencyDice scoped_dice;
   content::TestBrowserThreadBundle test_thread_bundle;
diff --git a/chrome/browser/signin/consistency_cookie_browsertest.cc b/chrome/browser/signin/consistency_cookie_browsertest.cc
index 97b61b5..d03c3b7 100644
--- a/chrome/browser/signin/consistency_cookie_browsertest.cc
+++ b/chrome/browser/signin/consistency_cookie_browsertest.cc
@@ -170,7 +170,9 @@
 
 // Tests that the ConsistencyCookieManager can set and change the cookie in HTTP
 // and javascript.
-IN_PROC_BROWSER_TEST_F(ConsistencyCookieBrowserTest, Basic) {
+// TODO(https://crbug.com/964264): Fails on some bots with error
+// net::ERR_SSL_PINNED_KEY_NOT_IN_CERT_CHAIN.
+IN_PROC_BROWSER_TEST_F(ConsistencyCookieBrowserTest, DISABLED_Basic) {
   // Check the initial value.
   CheckCookieValue(std::string(kConsistencyCookieName) + "=initial_value");
   // Change the cookie.
diff --git a/chrome/browser/signin/signin_error_notifier_ash.cc b/chrome/browser/signin/signin_error_notifier_ash.cc
index 5cf827d..7c5ba38 100644
--- a/chrome/browser/signin/signin_error_notifier_ash.cc
+++ b/chrome/browser/signin/signin_error_notifier_ash.cc
@@ -14,6 +14,7 @@
 #include "base/strings/string16.h"
 #include "base/strings/utf_string_conversions.h"
 #include "build/build_config.h"
+#include "chrome/browser/browser_process.h"
 #include "chrome/browser/chromeos/account_manager/account_manager_util.h"
 #include "chrome/browser/chromeos/login/user_flow.h"
 #include "chrome/browser/chromeos/login/users/chrome_user_manager.h"
@@ -32,6 +33,7 @@
 #include "chrome/grit/chromium_strings.h"
 #include "chrome/grit/generated_resources.h"
 #include "chrome/grit/theme_resources.h"
+#include "chromeos/components/account_manager/account_manager_factory.h"
 #include "components/account_id/account_id.h"
 #include "components/user_manager/user_manager.h"
 #include "services/identity/public/cpp/identity_manager.h"
@@ -50,6 +52,17 @@
   chrome::AttemptUserExit();
 }
 
+bool AreAllAccountsMigrated(
+    const chromeos::AccountManager* const account_manager,
+    const std::vector<chromeos::AccountManager::Account>& accounts) {
+  for (const auto& account : accounts) {
+    if (account_manager->HasDummyGaiaToken(account.key)) {
+      return false;
+    }
+  }
+  return true;
+}
+
 }  // namespace
 
 SigninErrorNotifier::SigninErrorNotifier(SigninErrorController* controller,
@@ -57,7 +70,11 @@
     : error_controller_(controller),
       profile_(profile),
       identity_manager_(IdentityManagerFactory::GetForProfile(profile_)),
+      account_manager_(g_browser_process->platform_part()
+                           ->GetAccountManagerFactory()
+                           ->GetAccountManager(profile_->GetPath().value())),
       weak_factory_(this) {
+  DCHECK(account_manager_);
   // Create a unique notification ID for this profile.
   device_account_notification_id_ =
       kProfileSigninNotificationId + profile->GetProfileUserName();
@@ -152,6 +169,12 @@
 
 void SigninErrorNotifier::HandleSecondaryAccountError(
     const std::string& account_id) {
+  account_manager_->GetAccounts(base::BindOnce(
+      &SigninErrorNotifier::OnGetAccounts, weak_factory_.GetWeakPtr()));
+}
+
+void SigninErrorNotifier::OnGetAccounts(
+    const std::vector<chromeos::AccountManager::Account>& accounts) {
   message_center::NotifierId notifier_id(
       message_center::NotifierType::SYSTEM_COMPONENT,
       kProfileSigninNotificationId);
@@ -161,13 +184,24 @@
   notifier_id.profile_id =
       multi_user_util::GetAccountIdFromProfile(profile_).GetUserEmail();
 
+  const bool are_all_accounts_migrated =
+      AreAllAccountsMigrated(account_manager_, accounts);
+  const base::string16 message_title =
+      are_all_accounts_migrated
+          ? l10n_util::GetStringUTF16(
+                IDS_SIGNIN_ERROR_SECONDARY_ACCOUNT_BUBBLE_VIEW_TITLE)
+          : l10n_util::GetStringUTF16(
+                IDS_SIGNIN_ERROR_SECONDARY_ACCOUNT_MIGRATION_BUBBLE_VIEW_TITLE);
+  const base::string16 message_body =
+      are_all_accounts_migrated
+          ? GetMessageBody(true /* is_secondary_account_error */)
+          : l10n_util::GetStringUTF16(
+                IDS_SIGNIN_ERROR_SECONDARY_ACCOUNT_MIGRATION_BUBBLE_VIEW_MESSAGE);
+
   std::unique_ptr<message_center::Notification> notification =
       ash::CreateSystemNotification(
           message_center::NOTIFICATION_TYPE_SIMPLE,
-          secondary_account_notification_id_,
-          l10n_util::GetStringUTF16(
-              IDS_SIGNIN_ERROR_SECONDARY_ACCOUNT_BUBBLE_VIEW_TITLE),
-          GetMessageBody(true /* is_secondary_account_error */),
+          secondary_account_notification_id_, message_title, message_body,
           l10n_util::GetStringUTF16(
               IDS_SIGNIN_ERROR_SECONDARY_ACCOUNT_DISPLAY_SOURCE),
           GURL(secondary_account_notification_id_), notifier_id,
diff --git a/chrome/browser/signin/signin_error_notifier_ash.h b/chrome/browser/signin/signin_error_notifier_ash.h
index 7764731..1884668 100644
--- a/chrome/browser/signin/signin_error_notifier_ash.h
+++ b/chrome/browser/signin/signin_error_notifier_ash.h
@@ -6,11 +6,13 @@
 #define CHROME_BROWSER_SIGNIN_SIGNIN_ERROR_NOTIFIER_ASH_H_
 
 #include <string>
+#include <vector>
 
 #include "base/compiler_specific.h"
 #include "base/macros.h"
 #include "base/memory/weak_ptr.h"
 #include "base/strings/string16.h"
+#include "chromeos/components/account_manager/account_manager.h"
 #include "components/keyed_service/core/keyed_service.h"
 #include "components/signin/core/browser/signin_error_controller.h"
 
@@ -44,6 +46,10 @@
   // for the Secondary Account which received an error.
   void HandleSecondaryAccountError(const std::string& account_id);
 
+  // |chromeos::AccountManager::GetAccounts| callback handler.
+  void OnGetAccounts(
+      const std::vector<chromeos::AccountManager::Account>& accounts);
+
   // Handles clicks on the Secondary Account reauth notification. See
   // |message_center::HandleNotificationClickDelegate|.
   void HandleSecondaryAccountReauthNotificationClick(
@@ -60,6 +66,9 @@
   // A non-owning pointer to IdentityManager.
   identity::IdentityManager* const identity_manager_;
 
+  // A non-owning pointer.
+  chromeos::AccountManager* const account_manager_;
+
   // Used to keep track of the message center notifications.
   std::string device_account_notification_id_;
   std::string secondary_account_notification_id_;
diff --git a/chrome/browser/ui/sync/profile_signin_confirmation_helper_unittest.cc b/chrome/browser/ui/sync/profile_signin_confirmation_helper_unittest.cc
index 711b737..56e1744 100644
--- a/chrome/browser/ui/sync/profile_signin_confirmation_helper_unittest.cc
+++ b/chrome/browser/ui/sync/profile_signin_confirmation_helper_unittest.cc
@@ -181,6 +181,7 @@
 // http://crbug.com/393149
 TEST_F(ProfileSigninConfirmationHelperTest, DISABLED_DoNotPromptForNewProfile) {
   // Profile is new and there's no profile data.
+  profile_->SetIsNewProfile(true);
   EXPECT_FALSE(
       GetCallbackResult(
           base::Bind(
@@ -192,6 +193,7 @@
   ASSERT_TRUE(model_);
 
   // Profile is new but has bookmarks.
+  profile_->SetIsNewProfile(true);
   model_->AddURL(model_->bookmark_bar_node(), 0,
                  base::string16(base::ASCIIToUTF16("foo")),
                  GURL("http://foo.com"));
@@ -208,9 +210,8 @@
       extensions::ExtensionSystem::Get(profile_.get())->extension_service();
   ASSERT_TRUE(extensions);
 
-  // Profile is new but has synced extensions.
-
-  // (The web store doesn't count.)
+  // Profile is new but has synced extensions (The web store doesn't count).
+  profile_->SetIsNewProfile(true);
   scoped_refptr<extensions::Extension> webstore =
       CreateExtension("web store",
                       extensions::kWebStoreAppId,
@@ -240,6 +241,7 @@
 
   // Profile is new but has more than $(kHistoryEntriesBeforeNewProfilePrompt)
   // history items.
+  profile_->SetIsNewProfile(true);
   char buf[18];
   for (int i = 0; i < 10; i++) {
     base::snprintf(buf, base::size(buf), "http://foo.com/%d", i);
@@ -263,6 +265,7 @@
   ASSERT_TRUE(history);
 
   // Profile is new but has a typed URL.
+  profile_->SetIsNewProfile(true);
   history->AddPage(
       GURL("http://example.com"), base::Time::Now(), NULL, 1,
       GURL(), history::RedirectList(), ui::PAGE_TRANSITION_TYPED,
@@ -276,7 +279,7 @@
 
 TEST_F(ProfileSigninConfirmationHelperTest, PromptForNewProfile_Restarted) {
   // Browser has been shut down since profile was created.
-  user_prefs_->set_read_error(PersistentPrefStore::PREF_READ_ERROR_NONE);
+  profile_->SetIsNewProfile(false);
   EXPECT_TRUE(
       GetCallbackResult(
           base::Bind(
diff --git a/chrome/browser/ui/views/plugin_vm/plugin_vm_launcher_view.cc b/chrome/browser/ui/views/plugin_vm/plugin_vm_launcher_view.cc
index 2c4c440..aeaa073 100644
--- a/chrome/browser/ui/views/plugin_vm/plugin_vm_launcher_view.cc
+++ b/chrome/browser/ui/views/plugin_vm/plugin_vm_launcher_view.cc
@@ -174,8 +174,7 @@
   switch (state_) {
     case State::START_DOWNLOADING:
     case State::DOWNLOADING:
-    case State::UNZIPPING:
-    case State::REGISTERING:
+    case State::IMPORTING:
       return ui::DIALOG_BUTTON_CANCEL;
     case State::FINISHED:
       return ui::DIALOG_BUTTON_OK;
@@ -191,8 +190,7 @@
   switch (state_) {
     case State::START_DOWNLOADING:
     case State::DOWNLOADING:
-    case State::UNZIPPING:
-    case State::REGISTERING: {
+    case State::IMPORTING: {
       DCHECK_EQ(button, ui::DIALOG_BUTTON_CANCEL);
       return l10n_util::GetStringUTF16(IDS_APP_CANCEL);
     }
@@ -232,10 +230,8 @@
 bool PluginVmLauncherView::Cancel() {
   if (state_ == State::DOWNLOADING || state_ == State::START_DOWNLOADING)
     plugin_vm_image_manager_->CancelDownload();
-  if (state_ == State::UNZIPPING)
-    plugin_vm_image_manager_->CancelUnzipping();
-
-  // TODO(https://crbug.com/947014): Cancel registering.
+  if (state_ == State::IMPORTING)
+    plugin_vm_image_manager_->CancelImport();
 
   return true;
 }
@@ -278,8 +274,8 @@
   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
   DCHECK_EQ(state_, State::DOWNLOADING);
 
-  plugin_vm_image_manager_->StartUnzipping();
-  state_ = State::UNZIPPING;
+  plugin_vm_image_manager_->StartImport();
+  state_ = State::IMPORTING;
   OnStateUpdated();
 }
 
@@ -294,65 +290,45 @@
   OnStateUpdated();
 }
 
-void PluginVmLauncherView::OnUnzippingProgressUpdated(
-    int64_t bytes_unzipped,
-    int64_t plugin_vm_image_size,
-    int64_t unzipping_bytes_per_sec) {
+void PluginVmLauncherView::OnImportProgressUpdated(
+    uint64_t percent_completed,
+    int64_t import_percent_per_second) {
   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
-  DCHECK_EQ(state_, State::UNZIPPING);
+  DCHECK_EQ(state_, State::IMPORTING);
 
   base::Optional<double> fraction_complete =
-      GetFractionComplete(bytes_unzipped, plugin_vm_image_size);
+      GetFractionComplete(percent_completed, 100.0);
   if (fraction_complete.has_value())
     progress_bar_->SetValue(fraction_complete.value());
   else
     progress_bar_->SetValue(-1);
 
-  base::string16 time_left_message = GetTimeLeftMessage(
-      bytes_unzipped, plugin_vm_image_size, unzipping_bytes_per_sec);
+  base::string16 time_left_message =
+      GetTimeLeftMessage(percent_completed, 100.0, import_percent_per_second);
   time_left_message_label_->SetVisible(!time_left_message.empty());
   time_left_message_label_->SetText(time_left_message);
 }
 
-void PluginVmLauncherView::OnUnzipped() {
-  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
-  DCHECK_EQ(state_, State::UNZIPPING);
-
-  state_ = State::REGISTERING;
-  OnStateUpdated();
-
-  plugin_vm_image_manager_->StartRegistration();
-}
-
-void PluginVmLauncherView::OnUnzippingFailed() {
+void PluginVmLauncherView::OnImportFailed() {
   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
 
   state_ = State::ERROR;
   OnStateUpdated();
 }
 
-void PluginVmLauncherView::OnRegistered() {
+void PluginVmLauncherView::OnImported() {
   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
-  DCHECK_EQ(state_, State::REGISTERING);
+  DCHECK_EQ(state_, State::IMPORTING);
 
   state_ = State::FINISHED;
   OnStateUpdated();
 }
 
-void PluginVmLauncherView::OnRegistrationFailed() {
-  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
-  DCHECK_EQ(state_, State::REGISTERING);
-
-  state_ = State::ERROR;
-  OnStateUpdated();
-}
-
 base::string16 PluginVmLauncherView::GetBigMessage() const {
   switch (state_) {
     case State::START_DOWNLOADING:
     case State::DOWNLOADING:
-    case State::UNZIPPING:
-    case State::REGISTERING:
+    case State::IMPORTING:
       return l10n_util::GetStringUTF16(
           IDS_PLUGIN_VM_LAUNCHER_ENVIRONMENT_SETTING_TITLE);
     case State::FINISHED:
@@ -371,12 +347,9 @@
     case State::DOWNLOADING:
       return l10n_util::GetStringUTF16(
           IDS_PLUGIN_VM_LAUNCHER_DOWNLOADING_MESSAGE);
-    case State::UNZIPPING:
+    case State::IMPORTING:
       return l10n_util::GetStringUTF16(
-          IDS_PLUGIN_VM_LAUNCHER_UNZIPPING_MESSAGE);
-    case State::REGISTERING:
-      return l10n_util::GetStringUTF16(
-          IDS_PLUGIN_VM_LAUNCHER_REGISTERING_MESSAGE);
+          IDS_PLUGIN_VM_LAUNCHER_IMPORTING_MESSAGE);
     case State::FINISHED:
       return l10n_util::GetStringUTF16(IDS_PLUGIN_VM_LAUNCHER_FINISHED_MESSAGE);
     case State::ERROR:
@@ -411,15 +384,15 @@
   SetMessageLabel();
   SetBigImage();
 
-  const bool progress_bar_visible =
-      state_ == State::START_DOWNLOADING || state_ == State::DOWNLOADING ||
-      state_ == State::UNZIPPING || state_ == State::REGISTERING;
+  const bool progress_bar_visible = state_ == State::START_DOWNLOADING ||
+                                    state_ == State::DOWNLOADING ||
+                                    state_ == State::IMPORTING;
   progress_bar_->SetVisible(progress_bar_visible);
   // Values outside the range [0,1] display an infinite loading animation.
   progress_bar_->SetValue(-1);
 
   const bool time_left_message_label_visible =
-      state_ == State::DOWNLOADING || state_ == State::UNZIPPING;
+      state_ == State::DOWNLOADING || state_ == State::IMPORTING;
   time_left_message_label_->SetVisible(time_left_message_label_visible);
 
   const bool download_progress_message_label_visible =
@@ -459,7 +432,7 @@
     int64_t processed_bytes,
     int64_t bytes_to_be_processed,
     int64_t bytes_per_sec) const {
-  DCHECK(state_ == State::DOWNLOADING || state_ == State::UNZIPPING);
+  DCHECK(state_ == State::DOWNLOADING || state_ == State::IMPORTING);
 
   base::Optional<double> fraction_complete =
       GetFractionComplete(processed_bytes, bytes_to_be_processed);
diff --git a/chrome/browser/ui/views/plugin_vm/plugin_vm_launcher_view.h b/chrome/browser/ui/views/plugin_vm/plugin_vm_launcher_view.h
index a6bc732..7a8192a 100644
--- a/chrome/browser/ui/views/plugin_vm/plugin_vm_launcher_view.h
+++ b/chrome/browser/ui/views/plugin_vm/plugin_vm_launcher_view.h
@@ -42,13 +42,10 @@
   void OnDownloadCompleted() override;
   void OnDownloadCancelled() override;
   void OnDownloadFailed() override;
-  void OnUnzippingProgressUpdated(int64_t bytes_unzipped,
-                                  int64_t plugin_vm_image_size,
-                                  int64_t unzipping_bytes_per_sec) override;
-  void OnUnzipped() override;
-  void OnUnzippingFailed() override;
-  void OnRegistered() override;
-  void OnRegistrationFailed() override;
+  void OnImportProgressUpdated(uint64_t percent_completed,
+                               int64_t import_percent_per_sec) override;
+  void OnImported() override;
+  void OnImportFailed() override;
 
   // Public for testing purposes.
   base::string16 GetBigMessage() const;
@@ -58,8 +55,7 @@
   enum class State {
     START_DOWNLOADING,  // PluginVm image downloading should be started.
     DOWNLOADING,        // PluginVm image downloading is in progress.
-    UNZIPPING,          // Downloaded PluginVm image unzipping is in progress.
-    REGISTERING,        // PluginVm image registering is in progress.
+    IMPORTING,          // Downloaded PluginVm image importing is in progress.
     FINISHED,           // PluginVm environment setting has been finished.
     ERROR,              // Something unexpected happened.
     NOT_ALLOWED,        // PluginVm is disallowed on the device.
diff --git a/chrome/browser/ui/views/plugin_vm/plugin_vm_launcher_view_browsertest.cc b/chrome/browser/ui/views/plugin_vm/plugin_vm_launcher_view_browsertest.cc
index 1d7ba97..87f48d5 100644
--- a/chrome/browser/ui/views/plugin_vm/plugin_vm_launcher_view_browsertest.cc
+++ b/chrome/browser/ui/views/plugin_vm/plugin_vm_launcher_view_browsertest.cc
@@ -9,6 +9,7 @@
 #include "base/threading/thread_restrictions.h"
 #include "chrome/browser/chromeos/login/users/mock_user_manager.h"
 #include "chrome/browser/chromeos/plugin_vm/plugin_vm_pref_names.h"
+#include "chrome/browser/chromeos/plugin_vm/plugin_vm_test_helper.h"
 #include "chrome/browser/chromeos/profiles/profile_helper.h"
 #include "chrome/browser/chromeos/settings/cros_settings.h"
 #include "chrome/browser/chromeos/settings/scoped_testing_cros_settings.h"
@@ -17,6 +18,9 @@
 #include "chrome/browser/ui/browser.h"
 #include "chrome/browser/ui/test/test_browser_dialog.h"
 #include "chrome/grit/generated_resources.h"
+#include "chromeos/dbus/dbus_thread_manager.h"
+#include "chromeos/dbus/fake_concierge_client.h"
+#include "chromeos/dbus/fake_debug_daemon_client.h"
 #include "components/account_id/account_id.h"
 #include "components/download/public/background_service/download_metadata.h"
 #include "components/prefs/pref_service.h"
@@ -29,7 +33,6 @@
 namespace {
 
 const char kZipFile[] = "/downloads/a_zip_file.zip";
-const char kZippedFile[] = "a_file.txt";
 const char kZipFileHash[] =
     "bb077522e6c6fec07cf863ca44d5701935c4bc36ed12ef154f4cc22df70aec18";
 const char kNonMatchingHash[] =
@@ -92,6 +95,9 @@
 
   void SetUpOnMainThread() override {
     ASSERT_TRUE(embedded_test_server()->Start());
+    fake_concierge_client_ = static_cast<chromeos::FakeConciergeClient*>(
+        chromeos::DBusThreadManager::Get()->GetConciergeClient());
+    fake_concierge_client_->set_disk_image_progress_signal_connected(true);
   }
 
   // DialogBrowserTest:
@@ -125,7 +131,6 @@
     EXPECT_EQ(
         view_->GetMessage(),
         l10n_util::GetStringUTF16(IDS_PLUGIN_VM_LAUNCHER_NOT_ALLOWED_MESSAGE));
-    CheckNoPluginVmImageDirExists();
   }
 
   void CheckSetupFailed() {
@@ -135,21 +140,6 @@
               l10n_util::GetStringUTF16(IDS_PLUGIN_VM_LAUNCHER_RETRY_BUTTON));
     EXPECT_EQ(view_->GetBigMessage(),
               l10n_util::GetStringUTF16(IDS_PLUGIN_VM_LAUNCHER_ERROR_TITLE));
-    EXPECT_EQ(view_->GetMessage(),
-              l10n_util::GetStringUTF16(IDS_PLUGIN_VM_LAUNCHER_ERROR_MESSAGE));
-    CheckNoPluginVmImageDirExists();
-  }
-
-  void CheckNoPluginVmImageDirExists() {
-    base::FilePath plugin_vm_image_dir =
-        browser()
-            ->profile()
-            ->GetPath()
-            .AppendASCII(plugin_vm::kCrosvmDir)
-            .AppendASCII(plugin_vm::kPvmDir)
-            .AppendASCII(plugin_vm::kPluginVmImageDir);
-    base::ScopedAllowBlockingForTesting allow_blocking;
-    EXPECT_FALSE(base::DirectoryExists(plugin_vm_image_dir));
   }
 
   void CheckSetupIsFinishedSuccessfully() {
@@ -159,17 +149,6 @@
               l10n_util::GetStringUTF16(IDS_PLUGIN_VM_LAUNCHER_LAUNCH_BUTTON));
     EXPECT_EQ(view_->GetBigMessage(),
               l10n_util::GetStringUTF16(IDS_PLUGIN_VM_LAUNCHER_FINISHED_TITLE));
-
-    base::FilePath plugin_vm_image_dir =
-        browser()
-            ->profile()
-            ->GetPath()
-            .AppendASCII(plugin_vm::kCrosvmDir)
-            .AppendASCII(plugin_vm::kPvmDir)
-            .AppendASCII(plugin_vm::kPluginVmImageDir);
-    base::ScopedAllowBlockingForTesting allow_blocking;
-    EXPECT_TRUE(base::DirectoryExists(plugin_vm_image_dir));
-    EXPECT_TRUE(base::PathExists(plugin_vm_image_dir.AppendASCII(kZippedFile)));
   }
 
   void SetPluginVmDevicePolicies() {
@@ -196,6 +175,9 @@
     plugin_vm_image->SetKey("hash", base::Value(hash));
   }
 
+ protected:
+  chromeos::FakeConciergeClient* fake_concierge_client_;
+
  private:
   DISALLOW_COPY_AND_ASSIGN(PluginVmLauncherViewBrowserTest);
 };
@@ -209,6 +191,7 @@
                        SetupShouldFinishSuccessfully) {
   SetPluginVmDevicePolicies();
   SetUserWithAffiliation();
+  plugin_vm::SetupConciergeForSuccessfulDiskImageImport(fake_concierge_client_);
   SetPluginVmImagePref(embedded_test_server()->GetURL(kZipFile).spec(),
                        kZipFileHash);
 
@@ -264,6 +247,7 @@
 
   CheckSetupFailed();
 
+  plugin_vm::SetupConciergeForSuccessfulDiskImageImport(fake_concierge_client_);
   SetPluginVmImagePref(embedded_test_server()->GetURL(kZipFile).spec(),
                        kZipFileHash);
 
diff --git a/chrome/browser/ui/webui/chromeos/account_manager_welcome_ui.cc b/chrome/browser/ui/webui/chromeos/account_manager_welcome_ui.cc
index 9ae1f78..e40c57f 100644
--- a/chrome/browser/ui/webui/chromeos/account_manager_welcome_ui.cc
+++ b/chrome/browser/ui/webui/chromeos/account_manager_welcome_ui.cc
@@ -7,6 +7,7 @@
 #include "base/bind.h"
 #include "base/strings/utf_string_conversions.h"
 #include "chrome/browser/profiles/profile.h"
+#include "chrome/common/url_constants.h"
 #include "chrome/common/webui_url_constants.h"
 #include "chrome/grit/browser_resources.h"
 #include "chrome/grit/generated_resources.h"
@@ -16,13 +17,6 @@
 
 namespace chromeos {
 
-namespace {
-
-constexpr char kAccountManagerLearnMoreURL[] =
-    "https://support.google.com/chromebook/?p=google_accounts";
-
-}  // namespace
-
 AccountManagerWelcomeUI::AccountManagerWelcomeUI(content::WebUI* web_ui)
     : ui::WebDialogUI(web_ui), weak_factory_(this) {
   content::WebUIDataSource* html_source = content::WebUIDataSource::Create(
@@ -37,11 +31,13 @@
   // Add localized strings.
   html_source->AddLocalizedString("welcomeTitle",
                                   IDS_ACCOUNT_MANAGER_WELCOME_TITLE);
-  html_source->AddString("welcomeMessage",
-                         l10n_util::GetStringFUTF16(
-                             IDS_ACCOUNT_MANAGER_WELCOME_TEXT,
-                             base::ASCIIToUTF16(kAccountManagerLearnMoreURL)));
-  html_source->AddLocalizedString("okButton", IDS_APP_OK);
+  html_source->AddString(
+      "welcomeMessage",
+      l10n_util::GetStringFUTF16(
+          IDS_ACCOUNT_MANAGER_WELCOME_TEXT,
+          base::ASCIIToUTF16(chrome::kAccountManagerLearnMoreURL)));
+  html_source->AddLocalizedString("okButton",
+                                  IDS_ACCOUNT_MANAGER_WELCOME_BUTTON);
 
   // Add required resources.
   html_source->AddResourcePath("account_manager_welcome.css",
diff --git a/chrome/browser/ui/webui/chromeos/insession_password_change_handler_chromeos.cc b/chrome/browser/ui/webui/chromeos/insession_password_change_handler_chromeos.cc
index e87f533..d3533ba 100644
--- a/chrome/browser/ui/webui/chromeos/insession_password_change_handler_chromeos.cc
+++ b/chrome/browser/ui/webui/chromeos/insession_password_change_handler_chromeos.cc
@@ -11,6 +11,7 @@
 #include "base/macros.h"
 #include "base/values.h"
 #include "chrome/browser/chromeos/login/auth/chrome_cryptohome_authenticator.h"
+#include "chrome/browser/chromeos/login/saml/saml_password_expiry_notification.h"
 #include "chrome/browser/chromeos/policy/user_cloud_policy_manager_chromeos.h"
 #include "chrome/browser/chromeos/policy/user_policy_manager_factory_chromeos.h"
 #include "chrome/browser/chromeos/profiles/profile_helper.h"
@@ -110,6 +111,17 @@
   user_manager::UserManager::Get()->SaveForceOnlineSignin(
       user_context.GetAccountId(), false);
   authenticator_.reset();
+
+  // Clear expiration time from prefs so that we don't keep nagging the user to
+  // change password (until the SAML provider tells us a new expiration time).
+  Profile* profile = Profile::FromWebUI(web_ui());
+  SamlPasswordAttributes loaded =
+      SamlPasswordAttributes::LoadFromPrefs(profile->GetPrefs());
+  SamlPasswordAttributes(
+      /*modified_time=*/base::Time::Now(), /*expiration_time=*/base::Time(),
+      loaded.password_change_url())
+      .SaveToPrefs(profile->GetPrefs());
+  DismissSamlPasswordExpiryNotification(profile);
 }
 
 void InSessionPasswordChangeHandler::OnAuthFailure(const AuthFailure& error) {
diff --git a/chrome/browser/ui/webui/media/media_engagement_ui.cc b/chrome/browser/ui/webui/media/media_engagement_ui.cc
index 1e293ad..e69a536 100644
--- a/chrome/browser/ui/webui/media/media_engagement_ui.cc
+++ b/chrome/browser/ui/webui/media/media_engagement_ui.cc
@@ -78,6 +78,7 @@
         base::FeatureList::IsEnabled(
             media::kMediaEngagementBypassAutoplayPolicies),
         base::FeatureList::IsEnabled(media::kPreloadMediaEngagementData),
+        base::FeatureList::IsEnabled(media::kMediaEngagementHTTPSOnly),
         base::FeatureList::IsEnabled(media::kAutoplayDisableSettings),
         base::FeatureList::IsEnabled(media::kAutoplayWhitelistSettings),
         GetBlockAutoplayPref(),
diff --git a/chrome/browser/ui/webui/settings/chromeos/account_manager_handler.cc b/chrome/browser/ui/webui/settings/chromeos/account_manager_handler.cc
index 5879756..24a055e 100644
--- a/chrome/browser/ui/webui/settings/chromeos/account_manager_handler.cc
+++ b/chrome/browser/ui/webui/settings/chromeos/account_manager_handler.cc
@@ -176,6 +176,8 @@
                         webui::GetBitmapDataUrl(
                             default_icon.GetRepresentation(1.0f).GetBitmap()));
     }
+    account.SetBoolean("unmigrated",
+                       account_manager_->HasDummyGaiaToken(account_key));
 
     if (IsSameAccount(account_key, device_account_id)) {
       device_account = std::move(account);
diff --git a/chrome/browser/ui/webui/settings/people_handler_unittest.cc b/chrome/browser/ui/webui/settings/people_handler_unittest.cc
index 481934e..7090d96 100644
--- a/chrome/browser/ui/webui/settings/people_handler_unittest.cc
+++ b/chrome/browser/ui/webui/settings/people_handler_unittest.cc
@@ -9,6 +9,7 @@
 #include <vector>
 
 #include "base/bind.h"
+#include "base/callback_helpers.h"
 #include "base/command_line.h"
 #include "base/json/json_writer.h"
 #include "base/macros.h"
@@ -16,8 +17,8 @@
 #include "base/values.h"
 #include "build/build_config.h"
 #include "chrome/browser/defaults.h"
+#include "chrome/browser/first_run/first_run.h"
 #include "chrome/browser/signin/account_consistency_mode_manager.h"
-#include "chrome/browser/signin/account_consistency_mode_manager_factory.h"
 #include "chrome/browser/signin/identity_test_environment_profile_adaptor.h"
 #include "chrome/browser/signin/scoped_account_consistency.h"
 #include "chrome/browser/signin/signin_error_controller_factory.h"
@@ -1391,6 +1392,14 @@
     : public ::testing::TestWithParam<std::tuple<bool, bool>> {};
 
 TEST_P(PeopleHandlerDiceUnifiedConsentTest, StoredAccountsList) {
+  // Do not be in first run, so that the profiles are not created as "new
+  // profiles" and automatically migrated to Dice.
+  first_run::ResetCachedSentinelDataForTesting();
+  base::ScopedClosureRunner(
+      base::BindOnce(&first_run::ResetCachedSentinelDataForTesting));
+  base::CommandLine::ForCurrentProcess()->AppendSwitch(switches::kNoFirstRun);
+  ASSERT_FALSE(first_run::IsChromeFirstRun());
+
   content::TestBrowserThreadBundle test_browser_thread_bundle;
 
   // Decode test parameters.
@@ -1405,18 +1414,13 @@
       dice_enabled ? signin::AccountConsistencyMethod::kDice
                    : signin::AccountConsistencyMethod::kDiceMigration);
 
-  // Create a pre-dice profile so that it does not automatically migrates to
-  // Dice.
+  // Setup the profile.
   std::unique_ptr<TestingProfile> profile =
       IdentityTestEnvironmentProfileAdaptor::
-          CreateProfileForIdentityTestEnvironment(
-              {{AccountConsistencyModeManagerFactory::GetInstance(),
-                base::BindRepeating([](content::BrowserContext* context)
-                                        -> std::unique_ptr<KeyedService> {
-                  return std::make_unique<AccountConsistencyModeManager>(
-                      Profile::FromBrowserContext(context),
-                      /*auto_migrate_to_dice=*/false);
-                })}});
+          CreateProfileForIdentityTestEnvironment();
+  ASSERT_EQ(
+      dice_enabled,
+      AccountConsistencyModeManager::IsDiceEnabledForProfile(profile.get()));
 
   auto identity_test_env_adaptor =
       std::make_unique<IdentityTestEnvironmentProfileAdaptor>(profile.get());
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 1cfbd75..dd8d0a6f 100644
--- a/chrome/browser/ui/webui/settings/settings_localized_strings_provider.cc
+++ b/chrome/browser/ui/webui/settings/settings_localized_strings_provider.cc
@@ -329,6 +329,8 @@
       base::FeatureList::IsEnabled(features::kExperimentalAccessibilityLabels));
 
 #if defined(OS_CHROMEOS)
+  html_source->AddString("accountManagerLearnMoreUrl",
+                         chrome::kAccountManagerLearnMoreURL);
   html_source->AddString("a11yLearnMoreUrl",
                          chrome::kChromeAccessibilityHelpURL);
 
@@ -1648,6 +1650,10 @@
     {"removeAccountLabel", IDS_SETTINGS_ACCOUNT_MANAGER_REMOVE_ACCOUNT_LABEL},
     {"accountManagerSignedOutAccountName",
      IDS_SETTINGS_ACCOUNT_MANAGER_SIGNED_OUT_ACCOUNT_PLACEHOLDER},
+    {"accountManagerUnmigratedAccountName",
+     IDS_SETTINGS_ACCOUNT_MANAGER_UNMIGRATED_ACCOUNT_PLACEHOLDER},
+    {"accountManagerMigrationLabel",
+     IDS_SETTINGS_ACCOUNT_MANAGER_MIGRATION_LABEL},
     {"accountManagerReauthenticationLabel",
      IDS_SETTINGS_ACCOUNT_MANAGER_REAUTHENTICATION_LABEL},
     {"accountManagerManagedLabel",
diff --git a/chrome/browser/web_applications/components/web_app_shortcut_mac.mm b/chrome/browser/web_applications/components/web_app_shortcut_mac.mm
index d67902f..79f0cdf 100644
--- a/chrome/browser/web_applications/components/web_app_shortcut_mac.mm
+++ b/chrome/browser/web_applications/components/web_app_shortcut_mac.mm
@@ -958,14 +958,6 @@
             forKey:app_mode::kLSHasLocalizedDisplayNameKey];
   [plist setObject:[NSNumber numberWithBool:YES]
             forKey:app_mode::kNSHighResolutionCapableKey];
-  [plist
-      setObject:[NSNumber numberWithUnsignedInteger:
-                              app_mode::kCurrentChromeAppModeInfoMajorVersion]
-         forKey:app_mode::kCrAppModeMajorVersionKey];
-  [plist
-      setObject:[NSNumber numberWithUnsignedInteger:
-                              app_mode::kCurrentChromeAppModeInfoMinorVersion]
-         forKey:app_mode::kCrAppModeMinorVersionKey];
   if (info_->extension_id == app_mode::kAppListModeId) {
     // Prevent the app list from bouncing in the dock, and getting a run light.
     [plist setObject:[NSNumber numberWithBool:YES] forKey:kLSUIElement];
diff --git a/chrome/common/mac/app_mode_chrome_locator.h b/chrome/common/mac/app_mode_chrome_locator.h
index 5b95ee7..1abe7e07 100644
--- a/chrome/common/mac/app_mode_chrome_locator.h
+++ b/chrome/common/mac/app_mode_chrome_locator.h
@@ -23,18 +23,17 @@
 
 // Given the path to the Chrome bundle, and an optional framework version, read
 // the following information:
-// |executable_path| - Path to the Chrome executable.
-// |version_path| - |chrome_bundle|/Contents/Versions/|raw_version_str|/
-// |framework_shlib_path| - Path to the chrome framework's shared library (not
-//                          the framework directory).
-// If |version_str| is not given, this will read the current Chrome version from
-// the bundle's plist.
-// Returns true if all information read succesfuly, false otherwise.
+//   |executable_path| - Path to the Chrome executable.
+//   |framework_path| - Path of the Chrome Framework.framework.
+//   |framework_dylib_path| - Path to the Chrome Framework's shared library.
+// If |version_str| is not given, this will return this data for the current
+// Chrome version. Returns true if all information read successfully; false
+// otherwise.
 bool GetChromeBundleInfo(const base::FilePath& chrome_bundle,
                          const std::string& version_str,
                          base::FilePath* executable_path,
-                         base::FilePath* version_path,
-                         base::FilePath* framework_shlib_path);
+                         base::FilePath* framework_path,
+                         base::FilePath* framework_dylib_path);
 
 }  // namespace app_mode
 
diff --git a/chrome/common/mac/app_mode_chrome_locator.mm b/chrome/common/mac/app_mode_chrome_locator.mm
index efd272e..7e5f3fa 100644
--- a/chrome/common/mac/app_mode_chrome_locator.mm
+++ b/chrome/common/mac/app_mode_chrome_locator.mm
@@ -10,12 +10,50 @@
 #include "base/files/file_path.h"
 #include "base/files/file_util.h"
 #include "base/mac/foundation_util.h"
+#include "base/optional.h"
 #include "base/strings/sys_string_conversions.h"
 #include "chrome/common/chrome_constants.h"
 #include "chrome/common/mac/app_mode_common.h"
 
 namespace app_mode {
 
+namespace {
+
+struct PathAndStructure {
+  NSString* framework_dylib_path;  // weak
+  bool is_new_app_structure;
+};
+
+base::Optional<PathAndStructure> GetFrameworkDylibPathAndStructure(
+    NSString* bundle_path,
+    NSString* version) {
+  // NEW STYLE:
+  // Chromium.app/Contents/Frameworks/Chromium Framework.framework/
+  //   Versions/<version>/Chromium Framework
+  NSString* path = [NSString pathWithComponents:@[
+    bundle_path, @"Contents", @"Frameworks", @(chrome::kFrameworkName),
+    @"Versions", version, @(chrome::kFrameworkExecutableName)
+  ]];
+
+  if ([[NSFileManager defaultManager] fileExistsAtPath:path])
+    return PathAndStructure{path, true};
+
+  // OLD STYLE:
+  // Chromium.app/Contents/Versions/<version>/Chromium Framework.framework/
+  //   Versions/A/Chromium Framework
+  path = [NSString pathWithComponents:@[
+    bundle_path, @"Contents", @"Versions", version, @(chrome::kFrameworkName),
+    @"Versions", @"A", @(chrome::kFrameworkExecutableName)
+  ]];
+
+  if ([[NSFileManager defaultManager] fileExistsAtPath:path])
+    return PathAndStructure{path, false};
+
+  return base::nullopt;
+}
+
+}  // namespace
+
 bool FindBundleById(NSString* bundle_id, base::FilePath* out_bundle) {
   NSWorkspace* ws = [NSWorkspace sharedWorkspace];
   NSString *bundlePath = [ws absolutePathForAppBundleWithIdentifier:bundle_id];
@@ -26,86 +64,89 @@
   return true;
 }
 
-NSString* GetVersionedPath(NSString* bundle_path, NSString* version) {
-  return [NSString
-      pathWithComponents:@[ bundle_path, @"Contents", @"Versions", version ]];
-}
-
 bool GetChromeBundleInfo(const base::FilePath& chrome_bundle,
                          const std::string& version_str,
                          base::FilePath* executable_path,
-                         base::FilePath* version_path,
-                         base::FilePath* framework_shlib_path) {
-  using base::mac::ObjCCast;
-
+                         base::FilePath* framework_path,
+                         base::FilePath* framework_dylib_path) {
   NSString* cr_bundle_path = base::mac::FilePathToNSString(chrome_bundle);
   NSBundle* cr_bundle = [NSBundle bundleWithPath:cr_bundle_path];
-
   if (!cr_bundle)
     return false;
 
-  // Get versioned directory.
-  NSString* cr_versioned_path;
+  // Try to get the version requested, if present.
+  base::Optional<PathAndStructure> framework_path_and_structure;
   if (!version_str.empty()) {
-    cr_versioned_path =
-        GetVersionedPath(cr_bundle_path, base::SysUTF8ToNSString(version_str));
+    framework_path_and_structure = GetFrameworkDylibPathAndStructure(
+        cr_bundle_path, base::SysUTF8ToNSString(version_str));
   }
 
-  if (version_str.empty() ||
-      !base::PathExists(base::mac::NSStringToFilePath(cr_versioned_path))) {
-    // Read version string.
-    NSString* cr_version = ObjCCast<NSString>(
-        [cr_bundle objectForInfoDictionaryKey:app_mode::kCrBundleVersionKey]);
-    if (!cr_version) {
-      // Older bundles have the Chrome version in the following key.
-      cr_version = ObjCCast<NSString>([cr_bundle
-          objectForInfoDictionaryKey:app_mode::kCFBundleShortVersionStringKey]);
+  // If the version requested is not present, or no specific version was
+  // requested, fall back to the "current" version. For new-style bundle
+  // structures, use the "Current" symlink. (This will intentionally return nil
+  // with the old bundle structure.)
+  //
+  // Note that the scenario where a specific version was requested but is not
+  // present is a "should not happen" scenario. Chromium, while it is running,
+  // maintains a link to the currently running version, and this function's
+  // caller checked to see if the Chromium was still running. However, even in
+  // this bizarre case, it's best to find _some_ Chromium.
+  if (!framework_path_and_structure) {
+    framework_path_and_structure =
+        GetFrameworkDylibPathAndStructure(cr_bundle_path, @"Current");
+    if (framework_path_and_structure) {
+      framework_path_and_structure->framework_dylib_path =
+          [framework_path_and_structure
+                  ->framework_dylib_path stringByResolvingSymlinksInPath];
     }
-    if (!cr_version)
-      return false;
-
-    cr_versioned_path = GetVersionedPath(cr_bundle_path, cr_version);
   }
 
-  // Get the framework path.
-  NSString* cr_bundle_exe =
-      ObjCCast<NSString>(
-          [cr_bundle objectForInfoDictionaryKey:@"CFBundleExecutable"]);
-  // Essentially we want chrome::kFrameworkName which looks like
-  // "$PRODUCT_STRING Framework.framework". The library itself is at
-  // "$PRODUCT_STRING Framework.framework/$PRODUCT_STRING Framework". Note that
-  // $PRODUCT_STRING is not |cr_bundle_exe| because in Canary the framework is
-  // still called "Google Chrome Framework".
-  // However, we want the shims to be agnostic to distribution and operate based
-  // on the data in their plist, so encode the framework names here.
-  NSDictionary* framework_for_exe = @{
-    @"Chromium": @"Chromium",
-    @"Google Chrome": @"Google Chrome",
-    @"Google Chrome Canary": @"Google Chrome",
-  };
-  NSString* framework_name = [framework_for_exe objectForKey:cr_bundle_exe];
-  NSString* cr_framework_shlib_path =
-      [cr_versioned_path stringByAppendingPathComponent:
-          [framework_name stringByAppendingString:@" Framework.framework"]];
-  cr_framework_shlib_path =
-      [cr_framework_shlib_path stringByAppendingPathComponent:
-          [framework_name stringByAppendingString:@" Framework"]];
-  if (!cr_bundle_exe || !cr_framework_shlib_path)
+  // At this point it is known that it is an old-style bundle structure (or a
+  // rather broken new-style bundle). Try explicitly specifying the version of
+  // the framework matching the outer bundle version.
+  if (!framework_path_and_structure) {
+    NSString* cr_version = base::mac::ObjCCast<NSString>([cr_bundle
+        objectForInfoDictionaryKey:app_mode::kCFBundleShortVersionStringKey]);
+    if (cr_version) {
+      framework_path_and_structure =
+          GetFrameworkDylibPathAndStructure(cr_bundle_path, cr_version);
+    }
+  }
+
+  if (!framework_path_and_structure)
     return false;
 
-  // A few more sanity checks.
+  // A few sanity checks.
   BOOL is_directory;
   BOOL exists = [[NSFileManager defaultManager]
-                    fileExistsAtPath:cr_framework_shlib_path
-                         isDirectory:&is_directory];
+      fileExistsAtPath:framework_path_and_structure->framework_dylib_path
+           isDirectory:&is_directory];
   if (!exists || is_directory)
     return false;
 
-  // Everything OK, copy output parameters.
+  NSString* cr_framework_path;
+
+  if (framework_path_and_structure->is_new_app_structure) {
+    // For the path to the framework version itself, remove the framework name.
+    cr_framework_path =
+        [framework_path_and_structure
+                ->framework_dylib_path stringByDeletingLastPathComponent];
+  } else {
+    // For the path to the framework itself, remove the framework name ...
+    cr_framework_path =
+        [framework_path_and_structure
+                ->framework_dylib_path stringByDeletingLastPathComponent];
+    // ... the "A" ...
+    cr_framework_path = [cr_framework_path stringByDeletingLastPathComponent];
+    // ... and the "Versions" directory.
+    cr_framework_path = [cr_framework_path stringByDeletingLastPathComponent];
+  }
+
+  // Everything is OK; copy the output parameters.
   *executable_path = base::mac::NSStringToFilePath([cr_bundle executablePath]);
-  *version_path = base::mac::NSStringToFilePath(cr_versioned_path);
-  *framework_shlib_path =
-      base::mac::NSStringToFilePath(cr_framework_shlib_path);
+  *framework_path = base::mac::NSStringToFilePath(cr_framework_path);
+  *framework_dylib_path = base::mac::NSStringToFilePath(
+      framework_path_and_structure->framework_dylib_path);
   return true;
 }
 
diff --git a/chrome/common/mac/app_mode_chrome_locator_browsertest.mm b/chrome/common/mac/app_mode_chrome_locator_browsertest.mm
index 6631a30..615af70 100644
--- a/chrome/common/mac/app_mode_chrome_locator_browsertest.mm
+++ b/chrome/common/mac/app_mode_chrome_locator_browsertest.mm
@@ -10,6 +10,7 @@
 #include "base/files/file_util.h"
 #include "base/files/scoped_temp_dir.h"
 #include "base/path_service.h"
+#include "chrome/common/buildflags.h"
 #include "chrome/common/chrome_constants.h"
 #include "components/version_info/version_info.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -48,11 +49,11 @@
   ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
 
   base::FilePath executable_path;
-  base::FilePath version_path;
   base::FilePath framework_path;
+  base::FilePath framework_dylib_path;
   EXPECT_FALSE(app_mode::GetChromeBundleInfo(temp_dir.GetPath(), std::string(),
-                                             &executable_path, &version_path,
-                                             &framework_path));
+                                             &executable_path, &framework_path,
+                                             &framework_dylib_path));
 }
 
 TEST(ChromeLocatorTest, GetChromeBundleInfo) {
@@ -61,16 +62,14 @@
   ASSERT_TRUE(base::DirectoryExists(chrome_bundle_path));
 
   base::FilePath executable_path;
-  base::FilePath version_path;
   base::FilePath framework_path;
-  EXPECT_TRUE(app_mode::GetChromeBundleInfo(chrome_bundle_path,
-                                            std::string(),
-                                            &executable_path,
-                                            &version_path,
-                                            &framework_path));
+  base::FilePath framework_dylib_path;
+  EXPECT_TRUE(app_mode::GetChromeBundleInfo(chrome_bundle_path, std::string(),
+                                            &executable_path, &framework_path,
+                                            &framework_dylib_path));
   EXPECT_TRUE(base::PathExists(executable_path));
-  EXPECT_TRUE(base::DirectoryExists(version_path));
-  EXPECT_TRUE(base::PathExists(framework_path));
+  EXPECT_TRUE(base::DirectoryExists(framework_path));
+  EXPECT_TRUE(base::PathExists(framework_dylib_path));
 }
 
 TEST(ChromeLocatorTest, GetChromeBundleInfoWithLatestVersion) {
@@ -79,16 +78,14 @@
   ASSERT_TRUE(base::DirectoryExists(chrome_bundle_path));
 
   base::FilePath executable_path;
-  base::FilePath version_path;
   base::FilePath framework_path;
-  EXPECT_TRUE(app_mode::GetChromeBundleInfo(chrome_bundle_path,
-                                            version_info::GetVersionNumber(),
-                                            &executable_path,
-                                            &version_path,
-                                            &framework_path));
+  base::FilePath framework_dylib_path;
+  EXPECT_TRUE(app_mode::GetChromeBundleInfo(
+      chrome_bundle_path, version_info::GetVersionNumber(), &executable_path,
+      &framework_path, &framework_dylib_path));
   EXPECT_TRUE(base::PathExists(executable_path));
-  EXPECT_TRUE(base::DirectoryExists(version_path));
-  EXPECT_TRUE(base::PathExists(framework_path));
+  EXPECT_TRUE(base::DirectoryExists(framework_path));
+  EXPECT_TRUE(base::PathExists(framework_dylib_path));
 }
 
 TEST(ChromeLocatorTest, GetChromeBundleInfoWithInvalidVersion) {
@@ -97,17 +94,15 @@
   ASSERT_TRUE(base::DirectoryExists(chrome_bundle_path));
 
   base::FilePath executable_path;
-  base::FilePath version_path;
   base::FilePath framework_path;
+  base::FilePath framework_dylib_path;
   // This still passes because it should default to the latest version.
-  EXPECT_TRUE(app_mode::GetChromeBundleInfo(chrome_bundle_path,
-                                            std::string("invalid_version"),
-                                            &executable_path,
-                                            &version_path,
-                                            &framework_path));
+  EXPECT_TRUE(app_mode::GetChromeBundleInfo(
+      chrome_bundle_path, std::string("invalid_version"), &executable_path,
+      &framework_path, &framework_dylib_path));
   EXPECT_TRUE(base::PathExists(executable_path));
-  EXPECT_TRUE(base::DirectoryExists(version_path));
-  EXPECT_TRUE(base::PathExists(framework_path));
+  EXPECT_TRUE(base::DirectoryExists(framework_path));
+  EXPECT_TRUE(base::PathExists(framework_dylib_path));
 }
 
 TEST(ChromeLocatorTest, GetChromeBundleInfoWithPreviousVersion) {
@@ -116,24 +111,30 @@
   ASSERT_TRUE(base::DirectoryExists(chrome_bundle_path));
 
   // Make a symlink that pretends to be a previous version.
+#if BUILDFLAG(NEW_MAC_BUNDLE_STRUCTURE)
+  base::FilePath fake_version_directory = chrome_bundle_path.Append("Contents")
+                                              .Append("Frameworks")
+                                              .Append(chrome::kFrameworkName)
+                                              .Append("Versions")
+                                              .Append("previous_version");
+#else
   base::FilePath fake_version_directory = chrome_bundle_path.Append("Contents")
                                               .Append("Versions")
                                               .Append("previous_version");
+#endif
   EXPECT_TRUE(
       base::CreateSymbolicLink(base::FilePath(version_info::GetVersionNumber()),
                                fake_version_directory));
 
   base::FilePath executable_path;
-  base::FilePath version_path;
   base::FilePath framework_path;
-  EXPECT_TRUE(app_mode::GetChromeBundleInfo(chrome_bundle_path,
-                                            std::string("previous_version"),
-                                            &executable_path,
-                                            &version_path,
-                                            &framework_path));
+  base::FilePath framework_dylib_path;
+  EXPECT_TRUE(app_mode::GetChromeBundleInfo(
+      chrome_bundle_path, std::string("previous_version"), &executable_path,
+      &framework_path, &framework_dylib_path));
   EXPECT_TRUE(base::PathExists(executable_path));
-  EXPECT_TRUE(base::DirectoryExists(version_path));
-  EXPECT_TRUE(base::PathExists(framework_path));
+  EXPECT_TRUE(base::DirectoryExists(framework_path));
+  EXPECT_TRUE(base::PathExists(framework_dylib_path));
 
   base::DeleteFile(fake_version_directory, false);
 }
diff --git a/chrome/common/mac/app_mode_common.h b/chrome/common/mac/app_mode_common.h
index ade21e3..db0140a 100644
--- a/chrome/common/mac/app_mode_common.h
+++ b/chrome/common/mac/app_mode_common.h
@@ -6,8 +6,10 @@
 #define CHROME_COMMON_MAC_APP_MODE_COMMON_H_
 
 #include <CoreServices/CoreServices.h>
+
 #include "base/files/file_path.h"
 #include "base/strings/string16.h"
+#include "base/strings/stringize_macros.h"
 
 #ifdef __OBJC__
 @class NSString;
@@ -18,6 +20,18 @@
 // This file contains constants, interfaces, etc. which are common to the
 // browser application and the app mode loader (a.k.a. shim).
 
+// The version of the ChromeAppModeInfo struct below. If the format of the
+// struct ever changes, be sure to update the APP_SHIM_VERSION_NUMBER here and
+// the corresponding line in //chrome/app/framework.order .
+#define APP_SHIM_VERSION_NUMBER 6
+
+// All the other macro magic to make APP_SHIM_VERSION_NUMBER usable.
+#define APP_MODE_CONCAT(a, b) a##b
+#define APP_MODE_CONCAT2(a, b) APP_MODE_CONCAT(a, b)
+#define APP_SHIM_ENTRY_POINT_NAME \
+  APP_MODE_CONCAT2(ChromeAppModeStart_v, APP_SHIM_VERSION_NUMBER)
+#define APP_SHIM_ENTRY_POINT_NAME_STRING STRINGIZE(APP_SHIM_ENTRY_POINT_NAME)
+
 namespace app_mode {
 
 // The IPC socket used to communicate between app shims and Chrome will be
@@ -138,56 +152,49 @@
 // Bundle ID of the Chrome browser bundle.
 extern NSString* const kShortcutBrowserBundleIDPlaceholder;
 
-// Current major version |ChromeAppModeInfo|. This corresponds to the entrypoint
-// ChromeAppModeStart_v5 and also the shortcut version
-// kCurrentAppShortcutsVersion. All three must be updated in lock-step.
-const uint32_t kCurrentChromeAppModeInfoMajorVersion = 5;
-
-// The minor version of |ChromeAppModeInfo|. This can be used to add additional
-// optional members to ChromeAppModeInfo as needed.
-const uint32_t kCurrentChromeAppModeInfoMinorVersion = 0;
-
 // The structure used to pass information from the app mode loader to the
-// (browser) framework. Across Chrome versions, the layout of this structure
-// **MUST NOT CHANGE** and **MUST NOT CHANGE** and **MUST NOT CHANGE**. This
-// implies that no base/ or std:: types may be used in this structure.
+// (browser) framework via the entry point ChromeAppModeStart_vN.
+//
+// As long as the name of the entry point is kept constant and
+// APP_SHIM_VERSION_NUMBER does not change, the layout of this structure
+// **MUST NOT CHANGE**, even across Chromium versions. This implies that no
+// base/ or std:: types may be used in this structure.
+//
+// However, this structure *may* be changed as long as the
+// APP_SHIM_VERSION_NUMBER above is updated; don't forget to also update the
+// corresponding line in //chrome/app/framework.order .
 struct ChromeAppModeInfo {
-  // Major and minor version number of this structure (see
-  // kCurrentChromeAppModeInfoMajorVersion and
-  // kCurrentChromeAppModeInfoMinorVersion).
-  uint32_t major_version;
-  uint32_t minor_version;
-
-  // Original |argc| and |argv|.
+  // Original |argc| and |argv| of the App Mode shortcut.
   int argc;
   char** argv;
 
-  // Versioned path to the browser which is being loaded as UTF8.
-  const char* chrome_versioned_path;
+  // Path of the Chromium Framework, as UTF-8. This will be the input to
+  // SetOverrideFrameworkBundlePath().
+  const char* chrome_framework_path;
 
-  // Path to Chrome app bundle as UTF8.
+  // Path to Chromium app bundle, as UTF-8.
   const char* chrome_outer_bundle_path;
 
   // Information about the App Mode shortcut:
 
-  // Path to the App Mode Loader application bundle that launched the process
-  // as UTF8.
+  // Path to the App Mode Loader application bundle that launched the process,
+  // as UTF-8.
   const char* app_mode_bundle_path;
 
-  // Short UTF8 ID string, preferably derived from |app_mode_short_name|. Should
-  // be safe for the file system.
+  // Short UTF-8 ID string, preferably derived from |app_mode_short_name|.
+  // Should be safe for the file system.
   const char* app_mode_id;
 
-  // Unrestricted (e.g., several-word) UTF8-encoded name for the shortcut.
+  // Unrestricted (e.g., several-word) UTF-8-encoded name for the shortcut.
   const char* app_mode_name;
 
-  // URL for the shortcut. Must be a valid UTF8-encoded URL.
+  // URL for the shortcut. Must be a valid UTF-8-encoded URL.
   const char* app_mode_url;
 
-  // Path to the app's user data directory as UTF8.
+  // Path to the app's user data directory, as UTF-8.
   const char* user_data_dir;
 
-  // Directory of the profile associated with the app as UTF8.
+  // Directory of the profile associated with the app, as UTF-8.
   const char* profile_dir;
 };
 
diff --git a/chrome/common/mac/app_mode_common.mm b/chrome/common/mac/app_mode_common.mm
index ca35fc5..de0be29 100644
--- a/chrome/common/mac/app_mode_common.mm
+++ b/chrome/common/mac/app_mode_common.mm
@@ -64,20 +64,19 @@
 // ChromeAppModeInfo is built into the app_shim_loader binary that is not
 // updated with Chrome. If the layout of this structure changes, then Chrome
 // must rebuild all app shims. See https://crrev.com/362634 as an example.
-static_assert(offsetof(ChromeAppModeInfo, major_version) == 0x0 &&
-                  offsetof(ChromeAppModeInfo, minor_version) == 0x4 &&
-                  offsetof(ChromeAppModeInfo, argc) == 0x8 &&
-                  offsetof(ChromeAppModeInfo, argv) == 0x10 &&
-                  offsetof(ChromeAppModeInfo, chrome_versioned_path) == 0x18 &&
-                  offsetof(ChromeAppModeInfo, chrome_outer_bundle_path) ==
-                      0x20 &&
-                  offsetof(ChromeAppModeInfo, app_mode_bundle_path) == 0x28 &&
-                  offsetof(ChromeAppModeInfo, app_mode_id) == 0x30 &&
-                  offsetof(ChromeAppModeInfo, app_mode_name) == 0x38 &&
-                  offsetof(ChromeAppModeInfo, app_mode_url) == 0x40 &&
-                  offsetof(ChromeAppModeInfo, user_data_dir) == 0x48 &&
-                  offsetof(ChromeAppModeInfo, profile_dir) == 0x50,
-              "ChromeAppModeInfo layout has changed, rebuild all app shims.");
+static_assert(
+    offsetof(ChromeAppModeInfo, argc) == 0x0 &&
+        offsetof(ChromeAppModeInfo, argv) == 0x8 &&
+        offsetof(ChromeAppModeInfo, chrome_framework_path) == 0x10 &&
+        offsetof(ChromeAppModeInfo, chrome_outer_bundle_path) == 0x18 &&
+        offsetof(ChromeAppModeInfo, app_mode_bundle_path) == 0x20 &&
+        offsetof(ChromeAppModeInfo, app_mode_id) == 0x28 &&
+        offsetof(ChromeAppModeInfo, app_mode_name) == 0x30 &&
+        offsetof(ChromeAppModeInfo, app_mode_url) == 0x38 &&
+        offsetof(ChromeAppModeInfo, user_data_dir) == 0x40 &&
+        offsetof(ChromeAppModeInfo, profile_dir) == 0x48,
+    "ChromeAppModeInfo layout has changed; bump the APP_SHIM_VERSION_NUMBER "
+    "in chrome/common/mac/app_mode_common.h. (And fix this static_assert.)");
 
 void VerifySocketPermissions(const base::FilePath& socket_path) {
   CHECK(base::PathIsWritable(socket_path));
diff --git a/chrome/common/url_constants.cc b/chrome/common/url_constants.cc
index daa5cfe..e85e33b 100644
--- a/chrome/common/url_constants.cc
+++ b/chrome/common/url_constants.cc
@@ -262,6 +262,9 @@
 #endif
 
 #if defined(OS_CHROMEOS)
+const char kAccountManagerLearnMoreURL[] =
+    "https://support.google.com/chromebook/?p=google_accounts";
+
 const char kAndroidAppsLearnMoreURL[] =
     "https://support.google.com/chromebook/?p=playapps";
 
diff --git a/chrome/common/url_constants.h b/chrome/common/url_constants.h
index 72d5ffa..895c834 100644
--- a/chrome/common/url_constants.h
+++ b/chrome/common/url_constants.h
@@ -209,6 +209,9 @@
 #endif
 
 #if defined(OS_CHROMEOS)
+// Help center URL for Chrome OS Account Manager.
+extern const char kAccountManagerLearnMoreURL[];
+
 // The URL for the "learn more" link for Google Play Store (ARC) settings.
 extern const char kAndroidAppsLearnMoreURL[];
 
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index 845707f..2f90e3b 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -1979,6 +1979,7 @@
         "../browser/chromeos/login/wizard_controller_browsertest.cc",
         "../browser/chromeos/net/network_portal_detector_impl_browsertest.cc",
         "../browser/chromeos/network_change_manager_client_browsertest.cc",
+        "../browser/chromeos/plugin_vm/plugin_vm_test_helper.cc",
         "../browser/chromeos/policy/affiliation_test_helper.cc",
         "../browser/chromeos/policy/affiliation_test_helper.h",
         "../browser/chromeos/policy/blocking_login_browsertest.cc",
diff --git a/chrome/test/base/testing_profile_manager.cc b/chrome/test/base/testing_profile_manager.cc
index e29f63a..370acd5 100644
--- a/chrome/test/base/testing_profile_manager.cc
+++ b/chrome/test/base/testing_profile_manager.cc
@@ -76,7 +76,8 @@
     const base::string16& user_name,
     int avatar_id,
     const std::string& supervised_user_id,
-    TestingProfile::TestingFactories testing_factories) {
+    TestingProfile::TestingFactories testing_factories,
+    base::Optional<bool> override_new_profile) {
   DCHECK(called_set_up_);
 
   // Create a path for the profile based on the name.
@@ -101,6 +102,8 @@
   builder.SetPrefService(std::move(prefs));
   builder.SetSupervisedUserId(supervised_user_id);
   builder.SetProfileName(profile_name);
+  if (override_new_profile)
+    builder.OverrideIsNewProfile(*override_new_profile);
 
   for (TestingProfile::TestingFactories::value_type& pair : testing_factories)
     builder.AddTestingFactory(pair.first, std::move(pair.second));
diff --git a/chrome/test/base/testing_profile_manager.h b/chrome/test/base/testing_profile_manager.h
index b4510b6..39eb602 100644
--- a/chrome/test/base/testing_profile_manager.h
+++ b/chrome/test/base/testing_profile_manager.h
@@ -66,7 +66,8 @@
       const base::string16& user_name,
       int avatar_id,
       const std::string& supervised_user_id,
-      TestingProfile::TestingFactories testing_factories);
+      TestingProfile::TestingFactories testing_factories,
+      base::Optional<bool> override_new_profile = base::Optional<bool>());
 
   // Small helper for creating testing profiles. Just forwards to above.
   TestingProfile* CreateTestingProfile(const std::string& name);
diff --git a/chrome/test/data/webui/settings/people_page_account_manager_test.js b/chrome/test/data/webui/settings/people_page_account_manager_test.js
index c9cdede..63368ca 100644
--- a/chrome/test/data/webui/settings/people_page_account_manager_test.js
+++ b/chrome/test/data/webui/settings/people_page_account_manager_test.js
@@ -25,6 +25,7 @@
           accountType: 1,
           isDeviceAccount: true,
           isSignedIn: true,
+          unmigrated: false,
           fullName: 'Device Account',
           email: 'admin@domain.com',
           pic: 'data:image/png;base64,abc123',
@@ -35,6 +36,7 @@
           accountType: 1,
           isDeviceAccount: false,
           isSignedIn: true,
+          unmigrated: false,
           fullName: 'Secondary Account 1',
           email: 'user1@example.com',
           pic: '',
@@ -44,9 +46,20 @@
           accountType: 1,
           isDeviceAccount: false,
           isSignedIn: false,
+          unmigrated: false,
           fullName: 'Secondary Account 2',
           email: 'user2@example.com',
           pic: '',
+        },
+        {
+          id: '1010',
+          accountType: 1,
+          isDeviceAccount: false,
+          isSignedIn: false,
+          unmigrated: true,
+          fullName: 'Secondary Account 3',
+          email: 'user3@example.com',
+          pic: '',
         }
       ]);
     }
@@ -96,6 +109,7 @@
             accountType: 1,
             isDeviceAccount: true,
             isSignedIn: true,
+            unmigrated: false,
             fullName: 'Device Account',
             email: 'admin@domain.com',
             pic: 'data:image/png;base64,abc123',
@@ -132,8 +146,8 @@
     test('AccountListIsPopulatedAtStartup', function() {
       return browserProxy.whenCalled('getAccounts').then(() => {
         Polymer.dom.flush();
-        // 3 accounts were added in |getAccounts()| mock above.
-        assertEquals(3, accountList.items.length);
+        // 4 accounts were added in |getAccounts()| mock above.
+        assertEquals(4, accountList.items.length);
       });
     });
 
@@ -156,6 +170,26 @@
       });
     });
 
+    test('UnauthenticatedAccountLabel', function() {
+      return browserProxy.whenCalled('getAccounts').then(() => {
+        Polymer.dom.flush();
+        assertEquals(
+            loadTimeData.getString('accountManagerReauthenticationLabel'),
+            accountManager.root.querySelectorAll('.reauth-button')[0]
+                .textContent.trim());
+      });
+    });
+
+    test('UnmigratedAccountLabel', function() {
+      return browserProxy.whenCalled('getAccounts').then(() => {
+        Polymer.dom.flush();
+        assertEquals(
+            loadTimeData.getString('accountManagerMigrationLabel'),
+            accountManager.root.querySelectorAll('.reauth-button')[1]
+                .textContent.trim());
+      });
+    });
+
     test('RemoveAccount', function() {
       return browserProxy.whenCalled('getAccounts').then(() => {
         Polymer.dom.flush();
diff --git a/components/autofill_assistant/browser/BUILD.gn b/components/autofill_assistant/browser/BUILD.gn
index 179b562..00412bf 100644
--- a/components/autofill_assistant/browser/BUILD.gn
+++ b/components/autofill_assistant/browser/BUILD.gn
@@ -162,6 +162,7 @@
     "actions/mock_action_delegate.cc",
     "actions/mock_action_delegate.h",
     "actions/prompt_action_unittest.cc",
+    "actions/wait_for_dom_action_unittest.cc",
     "batch_element_checker_unittest.cc",
     "controller_unittest.cc",
     "element_area_unittest.cc",
diff --git a/components/autofill_assistant/browser/actions/action_delegate.h b/components/autofill_assistant/browser/actions/action_delegate.h
index eb9f95f..8abc46e 100644
--- a/components/autofill_assistant/browser/actions/action_delegate.h
+++ b/components/autofill_assistant/browser/actions/action_delegate.h
@@ -47,8 +47,7 @@
   virtual std::string GetStatusMessage() = 0;
 
   // Checks one or more elements.
-  virtual void RunElementChecks(BatchElementChecker* checker,
-                                base::OnceCallback<void()> all_done) = 0;
+  virtual void RunElementChecks(BatchElementChecker* checker) = 0;
 
   // Wait for a short time for a given selector to appear.
   //
@@ -63,27 +62,17 @@
   virtual void ShortWaitForElement(const Selector& selector,
                                    base::OnceCallback<void(bool)> callback) = 0;
 
-  enum class SelectorPredicate {
-    // The selector matches elements
-    kMatches,
-
-    // The selector doesn't match any elements
-    kDoesntMatch
-  };
-
-  // Wait for up to |max_wait_time| for the element |selectors| to match
-  // element(s) on the page, then call |callback| with true if at least an
-  // element matched, false otherwise.
-  //
-  // |selector_predicate| specifies the condition that must be satisfied for
-  // WaitForDom to return successfully. It applies to the given |selector|.
+  // Wait for up to |max_wait_time| for element conditions to match on the page,
+  // then call |callback| with a successful status if at least an element
+  // matched, an error status otherwise.
   //
   // If |allow_interrupt| interrupts can run while waiting.
   virtual void WaitForDom(
       base::TimeDelta max_wait_time,
       bool allow_interrupt,
-      SelectorPredicate selector_predicate,
-      const Selector& selector,
+      base::RepeatingCallback<void(BatchElementChecker*,
+                                   base::OnceCallback<void(bool)>)>
+          check_elements,
       base::OnceCallback<void(ProcessedActionStatusProto)> callback) = 0;
 
   // Click or tap the element given by |selector| on the web page.
diff --git a/components/autofill_assistant/browser/actions/autofill_action.cc b/components/autofill_assistant/browser/actions/autofill_action.cc
index 81b1471..655c37d 100644
--- a/components/autofill_assistant/browser/actions/autofill_action.cc
+++ b/components/autofill_assistant/browser/actions/autofill_action.cc
@@ -153,11 +153,11 @@
         base::BindOnce(&AutofillAction::OnGetRequiredFieldValue,
                        weak_ptr_factory_.GetWeakPtr(), i));
   }
-  delegate->RunElementChecks(
-      batch_element_checker_.get(),
+  batch_element_checker_->AddAllDoneCallback(
       base::BindOnce(&AutofillAction::OnCheckRequiredFieldsDone,
                      weak_ptr_factory_.GetWeakPtr(), base::Unretained(delegate),
                      allow_fallback));
+  delegate->RunElementChecks(batch_element_checker_.get());
 }
 
 void AutofillAction::OnGetRequiredFieldValue(int required_fields_index,
diff --git a/components/autofill_assistant/browser/actions/autofill_action_unittest.cc b/components/autofill_assistant/browser/actions/autofill_action_unittest.cc
index 420cad5..7614d11 100644
--- a/components/autofill_assistant/browser/actions/autofill_action_unittest.cc
+++ b/components/autofill_assistant/browser/actions/autofill_action_unittest.cc
@@ -116,9 +116,8 @@
     ON_CALL(mock_action_delegate_, GetPersonalDataManager)
         .WillByDefault(Return(personal_data_manager_.get()));
     ON_CALL(mock_action_delegate_, RunElementChecks)
-        .WillByDefault(Invoke([this](BatchElementChecker* checker,
-                                     base::OnceCallback<void()> all_done) {
-          checker->Run(&mock_web_controller_, std::move(all_done));
+        .WillByDefault(Invoke([this](BatchElementChecker* checker) {
+          checker->Run(&mock_web_controller_);
         }));
     ON_CALL(mock_action_delegate_, OnShortWaitForElement(_, _))
         .WillByDefault(RunOnceCallback<1>(true));
diff --git a/components/autofill_assistant/browser/actions/mock_action_delegate.h b/components/autofill_assistant/browser/actions/mock_action_delegate.h
index 526db5a..c4db4998 100644
--- a/components/autofill_assistant/browser/actions/mock_action_delegate.h
+++ b/components/autofill_assistant/browser/actions/mock_action_delegate.h
@@ -22,8 +22,7 @@
   MockActionDelegate();
   ~MockActionDelegate() override;
 
-  MOCK_METHOD2(RunElementChecks,
-               void(BatchElementChecker*, base::OnceCallback<void()>));
+  MOCK_METHOD1(RunElementChecks, void(BatchElementChecker*));
 
   void ShortWaitForElement(const Selector& selector,
                            base::OnceCallback<void(bool)> callback) override {
@@ -36,19 +35,20 @@
   void WaitForDom(
       base::TimeDelta max_wait_time,
       bool allow_interrupt,
-      ActionDelegate::SelectorPredicate selector_predicate,
-      const Selector& selector,
+      base::RepeatingCallback<void(BatchElementChecker*,
+                                   base::OnceCallback<void(bool)>)>
+          check_elements,
       base::OnceCallback<void(ProcessedActionStatusProto)> callback) override {
-    OnWaitForDom(max_wait_time, allow_interrupt, selector_predicate, selector,
-                 callback);
+    OnWaitForDom(max_wait_time, allow_interrupt, check_elements, callback);
   }
 
-  MOCK_METHOD5(OnWaitForDom,
-               void(base::TimeDelta,
-                    bool,
-                    ActionDelegate::SelectorPredicate,
-                    const Selector&,
-                    base::OnceCallback<void(ProcessedActionStatusProto)>&));
+  MOCK_METHOD4(
+      OnWaitForDom,
+      void(base::TimeDelta,
+           bool,
+           base::RepeatingCallback<void(BatchElementChecker*,
+                                        base::OnceCallback<void(bool)>)>&,
+           base::OnceCallback<void(ProcessedActionStatusProto)>&));
 
   MOCK_METHOD1(SetStatusMessage, void(const std::string& message));
   MOCK_METHOD0(GetStatusMessage, std::string());
diff --git a/components/autofill_assistant/browser/actions/prompt_action.cc b/components/autofill_assistant/browser/actions/prompt_action.cc
index 7949620..41f1f81 100644
--- a/components/autofill_assistant/browser/actions/prompt_action.cc
+++ b/components/autofill_assistant/browser/actions/prompt_action.cc
@@ -87,10 +87,9 @@
                              base::BindOnce(&PromptAction::OnPreconditionResult,
                                             weak_ptr_factory_.GetWeakPtr(), i));
   }
-  delegate_->RunElementChecks(
-      precondition_checker_.get(),
-      base::BindOnce(&PromptAction::OnPreconditionChecksDone,
-                     weak_ptr_factory_.GetWeakPtr()));
+  precondition_checker_->AddAllDoneCallback(base::BindOnce(
+      &PromptAction::OnPreconditionChecksDone, weak_ptr_factory_.GetWeakPtr()));
+  delegate_->RunElementChecks(precondition_checker_.get());
 }
 
 void PromptAction::OnPreconditionResult(size_t choice_index, bool result) {
@@ -156,9 +155,9 @@
         selector, base::BindOnce(&PromptAction::OnAutoSelectElementExists,
                                  weak_ptr_factory_.GetWeakPtr(), i));
   }
-  delegate_->RunElementChecks(auto_select_checker_.get(),
-                              base::BindOnce(&PromptAction::OnAutoSelectDone,
-                                             weak_ptr_factory_.GetWeakPtr()));
+  auto_select_checker_->AddAllDoneCallback(base::BindOnce(
+      &PromptAction::OnAutoSelectDone, weak_ptr_factory_.GetWeakPtr()));
+  delegate_->RunElementChecks(auto_select_checker_.get());
 }
 
 void PromptAction::OnAutoSelectElementExists(int choice_index, bool exists) {
diff --git a/components/autofill_assistant/browser/actions/prompt_action_unittest.cc b/components/autofill_assistant/browser/actions/prompt_action_unittest.cc
index 6a66382..f0cc663f 100644
--- a/components/autofill_assistant/browser/actions/prompt_action_unittest.cc
+++ b/components/autofill_assistant/browser/actions/prompt_action_unittest.cc
@@ -40,9 +40,8 @@
         .WillByDefault(RunOnceCallback<1>(false, ""));
 
     ON_CALL(mock_action_delegate_, RunElementChecks)
-        .WillByDefault(Invoke([this](BatchElementChecker* checker,
-                                     base::OnceCallback<void()> all_done) {
-          checker->Run(&mock_web_controller_, std::move(all_done));
+        .WillByDefault(Invoke([this](BatchElementChecker* checker) {
+          checker->Run(&mock_web_controller_);
         }));
     ON_CALL(mock_action_delegate_, Prompt(_))
         .WillByDefault(Invoke([this](std::unique_ptr<std::vector<Chip>> chips) {
diff --git a/components/autofill_assistant/browser/actions/wait_for_dom_action.cc b/components/autofill_assistant/browser/actions/wait_for_dom_action.cc
index c0127e86..8afcc88 100644
--- a/components/autofill_assistant/browser/actions/wait_for_dom_action.cc
+++ b/components/autofill_assistant/browser/actions/wait_for_dom_action.cc
@@ -32,34 +32,131 @@
   if (timeout_ms > 0)
     max_wait_time = base::TimeDelta::FromMilliseconds(timeout_ms);
 
-  Selector wait_until = Selector(proto_.wait_for_dom().wait_until());
-  Selector wait_while = Selector(proto_.wait_for_dom().wait_while());
-  ActionDelegate::SelectorPredicate selector_predicate;
-  Selector selector;
-  if (!wait_until.empty()) {
-    // wait until the selector matches something
-    selector_predicate = ActionDelegate::SelectorPredicate::kMatches;
-    selector = wait_until;
-  } else if (!wait_while.empty()) {
-    // wait as long as the selector matches something
-    selector_predicate = ActionDelegate::SelectorPredicate::kDoesntMatch;
-    selector = wait_while;
-  } else {
-    DVLOG(1) << __func__ << ": no selector specified for WaitForDom";
-    OnCheckDone(std::move(callback), INVALID_SELECTOR);
+  AddConditionsFromProto();
+  if (conditions_.empty()) {
+    DVLOG(2) << "WaitForDomAction: no selectors specified";
+    OnCheckDone(std::move(callback), INVALID_ACTION);
     return;
   }
+  for (size_t i = 0; i < conditions_.size(); i++) {
+    if (conditions_[i].selector.empty()) {
+      DVLOG(2) << "WaitForDomAction: selector for condition " << i
+               << " is empty";
+      OnCheckDone(std::move(callback), INVALID_SELECTOR);
+      return;
+    }
+  }
 
   delegate->WaitForDom(
       max_wait_time, proto_.wait_for_dom().allow_interrupt(),
-      selector_predicate, selector,
+      base::BindRepeating(&WaitForDomAction::CheckElements,
+                          weak_ptr_factory_.GetWeakPtr()),
       base::BindOnce(&WaitForDomAction::OnCheckDone,
                      weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
 }
 
+void WaitForDomAction::AddConditionsFromProto() {
+  require_all_ = false;
+  switch (proto_.wait_for_dom().wait_on_case()) {
+    case WaitForDomProto::kWaitUntil:
+      AddCondition(SelectorPredicate::kMatch,
+                   proto_.wait_for_dom().wait_until(), "");
+      break;
+
+    case WaitForDomProto::kWaitWhile:
+      AddCondition(SelectorPredicate::kNoMatch,
+                   proto_.wait_for_dom().wait_while(), "");
+      break;
+
+    case WaitForDomProto::kWaitForAll:
+      require_all_ = true;
+      for (const auto& condition_proto :
+           proto_.wait_for_dom().wait_for_all().conditions()) {
+        AddCondition(condition_proto);
+      }
+      break;
+
+    case WaitForDomProto::kWaitForAny:
+      for (const auto& condition_proto :
+           proto_.wait_for_dom().wait_for_any().conditions()) {
+        AddCondition(condition_proto);
+      }
+      break;
+
+    case WaitForDomProto::WAIT_ON_NOT_SET:
+      // an empty condition_ will be rejected when validating conditions_.
+      break;
+
+      // No default set to trigger a compilation error if a new case is added.
+  }
+}
+
+void WaitForDomAction::AddCondition(
+    const WaitForDomProto::ElementCondition& condition) {
+  if (condition.has_must_match()) {
+    AddCondition(SelectorPredicate::kMatch, condition.must_match(),
+                 condition.server_payload());
+  } else {
+    AddCondition(SelectorPredicate::kNoMatch, condition.must_not_match(),
+                 condition.server_payload());
+  }
+}
+
+void WaitForDomAction::AddCondition(SelectorPredicate predicate,
+                                    const ElementReferenceProto& selector_proto,
+                                    const std::string& server_payload) {
+  conditions_.emplace_back();
+  Condition& condition = conditions_.back();
+  condition.predicate = predicate;
+  condition.selector = Selector(selector_proto);
+  condition.server_payload = server_payload;
+}
+
+void WaitForDomAction::CheckElements(BatchElementChecker* checker,
+                                     base::OnceCallback<void(bool)> callback) {
+  for (size_t i = 0; i < conditions_.size(); i++) {
+    checker->AddElementCheck(
+        conditions_[i].selector,
+        base::BindOnce(&WaitForDomAction::OnSingleElementCheckDone,
+                       weak_ptr_factory_.GetWeakPtr(), i));
+  }
+  checker->AddAllDoneCallback(
+      base::BindOnce(&WaitForDomAction::OnAllElementChecksDone,
+                     weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
+}
+
+void WaitForDomAction::OnSingleElementCheckDone(size_t condition_index,
+                                                bool found) {
+  DCHECK(condition_index < conditions_.size());
+  Condition& condition = conditions_[condition_index];
+  condition.match =
+      condition.predicate == SelectorPredicate::kMatch ? found : !found;
+  // We can't stop here since the batch checker does not support stopping from a
+  // single element callback.
+}
+
+void WaitForDomAction::OnAllElementChecksDone(
+    base::OnceCallback<void(bool)> callback) {
+  size_t match_count = 0;
+  for (auto& condition : conditions_) {
+    if (condition.match) {
+      match_count++;
+    }
+  }
+  bool success =
+      require_all_ ? match_count == conditions_.size() : match_count > 0;
+  std::move(callback).Run(success);
+}
+
 void WaitForDomAction::OnCheckDone(ProcessActionCallback callback,
                                    ProcessedActionStatusProto status) {
   UpdateProcessedAction(status);
+  for (auto& condition : conditions_) {
+    if (condition.match && !condition.server_payload.empty()) {
+      processed_action_proto_->mutable_wait_for_dom_result()
+          ->add_matching_condition_payloads(condition.server_payload);
+    }
+  }
   std::move(callback).Run(std::move(processed_action_proto_));
 }
 }  // namespace autofill_assistant
diff --git a/components/autofill_assistant/browser/actions/wait_for_dom_action.h b/components/autofill_assistant/browser/actions/wait_for_dom_action.h
index 05f87ba..23025f4 100644
--- a/components/autofill_assistant/browser/actions/wait_for_dom_action.h
+++ b/components/autofill_assistant/browser/actions/wait_for_dom_action.h
@@ -15,6 +15,8 @@
 #include "components/autofill_assistant/browser/service.pb.h"
 
 namespace autofill_assistant {
+class BatchElementChecker;
+
 // An action to ask Chrome to wait for a DOM element to process next action.
 class WaitForDomAction : public Action {
  public:
@@ -22,13 +24,57 @@
   ~WaitForDomAction() override;
 
  private:
+  enum class SelectorPredicate {
+    // The selector matches elements
+    kMatch,
+
+    // The selector doesn't match any elements
+    kNoMatch
+  };
+
+  struct Condition {
+    // Whether the selector should match or not.
+    SelectorPredicate predicate = SelectorPredicate::kMatch;
+
+    // The selector to look for.
+    Selector selector;
+
+    // True if the condition matched.
+    bool match = false;
+
+    // A payload to report to the server when this condition match. Empty
+    // payloads are not reported.
+    std::string server_payload;
+  };
+
   // Overrides Action:
   void InternalProcessAction(ActionDelegate* delegate,
                              ProcessActionCallback callback) override;
 
+  // Initializes |require_all_| and |conditions_| from |proto_|.
+  void AddConditionsFromProto();
+
+  // Adds a single condition to |conditions_|.
+  void AddCondition(const WaitForDomProto::ElementCondition& condition);
+
+  // Adds a single condition to |conditions_|.
+  void AddCondition(SelectorPredicate predicate,
+                    const ElementReferenceProto& selector_proto,
+                    const std::string& server_payload);
+
+  // Check all elements using the given BatchElementChecker and reports the
+  // result to |callback|.
+  void CheckElements(BatchElementChecker* checker,
+                     base::OnceCallback<void(bool)> callback);
+  void OnSingleElementCheckDone(size_t condition_index, bool result);
+  void OnAllElementChecksDone(base::OnceCallback<void(bool)> callback);
+
   void OnCheckDone(ProcessActionCallback callback,
                    ProcessedActionStatusProto status);
 
+  bool require_all_ = false;
+  std::vector<Condition> conditions_;
+
   base::WeakPtrFactory<WaitForDomAction> weak_ptr_factory_;
 
   DISALLOW_COPY_AND_ASSIGN(WaitForDomAction);
diff --git a/components/autofill_assistant/browser/actions/wait_for_dom_action_unittest.cc b/components/autofill_assistant/browser/actions/wait_for_dom_action_unittest.cc
new file mode 100644
index 0000000..d574bbe
--- /dev/null
+++ b/components/autofill_assistant/browser/actions/wait_for_dom_action_unittest.cc
@@ -0,0 +1,271 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/autofill_assistant/browser/actions/wait_for_dom_action.h"
+
+#include <utility>
+
+#include "base/bind.h"
+#include "base/callback.h"
+#include "base/test/mock_callback.h"
+#include "components/autofill_assistant/browser/actions/mock_action_delegate.h"
+#include "components/autofill_assistant/browser/mock_run_once_callback.h"
+#include "components/autofill_assistant/browser/mock_web_controller.h"
+#include "testing/gmock/include/gmock/gmock.h"
+
+namespace autofill_assistant {
+namespace {
+
+using ::testing::_;
+using ::testing::ElementsAre;
+using ::testing::Invoke;
+using ::testing::Pointee;
+using ::testing::Property;
+
+class WaitForDomActionTest : public testing::Test {
+ public:
+  WaitForDomActionTest() {}
+
+  void SetUp() override {
+    ON_CALL(mock_web_controller_, OnElementCheck(_, _))
+        .WillByDefault(RunOnceCallback<1>(false));
+
+    EXPECT_CALL(mock_action_delegate_, OnWaitForDom(_, _, _, _))
+        .WillRepeatedly(Invoke(this, &WaitForDomActionTest::FakeWaitForDom));
+  }
+
+ protected:
+  // Fakes ActionDelegate::WaitForDom.
+  void FakeWaitForDom(
+      base::TimeDelta max_wait_time,
+      bool allow_interrupt,
+      base::RepeatingCallback<void(BatchElementChecker*,
+                                   base::OnceCallback<void(bool)>)>&
+          check_elements,
+      base::OnceCallback<void(ProcessedActionStatusProto)>& callback) {
+    checker_ = std::make_unique<BatchElementChecker>();
+    has_check_elements_result_ = false;
+    check_elements.Run(
+        checker_.get(),
+        base::BindOnce(&WaitForDomActionTest::OnCheckElementsDone,
+                       base::Unretained(this)));
+    checker_->AddAllDoneCallback(
+        base::BindOnce(&WaitForDomActionTest::OnWaitForDomDone,
+                       base::Unretained(this), std::move(callback)));
+    checker_->Run(&mock_web_controller_);
+  }
+
+  // Called from the check_elements callback passed to FakeWaitForDom.
+  void OnCheckElementsDone(bool result) {
+    ASSERT_FALSE(has_check_elements_result_);  // Duplicate calls
+    has_check_elements_result_ = true;
+    check_elements_result_ = result;
+  }
+
+  // Called by |checker_| once it's done. This ends the call to
+  // FakeWaitForDom().
+  void OnWaitForDomDone(
+      base::OnceCallback<void(ProcessedActionStatusProto)> callback) {
+    ASSERT_TRUE(
+        has_check_elements_result_);  // OnCheckElementsDone() not called
+    std::move(callback).Run(check_elements_result_ ? ACTION_APPLIED
+                                                   : ELEMENT_RESOLUTION_FAILED);
+  }
+
+  // Runs the action defined in |proto_| and reports the result to |callback_|.
+  void Run() {
+    ActionProto action_proto;
+    *action_proto.mutable_wait_for_dom() = proto_;
+    WaitForDomAction action(action_proto);
+    action.ProcessAction(&mock_action_delegate_, callback_.Get());
+  }
+
+  MockActionDelegate mock_action_delegate_;
+  MockWebController mock_web_controller_;
+  base::MockCallback<Action::ProcessActionCallback> callback_;
+  WaitForDomProto proto_;
+  std::unique_ptr<BatchElementChecker> checker_;
+  bool has_check_elements_result_ = false;
+  bool check_elements_result_ = false;
+};
+
+TEST_F(WaitForDomActionTest, NoSelectors) {
+  EXPECT_CALL(
+      callback_,
+      Run(Pointee(Property(&ProcessedActionProto::status, INVALID_ACTION))));
+  Run();
+}
+
+TEST_F(WaitForDomActionTest, MatchOneElementFound) {
+  EXPECT_CALL(mock_web_controller_, OnElementCheck(Selector({"#element"}), _))
+      .WillRepeatedly(RunOnceCallback<1>(true));
+
+  proto_.mutable_wait_until()->add_selectors("#element");
+  EXPECT_CALL(
+      callback_,
+      Run(Pointee(Property(&ProcessedActionProto::status, ACTION_APPLIED))));
+  Run();
+}
+
+TEST_F(WaitForDomActionTest, MatchOneElementNotFound) {
+  proto_.mutable_wait_until()->add_selectors("#element");
+  EXPECT_CALL(callback_, Run(Pointee(Property(&ProcessedActionProto::status,
+                                              ELEMENT_RESOLUTION_FAILED))));
+  Run();
+}
+
+TEST_F(WaitForDomActionTest, NoMatchOneElementFound) {
+  EXPECT_CALL(mock_web_controller_, OnElementCheck(Selector({"#element"}), _))
+      .WillRepeatedly(RunOnceCallback<1>(true));
+
+  proto_.mutable_wait_while()->add_selectors("#element");
+  EXPECT_CALL(callback_, Run(Pointee(Property(&ProcessedActionProto::status,
+                                              ELEMENT_RESOLUTION_FAILED))));
+  Run();
+}
+
+TEST_F(WaitForDomActionTest, NoMatchOneElementNotFound) {
+  proto_.mutable_wait_while()->add_selectors("#element");
+  EXPECT_CALL(
+      callback_,
+      Run(Pointee(Property(&ProcessedActionProto::status, ACTION_APPLIED))));
+  Run();
+}
+
+TEST_F(WaitForDomActionTest, AllConditionsMetWantAll) {
+  EXPECT_CALL(mock_web_controller_, OnElementCheck(Selector({"#element1"}), _))
+      .WillRepeatedly(RunOnceCallback<1>(true));
+  EXPECT_CALL(mock_web_controller_, OnElementCheck(Selector({"#element2"}), _))
+      .WillRepeatedly(RunOnceCallback<1>(false));
+  proto_.mutable_wait_for_all()
+      ->add_conditions()
+      ->mutable_must_match()
+      ->add_selectors("#element1");
+  proto_.mutable_wait_for_all()
+      ->add_conditions()
+      ->mutable_must_not_match()
+      ->add_selectors("#element2");
+  EXPECT_CALL(
+      callback_,
+      Run(Pointee(Property(&ProcessedActionProto::status, ACTION_APPLIED))));
+  Run();
+}
+
+TEST_F(WaitForDomActionTest, AllConditionsMetWantSome) {
+  EXPECT_CALL(mock_web_controller_, OnElementCheck(Selector({"#element1"}), _))
+      .WillRepeatedly(RunOnceCallback<1>(true));
+  EXPECT_CALL(mock_web_controller_, OnElementCheck(Selector({"#element2"}), _))
+      .WillRepeatedly(RunOnceCallback<1>(false));
+  proto_.mutable_wait_for_any()
+      ->add_conditions()
+      ->mutable_must_match()
+      ->add_selectors("#element1");
+  proto_.mutable_wait_for_any()
+      ->add_conditions()
+      ->mutable_must_not_match()
+      ->add_selectors("#element2");
+  EXPECT_CALL(
+      callback_,
+      Run(Pointee(Property(&ProcessedActionProto::status, ACTION_APPLIED))));
+  Run();
+}
+
+TEST_F(WaitForDomActionTest, NoConditionsMetWantAll) {
+  proto_.mutable_wait_for_all()
+      ->add_conditions()
+      ->mutable_must_match()
+      ->add_selectors("#element1");
+  proto_.mutable_wait_for_all()
+      ->add_conditions()
+      ->mutable_must_match()
+      ->add_selectors("#element2");
+  EXPECT_CALL(callback_, Run(Pointee(Property(&ProcessedActionProto::status,
+                                              ELEMENT_RESOLUTION_FAILED))));
+  Run();
+}
+
+TEST_F(WaitForDomActionTest, NoConditionsMetWantSome) {
+  proto_.mutable_wait_for_any()
+      ->add_conditions()
+      ->mutable_must_match()
+      ->add_selectors("#element1");
+  proto_.mutable_wait_for_any()
+      ->add_conditions()
+      ->mutable_must_match()
+      ->add_selectors("#element2");
+  EXPECT_CALL(callback_, Run(Pointee(Property(&ProcessedActionProto::status,
+                                              ELEMENT_RESOLUTION_FAILED))));
+  Run();
+}
+
+TEST_F(WaitForDomActionTest, OnConditionMetWantAll) {
+  // element1 should be there, but isn't, so this doesn't satisfy wait_for_all
+  proto_.mutable_wait_for_all()
+      ->add_conditions()
+      ->mutable_must_match()
+      ->add_selectors("#element1");
+  proto_.mutable_wait_for_all()
+      ->add_conditions()
+      ->mutable_must_not_match()
+      ->add_selectors("#element2");
+  EXPECT_CALL(callback_, Run(Pointee(Property(&ProcessedActionProto::status,
+                                              ELEMENT_RESOLUTION_FAILED))));
+  Run();
+}
+
+TEST_F(WaitForDomActionTest, OnConditionMetWantSome) {
+  // element1 should be there, but isn't. element2 is not there, as expected.
+  // This satisfies wait_for_one.
+  proto_.mutable_wait_for_any()
+      ->add_conditions()
+      ->mutable_must_match()
+      ->add_selectors("#element1");
+  proto_.mutable_wait_for_any()
+      ->add_conditions()
+      ->mutable_must_not_match()
+      ->add_selectors("#element2");
+  EXPECT_CALL(
+      callback_,
+      Run(Pointee(Property(&ProcessedActionProto::status, ACTION_APPLIED))));
+  Run();
+}
+
+TEST_F(WaitForDomActionTest, ReportMatchesToServer) {
+  EXPECT_CALL(mock_web_controller_, OnElementCheck(Selector({"#element1"}), _))
+      .WillRepeatedly(RunOnceCallback<1>(true));
+  EXPECT_CALL(mock_web_controller_, OnElementCheck(Selector({"#element2"}), _))
+      .WillRepeatedly(RunOnceCallback<1>(false));
+  EXPECT_CALL(mock_web_controller_, OnElementCheck(Selector({"#element3"}), _))
+      .WillRepeatedly(RunOnceCallback<1>(false));
+  EXPECT_CALL(mock_web_controller_, OnElementCheck(Selector({"#element4"}), _))
+      .WillRepeatedly(RunOnceCallback<1>(true));
+
+  auto* condition1 = proto_.mutable_wait_for_any()->add_conditions();
+  condition1->mutable_must_match()->add_selectors("#element1");
+  condition1->set_server_payload("1");
+
+  auto* condition2 = proto_.mutable_wait_for_any()->add_conditions();
+  condition2->mutable_must_not_match()->add_selectors("#element2");
+  condition2->set_server_payload("2");
+
+  auto* condition3 = proto_.mutable_wait_for_any()->add_conditions();
+  condition3->mutable_must_match()->add_selectors("#element3");
+  condition3->set_server_payload("3");
+
+  auto* condition4 = proto_.mutable_wait_for_any()->add_conditions();
+  condition4->mutable_must_not_match()->add_selectors("#element4");
+  condition4->set_server_payload("4");
+
+  // Condition 1 and 2 are met, conditions 3 and 4 are not.
+
+  ProcessedActionProto capture;
+  EXPECT_CALL(callback_, Run(_)).WillOnce(testing::SaveArgPointee<0>(&capture));
+  Run();
+
+  EXPECT_THAT(capture.wait_for_dom_result().matching_condition_payloads(),
+              ElementsAre("1", "2"));
+}
+
+}  // namespace
+}  // namespace autofill_assistant
diff --git a/components/autofill_assistant/browser/batch_element_checker.cc b/components/autofill_assistant/browser/batch_element_checker.cc
index 741ee50..ef03a1c 100644
--- a/components/autofill_assistant/browser/batch_element_checker.cc
+++ b/components/autofill_assistant/browser/batch_element_checker.cc
@@ -36,13 +36,16 @@
   return element_check_callbacks_.empty() && get_field_value_callbacks_.empty();
 }
 
-void BatchElementChecker::Run(WebController* web_controller,
-                              base::OnceCallback<void()> all_done) {
+void BatchElementChecker::AddAllDoneCallback(
+    base::OnceCallback<void()> all_done) {
+  all_done_.emplace_back(std::move(all_done));
+}
+
+void BatchElementChecker::Run(WebController* web_controller) {
   DCHECK(web_controller);
   DCHECK(!started_);
   started_ = true;
 
-  all_done_ = std::move(all_done);
   pending_checks_count_ =
       element_check_callbacks_.size() + get_field_value_callbacks_.size() + 1;
 
@@ -104,10 +107,12 @@
   pending_checks_count_--;
   DCHECK_GE(pending_checks_count_, 0);
   if (pending_checks_count_ <= 0) {
-    DCHECK(all_done_);
-    std::move(all_done_).Run();
-    // Don't do anything after calling all_done, since this could have been
-    // deleted.
+    std::vector<base::OnceCallback<void()>> all_done = std::move(all_done_);
+    // Callbacks in all_done_ can delete the current instance. Nothing can
+    // safely access |this| after this point.
+    for (auto& callback : all_done) {
+      std::move(callback).Run();
+    }
   }
 }
 
diff --git a/components/autofill_assistant/browser/batch_element_checker.h b/components/autofill_assistant/browser/batch_element_checker.h
index ff41fab..dabe7ee 100644
--- a/components/autofill_assistant/browser/batch_element_checker.h
+++ b/components/autofill_assistant/browser/batch_element_checker.h
@@ -52,11 +52,19 @@
   void AddFieldValueCheck(const Selector& selector,
                           GetFieldValueCallback callback);
 
+  // A callback to call once all the elements have been checked. These callbacks
+  // are guaranteed to be called in order, finishing with the callback passed to
+  // Run().
+  //
+  // These callback are allowed to delete the current instance.
+  void AddAllDoneCallback(base::OnceCallback<void()> all_done);
+
   // Returns true if all there are no checks to run.
   bool empty() const;
 
-  // Runs the checks. Call |all_done| once all the results have been reported.
-  void Run(WebController* web_controller, base::OnceCallback<void()> all_done);
+  // Runs the checks. Once all checks are done, calls the callbacks registered
+  // to AddAllDoneCallback().
+  void Run(WebController* web_controller);
 
  private:
   void OnElementChecked(std::vector<ElementCheckCallback>* callbacks,
@@ -80,7 +88,7 @@
   // Run() was called. Checking elements might or might not have finished yet.
   bool started_ = false;
 
-  base::OnceCallback<void()> all_done_;
+  std::vector<base::OnceCallback<void()>> all_done_;
 
   base::WeakPtrFactory<BatchElementChecker> weak_ptr_factory_;
 
diff --git a/components/autofill_assistant/browser/batch_element_checker_unittest.cc b/components/autofill_assistant/browser/batch_element_checker_unittest.cc
index dd9e7a2..4fb0a40 100644
--- a/components/autofill_assistant/browser/batch_element_checker_unittest.cc
+++ b/components/autofill_assistant/browser/batch_element_checker_unittest.cc
@@ -17,6 +17,7 @@
 
 using ::testing::_;
 using ::testing::Contains;
+using ::testing::ElementsAre;
 using ::testing::Eq;
 using ::testing::InSequence;
 using ::testing::Key;
@@ -31,6 +32,13 @@
  protected:
   BatchElementCheckerTest() : checks_() {}
 
+  void SetUp() override {
+    ON_CALL(mock_web_controller_, OnElementCheck(_, _))
+        .WillByDefault(RunOnceCallback<1>(false));
+    ON_CALL(mock_web_controller_, OnGetFieldValue(_, _))
+        .WillByDefault(RunOnceCallback<1>(false, ""));
+  }
+
   void OnElementExistenceCheck(const std::string& name, bool result) {
     element_exists_results_[name] = result;
   }
@@ -72,7 +80,8 @@
   }
 
   void Run(const std::string& callback_name) {
-    checks_.Run(&mock_web_controller_, DoneCallback(callback_name));
+    checks_.AddAllDoneCallback(DoneCallback(callback_name));
+    checks_.Run(&mock_web_controller_);
   }
 
   MockWebController mock_web_controller_;
@@ -88,6 +97,8 @@
   checks_.AddElementCheck(Selector({"exists"}),
                           ElementExistenceCallback("exists"));
   EXPECT_FALSE(checks_.empty());
+  Run("all_done");
+  EXPECT_THAT(all_done_, Contains("all_done"));
 }
 
 TEST_F(BatchElementCheckerTest, OneElementFound) {
@@ -212,6 +223,16 @@
   EXPECT_THAT(all_done_, Contains("was_run"));
 }
 
+TEST_F(BatchElementCheckerTest, CallMultipleAllDoneCallbacks) {
+  checks_.AddElementCheck(Selector({"exists"}),
+                          ElementExistenceCallback("exists"));
+  checks_.AddAllDoneCallback(DoneCallback("1"));
+  checks_.AddAllDoneCallback(DoneCallback("2"));
+  checks_.AddAllDoneCallback(DoneCallback("3"));
+  checks_.Run(&mock_web_controller_);
+  EXPECT_THAT(all_done_, ElementsAre("1", "2", "3"));
+}
+
 // Deduplicate get field
 
 }  // namespace
diff --git a/components/autofill_assistant/browser/element_precondition_unittest.cc b/components/autofill_assistant/browser/element_precondition_unittest.cc
index 79dcff9..40ab14a0 100644
--- a/components/autofill_assistant/browser/element_precondition_unittest.cc
+++ b/components/autofill_assistant/browser/element_precondition_unittest.cc
@@ -49,7 +49,7 @@
     ElementPrecondition precondition(exist_, value_match_);
     BatchElementChecker batch_checks;
     precondition.Check(&batch_checks, std::move(callback));
-    batch_checks.Run(&mock_web_controller_, base::DoNothing());
+    batch_checks.Run(&mock_web_controller_);
   }
 
   MockWebController mock_web_controller_;
diff --git a/components/autofill_assistant/browser/script_executor.cc b/components/autofill_assistant/browser/script_executor.cc
index dc25b8c..43a448c 100644
--- a/components/autofill_assistant/browser/script_executor.cc
+++ b/components/autofill_assistant/browser/script_executor.cc
@@ -157,17 +157,18 @@
   }
 }
 
-void ScriptExecutor::RunElementChecks(BatchElementChecker* checker,
-                                      base::OnceCallback<void()> all_done) {
-  return checker->Run(delegate_->GetWebController(), std::move(all_done));
+void ScriptExecutor::RunElementChecks(BatchElementChecker* checker) {
+  return checker->Run(delegate_->GetWebController());
 }
 
 void ScriptExecutor::ShortWaitForElement(
     const Selector& selector,
     base::OnceCallback<void(bool)> callback) {
   wait_for_dom_ = std::make_unique<WaitForDomOperation>(
-      this, kShortWaitForElementDeadline, /* allow_interrupt= */ false,
-      SelectorPredicate::kMatches, selector,
+      this, delegate_, kShortWaitForElementDeadline,
+      /* allow_interrupt= */ false,
+      base::BindRepeating(&ScriptExecutor::CheckElementMatches,
+                          weak_ptr_factory_.GetWeakPtr(), selector),
       base::BindOnce(&ScriptExecutor::OnShortWaitForElement,
                      weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
   wait_for_dom_->Run();
@@ -176,11 +177,12 @@
 void ScriptExecutor::WaitForDom(
     base::TimeDelta max_wait_time,
     bool allow_interrupt,
-    ActionDelegate::SelectorPredicate selector_predicate,
-    const Selector& selector,
+    base::RepeatingCallback<void(BatchElementChecker*,
+                                 base::OnceCallback<void(bool)>)>
+        check_elements,
     base::OnceCallback<void(ProcessedActionStatusProto)> callback) {
   wait_for_dom_ = std::make_unique<WaitForDomOperation>(
-      this, max_wait_time, allow_interrupt, selector_predicate, selector,
+      this, delegate_, max_wait_time, allow_interrupt, check_elements,
       base::BindOnce(&ScriptExecutor::OnWaitForElementVisibleWithInterrupts,
                      weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
   wait_for_dom_->Run();
@@ -621,6 +623,13 @@
   ProcessNextAction();
 }
 
+void ScriptExecutor::CheckElementMatches(
+    const Selector& selector,
+    BatchElementChecker* checker,
+    base::OnceCallback<void(bool)> callback) {
+  checker->AddElementCheck(selector, std::move(callback));
+}
+
 void ScriptExecutor::OnShortWaitForElement(
     base::OnceCallback<void(bool)> callback,
     bool element_found,
@@ -657,27 +666,29 @@
 
 ScriptExecutor::WaitForDomOperation::WaitForDomOperation(
     ScriptExecutor* main_script,
+    ScriptExecutorDelegate* delegate,
     base::TimeDelta max_wait_time,
     bool allow_interrupt,
-    ActionDelegate::SelectorPredicate selector_predicate,
-    const Selector& selector,
+    base::RepeatingCallback<void(BatchElementChecker*,
+                                 base::OnceCallback<void(bool)>)>
+        check_elements,
     WaitForDomOperation::Callback callback)
     : main_script_(main_script),
+      delegate_(delegate),
       max_wait_time_(max_wait_time),
       allow_interrupt_(allow_interrupt),
-      selector_predicate_(selector_predicate),
-      selector_(selector),
+      check_elements_(std::move(check_elements)),
       callback_(std::move(callback)),
       retry_timer_(kPeriodicElementCheck),
       weak_ptr_factory_(this) {}
 
 ScriptExecutor::WaitForDomOperation::~WaitForDomOperation() {
-  main_script_->delegate_->RemoveListener(this);
+  delegate_->RemoveListener(this);
 }
 
 void ScriptExecutor::WaitForDomOperation::Run() {
-  main_script_->delegate_->AddListener(this);
-  if (main_script_->delegate_->IsNavigatingToNewDocument())
+  delegate_->AddListener(this);
+  if (delegate_->IsNavigatingToNewDocument())
     return;  // start paused
 
   Start();
@@ -710,7 +721,7 @@
 }
 
 void ScriptExecutor::WaitForDomOperation::OnNavigationStateChanged() {
-  if (main_script_->delegate_->IsNavigatingToNewDocument()) {
+  if (delegate_->IsNavigatingToNewDocument()) {
     Pause();
   } else {
     Continue();
@@ -736,9 +747,9 @@
   element_check_result_ = false;
   runnable_interrupts_.clear();
   batch_element_checker_ = std::make_unique<BatchElementChecker>();
-  batch_element_checker_->AddElementCheck(
-      selector_, base::BindOnce(&WaitForDomOperation::OnElementCheckDone,
-                                base::Unretained(this)));
+  check_elements_.Run(batch_element_checker_.get(),
+                      base::BindOnce(&WaitForDomOperation::OnElementCheckDone,
+                                     base::Unretained(this)));
   if (allow_interrupt_) {
     for (const auto* interrupt : *main_script_->ordered_interrupts_) {
       if (ran_interrupts_.find(interrupt->handle.path) !=
@@ -748,9 +759,8 @@
       }
 
       interrupt->precondition->Check(
-          main_script_->delegate_->GetCurrentURL(),
-          batch_element_checker_.get(),
-          main_script_->delegate_->GetTriggerContext()->script_parameters,
+          delegate_->GetCurrentURL(), batch_element_checker_.get(),
+          delegate_->GetTriggerContext()->script_parameters,
           *main_script_->scripts_state_,
           base::BindOnce(&WaitForDomOperation::OnPreconditionCheckDone,
                          weak_ptr_factory_.GetWeakPtr(),
@@ -758,10 +768,10 @@
     }
   }
 
-  batch_element_checker_->Run(
-      main_script_->delegate_->GetWebController(),
+  batch_element_checker_->AddAllDoneCallback(
       base::BindOnce(&WaitForDomOperation::OnAllChecksDone,
                      base::Unretained(this), std::move(report_attempt_result)));
+  batch_element_checker_->Run(delegate_->GetWebController());
 }
 
 void ScriptExecutor::WaitForDomOperation::OnPreconditionCheckDone(
@@ -771,19 +781,8 @@
     runnable_interrupts_.insert(interrupt);
 }
 
-void ScriptExecutor::WaitForDomOperation::OnElementCheckDone(bool found) {
-  switch (selector_predicate_) {
-    case ActionDelegate::SelectorPredicate::kMatches:
-      element_check_result_ = found;
-      break;
-
-    case ActionDelegate::SelectorPredicate::kDoesntMatch:
-      element_check_result_ = !found;
-      break;
-
-      // Default intentionally left unset to cause a compilation error if a new
-      // value is added.
-  }
+void ScriptExecutor::WaitForDomOperation::OnElementCheckDone(bool result) {
+  element_check_result_ = result;
 
   // Wait for all checks to run before reporting that the element was found to
   // the caller, so interrupts have a chance to run.
@@ -813,7 +812,7 @@
       interrupt->handle.path, main_script_->last_global_payload_,
       main_script_->initial_script_payload_,
       /* listener= */ this, main_script_->scripts_state_, &no_interrupts_,
-      main_script_->delegate_);
+      delegate_);
   interrupt_executor_->Run(
       base::BindOnce(&ScriptExecutor::WaitForDomOperation::OnInterruptDone,
                      base::Unretained(this)));
@@ -848,7 +847,7 @@
   if (!callback_)
     return;
 
-  RestorePreInterruptScroll(check_result);
+  RestorePreInterruptScroll();
   std::move(callback_).Run(check_result, result, ran_interrupts_);
 }
 
@@ -856,7 +855,7 @@
   if (saved_pre_interrupt_state_)
     return;
 
-  pre_interrupt_status_ = main_script_->delegate_->GetStatusMessage();
+  pre_interrupt_status_ = delegate_->GetStatusMessage();
   saved_pre_interrupt_state_ = true;
 }
 
@@ -864,20 +863,15 @@
   if (!saved_pre_interrupt_state_)
     return;
 
-  main_script_->delegate_->SetStatusMessage(pre_interrupt_status_);
+  delegate_->SetStatusMessage(pre_interrupt_status_);
 }
 
-void ScriptExecutor::WaitForDomOperation::RestorePreInterruptScroll(
-    bool check_result) {
+void ScriptExecutor::WaitForDomOperation::RestorePreInterruptScroll() {
   if (!saved_pre_interrupt_state_)
     return;
 
-  auto* delegate = main_script_->delegate_;
-  if (check_result &&
-      selector_predicate_ == ActionDelegate::SelectorPredicate::kMatches) {
-    delegate->GetWebController()->FocusElement(selector_, base::DoNothing());
-  } else if (!main_script_->last_focused_element_selector_.empty()) {
-    delegate->GetWebController()->FocusElement(
+  if (!main_script_->last_focused_element_selector_.empty()) {
+    delegate_->GetWebController()->FocusElement(
         main_script_->last_focused_element_selector_, base::DoNothing());
   }
 }
diff --git a/components/autofill_assistant/browser/script_executor.h b/components/autofill_assistant/browser/script_executor.h
index c16bfcc..68c74b5 100644
--- a/components/autofill_assistant/browser/script_executor.h
+++ b/components/autofill_assistant/browser/script_executor.h
@@ -96,15 +96,15 @@
   void OnNavigationStateChanged() override;
 
   // Override ActionDelegate:
-  void RunElementChecks(BatchElementChecker* checker,
-                        base::OnceCallback<void()> all_done) override;
+  void RunElementChecks(BatchElementChecker* checker) override;
   void ShortWaitForElement(const Selector& selector,
                            base::OnceCallback<void(bool)> callback) override;
   void WaitForDom(
       base::TimeDelta max_wait_time,
       bool allow_interrupt,
-      ActionDelegate::SelectorPredicate selector_predicate,
-      const Selector& selector,
+      base::RepeatingCallback<void(BatchElementChecker*,
+                                   base::OnceCallback<void(bool)>)>
+          check_elements,
       base::OnceCallback<void(ProcessedActionStatusProto)> callback) override;
   void SetStatusMessage(const std::string& message) override;
   std::string GetStatusMessage() override;
@@ -200,12 +200,15 @@
                                              const std::set<std::string>&)>;
 
     // |main_script_| must not be null and outlive this instance.
-    WaitForDomOperation(ScriptExecutor* main_script,
-                        base::TimeDelta max_wait_time,
-                        bool allow_interrupt,
-                        ActionDelegate::SelectorPredicate selector_predicate,
-                        const Selector& selectors,
-                        WaitForDomOperation::Callback callback);
+    WaitForDomOperation(
+        ScriptExecutor* main_script,
+        ScriptExecutorDelegate* delegate,
+        base::TimeDelta max_wait_time,
+        bool allow_interrupt,
+        base::RepeatingCallback<void(BatchElementChecker*,
+                                     base::OnceCallback<void(bool)>)>
+            check_elements,
+        WaitForDomOperation::Callback callback);
     ~WaitForDomOperation() override;
 
     void Run();
@@ -244,13 +247,15 @@
 
     // if save_pre_interrupt_state_ is set, attempt to scroll the page back to
     // the original area.
-    void RestorePreInterruptScroll(bool element_found);
+    void RestorePreInterruptScroll();
 
     ScriptExecutor* main_script_;
+    ScriptExecutorDelegate* delegate_;
     const base::TimeDelta max_wait_time_;
     const bool allow_interrupt_;
-    const ActionDelegate::SelectorPredicate selector_predicate_;
-    const Selector selector_;
+    base::RepeatingCallback<void(BatchElementChecker*,
+                                 base::OnceCallback<void(bool)>)>
+        check_elements_;
     WaitForDomOperation::Callback callback_;
 
     std::unique_ptr<BatchElementChecker> batch_element_checker_;
@@ -292,6 +297,9 @@
   void ProcessAction(Action* action);
   void GetNextActions();
   void OnProcessedAction(std::unique_ptr<ProcessedActionProto> action);
+  void CheckElementMatches(const Selector& selector,
+                           BatchElementChecker* checker,
+                           base::OnceCallback<void(bool)> callback);
   void OnShortWaitForElement(base::OnceCallback<void(bool)> callback,
                              bool element_found,
                              const Result* interrupt_result,
diff --git a/components/autofill_assistant/browser/script_precondition_unittest.cc b/components/autofill_assistant/browser/script_precondition_unittest.cc
index 4802b9e..bd5ac58 100644
--- a/components/autofill_assistant/browser/script_precondition_unittest.cc
+++ b/components/autofill_assistant/browser/script_precondition_unittest.cc
@@ -77,7 +77,7 @@
     BatchElementChecker batch_checks;
     precondition->Check(url_, &batch_checks, parameters_, executed_scripts_,
                         callback.Get());
-    batch_checks.Run(&mock_web_controller_, /* all_done=*/base::DoNothing());
+    batch_checks.Run(&mock_web_controller_);
     return callback.GetResultOrDie();
   }
 
diff --git a/components/autofill_assistant/browser/script_tracker.cc b/components/autofill_assistant/browser/script_tracker.cc
index 1b49b07..c3c49ab 100644
--- a/components/autofill_assistant/browser/script_tracker.cc
+++ b/components/autofill_assistant/browser/script_tracker.cc
@@ -99,9 +99,9 @@
     TerminatePendingChecks();
     return;
   }
-  batch_element_checker_->Run(delegate_->GetWebController(),
-                              base::BindOnce(&ScriptTracker::OnCheckDone,
-                                             weak_ptr_factory_.GetWeakPtr()));
+  batch_element_checker_->AddAllDoneCallback(base::BindOnce(
+      &ScriptTracker::OnCheckDone, weak_ptr_factory_.GetWeakPtr()));
+  batch_element_checker_->Run(delegate_->GetWebController());
 }
 
 void ScriptTracker::ExecuteScript(const std::string& script_path,
diff --git a/components/autofill_assistant/browser/service.proto b/components/autofill_assistant/browser/service.proto
index 03b94c9..525a1f4 100644
--- a/components/autofill_assistant/browser/service.proto
+++ b/components/autofill_assistant/browser/service.proto
@@ -375,6 +375,8 @@
     PaymentDetails payment_details = 15;
     // Should be set as a result of SetFormFieldValueAction.
     SetFormFieldValueProto.Result set_form_field_value_result = 17;
+    // May be set as a result of WaitForDomProto.
+    WaitForDomProto.Result wait_for_dom_result = 22;
     // Should be set as a result of FormAction.
     FormProto.Result form_result = 21;
   }
@@ -741,6 +743,18 @@
     // Wait as long as there's at least one element that matches the given
     // reference.
     ElementReferenceProto wait_while = 6;
+
+    // Wait until at least one condition is met.
+    //
+    // If, at the end of the timeout, no conditions are not met, the action
+    // returns ELEMENT_RESOLUTION_FAILED.
+    ElementConditions wait_for_any = 7;
+
+    // Wait until all conditions are met.
+    //
+    // If, at the end of the timeout, not all condition are met, the action
+    // returns ELEMENT_RESOLUTION_FAILED.
+    ElementConditions wait_for_all = 8;
   }
 
   // If true, run scripts flagged with 'interrupt=true' as soon as their
@@ -748,6 +762,30 @@
   optional bool allow_interrupt = 3;
 
   reserved 4;
+
+  // Multiple element conditions.
+  message ElementConditions { repeated ElementCondition conditions = 1; }
+
+  // A single element condition.
+  message ElementCondition {
+    oneof condition {
+      // There must be at least one matching element.
+      ElementReferenceProto must_match = 1;
+
+      // There must not be any matching elements.
+      ElementReferenceProto must_not_match = 2;
+    }
+
+    // If this condition is met and this is nonempty, put the following payload
+    // into the result.
+    optional bytes server_payload = 5;
+  }
+
+  // Result to include into ProcessedActionProto.
+  message Result {
+    // Payload of all matching conditions, if one is set.
+    repeated bytes matching_condition_payloads = 1;
+  }
 }
 
 // Volatile upload of a portion of the dom for backend analysis, does not store
diff --git a/components/components_strings.grd b/components/components_strings.grd
index 3b165b2..ed2cf8e 100644
--- a/components/components_strings.grd
+++ b/components/components_strings.grd
@@ -351,6 +351,9 @@
       <message name="IDS_ACCNAME_LOCATION" desc="The accessible name for the editable-text portion of the omnibox.">
         Address and search bar
       </message>
+      <message name="IDS_ACCNAME_PARTICLE_DISC" desc="The accessible name for the Account Consistency Disc on New Tab Page">
+        Google Account
+      </message>
 
       <message name="IDS_UTILITY_PROCESS_JSON_PARSER_NAME" desc="The name of the utility process used for parsing JSON files.">
         JSON Parser
diff --git a/components/exo/surface_tree_host.cc b/components/exo/surface_tree_host.cc
index dc973767..a9e3a27 100644
--- a/components/exo/surface_tree_host.cc
+++ b/components/exo/surface_tree_host.cc
@@ -110,8 +110,7 @@
   // This method applies multiple changes to the window tree. Use ScopedPause to
   // ensure that occlusion isn't recomputed before all changes have been
   // applied.
-  aura::WindowOcclusionTracker::ScopedPause pause_occlusion(
-      aura::Env::GetInstance());
+  aura::WindowOcclusionTracker::ScopedPause pause_occlusion;
 
   if (root_surface_) {
     root_surface_->window()->Hide();
@@ -276,8 +275,7 @@
   // This method applies multiple changes to the window tree. Use ScopedPause
   // to ensure that occlusion isn't recomputed before all changes have been
   // applied.
-  aura::WindowOcclusionTracker::ScopedPause pause_occlusion(
-      aura::Env::GetInstance());
+  aura::WindowOcclusionTracker::ScopedPause pause_occlusion;
 
   gfx::Rect bounds = root_surface_->surface_hierarchy_content_bounds();
   host_window_->SetBounds(
diff --git a/components/feedback/OWNERS b/components/feedback/OWNERS
index 8b11b53..b7d64ae 100644
--- a/components/feedback/OWNERS
+++ b/components/feedback/OWNERS
@@ -1,6 +1,5 @@
 afakhry@chromium.org
 bsimonnet@chromium.org
-rkc@chromium.org
 zork@chromium.org
 
 per-file anonymizer_tool*=battre@chromium.org
diff --git a/components/invalidation/impl/invalidation_switches.cc b/components/invalidation/impl/invalidation_switches.cc
index 588d2f2..036a094 100644
--- a/components/invalidation/impl/invalidation_switches.cc
+++ b/components/invalidation/impl/invalidation_switches.cc
@@ -39,7 +39,7 @@
 // should be removed.
 const base::Feature kTiclInvalidationsStartInvalidatorOnActiveHandler = {
     "TiclInvalidationsStartInvalidatorOnActiveHandler",
-    base::FEATURE_ENABLED_BY_DEFAULT};
+    base::FEATURE_DISABLED_BY_DEFAULT};
 
 }  // namespace switches
 }  // namespace invalidation
diff --git a/components/minidump_uploader/android/java/src/org/chromium/components/minidump_uploader/MinidumpUploadCallable.java b/components/minidump_uploader/android/java/src/org/chromium/components/minidump_uploader/MinidumpUploadCallable.java
index f7038cb..2b43ebf 100644
--- a/components/minidump_uploader/android/java/src/org/chromium/components/minidump_uploader/MinidumpUploadCallable.java
+++ b/components/minidump_uploader/android/java/src/org/chromium/components/minidump_uploader/MinidumpUploadCallable.java
@@ -8,7 +8,6 @@
 import android.support.annotation.IntDef;
 
 import org.chromium.base.ContextUtils;
-import org.chromium.base.FileUtils;
 import org.chromium.base.Log;
 import org.chromium.base.StreamUtil;
 import org.chromium.base.VisibleForTesting;
@@ -17,11 +16,14 @@
 import org.chromium.components.minidump_uploader.util.HttpURLConnectionFactoryImpl;
 
 import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileReader;
 import java.io.FileWriter;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.net.HttpURLConnection;
@@ -117,8 +119,7 @@
                 return MinidumpUploadStatus.FAILURE;
             }
             minidumpInputStream = new FileInputStream(mFileToUpload);
-            FileUtils.copyStream(
-                    minidumpInputStream, new GZIPOutputStream(connection.getOutputStream()));
+            streamCopy(minidumpInputStream, new GZIPOutputStream(connection.getOutputStream()));
             boolean success = handleExecutionResponse(connection);
 
             return success ? MinidumpUploadStatus.SUCCESS : MinidumpUploadStatus.FAILURE;
@@ -126,11 +127,14 @@
             // ArrayIndexOutOfBoundsException due to bad GZIPOutputStream implementation on some
             // old sony devices.
             // For now just log the stack trace.
-            Log.w(TAG, "Error while uploading %s", mFileToUpload.getName(), e);
+            Log.w(TAG, "Error while uploading " + mFileToUpload.getName(), e);
             return MinidumpUploadStatus.FAILURE;
         } finally {
             connection.disconnect();
-            StreamUtil.closeQuietly(minidumpInputStream);
+
+            if (minidumpInputStream != null) {
+                StreamUtil.closeQuietly(minidumpInputStream);
+            }
         }
     }
 
@@ -170,9 +174,9 @@
         if (isSuccessful(responseCode)) {
             String responseContent = getResponseContentAsString(connection);
             // The crash server returns the crash ID.
-            String uploadId = responseContent.isEmpty() ? "unknown" : responseContent;
+            String uploadId = responseContent != null ? responseContent : "unknown";
             String crashFileName = mFileToUpload.getName();
-            Log.i(TAG, "Minidump %s uploaded successfully, id: %s", crashFileName, uploadId);
+            Log.i(TAG, "Minidump " + crashFileName + " uploaded successfully, id: " + uploadId);
 
             // TODO(acleung): MinidumpUploadService is in charge of renaming while this class is
             // in charge of deleting. We should move all the file system operations into
@@ -280,8 +284,33 @@
      */
     private static String getResponseContentAsString(HttpURLConnection connection)
             throws IOException {
-        byte[] bytes = FileUtils.readStream(connection.getInputStream());
-        return new String(bytes);
+        String responseContent = null;
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        streamCopy(connection.getInputStream(), baos);
+        if (baos.size() > 0) {
+            responseContent = baos.toString();
+        }
+        return responseContent;
+    }
+
+    /**
+     * Copies all available data from |inStream| to |outStream|. Closes both
+     * streams when done.
+     *
+     * @param inStream the stream to read
+     * @param outStream the stream to write to
+     * @throws IOException
+     */
+    private static void streamCopy(InputStream inStream, OutputStream outStream)
+            throws IOException {
+        byte[] temp = new byte[4096];
+        int bytesRead = inStream.read(temp);
+        while (bytesRead >= 0) {
+            outStream.write(temp, 0, bytesRead);
+            bytesRead = inStream.read(temp);
+        }
+        inStream.close();
+        outStream.close();
     }
 
     // TODO(gayane): Remove this function and unused prefs in M51. crbug.com/555022
diff --git a/components/nacl/browser/nacl_process_host.cc b/components/nacl/browser/nacl_process_host.cc
index eadd21e..565dfc2 100644
--- a/components/nacl/browser/nacl_process_host.cc
+++ b/components/nacl/browser/nacl_process_host.cc
@@ -409,6 +409,10 @@
     }
   }
 
+  // Create a shared memory region that the renderer and plugin share for
+  // reporting crash information.
+  crash_info_shmem_.CreateAnonymous(kNaClCrashInfoShmemSize);
+
   // Launch the process
   if (!LaunchSelLdr()) {
     delete this;
@@ -625,19 +629,20 @@
 void NaClProcessHost::ReplyToRenderer(
     mojo::ScopedMessagePipeHandle ppapi_channel_handle,
     mojo::ScopedMessagePipeHandle trusted_channel_handle,
-    mojo::ScopedMessagePipeHandle manifest_service_channel_handle,
-    base::ReadOnlySharedMemoryRegion crash_info_shmem_region) {
+    mojo::ScopedMessagePipeHandle manifest_service_channel_handle) {
   // Hereafter, we always send an IPC message with handles created above
   // which, on Windows, are not closable in this process.
   std::string error_message;
-  if (!uses_nonsfi_mode_ && !crash_info_shmem_region.IsValid()) {
+  base::SharedMemoryHandle crash_info_shmem_renderer_handle =
+      crash_info_shmem_.handle().Duplicate();
+  if (!crash_info_shmem_renderer_handle.IsValid()) {
     // On error, we do not send "IPC::ChannelHandle"s to the renderer process.
     // Note that some other FDs/handles still get sent to the renderer, but
     // will be closed there.
     ppapi_channel_handle.reset();
     trusted_channel_handle.reset();
     manifest_service_channel_handle.reset();
-    error_message = "shared memory region not valid";
+    error_message = "handle duplication failed";
   }
 
   const ChildProcessData& data = process_->GetData();
@@ -645,8 +650,12 @@
       NaClLaunchResult(
           ppapi_channel_handle.release(), trusted_channel_handle.release(),
           manifest_service_channel_handle.release(), data.GetProcess().Pid(),
-          data.id, std::move(crash_info_shmem_region)),
+          data.id, crash_info_shmem_renderer_handle),
       error_message);
+
+  // Now that the crash information shmem handles have been shared with the
+  // plugin and the renderer, the browser can close its handle.
+  crash_info_shmem_.Close();
 }
 
 void NaClProcessHost::SendErrorToRenderer(const std::string& error_message) {
@@ -788,12 +797,9 @@
 #endif
   }
 
-  // Create a shared memory region that the renderer and the plugin share to
-  // report crash information.
-  params.crash_info_shmem_region =
-      base::WritableSharedMemoryRegion::Create(kNaClCrashInfoShmemSize);
-  if (!params.crash_info_shmem_region.IsValid()) {
-    DLOG(ERROR) << "Failed to create a shared memory buffer";
+  params.crash_info_shmem_handle = crash_info_shmem_.handle().Duplicate();
+  if (!params.crash_info_shmem_handle.IsValid()) {
+    DLOG(ERROR) << "Failed to duplicate a shared memory buffer";
     return false;
   }
 
@@ -826,16 +832,14 @@
       // into the plugin process.
       base::PostTaskWithTraitsAndReplyWithResult(
           FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
-          base::BindOnce(OpenNaClReadExecImpl, file_path,
-                         true /* is_executable */),
-          base::BindOnce(&NaClProcessHost::StartNaClFileResolved,
-                         weak_factory_.GetWeakPtr(), std::move(params),
-                         file_path));
+          base::Bind(OpenNaClReadExecImpl, file_path, true /* is_executable */),
+          base::Bind(&NaClProcessHost::StartNaClFileResolved,
+                     weak_factory_.GetWeakPtr(), params, file_path));
       return true;
     }
   }
 
-  StartNaClFileResolved(std::move(params), base::FilePath(), base::File());
+  StartNaClFileResolved(params, base::FilePath(), base::File());
   return true;
 }
 
@@ -874,12 +878,10 @@
 
     // On success, send back a success message to the renderer process,
     // and transfer the channel handles for the NaCl loader process to
-    // |params|. Also send an invalid shared memory region as nonsfi_mode
-    // does not use the region.
+    // |params|.
     ReplyToRenderer(std::move(ppapi_renderer_channel.handle1),
                     std::move(trusted_service_channel.handle1),
-                    std::move(manifest_service_channel.handle1),
-                    base::ReadOnlySharedMemoryRegion());
+                    std::move(manifest_service_channel.handle1));
     params.ppapi_browser_channel_handle =
         ppapi_browser_channel.handle0.release();
     params.ppapi_renderer_channel_handle =
@@ -891,7 +893,7 @@
   }
 #endif
 
-  process_->Send(new NaClProcessMsg_Start(std::move(params)));
+  process_->Send(new NaClProcessMsg_Start(params));
 }
 
 bool NaClProcessHost::StartPPAPIProxy(
@@ -963,8 +965,7 @@
     const IPC::ChannelHandle& raw_ppapi_browser_channel_handle,
     const IPC::ChannelHandle& raw_ppapi_renderer_channel_handle,
     const IPC::ChannelHandle& raw_trusted_renderer_channel_handle,
-    const IPC::ChannelHandle& raw_manifest_service_channel_handle,
-    base::ReadOnlySharedMemoryRegion crash_info_shmem_region) {
+    const IPC::ChannelHandle& raw_manifest_service_channel_handle) {
   DCHECK(raw_ppapi_browser_channel_handle.is_mojo_channel_handle());
   DCHECK(raw_ppapi_renderer_channel_handle.is_mojo_channel_handle());
   DCHECK(raw_trusted_renderer_channel_handle.is_mojo_channel_handle());
@@ -987,8 +988,7 @@
   // Let the renderer know that the IPC channels are established.
   ReplyToRenderer(std::move(ppapi_renderer_channel_handle),
                   std::move(trusted_renderer_channel_handle),
-                  std::move(manifest_service_channel_handle),
-                  std::move(crash_info_shmem_region));
+                  std::move(manifest_service_channel_handle));
 }
 
 bool NaClProcessHost::StartWithLaunchedProcess() {
diff --git a/components/nacl/browser/nacl_process_host.h b/components/nacl/browser/nacl_process_host.h
index afdd695..9771dba 100644
--- a/components/nacl/browser/nacl_process_host.h
+++ b/components/nacl/browser/nacl_process_host.h
@@ -15,8 +15,8 @@
 #include "base/files/file.h"
 #include "base/files/file_path.h"
 #include "base/macros.h"
-#include "base/memory/read_only_shared_memory_region.h"
 #include "base/memory/ref_counted.h"
+#include "base/memory/shared_memory.h"
 #include "base/memory/weak_ptr.h"
 #include "base/process/process.h"
 #include "components/nacl/common/nacl_types.h"
@@ -144,8 +144,7 @@
   void ReplyToRenderer(
       mojo::ScopedMessagePipeHandle ppapi_channel_handle,
       mojo::ScopedMessagePipeHandle trusted_channel_handle,
-      mojo::ScopedMessagePipeHandle manifest_service_channel_handle,
-      base::ReadOnlySharedMemoryRegion crash_info_shmem_region);
+      mojo::ScopedMessagePipeHandle manifest_service_channel_handle);
 
   // Sends the reply with error message to the renderer.
   void SendErrorToRenderer(const std::string& error_message);
@@ -195,8 +194,7 @@
       const IPC::ChannelHandle& ppapi_browser_channel_handle,
       const IPC::ChannelHandle& ppapi_renderer_channel_handle,
       const IPC::ChannelHandle& trusted_renderer_channel_handle,
-      const IPC::ChannelHandle& manifest_service_channel_handle,
-      base::ReadOnlySharedMemoryRegion crash_info_shmem_region);
+      const IPC::ChannelHandle& manifest_service_channel_handle);
 
   GURL manifest_url_;
   base::File nexe_file_;
@@ -248,6 +246,10 @@
   // Throttling time in milliseconds for PpapiHostMsg_Keepalive IPCs.
   static unsigned keepalive_throttle_interval_milliseconds_;
 
+  // Shared memory provided to the plugin and renderer for
+  // reporting crash information.
+  base::SharedMemory crash_info_shmem_;
+
   base::WeakPtrFactory<NaClProcessHost> weak_factory_;
 
   DISALLOW_COPY_AND_ASSIGN(NaClProcessHost);
diff --git a/components/nacl/common/nacl_host_messages.h b/components/nacl/common/nacl_host_messages.h
index da22992..f7c5588 100644
--- a/components/nacl/common/nacl_host_messages.h
+++ b/components/nacl/common/nacl_host_messages.h
@@ -44,7 +44,7 @@
   IPC_STRUCT_TRAITS_MEMBER(manifest_service_ipc_channel_handle)
   IPC_STRUCT_TRAITS_MEMBER(plugin_pid)
   IPC_STRUCT_TRAITS_MEMBER(plugin_child_id)
-  IPC_STRUCT_TRAITS_MEMBER(crash_info_shmem_region)
+  IPC_STRUCT_TRAITS_MEMBER(crash_info_shmem_handle)
 IPC_STRUCT_TRAITS_END()
 
 IPC_STRUCT_TRAITS_BEGIN(nacl::PnaclCacheInfo)
diff --git a/components/nacl/common/nacl_messages.h b/components/nacl/common/nacl_messages.h
index 052527a..d6862ab 100644
--- a/components/nacl/common/nacl_messages.h
+++ b/components/nacl/common/nacl_messages.h
@@ -8,7 +8,6 @@
 
 #include <stdint.h>
 
-#include "base/memory/read_only_shared_memory_region.h"
 #include "base/process/process.h"
 #include "build/build_config.h"
 #include "components/nacl/common/nacl_types.h"
@@ -39,7 +38,7 @@
   IPC_STRUCT_TRAITS_MEMBER(version)
   IPC_STRUCT_TRAITS_MEMBER(enable_debug_stub)
   IPC_STRUCT_TRAITS_MEMBER(process_type)
-  IPC_STRUCT_TRAITS_MEMBER(crash_info_shmem_region)
+  IPC_STRUCT_TRAITS_MEMBER(crash_info_shmem_handle)
 IPC_STRUCT_TRAITS_END()
 
 IPC_STRUCT_TRAITS_BEGIN(nacl::NaClResourcePrefetchResult)
@@ -130,10 +129,8 @@
 // created successfully.
 // This is used for SFI mode only. Non-SFI mode passes channel handles in
 // NaClStartParams instead.
-IPC_MESSAGE_CONTROL5(
-    NaClProcessHostMsg_PpapiChannelsCreated,
-    IPC::ChannelHandle, /* browser_channel_handle */
-    IPC::ChannelHandle, /* ppapi_renderer_channel_handle */
-    IPC::ChannelHandle, /* trusted_renderer_channel_handle */
-    IPC::ChannelHandle, /* manifest_service_channel_handle */
-    base::ReadOnlySharedMemoryRegion /* crash_info_shmem_region */)
+IPC_MESSAGE_CONTROL4(NaClProcessHostMsg_PpapiChannelsCreated,
+                     IPC::ChannelHandle, /* browser_channel_handle */
+                     IPC::ChannelHandle, /* ppapi_renderer_channel_handle */
+                     IPC::ChannelHandle, /* trusted_renderer_channel_handle */
+                     IPC::ChannelHandle /* manifest_service_channel_handle */)
diff --git a/components/nacl/common/nacl_types.cc b/components/nacl/common/nacl_types.cc
index 88934a1..76bbfa6 100644
--- a/components/nacl/common/nacl_types.cc
+++ b/components/nacl/common/nacl_types.cc
@@ -19,7 +19,7 @@
       process_type(kUnknownNaClProcessType) {
 }
 
-NaClStartParams::NaClStartParams(NaClStartParams&& other) = default;
+NaClStartParams::NaClStartParams(const NaClStartParams& other) = default;
 
 NaClStartParams::~NaClStartParams() {
 }
@@ -98,13 +98,14 @@
     const IPC::ChannelHandle& manifest_service_ipc_channel_handle,
     base::ProcessId plugin_pid,
     int plugin_child_id,
-    base::ReadOnlySharedMemoryRegion crash_info_shmem_region)
+    base::SharedMemoryHandle crash_info_shmem_handle)
     : ppapi_ipc_channel_handle(ppapi_ipc_channel_handle),
       trusted_ipc_channel_handle(trusted_ipc_channel_handle),
       manifest_service_ipc_channel_handle(manifest_service_ipc_channel_handle),
       plugin_pid(plugin_pid),
       plugin_child_id(plugin_child_id),
-      crash_info_shmem_region(std::move(crash_info_shmem_region)) {}
+      crash_info_shmem_handle(crash_info_shmem_handle) {
+}
 
 NaClLaunchResult::~NaClLaunchResult() {
 }
diff --git a/components/nacl/common/nacl_types.h b/components/nacl/common/nacl_types.h
index 495629a..6c99162 100644
--- a/components/nacl/common/nacl_types.h
+++ b/components/nacl/common/nacl_types.h
@@ -11,8 +11,7 @@
 #include <utility>
 #include <vector>
 
-#include "base/memory/read_only_shared_memory_region.h"
-#include "base/memory/writable_shared_memory_region.h"
+#include "base/memory/shared_memory.h"
 #include "base/process/process_handle.h"
 #include "build/build_config.h"
 #include "ipc/ipc_channel.h"
@@ -68,7 +67,7 @@
 // Parameters sent to the NaCl process when we start it.
 struct NaClStartParams {
   NaClStartParams();
-  NaClStartParams(NaClStartParams&& other);
+  NaClStartParams(const NaClStartParams& other);
   ~NaClStartParams();
 
   IPC::PlatformFileForTransit nexe_file;
@@ -104,14 +103,11 @@
   NaClAppProcessType process_type;
 
   // For NaCl <-> renderer crash information reporting.
-  base::WritableSharedMemoryRegion crash_info_shmem_region;
+  base::SharedMemoryHandle crash_info_shmem_handle;
 
   // NOTE: Any new fields added here must also be added to the IPC
   // serialization in nacl_messages.h and (for POD fields) the constructor
   // in nacl_types.cc.
-
- private:
-  DISALLOW_COPY_AND_ASSIGN(NaClStartParams);
 };
 
 // Parameters sent to the browser process to have it launch a NaCl process.
@@ -157,7 +153,7 @@
       const IPC::ChannelHandle& manifest_service_ipc_channel_handle,
       base::ProcessId plugin_pid,
       int plugin_child_id,
-      base::ReadOnlySharedMemoryRegion crash_info_shmem_region);
+      base::SharedMemoryHandle crash_info_shmem_handle);
   ~NaClLaunchResult();
 
   // For plugin <-> renderer PPAPI communication.
@@ -174,10 +170,7 @@
   int plugin_child_id;
 
   // For NaCl <-> renderer crash information reporting.
-  base::ReadOnlySharedMemoryRegion crash_info_shmem_region;
-
- private:
-  DISALLOW_COPY_AND_ASSIGN(NaClLaunchResult);
+  base::SharedMemoryHandle crash_info_shmem_handle;
 };
 
 }  // namespace nacl
diff --git a/components/nacl/loader/nacl_listener.cc b/components/nacl/loader/nacl_listener.cc
index 8f89078..db8875f 100644
--- a/components/nacl/loader/nacl_listener.cc
+++ b/components/nacl/loader/nacl_listener.cc
@@ -19,7 +19,6 @@
 
 #include "base/command_line.h"
 #include "base/logging.h"
-#include "base/memory/read_only_shared_memory_region.h"
 #include "base/message_loop/message_loop.h"
 #include "base/rand_util.h"
 #include "base/run_loop.h"
@@ -283,7 +282,7 @@
   }
 }
 
-void NaClListener::OnStart(nacl::NaClStartParams params) {
+void NaClListener::OnStart(const nacl::NaClStartParams& params) {
   is_started_ = true;
 #if defined(OS_LINUX) || defined(OS_MACOSX)
   int urandom_fd = HANDLE_EINTR(dup(base::GetUrandomFD()));
@@ -295,13 +294,10 @@
   struct NaClApp* nap = NULL;
   NaClChromeMainInit();
 
-  CHECK(params.crash_info_shmem_region.IsValid());
-  crash_info_shmem_mapping_ = params.crash_info_shmem_region.Map();
-  base::ReadOnlySharedMemoryRegion ro_shmem_region =
-      base::WritableSharedMemoryRegion::ConvertToReadOnly(
-          std::move(params.crash_info_shmem_region));
-  CHECK(crash_info_shmem_mapping_.IsValid());
-  CHECK(ro_shmem_region.IsValid());
+  CHECK(base::SharedMemory::IsHandleValid(params.crash_info_shmem_handle));
+  crash_info_shmem_.reset(new base::SharedMemory(
+      params.crash_info_shmem_handle, false /* not readonly */));
+  CHECK(crash_info_shmem_->Map(nacl::kNaClCrashInfoShmemSize));
   NaClSetFatalErrorCallback(&FatalLogHandler);
 
   nap = NaClAppCreate();
@@ -334,7 +330,7 @@
   if (!Send(new NaClProcessHostMsg_PpapiChannelsCreated(
           browser_handle, ppapi_renderer_handle,
           MakeRequest(&renderer_host).PassMessagePipe().release(),
-          manifest_service_handle, ro_shmem_region)))
+          manifest_service_handle)))
     LOG(FATAL) << "Failed to send IPC channel handle to NaClProcessHost.";
 
   trusted_listener_ = std::make_unique<NaClTrustedListener>(
diff --git a/components/nacl/loader/nacl_listener.h b/components/nacl/loader/nacl_listener.h
index e3be7cc..ed95cc3 100644
--- a/components/nacl/loader/nacl_listener.h
+++ b/components/nacl/loader/nacl_listener.h
@@ -14,7 +14,7 @@
 
 #include "base/macros.h"
 #include "base/memory/ref_counted.h"
-#include "base/memory/shared_memory_mapping.h"
+#include "base/memory/shared_memory.h"
 #include "base/single_thread_task_runner.h"
 #include "base/synchronization/waitable_event.h"
 #include "base/threading/thread.h"
@@ -50,9 +50,7 @@
   }
 #endif
 
-  void* crash_info_shmem_memory() const {
-    return crash_info_shmem_mapping_.memory();
-  }
+  void* crash_info_shmem_memory() const { return crash_info_shmem_->memory(); }
 
   NaClTrustedListener* trusted_listener() const {
     return trusted_listener_.get();
@@ -81,7 +79,7 @@
 
   void OnAddPrefetchedResource(
       const nacl::NaClResourcePrefetchResult& prefetched_resource_file);
-  void OnStart(nacl::NaClStartParams params);
+  void OnStart(const nacl::NaClStartParams& params);
 
   // A channel back to the browser.
   std::unique_ptr<IPC::SyncChannel> channel_;
@@ -104,7 +102,7 @@
   int number_of_cores_;
 #endif
 
-  base::WritableSharedMemoryMapping crash_info_shmem_mapping_;
+  std::unique_ptr<base::SharedMemory> crash_info_shmem_;
 
   std::unique_ptr<NaClTrustedListener> trusted_listener_;
 
diff --git a/components/nacl/renderer/nexe_load_manager.cc b/components/nacl/renderer/nexe_load_manager.cc
index 2f2bd61..206bab7 100644
--- a/components/nacl/renderer/nexe_load_manager.cc
+++ b/components/nacl/renderer/nexe_load_manager.cc
@@ -9,7 +9,6 @@
 
 #include "base/command_line.h"
 #include "base/logging.h"
-#include "base/memory/shared_memory_mapping.h"
 #include "base/metrics/histogram.h"
 #include "base/strings/string_tokenizer.h"
 #include "base/strings/string_util.h"
@@ -102,6 +101,8 @@
     base::TimeDelta uptime = base::Time::Now() - ready_time_;
     HistogramTimeLarge("NaCl.ModuleUptime.Normal", uptime.InMilliseconds());
   }
+  if (base::SharedMemory::IsHandleValid(crash_info_shmem_handle_))
+    base::SharedMemory::CloseHandle(crash_info_shmem_handle_);
 }
 
 void NexeLoadManager::NexeFileDidOpen(int32_t pp_error,
@@ -259,15 +260,23 @@
   // invocation will just be a no-op, since the entire crash log will
   // have been received and we'll just get an EOF indication.
 
-  base::ReadOnlySharedMemoryMapping shmem_mapping =
-      crash_info_shmem_region_.MapAt(0, kNaClCrashInfoShmemSize);
-  if (shmem_mapping.IsValid()) {
-    base::BufferIterator<const uint8_t> buffer =
-        shmem_mapping.GetMemoryAsBufferIterator<uint8_t>();
-    const uint32_t* crash_log_length = buffer.Object<uint32_t>();
-    base::span<const uint8_t> data = buffer.Span<uint8_t>(
-        std::min<uint32_t>(*crash_log_length, kNaClCrashInfoMaxLogSize));
-    std::string crash_log(data.begin(), data.end());
+  base::SharedMemory shmem(crash_info_shmem_handle_, true);
+  // When shmem goes out of scope, the handle will be closed. Invalidate
+  // our handle so our destructor doesn't try to close it again.
+  crash_info_shmem_handle_ = base::SharedMemoryHandle();
+  if (shmem.Map(kNaClCrashInfoShmemSize)) {
+    uint32_t crash_log_length;
+    // We cast the length value to volatile here to prevent the compiler from
+    // reordering instructions in a way that could introduce a TOCTTOU race.
+    crash_log_length = *(static_cast<volatile uint32_t*>(shmem.memory()));
+    crash_log_length = std::min<uint32_t>(crash_log_length,
+                                          kNaClCrashInfoMaxLogSize);
+
+    std::unique_ptr<char[]> crash_log_data(new char[kNaClCrashInfoShmemSize]);
+    memcpy(crash_log_data.get(),
+           static_cast<char*>(shmem.memory()) + sizeof(uint32_t),
+           crash_log_length);
+    std::string crash_log(crash_log_data.get(), crash_log_length);
     CopyCrashLogToJsConsole(crash_log);
   }
 }
diff --git a/components/nacl/renderer/nexe_load_manager.h b/components/nacl/renderer/nexe_load_manager.h
index e6a07f7..ae68b2f 100644
--- a/components/nacl/renderer/nexe_load_manager.h
+++ b/components/nacl/renderer/nexe_load_manager.h
@@ -13,7 +13,7 @@
 
 #include "base/files/file.h"
 #include "base/macros.h"
-#include "base/memory/read_only_shared_memory_region.h"
+#include "base/memory/shared_memory.h"
 #include "base/memory/weak_ptr.h"
 #include "base/time/time.h"
 #include "components/nacl/renderer/ppb_nacl_private.h"
@@ -115,9 +115,8 @@
 
   const std::string& program_url() const { return program_url_; }
 
-  void set_crash_info_shmem_region(
-      base::ReadOnlySharedMemoryRegion shmem_region) {
-    crash_info_shmem_region_ = std::move(shmem_region);
+  void set_crash_info_shmem_handle(base::SharedMemoryHandle h) {
+    crash_info_shmem_handle_ = h;
   }
 
   bool nonsfi() const { return nonsfi_; }
@@ -185,7 +184,7 @@
   // A flag that indicates if the plugin is using Non-SFI mode.
   bool nonsfi_;
 
-  base::ReadOnlySharedMemoryRegion crash_info_shmem_region_;
+  base::SharedMemoryHandle crash_info_shmem_handle_;
 
   std::unique_ptr<TrustedPluginChannel> trusted_plugin_channel_;
   std::unique_ptr<ManifestServiceChannel> manifest_service_channel_;
diff --git a/components/nacl/renderer/ppb_nacl_private_impl.cc b/components/nacl/renderer/ppb_nacl_private_impl.cc
index 31cc2fec..0cb13c17 100644
--- a/components/nacl/renderer/ppb_nacl_private_impl.cc
+++ b/components/nacl/renderer/ppb_nacl_private_impl.cc
@@ -487,6 +487,10 @@
     // Even on error, some FDs/handles may be passed to here.
     // We must release those resources.
     // See also nacl_process_host.cc.
+    if (base::SharedMemory::IsHandleValid(
+            launch_result.crash_info_shmem_handle))
+      base::SharedMemory::CloseHandle(launch_result.crash_info_shmem_handle);
+
     if (PP_ToBool(main_service_runtime)) {
       load_manager->ReportLoadError(PP_NACL_ERROR_SEL_LDR_LAUNCH,
                                     "ServiceRuntime: failed to start",
@@ -523,8 +527,8 @@
   }
 
   // Store the crash information shared memory handle.
-  load_manager->set_crash_info_shmem_region(
-      std::move(launch_result.crash_info_shmem_region));
+  load_manager->set_crash_info_shmem_handle(
+      launch_result.crash_info_shmem_handle);
 
   // Create the trusted plugin channel.
   if (!IsValidChannelHandle(launch_result.trusted_ipc_channel_handle)) {
diff --git a/components/offline_pages/core/prefetch/prefetch_dispatcher_impl.cc b/components/offline_pages/core/prefetch/prefetch_dispatcher_impl.cc
index 72d3960..a679cb6 100644
--- a/components/offline_pages/core/prefetch/prefetch_dispatcher_impl.cc
+++ b/components/offline_pages/core/prefetch/prefetch_dispatcher_impl.cc
@@ -264,8 +264,7 @@
 
   std::unique_ptr<Task> generate_page_bundle_task =
       std::make_unique<GeneratePageBundleTask>(
-          this, service_->GetPrefetchStore(), service_->GetPrefetchGCMHandler(),
-          service_->GetCachedGCMToken(),
+          this, service_->GetPrefetchStore(), service_->GetCachedGCMToken(),
           service_->GetPrefetchNetworkRequestFactory(),
           base::BindOnce(
               &PrefetchDispatcherImpl::DidGenerateBundleOrGetOperationRequest,
diff --git a/components/offline_pages/core/prefetch/prefetch_service.h b/components/offline_pages/core/prefetch/prefetch_service.h
index d7d9d87..53d1b8ee 100644
--- a/components/offline_pages/core/prefetch/prefetch_service.h
+++ b/components/offline_pages/core/prefetch/prefetch_service.h
@@ -84,6 +84,8 @@
   // suggestion from the Prefetching pipeline and/or the Offline Pages database.
   virtual void RemoveSuggestion(GURL url) = 0;
 
+  // Returns a pointer to the PrefetchGCMHandler. It is not available in reduced
+  // mode.
   virtual PrefetchGCMHandler* GetPrefetchGCMHandler() = 0;
 
   // Obtains the current GCM token from the PrefetchGCMHandler
diff --git a/components/offline_pages/core/prefetch/prefetch_service_impl.cc b/components/offline_pages/core/prefetch/prefetch_service_impl.cc
index 0f14b3a..ba3643b 100644
--- a/components/offline_pages/core/prefetch/prefetch_service_impl.cc
+++ b/components/offline_pages/core/prefetch/prefetch_service_impl.cc
@@ -7,6 +7,7 @@
 #include <utility>
 
 #include "base/bind.h"
+#include "base/bind_helpers.h"
 #include "base/logging.h"
 #include "components/image_fetcher/core/image_fetcher.h"
 #include "components/offline_pages/core/client_id.h"
@@ -29,7 +30,6 @@
 PrefetchServiceImpl::PrefetchServiceImpl(
     std::unique_ptr<OfflineMetricsCollector> offline_metrics_collector,
     std::unique_ptr<PrefetchDispatcher> dispatcher,
-    std::unique_ptr<PrefetchGCMHandler> gcm_handler,
     std::unique_ptr<PrefetchNetworkRequestFactory> network_request_factory,
     OfflinePageModel* offline_page_model,
     std::unique_ptr<PrefetchStore> prefetch_store,
@@ -42,7 +42,6 @@
     image_fetcher::ImageFetcher* image_fetcher)
     : offline_metrics_collector_(std::move(offline_metrics_collector)),
       prefetch_dispatcher_(std::move(dispatcher)),
-      prefetch_gcm_handler_(std::move(gcm_handler)),
       network_request_factory_(std::move(network_request_factory)),
       offline_page_model_(offline_page_model),
       prefetch_store_(std::move(prefetch_store)),
@@ -56,7 +55,6 @@
       weak_ptr_factory_(this) {
   prefetch_dispatcher_->SetService(this);
   prefetch_downloader_->SetPrefetchService(this);
-  prefetch_gcm_handler_->SetService(this);
   if (suggested_articles_observer_)
     suggested_articles_observer_->SetPrefetchService(this);
 }
@@ -79,25 +77,34 @@
 }
 
 void PrefetchServiceImpl::SetCachedGCMToken(const std::string& gcm_token) {
-  gcm_token_ = gcm_token;
+  // This method is passed a cached token that was stored in the job scheduler,
+  // to be used until the PrefetchGCMHandler is created. In some cases, the
+  // PrefetchGCMHandler could have been already created and a fresher token
+  // requested before this function is called. Make sure to not override a
+  // fresher token with a stale one.
+  if (gcm_token_.empty())
+    gcm_token_ = gcm_token;
 }
 
 const std::string& PrefetchServiceImpl::GetCachedGCMToken() const {
+  DCHECK(!gcm_token_.empty()) << "No cached token is set, you should call "
+                                 "PrefetchService::GetGCMToken instead";
   return gcm_token_;
 }
 
 void PrefetchServiceImpl::GetGCMToken(GCMTokenCallback callback) {
   DCHECK(prefetch_gcm_handler_);
   prefetch_gcm_handler_->GetGCMToken(base::AdaptCallbackForRepeating(
-      base::BindOnce(&PrefetchServiceImpl::OnGCMTokenReceived,
-                     weak_ptr_factory_.GetWeakPtr(), std::move(callback))));
+      base::BindOnce(&PrefetchServiceImpl::OnGCMTokenReceived, GetWeakPtr(),
+                     std::move(callback))));
 }
 
 void PrefetchServiceImpl::OnGCMTokenReceived(
     GCMTokenCallback callback,
     const std::string& gcm_token,
     instance_id::InstanceID::Result result) {
-  // Keep the token fresh
+  // TODO(dimich): Add UMA reporting on instance_id::InstanceID::Result.
+  // Keep the cached token fresh
   gcm_token_ = gcm_token;
   std::move(callback).Run(gcm_token);
 }
@@ -157,9 +164,17 @@
 }
 
 PrefetchGCMHandler* PrefetchServiceImpl::GetPrefetchGCMHandler() {
+  DCHECK(prefetch_gcm_handler_);
   return prefetch_gcm_handler_.get();
 }
 
+void PrefetchServiceImpl::SetPrefetchGCMHandler(
+    std::unique_ptr<PrefetchGCMHandler> handler) {
+  DCHECK(!prefetch_gcm_handler_);
+  prefetch_gcm_handler_ = std::move(handler);
+  prefetch_gcm_handler_->SetService(this);
+}
+
 PrefetchNetworkRequestFactory*
 PrefetchServiceImpl::GetPrefetchNetworkRequestFactory() {
   return network_request_factory_.get();
@@ -203,7 +218,12 @@
   return image_fetcher_;
 }
 
+base::WeakPtr<PrefetchServiceImpl> PrefetchServiceImpl::GetWeakPtr() {
+  return weak_ptr_factory_.GetWeakPtr();
+}
+
 void PrefetchServiceImpl::Shutdown() {
+  prefetch_gcm_handler_.reset();
   suggested_articles_observer_.reset();
   prefetch_downloader_.reset();
 }
diff --git a/components/offline_pages/core/prefetch/prefetch_service_impl.h b/components/offline_pages/core/prefetch/prefetch_service_impl.h
index ad0524b..5f44885 100644
--- a/components/offline_pages/core/prefetch/prefetch_service_impl.h
+++ b/components/offline_pages/core/prefetch/prefetch_service_impl.h
@@ -25,7 +25,6 @@
   PrefetchServiceImpl(
       std::unique_ptr<OfflineMetricsCollector> offline_metrics_collector,
       std::unique_ptr<PrefetchDispatcher> dispatcher,
-      std::unique_ptr<PrefetchGCMHandler> gcm_handler,
       std::unique_ptr<PrefetchNetworkRequestFactory> network_request_factory,
       OfflinePageModel* offline_page_model,
       std::unique_ptr<PrefetchStore> prefetch_store,
@@ -64,6 +63,8 @@
   PrefetchImporter* GetPrefetchImporter() override;
   PrefetchBackgroundTaskHandler* GetPrefetchBackgroundTaskHandler() override;
 
+  void SetPrefetchGCMHandler(std::unique_ptr<PrefetchGCMHandler> handler);
+
   // Thumbnail fetchers. With Feed, GetImageFetcher() is available
   // and GetThumbnailFetcher() is null.
   ThumbnailFetcher* GetThumbnailFetcher() override;
@@ -71,6 +72,8 @@
 
   SuggestedArticlesObserver* GetSuggestedArticlesObserverForTesting() override;
 
+  base::WeakPtr<PrefetchServiceImpl> GetWeakPtr();
+
   // KeyedService implementation:
   void Shutdown() override;
 
@@ -81,10 +84,10 @@
 
   OfflineEventLogger logger_;
   std::string gcm_token_;
+  std::unique_ptr<PrefetchGCMHandler> prefetch_gcm_handler_;
 
   std::unique_ptr<OfflineMetricsCollector> offline_metrics_collector_;
   std::unique_ptr<PrefetchDispatcher> prefetch_dispatcher_;
-  std::unique_ptr<PrefetchGCMHandler> prefetch_gcm_handler_;
   std::unique_ptr<PrefetchNetworkRequestFactory> network_request_factory_;
   OfflinePageModel* offline_page_model_;
   std::unique_ptr<PrefetchStore> prefetch_store_;
diff --git a/components/offline_pages/core/prefetch/prefetch_service_test_taco.cc b/components/offline_pages/core/prefetch/prefetch_service_test_taco.cc
index f19bafe..f0eedd7 100644
--- a/components/offline_pages/core/prefetch/prefetch_service_test_taco.cc
+++ b/components/offline_pages/core/prefetch/prefetch_service_test_taco.cc
@@ -194,14 +194,15 @@
 
 void PrefetchServiceTestTaco::CreatePrefetchService() {
   CHECK(!prefetch_service_);
-  prefetch_service_ = std::make_unique<PrefetchServiceImpl>(
+  auto service = std::make_unique<PrefetchServiceImpl>(
       std::move(metrics_collector_), std::move(dispatcher_),
-      std::move(gcm_handler_), std::move(network_request_factory_),
-      offline_page_model_.get(), std::move(prefetch_store_),
-      std::move(suggested_articles_observer_), std::move(prefetch_downloader_),
-      std::move(prefetch_importer_),
+      std::move(network_request_factory_), offline_page_model_.get(),
+      std::move(prefetch_store_), std::move(suggested_articles_observer_),
+      std::move(prefetch_downloader_), std::move(prefetch_importer_),
       std::move(prefetch_background_task_handler_),
       std::move(thumbnail_fetcher_), thumbnail_image_fetcher_.get());
+  service->SetPrefetchGCMHandler(std::move(gcm_handler_));
+  prefetch_service_ = std::move(service);
 }
 
 std::unique_ptr<PrefetchService>
diff --git a/components/offline_pages/core/prefetch/stub_prefetch_service.cc b/components/offline_pages/core/prefetch/stub_prefetch_service.cc
index 42225af..af8ddba 100644
--- a/components/offline_pages/core/prefetch/stub_prefetch_service.cc
+++ b/components/offline_pages/core/prefetch/stub_prefetch_service.cc
@@ -19,6 +19,7 @@
 void StubPrefetchService::RemoveSuggestion(GURL url) {}
 
 void StubPrefetchService::SetCachedGCMToken(const std::string& gcm_token) {}
+
 void StubPrefetchService::GetGCMToken(GCMTokenCallback callback) {}
 
 void StubPrefetchService::ForceRefreshSuggestions() {}
diff --git a/components/offline_pages/core/prefetch/tasks/generate_page_bundle_task.cc b/components/offline_pages/core/prefetch/tasks/generate_page_bundle_task.cc
index 134e726..871c2ad 100644
--- a/components/offline_pages/core/prefetch/tasks/generate_page_bundle_task.cc
+++ b/components/offline_pages/core/prefetch/tasks/generate_page_bundle_task.cc
@@ -144,13 +144,11 @@
 GeneratePageBundleTask::GeneratePageBundleTask(
     PrefetchDispatcher* prefetch_dispatcher,
     PrefetchStore* prefetch_store,
-    PrefetchGCMHandler* gcm_handler,
     const std::string& gcm_token,
     PrefetchNetworkRequestFactory* request_factory,
     PrefetchRequestFinishedCallback callback)
     : prefetch_dispatcher_(prefetch_dispatcher),
       prefetch_store_(prefetch_store),
-      gcm_handler_(gcm_handler),
       gcm_token_(gcm_token),
       request_factory_(request_factory),
       callback_(std::move(callback)),
@@ -175,24 +173,7 @@
   DCHECK(!url_and_ids->urls.empty());
   DCHECK_EQ(url_and_ids->urls.size(), url_and_ids->ids.size());
 
-  if (gcm_handler_) {
-    gcm_handler_->GetGCMToken(base::AdaptCallbackForRepeating(
-        base::BindOnce(&GeneratePageBundleTask::GotRegistrationId,
-                       weak_factory_.GetWeakPtr(), std::move(url_and_ids))));
-  } else {
-    DCHECK(!gcm_token_.empty());
-    GotRegistrationId(std::move(url_and_ids), gcm_token_,
-                      instance_id::InstanceID::Result::SUCCESS);
-  }
-}
-
-void GeneratePageBundleTask::GotRegistrationId(
-    std::unique_ptr<UrlAndIds> url_and_ids,
-    const std::string& id,
-    instance_id::InstanceID::Result result) {
-  DCHECK(url_and_ids);
-  // TODO(dimich): Add UMA reporting on instance_id::InstanceID::Result.
-  request_factory_->MakeGeneratePageBundleRequest(url_and_ids->urls, id,
+  request_factory_->MakeGeneratePageBundleRequest(url_and_ids->urls, gcm_token_,
                                                   std::move(callback_));
   prefetch_dispatcher_->GeneratePageBundleRequested(
       std::make_unique<PrefetchDispatcher::IdsVector>(
diff --git a/components/offline_pages/core/prefetch/tasks/generate_page_bundle_task.h b/components/offline_pages/core/prefetch/tasks/generate_page_bundle_task.h
index 224e487..1d2edff 100644
--- a/components/offline_pages/core/prefetch/tasks/generate_page_bundle_task.h
+++ b/components/offline_pages/core/prefetch/tasks/generate_page_bundle_task.h
@@ -10,13 +10,11 @@
 #include <vector>
 
 #include "base/memory/weak_ptr.h"
-#include "components/gcm_driver/instance_id/instance_id.h"
 #include "components/offline_pages/core/prefetch/prefetch_dispatcher.h"
 #include "components/offline_pages/core/prefetch/prefetch_types.h"
 #include "components/offline_pages/task/task.h"
 
 namespace offline_pages {
-class PrefetchGCMHandler;
 class PrefetchNetworkRequestFactory;
 class PrefetchStore;
 
@@ -28,7 +26,6 @@
 
   GeneratePageBundleTask(PrefetchDispatcher* prefetch_dispatcher,
                          PrefetchStore* prefetch_store,
-                         PrefetchGCMHandler* gcm_handler,
                          const std::string& gcm_token,
                          PrefetchNetworkRequestFactory* request_factory,
                          PrefetchRequestFinishedCallback callback);
@@ -39,13 +36,9 @@
 
  private:
   void StartGeneratePageBundle(std::unique_ptr<UrlAndIds> url_and_ids);
-  void GotRegistrationId(std::unique_ptr<UrlAndIds> url_and_ids,
-                         const std::string& id,
-                         instance_id::InstanceID::Result result);
 
   PrefetchDispatcher* prefetch_dispatcher_;
   PrefetchStore* prefetch_store_;
-  PrefetchGCMHandler* gcm_handler_;
   std::string gcm_token_;
   PrefetchNetworkRequestFactory* request_factory_;
   PrefetchRequestFinishedCallback callback_;
diff --git a/components/offline_pages/core/prefetch/tasks/generate_page_bundle_task_unittest.cc b/components/offline_pages/core/prefetch/tasks/generate_page_bundle_task_unittest.cc
index 43546aa..5dc4496 100644
--- a/components/offline_pages/core/prefetch/tasks/generate_page_bundle_task_unittest.cc
+++ b/components/offline_pages/core/prefetch/tasks/generate_page_bundle_task_unittest.cc
@@ -16,7 +16,6 @@
 #include "components/offline_pages/core/prefetch/store/prefetch_store_utils.h"
 #include "components/offline_pages/core/prefetch/tasks/prefetch_task_test_base.h"
 #include "components/offline_pages/core/prefetch/test_prefetch_dispatcher.h"
-#include "components/offline_pages/core/prefetch/test_prefetch_gcm_handler.h"
 #include "components/offline_pages/core/test_scoped_offline_clock.h"
 #include "components/offline_pages/task/task.h"
 #include "services/network/test/test_utils.h"
@@ -37,13 +36,11 @@
   GeneratePageBundleTaskTest() = default;
   ~GeneratePageBundleTaskTest() override = default;
 
-  TestPrefetchGCMHandler* gcm_handler() { return &gcm_handler_; }
   std::string gcm_token() { return "dummy_gcm_token"; }
 
   TestPrefetchDispatcher* dispatcher() { return &dispatcher_; }
 
  private:
-  TestPrefetchGCMHandler gcm_handler_;
   TestPrefetchDispatcher dispatcher_;
 };
 
@@ -52,16 +49,16 @@
 
   base::MockCallback<PrefetchRequestFinishedCallback> callback;
   RunTask(std::make_unique<GeneratePageBundleTask>(
-      dispatcher(), store(), gcm_handler(), gcm_token(),
-      prefetch_request_factory(), callback.Get()));
+      dispatcher(), store(), gcm_token(), prefetch_request_factory(),
+      callback.Get()));
   EXPECT_EQ(0, dispatcher()->generate_page_bundle_requested);
 }
 
 TEST_F(GeneratePageBundleTaskTest, EmptyTask) {
   base::MockCallback<PrefetchRequestFinishedCallback> callback;
   RunTask(std::make_unique<GeneratePageBundleTask>(
-      dispatcher(), store(), gcm_handler(), gcm_token(),
-      prefetch_request_factory(), callback.Get()));
+      dispatcher(), store(), gcm_token(), prefetch_request_factory(),
+      callback.Get()));
 
   EXPECT_FALSE(prefetch_request_factory()->HasOutstandingRequests());
   auto requested_urls = prefetch_request_factory()->GetAllUrlsRequested();
@@ -104,7 +101,7 @@
 
   clock.Advance(base::TimeDelta::FromHours(1));
 
-  GeneratePageBundleTask task(dispatcher(), store(), gcm_handler(), gcm_token(),
+  GeneratePageBundleTask task(dispatcher(), store(), gcm_token(),
                               prefetch_request_factory(),
                               request_callback.Get());
   RunTask(&task);
diff --git a/components/send_tab_to_self/BUILD.gn b/components/send_tab_to_self/BUILD.gn
index ed3c7e1f..907f11d 100644
--- a/components/send_tab_to_self/BUILD.gn
+++ b/components/send_tab_to_self/BUILD.gn
@@ -36,12 +36,15 @@
   public_deps = [
     "//components/send_tab_to_self/proto:send_tab_to_self_proto",
   ]
-  if (is_ios || is_android) {
+  if (is_android) {
     sources += [
       "send_tab_to_self_infobar_delegate.cc",
       "send_tab_to_self_infobar_delegate.h",
     ]
-    deps += [ "//components/infobars/core" ]
+    deps += [
+      "//components/infobars/core",
+      "//content/public/browser",
+    ]
   }
 }
 
diff --git a/components/send_tab_to_self/DEPS b/components/send_tab_to_self/DEPS
index ff5519b..de5e9a8 100644
--- a/components/send_tab_to_self/DEPS
+++ b/components/send_tab_to_self/DEPS
@@ -5,5 +5,6 @@
   "+components/sync_device_info",
   "+components/version_info",
   "+components/history",
+  "+content/public/browser",
   "+google_apis",
 ]
diff --git a/components/send_tab_to_self/send_tab_to_self_entry.cc b/components/send_tab_to_self/send_tab_to_self_entry.cc
index b5cba87..30d63b1 100644
--- a/components/send_tab_to_self/send_tab_to_self_entry.cc
+++ b/components/send_tab_to_self/send_tab_to_self_entry.cc
@@ -156,4 +156,16 @@
           kExpiryTime);
 }
 
+std::unique_ptr<SendTabToSelfEntry> SendTabToSelfEntry::FromRequiredFields(
+    const std::string& guid,
+    const GURL& url,
+    const std::string& target_device_sync_cache_guid) {
+  if (guid.empty() || !url.is_valid()) {
+    return nullptr;
+  }
+  return std::make_unique<SendTabToSelfEntry>(guid, url, "", base::Time(),
+                                              base::Time(), "",
+                                              target_device_sync_cache_guid);
+}
+
 }  // namespace send_tab_to_self
diff --git a/components/send_tab_to_self/send_tab_to_self_entry.h b/components/send_tab_to_self/send_tab_to_self_entry.h
index 10d1bf7..549c8a0 100644
--- a/components/send_tab_to_self/send_tab_to_self_entry.h
+++ b/components/send_tab_to_self/send_tab_to_self_entry.h
@@ -78,6 +78,12 @@
   // Returns if the Entry has expired based on the |current_time|.
   bool IsExpired(base::Time current_time) const;
 
+  // Creates a SendTabToSelfEntry consisting of only the required fields.
+  static std::unique_ptr<SendTabToSelfEntry> FromRequiredFields(
+      const std::string& guid,
+      const GURL& url,
+      const std::string& target_device_sync_cache_guid);
+
  private:
   std::string guid_;
   GURL url_;
diff --git a/components/send_tab_to_self/send_tab_to_self_entry_unittest.cc b/components/send_tab_to_self/send_tab_to_self_entry_unittest.cc
index d2c65c3..6743a52 100644
--- a/components/send_tab_to_self/send_tab_to_self_entry_unittest.cc
+++ b/components/send_tab_to_self/send_tab_to_self_entry_unittest.cc
@@ -78,6 +78,17 @@
   EXPECT_TRUE(IsEqualForTesting(entry, pb_entry.specifics()));
 }
 
+// Tests that the send tab to self entry is correctly created from the required
+// fields
+TEST(SendTabToSelfEntry, FromRequiredFields) {
+  SendTabToSelfEntry expected("1", GURL("http://example.com"), "", base::Time(),
+                              base::Time(), "", "target_device");
+  std::unique_ptr<SendTabToSelfEntry> actual =
+      SendTabToSelfEntry::FromRequiredFields("1", GURL("http://example.com"),
+                                             "target_device");
+  EXPECT_TRUE(IsEqualForTesting(expected, *actual));
+}
+
 // Tests that the send tab to self entry is correctly parsed from
 // sync_pb::SendTabToSelfSpecifics.
 TEST(SendTabToSelfEntry, FromProto) {
diff --git a/components/send_tab_to_self/send_tab_to_self_infobar_delegate.cc b/components/send_tab_to_self/send_tab_to_self_infobar_delegate.cc
index 2ecb33d..a7d2327 100644
--- a/components/send_tab_to_self/send_tab_to_self_infobar_delegate.cc
+++ b/components/send_tab_to_self/send_tab_to_self_infobar_delegate.cc
@@ -7,14 +7,17 @@
 #include "base/memory/ptr_util.h"
 #include "base/strings/utf_string_conversions.h"
 #include "components/send_tab_to_self/send_tab_to_self_entry.h"
+#include "content/public/browser/web_contents.h"
 #include "url/gurl.h"
 
 namespace send_tab_to_self {
 
 // static
 std::unique_ptr<SendTabToSelfInfoBarDelegate>
-SendTabToSelfInfoBarDelegate::Create(const SendTabToSelfEntry* entry) {
-  return base::WrapUnique(new SendTabToSelfInfoBarDelegate(entry));
+SendTabToSelfInfoBarDelegate::Create(content::WebContents* web_contents,
+                                     const SendTabToSelfEntry* entry) {
+  return base::WrapUnique(
+      new SendTabToSelfInfoBarDelegate(web_contents, entry));
 }
 
 SendTabToSelfInfoBarDelegate::~SendTabToSelfInfoBarDelegate() {}
@@ -26,7 +29,15 @@
 }
 
 void SendTabToSelfInfoBarDelegate::OpenTab() {
-  NOTIMPLEMENTED();
+  content::OpenURLParams open_url_params(
+      entry_->GetURL(), content::Referrer(),
+      WindowOpenDisposition::NEW_FOREGROUND_TAB,
+      ui::PageTransition::PAGE_TRANSITION_LINK,
+      false /* is_renderer_initiated */);
+  web_contents_->OpenURL(open_url_params);
+
+  // TODO(crbug.com/944602): Update the model to reflect that an infobar is
+  // shown.
 }
 
 void SendTabToSelfInfoBarDelegate::InfoBarDismissed() {
@@ -39,7 +50,9 @@
 }
 
 SendTabToSelfInfoBarDelegate::SendTabToSelfInfoBarDelegate(
+    content::WebContents* web_contents,
     const SendTabToSelfEntry* entry) {
+  web_contents_ = web_contents;
   entry_ = entry;
 }
 
diff --git a/components/send_tab_to_self/send_tab_to_self_infobar_delegate.h b/components/send_tab_to_self/send_tab_to_self_infobar_delegate.h
index e959ff3..b7d5fb4 100644
--- a/components/send_tab_to_self/send_tab_to_self_infobar_delegate.h
+++ b/components/send_tab_to_self/send_tab_to_self_infobar_delegate.h
@@ -11,15 +11,21 @@
 #include "base/strings/string16.h"
 #include "components/infobars/core/infobar_delegate.h"
 
+namespace content {
+class WebContents;
+}
+
 namespace send_tab_to_self {
 
 class SendTabToSelfEntry;
 
 // Delegate containing logic about what to display and how to behave
-// in the SendTabToSelf infobar. Used across Android and iOS.
+// in the SendTabToSelf infobar. Used on Android.
+// TODO(crbug.com/964112): Rename this class to be Android specific.
 class SendTabToSelfInfoBarDelegate : public infobars::InfoBarDelegate {
  public:
   static std::unique_ptr<SendTabToSelfInfoBarDelegate> Create(
+      content::WebContents* web_contents,
       const SendTabToSelfEntry* entry);
   ~SendTabToSelfInfoBarDelegate() override;
 
@@ -34,8 +40,11 @@
   infobars::InfoBarDelegate::InfoBarIdentifier GetIdentifier() const override;
 
  private:
-  explicit SendTabToSelfInfoBarDelegate(const SendTabToSelfEntry* entry);
+  explicit SendTabToSelfInfoBarDelegate(content::WebContents* web_contents,
+                                        const SendTabToSelfEntry* entry);
 
+  // The web_content the infobar is attached to. Must outlive this class.
+  content::WebContents* web_contents_ = nullptr;
   // The entry that was share to this device. Must outlive this instance.
   const SendTabToSelfEntry* entry_ = nullptr;
 
@@ -44,4 +53,4 @@
 
 }  // namespace send_tab_to_self
 
-#endif  // COMPONENTS_SEND_TAB_TO_SELF_SEND_TAB_TO_SELF_INFOBAR_DELEGATE_H_
\ No newline at end of file
+#endif  // COMPONENTS_SEND_TAB_TO_SELF_SEND_TAB_TO_SELF_INFOBAR_DELEGATE_H_
diff --git a/components/sync/driver/about_sync_util.cc b/components/sync/driver/about_sync_util.cc
index 90e407c..795c7e7 100644
--- a/components/sync/driver/about_sync_util.cc
+++ b/components/sync/driver/about_sync_util.cc
@@ -333,10 +333,10 @@
   Stat<bool>* user_is_primary = section_identity->AddBoolStat("Is Primary");
 
   Section* section_credentials = section_list.AddSection("Credentials");
-  Stat<std::string>* request_token_time =
+  Stat<std::string>* token_request_time =
       section_credentials->AddStringStat("Requested Token");
-  Stat<std::string>* receive_token_time =
-      section_credentials->AddStringStat("Received Token");
+  Stat<std::string>* token_response_time =
+      section_credentials->AddStringStat("Received Token Response");
   Stat<std::string>* last_token_request_result =
       section_credentials->AddStringStat("Last Token Request Result");
   Stat<bool>* has_token = section_credentials->AddBoolStat("Has Token");
@@ -477,8 +477,8 @@
   user_is_primary->Set(service->IsAuthenticatedAccountPrimary());
 
   // Credentials.
-  request_token_time->Set(GetTimeStr(token_status.token_request_time, "n/a"));
-  receive_token_time->Set(GetTimeStr(token_status.token_receive_time, "n/a"));
+  token_request_time->Set(GetTimeStr(token_status.token_request_time, "n/a"));
+  token_response_time->Set(GetTimeStr(token_status.token_response_time, "n/a"));
   std::string err = token_status.last_get_token_error.error_message();
   last_token_request_result->Set(err.empty() ? "OK" : err);
   has_token->Set(token_status.has_token);
diff --git a/components/sync/driver/profile_sync_service.cc b/components/sync/driver/profile_sync_service.cc
index 4ad6d7c..cae97d3 100644
--- a/components/sync/driver/profile_sync_service.cc
+++ b/components/sync/driver/profile_sync_service.cc
@@ -1673,11 +1673,23 @@
 
 CoreAccountInfo ProfileSyncService::GetAuthenticatedAccountInfo() const {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  if (!auth_manager_) {
+    // Some crashes on iOS (crbug.com/962384) suggest that ProfileSyncService
+    // gets called after it has been already shutdown. It's not clear why this
+    // actually happens. We add this null check here to protect against such
+    // crashes.
+    return CoreAccountInfo();
+  }
   return auth_manager_->GetActiveAccountInfo().account_info;
 }
 
 bool ProfileSyncService::IsAuthenticatedAccountPrimary() const {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  if (!auth_manager_) {
+    // This is a precautionary check to be consistent with the check in
+    // GetAuthenticatedAccountInfo().
+    return false;
+  }
   return auth_manager_->GetActiveAccountInfo().is_primary;
 }
 
diff --git a/components/sync/driver/profile_sync_service_unittest.cc b/components/sync/driver/profile_sync_service_unittest.cc
index 05c9c12..9f4ac24 100644
--- a/components/sync/driver/profile_sync_service_unittest.cc
+++ b/components/sync/driver/profile_sync_service_unittest.cc
@@ -561,7 +561,7 @@
   ASSERT_EQ(CONNECTION_NOT_ATTEMPTED, token_status.connection_status);
   ASSERT_TRUE(token_status.connection_status_update_time.is_null());
   ASSERT_FALSE(token_status.token_request_time.is_null());
-  ASSERT_TRUE(token_status.token_receive_time.is_null());
+  ASSERT_TRUE(token_status.token_response_time.is_null());
   ASSERT_FALSE(token_status.has_token);
 
   // The token request will take the form of a posted task.  Run it.
@@ -571,7 +571,7 @@
   token_status = service()->GetSyncTokenStatusForDebugging();
   EXPECT_TRUE(token_status.connection_status_update_time.is_null());
   EXPECT_FALSE(token_status.token_request_time.is_null());
-  EXPECT_FALSE(token_status.token_receive_time.is_null());
+  EXPECT_FALSE(token_status.token_response_time.is_null());
   EXPECT_EQ(GoogleServiceAuthError::AuthErrorNone(),
             token_status.last_get_token_error);
   EXPECT_TRUE(token_status.next_token_request_time.is_null());
@@ -586,7 +586,7 @@
   EXPECT_EQ(CONNECTION_AUTH_ERROR, token_status.connection_status);
   EXPECT_FALSE(token_status.connection_status_update_time.is_null());
   EXPECT_FALSE(token_status.token_request_time.is_null());
-  EXPECT_FALSE(token_status.token_receive_time.is_null());
+  EXPECT_FALSE(token_status.token_response_time.is_null());
   EXPECT_EQ(GoogleServiceAuthError::AuthErrorNone(),
             token_status.last_get_token_error);
   EXPECT_FALSE(token_status.next_token_request_time.is_null());
diff --git a/components/sync/driver/sync_auth_manager.cc b/components/sync/driver/sync_auth_manager.cc
index 72a4a5c..82f23e57 100644
--- a/components/sync/driver/sync_auth_manager.cc
+++ b/components/sync/driver/sync_auth_manager.cc
@@ -83,7 +83,7 @@
   sync_account_ = DetermineAccountToUse();
 }
 
-syncer::SyncAccountInfo SyncAuthManager::GetActiveAccountInfo() const {
+SyncAccountInfo SyncAuthManager::GetActiveAccountInfo() const {
   // Note: |sync_account_| should generally be identical to the result of a
   // DetermineAccountToUse() call, but there are a few edge cases when it isn't:
   // E.g. when another identity observer gets notified before us and calls in
@@ -94,8 +94,7 @@
 
 GoogleServiceAuthError SyncAuthManager::GetLastAuthError() const {
   // TODO(crbug.com/921553): Which error should take precedence?
-  if (partial_token_status_.connection_status ==
-      syncer::CONNECTION_SERVER_ERROR) {
+  if (partial_token_status_.connection_status == CONNECTION_SERVER_ERROR) {
     // TODO(crbug.com/921553): Verify whether CONNECTION_FAILED is really an
     // appropriate auth error here; maybe SERVICE_ERROR would be better? Or
     // maybe we shouldn't expose this case as an auth error at all?
@@ -106,8 +105,7 @@
 
 base::Time SyncAuthManager::GetLastAuthErrorTime() const {
   // See GetLastAuthError().
-  if (partial_token_status_.connection_status ==
-      syncer::CONNECTION_SERVER_ERROR) {
+  if (partial_token_status_.connection_status == CONNECTION_SERVER_ERROR) {
     return partial_token_status_.connection_status_update_time;
   }
   return last_auth_error_time_;
@@ -117,10 +115,10 @@
   return IsWebSignout(GetLastAuthError());
 }
 
-syncer::SyncTokenStatus SyncAuthManager::GetSyncTokenStatus() const {
+SyncTokenStatus SyncAuthManager::GetSyncTokenStatus() const {
   DCHECK(partial_token_status_.next_token_request_time.is_null());
 
-  syncer::SyncTokenStatus token_status = partial_token_status_;
+  SyncTokenStatus token_status = partial_token_status_;
   token_status.has_token = !access_token_.empty();
   if (request_access_token_retry_timer_.IsRunning()) {
     base::TimeDelta delta =
@@ -131,10 +129,10 @@
   return token_status;
 }
 
-syncer::SyncCredentials SyncAuthManager::GetCredentials() const {
+SyncCredentials SyncAuthManager::GetCredentials() const {
   const CoreAccountInfo& account_info = sync_account_.account_info;
 
-  syncer::SyncCredentials credentials;
+  SyncCredentials credentials;
   credentials.account_id = account_info.account_id;
   credentials.email = account_info.email;
   credentials.access_token = access_token_;
@@ -157,7 +155,7 @@
   RequestAccessToken();
 }
 
-void SyncAuthManager::ConnectionStatusChanged(syncer::ConnectionStatus status) {
+void SyncAuthManager::ConnectionStatusChanged(ConnectionStatus status) {
   DCHECK(registered_for_auth_notifications_);
   DCHECK(connection_open_);
 
@@ -165,7 +163,7 @@
   partial_token_status_.connection_status = status;
 
   switch (status) {
-    case syncer::CONNECTION_AUTH_ERROR:
+    case CONNECTION_AUTH_ERROR:
       // Sync server returned error indicating that access token is invalid. It
       // could be either expired or access is revoked. Let's request another
       // access token and if access is revoked then request for token will fail
@@ -202,7 +200,7 @@
         ScheduleAccessTokenRequest();
       }
       break;
-    case syncer::CONNECTION_OK:
+    case CONNECTION_OK:
       // Reset backoff time after successful connection.
       // Request shouldn't be scheduled at this time. But if it is, it's
       // possible that sync flips between OK and auth error states rapidly,
@@ -212,12 +210,12 @@
         request_access_token_backoff_.Reset();
       }
       break;
-    case syncer::CONNECTION_SERVER_ERROR:
+    case CONNECTION_SERVER_ERROR:
       // Note: This case will be exposed as an auth error, due to the
-      // |connection_status| in |partial_token_error_|.
+      // |connection_status| in |partial_token_status_|.
       DCHECK(GetLastAuthError().IsTransientError());
       break;
-    case syncer::CONNECTION_NOT_ATTEMPTED:
+    case CONNECTION_NOT_ATTEMPTED:
       // The connection status should never change to "not attempted".
       NOTREACHED();
       break;
@@ -261,7 +259,7 @@
   DCHECK(registered_for_auth_notifications_);
   DCHECK(connection_open_);
 
-  partial_token_status_ = syncer::SyncTokenStatus();
+  partial_token_status_ = SyncTokenStatus();
   ClearAccessTokenAndRequest();
 
   connection_open_ = false;
@@ -274,8 +272,7 @@
 
 void SyncAuthManager::OnPrimaryAccountCleared(
     const CoreAccountInfo& previous_primary_account_info) {
-  UMA_HISTOGRAM_ENUMERATION("Sync.StopSource", syncer::SIGN_OUT,
-                            syncer::STOP_SOURCE_LIMIT);
+  UMA_HISTOGRAM_ENUMERATION("Sync.StopSource", SIGN_OUT, STOP_SOURCE_LIMIT);
   UpdateSyncAccountIfNecessary();
 }
 
@@ -386,7 +383,7 @@
   request_access_token_backoff_.Reset();
 }
 
-syncer::SyncAccountInfo SyncAuthManager::DetermineAccountToUse() const {
+SyncAccountInfo SyncAuthManager::DetermineAccountToUse() const {
   DCHECK(registered_for_auth_notifications_);
   return syncer::DetermineAccountToUse(
       identity_manager_,
@@ -396,7 +393,7 @@
 bool SyncAuthManager::UpdateSyncAccountIfNecessary() {
   DCHECK(registered_for_auth_notifications_);
 
-  syncer::SyncAccountInfo new_account = DetermineAccountToUse();
+  SyncAccountInfo new_account = DetermineAccountToUse();
   if (new_account.account_info.account_id ==
       sync_account_.account_info.account_id) {
     // We're already using this account (or there was and is no account to use).
@@ -416,10 +413,10 @@
 
   // Sign out of the old account (if any).
   if (!sync_account_.account_info.account_id.empty()) {
-    sync_account_ = syncer::SyncAccountInfo();
+    sync_account_ = SyncAccountInfo();
     // Also clear any pending request or auth errors we might have, since they
     // aren't meaningful anymore.
-    partial_token_status_ = syncer::SyncTokenStatus();
+    partial_token_status_ = SyncTokenStatus();
     ClearAccessTokenAndRequest();
     SetLastAuthError(GoogleServiceAuthError::AuthErrorNone());
     account_state_changed_callback_.Run();
@@ -458,7 +455,7 @@
 
   // Finally, kick off a new access token fetch.
   partial_token_status_.token_request_time = base::Time::Now();
-  partial_token_status_.token_receive_time = base::Time();
+  partial_token_status_.token_response_time = base::Time();
   ongoing_access_token_fetch_ =
       identity_manager_->CreateAccessTokenFetcherForAccount(
           sync_account_.account_info.account_id, kSyncOAuthConsumerName,
@@ -478,6 +475,7 @@
   DCHECK(!request_access_token_retry_timer_.IsRunning());
 
   access_token_ = access_token_info.token;
+  partial_token_status_.token_response_time = base::Time::Now();
   partial_token_status_.last_get_token_error = error;
 
   DCHECK_EQ(access_token_.empty(),
@@ -485,7 +483,6 @@
 
   switch (error.state()) {
     case GoogleServiceAuthError::NONE:
-      partial_token_status_.token_receive_time = base::Time::Now();
       SetLastAuthError(GoogleServiceAuthError::AuthErrorNone());
       break;
     case GoogleServiceAuthError::CONNECTION_FAILED:
diff --git a/components/sync/driver/sync_auth_manager.h b/components/sync/driver/sync_auth_manager.h
index ee7c2d5..3764af6 100644
--- a/components/sync/driver/sync_auth_manager.h
+++ b/components/sync/driver/sync_auth_manager.h
@@ -61,7 +61,7 @@
 
   // Returns the account which should be used when communicating with the Sync
   // server. Note that this account may not be blessed for Sync-the-feature.
-  syncer::SyncAccountInfo GetActiveAccountInfo() const;
+  SyncAccountInfo GetActiveAccountInfo() const;
 
   // Returns the last auth error that was encountered. The error could have come
   // from the Sync server or from the IdentityManager.
@@ -76,13 +76,13 @@
   bool IsSyncPaused() const;
 
   // Returns the credentials to be passed to the SyncEngine.
-  syncer::SyncCredentials GetCredentials() const;
+  SyncCredentials GetCredentials() const;
 
   const std::string& access_token() const { return access_token_; }
 
   // Returns the state of the access token and token request, for display in
   // internals UI.
-  syncer::SyncTokenStatus GetSyncTokenStatus() const;
+  SyncTokenStatus GetSyncTokenStatus() const;
 
   // Called by ProfileSyncService when Sync starts up and will try talking to
   // the server soon. This initiates fetching an access token.
@@ -90,7 +90,7 @@
 
   // Called by ProfileSyncService when the status of the connection to the Sync
   // server changed. Updates auth error state accordingly.
-  void ConnectionStatusChanged(syncer::ConnectionStatus status);
+  void ConnectionStatusChanged(ConnectionStatus status);
 
   // Called by ProfileSyncService when the connection to the Sync server is
   // closed (due to Sync being shut down). Clears all related state (such as
@@ -114,7 +114,7 @@
   void ResetRequestAccessTokenBackoffForTest();
 
  private:
-  syncer::SyncAccountInfo DetermineAccountToUse() const;
+  SyncAccountInfo DetermineAccountToUse() const;
 
   // Updates |sync_account_| to the appropriate account (i.e.
   // DetermineAccountToUse) if necessary, and notifies observers of any changes
@@ -157,7 +157,7 @@
   // The account which we are using to sync. If this is non-empty, that does
   // *not* necessarily imply that Sync is actually running, e.g. because of
   // delayed startup.
-  syncer::SyncAccountInfo sync_account_;
+  SyncAccountInfo sync_account_;
 
   // This is a cache of the last authentication response we received from
   // Chrome's identity/token management system.
@@ -189,7 +189,7 @@
   // Info about the state of our access token, for display in the internals UI.
   // "Partial" because this instance is not fully populated - in particular,
   // |has_token| and |next_token_request_time| get computed on demand.
-  syncer::SyncTokenStatus partial_token_status_;
+  SyncTokenStatus partial_token_status_;
 
   base::WeakPtrFactory<SyncAuthManager> weak_ptr_factory_;
 
diff --git a/components/sync/driver/sync_token_status.h b/components/sync/driver/sync_token_status.h
index a551ae6..0063c46 100644
--- a/components/sync/driver/sync_token_status.h
+++ b/components/sync/driver/sync_token_status.h
@@ -11,7 +11,7 @@
 
 namespace syncer {
 
-// Status of sync server connection, sync token and token request.
+// Status of sync server connection, OAuth2 access token and token request.
 struct SyncTokenStatus {
   SyncTokenStatus();
 
@@ -19,16 +19,18 @@
   base::Time connection_status_update_time;
   ConnectionStatus connection_status = CONNECTION_NOT_ATTEMPTED;
 
-  // The last times when an OAuth2 access token was requested and received.
+  // The last times when an OAuth2 access token was requested, and when a
+  // response was received (whether successful or not).
   base::Time token_request_time;
-  base::Time token_receive_time;
+  base::Time token_response_time;
+
+  // The error we received for the last OAuth2 access token request (or
+  // AuthErrorNone() if it succeeded).
+  GoogleServiceAuthError last_get_token_error;
 
   // Whether we currently have an OAuth2 access token.
   bool has_token = false;
 
-  // The error returned by OAuth2TokenService for the last token request.
-  GoogleServiceAuthError last_get_token_error;
-
   // The time when the next token request is scheduled, or a null time if no
   // request is scheduled.
   base::Time next_token_request_time;
diff --git a/components/sync/nigori/nigori_sync_bridge_impl.cc b/components/sync/nigori/nigori_sync_bridge_impl.cc
index 211fe30..b701b47 100644
--- a/components/sync/nigori/nigori_sync_bridge_impl.cc
+++ b/components/sync/nigori/nigori_sync_bridge_impl.cc
@@ -19,6 +19,8 @@
 
 namespace {
 
+using sync_pb::NigoriSpecifics;
+
 // Attempts to decrypt |keystore_decryptor_token| with |keystore_keys|. Returns
 // serialized Nigori key if successful and base::nullopt otherwise.
 base::Optional<std::string> DecryptKeystoreDecryptor(
@@ -63,7 +65,7 @@
 // 4. keybag_is_frozen = true.
 // 5. keystore_migration_time is current time.
 // 6. Other fields are default.
-base::Optional<sync_pb::NigoriSpecifics> MakeDefaultKeystoreNigori(
+base::Optional<NigoriSpecifics> MakeDefaultKeystoreNigori(
     const std::vector<std::string>& keystore_keys,
     Encryptor* encryptor) {
   DCHECK(encryptor);
@@ -79,7 +81,7 @@
       return base::nullopt;
     }
   }
-  sync_pb::NigoriSpecifics specifics;
+  NigoriSpecifics specifics;
   if (!cryptographer.EncryptString(
           cryptographer.GetDefaultNigoriKeyData(),
           specifics.mutable_keystore_decryptor_token())) {
@@ -90,20 +92,133 @@
     DLOG(ERROR) << "Failed to encrypt keystore keys into encryption_keybag.";
     return base::nullopt;
   }
-  specifics.set_passphrase_type(
-      EnumPassphraseTypeToProto(PassphraseType::KEYSTORE_PASSPHRASE));
+  specifics.set_passphrase_type(NigoriSpecifics::KEYSTORE_PASSPHRASE);
   // Let non-USS client know, that Nigori doesn't need migration.
   specifics.set_keybag_is_frozen(true);
   specifics.set_keystore_migration_time(TimeToProtoTime(base::Time::Now()));
   return specifics;
 }
 
+// Validates given |specifics| assuming it's not specifics received from the
+// server during first-time sync for current user (i.e. it's not a default
+// specifics).
+bool IsValidNigoriSpecifics(const NigoriSpecifics& specifics) {
+  if (specifics.encryption_keybag().blob().empty()) {
+    DLOG(ERROR) << "Specifics contains empty encryption_keybag.";
+    return false;
+  }
+  if (!specifics.has_passphrase_type()) {
+    DLOG(ERROR) << "Specifics has no passphrase_type.";
+    return false;
+  }
+  switch (specifics.passphrase_type()) {
+    case NigoriSpecifics::UNKNOWN:
+      DLOG(ERROR) << "Received UNKNOWN passphrase_type";
+      return false;
+    case NigoriSpecifics::IMPLICIT_PASSPHRASE:
+      // TODO(crbug.com/922900): we hope that IMPLICIT_PASSPHRASE support is not
+      // needed in new implementation. In case we need to continue migration to
+      // keystore we will need to support it.
+      // Note: in case passphrase_type is not set, we also end up here, because
+      // IMPLICIT_PASSPHRASE is a default value.
+      DLOG(ERROR) << "IMPLICIT_PASSPHRASE is not supported.";
+      return false;
+    case NigoriSpecifics::KEYSTORE_PASSPHRASE:
+      if (specifics.keystore_decryptor_token().blob().empty()) {
+        DLOG(ERROR) << "Keystore Nigori should have filled "
+                    << "keystore_decryptor_token.";
+        return false;
+      }
+      break;
+    case NigoriSpecifics::FROZEN_IMPLICIT_PASSPHRASE:
+    case NigoriSpecifics::CUSTOM_PASSPHRASE:
+      if (!specifics.encrypt_everything()) {
+        DLOG(ERROR) << "Nigori with explicit passphrase type should have "
+                       "enabled encrypt_everything.";
+        return false;
+      }
+  }
+  return true;
+}
+
+bool IsValidPassphraseTransition(
+    NigoriSpecifics::PassphraseType old_passphrase_type,
+    NigoriSpecifics::PassphraseType new_passphrase_type) {
+  // We never allow setting IMPLICIT_PASSPHRASE as current passphrase type.
+  DCHECK_NE(old_passphrase_type, NigoriSpecifics::IMPLICIT_PASSPHRASE);
+  // We assume that |new_passphrase_type| is valid.
+  DCHECK_NE(new_passphrase_type, NigoriSpecifics::UNKNOWN);
+  DCHECK_NE(new_passphrase_type, NigoriSpecifics::IMPLICIT_PASSPHRASE);
+
+  if (old_passphrase_type == new_passphrase_type) {
+    return true;
+  }
+  switch (old_passphrase_type) {
+    case NigoriSpecifics::UNKNOWN:
+      // This can happen iff we have not synced local state yet, so we accept
+      // any valid passphrase type (invalid filtered before).
+      return true;
+    case NigoriSpecifics::IMPLICIT_PASSPHRASE:
+      NOTREACHED();
+      return false;
+    case NigoriSpecifics::KEYSTORE_PASSPHRASE:
+      return new_passphrase_type == NigoriSpecifics::CUSTOM_PASSPHRASE;
+    case NigoriSpecifics::FROZEN_IMPLICIT_PASSPHRASE:
+      // There is no client side code which can cause such transition, but
+      // technically it's a valid one and can be implemented in the future.
+      return new_passphrase_type == NigoriSpecifics::CUSTOM_PASSPHRASE;
+    case NigoriSpecifics::CUSTOM_PASSPHRASE:
+      return false;
+  }
+  NOTREACHED();
+  return false;
+}
+
+// Updates |*current_type| if needed. Returns true if its value was changed.
+bool UpdatePassphraseType(NigoriSpecifics::PassphraseType new_type,
+                          NigoriSpecifics::PassphraseType* current_type) {
+  DCHECK(current_type);
+  DCHECK(IsValidPassphraseTransition(*current_type, new_type));
+  if (*current_type == new_type) {
+    return false;
+  }
+  *current_type = new_type;
+  return true;
+}
+
+bool IsValidEncryptedTypesTransition(bool old_encrypt_everything,
+                                     const NigoriSpecifics& specifics) {
+  // We don't support relaxing the encryption requirements.
+  // TODO(crbug.com/922900): more logic is to be added here, once we support
+  // enforced encryption for individual datatypes.
+  return specifics.encrypt_everything() || !old_encrypt_everything;
+}
+
+// Updates |*current_encrypt_everything| if needed. Returns true if its value
+// was changed.
+bool UpdateEncryptedTypes(const NigoriSpecifics& specifics,
+                          bool* current_encrypt_everything) {
+  DCHECK(current_encrypt_everything);
+  DCHECK(
+      IsValidEncryptedTypesTransition(*current_encrypt_everything, specifics));
+  // TODO(crbug.com/922900): more logic is to be added here, once we support
+  // enforced encryption for individual datatypes.
+  if (*current_encrypt_everything == specifics.encrypt_everything()) {
+    return false;
+  }
+  *current_encrypt_everything = specifics.encrypt_everything();
+  return true;
+}
+
 }  // namespace
 
 NigoriSyncBridgeImpl::NigoriSyncBridgeImpl(
     std::unique_ptr<NigoriLocalChangeProcessor> processor,
     Encryptor* encryptor)
-    : processor_(std::move(processor)), cryptographer_(encryptor) {}
+    : processor_(std::move(processor)),
+      cryptographer_(encryptor),
+      passphrase_type_(NigoriSpecifics::UNKNOWN),
+      encrypt_everything_(false) {}
 
 NigoriSyncBridgeImpl::~NigoriSyncBridgeImpl() {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
@@ -111,17 +226,20 @@
 
 void NigoriSyncBridgeImpl::AddObserver(Observer* observer) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  NOTIMPLEMENTED();
+  observers_.AddObserver(observer);
 }
 
 void NigoriSyncBridgeImpl::RemoveObserver(Observer* observer) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  NOTIMPLEMENTED();
+  observers_.RemoveObserver(observer);
 }
 
 void NigoriSyncBridgeImpl::Init() {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   NOTIMPLEMENTED();
+  // TODO(crbug.com/922900): notify observers about cryptographer change in
+  // case UpdateLocalState() is not called in this function (i.e.
+  // initialization implemented in constructor).
 }
 
 void NigoriSyncBridgeImpl::SetEncryptionPassphrase(
@@ -221,7 +339,7 @@
   }
   // We received uninitialized Nigori and need to initialize it as default
   // keystore Nigori.
-  base::Optional<sync_pb::NigoriSpecifics> initialized_specifics =
+  base::Optional<NigoriSpecifics> initialized_specifics =
       MakeDefaultKeystoreNigori(keystore_keys_, cryptographer_.encryptor());
   if (!initialized_specifics) {
     return ModelError(FROM_HERE, "Failed to initialize keystore Nigori.");
@@ -234,6 +352,7 @@
 base::Optional<ModelError> NigoriSyncBridgeImpl::ApplySyncChanges(
     base::Optional<EntityData> data) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  DCHECK_NE(passphrase_type_, NigoriSpecifics::UNKNOWN);
   if (!data) {
     // TODO(crbug.com/922900): persist SyncMetadata and ModelTypeState.
     NOTIMPLEMENTED();
@@ -244,31 +363,98 @@
 }
 
 base::Optional<ModelError> NigoriSyncBridgeImpl::UpdateLocalState(
-    const sync_pb::NigoriSpecifics& specifics) {
-  if (ProtoPassphraseTypeToEnum(specifics.passphrase_type()) !=
-      PassphraseType::KEYSTORE_PASSPHRASE) {
-    // TODO(crbug.com/922900): support other passphrase types.
-    NOTIMPLEMENTED();
-    return ModelError(FROM_HERE,
-                      "Only keystore Nigori node is currently supported.");
+    const NigoriSpecifics& specifics) {
+  if (!IsValidNigoriSpecifics(specifics)) {
+    return ModelError(FROM_HERE, "NigoriSpecifics is not valid.");
   }
+  if (!IsValidPassphraseTransition(
+          /*old_passphrase_type=*/passphrase_type_,
+          /*new_paspshrase_type=*/specifics.passphrase_type())) {
+    return ModelError(FROM_HERE, "Invalid passphrase type transition.");
+  }
+  if (!IsValidEncryptedTypesTransition(encrypt_everything_, specifics)) {
+    return ModelError(FROM_HERE, "Invalid encrypted types transition.");
+  }
+
+  DCHECK(specifics.has_passphrase_type());
+  const bool passphrase_type_changed =
+      UpdatePassphraseType(specifics.passphrase_type(), &passphrase_type_);
+
+  const bool encrypted_types_changed =
+      UpdateEncryptedTypes(specifics, &encrypt_everything_);
+
+  if (specifics.has_custom_passphrase_time()) {
+    custom_passphrase_time_ =
+        ProtoTimeToTime(specifics.custom_passphrase_time());
+  }
+  if (specifics.has_keystore_migration_time()) {
+    keystore_migration_time_ =
+        ProtoTimeToTime(specifics.keystore_migration_time());
+  }
+
   DCHECK(!keystore_keys_.empty());
-  // TODO(crbug.com/922900): sanitize remote update (e.g. ensure, that
-  // encryption_keybag() and keystore_decryptor_token() aren't empty).
-  const sync_pb::EncryptedData& keybag = specifics.encryption_keybag();
-  if (cryptographer_.CanDecrypt(keybag)) {
-    // We need to ensure, that |cryptographer_| has all keys.
-    cryptographer_.InstallKeys(keybag);
+  const sync_pb::EncryptedData& encryption_keybag =
+      specifics.encryption_keybag();
+  switch (passphrase_type_) {
+    case NigoriSpecifics::UNKNOWN:
+    case NigoriSpecifics::IMPLICIT_PASSPHRASE:
+      // NigoriSpecifics with UNKNOWN or IMPLICIT_PASSPHRASE type is not valid
+      // and shouldn't reach this codepath. We just updated |passphrase_type_|
+      // from specifics, so it can't be in these states as well.
+      NOTREACHED();
+      break;
+    case NigoriSpecifics::KEYSTORE_PASSPHRASE: {
+      base::Optional<ModelError> error = UpdateCryptographerFromKeystoreNigori(
+          encryption_keybag, specifics.keystore_decryptor_token());
+      if (error) {
+        return error;
+      }
+      break;
+    }
+    case NigoriSpecifics::FROZEN_IMPLICIT_PASSPHRASE:
+    case NigoriSpecifics::CUSTOM_PASSPHRASE:
+      UpdateCryptographerFromExplicitPassphraseNigori(encryption_keybag);
+  }
+
+  if (passphrase_type_changed) {
+    for (auto& observer : observers_) {
+      observer.OnPassphraseTypeChanged(
+          ProtoPassphraseTypeToEnum(passphrase_type_),
+          GetExplicitPassphraseTime());
+    }
+  }
+  if (encrypted_types_changed) {
+    // Currently the only way to change encrypted types is to enable
+    // encrypt_everything.
+    DCHECK(encrypt_everything_);
+    for (auto& observer : observers_) {
+      observer.OnEncryptedTypesChanged(EncryptableUserTypes(),
+                                       encrypt_everything_);
+    }
+  }
+  for (auto& observer : observers_) {
+    observer.OnCryptographerStateChanged(&cryptographer_);
+  }
+  return base::nullopt;
+}
+
+base::Optional<ModelError>
+NigoriSyncBridgeImpl::UpdateCryptographerFromKeystoreNigori(
+    const sync_pb::EncryptedData& encryption_keybag,
+    const sync_pb::EncryptedData& keystore_decryptor_token) {
+  DCHECK(!encryption_keybag.blob().empty());
+  DCHECK(!keystore_decryptor_token.blob().empty());
+  if (cryptographer_.CanDecrypt(encryption_keybag)) {
+    cryptographer_.InstallKeys(encryption_keybag);
     return base::nullopt;
   }
-  // We weren't able to decrypt the keybag with current |cryptographer_| state,
-  // but we still can decrypt it with |keystore_keys_|. Note: it's a normal
-  // situation, once we perform initial sync or key rotation was performed by
-  // another client.
-  cryptographer_.SetPendingKeys(keybag);
+  // We weren't able to decrypt the keybag with current |cryptographer_|
+  // state, but we still can decrypt it with |keystore_keys_|. Note: it's a
+  // normal situation, once we perform initial sync or key rotation was
+  // performed by another client.
+  cryptographer_.SetPendingKeys(encryption_keybag);
   base::Optional<std::string> serialized_keystore_decryptor =
-      DecryptKeystoreDecryptor(keystore_keys_,
-                               specifics.keystore_decryptor_token(),
+      DecryptKeystoreDecryptor(keystore_keys_, keystore_decryptor_token,
                                cryptographer_.encryptor());
   if (!serialized_keystore_decryptor ||
       !cryptographer_.ImportNigoriKey(*serialized_keystore_decryptor) ||
@@ -280,6 +466,14 @@
   return base::nullopt;
 }
 
+void NigoriSyncBridgeImpl::UpdateCryptographerFromExplicitPassphraseNigori(
+    const sync_pb::EncryptedData& keybag) {
+  // TODO(crbug.com/922900): support the case when client knows passphrase.
+  NOTIMPLEMENTED();
+  DCHECK(!keybag.blob().empty());
+  cryptographer_.SetPendingKeys(keybag);
+}
+
 std::unique_ptr<EntityData> NigoriSyncBridgeImpl::GetData() {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   NOTIMPLEMENTED();
@@ -303,4 +497,23 @@
   return cryptographer_;
 }
 
+base::Time NigoriSyncBridgeImpl::GetExplicitPassphraseTime() const {
+  switch (passphrase_type_) {
+    case NigoriSpecifics::IMPLICIT_PASSPHRASE:
+      // IMPLICIT_PASSPHRASE type isn't supported and should be never set as
+      // |passphrase_type_|;
+      NOTREACHED();
+      return base::Time();
+    case NigoriSpecifics::UNKNOWN:
+    case NigoriSpecifics::KEYSTORE_PASSPHRASE:
+      return base::Time();
+    case NigoriSpecifics::FROZEN_IMPLICIT_PASSPHRASE:
+      return keystore_migration_time_;
+    case NigoriSpecifics::CUSTOM_PASSPHRASE:
+      return custom_passphrase_time_;
+  }
+  NOTREACHED();
+  return custom_passphrase_time_;
+}
+
 }  // namespace syncer
diff --git a/components/sync/nigori/nigori_sync_bridge_impl.h b/components/sync/nigori/nigori_sync_bridge_impl.h
index 5d4f2d11..f5a7998 100644
--- a/components/sync/nigori/nigori_sync_bridge_impl.h
+++ b/components/sync/nigori/nigori_sync_bridge_impl.h
@@ -10,6 +10,7 @@
 #include <vector>
 
 #include "base/macros.h"
+#include "base/observer_list.h"
 #include "base/optional.h"
 #include "base/sequence_checker.h"
 #include "base/time/time.h"
@@ -73,12 +74,18 @@
   const Cryptographer& GetCryptographerForTesting() const;
 
  private:
-  // Updates state of |cryptographer_| according to given |specifics|.
-  // TODO(crbug.com/922900): update the comment above once more fields are
-  // supported.
   base::Optional<ModelError> UpdateLocalState(
       const sync_pb::NigoriSpecifics& specifics);
 
+  base::Optional<ModelError> UpdateCryptographerFromKeystoreNigori(
+      const sync_pb::EncryptedData& encryption_keybag,
+      const sync_pb::EncryptedData& keystore_decryptor_token);
+
+  void UpdateCryptographerFromExplicitPassphraseNigori(
+      const sync_pb::EncryptedData& keybag);
+
+  base::Time GetExplicitPassphraseTime() const;
+
   const std::unique_ptr<NigoriLocalChangeProcessor> processor_;
 
   // Base64 encoded keystore keys. The last element is the current keystore
@@ -87,6 +94,15 @@
   std::vector<std::string> keystore_keys_;
 
   Cryptographer cryptographer_;
+  sync_pb::NigoriSpecifics::PassphraseType passphrase_type_;
+  bool encrypt_everything_;
+  base::Time custom_passphrase_time_;
+  base::Time keystore_migration_time_;
+
+  // TODO(crbug/922900): consider using checked ObserverList once
+  // SyncEncryptionHandlerImpl is no longer needed or consider refactoring old
+  // implementation to use checked ObserverList as well.
+  base::ObserverList<SyncEncryptionHandler::Observer>::Unchecked observers_;
 
   SEQUENCE_CHECKER(sequence_checker_);
 
diff --git a/components/sync/nigori/nigori_sync_bridge_impl_unittest.cc b/components/sync/nigori/nigori_sync_bridge_impl_unittest.cc
index 16f34ff..1f17ecc 100644
--- a/components/sync/nigori/nigori_sync_bridge_impl_unittest.cc
+++ b/components/sync/nigori/nigori_sync_bridge_impl_unittest.cc
@@ -8,6 +8,7 @@
 
 #include "base/base64.h"
 #include "components/sync/base/fake_encryptor.h"
+#include "components/sync/base/time.h"
 #include "components/sync/model/entity_data.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -16,10 +17,44 @@
 
 namespace {
 
+using testing::_;
 using testing::Eq;
+using testing::Ne;
+using testing::NotNull;
 
 const char kNigoriKeyName[] = "nigori-key";
 
+MATCHER(NotNullTime, "") {
+  return !arg.is_null();
+}
+
+MATCHER_P(HasDefaultKeyDerivedFrom, key_params, "") {
+  const Cryptographer& cryptographer = arg;
+  Nigori expected_default_nigori;
+  expected_default_nigori.InitByDerivation(key_params.derivation_params,
+                                           key_params.password);
+  std::string expected_default_key_name;
+  EXPECT_TRUE(expected_default_nigori.Permute(
+      Nigori::Type::Password, kNigoriKeyName, &expected_default_key_name));
+  return cryptographer.GetDefaultNigoriKeyName() == expected_default_key_name;
+}
+
+MATCHER(HasKeystoreNigori, "") {
+  const std::unique_ptr<EntityData>& entity_data = arg;
+  if (!entity_data || !entity_data->specifics.has_nigori()) {
+    return false;
+  }
+  const sync_pb::NigoriSpecifics& specifics = entity_data->specifics.nigori();
+  if (specifics.passphrase_type() !=
+      sync_pb::NigoriSpecifics::KEYSTORE_PASSPHRASE) {
+    return false;
+  }
+  return !specifics.encryption_keybag().blob().empty() &&
+         !specifics.keystore_decryptor_token().blob().empty() &&
+         specifics.keybag_is_frozen() &&
+         specifics.has_keystore_migration_time();
+}
+
 KeyParams Pbkdf2KeyParams(std::string key) {
   return {KeyDerivationParams::CreateForPbkdf2(), std::move(key)};
 }
@@ -45,6 +80,26 @@
                base::WeakPtr<ModelTypeControllerDelegate>());
 };
 
+class MockObserver : public SyncEncryptionHandler::Observer {
+ public:
+  MockObserver() = default;
+  ~MockObserver() = default;
+
+  MOCK_METHOD3(OnPassphraseRequired,
+               void(PassphraseRequiredReason,
+                    const KeyDerivationParams&,
+                    const sync_pb::EncryptedData&));
+  MOCK_METHOD0(OnPassphraseAccepted, void());
+  MOCK_METHOD2(OnBootstrapTokenUpdated,
+               void(const std::string&, BootstrapTokenType type));
+  MOCK_METHOD2(OnEncryptedTypesChanged, void(ModelTypeSet, bool));
+  MOCK_METHOD0(OnEncryptionComplete, void());
+  MOCK_METHOD1(OnCryptographerStateChanged, void(Cryptographer*));
+  MOCK_METHOD2(OnPassphraseTypeChanged, void(PassphraseType, base::Time));
+  MOCK_METHOD1(OnLocalSetPassphraseEncryption,
+               void(const SyncEncryptionHandler::NigoriState&));
+};
+
 class NigoriSyncBridgeImplTest : public testing::Test {
  protected:
   NigoriSyncBridgeImplTest() {
@@ -52,10 +107,14 @@
     processor_ = processor.get();
     bridge_ = std::make_unique<NigoriSyncBridgeImpl>(std::move(processor),
                                                      &encryptor_);
+    bridge_->AddObserver(&observer_);
   }
 
+  ~NigoriSyncBridgeImplTest() override { bridge_->RemoveObserver(&observer_); }
+
   NigoriSyncBridgeImpl* bridge() { return bridge_.get(); }
   MockNigoriLocalChangeProcessor* processor() { return processor_; }
+  MockObserver* observer() { return &observer_; }
 
   // Builds NigoriSpecifics with following fields:
   // 1. encryption_keybag contains all keys derived from |keybag_keys_params|
@@ -71,8 +130,7 @@
       const std::vector<KeyParams>& keybag_keys_params,
       const KeyParams& keystore_decryptor_params,
       const KeyParams& keystore_key_params) {
-    sync_pb::NigoriSpecifics specifics =
-        sync_pb::NigoriSpecifics::default_instance();
+    sync_pb::NigoriSpecifics specifics;
 
     Cryptographer cryptographer(&encryptor_);
     cryptographer.AddKey(keystore_decryptor_params);
@@ -94,12 +152,28 @@
     return specifics;
   }
 
+  sync_pb::NigoriSpecifics BuildCustomPassphraseNigoriSpecifics(
+      const KeyParams& key_params) {
+    sync_pb::NigoriSpecifics specifics;
+
+    Cryptographer cryptographer(&encryptor_);
+    cryptographer.AddKey(key_params);
+    EXPECT_TRUE(cryptographer.GetKeys(specifics.mutable_encryption_keybag()));
+
+    specifics.set_custom_passphrase_time(TimeToProtoTime(base::Time::Now()));
+    specifics.set_passphrase_type(sync_pb::NigoriSpecifics::CUSTOM_PASSPHRASE);
+    specifics.set_encrypt_everything(true);
+
+    return specifics;
+  }
+
  private:
   // Don't change the order. |encryptor_| should outlive |bridge_|.
   FakeEncryptor encryptor_;
   std::unique_ptr<NigoriSyncBridgeImpl> bridge_;
   // Ownership transferred to |bridge_|.
   MockNigoriLocalChangeProcessor* processor_;
+  testing::NiceMock<MockObserver> observer_;
 };
 
 MATCHER_P(CanDecryptWith, key_params, "") {
@@ -124,39 +198,13 @@
   return decrypted == unencrypted;
 }
 
-MATCHER_P(HasDefaultKeyDerivedFrom, key_params, "") {
-  const Cryptographer& cryptographer = arg;
-  Nigori expected_default_nigori;
-  expected_default_nigori.InitByDerivation(key_params.derivation_params,
-                                           key_params.password);
-  std::string expected_default_key_name;
-  EXPECT_TRUE(expected_default_nigori.Permute(
-      Nigori::Type::Password, kNigoriKeyName, &expected_default_key_name));
-  return cryptographer.GetDefaultNigoriKeyName() == expected_default_key_name;
-}
-
-MATCHER(HasKeystoreNigori, "") {
-  const std::unique_ptr<EntityData>& entity_data = arg;
-  if (!entity_data || !entity_data->specifics.has_nigori()) {
-    return false;
-  }
-  const sync_pb::NigoriSpecifics& specifics = entity_data->specifics.nigori();
-  if (ProtoPassphraseTypeToEnum(specifics.passphrase_type()) !=
-      PassphraseType::KEYSTORE_PASSPHRASE) {
-    return false;
-  }
-  return !specifics.encryption_keybag().blob().empty() &&
-         !specifics.keystore_decryptor_token().blob().empty() &&
-         specifics.keybag_is_frozen() &&
-         specifics.has_keystore_migration_time();
-}
-
 // Simplest case of keystore Nigori: we have only one keystore key and no old
 // keys. This keystore key is encrypted in both encryption_keybag and
 // keystore_decryptor_token. Client receives such Nigori if initialization of
 // Nigori node was done after keystore was introduced and no key rotations
 // happened.
-TEST_F(NigoriSyncBridgeImplTest, ShouldAcceptKeysFromKeystoreNigori) {
+TEST_F(NigoriSyncBridgeImplTest,
+       ShouldAcceptKeysFromKeystoreNigoriAndNotifyObservers) {
   const std::string kRawKeystoreKey = "raw_keystore_key";
   const KeyParams kKeystoreKeyParams = KeystoreKeyParams(kRawKeystoreKey);
   EntityData entity_data;
@@ -166,6 +214,8 @@
       /*keystore_key_params=*/kKeystoreKeyParams);
 
   EXPECT_TRUE(bridge()->SetKeystoreKeys({kRawKeystoreKey}));
+
+  EXPECT_CALL(*observer(), OnCryptographerStateChanged(NotNull()));
   EXPECT_THAT(bridge()->MergeSyncData(std::move(entity_data)),
               Eq(base::nullopt));
 
@@ -277,6 +327,80 @@
   // KeystorePassphrase once passphrase type support is implemented.
 }
 
+// Tests that we can perform initial sync with custom passphrase Nigori.
+// We should notify observers about encryption state changes and cryptographer
+// shouldn't be ready (by having pending keys) until user provides the
+// passphrase.
+TEST_F(NigoriSyncBridgeImplTest,
+       ShouldNotifyWhenSyncedWithCustomPassphraseNigori) {
+  EntityData entity_data;
+  *entity_data.specifics.mutable_nigori() =
+      BuildCustomPassphraseNigoriSpecifics(Pbkdf2KeyParams("passphrase"));
+
+  ASSERT_TRUE(bridge()->SetKeystoreKeys({"keystore_key"}));
+
+  EXPECT_CALL(*observer(), OnEncryptedTypesChanged(
+                               /*encrypted_types=*/EncryptableUserTypes(),
+                               /*encrypt_everything=*/true));
+  EXPECT_CALL(*observer(), OnCryptographerStateChanged(NotNull()));
+  EXPECT_CALL(*observer(),
+              OnPassphraseTypeChanged(PassphraseType::CUSTOM_PASSPHRASE,
+                                      NotNullTime()));
+  EXPECT_THAT(bridge()->MergeSyncData(std::move(entity_data)),
+              Eq(base::nullopt));
+  EXPECT_TRUE(bridge()->GetCryptographerForTesting().has_pending_keys());
+}
+
+// Tests that we can process remote update with custom passphrase Nigori, while
+// we already have keystore Nigori locally.
+// We should notify observers about encryption state changes and cryptographer
+// shouldn't be ready (by having pending keys) until user provides the
+// passphrase.
+TEST_F(NigoriSyncBridgeImplTest, ShouldTransitToCustomPassphrase) {
+  EntityData default_entity_data;
+  *default_entity_data.specifics.mutable_nigori() =
+      sync_pb::NigoriSpecifics::default_instance();
+
+  ASSERT_TRUE(bridge()->SetKeystoreKeys({"keystore_key"}));
+  // Note: passing default Nigori to MergeSyncData() leads to instantiation of
+  // keystore Nigori.
+  ASSERT_THAT(bridge()->MergeSyncData(std::move(default_entity_data)),
+              Eq(base::nullopt));
+
+  EntityData new_entity_data;
+  *new_entity_data.specifics.mutable_nigori() =
+      BuildCustomPassphraseNigoriSpecifics(Pbkdf2KeyParams("passphrase"));
+
+  EXPECT_CALL(*observer(), OnEncryptedTypesChanged(
+                               /*encrypted_types=*/EncryptableUserTypes(),
+                               /*encrypt_everything=*/true));
+  EXPECT_CALL(*observer(), OnCryptographerStateChanged(NotNull()));
+  EXPECT_CALL(*observer(),
+              OnPassphraseTypeChanged(PassphraseType::CUSTOM_PASSPHRASE,
+                                      NotNullTime()));
+  EXPECT_THAT(bridge()->ApplySyncChanges(std::move(new_entity_data)),
+              Eq(base::nullopt));
+  EXPECT_TRUE(bridge()->GetCryptographerForTesting().has_pending_keys());
+}
+
+// Tests that we don't try to overwrite default passphrase type and report
+// ModelError unless we received default Nigori node (which is determined by
+// the size of encryption_keybag). It's a requirement because receiving default
+// passphrase type might mean that some newer client switched to the new
+// passphrase type.
+TEST_F(NigoriSyncBridgeImplTest, ShouldFailOnUnknownPassprase) {
+  EntityData entity_data;
+  *entity_data.specifics.mutable_nigori() =
+      sync_pb::NigoriSpecifics::default_instance();
+  entity_data.specifics.mutable_nigori()->mutable_encryption_keybag()->set_blob(
+      "data");
+  ASSERT_TRUE(bridge()->SetKeystoreKeys({"keystore_key"}));
+
+  EXPECT_CALL(*processor(), Put(_)).Times(0);
+  EXPECT_THAT(bridge()->MergeSyncData(std::move(entity_data)),
+              Ne(base::nullopt));
+}
+
 }  // namespace
 
 }  // namespace syncer
diff --git a/components/sync/protocol/send_tab_to_self_specifics.proto b/components/sync/protocol/send_tab_to_self_specifics.proto
index 013b906..2aad0d8 100644
--- a/components/sync/protocol/send_tab_to_self_specifics.proto
+++ b/components/sync/protocol/send_tab_to_self_specifics.proto
@@ -17,23 +17,22 @@
 // a url across devices.
 message SendTabToSelfSpecifics {
   // A random unique identifier for each shared tab.
-  // Required
+  // Required.
   optional string guid = 5;
   // The name of the tab being shared.
   optional string title = 1;
   // The URL of the tab being shared.
-  // Required
+  // Required.
   optional string url = 2;
   // The time the tab was shared as measured by the client in microseconds since
   // the windows epoch.
-  // Required
   optional int64 shared_time_usec = 3;
   // The time the tab was navigated to as measured by the client in microseconds
   // since the windows epoch.
-  // Required
   optional int64 navigation_time_usec = 6;
   // A non-unique but human readable name to describe this client, used in UI.
   optional string device_name = 4;
   // The stable Device_id of the device that this tab was shared with.
+  // Required.
   optional string target_device_sync_cache_guid = 7;
 }
diff --git a/content/browser/loader/prefetched_signed_exchange_cache.cc b/content/browser/loader/prefetched_signed_exchange_cache.cc
index 472f18e..e5256bf 100644
--- a/content/browser/loader/prefetched_signed_exchange_cache.cc
+++ b/content/browser/loader/prefetched_signed_exchange_cache.cc
@@ -119,7 +119,8 @@
       const network::ResourceResponseHead& inner_response,
       std::unique_ptr<const storage::BlobDataHandle> blob_data_handle,
       const network::URLLoaderCompletionStatus& completion_status,
-      network::mojom::URLLoaderClientPtr client)
+      network::mojom::URLLoaderClientPtr client,
+      bool is_navigation_request)
       : blob_data_handle_(std::move(blob_data_handle)),
         completion_status_(completion_status),
         client_(std::move(client)),
@@ -128,10 +129,14 @@
     UpdateRequestResponseStartTime(&response);
     response.encoded_data_length = 0;
     client_->OnReceiveResponse(response);
-    // When Network Service is not enabled, we need to wait ProceedWithResponse.
+    // When Network Service is not enabled, we need to wait ProceedWithResponse
+    // for navigation request.
     // See https://crbug.com/791049.
-    if (base::FeatureList::IsEnabled(network::features::kNetworkService))
-      SendResponseBody();
+    if (is_navigation_request &&
+        !base::FeatureList::IsEnabled(network::features::kNetworkService)) {
+      return;
+    }
+    SendResponseBody();
   }
   ~InnerResponseURLLoader() override {}
 
@@ -235,13 +240,13 @@
                                 traffic_annotation) override {
     // TODO(crbug.com/935267): Implement CORS check.
     DCHECK_EQ(request.url, entry_->inner_url());
-    mojo::MakeStrongBinding(
-        std::make_unique<InnerResponseURLLoader>(
-            *entry_->inner_response(),
-            std::make_unique<const storage::BlobDataHandle>(
-                *entry_->blob_data_handle()),
-            *entry_->completion_status(), std::move(client)),
-        std::move(loader));
+    mojo::MakeStrongBinding(std::make_unique<InnerResponseURLLoader>(
+                                *entry_->inner_response(),
+                                std::make_unique<const storage::BlobDataHandle>(
+                                    *entry_->blob_data_handle()),
+                                *entry_->completion_status(), std::move(client),
+                                false /* is_navigation_request */),
+                            std::move(loader));
   }
   void Clone(network::mojom::URLLoaderFactoryRequest request) override {
     bindings_.AddBinding(this, std::move(request));
@@ -338,7 +343,8 @@
             *exchange_->inner_response(),
             std::make_unique<const storage::BlobDataHandle>(
                 *exchange_->blob_data_handle()),
-            *exchange_->completion_status(), std::move(client)),
+            *exchange_->completion_status(), std::move(client),
+            true /* is_navigation_request */),
         std::move(request));
   }
 
diff --git a/content/browser/resources/media/stats_rates_calculator.js b/content/browser/resources/media/stats_rates_calculator.js
index 2d85eee..36cff04 100644
--- a/content/browser/resources/media/stats_rates_calculator.js
+++ b/content/browser/resources/media/stats_rates_calculator.js
@@ -189,7 +189,37 @@
         names: [
           ['bytesSent', 'timestamp'],
           ['packetsSent', 'timestamp'],
+          [
+            'totalPacketSendDelay',
+            'packetsSent',
+            '[totalPacketSendDelay/packetsSent_in_ms]',
+            (value) => {
+              return value * 1000;  // s -> ms
+            },
+          ],
           ['framesEncoded', 'timestamp'],
+          [
+            'totalEncodedBytesTarget', 'framesEncoded',
+            '[targetEncodedBytes/s]',
+            (value, currentStats, previousStats) => {
+              if (!previousStats) {
+                return 0;
+              }
+              const deltaTime =
+                  currentStats.timestamp - previousStats.timestamp;
+              const deltaFrames =
+                  currentStats.framesEncoded - previousStats.framesEncoded;
+              const encodedFrameRate = deltaFrames / deltaTime;
+              return value * encodedFrameRate;
+            }
+          ],
+          [
+            'totalEncodeTime', 'framesEncoded',
+            '[totalEncodeTime/framesEncoded_in_ms]',
+            (value) => {
+              return value * 1000;  // s -> ms
+            }
+          ],
           ['qpSum', 'framesEncoded'],
         ],
       },
@@ -252,7 +282,8 @@
                     let result = this.calculateAccumulativeMetricOverSamples_(
                         stats.id, accumulativeMetric, samplesMetric);
                     if (result && transformation) {
-                      result = transformation(result);
+                      const previousStats = this.previousReport.get(stats.id);
+                      result = transformation(result, stats, previousStats);
                     }
                     this.currentReport.setCalculatedMetric(
                         stats.id, accumulativeMetric, resultName, result);
diff --git a/content/browser/web_package/signed_exchange_subresource_prefetch_browsertest.cc b/content/browser/web_package/signed_exchange_subresource_prefetch_browsertest.cc
index 79e284b..579c44d 100644
--- a/content/browser/web_package/signed_exchange_subresource_prefetch_browsertest.cc
+++ b/content/browser/web_package/signed_exchange_subresource_prefetch_browsertest.cc
@@ -27,6 +27,17 @@
 
 namespace content {
 
+namespace {
+
+std::string GetHeaderIntegrityString(const net::SHA256HashValue& hash) {
+  std::string header_integrity_string = net::HashValue(hash).ToString();
+  // Change "sha256/" to "sha256-".
+  header_integrity_string[6] = '-';
+  return header_integrity_string;
+}
+
+}  // namespace
+
 struct SignedExchangeSubresourcePrefetchBrowserTestParam {
   SignedExchangeSubresourcePrefetchBrowserTestParam(
       bool network_service_enabled)
@@ -274,15 +285,19 @@
 IN_PROC_BROWSER_TEST_P(SignedExchangeSubresourcePrefetchBrowserTest,
                        PrefetchAlternativeSubresourceSXG) {
   int script_sxg_fetch_count = 0;
+  int script_fetch_count = 0;
   const char* prefetch_path = "/prefetch.html";
   const char* target_sxg_path = "/target.sxg";
   const char* target_path = "/target.html";
-  const char* script_path_in_sxg = "/script.js";
   const char* script_sxg_path = "/script_js.sxg";
+  const char* script_path = "/script.js";
 
   base::RunLoop script_sxg_prefetch_waiter;
   RegisterRequestMonitor(embedded_test_server(), script_sxg_path,
                          &script_sxg_fetch_count, &script_sxg_prefetch_waiter);
+  base::RunLoop script_prefetch_waiter;
+  RegisterRequestMonitor(embedded_test_server(), script_path,
+                         &script_fetch_count, &script_prefetch_waiter);
   RegisterRequestHandler(embedded_test_server());
   ASSERT_TRUE(embedded_test_server()->Start());
   EXPECT_EQ(0, prefetch_url_loader_called_);
@@ -290,7 +305,12 @@
   const GURL target_sxg_url = embedded_test_server()->GetURL(target_sxg_path);
   const GURL target_url = embedded_test_server()->GetURL(target_path);
   const GURL script_sxg_url = embedded_test_server()->GetURL(script_sxg_path);
-  const GURL script_url = embedded_test_server()->GetURL(script_path_in_sxg);
+  const GURL script_url = embedded_test_server()->GetURL(script_path);
+
+  const net::SHA256HashValue target_header_integrity = {{0x01}};
+  const net::SHA256HashValue script_header_integrity = {{0x02}};
+  const std::string script_header_integrity_string =
+      GetHeaderIntegrityString(script_header_integrity);
 
   const std::string outer_link_header = base::StringPrintf(
       "<%s>;"
@@ -299,37 +319,32 @@
       "anchor=\"%s\"",
       script_sxg_url.spec().c_str(), script_url.spec().c_str());
   const std::string inner_link_headers = base::StringPrintf(
-      "Link: <%s>;"
-      "rel=\"allowed-alt-sxg\";header-integrity=\"%s\","
+      "Link: "
+      "<%s>;rel=\"allowed-alt-sxg\";header-integrity=\"%s\","
       "<%s>;rel=\"preload\";as=\"script\"",
-      script_url.spec().c_str(),
-      // This is just a dummy data as of now.
-      // TODO(crbug.com/935267): When we will implement the header integrity
-      // checking logic, add tests for it.
-      "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
+      script_url.spec().c_str(), script_header_integrity_string.c_str(),
       script_url.spec().c_str());
 
   RegisterResponse(
       prefetch_path,
       ResponseEntry(base::StringPrintf(
           "<body><link rel='prefetch' href='%s'></body>", target_sxg_path)));
-  RegisterResponse(
-      target_sxg_path,
-      // We mock the SignedExchangeHandler, so just return a HTML content
-      // as "application/signed-exchange;v=b3".
-      ResponseEntry("<head><title>Prefetch Target (SXG)</title><script "
-                    "src=\"./preload.js\"></script></head>",
-                    "application/signed-exchange;v=b3",
-                    {{"x-content-type-options", "nosniff"},
-                     {"link", outer_link_header}}));
+  RegisterResponse(script_path, ResponseEntry("document.title=\"from server\";",
+                                              "text/javascript"));
+  RegisterResponse(target_sxg_path,
+                   // We mock the SignedExchangeHandler, so just return a HTML
+                   // content as "application/signed-exchange;v=b3".
+                   ResponseEntry("<head><title>Prefetch Target (SXG)</title>"
+                                 "<script src=\"./script.js\"></script></head>",
+                                 "application/signed-exchange;v=b3",
+                                 {{"x-content-type-options", "nosniff"},
+                                  {"link", outer_link_header}}));
   RegisterResponse(script_sxg_path,
                    // We mock the SignedExchangeHandler, so just return a JS
                    // content as "application/signed-exchange;v=b3".
                    ResponseEntry("document.title=\"done\";",
                                  "application/signed-exchange;v=b3",
                                  {{"x-content-type-options", "nosniff"}}));
-  const net::SHA256HashValue target_header_integrity = {{0x01}};
-  const net::SHA256HashValue script_header_integrity = {{0x02}};
   MockSignedExchangeHandlerFactory factory(
       {MockSignedExchangeHandlerParams(
            target_sxg_url, SignedExchangeLoadResult::kSuccess, net::OK,
@@ -343,6 +358,7 @@
   NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path));
   script_sxg_prefetch_waiter.Run();
   EXPECT_EQ(1, script_sxg_fetch_count);
+  EXPECT_EQ(0, script_fetch_count);
 
   WaitUntilLoaded(target_sxg_url);
   WaitUntilLoaded(script_sxg_url);
@@ -361,6 +377,500 @@
   EXPECT_EQ(script_sxg_url, script_it->second->outer_url());
   EXPECT_EQ(script_url, script_it->second->inner_url());
   EXPECT_EQ(script_header_integrity, *script_it->second->header_integrity());
+
+  // Subsequent navigation to the target URL wouldn't hit the network for
+  // the target URL. The target content should still be read correctly.
+  // The content is loaded from PrefetchedSignedExchangeCache. And the script
+  // is also loaded from PrefetchedSignedExchangeCache.
+  NavigateToURLAndWaitTitle(target_sxg_url, "done");
+
+  EXPECT_EQ(1, script_sxg_fetch_count);
+  EXPECT_EQ(0, script_fetch_count);
+}
+
+IN_PROC_BROWSER_TEST_P(SignedExchangeSubresourcePrefetchBrowserTest,
+                       PrefetchAlternativeSubresourceSXG_MultipleResources) {
+  int script1_sxg_fetch_count = 0;
+  int script1_fetch_count = 0;
+  int script2_sxg_fetch_count = 0;
+  int script2_fetch_count = 0;
+  const char* prefetch_path = "/prefetch.html";
+  const char* target_sxg_path = "/target.sxg";
+  const char* target_path = "/target.html";
+  const char* script1_sxg_path = "/script1_js.sxg";
+  const char* script1_path = "/script1.js";
+  const char* script2_sxg_path = "/script2_js.sxg";
+  const char* script2_path = "/script2.js";
+
+  base::RunLoop script1_sxg_prefetch_waiter;
+  RegisterRequestMonitor(embedded_test_server(), script1_sxg_path,
+                         &script1_sxg_fetch_count,
+                         &script1_sxg_prefetch_waiter);
+  base::RunLoop script1_prefetch_waiter;
+  RegisterRequestMonitor(embedded_test_server(), script1_path,
+                         &script1_fetch_count, &script1_prefetch_waiter);
+  base::RunLoop script2_sxg_prefetch_waiter;
+  RegisterRequestMonitor(embedded_test_server(), script2_sxg_path,
+                         &script2_sxg_fetch_count,
+                         &script2_sxg_prefetch_waiter);
+  base::RunLoop script2_prefetch_waiter;
+  RegisterRequestMonitor(embedded_test_server(), script2_path,
+                         &script2_fetch_count, &script2_prefetch_waiter);
+  RegisterRequestHandler(embedded_test_server());
+  ASSERT_TRUE(embedded_test_server()->Start());
+  EXPECT_EQ(0, prefetch_url_loader_called_);
+
+  const GURL target_sxg_url = embedded_test_server()->GetURL(target_sxg_path);
+  const GURL target_url = embedded_test_server()->GetURL(target_path);
+  const GURL script1_sxg_url = embedded_test_server()->GetURL(script1_sxg_path);
+  const GURL script1_url = embedded_test_server()->GetURL(script1_path);
+  const GURL script2_sxg_url = embedded_test_server()->GetURL(script2_sxg_path);
+  const GURL script2_url = embedded_test_server()->GetURL(script2_path);
+
+  const net::SHA256HashValue target_header_integrity = {{0x01}};
+  const net::SHA256HashValue script1_header_integrity = {{0x02}};
+  const std::string script1_header_integrity_string =
+      GetHeaderIntegrityString(script1_header_integrity);
+  const net::SHA256HashValue script2_header_integrity = {{0x03}};
+  const std::string script2_header_integrity_string =
+      GetHeaderIntegrityString(script2_header_integrity);
+
+  const std::string outer_link_header = base::StringPrintf(
+      "<%s>;rel=\"alternate\";type=\"application/signed-exchange;v=b3\";"
+      "anchor=\"%s\","
+      "<%s>;rel=\"alternate\";type=\"application/signed-exchange;v=b3\";"
+      "anchor=\"%s\"",
+      script1_sxg_url.spec().c_str(), script1_url.spec().c_str(),
+      script2_sxg_url.spec().c_str(), script2_url.spec().c_str());
+  const std::string inner_link_headers = base::StringPrintf(
+      "Link: "
+      "<%s>;rel=\"allowed-alt-sxg\";header-integrity=\"%s\","
+      "<%s>;rel=\"preload\";as=\"script\","
+      "<%s>;rel=\"allowed-alt-sxg\";header-integrity=\"%s\","
+      "<%s>;rel=\"preload\";as=\"script\"",
+      script1_url.spec().c_str(), script1_header_integrity_string.c_str(),
+      script1_url.spec().c_str(), script2_url.spec().c_str(),
+      script2_header_integrity_string.c_str(), script2_url.spec().c_str());
+
+  RegisterResponse(
+      prefetch_path,
+      ResponseEntry(base::StringPrintf(
+          "<body><link rel='prefetch' href='%s'></body>", target_sxg_path)));
+  RegisterResponse(script1_path, ResponseEntry("var test_title=\"from\";",
+                                               "text/javascript"));
+  RegisterResponse(script2_path,
+                   ResponseEntry("document.title=test_title+\"server\";",
+                                 "text/javascript"));
+  RegisterResponse(
+      target_sxg_path,
+      // We mock the SignedExchangeHandler, so just return a HTML
+      // content as "application/signed-exchange;v=b3".
+      ResponseEntry("<head><title>Prefetch Target (SXG)</title>"
+                    "<script src=\"./script1.js\"></script>"
+                    "<script src=\"./script2.js\"></script></head>",
+                    "application/signed-exchange;v=b3",
+                    {{"x-content-type-options", "nosniff"},
+                     {"link", outer_link_header}}));
+  RegisterResponse(script1_sxg_path,
+                   // We mock the SignedExchangeHandler, so just return a JS
+                   // content as "application/signed-exchange;v=b3".
+                   ResponseEntry("var test_title=\"done\";",
+                                 "application/signed-exchange;v=b3",
+                                 {{"x-content-type-options", "nosniff"}}));
+  RegisterResponse(script2_sxg_path,
+                   // We mock the SignedExchangeHandler, so just return a JS
+                   // content as "application/signed-exchange;v=b3".
+                   ResponseEntry("document.title=test_title;",
+                                 "application/signed-exchange;v=b3",
+                                 {{"x-content-type-options", "nosniff"}}));
+  MockSignedExchangeHandlerFactory factory({
+      MockSignedExchangeHandlerParams(
+          target_sxg_url, SignedExchangeLoadResult::kSuccess, net::OK,
+          target_url, "text/html", {inner_link_headers},
+          target_header_integrity),
+      MockSignedExchangeHandlerParams(
+          script1_sxg_url, SignedExchangeLoadResult::kSuccess, net::OK,
+          script1_url, "text/javascript", {}, script1_header_integrity),
+      MockSignedExchangeHandlerParams(
+          script2_sxg_url, SignedExchangeLoadResult::kSuccess, net::OK,
+          script2_url, "text/javascript", {}, script2_header_integrity),
+  });
+  ScopedSignedExchangeHandlerFactory scoped_factory(&factory);
+
+  NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path));
+  script1_sxg_prefetch_waiter.Run();
+  script2_sxg_prefetch_waiter.Run();
+  EXPECT_EQ(1, script1_sxg_fetch_count);
+  EXPECT_EQ(0, script1_fetch_count);
+  EXPECT_EQ(1, script2_sxg_fetch_count);
+  EXPECT_EQ(0, script2_fetch_count);
+
+  WaitUntilLoaded(target_sxg_url);
+  WaitUntilLoaded(script1_sxg_url);
+  WaitUntilLoaded(script2_sxg_url);
+
+  const auto& cached_exchanges = GetCachedExchanges();
+  EXPECT_EQ(3u, cached_exchanges.size());
+
+  const auto target_it = cached_exchanges.find(target_sxg_url);
+  ASSERT_TRUE(target_it != cached_exchanges.end());
+  EXPECT_EQ(target_sxg_url, target_it->second->outer_url());
+  EXPECT_EQ(target_url, target_it->second->inner_url());
+  EXPECT_EQ(target_header_integrity, *target_it->second->header_integrity());
+
+  const auto script1_it = cached_exchanges.find(script1_sxg_url);
+  ASSERT_TRUE(script1_it != cached_exchanges.end());
+  EXPECT_EQ(script1_sxg_url, script1_it->second->outer_url());
+  EXPECT_EQ(script1_url, script1_it->second->inner_url());
+  EXPECT_EQ(script1_header_integrity, *script1_it->second->header_integrity());
+
+  const auto script2_it = cached_exchanges.find(script2_sxg_url);
+  ASSERT_TRUE(script2_it != cached_exchanges.end());
+  EXPECT_EQ(script2_sxg_url, script2_it->second->outer_url());
+  EXPECT_EQ(script2_url, script2_it->second->inner_url());
+  EXPECT_EQ(script2_header_integrity, *script2_it->second->header_integrity());
+
+  // Subsequent navigation to the target URL wouldn't hit the network for
+  // the target URL. The target content should still be read correctly.
+  // The content is loaded from PrefetchedSignedExchangeCache. And the scripts
+  // are also loaded from PrefetchedSignedExchangeCache.
+  NavigateToURLAndWaitTitle(target_sxg_url, "done");
+
+  EXPECT_EQ(1, script1_sxg_fetch_count);
+  EXPECT_EQ(0, script1_fetch_count);
+  EXPECT_EQ(1, script2_sxg_fetch_count);
+  EXPECT_EQ(0, script2_fetch_count);
+}
+
+IN_PROC_BROWSER_TEST_P(SignedExchangeSubresourcePrefetchBrowserTest,
+                       PrefetchAlternativeSubresourceSXG_SameUrl) {
+  int script_sxg_fetch_count = 0;
+  int script_fetch_count = 0;
+  const char* prefetch_path = "/prefetch.html";
+  const char* target_sxg_path = "/target.html";
+  const char* target_path = "/target.html";
+  const char* script_sxg_path = "/script.js";
+  const char* script_path = "/script.js";
+
+  base::RunLoop script_sxg_prefetch_waiter;
+  RegisterRequestMonitor(embedded_test_server(), script_sxg_path,
+                         &script_sxg_fetch_count, &script_sxg_prefetch_waiter);
+  RegisterRequestHandler(embedded_test_server());
+  ASSERT_TRUE(embedded_test_server()->Start());
+  EXPECT_EQ(0, prefetch_url_loader_called_);
+
+  const GURL target_sxg_url = embedded_test_server()->GetURL(target_sxg_path);
+  const GURL target_url = embedded_test_server()->GetURL(target_path);
+  const GURL script_sxg_url = embedded_test_server()->GetURL(script_sxg_path);
+  const GURL script_url = embedded_test_server()->GetURL(script_path);
+
+  const net::SHA256HashValue target_header_integrity = {{0x01}};
+  const net::SHA256HashValue script_header_integrity = {{0x02}};
+  const std::string script_header_integrity_string =
+      GetHeaderIntegrityString(script_header_integrity);
+
+  const std::string outer_link_header = base::StringPrintf(
+      "<%s>;"
+      "rel=\"alternate\";"
+      "type=\"application/signed-exchange;v=b3\";"
+      "anchor=\"%s\"",
+      script_sxg_url.spec().c_str(), script_url.spec().c_str());
+  const std::string inner_link_headers = base::StringPrintf(
+      "Link: "
+      "<%s>;rel=\"allowed-alt-sxg\";header-integrity=\"%s\","
+      "<%s>;rel=\"preload\";as=\"script\"",
+      script_url.spec().c_str(), script_header_integrity_string.c_str(),
+      script_url.spec().c_str());
+
+  RegisterResponse(
+      prefetch_path,
+      ResponseEntry(base::StringPrintf(
+          "<body><link rel='prefetch' href='%s'></body>", target_sxg_path)));
+  RegisterResponse(script_path, ResponseEntry("document.title=\"from server\";",
+                                              "text/javascript"));
+  RegisterResponse(target_sxg_path,
+                   // We mock the SignedExchangeHandler, so just return a HTML
+                   // content as "application/signed-exchange;v=b3".
+                   ResponseEntry("<head><title>Prefetch Target (SXG)</title>"
+                                 "<script src=\"./script.js\"></script></head>",
+                                 "application/signed-exchange;v=b3",
+                                 {{"x-content-type-options", "nosniff"},
+                                  {"link", outer_link_header}}));
+  RegisterResponse(script_sxg_path,
+                   // We mock the SignedExchangeHandler, so just return a JS
+                   // content as "application/signed-exchange;v=b3".
+                   ResponseEntry("document.title=\"done\";",
+                                 "application/signed-exchange;v=b3",
+                                 {{"x-content-type-options", "nosniff"}}));
+  MockSignedExchangeHandlerFactory factory(
+      {MockSignedExchangeHandlerParams(
+           target_sxg_url, SignedExchangeLoadResult::kSuccess, net::OK,
+           target_url, "text/html", {inner_link_headers},
+           target_header_integrity),
+       MockSignedExchangeHandlerParams(
+           script_sxg_url, SignedExchangeLoadResult::kSuccess, net::OK,
+           script_url, "text/javascript", {}, script_header_integrity)});
+  ScopedSignedExchangeHandlerFactory scoped_factory(&factory);
+
+  NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path));
+  script_sxg_prefetch_waiter.Run();
+  EXPECT_EQ(1, script_sxg_fetch_count);
+  EXPECT_EQ(0, script_fetch_count);
+
+  WaitUntilLoaded(target_sxg_url);
+  WaitUntilLoaded(script_sxg_url);
+
+  const auto& cached_exchanges = GetCachedExchanges();
+  EXPECT_EQ(2u, cached_exchanges.size());
+
+  const auto target_it = cached_exchanges.find(target_sxg_url);
+  ASSERT_TRUE(target_it != cached_exchanges.end());
+  EXPECT_EQ(target_sxg_url, target_it->second->outer_url());
+  EXPECT_EQ(target_url, target_it->second->inner_url());
+  EXPECT_EQ(target_header_integrity, *target_it->second->header_integrity());
+
+  const auto script_it = cached_exchanges.find(script_sxg_url);
+  ASSERT_TRUE(script_it != cached_exchanges.end());
+  EXPECT_EQ(script_sxg_url, script_it->second->outer_url());
+  EXPECT_EQ(script_url, script_it->second->inner_url());
+  EXPECT_EQ(script_header_integrity, *script_it->second->header_integrity());
+
+  // Subsequent navigation to the target URL wouldn't hit the network for
+  // the target URL. The target content should still be read correctly.
+  // The content is loaded from PrefetchedSignedExchangeCache. And the script
+  // is also loaded from PrefetchedSignedExchangeCache.
+  NavigateToURLAndWaitTitle(target_sxg_url, "done");
+}
+
+IN_PROC_BROWSER_TEST_P(SignedExchangeSubresourcePrefetchBrowserTest,
+                       PrefetchAlternativeSubresourceSXG_IntegrityMismatch) {
+  int script_sxg_fetch_count = 0;
+  int script_fetch_count = 0;
+  const char* prefetch_path = "/prefetch.html";
+  const char* target_sxg_path = "/target.sxg";
+  const char* target_path = "/target.html";
+  const char* script_path = "/script.js";
+  const char* script_sxg_path = "/script_js.sxg";
+
+  base::RunLoop script_sxg_prefetch_waiter;
+  RegisterRequestMonitor(embedded_test_server(), script_sxg_path,
+                         &script_sxg_fetch_count, &script_sxg_prefetch_waiter);
+  base::RunLoop script_prefetch_waiter;
+  RegisterRequestMonitor(embedded_test_server(), script_path,
+                         &script_fetch_count, &script_prefetch_waiter);
+  RegisterRequestHandler(embedded_test_server());
+  ASSERT_TRUE(embedded_test_server()->Start());
+  EXPECT_EQ(0, prefetch_url_loader_called_);
+
+  const GURL target_sxg_url = embedded_test_server()->GetURL(target_sxg_path);
+  const GURL target_url = embedded_test_server()->GetURL(target_path);
+  const GURL script_sxg_url = embedded_test_server()->GetURL(script_sxg_path);
+  const GURL script_url = embedded_test_server()->GetURL(script_path);
+
+  const net::SHA256HashValue target_header_integrity = {{0x01}};
+  const net::SHA256HashValue script_header_integrity = {{0x02}};
+  const net::SHA256HashValue wrong_script_header_integrity = {{0x03}};
+  // Use the wrong header integrity value for "allowed-alt-sxg" link header to
+  // trigger the integrity mismatch fallback logic.
+  const std::string script_header_integrity_string =
+      GetHeaderIntegrityString(wrong_script_header_integrity);
+
+  const std::string outer_link_header = base::StringPrintf(
+      "<%s>;"
+      "rel=\"alternate\";"
+      "type=\"application/signed-exchange;v=b3\";"
+      "anchor=\"%s\"",
+      script_sxg_url.spec().c_str(), script_url.spec().c_str());
+  const std::string inner_link_headers = base::StringPrintf(
+      "Link: "
+      "<%s>;rel=\"allowed-alt-sxg\";header-integrity=\"%s\","
+      "<%s>;rel=\"preload\";as=\"script\"",
+      script_url.spec().c_str(), script_header_integrity_string.c_str(),
+      script_url.spec().c_str());
+
+  RegisterResponse(
+      prefetch_path,
+      ResponseEntry(base::StringPrintf(
+          "<body><link rel='prefetch' href='%s'></body>", target_sxg_path)));
+  RegisterResponse(script_path, ResponseEntry("document.title=\"from server\";",
+                                              "text/javascript"));
+  RegisterResponse(target_sxg_path,
+                   // We mock the SignedExchangeHandler, so just return a HTML
+                   // content as "application/signed-exchange;v=b3".
+                   ResponseEntry("<head><title>Prefetch Target (SXG)</title>"
+                                 "<script src=\"./script.js\"></script></head>",
+                                 "application/signed-exchange;v=b3",
+                                 {{"x-content-type-options", "nosniff"},
+                                  {"link", outer_link_header}}));
+  RegisterResponse(script_sxg_path,
+                   // We mock the SignedExchangeHandler, so just return a JS
+                   // content as "application/signed-exchange;v=b3".
+                   ResponseEntry("document.title=\"done\";",
+                                 "application/signed-exchange;v=b3",
+                                 {{"x-content-type-options", "nosniff"}}));
+  MockSignedExchangeHandlerFactory factory(
+      {MockSignedExchangeHandlerParams(
+           target_sxg_url, SignedExchangeLoadResult::kSuccess, net::OK,
+           target_url, "text/html", {inner_link_headers},
+           target_header_integrity),
+       MockSignedExchangeHandlerParams(
+           script_sxg_url, SignedExchangeLoadResult::kSuccess, net::OK,
+           script_url, "text/javascript", {}, script_header_integrity)});
+  ScopedSignedExchangeHandlerFactory scoped_factory(&factory);
+
+  NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path));
+  script_sxg_prefetch_waiter.Run();
+  EXPECT_EQ(1, script_sxg_fetch_count);
+  EXPECT_EQ(0, script_fetch_count);
+
+  WaitUntilLoaded(target_sxg_url);
+  WaitUntilLoaded(script_sxg_url);
+
+  // The value of "header-integrity" in "allowed-alt-sxg" link header of the
+  // inner response doesn't match the actual header integrity of script_js.sxg.
+  // So the script request must go to the server.
+  NavigateToURLAndWaitTitle(target_sxg_url, "from server");
+
+  EXPECT_EQ(1, script_fetch_count);
+}
+
+IN_PROC_BROWSER_TEST_P(
+    SignedExchangeSubresourcePrefetchBrowserTest,
+    PrefetchAlternativeSubresourceSXG_MultipleResources_IntegrityMismatch) {
+  int script1_sxg_fetch_count = 0;
+  int script1_fetch_count = 0;
+  int script2_sxg_fetch_count = 0;
+  int script2_fetch_count = 0;
+  const char* prefetch_path = "/prefetch.html";
+  const char* target_sxg_path = "/target.sxg";
+  const char* target_path = "/target.html";
+  const char* script1_sxg_path = "/script1_js.sxg";
+  const char* script1_path = "/script1.js";
+  const char* script2_sxg_path = "/script2_js.sxg";
+  const char* script2_path = "/script2.js";
+
+  base::RunLoop script1_sxg_prefetch_waiter;
+  RegisterRequestMonitor(embedded_test_server(), script1_sxg_path,
+                         &script1_sxg_fetch_count,
+                         &script1_sxg_prefetch_waiter);
+  base::RunLoop script1_prefetch_waiter;
+  RegisterRequestMonitor(embedded_test_server(), script1_path,
+                         &script1_fetch_count, &script1_prefetch_waiter);
+  base::RunLoop script2_sxg_prefetch_waiter;
+  RegisterRequestMonitor(embedded_test_server(), script2_sxg_path,
+                         &script2_sxg_fetch_count,
+                         &script2_sxg_prefetch_waiter);
+  base::RunLoop script2_prefetch_waiter;
+  RegisterRequestMonitor(embedded_test_server(), script2_path,
+                         &script2_fetch_count, &script2_prefetch_waiter);
+  RegisterRequestHandler(embedded_test_server());
+  ASSERT_TRUE(embedded_test_server()->Start());
+  EXPECT_EQ(0, prefetch_url_loader_called_);
+
+  const GURL target_sxg_url = embedded_test_server()->GetURL(target_sxg_path);
+  const GURL target_url = embedded_test_server()->GetURL(target_path);
+  const GURL script1_sxg_url = embedded_test_server()->GetURL(script1_sxg_path);
+  const GURL script1_url = embedded_test_server()->GetURL(script1_path);
+  const GURL script2_sxg_url = embedded_test_server()->GetURL(script2_sxg_path);
+  const GURL script2_url = embedded_test_server()->GetURL(script2_path);
+
+  const net::SHA256HashValue target_header_integrity = {{0x01}};
+  const net::SHA256HashValue script1_header_integrity = {{0x02}};
+  const std::string script1_header_integrity_string =
+      GetHeaderIntegrityString(script1_header_integrity);
+  const net::SHA256HashValue script2_header_integrity = {{0x03}};
+  const net::SHA256HashValue wrong_script2_header_integrity = {{0x04}};
+  // Use the wrong header integrity value for "allowed-alt-sxg" link header to
+  // trigger the integrity mismatch fallback logic.
+  const std::string script2_header_integrity_string =
+      GetHeaderIntegrityString(wrong_script2_header_integrity);
+
+  const std::string outer_link_header = base::StringPrintf(
+      "<%s>;rel=\"alternate\";type=\"application/signed-exchange;v=b3\";"
+      "anchor=\"%s\","
+      "<%s>;rel=\"alternate\";type=\"application/signed-exchange;v=b3\";"
+      "anchor=\"%s\"",
+      script1_sxg_url.spec().c_str(), script1_url.spec().c_str(),
+      script2_sxg_url.spec().c_str(), script2_url.spec().c_str());
+  const std::string inner_link_headers = base::StringPrintf(
+      "Link: "
+      "<%s>;rel=\"allowed-alt-sxg\";header-integrity=\"%s\","
+      "<%s>;rel=\"preload\";as=\"script\","
+      "<%s>;rel=\"allowed-alt-sxg\";header-integrity=\"%s\","
+      "<%s>;rel=\"preload\";as=\"script\"",
+      script1_url.spec().c_str(), script1_header_integrity_string.c_str(),
+      script1_url.spec().c_str(), script2_url.spec().c_str(),
+      script2_header_integrity_string.c_str(), script2_url.spec().c_str());
+
+  RegisterResponse(
+      prefetch_path,
+      ResponseEntry(base::StringPrintf(
+          "<body><link rel='prefetch' href='%s'></body>", target_sxg_path)));
+  RegisterResponse(script1_path, ResponseEntry("var test_title=\"from\";",
+                                               "text/javascript"));
+  RegisterResponse(script2_path,
+                   ResponseEntry("document.title=test_title+\" server\";",
+                                 "text/javascript"));
+  RegisterResponse(
+      target_sxg_path,
+      // We mock the SignedExchangeHandler, so just return a HTML
+      // content as "application/signed-exchange;v=b3".
+      ResponseEntry("<head><title>Prefetch Target (SXG)</title>"
+                    "<script src=\"./script1.js\"></script>"
+                    "<script src=\"./script2.js\"></script></head>",
+                    "application/signed-exchange;v=b3",
+                    {{"x-content-type-options", "nosniff"},
+                     {"link", outer_link_header}}));
+  RegisterResponse(script1_sxg_path,
+                   // We mock the SignedExchangeHandler, so just return a JS
+                   // content as "application/signed-exchange;v=b3".
+                   ResponseEntry("var test_title=\"done\";",
+                                 "application/signed-exchange;v=b3",
+                                 {{"x-content-type-options", "nosniff"}}));
+  RegisterResponse(script2_sxg_path,
+                   // We mock the SignedExchangeHandler, so just return a JS
+                   // content as "application/signed-exchange;v=b3".
+                   ResponseEntry("document.title=test_title;",
+                                 "application/signed-exchange;v=b3",
+                                 {{"x-content-type-options", "nosniff"}}));
+  MockSignedExchangeHandlerFactory factory({
+      MockSignedExchangeHandlerParams(
+          target_sxg_url, SignedExchangeLoadResult::kSuccess, net::OK,
+          target_url, "text/html", {inner_link_headers},
+          target_header_integrity),
+      MockSignedExchangeHandlerParams(
+          script1_sxg_url, SignedExchangeLoadResult::kSuccess, net::OK,
+          script1_url, "text/javascript", {}, script1_header_integrity),
+      MockSignedExchangeHandlerParams(
+          script2_sxg_url, SignedExchangeLoadResult::kSuccess, net::OK,
+          script2_url, "text/javascript", {}, script2_header_integrity),
+  });
+  ScopedSignedExchangeHandlerFactory scoped_factory(&factory);
+
+  NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path));
+  script1_sxg_prefetch_waiter.Run();
+  script2_sxg_prefetch_waiter.Run();
+  EXPECT_EQ(1, script1_sxg_fetch_count);
+  EXPECT_EQ(0, script1_fetch_count);
+  EXPECT_EQ(1, script2_sxg_fetch_count);
+  EXPECT_EQ(0, script2_fetch_count);
+
+  WaitUntilLoaded(target_sxg_url);
+  WaitUntilLoaded(script1_sxg_url);
+  WaitUntilLoaded(script2_sxg_url);
+
+  const auto& cached_exchanges = GetCachedExchanges();
+  EXPECT_EQ(3u, cached_exchanges.size());
+
+  // The value of "header-integrity" in "allowed-alt-sxg" link header of the
+  // inner response doesn't match the actual header integrity of script2_js.sxg.
+  // So the all script requests must go to the server.
+  NavigateToURLAndWaitTitle(target_sxg_url, "from server");
+
+  EXPECT_EQ(1, script1_sxg_fetch_count);
+  EXPECT_EQ(1, script1_fetch_count);
+  EXPECT_EQ(1, script2_sxg_fetch_count);
+  EXPECT_EQ(1, script2_fetch_count);
 }
 
 INSTANTIATE_TEST_SUITE_P(
diff --git a/content/renderer/input/render_widget_input_handler.cc b/content/renderer/input/render_widget_input_handler.cc
index ff1ec5f..d5815b1 100644
--- a/content/renderer/input/render_widget_input_handler.cc
+++ b/content/renderer/input/render_widget_input_handler.cc
@@ -12,7 +12,6 @@
 #include "base/command_line.h"
 #include "base/metrics/histogram_macros.h"
 #include "build/build_config.h"
-#include "cc/trees/element_id.h"
 #include "cc/trees/swap_promise_monitor.h"
 #include "components/viz/common/surfaces/frame_sink_id.h"
 #include "content/common/input/input_event_ack.h"
@@ -29,7 +28,6 @@
 #include "third_party/blink/public/platform/scheduler/web_thread_scheduler.h"
 #include "third_party/blink/public/platform/web_float_point.h"
 #include "third_party/blink/public/platform/web_float_size.h"
-#include "third_party/blink/public/platform/web_gesture_device.h"
 #include "third_party/blink/public/platform/web_gesture_event.h"
 #include "third_party/blink/public/platform/web_keyboard_event.h"
 #include "third_party/blink/public/platform/web_mouse_wheel_event.h"
@@ -39,7 +37,6 @@
 #include "third_party/blink/public/web/web_frame_widget.h"
 #include "third_party/blink/public/web/web_local_frame.h"
 #include "third_party/blink/public/web/web_node.h"
-#include "ui/events/blink/event_with_callback.h"
 #include "ui/events/blink/web_input_event_traits.h"
 #include "ui/gfx/geometry/dip_util.h"
 #include "ui/gfx/geometry/point_conversions.h"
@@ -214,7 +211,6 @@
       widget_(widget),
       handling_input_event_(false),
       handling_event_overscroll_(nullptr),
-      handling_injected_scroll_params_(nullptr),
       handling_event_type_(WebInputEvent::kUndefined),
       suppress_next_char_events_(false) {
   DCHECK(delegate);
@@ -317,14 +313,6 @@
   base::AutoReset<base::Optional<cc::TouchAction>>
       handling_touch_action_resetter(&handling_touch_action_, base::nullopt);
 
-  // Calls into |InjectGestureScrollEvent()| while handling this event
-  // will populate injected_scroll_params.
-  std::unique_ptr<std::vector<InjectScrollGestureParams>>
-      injected_scroll_params;
-  base::AutoReset<std::unique_ptr<std::vector<InjectScrollGestureParams>>*>
-      injected_scroll_resetter(&handling_injected_scroll_params_,
-                               &injected_scroll_params);
-
 #if defined(OS_ANDROID)
   ImeEventGuard guard(widget_);
 #endif
@@ -437,29 +425,6 @@
                                       ? INPUT_EVENT_ACK_STATE_NOT_CONSUMED
                                       : INPUT_EVENT_ACK_STATE_CONSUMED;
 
-  // The handling of some input events on the main thread may require injecting
-  // scroll gestures back into blink, e.g., a mousedown on a scrollbar. We
-  // do this here so that we can attribute lateny information from the mouse as
-  // a scroll interaction, instead of just classifying as mouse input.
-  if (injected_scroll_params) {
-    // TODO(dlibby): Create a copy of latency_info with new type
-    // and set a new swap promise monitor.
-    WebFloatPoint position = ui::PositionInWidgetFromInputEvent(input_event);
-    for (const InjectScrollGestureParams& params : *injected_scroll_params) {
-      std::unique_ptr<WebGestureEvent> gesture_event =
-          ui::GenerateInjectedScrollGesture(
-              params.type, input_event.TimeStamp(), params.device, position,
-              params.scroll_delta, params.granularity);
-      if (params.type == WebInputEvent::Type::kGestureScrollBegin) {
-        gesture_event->data.scroll_begin.scrollable_area_element_id =
-            params.scrollable_area_element_id.GetInternalValue();
-      }
-
-      widget_->GetWebWidget()->HandleInputEvent(
-          blink::WebCoalescedInputEvent(*gesture_event.get()));
-    }
-  }
-
   // Send gesture scroll events and their dispositions to the compositor thread,
   // so that they can be used to produce the elastic overscroll effect on Mac.
   if (input_event.GetType() == WebInputEvent::kGestureScrollBegin ||
@@ -538,71 +503,6 @@
   delegate_->OnDidOverscroll(*params);
 }
 
-void RenderWidgetInputHandler::InjectGestureScrollEvent(
-    const blink::WebFloatSize& delta,
-    blink::WebScrollGranularity granularity,
-    cc::ElementId scrollable_area_element_id,
-    WebInputEvent::Type injected_type) {
-  DCHECK(ui::IsGestureScroll(injected_type));
-  // If we're currently handling an input event, cache the appropriate
-  // parameters so we can dispatch the events directly once blink finishes
-  // handling the event.
-  // Otherwise, queue the event on the main thread event queue.
-  // The latter may occur when scrollbar scrolls are injected due to
-  // autoscroll timer - i.e. not within the handling of a mouse event.
-  // We don't always just enqueue events, since events queued to the
-  // MainThreadEventQueue in the middle of dispatch (which we are) won't
-  // be dispatched until the next time the queue gets to run. The side effect
-  // of that would be an extra frame of latency if we're injecting a scroll
-  // during the handling of a rAF aligned input event, such as mouse move.
-  if (handling_injected_scroll_params_) {
-    // Multiple gestures may be injected during the dispatch of a single
-    // input event (e.g. Begin/Update). Create a vector and append to the
-    // end of it - the gestures will subsequently be injected in order.
-    if (!*handling_injected_scroll_params_) {
-      *handling_injected_scroll_params_ =
-          std::make_unique<std::vector<InjectScrollGestureParams>>();
-    }
-
-    InjectScrollGestureParams params{blink::WebGestureDevice::kScrollbar, delta,
-                                     granularity, scrollable_area_element_id,
-                                     injected_type};
-    (*handling_injected_scroll_params_)->push_back(params);
-  } else {
-    base::TimeTicks now = base::TimeTicks::Now();
-    std::unique_ptr<WebGestureEvent> gesture_event =
-        ui::GenerateInjectedScrollGesture(
-            injected_type, now, blink::WebGestureDevice::kScrollbar,
-            WebFloatPoint(0, 0), delta, granularity);
-    if (injected_type == WebInputEvent::Type::kGestureScrollBegin) {
-      gesture_event->data.scroll_begin.scrollable_area_element_id =
-          scrollable_area_element_id.GetInternalValue();
-    }
-
-    ui::LatencyInfo latency_info;
-    ui::WebScopedInputEvent web_scoped_gesture_event(gesture_event.release());
-
-    // We have an empty callback since sending gestures through the
-    // system can generate overscroll params - if the user holds down the
-    // mouse on one of the arrows the autoscroll timer continues to fire to
-    // support infinite scrollers.
-    // For scrollbar, we ignore overscroll params, as scrollbar doesn't
-    // participate in overscroll.
-    // TODO(dlibby): Change blink to not propagate overscroll for scrollbar
-    // gestures and remove this callback. Currently non-blocking events are
-    // set up such that the callback is not propagated into the event queue,
-    // and we DCHECK when overscroll data is passed to the event handler.
-    HandledEventCallback handled_event = base::BindOnce(
-        [](InputEventAckState, const ui::LatencyInfo& latency_info,
-           std::unique_ptr<ui::DidOverscrollParams> overscroll_params,
-           base::Optional<cc::TouchAction> touch_action) {});
-    widget_->GetInputEventQueue()->HandleEvent(
-        std::move(web_scoped_gesture_event), latency_info,
-        DISPATCH_TYPE_BLOCKING, INPUT_EVENT_ACK_STATE_NOT_CONSUMED,
-        std::move(handled_event));
-  }
-}
-
 bool RenderWidgetInputHandler::DidChangeCursor(const WebCursor& cursor) {
   if (current_cursor_.has_value() && current_cursor_.value() == cursor)
     return false;
diff --git a/content/renderer/input/render_widget_input_handler.h b/content/renderer/input/render_widget_input_handler.h
index 8a8c5942..3c945eb 100644
--- a/content/renderer/input/render_widget_input_handler.h
+++ b/content/renderer/input/render_widget_input_handler.h
@@ -23,7 +23,6 @@
 }  // namespace blink
 
 namespace cc {
-struct ElementId;
 struct OverscrollBehavior;
 }
 
@@ -67,11 +66,6 @@
                               const blink::WebFloatSize& velocity,
                               const cc::OverscrollBehavior& behavior);
 
-  void InjectGestureScrollEvent(const blink::WebFloatSize& delta,
-                                blink::WebScrollGranularity granularity,
-                                cc::ElementId scrollable_area_element_id,
-                                blink::WebInputEvent::Type injected_type);
-
   bool handling_input_event() const { return handling_input_event_; }
   void set_handling_input_event(bool handling_input_event) {
     handling_input_event_ = handling_input_event;
@@ -86,14 +80,6 @@
   bool DidChangeCursor(const WebCursor& cursor);
 
  private:
-  struct InjectScrollGestureParams {
-    blink::WebGestureDevice device;
-    gfx::Vector2dF scroll_delta;
-    blink::WebScrollGranularity granularity;
-    cc::ElementId scrollable_area_element_id;
-    blink::WebInputEvent::Type type;
-  };