diff --git a/DEPS b/DEPS
index 3231285e..db62c6c 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': '6c8f5b31ac49be07ce68efb176a803a38810cb44',
+  'skia_revision': 'b75be23bc485b3f87c7a2d3574ad5ec57c09ad51',
   # 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': 'c77ce38099096de1347aa59f9d5b93191bf4b4f4',
+  'v8_revision': 'ba0df1167f22ea47aff1aef9cc9ebc44320fbe8b',
   # 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,15 +145,15 @@
   # 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': '0f861bd8c46f7ae179c425a3be861b1bb8414dd2',
+  'angle_revision': 'fc0be0494ee791c1fa914dcd260e1d64c4b726e5',
   # 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': 'd354695df3cbfebe261ee554f6db7c768f57e41b',
+  'swiftshader_revision': '390d846c3e6c44c5e78e936c07902b2c6b1665cf',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling PDFium
   # and whatever else without interference from each other.
-  'pdfium_revision': '8f64f4a25dc8311f798947cf073ab152827f5328',
+  'pdfium_revision': 'a3097da6c7df969dfc7ee2d93a8072d61486ac34',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling openmax_dl
   # and whatever else without interference from each other.
@@ -200,7 +200,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': '178118d233d98876cdf0cb472e7c91614cc1988f',
+  'catapult_revision': 'b931deacdf624e10e33f8789de28f05e3ac56438',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling libFuzzer
   # and whatever else without interference from each other.
@@ -256,7 +256,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.
-  'spv_tools_revision': '32af42616abe283411c05992e88e33f08db7884b',
+  'spv_tools_revision': 'c8b09744c6a15f3586c26351376bf5c6f656e89f',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling feed
   # 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.
-  'dawn_revision': 'cc141797dfc8fd0d10eafb96c9bfab7b341b8bd4',
+  'dawn_revision': '00f6b1af41cab2dfa75a0c2759a3602307f2e9fd',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling feed
   # and whatever else without interference from each other.
@@ -806,7 +806,7 @@
 
   # Build tools for Chrome OS. Note: This depends on third_party/pyelftools.
   'src/third_party/chromite': {
-      'url': Var('chromium_git') + '/chromiumos/chromite.git' + '@' + '00aa8077cada1617a2b7b9b9cc441d1503216225',
+      'url': Var('chromium_git') + '/chromiumos/chromite.git' + '@' + 'db2f00ebdbd640cccdf8ddf91b9995c009cfdc70',
       'condition': 'checkout_linux',
   },
 
@@ -831,7 +831,7 @@
   },
 
   'src/third_party/depot_tools':
-    Var('chromium_git') + '/chromium/tools/depot_tools.git' + '@' + 'b3aca437d08fb4a2c6f656ad0a0afae3d0396cec',
+    Var('chromium_git') + '/chromium/tools/depot_tools.git' + '@' + '867e3c9511394f5bb2f51f09278e5ccc773adc36',
 
   'src/third_party/devtools-node-modules':
     Var('chromium_git') + '/external/github.com/ChromeDevTools/devtools-node-modules' + '@' + Var('devtools_node_modules_revision'),
@@ -1184,7 +1184,7 @@
   },
 
   'src/third_party/perfetto':
-    Var('android_git') + '/platform/external/perfetto.git' + '@' +  '7f17b0b76444d19ff5c39bede231acc23fafb348',
+    Var('android_git') + '/platform/external/perfetto.git' + '@' +  '4ff1283572dfd0d120ebd63eebe1189b442db23b',
 
   'src/third_party/perl': {
       'url': Var('chromium_git') + '/chromium/deps/perl.git' + '@' + 'ac0d98b5cee6c024b0cffeb4f8f45b6fc5ccdb78',
@@ -1396,7 +1396,7 @@
     Var('chromium_git') + '/v8/v8.git' + '@' +  Var('v8_revision'),
 
   'src-internal': {
-    'url': 'https://chrome-internal.googlesource.com/chrome/src-internal.git@c6d9391d98ae5bf66405852d2ab823f4de51d1b0',
+    'url': 'https://chrome-internal.googlesource.com/chrome/src-internal.git@754d7b73cd0385866900e072289aa6d6ffd17e13',
     'condition': 'checkout_src_internal',
   },
 
diff --git a/ash/app_list/app_list_controller_impl.cc b/ash/app_list/app_list_controller_impl.cc
index 40fdaba..de11b69 100644
--- a/ash/app_list/app_list_controller_impl.cc
+++ b/ash/app_list/app_list_controller_impl.cc
@@ -541,6 +541,10 @@
     UMA_HISTOGRAM_ENUMERATION(app_list::kAppListToggleMethodHistogram,
                               show_source);
   }
+
+  for (auto& observer : observers_)
+    observer.OnAppListToggled();
+
   return action;
 }
 
diff --git a/ash/app_list/app_list_controller_observer.h b/ash/app_list/app_list_controller_observer.h
index 920e69b..316fb12 100644
--- a/ash/app_list/app_list_controller_observer.h
+++ b/ash/app_list/app_list_controller_observer.h
@@ -14,6 +14,9 @@
  public:
   // Called when the AppList is shown or dismissed.
   virtual void OnAppListVisibilityChanged(bool shown, int64_t display_id) {}
+
+  // Called when the AppList button is toggled.
+  virtual void OnAppListToggled() {}
 };
 
 }  // namespace ash
diff --git a/ash/assistant/util/deep_link_util.cc b/ash/assistant/util/deep_link_util.cc
index c527ab93..fe204f1 100644
--- a/ash/assistant/util/deep_link_util.cc
+++ b/ash/assistant/util/deep_link_util.cc
@@ -152,8 +152,10 @@
   return it != params.end()
              ? base::Optional<std::string>(net::UnescapeURLComponent(
                    it->second,
-                   net::UnescapeRule::URL_SPECIAL_CHARS_EXCEPT_PATH_SEPARATORS |
-                       net::UnescapeRule::REPLACE_PLUS_WITH_SPACE))
+                   net::UnescapeRule::PATH_SEPARATORS |
+                       net::UnescapeRule::REPLACE_PLUS_WITH_SPACE |
+                       net::UnescapeRule::
+                           URL_SPECIAL_CHARS_EXCEPT_PATH_SEPARATORS))
              : base::nullopt;
 }
 
diff --git a/ash/assistant/util/deep_link_util_unittest.cc b/ash/assistant/util/deep_link_util_unittest.cc
index 1e279c1..783f6b2 100644
--- a/ash/assistant/util/deep_link_util_unittest.cc
+++ b/ash/assistant/util/deep_link_util_unittest.cc
@@ -65,9 +65,9 @@
       {"query", "googleassistant://send-query?q=query"},
 
       // OK: Query containing spaces and special characters.
-      {"query with spaces & special characters?",
+      {"query with / and spaces & special characters?",
        "googleassistant://"
-       "send-query?q=query+with+spaces+%26+special+characters%3F"},
+       "send-query?q=query+with+%2F+and+spaces+%26+special+characters%3F"},
   };
 
   for (const auto& test_case : test_cases) {
@@ -137,8 +137,8 @@
   AssertDeepLinkParamEq("true", DeepLinkParam::kRelaunch);
 
   // Case: Deep link parameter present, URL encoded.
-  params["q"] = "query+with+spaces+%26+special+characters%3F";
-  AssertDeepLinkParamEq("query with spaces & special characters?",
+  params["q"] = "query+with+%2F+and+spaces+%26+special+characters%3F";
+  AssertDeepLinkParamEq("query with / and spaces & special characters?",
                         DeepLinkParam::kQuery);
 
   // Case: Deep link parameters absent.
diff --git a/ash/display/window_tree_host_manager.cc b/ash/display/window_tree_host_manager.cc
index 2615263..da34601 100644
--- a/ash/display/window_tree_host_manager.cc
+++ b/ash/display/window_tree_host_manager.cc
@@ -27,6 +27,7 @@
 #include "ash/wm/window_util.h"
 #include "ash/ws/window_service_owner.h"
 #include "base/command_line.h"
+#include "base/metrics/histogram.h"
 #include "base/strings/stringprintf.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/threading/thread_task_runner_handle.h"
@@ -291,6 +292,19 @@
     }
   }
 
+  // Record display zoom for the primary display for https://crbug.com/955071.
+  // This can be removed after M79.
+  const display::ManagedDisplayInfo& display_info =
+      display_manager->GetDisplayInfo(primary_display_id);
+  int zoom_percent = std::round(display_info.zoom_factor() * 100);
+  constexpr int kMaxValue = 300;
+  constexpr int kBucketSize = 5;
+  constexpr int kBucketCount = kMaxValue / kBucketSize + 1;
+  base::LinearHistogram::FactoryGet(
+      "Ash.Display.PrimaryDisplayZoomAtStartup", kBucketSize, kMaxValue,
+      kBucketCount, base::HistogramBase::kUmaTargetedHistogramFlag)
+      ->Add(zoom_percent);
+
   for (auto& observer : observers_)
     observer.OnDisplaysInitialized();
 }
diff --git a/ash/shelf/shelf_layout_manager.cc b/ash/shelf/shelf_layout_manager.cc
index 2c9232a3..81189c53 100644
--- a/ash/shelf/shelf_layout_manager.cc
+++ b/ash/shelf/shelf_layout_manager.cc
@@ -641,6 +641,12 @@
   MaybeUpdateShelfBackground(AnimationChangeType::IMMEDIATE);
 }
 
+void ShelfLayoutManager::OnAppListToggled() {
+  // Reset the Shelf's drag status to deal with the edge case that toggling
+  // the AppList button while dragging the shelf in auto-hide mode.
+  gesture_drag_status_ = GestureDragStatus::GESTURE_DRAG_NONE;
+}
+
 void ShelfLayoutManager::OnHomeLauncherTargetPositionChanged(
     bool showing,
     int64_t display_id) {
diff --git a/ash/shelf/shelf_layout_manager.h b/ash/shelf/shelf_layout_manager.h
index 1c72070..83d759c 100644
--- a/ash/shelf/shelf_layout_manager.h
+++ b/ash/shelf/shelf_layout_manager.h
@@ -141,6 +141,7 @@
 
   // AppListControllerObserver:
   void OnAppListVisibilityChanged(bool shown, int64_t display_id) override;
+  void OnAppListToggled() override;
 
   // HomeLauncherGestureHandlerObserver:
   void OnHomeLauncherTargetPositionChanged(bool showing,
diff --git a/ash/shelf/shelf_layout_manager_unittest.cc b/ash/shelf/shelf_layout_manager_unittest.cc
index 3aad93f..007a08a 100644
--- a/ash/shelf/shelf_layout_manager_unittest.cc
+++ b/ash/shelf/shelf_layout_manager_unittest.cc
@@ -11,6 +11,7 @@
 #include "ash/accelerators/accelerator_table.h"
 #include "ash/accessibility/accessibility_controller.h"
 #include "ash/accessibility/test_accessibility_controller_client.h"
+#include "ash/app_list/app_list_controller_impl.h"
 #include "ash/app_list/test/app_list_test_helper.h"
 #include "ash/app_list/views/app_list_view.h"
 #include "ash/focus_cycler.h"
@@ -22,6 +23,7 @@
 #include "ash/public/cpp/shell_window_ids.h"
 #include "ash/public/cpp/window_properties.h"
 #include "ash/root_window_controller.h"
+#include "ash/screen_util.h"
 #include "ash/session/session_controller.h"
 #include "ash/shelf/app_list_button.h"
 #include "ash/shelf/shelf.h"
@@ -82,6 +84,12 @@
 namespace ash {
 namespace {
 
+void PressAppListButton() {
+  ash::Shell::Get()->app_list_controller()->OnAppListButtonPressed(
+      display::Screen::GetScreen()->GetPrimaryDisplay().id(),
+      app_list::AppListShowSource::kShelfButton, base::TimeTicks());
+}
+
 void StepWidgetLayerAnimatorToEnd(views::Widget* widget) {
   widget->GetNativeView()->layer()->GetAnimator()->Step(
       base::TimeTicks::Now() + base::TimeDelta::FromSeconds(1));
@@ -2532,6 +2540,59 @@
   TestHomeLauncherGestureHandler(/*autohide_shelf=*/true);
 }
 
+// Tests that the auto-hide shelf has expected behavior when pressing the
+// AppList button while the shelf is being dragged by gesture (see
+// https://crbug.com/953877).
+TEST_F(ShelfLayoutManagerTest, PressAppListBtnWhenShelfBeingDragged) {
+  // Create a widget to hide the shelf in auto-hide mode.
+  CreateTestWidget();
+  GetPrimaryShelf()->SetAutoHideBehavior(SHELF_AUTO_HIDE_BEHAVIOR_ALWAYS);
+  EXPECT_FALSE(GetPrimaryShelf()->IsVisible());
+
+  const WorkAreaInsets* const work_area =
+      WorkAreaInsets::ForWindow(GetShelfWidget()->GetNativeWindow());
+  gfx::Rect available_bounds = screen_util::GetDisplayBoundsWithShelf(
+      GetShelfWidget()->GetNativeWindow());
+  available_bounds.Inset(work_area->GetAccessibilityInsets());
+
+  // Emulate to drag the shelf to show it.
+  gfx::Point gesture_location = display::Screen::GetScreen()
+                                    ->GetPrimaryDisplay()
+                                    .bounds()
+                                    .bottom_center();
+  int delta_y = -1;
+  base::TimeTicks timestamp = base::TimeTicks::Now();
+
+  ui::GestureEvent start_event = ui::GestureEvent(
+      gesture_location.x(), gesture_location.y(), ui::EF_NONE, timestamp,
+      ui::GestureEventDetails(ui::ET_GESTURE_SCROLL_BEGIN, 0, delta_y));
+  GetPrimaryShelf()->ProcessGestureEvent(start_event);
+  gesture_location.Offset(0, delta_y);
+  delta_y = -20;
+  timestamp += base::TimeDelta::FromMilliseconds(200);
+  ui::GestureEvent update_event = ui::GestureEvent(
+      gesture_location.x(), gesture_location.y(), ui::EF_NONE, timestamp,
+      ui::GestureEventDetails(ui::ET_GESTURE_SCROLL_UPDATE, 0, delta_y));
+  GetPrimaryShelf()->ProcessGestureEvent(update_event);
+
+  // Emulate to press the AppList button while dragging the Shelf.
+  PressAppListButton();
+  EXPECT_TRUE(GetPrimaryShelf()->IsVisible());
+
+  // Press the AppList button to hide the AppList and Shelf. Check the following
+  // things:
+  // (1) Shelf is hidden
+  // (2) Shelf has correct bounds in screen coordinate.
+  PressAppListButton();
+  EXPECT_EQ(available_bounds.bottom_left() +
+                gfx::Point(0, -kHiddenShelfInScreenPortion).OffsetFromOrigin(),
+            GetPrimaryShelf()
+                ->GetShelfViewForTesting()
+                ->GetBoundsInScreen()
+                .origin());
+  EXPECT_FALSE(GetPrimaryShelf()->IsVisible());
+}
+
 // Tests that tap outside of the AUTO_HIDE_SHOWN shelf should hide it.
 TEST_F(ShelfLayoutManagerTest, TapOutsideOfAutoHideShownShelf) {
   views::Widget* widget = CreateTestWidget();
diff --git a/base/trace_event/builtin_categories.h b/base/trace_event/builtin_categories.h
index 0a0fb37..860caac 100644
--- a/base/trace_event/builtin_categories.h
+++ b/base/trace_event/builtin_categories.h
@@ -172,6 +172,7 @@
   X(TRACE_DISABLED_BY_DEFAULT("file"))                                   \
   X(TRACE_DISABLED_BY_DEFAULT("fonts"))                                  \
   X(TRACE_DISABLED_BY_DEFAULT("gpu_cmd_queue"))                          \
+  X(TRACE_DISABLED_BY_DEFAULT("gpu.dawn"))                               \
   X(TRACE_DISABLED_BY_DEFAULT("gpu.debug"))                              \
   X(TRACE_DISABLED_BY_DEFAULT("gpu_decoder"))                            \
   X(TRACE_DISABLED_BY_DEFAULT("gpu.device"))                             \
diff --git a/build/android/gyp/write_build_config.py b/build/android/gyp/write_build_config.py
index 05e2c1b..f097a4f 100755
--- a/build/android/gyp/write_build_config.py
+++ b/build/android/gyp/write_build_config.py
@@ -546,7 +546,6 @@
 This type corresponds to an Android app bundle (`.aab` file).
 
 --------------- END_MARKDOWN ---------------------------------------------------
-TODO(estevenson): Add docs for static library synchronized proguarding.
 """
 
 import collections
@@ -913,6 +912,8 @@
   parser.add_option('--incremental-install-json-path',
                     help="Path to the target's generated incremental install "
                     "json.")
+
+  # apk options that are static library specific
   parser.add_option(
       '--static-library-dependent-configs',
       help='GN list of .build_configs of targets that use this target as a '
@@ -920,6 +921,18 @@
   parser.add_option(
       '--static-library-jar-path',
       help='Equivalent to what normally would be the jar_path for the APK.')
+  parser.add_option(
+      '--resource-ids-provider',
+      help='Path to the .build_config for the APK that this static library '
+      'target uses to generate stable resource IDs.')
+  parser.add_option(
+      '--compressed-locales-provider',
+      help='Path to the .build_config that contains the compressed locales '
+      'Java list for this static library target.')
+  parser.add_option(
+      '--uncompressed-locales-provider',
+      help='Path to the .build_config that contains the uncompressed locales '
+      'Java list for this static library target.')
 
   parser.add_option('--tested-apk-config',
       help='Path to the build config of the tested apk (for an instrumentation '
@@ -1453,13 +1466,24 @@
         path: sorted(set(classpath))
         for path, classpath in classpath_entries_by_owning_config.iteritems()
     }
-    # Order matters here, must match the order passed in.
-    static_lib_jar_paths = [
-        static_lib_jar_paths[x]
-        for x in options.static_library_dependent_configs
-    ]
-    static_lib_jar_paths.append(options.static_library_jar_path)
-    deps_info['static_library_dependent_apk_jars'] = static_lib_jar_paths
+
+    # resource_ids_provider's jar must go first to ensure the correct R.java
+    # IDs are used.
+    ordered_static_lib_jar_paths = []
+    if options.resource_ids_provider:
+      assert (options.resource_ids_provider in options.
+              static_library_dependent_configs), (
+                  '--resource-ids-provider must be in '
+                  '--static-library-dependent-configs')
+      ordered_static_lib_jar_paths.append(
+          static_lib_jar_paths[options.resource_ids_provider])
+
+    ordered_static_lib_jar_paths.extend(
+        x for x in static_lib_jar_paths.itervalues()
+        if x not in ordered_static_lib_jar_paths)
+    ordered_static_lib_jar_paths.append(options.static_library_jar_path)
+    deps_info[
+        'static_library_dependent_apk_jars'] = ordered_static_lib_jar_paths
     deps_info['static_library_proguard_mapping_output_paths'] = [
         d['proguard_mapping_path']
         for d in static_library_dependent_configs_by_path.itervalues()
@@ -1675,10 +1699,28 @@
     }
     config['assets'], config['uncompressed_assets'], locale_paks = (
         _MergeAssets(deps.All('android_assets')))
-    config['compressed_locales_java_list'] = _CreateJavaLocaleListFromAssets(
-        config['assets'], locale_paks)
-    config['uncompressed_locales_java_list'] = _CreateJavaLocaleListFromAssets(
-        config['uncompressed_assets'], locale_paks)
+
+    if options.compressed_locales_provider:
+      dep_config = GetDepConfig(options.compressed_locales_provider)
+      if dep_config['type'] == 'android_app_bundle':
+        dep_config = GetDepConfig(dep_config['base_module_config'])
+      deps_info['compressed_locales_java_list'] = dep_config[
+          'compressed_locales_java_list']
+    else:
+      deps_info[
+          'compressed_locales_java_list'] = _CreateJavaLocaleListFromAssets(
+              config['assets'], locale_paks)
+
+    if options.uncompressed_locales_provider:
+      dep_config = GetDepConfig(options.uncompressed_locales_provider)
+      if dep_config['type'] == 'android_app_bundle':
+        dep_config = GetDepConfig(dep_config['base_module_config'])
+      deps_info['uncompressed_locales_java_list'] = dep_config[
+          'uncompressed_locales_java_list']
+    else:
+      deps_info[
+          'uncompressed_locales_java_list'] = _CreateJavaLocaleListFromAssets(
+              config['uncompressed_assets'], locale_paks)
 
     config['extra_android_manifests'] = filter(None, (
         d.get('android_manifest') for d in all_resources_deps))
diff --git a/build/config/android/internal_rules.gni b/build/config/android/internal_rules.gni
index b1842aa..42d2ccd 100644
--- a/build/config/android/internal_rules.gni
+++ b/build/config/android/internal_rules.gni
@@ -470,17 +470,26 @@
         rebase_path(invoker.static_library_jar_path, root_build_dir),
       ]
       _dependent_configs = []
-      foreach(d, invoker.static_library_dependent_targets) {
-        _target_label = get_label_info(d, "label_no_toolchain")
+      foreach(_target, invoker.static_library_dependent_targets) {
+        _target_name = _target.name
+        _target_label = get_label_info(_target_name, "label_no_toolchain")
         deps += [ "$_target_label$build_config_target_suffix" ]
-        _dep_gen_dir = get_label_info(d, "target_gen_dir")
-        _dep_name = get_label_info(d, "name")
-        _dependent_configs += [ "$_dep_gen_dir/$_dep_name.build_config" ]
+        _dep_gen_dir = get_label_info(_target_name, "target_gen_dir")
+        _dep_name = get_label_info(_target_name, "name")
+        _config =
+            rebase_path("$_dep_gen_dir/$_dep_name.build_config", root_build_dir)
+        _dependent_configs += [ _config ]
+        if (_target.is_resource_ids_provider) {
+          args += [ "--resource-ids-provider=$_config" ]
+        }
+        if (_target.is_compressed_locales_provider) {
+          args += [ "--compressed-locales-provider=$_config" ]
+        }
+        if (_target.is_uncompressed_locales_provider) {
+          args += [ "--uncompressed-locales-provider=$_config" ]
+        }
       }
-      _rebased_dependent_configs =
-          rebase_path(_dependent_configs, root_build_dir)
-      args +=
-          [ "--static-library-dependent-configs=$_rebased_dependent_configs" ]
+      args += [ "--static-library-dependent-configs=$_dependent_configs" ]
     }
     if (defined(invoker.gradle_treat_as_prebuilt) &&
         invoker.gradle_treat_as_prebuilt) {
diff --git a/build/config/android/rules.gni b/build/config/android/rules.gni
index 9c19665..b6f0ba1 100644
--- a/build/config/android/rules.gni
+++ b/build/config/android/rules.gni
@@ -1959,10 +1959,8 @@
         _rebased_build_config =
             rebase_path(invoker.build_config, root_build_dir)
         defines += [
-          "COMPRESSED_LOCALE_LIST=" +
-              "@FileArg($_rebased_build_config:compressed_locales_java_list)",
-          "UNCOMPRESSED_LOCALE_LIST=" +
-              "@FileArg($_rebased_build_config:uncompressed_locales_java_list)",
+          "COMPRESSED_LOCALE_LIST=" + "@FileArg($_rebased_build_config:deps_info:compressed_locales_java_list)",
+          "UNCOMPRESSED_LOCALE_LIST=" + "@FileArg($_rebased_build_config:deps_info:uncompressed_locales_java_list)",
         ]
         if (defined(invoker.firebase_app_id)) {
           defines += [ "_FIREBASE_APP_ID=${invoker.firebase_app_id}" ]
@@ -2049,9 +2047,13 @@
   #     resources with acceptable/non-acceptable optimizations.
   #   verify_android_configuration: Enables verification of expected merged
   #     manifest and proguard flags based on a golden file.
-  #   static_library_dependent_targets: A list of targets that use this target
-  #     as a static library. Common Java code from the targets listed in
-  #     static_library_dependent_targets will be moved into this target.
+  #   static_library_dependent_targets: A list of scopes describing targets that
+  #     use this target as a static library. Common Java code from the targets
+  #     listed in static_library_dependent_targets will be moved into this
+  #     target. Scope members are name, is_resource_ids_provider,
+  #     is_compressed_locales_provider, is_uncompressed_locales_provider.
+  #     TODO(estevenson): Add a README for static library targets and document
+  #                       additions to "deps_info" in write_build_config.py.
   #   static_library_provider: Specifies a single target that this target will
   #     use as a static library APK. When proguard is enabled, the
   #     static_library_provider target will provide the dex file(s) for this
@@ -2281,6 +2283,15 @@
       _static_library_apk_java_target_output = _jar_path
       _static_library_sync_dex_path =
           "$_gen_dir/static_library_synchronized_proguard.classes.dex.zip"
+      _resource_ids_provider_deps = []
+      foreach(_target, invoker.static_library_dependent_targets) {
+        if (_target.is_resource_ids_provider) {
+          assert(_resource_ids_provider_deps == [],
+                 "Can only have 1 resource_ids_provider_dep")
+          _resource_ids_provider_deps += [ _target.name ]
+        }
+      }
+      _resource_ids_provider_dep = _resource_ids_provider_deps[0]
     }
 
     _uses_static_library = defined(invoker.static_library_provider)
@@ -2371,7 +2382,6 @@
                                "png_to_webp",
                                "resource_blacklist_exceptions",
                                "resource_blacklist_regex",
-                               "resource_ids_provider_dep",
                                "resources_config_path",
                                "shared_resources",
                                "shared_resources_whitelist_locales",
@@ -2381,6 +2391,10 @@
       version_code = _version_code
       version_name = _version_name
 
+      if (defined(_resource_ids_provider_dep)) {
+        resource_ids_provider_dep = _resource_ids_provider_dep
+      }
+
       if (defined(invoker.post_process_package_resources_script)) {
         post_process_script = invoker.post_process_package_resources_script
       }
@@ -2749,7 +2763,7 @@
           ":$_java_target",
         ]
         foreach(_dep, invoker.static_library_dependent_targets) {
-          _target_label = get_label_info(_dep, "label_no_toolchain")
+          _target_label = get_label_info(_dep.name, "label_no_toolchain")
           deps += [ "${_target_label}__java" ]
         }
         inputs = [
@@ -2857,7 +2871,8 @@
                 "static_library_dependent_classpath_configs:" +
                 "$_rebased_build_config)",
           ]
-          foreach(_apk_as_module, invoker.static_library_dependent_targets) {
+          foreach(_target, invoker.static_library_dependent_targets) {
+            _apk_as_module = _target.name
             _module_config_target =
                 "${_apk_as_module}$build_config_target_suffix"
             _module_gen_dir = get_label_info(_apk_as_module, "target_gen_dir")
@@ -3255,7 +3270,6 @@
                                "proguard_jar_path",
                                "resource_blacklist_regex",
                                "resource_blacklist_exceptions",
-                               "resource_ids_provider_dep",
                                "resources_config_path",
                                "secondary_abi_loadable_modules",
                                "secondary_abi_shared_libraries",
@@ -3370,7 +3384,6 @@
                                "proguard_jar_path",
                                "resource_blacklist_exceptions",
                                "resource_blacklist_regex",
-                               "resource_ids_provider_dep",
                                "resources_config_path",
                                "secondary_abi_shared_libraries",
                                "shared_libraries",
diff --git a/build/fuchsia/linux.sdk.sha1 b/build/fuchsia/linux.sdk.sha1
index 416bcb5..4406479 100644
--- a/build/fuchsia/linux.sdk.sha1
+++ b/build/fuchsia/linux.sdk.sha1
@@ -1 +1 @@
-8914675421772048816
\ No newline at end of file
+8914644572533835024
\ No newline at end of file
diff --git a/build/fuchsia/mac.sdk.sha1 b/build/fuchsia/mac.sdk.sha1
index 8c8fa8f4..a24f789c 100644
--- a/build/fuchsia/mac.sdk.sha1
+++ b/build/fuchsia/mac.sdk.sha1
@@ -1 +1 @@
-8914678376428897744
\ No newline at end of file
+8914652258186523104
\ No newline at end of file
diff --git a/cc/layers/layer_impl.cc b/cc/layers/layer_impl.cc
index 5d4c278..19adeb1 100644
--- a/cc/layers/layer_impl.cc
+++ b/cc/layers/layer_impl.cc
@@ -143,7 +143,7 @@
                 draw_properties_.rounded_corner_bounds,
                 draw_properties_.clip_rect, draw_properties_.is_clipped,
                 contents_opaque, draw_properties_.opacity,
-                effect_node->has_render_surface ? SkBlendMode::kSrcOver
+                effect_node->HasRenderSurface() ? SkBlendMode::kSrcOver
                                                 : effect_node->blend_mode,
                 GetSortingContextId());
   state->is_fast_rounded_corner = draw_properties_.is_fast_rounded_corner;
@@ -169,7 +169,7 @@
                 draw_properties().rounded_corner_bounds,
                 draw_properties().clip_rect, draw_properties().is_clipped,
                 contents_opaque, draw_properties().opacity,
-                effect_node->has_render_surface ? SkBlendMode::kSrcOver
+                effect_node->HasRenderSurface() ? SkBlendMode::kSrcOver
                                                 : effect_node->blend_mode,
                 GetSortingContextId());
   state->is_fast_rounded_corner = draw_properties().is_fast_rounded_corner;
diff --git a/cc/layers/video_layer.h b/cc/layers/video_layer.h
index 0ae52c8..45cacd34 100644
--- a/cc/layers/video_layer.h
+++ b/cc/layers/video_layer.h
@@ -8,7 +8,7 @@
 #include "base/callback.h"
 #include "cc/cc_export.h"
 #include "cc/layers/layer.h"
-#include "media/base/video_rotation.h"
+#include "media/base/video_transformation.h"
 
 namespace media { class VideoFrame; }
 
diff --git a/cc/layers/video_layer_impl.h b/cc/layers/video_layer_impl.h
index 0f715c7..5d3aa87 100644
--- a/cc/layers/video_layer_impl.h
+++ b/cc/layers/video_layer_impl.h
@@ -10,7 +10,7 @@
 #include "cc/cc_export.h"
 #include "cc/layers/layer_impl.h"
 #include "components/viz/common/resources/release_callback.h"
-#include "media/base/video_rotation.h"
+#include "media/base/video_transformation.h"
 
 namespace media {
 class VideoFrame;
diff --git a/cc/test/layer_tree_pixel_test.cc b/cc/test/layer_tree_pixel_test.cc
index 1027eb2..3cdd0cfe 100644
--- a/cc/test/layer_tree_pixel_test.cc
+++ b/cc/test/layer_tree_pixel_test.cc
@@ -264,7 +264,7 @@
   root_effect.clip_id = 1;
   root_effect.stable_id = 1;
   root_effect.transform_id = 1;
-  root_effect.has_render_surface = true;
+  root_effect.render_surface_reason = RenderSurfaceReason::kTest;
   property_trees->effect_tree.Insert(root_effect, 0);
 
   ScrollNode scroll_node;
diff --git a/cc/trees/draw_property_utils.cc b/cc/trees/draw_property_utils.cc
index e32fa24..9c1bec65 100644
--- a/cc/trees/draw_property_utils.cc
+++ b/cc/trees/draw_property_utils.cc
@@ -49,7 +49,7 @@
     // doesn't set effect ids on clip nodes.
     return;
   }
-  DCHECK(effect_node->has_render_surface);
+  DCHECK(effect_node->HasRenderSurface());
   transform->matrix().postScale(effect_node->surface_contents_scale.x(),
                                 effect_node->surface_contents_scale.y(), 1.f);
 }
@@ -458,7 +458,7 @@
     const TransformTree& transform_tree,
     const EffectTree& effect_tree) {
   const EffectNode* effect_node = effect_tree.Node(layer->effect_tree_index());
-  if (effect_node->has_render_surface && effect_node->subtree_has_copy_request)
+  if (effect_node->HasRenderSurface() && effect_node->subtree_has_copy_request)
     return false;
 
   // If the layer transform is not invertible, it should be skipped. In case the
@@ -509,7 +509,7 @@
   // (included) and its target surface (excluded).
   const EffectNode* node = tree.Node(render_surface->EffectTreeIndex());
   float draw_opacity = tree.EffectiveOpacity(node);
-  for (node = tree.parent(node); node && !node->has_render_surface;
+  for (node = tree.parent(node); node && !node->HasRenderSurface();
        node = tree.parent(node)) {
     draw_opacity *= tree.EffectiveOpacity(node);
   }
@@ -656,7 +656,7 @@
   const EffectTree* effect_tree = &property_trees->effect_tree;
   const EffectNode* effect_node = effect_tree->Node(layer->effect_tree_index());
   const EffectNode* target_node =
-      effect_node->has_render_surface
+      effect_node->HasRenderSurface()
           ? effect_node
           : effect_tree->Node(effect_node->target_id);
   bool include_expanding_clips = false;
@@ -676,7 +676,7 @@
 
   // Return empty rrect if this node has a render surface but the function call
   // was made for a non render surface.
-  if (effect_node->has_render_surface && !for_render_surface)
+  if (effect_node->HasRenderSurface() && !for_render_surface)
     return kEmptyRoundedCornerInfo;
 
   // Traverse the parent chain up to the render target to find a node which has
@@ -691,7 +691,7 @@
 
     // Simply break if we reached a node that has a render surface or is the
     // render target.
-    if (node->has_render_surface || node->id == target_id)
+    if (node->HasRenderSurface() || node->id == target_id)
       break;
 
     node = effect_tree->parent(node);
@@ -723,7 +723,7 @@
     if (i == EffectTree::kContentsRootNodeId) {
       // Render target of the node corresponding to root is itself.
       node->target_id = EffectTree::kContentsRootNodeId;
-    } else if (effect_tree->parent(node)->has_render_surface) {
+    } else if (effect_tree->parent(node)->HasRenderSurface()) {
       node->target_id = node->parent_id;
     } else {
       node->target_id = effect_tree->parent(node)->target_id;
@@ -765,7 +765,7 @@
 
 void ConcatInverseSurfaceContentsScale(const EffectNode* effect_node,
                                        gfx::Transform* transform) {
-  DCHECK(effect_node->has_render_surface);
+  DCHECK(effect_node->HasRenderSurface());
   if (effect_node->surface_contents_scale.x() != 0.0 &&
       effect_node->surface_contents_scale.y() != 0.0)
     transform->Scale(1.0 / effect_node->surface_contents_scale.x(),
diff --git a/cc/trees/effect_node.cc b/cc/trees/effect_node.cc
index 3701e4c..acf8f94 100644
--- a/cc/trees/effect_node.cc
+++ b/cc/trees/effect_node.cc
@@ -17,7 +17,6 @@
       screen_space_opacity(1.f),
       backdrop_filter_quality(1.f),
       blend_mode(SkBlendMode::kSrcOver),
-      has_render_surface(false),
       cache_render_surface(false),
       has_copy_request(false),
       hidden_by_backface_visibility(false),
@@ -34,6 +33,7 @@
       effect_changed(false),
       subtree_has_copy_request(false),
       is_fast_rounded_corner(false),
+      render_surface_reason(RenderSurfaceReason::kNone),
       transform_id(0),
       clip_id(0),
       target_id(1),
@@ -48,7 +48,6 @@
          stable_id == other.stable_id && opacity == other.opacity &&
          screen_space_opacity == other.screen_space_opacity &&
          backdrop_filter_quality == other.backdrop_filter_quality &&
-         has_render_surface == other.has_render_surface &&
          cache_render_surface == other.cache_render_surface &&
          has_copy_request == other.has_copy_request &&
          filters == other.filters &&
@@ -57,6 +56,9 @@
          filters_origin == other.filters_origin &&
          rounded_corner_bounds == other.rounded_corner_bounds &&
          is_fast_rounded_corner == other.is_fast_rounded_corner &&
+         // The specific reason is just for tracing/testing/debugging, so just
+         // check whether a render surface is needed.
+         HasRenderSurface() == other.HasRenderSurface() &&
          blend_mode == other.blend_mode &&
          surface_contents_scale == other.surface_contents_scale &&
          unscaled_mask_target_size == other.unscaled_mask_target_size &&
@@ -83,6 +85,54 @@
              other.closest_ancestor_with_copy_request_id;
 }
 
+const char* RenderSurfaceReasonToString(RenderSurfaceReason reason) {
+  switch (reason) {
+    case RenderSurfaceReason::kNone:
+      return "none";
+    case RenderSurfaceReason::kRoot:
+      return "root";
+    case RenderSurfaceReason::k3dTransformFlattening:
+      return "3d transform flattening";
+    case RenderSurfaceReason::kBlendMode:
+      return "blend mode";
+    case RenderSurfaceReason::kBlendModeDstIn:
+      return "blend mode kDstIn";
+    case RenderSurfaceReason::kOpacity:
+      return "opacity";
+    case RenderSurfaceReason::kOpacityAnimation:
+      return "opacity animation";
+    case RenderSurfaceReason::kFilter:
+      return "filter";
+    case RenderSurfaceReason::kFilterAnimation:
+      return "filter animation";
+    case RenderSurfaceReason::kBackdropFilter:
+      return "backdrop filter";
+    case RenderSurfaceReason::kBackdropFilterAnimation:
+      return "backdrop filter animation";
+    case RenderSurfaceReason::kRoundedCorner:
+      return "rounded corner";
+    case RenderSurfaceReason::kClipPath:
+      return "clip path";
+    case RenderSurfaceReason::kClipAxisAlignment:
+      return "clip axis alignment";
+    case RenderSurfaceReason::kMask:
+      return "mask";
+    case RenderSurfaceReason::kRootOrIsolatedGroup:
+      return "root or isolated group";
+    case RenderSurfaceReason::kTrilinearFiltering:
+      return "trilinear filtering";
+    case RenderSurfaceReason::kCache:
+      return "cache";
+    case RenderSurfaceReason::kCopyRequest:
+      return "copy request";
+    case RenderSurfaceReason::kTest:
+      return "test";
+    default:
+      NOTREACHED() << static_cast<int>(reason);
+      return "";
+  }
+}
+
 void EffectNode::AsValueInto(base::trace_event::TracedValue* value) const {
   value->SetInteger("id", id);
   value->SetInteger("parent_id", parent_id);
@@ -91,7 +141,6 @@
   value->SetDouble("backdrop_filter_quality", backdrop_filter_quality);
   value->SetBoolean("is_fast_rounded_corner", is_fast_rounded_corner);
   value->SetString("blend_mode", SkBlendMode_Name(blend_mode));
-  value->SetBoolean("has_render_surface", has_render_surface);
   value->SetBoolean("cache_render_surface", cache_render_surface);
   value->SetBoolean("has_copy_request", has_copy_request);
   value->SetBoolean("double_sided", double_sided);
@@ -104,7 +153,10 @@
   value->SetBoolean("has_masking_child", has_masking_child);
   value->SetBoolean("is_masked", is_masked);
   value->SetBoolean("effect_changed", effect_changed);
-  value->SetInteger("subtree_has_copy_request", subtree_has_copy_request);
+  value->SetBoolean("subtree_has_copy_request", subtree_has_copy_request);
+  value->SetBoolean("is_fast_rounded_corner", is_fast_rounded_corner);
+  value->SetString("render_surface_reason",
+                   RenderSurfaceReasonToString(render_surface_reason));
   value->SetInteger("transform_id", transform_id);
   value->SetInteger("clip_id", clip_id);
   value->SetInteger("target_id", target_id);
diff --git a/cc/trees/effect_node.h b/cc/trees/effect_node.h
index f9ad9713..46d285e9 100644
--- a/cc/trees/effect_node.h
+++ b/cc/trees/effect_node.h
@@ -20,6 +20,33 @@
 
 namespace cc {
 
+enum class RenderSurfaceReason : uint8_t {
+  kNone,
+  kRoot,
+  k3dTransformFlattening,
+  kBlendMode,
+  kBlendModeDstIn,
+  kOpacity,
+  kOpacityAnimation,
+  kFilter,
+  kFilterAnimation,
+  kBackdropFilter,
+  kBackdropFilterAnimation,
+  kRoundedCorner,
+  kClipPath,
+  kClipAxisAlignment,
+  kMask,
+  kRootOrIsolatedGroup,
+  kTrilinearFiltering,
+  kCache,
+  kCopyRequest,
+  // This must be the last value because it's used in tracing code to know the
+  // number of reasons.
+  kTest,
+};
+
+CC_EXPORT const char* RenderSurfaceReasonToString(RenderSurfaceReason);
+
 struct CC_EXPORT EffectNode {
   EffectNode();
   EffectNode(const EffectNode& other);
@@ -55,7 +82,6 @@
 
   gfx::Size unscaled_mask_target_size;
 
-  bool has_render_surface : 1;
   bool cache_render_surface : 1;
   bool has_copy_request : 1;
   bool hidden_by_backface_visibility : 1;
@@ -85,7 +111,10 @@
   bool subtree_has_copy_request : 1;
   // If set, the effect node tries to not trigger a render surface due to it
   // having a rounded corner.
-  bool is_fast_rounded_corner;
+  bool is_fast_rounded_corner : 1;
+  // RenderSurfaceReason::kNone if this effect node should not create a render
+  // surface, or the reason that this effect node should create one.
+  RenderSurfaceReason render_surface_reason;
   // The transform node index of the transform to apply to this effect
   // node's content when rendering to a surface.
   int transform_id;
@@ -102,6 +131,10 @@
   int closest_ancestor_with_cached_render_surface_id;
   int closest_ancestor_with_copy_request_id;
 
+  bool HasRenderSurface() const {
+    return render_surface_reason != RenderSurfaceReason::kNone;
+  }
+
   bool operator==(const EffectNode& other) const;
 
   void AsValueInto(base::trace_event::TracedValue* value) const;
diff --git a/cc/trees/layer_tree_host_common.cc b/cc/trees/layer_tree_host_common.cc
index e46a166..6eb7ec0 100644
--- a/cc/trees/layer_tree_host_common.cc
+++ b/cc/trees/layer_tree_host_common.cc
@@ -489,6 +489,38 @@
                                 render_surface_list);
 }
 
+static void RecordRenderSurfaceReasonsForTracing(
+    const PropertyTrees* property_trees,
+    const RenderSurfaceList* render_surface_list) {
+  static const auto* tracing_enabled =
+      TRACE_EVENT_API_GET_CATEGORY_GROUP_ENABLED("cc");
+  if (!*tracing_enabled ||
+      // Don't output single root render surface.
+      render_surface_list->size() <= 1)
+    return;
+
+  TRACE_EVENT_INSTANT1("cc", "RenderSurfaceReasonCount",
+                       TRACE_EVENT_SCOPE_THREAD, "total",
+                       render_surface_list->size());
+
+  // kTest is the last value which is not included for tracing.
+  constexpr auto kNumReasons = static_cast<size_t>(RenderSurfaceReason::kTest);
+  int reason_counts[kNumReasons] = {0};
+  for (const auto* render_surface : *render_surface_list) {
+    const auto* effect_node =
+        property_trees->effect_tree.Node(render_surface->EffectTreeIndex());
+    reason_counts[static_cast<size_t>(effect_node->render_surface_reason)]++;
+  }
+  for (size_t i = 0; i < kNumReasons; i++) {
+    if (!reason_counts[i])
+      continue;
+    TRACE_EVENT_INSTANT1(
+        "cc", "RenderSurfaceReasonCount", TRACE_EVENT_SCOPE_THREAD,
+        RenderSurfaceReasonToString(static_cast<RenderSurfaceReason>(i)),
+        reason_counts[i]);
+  }
+}
+
 void CalculateDrawPropertiesInternal(
     LayerTreeHostCommon::CalcDrawPropsImplInputs* inputs,
     PropertyTreeOption property_tree_option) {
@@ -604,6 +636,8 @@
         inputs->root_layer->layer_tree_impl(), inputs->property_trees,
         inputs->render_surface_list, inputs->max_texture_size);
   }
+  RecordRenderSurfaceReasonsForTracing(inputs->property_trees,
+                                       inputs->render_surface_list);
 
   // A root layer render_surface should always exist after
   // CalculateDrawProperties.
diff --git a/cc/trees/layer_tree_host_common_unittest.cc b/cc/trees/layer_tree_host_common_unittest.cc
index 37d6f1c..93750755 100644
--- a/cc/trees/layer_tree_host_common_unittest.cc
+++ b/cc/trees/layer_tree_host_common_unittest.cc
@@ -1329,7 +1329,7 @@
   EffectTree& effect_tree =
       root->layer_tree_impl()->property_trees()->effect_tree;
   EffectNode* node = effect_tree.Node(clips_subtree->effect_tree_index());
-  EXPECT_TRUE(node->has_render_surface);
+  EXPECT_TRUE(node->HasRenderSurface());
 }
 
 TEST_F(LayerTreeHostCommonTest, EffectNodesForNonAxisAlignedClips) {
@@ -3852,7 +3852,7 @@
   EXPECT_EQ(GetRenderSurface(front_facing_child), GetRenderSurface(root));
   EXPECT_EQ(GetRenderSurface(back_facing_child), GetRenderSurface(root));
   EXPECT_NE(GetRenderSurface(front_facing_surface), GetRenderSurface(root));
-  // We expect that a has_render_surface was created but not used.
+  // We expect that a render surface was created but not used.
   EXPECT_NE(GetRenderSurface(back_facing_surface), GetRenderSurface(root));
   EXPECT_NE(GetRenderSurface(back_facing_surface),
             GetRenderSurface(front_facing_surface));
@@ -10696,7 +10696,7 @@
   const EffectNode* effect_node =
       effect_tree.Node(rounded_corner_layer_1->effect_tree_index());
   gfx::RRectF rounded_corner_bounds_1 = effect_node->rounded_corner_bounds;
-  EXPECT_FALSE(effect_node->has_render_surface);
+  EXPECT_FALSE(effect_node->HasRenderSurface());
   EXPECT_FLOAT_EQ(rounded_corner_bounds_1.GetSimpleRadius(),
                   kRoundedCorner1Radius);
   EXPECT_EQ(rounded_corner_bounds_1.rect(),
@@ -10706,7 +10706,7 @@
   // surface. It also has 2 descendants that draw.
   effect_node = effect_tree.Node(rounded_corner_layer_2->effect_tree_index());
   gfx::RRectF rounded_corner_bounds_2 = effect_node->rounded_corner_bounds;
-  EXPECT_TRUE(effect_node->has_render_surface);
+  EXPECT_TRUE(effect_node->HasRenderSurface());
   EXPECT_FLOAT_EQ(rounded_corner_bounds_2.GetSimpleRadius(),
                   kRoundedCorner2Radius);
   EXPECT_EQ(rounded_corner_bounds_2.rect(),
@@ -10716,7 +10716,7 @@
   // the creation of a render surface.
   effect_node = effect_tree.Node(rounded_corner_layer_3->effect_tree_index());
   gfx::RRectF rounded_corner_bounds_3 = effect_node->rounded_corner_bounds;
-  EXPECT_TRUE(effect_node->has_render_surface);
+  EXPECT_TRUE(effect_node->HasRenderSurface());
   EXPECT_FLOAT_EQ(rounded_corner_bounds_3.GetSimpleRadius(),
                   kRoundedCorner3Radius);
   EXPECT_EQ(rounded_corner_bounds_3.rect(),
@@ -10726,7 +10726,7 @@
   // rounded corner, it does not need a render surface.
   effect_node = effect_tree.Node(rounded_corner_layer_4->effect_tree_index());
   gfx::RRectF rounded_corner_bounds_4 = effect_node->rounded_corner_bounds;
-  EXPECT_FALSE(effect_node->has_render_surface);
+  EXPECT_FALSE(effect_node->HasRenderSurface());
   EXPECT_FLOAT_EQ(rounded_corner_bounds_4.GetSimpleRadius(),
                   kRoundedCorner4Radius);
   EXPECT_EQ(rounded_corner_bounds_4.rect(),
@@ -10891,7 +10891,7 @@
   const EffectNode* effect_node =
       effect_tree.Node(rounded_corner_layer_1->effect_tree_index());
   gfx::RRectF rounded_corner_bounds_1 = effect_node->rounded_corner_bounds;
-  EXPECT_FALSE(effect_node->has_render_surface);
+  EXPECT_FALSE(effect_node->HasRenderSurface());
   EXPECT_FLOAT_EQ(rounded_corner_bounds_1.GetSimpleRadius(),
                   kRoundedCorner1Radius);
   EXPECT_EQ(rounded_corner_bounds_1.rect(),
@@ -10901,7 +10901,7 @@
   // has a rounded corner, it does not need a render surface.
   effect_node = effect_tree.Node(rounded_corner_layer_2->effect_tree_index());
   gfx::RRectF rounded_corner_bounds_2 = effect_node->rounded_corner_bounds;
-  EXPECT_FALSE(effect_node->has_render_surface);
+  EXPECT_FALSE(effect_node->HasRenderSurface());
   EXPECT_FLOAT_EQ(rounded_corner_bounds_2.GetSimpleRadius(),
                   kRoundedCorner2Radius);
   EXPECT_EQ(rounded_corner_bounds_2.rect(),
@@ -11018,7 +11018,7 @@
   const EffectNode* effect_node =
       effect_tree.Node(rounded_corner_layer_1->effect_tree_index());
   gfx::RRectF rounded_corner_bounds_1 = effect_node->rounded_corner_bounds;
-  EXPECT_TRUE(effect_node->has_render_surface);
+  EXPECT_TRUE(effect_node->HasRenderSurface());
   EXPECT_FLOAT_EQ(rounded_corner_bounds_1.GetSimpleRadius(),
                   kRoundedCorner1Radius);
   EXPECT_EQ(rounded_corner_bounds_1.rect(),
@@ -11028,7 +11028,7 @@
   // has a rounded corner, it does not need a render surface.
   effect_node = effect_tree.Node(rounded_corner_layer_2->effect_tree_index());
   gfx::RRectF rounded_corner_bounds_2 = effect_node->rounded_corner_bounds;
-  EXPECT_FALSE(effect_node->has_render_surface);
+  EXPECT_FALSE(effect_node->HasRenderSurface());
   EXPECT_FLOAT_EQ(rounded_corner_bounds_2.GetSimpleRadius(),
                   kRoundedCorner2Radius);
   EXPECT_EQ(rounded_corner_bounds_2.rect(),
@@ -11169,7 +11169,7 @@
   const EffectNode* effect_node =
       effect_tree.Node(fast_rounded_corner_layer->effect_tree_index());
   gfx::RRectF rounded_corner_bounds_1 = effect_node->rounded_corner_bounds;
-  EXPECT_FALSE(effect_node->has_render_surface);
+  EXPECT_FALSE(effect_node->HasRenderSurface());
   EXPECT_TRUE(effect_node->is_fast_rounded_corner);
   EXPECT_FLOAT_EQ(rounded_corner_bounds_1.GetSimpleRadius(),
                   kRoundedCorner1Radius);
@@ -11179,7 +11179,7 @@
   // Since this node has 2 descendants that draw, it will have a rounded corner.
   effect_node = effect_tree.Node(rounded_corner_layer->effect_tree_index());
   gfx::RRectF rounded_corner_bounds_2 = effect_node->rounded_corner_bounds;
-  EXPECT_TRUE(effect_node->has_render_surface);
+  EXPECT_TRUE(effect_node->HasRenderSurface());
   EXPECT_FALSE(effect_node->is_fast_rounded_corner);
   EXPECT_FLOAT_EQ(rounded_corner_bounds_2.GetSimpleRadius(),
                   kRoundedCorner2Radius);
@@ -11346,7 +11346,7 @@
   const EffectNode* effect_node =
       effect_tree.Node(rounded_corner_layer_1->effect_tree_index());
   gfx::RRectF rounded_corner_bounds_1 = effect_node->rounded_corner_bounds;
-  EXPECT_TRUE(effect_node->has_render_surface);
+  EXPECT_TRUE(effect_node->HasRenderSurface());
   EXPECT_FALSE(effect_node->is_fast_rounded_corner);
   EXPECT_FLOAT_EQ(rounded_corner_bounds_1.GetSimpleRadius(),
                   kRoundedCorner1Radius);
@@ -11358,7 +11358,7 @@
   effect_node =
       effect_tree.Node(fast_rounded_corner_layer_2->effect_tree_index());
   gfx::RRectF rounded_corner_bounds_2 = effect_node->rounded_corner_bounds;
-  EXPECT_FALSE(effect_node->has_render_surface);
+  EXPECT_FALSE(effect_node->HasRenderSurface());
   EXPECT_TRUE(effect_node->is_fast_rounded_corner);
   EXPECT_FLOAT_EQ(rounded_corner_bounds_2.GetSimpleRadius(),
                   kRoundedCorner2Radius);
@@ -11369,7 +11369,7 @@
   // render surface.
   effect_node = effect_tree.Node(rounded_corner_layer_3->effect_tree_index());
   gfx::RRectF rounded_corner_bounds_3 = effect_node->rounded_corner_bounds;
-  EXPECT_TRUE(effect_node->has_render_surface);
+  EXPECT_TRUE(effect_node->HasRenderSurface());
   EXPECT_FALSE(effect_node->is_fast_rounded_corner);
   EXPECT_FLOAT_EQ(rounded_corner_bounds_3.GetSimpleRadius(),
                   kRoundedCorner3Radius);
@@ -11379,7 +11379,7 @@
   // Since this layer no descendants, it would no thave a render pass.
   effect_node = effect_tree.Node(rounded_corner_layer_4->effect_tree_index());
   gfx::RRectF rounded_corner_bounds_4 = effect_node->rounded_corner_bounds;
-  EXPECT_FALSE(effect_node->has_render_surface);
+  EXPECT_FALSE(effect_node->HasRenderSurface());
   EXPECT_FALSE(effect_node->is_fast_rounded_corner);
   EXPECT_FLOAT_EQ(rounded_corner_bounds_4.GetSimpleRadius(),
                   kRoundedCorner4Radius);
@@ -11554,7 +11554,7 @@
   const EffectNode* effect_node =
       effect_tree.Node(fast_rounded_corner_layer_1->effect_tree_index());
   gfx::RRectF rounded_corner_bounds_1 = effect_node->rounded_corner_bounds;
-  EXPECT_TRUE(effect_node->has_render_surface);
+  EXPECT_TRUE(effect_node->HasRenderSurface());
   EXPECT_TRUE(effect_node->is_fast_rounded_corner);
   EXPECT_FLOAT_EQ(rounded_corner_bounds_1.GetSimpleRadius(),
                   kRoundedCorner1Radius);
@@ -11565,7 +11565,7 @@
   // not have a render surface.
   effect_node = effect_tree.Node(rounded_corner_layer_1->effect_tree_index());
   gfx::RRectF rounded_corner_bounds_2 = effect_node->rounded_corner_bounds;
-  EXPECT_FALSE(effect_node->has_render_surface);
+  EXPECT_FALSE(effect_node->HasRenderSurface());
   EXPECT_FALSE(effect_node->is_fast_rounded_corner);
   EXPECT_FLOAT_EQ(rounded_corner_bounds_2.GetSimpleRadius(),
                   kRoundedCorner2Radius);
@@ -11576,7 +11576,7 @@
   // render surface.
   effect_node = effect_tree.Node(rounded_corner_layer_2->effect_tree_index());
   gfx::RRectF rounded_corner_bounds_3 = effect_node->rounded_corner_bounds;
-  EXPECT_TRUE(effect_node->has_render_surface);
+  EXPECT_TRUE(effect_node->HasRenderSurface());
   EXPECT_FALSE(effect_node->is_fast_rounded_corner);
   EXPECT_FLOAT_EQ(rounded_corner_bounds_3.GetSimpleRadius(),
                   kRoundedCorner3Radius);
@@ -11586,7 +11586,7 @@
   // Since this layer has no descendant, it does not need a render surface.
   effect_node = effect_tree.Node(rounded_corner_layer_3->effect_tree_index());
   gfx::RRectF rounded_corner_bounds_4 = effect_node->rounded_corner_bounds;
-  EXPECT_FALSE(effect_node->has_render_surface);
+  EXPECT_FALSE(effect_node->HasRenderSurface());
   EXPECT_FALSE(effect_node->is_fast_rounded_corner);
   EXPECT_FLOAT_EQ(rounded_corner_bounds_4.GetSimpleRadius(),
                   kRoundedCorner4Radius);
diff --git a/cc/trees/layer_tree_host_pixeltest_masks.cc b/cc/trees/layer_tree_host_pixeltest_masks.cc
index dc63c71..aac9b10 100644
--- a/cc/trees/layer_tree_host_pixeltest_masks.cc
+++ b/cc/trees/layer_tree_host_pixeltest_masks.cc
@@ -109,7 +109,7 @@
   EffectNode isolation_effect;
   isolation_effect.clip_id = 1;
   isolation_effect.stable_id = 2;
-  isolation_effect.has_render_surface = true;
+  isolation_effect.render_surface_reason = RenderSurfaceReason::kTest;
   isolation_effect.transform_id = 1;
   property_trees.effect_tree.Insert(isolation_effect, 1);
 
@@ -167,7 +167,7 @@
   EffectNode isolation_effect;
   isolation_effect.clip_id = 1;
   isolation_effect.stable_id = 2;
-  isolation_effect.has_render_surface = true;
+  isolation_effect.render_surface_reason = RenderSurfaceReason::kTest;
   isolation_effect.transform_id = 1;
   property_trees.effect_tree.Insert(isolation_effect, 1);
 
@@ -250,7 +250,7 @@
   EffectNode isolation_effect;
   isolation_effect.clip_id = 1;
   isolation_effect.stable_id = 2;
-  isolation_effect.has_render_surface = true;
+  isolation_effect.render_surface_reason = RenderSurfaceReason::kTest;
   isolation_effect.transform_id = 1;
   property_trees.effect_tree.Insert(isolation_effect, 1);
 
@@ -313,7 +313,7 @@
   EffectNode isolation_effect;
   isolation_effect.clip_id = 1;
   isolation_effect.stable_id = 2;
-  isolation_effect.has_render_surface = true;
+  isolation_effect.render_surface_reason = RenderSurfaceReason::kTest;
   isolation_effect.transform_id = 1;
   property_trees.effect_tree.Insert(isolation_effect, 1);
 
@@ -322,7 +322,7 @@
   mask_effect.stable_id = 3;
   mask_effect.transform_id = 1;
   mask_effect.blend_mode = SkBlendMode::kDstIn;
-  mask_effect.has_render_surface = true;
+  mask_effect.render_surface_reason = RenderSurfaceReason::kTest;
   property_trees.effect_tree.Insert(mask_effect, 2);
 
   scoped_refptr<SolidColorLayer> background =
@@ -378,7 +378,7 @@
   EffectNode isolation_effect;
   isolation_effect.clip_id = 1;
   isolation_effect.stable_id = 2;
-  isolation_effect.has_render_surface = true;
+  isolation_effect.render_surface_reason = RenderSurfaceReason::kTest;
   isolation_effect.transform_id = 1;
   property_trees.effect_tree.Insert(isolation_effect, 1);
 
@@ -426,7 +426,7 @@
   EffectNode isolation_effect;
   isolation_effect.clip_id = 1;
   isolation_effect.stable_id = 2;
-  isolation_effect.has_render_surface = true;
+  isolation_effect.render_surface_reason = RenderSurfaceReason::kTest;
   isolation_effect.transform_id = 1;
   property_trees.effect_tree.Insert(isolation_effect, 1);
 
@@ -502,7 +502,7 @@
   EffectNode isolation_effect;
   isolation_effect.clip_id = 1;
   isolation_effect.stable_id = 2;
-  isolation_effect.has_render_surface = true;
+  isolation_effect.render_surface_reason = RenderSurfaceReason::kTest;
   isolation_effect.transform_id = 1;
   property_trees.effect_tree.Insert(isolation_effect, 1);
 
@@ -562,7 +562,7 @@
   EffectNode isolation_effect;
   isolation_effect.clip_id = 1;
   isolation_effect.stable_id = 2;
-  isolation_effect.has_render_surface = true;
+  isolation_effect.render_surface_reason = RenderSurfaceReason::kTest;
   isolation_effect.transform_id = 1;
   property_trees.effect_tree.Insert(isolation_effect, 1);
 
diff --git a/cc/trees/layer_tree_host_unittest.cc b/cc/trees/layer_tree_host_unittest.cc
index 25d93cc..8bab2e6 100644
--- a/cc/trees/layer_tree_host_unittest.cc
+++ b/cc/trees/layer_tree_host_unittest.cc
@@ -1581,7 +1581,8 @@
 
 // This behavior is specific to Android WebView, which only uses
 // multi-threaded compositor.
-MULTI_THREAD_TEST_F(LayerTreeHostTestPrepareTilesWithoutDraw);
+// Flaky: https://crbug.com/947673
+// MULTI_THREAD_TEST_F(LayerTreeHostTestPrepareTilesWithoutDraw);
 
 // Verify CanDraw() is false until first commit.
 class LayerTreeHostTestCantDrawBeforeCommit : public LayerTreeHostTest {
diff --git a/cc/trees/property_tree.cc b/cc/trees/property_tree.cc
index 9801773..d75cf91d 100644
--- a/cc/trees/property_tree.cc
+++ b/cc/trees/property_tree.cc
@@ -831,13 +831,13 @@
   // when we actually encounter a masking child.
   node->has_masking_child = false;
   if (node->blend_mode == SkBlendMode::kDstIn) {
-    DCHECK(parent_node->has_render_surface);
+    DCHECK(parent_node->HasRenderSurface());
     parent_node->has_masking_child = true;
   }
 }
 
 void EffectTree::UpdateSurfaceContentsScale(EffectNode* effect_node) {
-  if (!effect_node->has_render_surface) {
+  if (!effect_node->HasRenderSurface()) {
     effect_node->surface_contents_scale = gfx::Vector2dF(1.0f, 1.0f);
     return;
   }
@@ -951,7 +951,7 @@
     int node_id,
     std::vector<std::unique_ptr<viz::CopyOutputRequest>>* requests) {
   EffectNode* effect_node = Node(node_id);
-  DCHECK(effect_node->has_render_surface);
+  DCHECK(effect_node->HasRenderSurface());
   DCHECK(effect_node->has_copy_request);
 
   // The area needs to be transformed from the space of content that draws to
@@ -1071,7 +1071,7 @@
   for (int id = kContentsRootNodeId; id < static_cast<int>(size()); ++id) {
     EffectNode* effect_node = Node(id);
     bool needs_render_surface =
-        id == kContentsRootNodeId || effect_node->has_render_surface;
+        id == kContentsRootNodeId || effect_node->HasRenderSurface();
     if (needs_render_surface == !!render_surfaces_[id])
       continue;
 
@@ -1120,7 +1120,7 @@
   std::vector<std::pair<uint64_t, int>> stable_id_node_id_list;
   for (int id = kContentsRootNodeId; id < static_cast<int>(size()); ++id) {
     EffectNode* node = Node(id);
-    if (node->has_render_surface) {
+    if (node->HasRenderSurface()) {
       stable_id_node_id_list.push_back(
           std::make_pair(node->stable_id, node->id));
     }
diff --git a/cc/trees/property_tree_builder.cc b/cc/trees/property_tree_builder.cc
index bf78d54..e5f34003 100644
--- a/cc/trees/property_tree_builder.cc
+++ b/cc/trees/property_tree_builder.cc
@@ -708,11 +708,11 @@
   return layer->test_properties()->cache_render_surface;
 }
 
-static inline bool ForceRenderSurface(Layer* layer) {
+static inline bool ForceRenderSurfaceForTesting(Layer* layer) {
   return layer->force_render_surface_for_testing();
 }
 
-static inline bool ForceRenderSurface(LayerImpl* layer) {
+static inline bool ForceRenderSurfaceForTesting(LayerImpl* layer) {
   return layer->test_properties()->force_render_surface;
 }
 
@@ -838,40 +838,40 @@
 }
 
 template <typename LayerType>
-bool ShouldCreateRenderSurface(const MutatorHost& mutator_host,
-                               LayerType* layer,
-                               gfx::Transform current_transform,
-                               bool animation_axis_aligned) {
+RenderSurfaceReason ComputeRenderSurfaceReason(const MutatorHost& mutator_host,
+                                               LayerType* layer,
+                                               gfx::Transform current_transform,
+                                               bool animation_axis_aligned) {
   const bool preserves_2d_axis_alignment =
       current_transform.Preserves2dAxisAlignment() && animation_axis_aligned;
   const bool is_root = !LayerParent(layer);
   if (is_root)
-    return true;
+    return RenderSurfaceReason::kRoot;
 
   // If the layer uses a mask.
   if (MaskLayer(layer)) {
-    return true;
+    return RenderSurfaceReason::kMask;
   }
 
   // If the layer uses trilinear filtering.
   if (TrilinearFiltering(layer)) {
-    return true;
+    return RenderSurfaceReason::kTrilinearFiltering;
   }
 
   // If the layer uses a CSS filter.
   if (!Filters(layer).IsEmpty()) {
-    return true;
+    return RenderSurfaceReason::kFilter;
   }
 
   // If the layer uses a CSS backdrop-filter.
   if (!BackdropFilters(layer).IsEmpty()) {
-    return true;
+    return RenderSurfaceReason::kBackdropFilter;
   }
 
   // If the layer will use a CSS filter.  In this case, the animation
   // will start and add a filter to this layer, so it needs a surface.
   if (HasPotentiallyRunningFilterAnimation(mutator_host, layer)) {
-    return true;
+    return RenderSurfaceReason::kFilterAnimation;
   }
 
   int num_descendants_that_draw_content = NumDescendantsThatDrawContent(layer);
@@ -880,15 +880,12 @@
   // parent (i.e. parent participates in a 3D rendering context).
   if (LayerIsInExisting3DRenderingContext(layer) &&
       ShouldFlattenTransform(layer) && num_descendants_that_draw_content > 0) {
-    TRACE_EVENT_INSTANT0(
-        "cc", "PropertyTreeBuilder::ShouldCreateRenderSurface flattening",
-        TRACE_EVENT_SCOPE_THREAD);
-    return true;
+    return RenderSurfaceReason::k3dTransformFlattening;
   }
 
   if (!IsFastRoundedCorner(layer) && HasRoundedCorner(layer) &&
       num_descendants_that_draw_content > 1) {
-    return true;
+    return RenderSurfaceReason::kRoundedCorner;
   }
 
   // If the layer has blending.
@@ -896,20 +893,14 @@
   // types of quads than viz::RenderPassDrawQuad. Layers having descendants that
   // draw content will still create a separate rendering surface.
   if (BlendMode(layer) != SkBlendMode::kSrcOver) {
-    TRACE_EVENT_INSTANT0(
-        "cc", "PropertyTreeBuilder::ShouldCreateRenderSurface blending",
-        TRACE_EVENT_SCOPE_THREAD);
-    return true;
+    return RenderSurfaceReason::kBlendMode;
   }
   // If the layer clips its descendants but it is not axis-aligned with respect
   // to its parent.
   bool layer_clips_external_content = LayerClipsSubtree(layer);
   if (layer_clips_external_content && !preserves_2d_axis_alignment &&
       num_descendants_that_draw_content > 0) {
-    TRACE_EVENT_INSTANT0(
-        "cc", "PropertyTreeBuilder::ShouldCreateRenderSurface clipping",
-        TRACE_EVENT_SCOPE_THREAD);
-    return true;
+    return RenderSurfaceReason::kClipAxisAlignment;
   }
 
   // If the layer has some translucency and does not have a preserves-3d
@@ -926,11 +917,8 @@
       HasPotentiallyRunningOpacityAnimation(mutator_host, layer);
   if (may_have_transparency && ShouldFlattenTransform(layer) &&
       at_least_two_layers_in_subtree_draw_content) {
-    TRACE_EVENT_INSTANT0(
-        "cc", "PropertyTreeBuilder::ShouldCreateRenderSurface opacity",
-        TRACE_EVENT_SCOPE_THREAD);
     DCHECK(!is_root);
-    return true;
+    return RenderSurfaceReason::kOpacity;
   }
   // If the layer has isolation.
   // TODO(rosca): to be optimized - create separate rendering surface only when
@@ -938,25 +926,22 @@
   // (layer has transparent background or descendants overflow).
   // https://code.google.com/p/chromium/issues/detail?id=301738
   if (IsRootForIsolatedGroup(layer)) {
-    TRACE_EVENT_INSTANT0(
-        "cc", "PropertyTreeBuilder::ShouldCreateRenderSurface isolation",
-        TRACE_EVENT_SCOPE_THREAD);
-    return true;
+    return RenderSurfaceReason::kRootOrIsolatedGroup;
   }
 
   // If we force it.
-  if (ForceRenderSurface(layer))
-    return true;
+  if (ForceRenderSurfaceForTesting(layer))
+    return RenderSurfaceReason::kTest;
 
   // If we cache it.
   if (CacheRenderSurface(layer))
-    return true;
+    return RenderSurfaceReason::kCache;
 
   // If we'll make a copy of the layer's contents.
   if (HasCopyRequest(layer))
-    return true;
+    return RenderSurfaceReason::kCopyRequest;
 
-  return false;
+  return RenderSurfaceReason::kNone;
 }
 
 static void TakeCopyRequests(
@@ -1020,10 +1005,12 @@
   data_for_children->animation_axis_aligned_since_render_target &=
       AnimationsPreserveAxisAlignment(mutator_host_, layer);
   data_for_children->compound_transform_since_render_target *= Transform(layer);
-  const bool should_create_render_surface = ShouldCreateRenderSurface(
+  auto render_surface_reason = ComputeRenderSurfaceReason(
       mutator_host_, layer,
       data_for_children->compound_transform_since_render_target,
       data_for_children->animation_axis_aligned_since_render_target);
+  bool should_create_render_surface =
+      render_surface_reason != RenderSurfaceReason::kNone;
 
   bool not_axis_aligned_since_last_clip =
       data_from_ancestor.not_axis_aligned_since_last_clip
@@ -1055,7 +1042,6 @@
   node->opacity = Opacity(layer);
   node->blend_mode = BlendMode(layer);
   node->unscaled_mask_target_size = layer->bounds();
-  node->has_render_surface = should_create_render_surface;
   node->cache_render_surface = CacheRenderSurface(layer);
   node->has_copy_request = HasCopyRequest(layer);
   node->filters = Filters(layer);
@@ -1073,6 +1059,7 @@
   node->is_currently_animating_filter = FilterIsAnimating(mutator_host_, layer);
   node->effect_changed = PropertyChanged(layer);
   node->subtree_has_copy_request = SubtreeHasCopyRequest(layer);
+  node->render_surface_reason = render_surface_reason;
   node->closest_ancestor_with_cached_render_surface_id =
       CacheRenderSurface(layer)
           ? node_id
@@ -1172,7 +1159,7 @@
   // single rrect per quad at draw time, it would be unable to handle
   // intersections thus resulting in artifacts.
   if (subtree_has_rounded_corner && has_rounded_corner)
-    effect_node->has_render_surface = true;
+    effect_node->render_surface_reason = RenderSurfaceReason::kRoundedCorner;
 
   // Inform the parent that its subtree has rounded corners if one of the two
   // scenario is true:
@@ -1183,9 +1170,9 @@
   // surface of its own to prevent blending artifacts due to intersecting
   // rounded corners.
   *data_for_children->subtree_has_rounded_corner =
-      (subtree_has_rounded_corner && !effect_node->has_render_surface) ||
+      (subtree_has_rounded_corner && !effect_node->HasRenderSurface()) ||
       has_rounded_corner;
-  return effect_node->has_render_surface;
+  return effect_node->HasRenderSurface();
 }
 
 static inline bool UserScrollableHorizontal(Layer* layer) {
diff --git a/cc/trees/property_tree_unittest.cc b/cc/trees/property_tree_unittest.cc
index 1e0a02a..848d6e0 100644
--- a/cc/trees/property_tree_unittest.cc
+++ b/cc/trees/property_tree_unittest.cc
@@ -195,7 +195,8 @@
 
   int grand_parent = tree.Insert(TransformNode(), 0);
   int effect_grand_parent = effect_tree.Insert(EffectNode(), 0);
-  effect_tree.Node(effect_grand_parent)->has_render_surface = true;
+  effect_tree.Node(effect_grand_parent)->render_surface_reason =
+      RenderSurfaceReason::kTest;
   effect_tree.Node(effect_grand_parent)->transform_id = grand_parent;
   effect_tree.Node(effect_grand_parent)->surface_contents_scale =
       gfx::Vector2dF(1.f, 1.f);
@@ -207,7 +208,8 @@
   int parent = tree.Insert(TransformNode(), grand_parent);
   int effect_parent = effect_tree.Insert(EffectNode(), effect_grand_parent);
   effect_tree.Node(effect_parent)->transform_id = parent;
-  effect_tree.Node(effect_parent)->has_render_surface = true;
+  effect_tree.Node(effect_parent)->render_surface_reason =
+      RenderSurfaceReason::kTest;
   effect_tree.Node(effect_parent)->surface_contents_scale =
       gfx::Vector2dF(1.f, 1.f);
   tree.Node(parent)->source_node_id = grand_parent;
@@ -493,7 +495,8 @@
 
   int parent = tree.Insert(TransformNode(), 0);
   int effect_parent = effect_tree.Insert(EffectNode(), 0);
-  effect_tree.Node(effect_parent)->has_render_surface = true;
+  effect_tree.Node(effect_parent)->render_surface_reason =
+      RenderSurfaceReason::kTest;
   effect_tree.Node(effect_parent)->surface_contents_scale =
       gfx::Vector2dF(1.f, 1.f);
   tree.Node(parent)->scrolls = true;
@@ -547,7 +550,7 @@
 
   EffectTree& effect_tree = property_trees.effect_tree;
   EffectNode effect_node;
-  effect_node.has_render_surface = true;
+  effect_node.render_surface_reason = RenderSurfaceReason::kTest;
   effect_node.has_copy_request = true;
   effect_node.transform_id = contents_root.id;
   effect_node.id = effect_tree.Insert(effect_node, 0);
@@ -646,7 +649,7 @@
 
   EffectTree& effect_tree = property_trees.effect_tree;
   EffectNode effect_node;
-  effect_node.has_render_surface = true;
+  effect_node.render_surface_reason = RenderSurfaceReason::kTest;
   effect_node.has_copy_request = true;
   effect_node.transform_id = contents_root.id;
   effect_node.id = effect_tree.Insert(effect_node, 0);
diff --git a/chrome/android/BUILD.gn b/chrome/android/BUILD.gn
index 8a553a2..99c043b 100644
--- a/chrome/android/BUILD.gn
+++ b/chrome/android/BUILD.gn
@@ -475,6 +475,8 @@
 java_group("chrome_all_java") {
   deps = [
     ":chrome_java",
+    "//chrome/android/features/autofill_assistant:animated_poodle_java",
+    "//chrome/android/features/autofill_assistant:java",
     "//chrome/android/features/keyboard_accessory:internal_java",
     "//chrome/android/features/media_router:java",
   ]
@@ -1763,7 +1765,6 @@
                              "module_name",
                              "verify_android_configuration",
                              "proguard_jar_path",
-                             "resource_ids_provider_dep",
                              "static_library_provider",
                              "target_type",
                              "use_trichrome_library",
@@ -1815,13 +1816,18 @@
     android_manifest_dep = ":trichrome_library_android_manifest"
     if (trichrome_synchronized_proguard) {
       static_library_dependent_targets = [
-        "//android_webview:trichrome_webview_apk",
-
-        # Webview must be listed first WebView's R classes take precedence.
-        # TODO(http://crbug.com/901465): Make this less subtle by handling
-        # order in writing_build_config.py by prioritizing the entry that
-        # matches resource_ids_provider_dep.
-        ":trichrome_chrome_apk",
+        {
+          name = "//android_webview:trichrome_webview_apk"
+          is_resource_ids_provider = true
+          is_compressed_locales_provider = false
+          is_uncompressed_locales_provider = true
+        },
+        {
+          name = ":trichrome_chrome_apk"
+          is_resource_ids_provider = false
+          is_compressed_locales_provider = true
+          is_uncompressed_locales_provider = false
+        },
       ]
     }
   }
@@ -1834,13 +1840,18 @@
     android_manifest_dep = ":trichrome_library_android_manifest"
     if (trichrome_synchronized_proguard) {
       static_library_dependent_targets = [
-        "//android_webview:trichrome_webview_for_bundle_apk",
-
-        # Webview must be listed first WebView's R classes take precedence.
-        # TODO(http://crbug.com/901465): Make this less subtle by handling
-        # order in write_build_config.py by prioritizing the entry that
-        # matches resource_ids_provider_dep.
-        ":trichrome_chrome_bundle",
+        {
+          name = "//android_webview:trichrome_webview_for_bundle_apk"
+          is_resource_ids_provider = true
+          is_compressed_locales_provider = false
+          is_uncompressed_locales_provider = true
+        },
+        {
+          name = ":trichrome_chrome_bundle"
+          is_resource_ids_provider = false
+          is_compressed_locales_provider = true
+          is_uncompressed_locales_provider = false
+        },
       ]
     }
   }
@@ -1861,7 +1872,6 @@
   use_trichrome_library = true
   if (trichrome_synchronized_proguard) {
     static_library_provider = ":trichrome_library_apk"
-    resource_ids_provider_dep = "//android_webview:trichrome_webview_apk"
   }
 }
 
@@ -2173,10 +2183,6 @@
         !_is_trichrome) {
       verify_android_configuration = true
     }
-    if (trichrome_synchronized_proguard) {
-      resource_ids_provider_dep =
-          "//android_webview:trichrome_webview_for_bundle_apk"
-    }
   }
 
   if (enable_arcore || enable_vr) {
@@ -2266,12 +2272,6 @@
         },
       ]
     }
-    extra_modules += [
-      {
-        name = "autofill_assistant"
-        module_target = ":${target_name}__autofill_assistant_bundle_module"
-      },
-    ]
   }
 }
 
diff --git a/chrome/android/chrome_java_sources.gni b/chrome/android/chrome_java_sources.gni
index d4a8d4e..6917ac9a 100644
--- a/chrome/android/chrome_java_sources.gni
+++ b/chrome/android/chrome_java_sources.gni
@@ -1112,6 +1112,10 @@
   "java/src/org/chromium/chrome/browser/omnibox/suggestions/editurl/EditUrlSuggestionProcessor.java",
   "java/src/org/chromium/chrome/browser/omnibox/suggestions/editurl/EditUrlSuggestionProperties.java",
   "java/src/org/chromium/chrome/browser/omnibox/suggestions/editurl/EditUrlSuggestionViewBinder.java",
+  "java/src/org/chromium/chrome/browser/omnibox/suggestions/entity/EntitySuggestionProcessor.java",
+  "java/src/org/chromium/chrome/browser/omnibox/suggestions/entity/EntitySuggestionView.java",
+  "java/src/org/chromium/chrome/browser/omnibox/suggestions/entity/EntitySuggestionViewBinder.java",
+  "java/src/org/chromium/chrome/browser/omnibox/suggestions/entity/EntitySuggestionViewProperties.java",
   "java/src/org/chromium/chrome/browser/page_info/CertificateChainHelper.java",
   "java/src/org/chromium/chrome/browser/page_info/CertificateViewer.java",
   "java/src/org/chromium/chrome/browser/page_info/ConnectionInfoPopup.java",
diff --git a/chrome/android/chrome_test_java_sources.gni b/chrome/android/chrome_test_java_sources.gni
index d316c192..a20f4d1 100644
--- a/chrome/android/chrome_test_java_sources.gni
+++ b/chrome/android/chrome_test_java_sources.gni
@@ -84,7 +84,6 @@
   "javatests/src/org/chromium/chrome/browser/contacts_picker/ContactsPickerDialogTest.java",
   "javatests/src/org/chromium/chrome/browser/contextmenu/ContextMenuTest.java",
   "javatests/src/org/chromium/chrome/browser/contextmenu/TabularContextMenuUiTest.java",
-  "javatests/src/org/chromium/chrome/browser/contextual_suggestions/ContextualSuggestionsTest.java",
   "javatests/src/org/chromium/chrome/browser/contextual_suggestions/EmptyEnabledStateMonitor.java",
   "javatests/src/org/chromium/chrome/browser/contextual_suggestions/EnabledStateMonitorTest.java",
   "javatests/src/org/chromium/chrome/browser/contextual_suggestions/FakeContextualSuggestionsSource.java",
diff --git a/chrome/android/features/tab_ui/BUILD.gn b/chrome/android/features/tab_ui/BUILD.gn
index 8e21108..4dea80a 100644
--- a/chrome/android/features/tab_ui/BUILD.gn
+++ b/chrome/android/features/tab_ui/BUILD.gn
@@ -20,6 +20,9 @@
     "java/src/org/chromium/chrome/browser/tasks/tab_management/GridTabSwitcherMediator.java",
     "java/src/org/chromium/chrome/browser/tasks/tab_management/MultiThumbnailCardProvider.java",
     "java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridContainerViewBinder.java",
+    "java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogCoordinator.java",
+    "java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogMediator.java",
+    "java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogParent.java",
     "java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridSheetCoordinator.java",
     "java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridSheetMediator.java",
     "java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridSheetContent.java",
@@ -34,6 +37,7 @@
     "java/src/org/chromium/chrome/browser/tasks/tab_management/TabGroupUiToolbarViewBinder.java",
     "java/src/org/chromium/chrome/browser/tasks/tab_management/TabListContainerProperties.java",
     "java/src/org/chromium/chrome/browser/tasks/tab_management/TabListCoordinator.java",
+    "java/src/org/chromium/chrome/browser/tasks/tab_management/TabListFaviconProvider.java",
     "java/src/org/chromium/chrome/browser/tasks/tab_management/TabListMediator.java",
     "java/src/org/chromium/chrome/browser/tasks/tab_management/TabListModel.java",
     "java/src/org/chromium/chrome/browser/tasks/tab_management/TabListRecyclerView.java",
@@ -43,7 +47,6 @@
     "java/src/org/chromium/chrome/browser/tasks/tab_management/TabStripToolbarViewProperties.java",
     "java/src/org/chromium/chrome/browser/tasks/tab_management/TabStripViewBinder.java",
     "java/src/org/chromium/chrome/browser/tasks/tab_management/TabStripViewHolder.java",
-    "java/src/org/chromium/chrome/browser/tasks/tab_management/TabListFaviconProvider.java",
   ]
 
   deps = [
diff --git a/chrome/android/features/tab_ui/java/res/layout/bottom_tab_grid_toolbar.xml b/chrome/android/features/tab_ui/java/res/layout/bottom_tab_grid_toolbar.xml
index e9314d1..5c4d46a 100644
--- a/chrome/android/features/tab_ui/java/res/layout/bottom_tab_grid_toolbar.xml
+++ b/chrome/android/features/tab_ui/java/res/layout/bottom_tab_grid_toolbar.xml
@@ -14,7 +14,8 @@
         android:layout_width="match_parent"
         android:layout_height="@dimen/bottom_sheet_peek_height"
         android:orientation="horizontal"
-        android:gravity="center_vertical">
+        android:gravity="center_vertical"
+        android:clickable="true">
         <org.chromium.ui.widget.ChromeImageView
             android:id="@+id/toolbar_left_button"
             style="@style/BottomToolbarButton"
diff --git a/chrome/android/features/tab_ui/java/res/values/dimens.xml b/chrome/android/features/tab_ui/java/res/values/dimens.xml
index 4578919..d31dd44 100644
--- a/chrome/android/features/tab_ui/java/res/values/dimens.xml
+++ b/chrome/android/features/tab_ui/java/res/values/dimens.xml
@@ -10,6 +10,8 @@
     <dimen name="tab_list_mini_card_radius">4dp</dimen>
     <dimen name="tab_list_mini_card_frame_size">1dp</dimen>
     <dimen name="tab_grid_close_button_size">18dp</dimen>
+    <dimen name="tab_grid_dialog_side_margin">16dp</dimen>
+    <dimen name="tab_grid_dialog_top_margin">85dp</dimen>
     <dimen name="tab_grid_thumbnail_card_default_size">152dp</dimen>
     <dimen name="tab_grid_thumbnail_favicon_frame_padding">16dp</dimen>
     <dimen name="tab_grid_thumbnail_favicon_padding">24dp</dimen>
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/GridTabSwitcherCoordinator.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/GridTabSwitcherCoordinator.java
index b7641780..474a57e4 100644
--- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/GridTabSwitcherCoordinator.java
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/GridTabSwitcherCoordinator.java
@@ -14,10 +14,12 @@
 import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
 import org.chromium.chrome.browser.lifecycle.Destroyable;
 import org.chromium.chrome.browser.tab.Tab;
+import org.chromium.chrome.browser.tabmodel.TabCreatorManager;
 import org.chromium.chrome.browser.tabmodel.TabList;
 import org.chromium.chrome.browser.tabmodel.TabModel;
 import org.chromium.chrome.browser.tabmodel.TabModelSelector;
 import org.chromium.chrome.browser.toolbar.ToolbarManager;
+import org.chromium.chrome.browser.util.FeatureUtilities;
 import org.chromium.ui.modelutil.PropertyModel;
 import org.chromium.ui.modelutil.PropertyModelChangeProcessor;
 
@@ -35,15 +37,32 @@
     private final TabListCoordinator mTabGridCoordinator;
     private final GridTabSwitcherMediator mMediator;
     private final MultiThumbnailCardProvider mMultiThumbnailCardProvider;
+    private final TabGridDialogCoordinator mTabGridDialogCoordinator;
 
     public GridTabSwitcherCoordinator(Context context,
             ActivityLifecycleDispatcher lifecycleDispatcher, ToolbarManager toolbarManager,
             TabModelSelector tabModelSelector, TabContentManager tabContentManager,
-            CompositorViewHolder compositorViewHolder, ChromeFullscreenManager fullscreenManager) {
+            CompositorViewHolder compositorViewHolder, ChromeFullscreenManager fullscreenManager,
+            TabCreatorManager tabCreatorManager) {
         PropertyModel containerViewModel = new PropertyModel(TabListContainerProperties.ALL_KEYS);
+        TabListMediator.GridCardOnClickListenerProvider gridCardOnClickListenerProvider;
+        if (FeatureUtilities.isTabGroupsAndroidUiImprovementsEnabled()) {
+            mTabGridDialogCoordinator = new TabGridDialogCoordinator(context, tabModelSelector,
+                    tabContentManager, tabCreatorManager, new CompositorViewHolder(context), this);
 
-        mMediator = new GridTabSwitcherMediator(this, containerViewModel, tabModelSelector,
-                fullscreenManager, compositorViewHolder);
+            mMediator = new GridTabSwitcherMediator(this, containerViewModel, tabModelSelector,
+                    fullscreenManager, compositorViewHolder,
+                    mTabGridDialogCoordinator.getResetHandler());
+
+            gridCardOnClickListenerProvider = mMediator::getGridCardOnClickListener;
+        } else {
+            mTabGridDialogCoordinator = null;
+
+            mMediator = new GridTabSwitcherMediator(this, containerViewModel, tabModelSelector,
+                    fullscreenManager, compositorViewHolder, null);
+
+            gridCardOnClickListenerProvider = null;
+        }
 
         mMultiThumbnailCardProvider =
                 new MultiThumbnailCardProvider(context, tabContentManager, tabModelSelector);
@@ -60,8 +79,8 @@
 
         mTabGridCoordinator = new TabListCoordinator(TabListCoordinator.TabListMode.GRID, context,
                 tabModelSelector, mMultiThumbnailCardProvider, titleProvider, true,
-                mMediator::getCreateGroupButtonOnClickListener, compositorViewHolder, true,
-                COMPONENT_NAME);
+                mMediator::getCreateGroupButtonOnClickListener, gridCardOnClickListenerProvider,
+                compositorViewHolder, true, COMPONENT_NAME);
 
         mContainerViewChangeProcessor = PropertyModelChangeProcessor.create(containerViewModel,
                 mTabGridCoordinator.getContainerView(), TabGridContainerViewBinder::bind);
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/GridTabSwitcherMediator.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/GridTabSwitcherMediator.java
index 31c7bfc..1c3d82a9 100644
--- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/GridTabSwitcherMediator.java
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/GridTabSwitcherMediator.java
@@ -38,6 +38,8 @@
 import org.chromium.content_public.browser.LoadUrlParams;
 import org.chromium.ui.modelutil.PropertyModel;
 
+import java.util.List;
+
 /**
  * The Mediator that is responsible for resetting the tab grid based on visibility and model
  * changes.
@@ -55,6 +57,7 @@
     private final TabModelSelectorObserver mTabModelSelectorObserver;
     private final ObserverList<OverviewModeObserver> mObservers = new ObserverList<>();
     private final ChromeFullscreenManager mFullscreenManager;
+    private final TabGridDialogMediator.ResetHandler mTabGridDialogResetHandler;
     private final ChromeFullscreenManager.FullscreenListener mFullscreenListener =
             new ChromeFullscreenManager.FullscreenListener() {
                 @Override
@@ -99,7 +102,8 @@
      */
     GridTabSwitcherMediator(ResetHandler resetHandler, PropertyModel containerViewModel,
             TabModelSelector tabModelSelector, ChromeFullscreenManager fullscreenManager,
-            CompositorViewHolder compositorViewHolder) {
+            CompositorViewHolder compositorViewHolder,
+            TabGridDialogMediator.ResetHandler tabGridDialogResetHandler) {
         mResetHandler = resetHandler;
         mContainerViewModel = containerViewModel;
         mTabModelSelector = tabModelSelector;
@@ -150,6 +154,7 @@
                 BOTTOM_CONTROLS_HEIGHT, fullscreenManager.getBottomControlsHeight());
 
         mCompositorViewHolder = compositorViewHolder;
+        mTabGridDialogResetHandler = tabGridDialogResetHandler;
     }
 
     private void setVisibility(boolean isVisible) {
@@ -245,6 +250,14 @@
     }
 
     @Nullable
+    TabListMediator.TabActionListener getGridCardOnClickListener(Tab tab) {
+        if (!ableToOpenDialog(tab)) return null;
+        return tabId -> {
+            mTabGridDialogResetHandler.resetWithListOfTabs(getRelatedTabs(tabId));
+        };
+    }
+
+    @Nullable
     TabListMediator.TabActionListener getCreateGroupButtonOnClickListener(Tab tab) {
         if (!ableToCreateGroup(tab)) return null;
 
@@ -261,10 +274,18 @@
     private boolean ableToCreateGroup(Tab tab) {
         return FeatureUtilities.isTabGroupsAndroidEnabled()
                 && mTabModelSelector.isIncognitoSelected() == tab.isIncognito()
-                && mTabModelSelector.getTabModelFilterProvider()
-                           .getCurrentTabModelFilter()
-                           .getRelatedTabList(tab.getId())
-                           .size()
-                == 1;
+                && getRelatedTabs(tab.getId()).size() == 1;
+    }
+
+    private boolean ableToOpenDialog(Tab tab) {
+        return FeatureUtilities.isTabGroupsAndroidEnabled()
+                && mTabModelSelector.isIncognitoSelected() == tab.isIncognito()
+                && getRelatedTabs(tab.getId()).size() != 1;
+    }
+
+    private List<Tab> getRelatedTabs(int tabId) {
+        return mTabModelSelector.getTabModelFilterProvider()
+                .getCurrentTabModelFilter()
+                .getRelatedTabList(tabId);
     }
 }
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogCoordinator.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogCoordinator.java
new file mode 100644
index 0000000..5b9ecf11
--- /dev/null
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogCoordinator.java
@@ -0,0 +1,84 @@
+// 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.tasks.tab_management;
+
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.view.ViewGroup;
+
+import org.chromium.chrome.browser.compositor.CompositorViewHolder;
+import org.chromium.chrome.browser.compositor.layouts.content.TabContentManager;
+import org.chromium.chrome.browser.tab.Tab;
+import org.chromium.chrome.browser.tabmodel.TabCreatorManager;
+import org.chromium.chrome.browser.tabmodel.TabModelSelector;
+import org.chromium.ui.modelutil.PropertyModel;
+
+import java.util.List;
+
+/**
+ * A coordinator for TabGridDialog component. Manages the communication with
+ * {@link TabListCoordinator} as well as the life-cycle of shared component
+ * objects.
+ */
+public class TabGridDialogCoordinator {
+    final static String COMPONENT_NAME = "TabGridDialog";
+    private final Context mContext;
+    private final TabListCoordinator mTabListCoordinator;
+    private final TabGridDialogMediator mMediator;
+    private final PropertyModel mToolbarPropertyModel;
+    private TabGridSheetToolbarCoordinator mToolbarCoordinator;
+    private ViewGroup mParentView;
+    private TabGridDialogParent mParentLayout;
+
+    TabGridDialogCoordinator(Context context, TabModelSelector tabModelSelector,
+            TabContentManager tabContentManager, TabCreatorManager tabCreatorManager,
+            CompositorViewHolder compositorViewHolder,
+            GridTabSwitcherMediator.ResetHandler resetHandler) {
+        mContext = context;
+
+        mToolbarPropertyModel = new PropertyModel(TabGridSheetProperties.ALL_KEYS);
+
+        mTabListCoordinator = new TabListCoordinator(TabListCoordinator.TabListMode.GRID, context,
+                tabModelSelector, tabContentManager::getTabThumbnailWithCallback, null, false, null,
+                null, compositorViewHolder, false, COMPONENT_NAME);
+
+        mMediator = new TabGridDialogMediator(context, this::resetWithListOfTabs,
+                mToolbarPropertyModel, tabModelSelector, tabCreatorManager, resetHandler);
+
+        mParentView = compositorViewHolder;
+
+        mParentLayout = new TabGridDialogParent(context);
+    }
+
+    /**
+     * Destroy any members that needs clean up.
+     */
+    public void destroy() {
+        mTabListCoordinator.destroy();
+        mMediator.destroy();
+    }
+
+    private void updateDialogContent(List<Tab> tabs) {
+        if (tabs != null) {
+            TabListRecyclerView recyclerView = mTabListCoordinator.getContainerView();
+            mToolbarCoordinator = new TabGridSheetToolbarCoordinator(
+                    mContext, recyclerView, mToolbarPropertyModel, mParentView, mParentLayout);
+        } else {
+            if (mToolbarCoordinator != null) {
+                mToolbarCoordinator.destroy();
+            }
+        }
+    }
+
+    TabGridDialogMediator.ResetHandler getResetHandler() {
+        return this::resetWithListOfTabs;
+    }
+
+    public void resetWithListOfTabs(@Nullable List<Tab> tabs) {
+        mTabListCoordinator.resetWithListOfTabs(tabs);
+        updateDialogContent(tabs);
+        mMediator.onReset(tabs == null ? null : tabs.get(0).getId());
+    }
+}
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogMediator.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogMediator.java
new file mode 100644
index 0000000..a777fa501
--- /dev/null
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogMediator.java
@@ -0,0 +1,192 @@
+// 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.tasks.tab_management;
+
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.view.View;
+
+import org.chromium.base.metrics.RecordUserAction;
+import org.chromium.chrome.browser.UrlConstants;
+import org.chromium.chrome.browser.tab.Tab;
+import org.chromium.chrome.browser.tabmodel.EmptyTabModelObserver;
+import org.chromium.chrome.browser.tabmodel.TabCreatorManager;
+import org.chromium.chrome.browser.tabmodel.TabLaunchType;
+import org.chromium.chrome.browser.tabmodel.TabModelObserver;
+import org.chromium.chrome.browser.tabmodel.TabModelSelector;
+import org.chromium.chrome.browser.tabmodel.TabSelectionType;
+import org.chromium.chrome.browser.widget.ScrimView;
+import org.chromium.content_public.browser.LoadUrlParams;
+import org.chromium.ui.modelutil.PropertyModel;
+
+import java.util.List;
+
+/**
+ * A mediator for the TabGridDialog component, responsible for communicating
+ * with the components' coordinator as well as managing the business logic
+ * for dialog show/hide.
+ */
+public class TabGridDialogMediator {
+    /**
+     * Defines an interface for a {@link TabGridDialogMediator} reset event handler.
+     */
+    interface ResetHandler {
+        /**
+         * Handles a reset event originated from {@link TabGridDialogMediator} and {@link
+         * GridTabSwitcherMediator}.
+         *
+         * @param tabs List of Tabs to reset.
+         */
+        void resetWithListOfTabs(@Nullable List<Tab> tabs);
+    }
+
+    private final Context mContext;
+    private final PropertyModel mModel;
+    private final TabModelSelector mTabModelSelector;
+    private final TabModelObserver mTabModelObserver;
+    private final TabCreatorManager mTabCreatorManager;
+    private final TabGridDialogMediator.ResetHandler mDialogResetHandler;
+    private final GridTabSwitcherMediator.ResetHandler mGridTabSwitcherResetHandler;
+    private int mCurrentTabId = Tab.INVALID_TAB_ID;
+
+    TabGridDialogMediator(Context context, TabGridDialogMediator.ResetHandler dialogResetHandler,
+            PropertyModel model, TabModelSelector tabModelSelector,
+            TabCreatorManager tabCreatorManager,
+            GridTabSwitcherMediator.ResetHandler gridTabSwitcherResetHandler) {
+        mContext = context;
+        mModel = model;
+        mTabModelSelector = tabModelSelector;
+        mTabCreatorManager = tabCreatorManager;
+        mDialogResetHandler = dialogResetHandler;
+        mGridTabSwitcherResetHandler = gridTabSwitcherResetHandler;
+
+        // Register for tab model.
+        mTabModelObserver = new EmptyTabModelObserver() {
+            @Override
+            public void didAddTab(Tab tab, @TabLaunchType int type) {
+                updateDialog();
+                updateGridTabSwitcher();
+            }
+
+            @Override
+            public void tabClosureUndone(Tab tab) {
+                updateDialog();
+                updateGridTabSwitcher();
+            }
+
+            @Override
+            public void didSelectTab(Tab tab, int type, int lastId) {
+                if (type == TabSelectionType.FROM_USER)
+                    mModel.set(TabGridSheetProperties.IS_DIALOG_VISIBLE, false);
+            }
+
+            @Override
+            public void willCloseTab(Tab tab, boolean animate) {
+                updateDialog();
+                updateGridTabSwitcher();
+                List<Tab> relatedTabs = getRelatedTabs(tab.getId());
+                // If current tab is closed and tab group is not empty, hand over ID of the next
+                // tab in the group to mCurrentTabId.
+                if (relatedTabs.size() == 0) return;
+                if (tab.getId() == mCurrentTabId) {
+                    mCurrentTabId = relatedTabs.get(0).getId();
+                }
+            }
+        };
+        mTabModelSelector.getTabModelFilterProvider().addTabModelFilterObserver(mTabModelObserver);
+
+        // Setup toolbar property model.
+        setupToolbarClickHandlers();
+
+        // Setup ScrimView observer.
+        setupScrimViewObserver();
+    }
+
+    void onReset(Integer tabId) {
+        if (tabId != null) {
+            mCurrentTabId = tabId;
+            updateDialog();
+            mModel.set(TabGridSheetProperties.IS_DIALOG_VISIBLE, true);
+        } else {
+            mModel.set(TabGridSheetProperties.IS_DIALOG_VISIBLE, false);
+        }
+    }
+
+    /**
+     * Destroy any members that needs clean up.
+     */
+    public void destroy() {
+        if (mTabModelObserver != null) {
+            mTabModelSelector.getTabModelFilterProvider().removeTabModelFilterObserver(
+                    mTabModelObserver);
+        }
+    }
+
+    private void updateGridTabSwitcher() {
+        mGridTabSwitcherResetHandler.resetWithTabList(
+                mTabModelSelector.getTabModelFilterProvider().getCurrentTabModelFilter());
+    }
+
+    private void updateDialog() {
+        if (mCurrentTabId == Tab.INVALID_TAB_ID) return;
+        List<Tab> relatedTabs = getRelatedTabs(mCurrentTabId);
+        int tabsCount = relatedTabs.size();
+        if (tabsCount == 0) {
+            mDialogResetHandler.resetWithListOfTabs(null);
+            return;
+        }
+        mModel.set(TabGridSheetProperties.HEADER_TITLE,
+                mContext.getResources().getQuantityString(
+                        org.chromium.chrome.R.plurals.bottom_tab_grid_title_placeholder, tabsCount,
+                        tabsCount));
+    }
+
+    private void setupToolbarClickHandlers() {
+        mModel.set(
+                TabGridSheetProperties.COLLAPSE_CLICK_LISTENER, getCollapseButtonClickListener());
+        mModel.set(TabGridSheetProperties.ADD_CLICK_LISTENER, getAddButtonClickListener());
+    }
+
+    private void setupScrimViewObserver() {
+        ScrimView.ScrimObserver scrimObserver = new ScrimView.ScrimObserver() {
+            @Override
+            public void onScrimClick() {
+                mModel.set(TabGridSheetProperties.IS_DIALOG_VISIBLE, false);
+            }
+            @Override
+            public void onScrimVisibilityChanged(boolean visible) {}
+        };
+        mModel.set(TabGridSheetProperties.SCRIMVIEW_OBSERVER, scrimObserver);
+    }
+
+    private View.OnClickListener getCollapseButtonClickListener() {
+        return view -> {
+            RecordUserAction.record("TabGroup.DialogMinimizedFromGrid");
+            mModel.set(TabGridSheetProperties.IS_DIALOG_VISIBLE, false);
+        };
+    }
+
+    private View.OnClickListener getAddButtonClickListener() {
+        return view -> {
+            Tab currentTab = mTabModelSelector.getTabById(mCurrentTabId);
+            List<Tab> relatedTabs = getRelatedTabs(currentTab.getId());
+
+            assert relatedTabs.size() > 0;
+
+            Tab parentTabToAttach = relatedTabs.get(relatedTabs.size() - 1);
+            mTabCreatorManager.getTabCreator(currentTab.isIncognito())
+                    .createNewTab(new LoadUrlParams(UrlConstants.NTP_URL),
+                            TabLaunchType.FROM_CHROME_UI, parentTabToAttach);
+            RecordUserAction.record(
+                    "MobileNewTabOpened." + TabGridDialogCoordinator.COMPONENT_NAME);
+        };
+    }
+
+    private List<Tab> getRelatedTabs(int tabId) {
+        return mTabModelSelector.getTabModelFilterProvider()
+                .getCurrentTabModelFilter()
+                .getRelatedTabList(tabId);
+    }
+}
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogParent.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogParent.java
new file mode 100644
index 0000000..632f0200
--- /dev/null
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridDialogParent.java
@@ -0,0 +1,130 @@
+// 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.tasks.tab_management;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Color;
+import android.util.DisplayMetrics;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.PopupWindow;
+
+import org.chromium.chrome.browser.widget.ScrimView;
+import org.chromium.chrome.tab_ui.R;
+import org.chromium.ui.interpolators.BakedBezierInterpolator;
+
+/**
+ * Parent for TabGridDialog component.
+ * TODO(yuezhanggg): Add animations of card scales up to dialog and dialog scales down to card when
+ * show/hide dialog.
+ */
+public class TabGridDialogParent {
+    private PopupWindow mPopupWindow;
+    private LinearLayout mDialogContainerView;
+    private ScrimView mScrimView;
+    private ScrimView.ScrimParams mScrimParams;
+    private ValueAnimator mDialogFadeIn;
+    private ValueAnimator mDialogFadeOut;
+    private Animator mCurrentAnimator;
+
+    TabGridDialogParent(Context context) {
+        setUpDialog(context);
+    }
+
+    private void setUpDialog(Context context) {
+        FrameLayout backgroundView = new FrameLayout(context);
+        mDialogContainerView = new LinearLayout(context);
+        FrameLayout.LayoutParams containerParams = new FrameLayout.LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
+        int sideMargin =
+                (int) context.getResources().getDimension(R.dimen.tab_grid_dialog_side_margin);
+        int topMargin =
+                (int) context.getResources().getDimension(R.dimen.tab_grid_dialog_top_margin);
+        containerParams.setMargins(sideMargin, topMargin, sideMargin, topMargin);
+        mDialogContainerView.setLayoutParams(containerParams);
+        mDialogContainerView.setBackgroundColor(Color.WHITE);
+        mDialogContainerView.setOrientation(LinearLayout.VERTICAL);
+        backgroundView.addView(mDialogContainerView);
+
+        DisplayMetrics displayMetrics = new DisplayMetrics();
+        ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE))
+                .getDefaultDisplay()
+                .getMetrics(displayMetrics);
+        mPopupWindow = new PopupWindow(
+                backgroundView, displayMetrics.widthPixels, displayMetrics.heightPixels);
+        mScrimView = new ScrimView(context, null, backgroundView);
+
+        mDialogFadeIn = ObjectAnimator.ofFloat(mDialogContainerView, View.ALPHA, 0f, 1f);
+        mDialogFadeIn.setInterpolator(BakedBezierInterpolator.FADE_IN_CURVE);
+        mDialogFadeIn.setDuration(TabListRecyclerView.BASE_ANIMATION_DURATION_MS);
+        mDialogFadeIn.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mCurrentAnimator = null;
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                mCurrentAnimator = null;
+            }
+        });
+
+        mDialogFadeOut = ObjectAnimator.ofFloat(mDialogContainerView, View.ALPHA, 1f, 0f);
+        mDialogFadeOut.setInterpolator(BakedBezierInterpolator.FADE_OUT_CURVE);
+        mDialogFadeOut.setDuration(TabListRecyclerView.BASE_ANIMATION_DURATION_MS);
+        mDialogFadeOut.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mPopupWindow.dismiss();
+                mCurrentAnimator = null;
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                mPopupWindow.dismiss();
+                mCurrentAnimator = null;
+            }
+        });
+    }
+
+    void setScrimViewObserver(ScrimView.ScrimObserver scrimViewObserver) {
+        mScrimParams =
+                new ScrimView.ScrimParams(mDialogContainerView, false, true, 0, scrimViewObserver);
+    }
+
+    void updateDialog(View toolbarView, View recyclerView) {
+        mDialogContainerView.removeAllViews();
+        mDialogContainerView.addView(toolbarView);
+        mDialogContainerView.addView(recyclerView);
+        recyclerView.setVisibility(View.VISIBLE);
+    }
+
+    void showDialog(View parent) {
+        if (mCurrentAnimator != null && mCurrentAnimator != mDialogFadeIn) {
+            mCurrentAnimator.cancel();
+        }
+        mPopupWindow.showAtLocation(parent, Gravity.CENTER, 0, 0);
+        mScrimView.showScrim(mScrimParams);
+        mDialogFadeIn.start();
+        mCurrentAnimator = mDialogFadeIn;
+    }
+
+    void hideDialog() {
+        if (mCurrentAnimator != null && mCurrentAnimator != mDialogFadeOut) {
+            mCurrentAnimator.cancel();
+        }
+        mScrimView.hideScrim(true);
+        mDialogFadeOut.start();
+        mCurrentAnimator = mDialogFadeOut;
+    }
+}
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridSheetCoordinator.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridSheetCoordinator.java
index bf65a036..504d183 100644
--- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridSheetCoordinator.java
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridSheetCoordinator.java
@@ -44,7 +44,7 @@
 
         mTabGridCoordinator = new TabListCoordinator(TabListCoordinator.TabListMode.GRID, context,
                 tabModelSelector, tabContentManager::getTabThumbnailWithCallback, null, false, null,
-                bottomSheetController.getBottomSheet(), false, COMPONENT_NAME);
+                null, bottomSheetController.getBottomSheet(), false, COMPONENT_NAME);
 
         mMediator = new TabGridSheetMediator(mContext, bottomSheetController,
                 this::resetWithListOfTabs, mToolbarPropertyModel, tabModelSelector,
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridSheetMediator.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridSheetMediator.java
index 36d84f28..a98353a 100644
--- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridSheetMediator.java
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridSheetMediator.java
@@ -30,7 +30,7 @@
 import java.util.List;
 
 /**
- * A mediator for the TabGridSheet component, respoonsible for communicating
+ * A mediator for the TabGridSheet component, responsible for communicating
  * with the components' coordinator as well as managing the state of the bottom
  * sheet.
  */
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridSheetProperties.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridSheetProperties.java
index d8897903..6f94201 100644
--- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridSheetProperties.java
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridSheetProperties.java
@@ -7,6 +7,7 @@
 import android.content.res.ColorStateList;
 import android.view.View.OnClickListener;
 
+import org.chromium.chrome.browser.widget.ScrimView;
 import org.chromium.ui.modelutil.PropertyKey;
 import org.chromium.ui.modelutil.PropertyModel;
 
@@ -25,7 +26,13 @@
             new PropertyModel.WritableIntPropertyKey();
     public static final PropertyModel.WritableObjectPropertyKey<ColorStateList> TINT =
             new PropertyModel.WritableObjectPropertyKey<>();
+    public static final PropertyModel.WritableBooleanPropertyKey IS_DIALOG_VISIBLE =
+            new PropertyModel.WritableBooleanPropertyKey();
+    public static final PropertyModel
+            .WritableObjectPropertyKey<ScrimView.ScrimObserver> SCRIMVIEW_OBSERVER =
+            new PropertyModel.WritableObjectPropertyKey<>();
 
-    public static final PropertyKey[] ALL_KEYS = new PropertyKey[] {COLLAPSE_CLICK_LISTENER,
-            ADD_CLICK_LISTENER, HEADER_TITLE, CONTENT_TOP_MARGIN, PRIMARY_COLOR, TINT};
+    public static final PropertyKey[] ALL_KEYS =
+            new PropertyKey[] {COLLAPSE_CLICK_LISTENER, ADD_CLICK_LISTENER, HEADER_TITLE,
+                    CONTENT_TOP_MARGIN, PRIMARY_COLOR, TINT, IS_DIALOG_VISIBLE, SCRIMVIEW_OBSERVER};
 }
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridSheetToolbarCoordinator.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridSheetToolbarCoordinator.java
index 557c080..3aada02 100644
--- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridSheetToolbarCoordinator.java
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridSheetToolbarCoordinator.java
@@ -33,10 +33,16 @@
      */
     TabGridSheetToolbarCoordinator(
             Context context, ViewGroup contentView, PropertyModel toolbarPropertyModel) {
+        this(context, contentView, toolbarPropertyModel, null, null);
+    }
+
+    TabGridSheetToolbarCoordinator(Context context, ViewGroup contentView,
+            PropertyModel toolbarPropertyModel, ViewGroup parentView, TabGridDialogParent dialog) {
         mToolbarView = (TabGroupUiToolbarView) LayoutInflater.from(context).inflate(
                 R.layout.bottom_tab_grid_toolbar, contentView, false);
         mModelChangeProcessor = PropertyModelChangeProcessor.create(toolbarPropertyModel,
-                new TabGridSheetViewBinder.ViewHolder(mToolbarView, contentView),
+                new TabGridSheetViewBinder.ViewHolder(
+                        mToolbarView, contentView, parentView, dialog),
                 TabGridSheetViewBinder::bind);
     }
 
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridSheetViewBinder.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridSheetViewBinder.java
index 6c39e67d..458f9c8 100644
--- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridSheetViewBinder.java
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGridSheetViewBinder.java
@@ -8,10 +8,14 @@
 import static org.chromium.chrome.browser.tasks.tab_management.TabGridSheetProperties.COLLAPSE_CLICK_LISTENER;
 import static org.chromium.chrome.browser.tasks.tab_management.TabGridSheetProperties.CONTENT_TOP_MARGIN;
 import static org.chromium.chrome.browser.tasks.tab_management.TabGridSheetProperties.HEADER_TITLE;
+import static org.chromium.chrome.browser.tasks.tab_management.TabGridSheetProperties.IS_DIALOG_VISIBLE;
 import static org.chromium.chrome.browser.tasks.tab_management.TabGridSheetProperties.PRIMARY_COLOR;
+import static org.chromium.chrome.browser.tasks.tab_management.TabGridSheetProperties.SCRIMVIEW_OBSERVER;
 import static org.chromium.chrome.browser.tasks.tab_management.TabGridSheetProperties.TINT;
 
+import android.support.annotation.Nullable;
 import android.view.View;
+import android.view.ViewGroup;
 import android.widget.FrameLayout;
 
 import org.chromium.ui.modelutil.PropertyKey;
@@ -27,10 +31,17 @@
     public static class ViewHolder {
         public final TabGroupUiToolbarView toolbarView;
         public final View contentView;
+        @Nullable
+        public ViewGroup parentView;
+        @Nullable
+        public TabGridDialogParent dialogView;
 
-        ViewHolder(TabGroupUiToolbarView toolbarView, View contentView) {
+        ViewHolder(TabGroupUiToolbarView toolbarView, View contentView,
+                @Nullable ViewGroup parentView, @Nullable TabGridDialogParent dialogView) {
             this.toolbarView = toolbarView;
             this.contentView = contentView;
+            this.parentView = parentView;
+            this.dialogView = dialogView;
         }
     }
 
@@ -56,6 +67,15 @@
             viewHolder.contentView.setBackgroundColor(model.get(PRIMARY_COLOR));
         } else if (TINT == propertyKey) {
             viewHolder.toolbarView.setTint(model.get(TINT));
+        } else if (SCRIMVIEW_OBSERVER == propertyKey) {
+            viewHolder.dialogView.setScrimViewObserver(model.get(SCRIMVIEW_OBSERVER));
+        } else if (IS_DIALOG_VISIBLE == propertyKey) {
+            if (model.get(IS_DIALOG_VISIBLE)) {
+                viewHolder.dialogView.updateDialog(viewHolder.toolbarView, viewHolder.contentView);
+                viewHolder.dialogView.showDialog(viewHolder.parentView);
+            } else {
+                viewHolder.dialogView.hideDialog();
+            }
         }
     }
 }
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGroupUiCoordinator.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGroupUiCoordinator.java
index 0ddf740..32bf5b1 100644
--- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGroupUiCoordinator.java
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGroupUiCoordinator.java
@@ -74,7 +74,7 @@
         TabContentManager tabContentManager = activity.getTabContentManager();
 
         mTabStripCoordinator = new TabListCoordinator(TabListCoordinator.TabListMode.STRIP,
-                mContext, tabModelSelector, null, null, false, null,
+                mContext, tabModelSelector, null, null, false, null, null,
                 mTabStripToolbarCoordinator.getTabListContainerView(), true, COMPONENT_NAME);
 
         mTabGridSheetCoordinator =
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGroupUiToolbarView.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGroupUiToolbarView.java
index 87657a0..78f9939 100644
--- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGroupUiToolbarView.java
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGroupUiToolbarView.java
@@ -50,6 +50,10 @@
         mRightButton.setOnClickListener(listener);
     }
 
+    void setTitleOnClickListener(OnClickListener listener) {
+        mTitleTextView.setOnClickListener(listener);
+    }
+
     ViewGroup getViewContainer() {
         return mContainerView;
     }
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListCoordinator.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListCoordinator.java
index 12fcb47..5824872 100644
--- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListCoordinator.java
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListCoordinator.java
@@ -71,6 +71,8 @@
             TabListMediator.ThumbnailProvider thumbnailProvider,
             TabListMediator.TitleProvider titleProvider, boolean closeRelatedTabs,
             @Nullable TabListMediator.CreateGroupButtonProvider createGroupButtonProvider,
+            @Nullable TabListMediator
+                    .GridCardOnClickListenerProvider gridCardOnClickListenerProvider,
             @NonNull ViewGroup parentView, boolean attachToParent, String componentName) {
         TabListModel tabListModel = new TabListModel();
         mMode = mode;
@@ -122,7 +124,7 @@
 
         mMediator = new TabListMediator(tabListModel, tabModelSelector, thumbnailProvider,
                 titleProvider, tabListFaviconProvider, closeRelatedTabs, createGroupButtonProvider,
-                componentName);
+                gridCardOnClickListenerProvider, componentName);
 
         if (mMode == TabListMode.GRID) {
             ItemTouchHelper touchHelper = new ItemTouchHelper(mMediator.getItemTouchHelperCallback(
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListMediator.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListMediator.java
index c72995a..92768311c 100644
--- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListMediator.java
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabListMediator.java
@@ -120,6 +120,18 @@
         TabActionListener getCreateGroupButtonOnClickListener(Tab tab);
     }
 
+    /**
+     * An interface to get the onClickListener for opening dialog when click on a grid card.
+     */
+    public interface GridCardOnClickListenerProvider {
+        /**
+         * @return {@link TabActionListener} to open tabgrid dialog. If the given {@link Tab} is not
+         * able to create group, return null;
+         */
+        @Nullable
+        TabActionListener getGridCardOnClickListener(Tab tab);
+    }
+
     @IntDef({TabClosedFrom.TAB_STRIP, TabClosedFrom.TAB_GRID_SHEET, TabClosedFrom.GRID_TAB_SWITCHER,
             TabClosedFrom.GRID_TAB_SWITCHER_GROUP})
     @Retention(RetentionPolicy.SOURCE)
@@ -141,6 +153,7 @@
     private final TabActionListener mTabClosedListener;
     private final TitleProvider mTitleProvider;
     private final CreateGroupButtonProvider mCreateGroupButtonProvider;
+    private final GridCardOnClickListenerProvider mGridCardOnClickListenerProvider;
     private final String mComponentName;
     private boolean mCloseAllRelatedTabs;
     private ComponentCallbacks mComponentCallbacks;
@@ -239,7 +252,9 @@
     public TabListMediator(TabListModel model, TabModelSelector tabModelSelector,
             @Nullable ThumbnailProvider thumbnailProvider, @Nullable TitleProvider titleProvider,
             TabListFaviconProvider tabListFaviconProvider, boolean closeRelatedTabs,
-            @Nullable CreateGroupButtonProvider createGroupButtonProvider, String componentName) {
+            @Nullable CreateGroupButtonProvider createGroupButtonProvider,
+            @Nullable GridCardOnClickListenerProvider gridCardOnClickListenerProvider,
+            String componentName) {
         mTabModelSelector = tabModelSelector;
         mThumbnailProvider = thumbnailProvider;
         mModel = model;
@@ -247,6 +262,7 @@
         mComponentName = componentName;
         mTitleProvider = titleProvider != null ? titleProvider : Tab::getTitle;
         mCreateGroupButtonProvider = createGroupButtonProvider;
+        mGridCardOnClickListenerProvider = gridCardOnClickListenerProvider;
         mCloseAllRelatedTabs = closeRelatedTabs;
 
         mTabModelObserver = new EmptyTabModelObserver() {
@@ -502,6 +518,13 @@
         if (mCloseAllRelatedTabs && !mShownIPH) {
             showIPH = getRelatedTabsForId(tab.getId()).size() > 1;
         }
+        TabActionListener tabSelectedListener;
+        if (mGridCardOnClickListenerProvider == null
+                || getRelatedTabsForId(tab.getId()).size() == 1) {
+            tabSelectedListener = mTabSelectedListener;
+        } else {
+            tabSelectedListener = mGridCardOnClickListenerProvider.getGridCardOnClickListener(tab);
+        }
 
         PropertyModel tabInfo =
                 new PropertyModel.Builder(TabProperties.ALL_KEYS_TAB_GRID)
@@ -511,7 +534,7 @@
                                 mTabListFaviconProvider.getDefaultFaviconDrawable())
                         .with(TabProperties.IS_SELECTED, isSelected)
                         .with(TabProperties.IPH_PROVIDER, showIPH ? mIphProvider : null)
-                        .with(TabProperties.TAB_SELECTED_LISTENER, mTabSelectedListener)
+                        .with(TabProperties.TAB_SELECTED_LISTENER, tabSelectedListener)
                         .with(TabProperties.TAB_CLOSED_LISTENER, mTabClosedListener)
                         .with(TabProperties.CREATE_GROUP_LISTENER, createGroupButtonOnClickListener)
                         .with(TabProperties.ALPHA, 1f)
diff --git a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabManagementModuleImpl.java b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabManagementModuleImpl.java
index 3c94a78..5879a14 100644
--- a/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabManagementModuleImpl.java
+++ b/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabManagementModuleImpl.java
@@ -29,7 +29,7 @@
         return new GridTabSwitcherCoordinator(activity, activity.getLifecycleDispatcher(),
                 activity.getToolbarManager(), activity.getTabModelSelector(),
                 activity.getTabContentManager(), activity.getCompositorViewHolder(),
-                activity.getFullscreenManager());
+                activity.getFullscreenManager(), activity);
     }
 
     @Override
diff --git a/chrome/android/features/tab_ui/junit/src/org/chromium/chrome/browser/tasks/tab_management/GridTabSwitcherMediatorUnitTest.java b/chrome/android/features/tab_ui/junit/src/org/chromium/chrome/browser/tasks/tab_management/GridTabSwitcherMediatorUnitTest.java
index 97fb1ae7..f0d4773f 100644
--- a/chrome/android/features/tab_ui/junit/src/org/chromium/chrome/browser/tasks/tab_management/GridTabSwitcherMediatorUnitTest.java
+++ b/chrome/android/features/tab_ui/junit/src/org/chromium/chrome/browser/tasks/tab_management/GridTabSwitcherMediatorUnitTest.java
@@ -156,7 +156,7 @@
         mModel = new PropertyModel(TabListContainerProperties.ALL_KEYS);
         mModel.addObserver(mPropertyObserver);
         mMediator = new GridTabSwitcherMediator(mResetHandler, mModel, mTabModelSelector,
-                mFullscreenManager, mCompositorViewHolder);
+                mFullscreenManager, mCompositorViewHolder, null);
         mMediator.addOverviewModeObserver(mOverviewModeObserver);
     }
 
diff --git a/chrome/android/features/tab_ui/junit/src/org/chromium/chrome/browser/tasks/tab_management/TabListMediatorUnitTest.java b/chrome/android/features/tab_ui/junit/src/org/chromium/chrome/browser/tasks/tab_management/TabListMediatorUnitTest.java
index 6bdf32a1..ff94d4d 100644
--- a/chrome/android/features/tab_ui/junit/src/org/chromium/chrome/browser/tasks/tab_management/TabListMediatorUnitTest.java
+++ b/chrome/android/features/tab_ui/junit/src/org/chromium/chrome/browser/tasks/tab_management/TabListMediatorUnitTest.java
@@ -131,7 +131,7 @@
         mModel = new TabListModel();
         mMediator = new TabListMediator(mModel, mTabModelSelector,
                 mTabContentManager::getTabThumbnailWithCallback, null, mTabListFaviconProvider,
-                false, null, getClass().getSimpleName());
+                false, null, null, getClass().getSimpleName());
     }
 
     @After
diff --git a/chrome/android/java/res/layout/omnibox_answer_suggestion.xml b/chrome/android/java/res/layout/omnibox_answer_suggestion.xml
index ee2b403b..72d5bb7 100644
--- a/chrome/android/java/res/layout/omnibox_answer_suggestion.xml
+++ b/chrome/android/java/res/layout/omnibox_answer_suggestion.xml
@@ -28,7 +28,7 @@
             android:layout_centerVertical="true"
             android:layout_height="36dp"
             android:layout_marginEnd="10dp"
-            android:layout_marginStart="@dimen/omnibox_answer_suggestion_icon_margin_start"
+            android:layout_marginStart="@dimen/omnibox_suggestion_icon_margin_start"
             android:layout_width="36dp"
             android:scaleType="fitCenter" />
 
diff --git a/chrome/android/java/res/layout/omnibox_entity_suggestion.xml b/chrome/android/java/res/layout/omnibox_entity_suggestion.xml
new file mode 100644
index 0000000..2f76cf0
--- /dev/null
+++ b/chrome/android/java/res/layout/omnibox_entity_suggestion.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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. -->
+<org.chromium.chrome.browser.omnibox.suggestions.entity.EntitySuggestionView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_height="@dimen/omnibox_suggestion_height"
+    android:layout_width="match_parent">
+
+    <view class="org.chromium.chrome.browser.omnibox.suggestions.entity.EntitySuggestionView$FocusableView"
+        android:id="@+id/omnibox_entity"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_alignParentStart="true"
+        android:layout_centerVertical="true"
+        android:layout_toStartOf="@+id/omnibox_entity_refine_icon"
+        android:background="?attr/selectableItemBackground"
+        android:clickable="true"
+        android:focusable="true"
+        android:paddingVertical="10dp">
+
+        <ImageView
+            android:id="@+id/omnibox_entity_image"
+            android:layout_width="@dimen/omnibox_suggestion_entity_icon_size"
+            android:layout_height="@dimen/omnibox_suggestion_entity_icon_size"
+            android:layout_centerVertical="true"
+            android:layout_marginEnd="8dp"
+            android:layout_marginStart="@dimen/omnibox_suggestion_icon_margin_start"
+            android:contentDescription="@null" />
+
+        <TextView
+            android:id="@+id/omnibox_entity_subject_text"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_alignParentEnd="true"
+            android:layout_toEndOf="@id/omnibox_entity_image"
+            android:ellipsize="end"
+            android:textAppearance="@style/TextAppearance.BlackTitle1"
+            android:maxLines="1"
+            android:singleLine="true"
+            android:textAlignment="viewStart" />
+
+        <TextView
+            android:id="@+id/omnibox_entity_description_text"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_alignEnd="@id/omnibox_entity_subject_text"
+            android:layout_alignStart="@id/omnibox_entity_subject_text"
+            android:layout_below="@id/omnibox_entity_subject_text"
+            android:ellipsize="end"
+            android:textAppearance="@style/TextAppearance.BlackHint2"
+            android:maxLines="3"
+            android:singleLine="false"
+            android:textAlignment="viewStart" />
+
+    </view>
+
+    <ImageView android:background="?attr/selectableItemBackground"
+        android:id="@id/omnibox_entity_refine_icon"
+        android:layout_width="@dimen/omnibox_suggestion_refine_width"
+        android:layout_height="match_parent"
+        android:layout_alignBottom="@id/omnibox_entity"
+        android:layout_alignParentEnd="true"
+        android:layout_alignTop="@id/omnibox_entity"
+        android:layout_centerVertical="true"
+        android:layout_marginEnd="@dimen/omnibox_suggestion_refine_view_modern_end_padding"
+        android:clickable="true"
+        android:contentDescription="@string/accessibility_omnibox_btn_refine"
+        android:focusable="true"
+        android:scaleType="center"
+        android:src="@drawable/btn_suggestion_refine" />
+
+</org.chromium.chrome.browser.omnibox.suggestions.entity.EntitySuggestionView>
diff --git a/chrome/android/java/res/values-sw600dp/dimens.xml b/chrome/android/java/res/values-sw600dp/dimens.xml
index 49c5003..dbdb8a98 100644
--- a/chrome/android/java/res/values-sw600dp/dimens.xml
+++ b/chrome/android/java/res/values-sw600dp/dimens.xml
@@ -34,7 +34,7 @@
     <dimen name="omnibox_suggestion_start_offset_without_icon">@dimen/location_bar_icon_width</dimen>
     <dimen name="omnibox_suggestion_start_offset_with_icon">@dimen/omnibox_suggestion_start_offset_without_icon</dimen>
 
-    <dimen name="omnibox_answer_suggestion_icon_margin_start">0dp</dimen>
+    <dimen name="omnibox_suggestion_icon_margin_start">0dp</dimen>
 
     <!-- NTP dimensions -->
     <dimen name="ntp_search_box_transition_length">60dp</dimen>
diff --git a/chrome/android/java/res/values/dimens.xml b/chrome/android/java/res/values/dimens.xml
index 16544e7..fa90ac3d 100644
--- a/chrome/android/java/res/values/dimens.xml
+++ b/chrome/android/java/res/values/dimens.xml
@@ -296,13 +296,14 @@
     <dimen name="omnibox_suggestion_start_offset_without_icon">18dp</dimen>
     <dimen name="omnibox_suggestion_start_offset_with_icon">56dp</dimen>
 
+    <dimen name="omnibox_suggestion_icon_margin_start">10dp</dimen>
     <dimen name="omnibox_suggestion_favicon_size">24dp</dimen>
+    <dimen name="omnibox_suggestion_entity_icon_size">36dp</dimen>
 
     <dimen name="omnibox_suggestion_refine_width">48dp</dimen>
     <dimen name="omnibox_suggestion_text_vertical_padding">5dp</dimen>
     <dimen name="omnibox_suggestion_multiline_text_vertical_padding">10dp</dimen>
     <dimen name="omnibox_suggestion_refine_view_modern_end_padding">4dp</dimen>
-    <dimen name="omnibox_answer_suggestion_icon_margin_start">10dp</dimen>
 
     <!-- NTP dimensions -->
     <dimen name="tile_grid_layout_max_width">504dp</dimen>
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ChromeActivity.java b/chrome/android/java/src/org/chromium/chrome/browser/ChromeActivity.java
index eb4d052..c40d8bf 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ChromeActivity.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ChromeActivity.java
@@ -1470,7 +1470,6 @@
             // TODO(yusufo): Unify initialization.
             initializeBottomSheet(
                     !ChromeFeatureList.isEnabled(ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_BUTTON));
-            getComponent().resolveContextualSuggestionsCoordinator();
         }
         AppHooks.get().startMonitoringNetworkQuality();
         AppHooks.get().startSystemSettingsObserver();
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ChromeFeatureList.java b/chrome/android/java/src/org/chromium/chrome/browser/ChromeFeatureList.java
index ed32631..2a64da80 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ChromeFeatureList.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ChromeFeatureList.java
@@ -313,6 +313,8 @@
     public static final String QUERY_IN_OMNIBOX = "QueryInOmnibox";
     public static final String TAB_ENGAGEMENT_REPORTING_ANDROID = "TabEngagementReportingAndroid";
     public static final String TAB_GROUPS_ANDROID = "TabGroupsAndroid";
+    public static final String TAB_GROUPS_UI_IMPROVEMENTS_ANDROID =
+            "TabGroupsUiImprovementsAndroid";
     public static final String TAB_GRID_LAYOUT_ANDROID = "TabGridLayoutAndroid";
     public static final String TAB_PERSISTENT_STORE_TASK_RUNNER = "TabPersistentStoreTaskRunner";
     public static final String TAB_REPARENTING = "TabReparenting";
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/dependency_injection/ChromeActivityComponent.java b/chrome/android/java/src/org/chromium/chrome/browser/dependency_injection/ChromeActivityComponent.java
index 256fc347..2f96c8eb 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/dependency_injection/ChromeActivityComponent.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/dependency_injection/ChromeActivityComponent.java
@@ -4,7 +4,6 @@
 
 package org.chromium.chrome.browser.dependency_injection;
 
-import org.chromium.chrome.browser.contextual_suggestions.ContextualSuggestionsCoordinator;
 import org.chromium.chrome.browser.contextual_suggestions.ContextualSuggestionsModule;
 
 import dagger.Subcomponent;
@@ -16,7 +15,4 @@
 @ActivityScope
 public interface ChromeActivityComponent {
     ChromeAppComponent getParent();
-
-    // Temporary getters for DI migration process.
-    ContextualSuggestionsCoordinator resolveContextualSuggestionsCoordinator();
 }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/infobar/ConfirmInfoBar.java b/chrome/android/java/src/org/chromium/chrome/browser/infobar/ConfirmInfoBar.java
index 3e8a14640..9c565b9 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/infobar/ConfirmInfoBar.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/infobar/ConfirmInfoBar.java
@@ -6,9 +6,15 @@
 
 import android.graphics.Bitmap;
 import android.support.annotation.ColorRes;
+import android.text.TextUtils;
 
 import org.chromium.base.annotations.CalledByNative;
 import org.chromium.chrome.browser.ResourceId;
+import org.chromium.chrome.browser.touchless.dialog.TouchlessDialogProperties;
+import org.chromium.chrome.browser.touchless.dialog.TouchlessDialogProperties.DialogListItemProperties;
+import org.chromium.ui.modelutil.PropertyModel;
+
+import java.util.ArrayList;
 
 /**
  * An infobar that presents the user with several buttons.
@@ -59,6 +65,40 @@
         onButtonClicked(action);
     }
 
+    @Override
+    public boolean supportsTouchlessMode() {
+        // Only allow whitelisted implementations of the confirm infobar.
+        return getInfoBarIdentifier() == InfoBarIdentifier.POPUP_BLOCKED_INFOBAR_DELEGATE_MOBILE;
+    }
+
+    @Override
+    public PropertyModel createModel() {
+        PropertyModel model = super.createModel();
+
+        ArrayList<PropertyModel> options = new ArrayList<>();
+        if (!TextUtils.isEmpty(mPrimaryButtonText)) {
+            options.add(new PropertyModel.Builder(DialogListItemProperties.ALL_KEYS)
+                                .with(DialogListItemProperties.TEXT, mPrimaryButtonText)
+                                .with(DialogListItemProperties.CLICK_LISTENER,
+                                        (v) -> onButtonClicked(true))
+                                .build());
+        }
+
+        if (!TextUtils.isEmpty(mSecondaryButtonText)) {
+            options.add(new PropertyModel.Builder(DialogListItemProperties.ALL_KEYS)
+                                .with(DialogListItemProperties.TEXT, mSecondaryButtonText)
+                                .with(DialogListItemProperties.CLICK_LISTENER,
+                                        (v) -> onButtonClicked(false))
+                                .build());
+        }
+
+        PropertyModel[] optionModels = new PropertyModel[options.size()];
+        options.toArray(optionModels);
+        model.set(TouchlessDialogProperties.LIST_MODELS, optionModels);
+
+        return model;
+    }
+
     /**
      * Creates and begins the process for showing a ConfirmInfoBar.
      * @param enumeratedIconId ID corresponding to the icon that will be shown for the infobar.
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/infobar/InfoBar.java b/chrome/android/java/src/org/chromium/chrome/browser/infobar/InfoBar.java
index 04c05d8..2354fc1 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/infobar/InfoBar.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/infobar/InfoBar.java
@@ -323,7 +323,9 @@
 
     @Override
     public void onCloseButtonClicked() {
-        if (mNativeInfoBarPtr != 0) nativeOnCloseButtonClicked(mNativeInfoBarPtr);
+        if (mNativeInfoBarPtr != 0 && !mIsDismissed) {
+            nativeOnCloseButtonClicked(mNativeInfoBarPtr);
+        }
     }
 
     @Override
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/infobar/InfoBarContainerView.java b/chrome/android/java/src/org/chromium/chrome/browser/infobar/InfoBarContainerView.java
index 7221fd8..9953cd2 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/infobar/InfoBarContainerView.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/infobar/InfoBarContainerView.java
@@ -190,8 +190,8 @@
      */
     void setParentView(ViewGroup parent) {
         mParentView = parent;
-        removeFromParentView();
-        addToParentView();
+        // Don't attach the container to the new parent if it is not previously attached.
+        if (removeFromParentView()) addToParentView();
     }
 
     /**
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/AutocompleteCoordinator.java b/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/AutocompleteCoordinator.java
index a77cf3d..839799a 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/AutocompleteCoordinator.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/AutocompleteCoordinator.java
@@ -32,6 +32,8 @@
 import org.chromium.chrome.browser.omnibox.suggestions.basic.SuggestionViewViewBinder;
 import org.chromium.chrome.browser.omnibox.suggestions.editurl.EditUrlSuggestionProcessor;
 import org.chromium.chrome.browser.omnibox.suggestions.editurl.EditUrlSuggestionViewBinder;
+import org.chromium.chrome.browser.omnibox.suggestions.entity.EntitySuggestionView;
+import org.chromium.chrome.browser.omnibox.suggestions.entity.EntitySuggestionViewBinder;
 import org.chromium.chrome.browser.profiles.Profile;
 import org.chromium.chrome.browser.toolbar.ToolbarDataProvider;
 import org.chromium.chrome.browser.util.KeyNavigationUtil;
@@ -210,6 +212,12 @@
                         () -> (AnswerSuggestionView) LayoutInflater.from(mListView.getContext())
                                 .inflate(R.layout.omnibox_answer_suggestion, null),
                         AnswerSuggestionViewBinder::bind);
+
+                adapter.registerType(
+                        OmniboxSuggestionUiType.ENTITY_SUGGESTION,
+                        () -> (EntitySuggestionView) LayoutInflater.from(mListView.getContext())
+                                .inflate(R.layout.omnibox_entity_suggestion, null),
+                        EntitySuggestionViewBinder::bind);
                 // clang-format on
 
                 mHolder = new SuggestionListViewHolder(container, list, adapter);
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/AutocompleteMediator.java b/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/AutocompleteMediator.java
index bd42d6ef..f05dbdd6 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/AutocompleteMediator.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/AutocompleteMediator.java
@@ -37,6 +37,7 @@
 import org.chromium.chrome.browser.omnibox.suggestions.basic.SuggestionHost;
 import org.chromium.chrome.browser.omnibox.suggestions.basic.SuggestionViewDelegate;
 import org.chromium.chrome.browser.omnibox.suggestions.editurl.EditUrlSuggestionProcessor;
+import org.chromium.chrome.browser.omnibox.suggestions.entity.EntitySuggestionProcessor;
 import org.chromium.chrome.browser.profiles.Profile;
 import org.chromium.chrome.browser.toolbar.ToolbarDataProvider;
 import org.chromium.components.omnibox.AnswerType;
@@ -93,7 +94,8 @@
     private final Handler mHandler;
     private final BasicSuggestionProcessor mBasicSuggestionProcessor;
     private EditUrlSuggestionProcessor mEditUrlProcessor;
-    private final AnswerSuggestionProcessor mAnswerSuggestionProcessor;
+    private AnswerSuggestionProcessor mAnswerSuggestionProcessor;
+    private final EntitySuggestionProcessor mEntitySuggestionProcessor;
 
     private ToolbarDataProvider mDataProvider;
     private boolean mNativeInitialized;
@@ -155,6 +157,7 @@
         mAnswerSuggestionProcessor = new AnswerSuggestionProcessor(mContext, this, textProvider);
         mEditUrlProcessor = new EditUrlSuggestionProcessor(
                 delegate, (suggestion) -> onSelection(suggestion, 0));
+        mEntitySuggestionProcessor = new EntitySuggestionProcessor(mContext, this);
     }
 
     @Override
@@ -317,6 +320,7 @@
         mDeferredNativeRunnables.clear();
         mAnswerSuggestionProcessor.onNativeInitialized();
         mBasicSuggestionProcessor.onNativeInitialized();
+        mEntitySuggestionProcessor.onNativeInitialized();
         if (mEditUrlProcessor != null) mEditUrlProcessor.onNativeInitialized();
     }
 
@@ -355,6 +359,7 @@
         if (mEditUrlProcessor != null) mEditUrlProcessor.onUrlFocusChange(hasFocus);
         mAnswerSuggestionProcessor.onUrlFocusChange(hasFocus);
         mBasicSuggestionProcessor.onUrlFocusChange(hasFocus);
+        mEntitySuggestionProcessor.onUrlFocusChange(hasFocus);
     }
 
     /**
@@ -705,6 +710,8 @@
     private SuggestionProcessor getProcessorForSuggestion(OmniboxSuggestion suggestion) {
         if (mAnswerSuggestionProcessor.doesProcessSuggestion(suggestion)) {
             return mAnswerSuggestionProcessor;
+        } else if (mEntitySuggestionProcessor.doesProcessSuggestion(suggestion)) {
+            return mEntitySuggestionProcessor;
         } else if (mEditUrlProcessor != null
                 && mEditUrlProcessor.doesProcessSuggestion(suggestion)) {
             return mEditUrlProcessor;
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/OmniboxSuggestionUiType.java b/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/OmniboxSuggestionUiType.java
index c13051e..22a2a7a 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/OmniboxSuggestionUiType.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/OmniboxSuggestionUiType.java
@@ -11,10 +11,11 @@
 
 /** The different types of view that a suggestion can be. */
 @IntDef({OmniboxSuggestionUiType.DEFAULT, OmniboxSuggestionUiType.EDIT_URL_SUGGESTION,
-        OmniboxSuggestionUiType.ANSWER_SUGGESTION})
+        OmniboxSuggestionUiType.ANSWER_SUGGESTION, OmniboxSuggestionUiType.ENTITY_SUGGESTION})
 @Retention(RetentionPolicy.SOURCE)
 public @interface OmniboxSuggestionUiType {
     int DEFAULT = 0;
     int EDIT_URL_SUGGESTION = 1;
     int ANSWER_SUGGESTION = 2;
+    int ENTITY_SUGGESTION = 3;
 }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/entity/EntitySuggestionProcessor.java b/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/entity/EntitySuggestionProcessor.java
new file mode 100644
index 0000000..7aedbee9
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/entity/EntitySuggestionProcessor.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.omnibox.suggestions.entity;
+
+import android.content.Context;
+
+import org.chromium.chrome.browser.omnibox.OmniboxSuggestionType;
+import org.chromium.chrome.browser.omnibox.suggestions.AutocompleteCoordinator.SuggestionProcessor;
+import org.chromium.chrome.browser.omnibox.suggestions.OmniboxSuggestion;
+import org.chromium.chrome.browser.omnibox.suggestions.OmniboxSuggestionUiType;
+import org.chromium.chrome.browser.omnibox.suggestions.basic.SuggestionHost;
+import org.chromium.chrome.browser.omnibox.suggestions.basic.SuggestionViewDelegate;
+import org.chromium.ui.modelutil.PropertyModel;
+
+/** A class that handles model and view creation for the Entity suggestions. */
+public class EntitySuggestionProcessor implements SuggestionProcessor {
+    private final Context mContext;
+    private final SuggestionHost mSuggestionHost;
+
+    /**
+     * @param context An Android context.
+     * @param suggestionHost A handle to the object using the suggestions.
+     */
+    public EntitySuggestionProcessor(Context context, SuggestionHost suggestionHost) {
+        mContext = context;
+        mSuggestionHost = suggestionHost;
+    }
+
+    @Override
+    public boolean doesProcessSuggestion(OmniboxSuggestion suggestion) {
+        return suggestion.getType() == OmniboxSuggestionType.SEARCH_SUGGEST_ENTITY;
+    }
+
+    @Override
+    public int getViewTypeId() {
+        return OmniboxSuggestionUiType.ENTITY_SUGGESTION;
+    }
+
+    @Override
+    public PropertyModel createModelForSuggestion(OmniboxSuggestion suggestion) {
+        return new PropertyModel(EntitySuggestionViewProperties.ALL_KEYS);
+    }
+
+    @Override
+    public void onNativeInitialized() {}
+
+    @Override
+    public void onUrlFocusChange(boolean hasFocus) {}
+
+    @Override
+    public void populateModel(OmniboxSuggestion suggestion, PropertyModel model, int position) {
+        // TODO(ender): Fetch entity icon URL.
+        SuggestionViewDelegate delegate =
+                mSuggestionHost.createSuggestionViewDelegate(suggestion, position);
+
+        model.set(EntitySuggestionViewProperties.SUBJECT_TEXT, suggestion.getDisplayText());
+        model.set(EntitySuggestionViewProperties.DESCRIPTION_TEXT, suggestion.getDescription());
+        model.set(EntitySuggestionViewProperties.DELEGATE, delegate);
+    }
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/entity/EntitySuggestionView.java b/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/entity/EntitySuggestionView.java
new file mode 100644
index 0000000..0071cdf
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/entity/EntitySuggestionView.java
@@ -0,0 +1,124 @@
+// 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.omnibox.suggestions.entity;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import org.chromium.chrome.R;
+
+/**
+ * Container view for omnibox entity suggestions.
+ */
+public class EntitySuggestionView extends RelativeLayout {
+    /**
+     * EventListener is a class receiving all untranslated input events.
+     */
+    interface EventListener {
+        /**
+         * Process gesture event described by supplied MotionEvent.
+         * @param event Gesture motion event.
+         */
+        void onMotionEvent(MotionEvent event);
+
+        /** Process click event. */
+        void onClick();
+
+        /** Process long click event. */
+        void onLongClick();
+
+        /** Process select/highlight event. */
+        void onSelected();
+
+        /** Process refine event. */
+        void onRefine();
+    }
+
+    private EventListener mEventListener;
+    private View mEntityView;
+    private TextView mSubjectText;
+    private TextView mDescriptionText;
+    private ImageView mEntityImageView;
+    private ImageView mRefineView;
+
+    /**
+     * Container view for omnibox suggestions allowing soft focus from keyboard.
+     */
+    public static class FocusableView extends RelativeLayout {
+        /** Creates new instance of FocusableView. */
+        public FocusableView(Context context, AttributeSet attributes) {
+            super(context, attributes);
+        }
+
+        @Override
+        public boolean isFocused() {
+            return super.isFocused() || (isSelected() && !isInTouchMode());
+        }
+    }
+
+    /** Creates new instance of EntitySuggestionView. */
+    public EntitySuggestionView(Context context, AttributeSet attributes) {
+        super(context, attributes);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mSubjectText = findViewById(R.id.omnibox_entity_subject_text);
+        mDescriptionText = findViewById(R.id.omnibox_entity_description_text);
+        mEntityImageView = findViewById(R.id.omnibox_entity_image);
+        mEntityView = findViewById(R.id.omnibox_entity);
+        mRefineView = findViewById(R.id.omnibox_entity_refine_icon);
+    }
+
+    @Override
+    public void setSelected(boolean selected) {
+        super.setSelected(selected);
+        mEntityView.setSelected(selected);
+        if (mEventListener != null && selected && !isInTouchMode()) {
+            mEventListener.onSelected();
+        }
+    }
+
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent ev) {
+        if (mEventListener != null) {
+            mEventListener.onMotionEvent(ev);
+        }
+        return super.dispatchTouchEvent(ev);
+    }
+
+    /** Specify delegate receiving click/refine events. */
+    void setDelegate(EventListener delegate) {
+        mEventListener = delegate;
+        mEntityView.setOnClickListener((View v) -> mEventListener.onClick());
+        mEntityView.setOnLongClickListener((View v) -> {
+            mEventListener.onLongClick();
+            return true;
+        });
+        mRefineView.setOnClickListener((View v) -> mEventListener.onRefine());
+    }
+
+    /**
+     * Specifies the text to be displayed as subject name.
+     * @param text Text to be displayed.
+     */
+    void setSubjectText(String text) {
+        mSubjectText.setText(text);
+    }
+
+    /**
+     * Specifies the text to be displayed as description.
+     * @param text Text to be displayed.
+     */
+    void setDescriptionText(String text) {
+        mDescriptionText.setText(text);
+    }
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/entity/EntitySuggestionViewBinder.java b/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/entity/EntitySuggestionViewBinder.java
new file mode 100644
index 0000000..a6130c4a
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/entity/EntitySuggestionViewBinder.java
@@ -0,0 +1,80 @@
+// 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.omnibox.suggestions.entity;
+
+import android.os.Handler;
+import android.view.MotionEvent;
+
+import org.chromium.chrome.browser.omnibox.suggestions.basic.SuggestionViewDelegate;
+import org.chromium.ui.modelutil.PropertyKey;
+import org.chromium.ui.modelutil.PropertyModel;
+
+/** A mechanism binding EntitySuggestion properties to its view. */
+public class EntitySuggestionViewBinder {
+    private static final class EventListener implements EntitySuggestionView.EventListener {
+        final Handler mHandler;
+        final SuggestionViewDelegate mDelegate;
+
+        EventListener(EntitySuggestionView view, SuggestionViewDelegate delegate) {
+            mHandler = new Handler();
+            mDelegate = delegate;
+        }
+
+        @Override
+        public void onMotionEvent(MotionEvent ev) {
+            // Whenever the suggestion dropdown is touched, we dispatch onGestureDown which is
+            // used to let autocomplete controller know that it should stop updating suggestions.
+            switch (ev.getActionMasked()) {
+                case MotionEvent.ACTION_DOWN:
+                    mDelegate.onGestureDown();
+                    break;
+                case MotionEvent.ACTION_UP:
+                    mDelegate.onGestureUp(ev.getEventTime());
+                    break;
+            }
+        }
+
+        @Override
+        public void onClick() {
+            postAction(() -> mDelegate.onSelection());
+        }
+
+        @Override
+        public void onLongClick() {
+            postAction(() -> mDelegate.onLongPress());
+        }
+
+        @Override
+        public void onSelected() {
+            postAction(() -> mDelegate.onSetUrlToSuggestion());
+        }
+
+        @Override
+        public void onRefine() {
+            postAction(() -> mDelegate.onRefineSuggestion());
+        }
+
+        /**
+         * Post delegate action to main thread.
+         * @param action Delegate action to invoke on the UI thread.
+         */
+        private void postAction(Runnable action) {
+            if (!mHandler.post(action)) action.run();
+        }
+    }
+
+    /** @see PropertyModelChangeProcessor.ViewBinder#bind(Object, Object, Object) */
+    public static void bind(
+            PropertyModel model, EntitySuggestionView view, PropertyKey propertyKey) {
+        if (EntitySuggestionViewProperties.DELEGATE.equals(propertyKey)) {
+            view.setDelegate(
+                    new EventListener(view, model.get(EntitySuggestionViewProperties.DELEGATE)));
+        } else if (EntitySuggestionViewProperties.SUBJECT_TEXT.equals(propertyKey)) {
+            view.setSubjectText(model.get(EntitySuggestionViewProperties.SUBJECT_TEXT));
+        } else if (EntitySuggestionViewProperties.DESCRIPTION_TEXT.equals(propertyKey)) {
+            view.setDescriptionText(model.get(EntitySuggestionViewProperties.DESCRIPTION_TEXT));
+        }
+    }
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/entity/EntitySuggestionViewProperties.java b/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/entity/EntitySuggestionViewProperties.java
new file mode 100644
index 0000000..3735a88
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/entity/EntitySuggestionViewProperties.java
@@ -0,0 +1,33 @@
+// 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.omnibox.suggestions.entity;
+
+import org.chromium.chrome.browser.omnibox.suggestions.SuggestionCommonProperties;
+import org.chromium.chrome.browser.omnibox.suggestions.basic.SuggestionViewDelegate;
+import org.chromium.ui.modelutil.PropertyKey;
+import org.chromium.ui.modelutil.PropertyModel;
+import org.chromium.ui.modelutil.PropertyModel.WritableObjectPropertyKey;
+
+/**
+ * The properties associated with rendering the entity suggestion view.
+ */
+class EntitySuggestionViewProperties {
+    /** The delegate to handle actions on the suggestion view. */
+    public static final WritableObjectPropertyKey<SuggestionViewDelegate> DELEGATE =
+            new WritableObjectPropertyKey<>();
+
+    /** Text content for the first line of text (subject). */
+    public static final WritableObjectPropertyKey<String> SUBJECT_TEXT =
+            new WritableObjectPropertyKey<>();
+    /** Text content for the second line of text (description). */
+    public static final WritableObjectPropertyKey<String> DESCRIPTION_TEXT =
+            new WritableObjectPropertyKey<>();
+
+    public static final PropertyKey[] ALL_UNIQUE_KEYS =
+            new PropertyKey[] {DELEGATE, SUBJECT_TEXT, DESCRIPTION_TEXT};
+
+    public static final PropertyKey[] ALL_KEYS =
+            PropertyModel.concatKeys(ALL_UNIQUE_KEYS, SuggestionCommonProperties.ALL_KEYS);
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/preferences/ChromePreferenceManager.java b/chrome/android/java/src/org/chromium/chrome/browser/preferences/ChromePreferenceManager.java
index 24f6322..0313ce6 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/preferences/ChromePreferenceManager.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/preferences/ChromePreferenceManager.java
@@ -316,6 +316,13 @@
     public static final String TAB_GROUPS_ANDROID_ENABLED_KEY = "tab_group_android_enabled";
 
     /**
+     * Whether or not the tab group UI improvement is enabled.
+     * Default value is false.
+     */
+    public static final String TAB_GROUPS_UI_IMPROVEMENTS_ANDROID_ENABLED_KEY =
+            "tab_group_ui_improvements_android_enabled";
+
+    /**
      * Key for whether PrefetchBackgroundTask should load native in service manager only mode.
      * Default value is false.
      */
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/suggestions/SuggestionsBinder.java b/chrome/android/java/src/org/chromium/chrome/browser/suggestions/SuggestionsBinder.java
index f9a1fcf..483799e 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/suggestions/SuggestionsBinder.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/suggestions/SuggestionsBinder.java
@@ -4,7 +4,6 @@
 
 package org.chromium.chrome.browser.suggestions;
 
-import android.content.res.ColorStateList;
 import android.graphics.Bitmap;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.ColorDrawable;
@@ -28,8 +27,6 @@
 import org.chromium.base.SysUtils;
 import org.chromium.chrome.R;
 import org.chromium.chrome.browser.compositor.animation.CompositorAnimationHandler;
-import org.chromium.chrome.browser.download.DownloadUtils;
-import org.chromium.chrome.browser.download.ui.DownloadFilter;
 import org.chromium.chrome.browser.ntp.cards.NewTabPageViewHolder;
 import org.chromium.chrome.browser.ntp.snippets.SnippetArticle;
 import org.chromium.chrome.browser.util.ViewUtils;
@@ -220,7 +217,6 @@
 
             mThumbnailView.setImageDrawable(colorDrawable);
         }
-        if (!mIsContextual) ApiCompatibilityUtils.setImageTintList(mThumbnailView, null);
 
         // Fetch thumbnail for the current article.
         mImageFetcher.makeArticleThumbnailRequest(
@@ -233,21 +229,6 @@
         mThumbnailView.setScaleType(ImageView.ScaleType.CENTER_CROP);
         mThumbnailView.setBackground(null);
         mThumbnailView.setImageDrawable(thumbnail);
-        if (!mIsContextual) ApiCompatibilityUtils.setImageTintList(mThumbnailView, null);
-    }
-
-    private void setThumbnailFromFileType(@DownloadFilter.Type int fileType) {
-        int iconBackgroundColor = DownloadUtils.getIconBackgroundColor(mThumbnailView.getContext());
-        ColorStateList iconForegroundColorList =
-                DownloadUtils.getIconForegroundColorList(mThumbnailView.getContext());
-
-        mThumbnailView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
-        mThumbnailView.setBackgroundColor(iconBackgroundColor);
-        mThumbnailView.setImageResource(
-                DownloadUtils.getIconResId(fileType, DownloadUtils.IconSize.DP_36));
-        if (!mIsContextual) {
-            ApiCompatibilityUtils.setImageTintList(mThumbnailView, iconForegroundColorList);
-        }
     }
 
     private void setDefaultFaviconOnView(int faviconSizePx) {
@@ -268,7 +249,6 @@
 
         mThumbnailView.setScaleType(ImageView.ScaleType.CENTER_CROP);
         mThumbnailView.setBackground(null);
-        if (!mIsContextual) ApiCompatibilityUtils.setImageTintList(mThumbnailView, null);
         int duration = FADE_IN_ANIMATION_TIME_MS;
         if (CompositorAnimationHandler.isInTestingMode()) {
             mThumbnailView.setImageDrawable(thumbnail);
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/tab/TabStateBrowserControlsVisibilityDelegate.java b/chrome/android/java/src/org/chromium/chrome/browser/tab/TabStateBrowserControlsVisibilityDelegate.java
index fb785eb..4220046d 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/tab/TabStateBrowserControlsVisibilityDelegate.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/tab/TabStateBrowserControlsVisibilityDelegate.java
@@ -135,6 +135,7 @@
     @Override
     public boolean canAutoHideBrowserControls() {
         if (ChromeFeatureList.isEnabled(ChromeFeatureList.DONT_AUTO_HIDE_BROWSER_CONTROLS)
+                && mTab.getActivity().getToolbarManager() != null
                 && mTab.getActivity().getToolbarManager().getBottomToolbarCoordinator() != null) {
             return false;
         }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/usage_stats/SuspendedTab.java b/chrome/android/java/src/org/chromium/chrome/browser/usage_stats/SuspendedTab.java
index 98f78a3..8becdfde 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/usage_stats/SuspendedTab.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/usage_stats/SuspendedTab.java
@@ -4,6 +4,7 @@
 
 package org.chromium.chrome.browser.usage_stats;
 
+import android.content.ActivityNotFoundException;
 import android.content.Context;
 import android.content.Intent;
 import android.view.LayoutInflater;
@@ -14,6 +15,8 @@
 import android.widget.LinearLayout.LayoutParams;
 import android.widget.TextView;
 
+import org.chromium.base.ContextUtils;
+import org.chromium.base.Log;
 import org.chromium.base.UserData;
 import org.chromium.base.VisibleForTesting;
 import org.chromium.chrome.R;
@@ -26,8 +29,11 @@
  * domain name (FQDN) has been suspended via Digital Wellbeing.
  */
 public class SuspendedTab extends EmptyTabObserver implements UserData {
-    private static final String DIGITAL_WELLBEING_DASHBOARD_ACTION =
-            "com.google.android.apps.wellbeing.action.APP_USAGE_DASHBOARD";
+    private static final String DIGITAL_WELLBEING_SITE_DETAILS_ACTION =
+            "org.chromium.chrome.browser.usage_stats.action.SHOW_WEBSITE_DETAILS";
+    private static final String EXTRA_FQDN_NAME =
+            "org.chromium.chrome.browser.usage_stats.extra.FULLY_QUALIFIED_DOMAIN_NAME";
+    private static final String TAG = "SuspendedTab";
     private static final Class<SuspendedTab> USER_DATA_KEY = SuspendedTab.class;
 
     public static SuspendedTab from(Tab tab) {
@@ -105,21 +111,6 @@
         LayoutInflater inflater = LayoutInflater.from(context);
 
         View suspendedTabView = inflater.inflate(R.layout.suspended_tab, null);
-        TextView explanationText =
-                (TextView) suspendedTabView.findViewById(R.id.suspended_tab_explanation);
-        explanationText.setText(
-                context.getString(R.string.usage_stats_site_paused_explanation, mFqdn));
-
-        View settingsLink = suspendedTabView.findViewById(R.id.suspended_tab_settings_button);
-        settingsLink.setOnClickListener(new OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                Intent intent = new Intent(DIGITAL_WELLBEING_DASHBOARD_ACTION);
-                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-                context.startActivity(intent);
-            }
-        });
-
         return suspendedTabView;
     }
 
@@ -134,6 +125,7 @@
         parent.addView(mView,
                 new LinearLayout.LayoutParams(
                         LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+        updateFqdnText();
     }
 
     private void updateFqdnText() {
@@ -141,6 +133,27 @@
         TextView explanationText = (TextView) mView.findViewById(R.id.suspended_tab_explanation);
         explanationText.setText(
                 context.getString(R.string.usage_stats_site_paused_explanation, mFqdn));
+        setSettingsLinkClickListener();
+    }
+
+    private void setSettingsLinkClickListener() {
+        Context context = mTab.getContext();
+        View settingsLink = mView.findViewById(R.id.suspended_tab_settings_button);
+        settingsLink.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                Intent intent = new Intent(DIGITAL_WELLBEING_SITE_DETAILS_ACTION);
+                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+                intent.putExtra(EXTRA_FQDN_NAME, mFqdn);
+                intent.putExtra(Intent.EXTRA_PACKAGE_NAME,
+                        ContextUtils.getApplicationContext().getPackageName());
+                try {
+                    context.startActivity(intent);
+                } catch (ActivityNotFoundException e) {
+                    Log.e(TAG, "No activity found for site details intent", e);
+                }
+            }
+        });
     }
 
     private void removeViewIfPresent() {
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/util/FeatureUtilities.java b/chrome/android/java/src/org/chromium/chrome/browser/util/FeatureUtilities.java
index 477b1887..67e707d 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/util/FeatureUtilities.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/util/FeatureUtilities.java
@@ -83,6 +83,7 @@
     private static Boolean sShouldPrioritizeBootstrapTasks;
     private static Boolean sIsGridTabSwitcherEnabled;
     private static Boolean sIsTabGroupsAndroidEnabled;
+    private static Boolean sIsTabGroupUiImprovementsAndroidEnabled;
     private static Boolean sFeedEnabled;
     private static Boolean sServiceManagerForBackgroundPrefetch;
     private static Boolean sIsNetworkServiceWarmUpEnabled;
@@ -213,6 +214,7 @@
 
         if (isHighEndPhone()) cacheGridTabSwitcherEnabled();
         if (isHighEndPhone()) cacheTabGroupsAndroidEnabled();
+        if (isHighEndPhone()) cacheTabGroupsAndroidUiImprovementsEnabled();
 
         // Propagate REACHED_CODE_PROFILER feature value to LibraryLoader. This can't be done in
         // LibraryLoader itself because it lives in //base and can't depend on ChromeFeatureList.
@@ -639,6 +641,34 @@
                         ContextUtils.getApplicationContext());
     }
 
+    private static void cacheTabGroupsAndroidUiImprovementsEnabled() {
+        ChromePreferenceManager.getInstance().writeBoolean(
+                ChromePreferenceManager.TAB_GROUPS_UI_IMPROVEMENTS_ANDROID_ENABLED_KEY,
+                !DeviceClassManager.enableAccessibilityLayout()
+                        && (ChromeFeatureList.isEnabled(
+                                    ChromeFeatureList.DOWNLOAD_TAB_MANAGEMENT_MODULE)
+                                || ChromeFeatureList.isEnabled(
+                                        ChromeFeatureList.TAB_GROUPS_UI_IMPROVEMENTS_ANDROID))
+                        && TabManagementModuleProvider.getTabManagementModule() != null
+                        && ChromeFeatureList.isEnabled(
+                                ChromeFeatureList.TAB_GROUPS_UI_IMPROVEMENTS_ANDROID));
+    }
+
+    /**
+     * @return Whether the tab group ui improvement feature is enabled and available for use.
+     */
+    public static boolean isTabGroupsAndroidUiImprovementsEnabled() {
+        if (!isTabGroupsAndroidEnabled()) return false;
+
+        if (sIsTabGroupUiImprovementsAndroidEnabled == null) {
+            ChromePreferenceManager preferenceManager = ChromePreferenceManager.getInstance();
+
+            sIsTabGroupUiImprovementsAndroidEnabled = preferenceManager.readBoolean(
+                    ChromePreferenceManager.TAB_GROUPS_UI_IMPROVEMENTS_ANDROID_ENABLED_KEY, false);
+        }
+        return sIsTabGroupUiImprovementsAndroidEnabled;
+    }
+
     /**
      * @return Whether this device is running Android Go. This is assumed when we're running Android
      * O or later and we're on a low-end device.
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/vr/ArConsentDialog.java b/chrome/android/java/src/org/chromium/chrome/browser/vr/ArConsentDialog.java
new file mode 100644
index 0000000..aec9936
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/vr/ArConsentDialog.java
@@ -0,0 +1,139 @@
+// 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.vr;
+
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.support.annotation.NonNull;
+
+import org.chromium.base.Log;
+import org.chromium.chrome.R;
+import org.chromium.chrome.browser.ChromeActivity;
+import org.chromium.ui.base.PermissionCallback;
+import org.chromium.ui.base.WindowAndroid;
+import org.chromium.ui.modaldialog.DialogDismissalCause;
+import org.chromium.ui.modaldialog.ModalDialogManager;
+import org.chromium.ui.modaldialog.ModalDialogProperties;
+import org.chromium.ui.modelutil.PropertyModel;
+
+/**
+ * Provides a consent dialog shown before entering an immersive AR session.
+ *
+ * <p>For the duration of the session, the site will get ARCore world understanding
+ * data such as hit tests or plane detection, and also camera movement tracking via
+ * 6DoF poses. The browser process separately receives the camera image and composites
+ * that image with the application-drawn AR image.</p>
+ *
+ * <p>This is different from typical camera permission usage since the web page
+ * will NOT get access to camera images. The user consent is only valid for
+ * the duration of one session and is not persistent.</p>
+ *
+ * <p>The browser needs Android-level camera access for using ARCore, this
+ * is requested if needed after the user has granted consent for the AR session.</p>
+ */
+public class ArConsentDialog implements ModalDialogProperties.Controller {
+    private static final String TAG = "ArConsentDialog";
+    private static final boolean DEBUG_LOGS = false;
+
+    private ModalDialogManager mModalDialogManager;
+    private ArCoreJavaUtils mArCoreJavaUtils;
+    private ChromeActivity mActivity;
+
+    public static void showDialog(ChromeActivity activity, ArCoreJavaUtils caller) {
+        ArConsentDialog dialog = new ArConsentDialog();
+        dialog.show(activity, caller);
+    }
+
+    public void show(@NonNull ChromeActivity activity, @NonNull ArCoreJavaUtils caller) {
+        mArCoreJavaUtils = caller;
+        mActivity = activity;
+
+        Resources resources = activity.getResources();
+        PropertyModel model = new PropertyModel.Builder(ModalDialogProperties.ALL_KEYS)
+                                      .with(ModalDialogProperties.CONTROLLER, this)
+                                      .with(ModalDialogProperties.TITLE, resources,
+                                              R.string.ar_immersive_mode_consent_title)
+                                      .with(ModalDialogProperties.MESSAGE, resources,
+                                              R.string.ar_immersive_mode_consent_message)
+                                      .with(ModalDialogProperties.POSITIVE_BUTTON_TEXT, resources,
+                                              R.string.ar_immersive_mode_consent_button)
+                                      .with(ModalDialogProperties.NEGATIVE_BUTTON_TEXT, resources,
+                                              R.string.cancel)
+                                      .with(ModalDialogProperties.CANCEL_ON_TOUCH_OUTSIDE, true)
+                                      .build();
+        mModalDialogManager = activity.getModalDialogManager();
+        mModalDialogManager.showDialog(model, ModalDialogManager.ModalDialogType.APP);
+    }
+
+    @Override
+    public void onClick(PropertyModel model, int buttonType) {
+        if (buttonType == ModalDialogProperties.ButtonType.NEGATIVE) {
+            mModalDialogManager.dismissDialog(model, DialogDismissalCause.NEGATIVE_BUTTON_CLICKED);
+        } else {
+            mModalDialogManager.dismissDialog(model, DialogDismissalCause.POSITIVE_BUTTON_CLICKED);
+        }
+    }
+
+    @Override
+    public void onDismiss(PropertyModel model, int dismissalCause) {
+        if (dismissalCause == DialogDismissalCause.POSITIVE_BUTTON_CLICKED) {
+            onConsentGranted();
+        } else {
+            onConsentDenied();
+        }
+    }
+
+    private void onConsentGranted() {
+        if (DEBUG_LOGS) Log.i(TAG, "onConsentGranted");
+
+        WindowAndroid window = mActivity.getWindowAndroid();
+        if (!window.hasPermission(android.Manifest.permission.CAMERA)) {
+            // The user has agreed to proceed with the AR session, but the browser
+            // application doesn't have the prerequisite Android-level camera permission
+            // needed for using ARCore internally. Show the system permission prompt.
+            requestCameraPermission();
+            return;
+        }
+        startSession();
+    }
+
+    private void requestCameraPermission() {
+        PermissionCallback callback = new PermissionCallback() {
+            @Override
+            public void onRequestPermissionsResult(String[] permissions, int[] grantResults) {
+                if (DEBUG_LOGS) Log.i(TAG, "onRequestPermissionsResult");
+                // If request is cancelled, the result arrays are empty.
+                if (grantResults.length > 0
+                        && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+                    if (DEBUG_LOGS) Log.i(TAG, "onRequestPermissionsResult=granted");
+                    startSession();
+                } else {
+                    // Didn't get permission :-(
+                    if (DEBUG_LOGS) Log.i(TAG, "onRequestPermissionsResult=denied");
+                    endSession();
+                }
+            }
+        };
+
+        WindowAndroid window = mActivity.getWindowAndroid();
+        window.requestPermissions(new String[] {android.Manifest.permission.CAMERA}, callback);
+    }
+
+    private void onConsentDenied() {
+        if (DEBUG_LOGS) Log.i(TAG, "onConsentDenied");
+        endSession();
+    }
+
+    private void startSession() {
+        if (DEBUG_LOGS) Log.i(TAG, "startSession");
+        // We have user consent to start the session.
+        mArCoreJavaUtils.onStartSession(mActivity);
+    }
+
+    private void endSession() {
+        if (DEBUG_LOGS) Log.i(TAG, "endSession");
+        mArCoreJavaUtils.onDrawingSurfaceDestroyed();
+    }
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/vr/ArCoreJavaUtils.java b/chrome/android/java/src/org/chromium/chrome/browser/vr/ArCoreJavaUtils.java
index 16ed2fd..a8f7cef1 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/vr/ArCoreJavaUtils.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/vr/ArCoreJavaUtils.java
@@ -6,6 +6,7 @@
 
 import android.app.Activity;
 import android.content.Context;
+import android.view.Surface;
 
 import dalvik.system.BaseDexClassLoader;
 
@@ -17,6 +18,7 @@
 import org.chromium.base.annotations.CalledByNative;
 import org.chromium.base.annotations.JNINamespace;
 import org.chromium.chrome.R;
+import org.chromium.chrome.browser.ChromeActivity;
 import org.chromium.chrome.browser.infobar.InfoBarIdentifier;
 import org.chromium.chrome.browser.infobar.SimpleConfirmInfoBarBuilder;
 import org.chromium.chrome.browser.modules.ModuleInstallUi;
@@ -29,11 +31,14 @@
 @JNINamespace("vr")
 public class ArCoreJavaUtils implements ModuleInstallUi.FailureUiListener {
     private static final String TAG = "ArCoreJavaUtils";
+    private static final boolean DEBUG_LOGS = false;
 
     private long mNativeArCoreJavaUtils;
     private boolean mAppInfoInitialized;
     private Tab mTab;
 
+    private ArImmersiveOverlay mArImmersiveOverlay;
+
     // Instance that requested installation of ARCore.
     // Should be non-null only if there is a pending request to install ARCore.
     private static ArCoreJavaUtils sRequestInstallInstance;
@@ -102,11 +107,53 @@
     }
 
     private ArCoreJavaUtils(long nativeArCoreJavaUtils) {
+        if (DEBUG_LOGS) Log.i(TAG, "constructor, nativeArCoreJavaUtils=" + nativeArCoreJavaUtils);
         mNativeArCoreJavaUtils = nativeArCoreJavaUtils;
         initializeAppInfo();
     }
 
     @CalledByNative
+    private void launchArConsentDialog(final Tab tab) {
+        if (DEBUG_LOGS) Log.i(TAG, "launchArConsentDialog");
+        final ChromeActivity activity = tab.getActivity();
+        ArConsentDialog.showDialog(activity, this);
+    }
+
+    @CalledByNative
+    private void destroyArImmersiveOverlay() {
+        if (DEBUG_LOGS) Log.i(TAG, "destroyArImmersiveOverlay");
+        if (mArImmersiveOverlay != null) {
+            mArImmersiveOverlay.destroyDialog();
+            mArImmersiveOverlay = null;
+        }
+    }
+
+    public void onStartSession(ChromeActivity activity) {
+        if (DEBUG_LOGS) Log.i(TAG, "onSessionStarted");
+        mArImmersiveOverlay = new ArImmersiveOverlay();
+        mArImmersiveOverlay.show(activity, this);
+    }
+
+    public void onDrawingSurfaceReady(Surface surface, int rotation, int width, int height) {
+        if (DEBUG_LOGS) Log.i(TAG, "onDrawingSurfaceReady");
+        if (mNativeArCoreJavaUtils == 0) return;
+        nativeOnDrawingSurfaceReady(mNativeArCoreJavaUtils, surface, rotation, width, height);
+    }
+
+    public void onDrawingSurfaceTouch(boolean isTouching, float x, float y) {
+        if (DEBUG_LOGS) Log.i(TAG, "onDrawingSurfaceTouch");
+        if (mNativeArCoreJavaUtils == 0) return;
+        nativeOnDrawingSurfaceTouch(mNativeArCoreJavaUtils, isTouching, x, y);
+    }
+
+    public void onDrawingSurfaceDestroyed() {
+        if (DEBUG_LOGS) Log.i(TAG, "onDrawingSurfaceDestroyed");
+        if (mNativeArCoreJavaUtils == 0) return;
+        mArImmersiveOverlay = null;
+        nativeOnDrawingSurfaceDestroyed(mNativeArCoreJavaUtils);
+    }
+
+    @CalledByNative
     private void onNativeDestroy() {
         mNativeArCoreJavaUtils = 0;
     }
@@ -298,4 +345,10 @@
             long nativeArCoreJavaUtils, boolean success);
     private native void nativeOnRequestInstallSupportedArCoreResult(
             long nativeArCoreJavaUtils, boolean success);
+
+    private native void nativeOnDrawingSurfaceReady(
+            long nativeArCoreJavaUtils, Surface surface, int rotation, int width, int height);
+    private native void nativeOnDrawingSurfaceTouch(
+            long nativeArCoreJavaUtils, boolean touching, float x, float y);
+    private native void nativeOnDrawingSurfaceDestroyed(long nativeArCoreJavaUtils);
 }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/vr/ArImmersiveOverlay.java b/chrome/android/java/src/org/chromium/chrome/browser/vr/ArImmersiveOverlay.java
new file mode 100644
index 0000000..4ee8082
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/vr/ArImmersiveOverlay.java
@@ -0,0 +1,174 @@
+// 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.vr;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.content.pm.ActivityInfo;
+import android.support.annotation.NonNull;
+import android.view.MotionEvent;
+import android.view.SurfaceHolder;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+
+import org.chromium.base.Log;
+import org.chromium.chrome.browser.ChromeActivity;
+import org.chromium.content_public.browser.ScreenOrientationDelegate;
+import org.chromium.content_public.browser.ScreenOrientationProvider;
+
+/**
+ * Provides a fullscreen overlay for immersive AR mode.
+ */
+public class ArImmersiveOverlay implements SurfaceHolder.Callback2,
+                                           DialogInterface.OnCancelListener, View.OnTouchListener,
+                                           ScreenOrientationDelegate {
+    private static final String TAG = "ArImmersiveOverlay";
+    private static final boolean DEBUG_LOGS = false;
+
+    // Android supports multiple variants of fullscreen applications. Currently, we use a fullscreen
+    // layout with translucent navigation bar, where the content shows behind the navigation bar.
+    // Alternatively, we could add FLAG_HIDE_NAVIGATION and FLAG_IMMERSIVE_STICKY to hide the
+    // navigation bar, but then we'd need to show a "pull from top and press back button to exit"
+    // prompt.
+    private static final int VISIBILITY_FLAGS_IMMERSIVE = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+            | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
+
+    private ArCoreJavaUtils mArCoreJavaUtils;
+    private ChromeActivity mActivity;
+    private boolean mSurfaceReportedReady;
+    private Dialog mDialog;
+    private Integer mRestoreOrientation;
+    private boolean mCleanupInProgress;
+
+    public void show(@NonNull ChromeActivity activity, @NonNull ArCoreJavaUtils caller) {
+        if (DEBUG_LOGS) Log.i(TAG, "constructor");
+        mArCoreJavaUtils = caller;
+        mActivity = activity;
+
+        // Create a fullscreen dialog and set the system / navigation bars translucent.
+        mDialog = new Dialog(activity, android.R.style.Theme_NoTitleBar_Fullscreen);
+        mDialog.getWindow().setBackgroundDrawable(null);
+        int wmFlags = WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
+                | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION;
+        mDialog.getWindow().addFlags(wmFlags);
+
+        mDialog.getWindow().takeSurface(this);
+
+        View view = mDialog.getWindow().getDecorView();
+        view.setSystemUiVisibility(VISIBILITY_FLAGS_IMMERSIVE);
+        view.setOnTouchListener(this);
+        mDialog.setOnCancelListener(this);
+
+        mDialog.getWindow().setLayout(
+                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
+        mDialog.show();
+    }
+
+    public void destroyDialog() {
+        if (DEBUG_LOGS) Log.i(TAG, "destroyDialog");
+        cleanupAndExit();
+    }
+
+    @Override // View.OnTouchListener
+    public boolean onTouch(View v, MotionEvent ev) {
+        // Only forward primary actions, ignore more complex events such as secondary pointer
+        // touches. Ignore batching since we're only sending one ray pose per frame.
+        if (ev.getAction() == MotionEvent.ACTION_DOWN || ev.getAction() == MotionEvent.ACTION_MOVE
+                || ev.getAction() == MotionEvent.ACTION_UP) {
+            boolean touching = ev.getAction() != MotionEvent.ACTION_UP;
+            if (DEBUG_LOGS) Log.i(TAG, "onTouch touching=" + touching);
+            mArCoreJavaUtils.onDrawingSurfaceTouch(touching, ev.getX(0), ev.getY(0));
+        }
+        return true;
+    }
+
+    @Override // ScreenOrientationDelegate
+    public boolean canUnlockOrientation(Activity activity, int defaultOrientation) {
+        if (mActivity == activity && mRestoreOrientation != null) {
+            mRestoreOrientation = defaultOrientation;
+            return false;
+        }
+        return true;
+    }
+
+    @Override // ScreenOrientationDelegate
+    public boolean canLockOrientation() {
+        return false;
+    }
+
+    @Override // SurfaceHolder.Callback2
+    public void surfaceCreated(SurfaceHolder holder) {
+        if (DEBUG_LOGS) Log.i(TAG, "surfaceCreated");
+        // Do nothing here, we'll handle setup on the following surfaceChanged.
+    }
+
+    @Override // SurfaceHolder.Callback2
+    public void surfaceRedrawNeeded(SurfaceHolder holder) {
+        if (DEBUG_LOGS) Log.i(TAG, "surfaceRedrawNeeded");
+    }
+
+    @Override // SurfaceHolder.Callback2
+    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+        // WebXR immersive sessions don't support resize, so use the first reported size.
+        // We shouldn't get resize events since we're using FLAG_LAYOUT_STABLE and are
+        // locking screen orientation.
+        if (mSurfaceReportedReady) {
+            int rotation = mActivity.getWindowManager().getDefaultDisplay().getRotation();
+            if (DEBUG_LOGS)
+                Log.i(TAG,
+                        "surfaceChanged ignoring change to width=" + width + " height=" + height
+                                + " rotation=" + rotation);
+            return;
+        }
+
+        // Save current orientation mode, and then lock current orientation.
+        ScreenOrientationProvider.setOrientationDelegate(this);
+        if (mRestoreOrientation == null) {
+            mRestoreOrientation = mActivity.getRequestedOrientation();
+        }
+        mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LOCKED);
+
+        int rotation = mActivity.getWindowManager().getDefaultDisplay().getRotation();
+        if (DEBUG_LOGS)
+            Log.i(TAG, "surfaceChanged size=" + width + "x" + height + " rotation=" + rotation);
+        mArCoreJavaUtils.onDrawingSurfaceReady(holder.getSurface(), rotation, width, height);
+        mSurfaceReportedReady = true;
+    }
+
+    @Override // SurfaceHolder.Callback2
+    public void surfaceDestroyed(SurfaceHolder holder) {
+        if (DEBUG_LOGS) Log.i(TAG, "surfaceDestroyed");
+        cleanupAndExit();
+    }
+
+    @Override // DialogInterface.OnCancelListener
+    public void onCancel(DialogInterface dialog) {
+        if (DEBUG_LOGS) Log.i(TAG, "onCancel");
+        cleanupAndExit();
+    }
+
+    public void cleanupAndExit() {
+        if (DEBUG_LOGS) Log.i(TAG, "cleanupAndExit");
+
+        // Avoid duplicate cleanup if we're exiting via destroyDialog, that
+        // triggers cleanupAndExit -> dismiss -> surfaceDestroyed -> cleanupAndExit.
+        if (mCleanupInProgress) return;
+        mCleanupInProgress = true;
+
+        // Restore orientation.
+        ScreenOrientationProvider.setOrientationDelegate(null);
+        if (mRestoreOrientation != null) mActivity.setRequestedOrientation(mRestoreOrientation);
+        mRestoreOrientation = null;
+
+        // The surface is destroyed when exiting via "back" button, but also in other lifecycle
+        // situations such as switching apps or toggling the phone's power button. Treat each of
+        // these as exiting the immersive session. We need to dismiss the dialog to ensure
+        // consistent state after non-exiting lifecycle events.
+        mDialog.dismiss();
+        mArCoreJavaUtils.onDrawingSurfaceDestroyed();
+    }
+}
diff --git a/chrome/android/java/strings/android_chrome_strings.grd b/chrome/android/java/strings/android_chrome_strings.grd
index 36977577..834170c 100644
--- a/chrome/android/java/strings/android_chrome_strings.grd
+++ b/chrome/android/java/strings/android_chrome_strings.grd
@@ -3880,6 +3880,19 @@
         <message name="IDS_AR_MODULE_TITLE" desc="Text shown when the AR module is referenced in install start, success, failure UI (e.g. in IDS_MODULE_INSTALL_START_TEXT, which will expand to 'Installing Augmented Reality for Chrome…').">
           Augmented Reality
         </message>
+        <message name="IDS_AR_IMMERSIVE_MODE_CONSENT_TITLE" desc="Title for dialog shown when a site requests consent for starting an augmented reality session.">
+          Start Augmented Reality session?
+        </message>
+        <message name="IDS_AR_IMMERSIVE_MODE_CONSENT_MESSAGE" desc="Message for dialog shown when a site requests consent for starting an augmented reality session.">
+          For the duration of this session, the site will be able to:
+• create a 3D map of your environment
+• track camera motion
+
+The site does NOT gain access to the camera. The camera images are only visible to you.
+        </message>
+        <message name="IDS_AR_IMMERSIVE_MODE_CONSENT_BUTTON" desc="Confirm button for dialog shown when a site requests consent for starting an augmented reality session.">
+          Enter AR
+        </message>
       </if>
 
       <!-- Dynamic feature modules -->
diff --git a/chrome/android/java/strings/android_chrome_strings_grd/IDS_AR_IMMERSIVE_MODE_CONSENT_BUTTON.png.sha1 b/chrome/android/java/strings/android_chrome_strings_grd/IDS_AR_IMMERSIVE_MODE_CONSENT_BUTTON.png.sha1
new file mode 100644
index 0000000..40f2a43
--- /dev/null
+++ b/chrome/android/java/strings/android_chrome_strings_grd/IDS_AR_IMMERSIVE_MODE_CONSENT_BUTTON.png.sha1
@@ -0,0 +1 @@
+e74fa94d9cdb44a1e0fd6775706a1ec6027c7bae
\ No newline at end of file
diff --git a/chrome/android/java/strings/android_chrome_strings_grd/IDS_AR_IMMERSIVE_MODE_CONSENT_MESSAGE.png.sha1 b/chrome/android/java/strings/android_chrome_strings_grd/IDS_AR_IMMERSIVE_MODE_CONSENT_MESSAGE.png.sha1
new file mode 100644
index 0000000..40f2a43
--- /dev/null
+++ b/chrome/android/java/strings/android_chrome_strings_grd/IDS_AR_IMMERSIVE_MODE_CONSENT_MESSAGE.png.sha1
@@ -0,0 +1 @@
+e74fa94d9cdb44a1e0fd6775706a1ec6027c7bae
\ No newline at end of file
diff --git a/chrome/android/java/strings/android_chrome_strings_grd/IDS_AR_IMMERSIVE_MODE_CONSENT_TITLE.png.sha1 b/chrome/android/java/strings/android_chrome_strings_grd/IDS_AR_IMMERSIVE_MODE_CONSENT_TITLE.png.sha1
new file mode 100644
index 0000000..40f2a43
--- /dev/null
+++ b/chrome/android/java/strings/android_chrome_strings_grd/IDS_AR_IMMERSIVE_MODE_CONSENT_TITLE.png.sha1
@@ -0,0 +1 @@
+e74fa94d9cdb44a1e0fd6775706a1ec6027c7bae
\ No newline at end of file
diff --git a/chrome/android/java_sources.gni b/chrome/android/java_sources.gni
index 173a9b3..8aedf50 100644
--- a/chrome/android/java_sources.gni
+++ b/chrome/android/java_sources.gni
@@ -44,9 +44,11 @@
 
 if (enable_arcore) {
   chrome_java_sources += [
+    "java/src/org/chromium/chrome/browser/vr/ArConsentDialog.java",
     "java/src/org/chromium/chrome/browser/vr/ArCoreJavaUtils.java",
     "java/src/org/chromium/chrome/browser/vr/ArDelegateImpl.java",
     "java/src/org/chromium/chrome/browser/vr/ArCoreShim.java",
+    "java/src/org/chromium/chrome/browser/vr/ArImmersiveOverlay.java",
   ]
 }
 
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/contextual_suggestions/ContextualSuggestionsTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/contextual_suggestions/ContextualSuggestionsTest.java
deleted file mode 100644
index a6951b8..0000000
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/contextual_suggestions/ContextualSuggestionsTest.java
+++ /dev/null
@@ -1,787 +0,0 @@
-// Copyright 2018 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.contextual_suggestions;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-
-import android.os.SystemClock;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.LargeTest;
-import android.support.test.filters.MediumTest;
-import android.support.v7.widget.RecyclerView;
-import android.view.View;
-
-import org.junit.After;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.RuleChain;
-import org.junit.rules.TestRule;
-import org.junit.runner.RunWith;
-
-import org.chromium.base.ThreadUtils;
-import org.chromium.base.test.util.CallbackHelper;
-import org.chromium.base.test.util.CommandLineFlags;
-import org.chromium.base.test.util.DisabledTest;
-import org.chromium.base.test.util.Feature;
-import org.chromium.base.test.util.Restriction;
-import org.chromium.chrome.R;
-import org.chromium.chrome.browser.ChromeActivity;
-import org.chromium.chrome.browser.ChromeFeatureList;
-import org.chromium.chrome.browser.ChromeSwitches;
-import org.chromium.chrome.browser.ChromeTabbedActivity;
-import org.chromium.chrome.browser.contextual_suggestions.ContextualSuggestionsModel.PropertyKey;
-import org.chromium.chrome.browser.dependency_injection.ChromeAppModule;
-import org.chromium.chrome.browser.dependency_injection.ModuleOverridesRule;
-import org.chromium.chrome.browser.feature_engagement.TrackerFactory;
-import org.chromium.chrome.browser.fullscreen.FullscreenManagerTestUtils;
-import org.chromium.chrome.browser.multiwindow.MultiWindowTestHelper;
-import org.chromium.chrome.browser.multiwindow.MultiWindowUtils;
-import org.chromium.chrome.browser.native_page.ContextMenuManager;
-import org.chromium.chrome.browser.ntp.cards.NewTabPageViewHolder.PartialBindCallback;
-import org.chromium.chrome.browser.ntp.snippets.SnippetArticle;
-import org.chromium.chrome.browser.ntp.snippets.SnippetArticleViewHolder;
-import org.chromium.chrome.browser.profiles.Profile;
-import org.chromium.chrome.browser.tabmodel.TabModel;
-import org.chromium.chrome.browser.tabmodel.TabSelectionType;
-import org.chromium.chrome.browser.test.ScreenShooter;
-import org.chromium.chrome.browser.toolbar.top.ToolbarPhone;
-import org.chromium.chrome.browser.widget.bottomsheet.BottomSheet;
-import org.chromium.chrome.test.BottomSheetTestRule;
-import org.chromium.chrome.test.ChromeActivityTestRule;
-import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
-import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
-import org.chromium.chrome.test.util.ChromeTabUtils;
-import org.chromium.chrome.test.util.RenderTestRule;
-import org.chromium.chrome.test.util.browser.Features.DisableFeatures;
-import org.chromium.chrome.test.util.browser.Features.EnableFeatures;
-import org.chromium.chrome.test.util.browser.RecyclerViewTestUtils;
-import org.chromium.chrome.test.util.browser.compositor.layouts.DisableChromeAnimations;
-import org.chromium.components.feature_engagement.FeatureConstants;
-import org.chromium.content_public.browser.LoadUrlParams;
-import org.chromium.content_public.browser.test.util.Criteria;
-import org.chromium.content_public.browser.test.util.CriteriaHelper;
-import org.chromium.content_public.browser.test.util.TestWebContentsObserver;
-import org.chromium.net.test.EmbeddedTestServer;
-import org.chromium.ui.modelutil.ListObservable;
-import org.chromium.ui.modelutil.ListObservable.ListObserver;
-import org.chromium.ui.test.util.UiRestriction;
-
-import java.util.Locale;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeoutException;
-
-/**
- * Tests related to displaying contextual suggestions in a bottom sheet.
- */
-@RunWith(ChromeJUnit4ClassRunner.class)
-@Restriction(UiRestriction.RESTRICTION_TYPE_PHONE)
-@CommandLineFlags.Add(ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE)
-public class ContextualSuggestionsTest {
-    private final TestRule mModuleOverridesRule = new ModuleOverridesRule()
-            .setOverride(ContextualSuggestionsModuleForTest.Factory.class,
-                    ContextualSuggestionsModuleForTest::new)
-            .setOverride(ChromeAppModule.Factory.class, ChromeAppModuleForTest::new);
-
-    private final ChromeTabbedActivityTestRule mActivityTestRule =
-            new ChromeTabbedActivityTestRule();
-
-    @Rule
-    public TestRule mOverrideModulesThenLaunchRule =
-            RuleChain.outerRule(mModuleOverridesRule).around(mActivityTestRule);
-    @Rule
-    public ScreenShooter mScreenShooter = new ScreenShooter();
-    @Rule
-    public TestRule mDisableChromeAnimations = new DisableChromeAnimations();
-    @Rule
-    public RenderTestRule mRenderTestRule = new RenderTestRule();
-
-    private static final String TEST_PAGE =
-            "/chrome/test/data/android/contextual_suggestions/contextual_suggestions_test.html";
-
-    private final FakeContextualSuggestionsSource mFakeSource =
-            new FakeContextualSuggestionsSource();
-
-    private EmbeddedTestServer mTestServer;
-    private ContextualSuggestionsCoordinator mCoordinator;
-    private ContextualSuggestionsMediator mMediator;
-    private ContextualSuggestionsModel mModel;
-    private BottomSheet mBottomSheet;
-    private FakeTracker mFakeTracker;
-
-    // Used in multi-instance test.
-    private ContextualSuggestionsCoordinator mCoordinator2;
-    private ContextualSuggestionsMediator mMediator2;
-    private ContextualSuggestionsModel mModel2;
-    private BottomSheet mBottomSheet2;
-
-    private int mNumberOfSourcesCreated;
-
-    private class ContextualSuggestionsModuleForTest extends ContextualSuggestionsModule {
-        @Override
-        public ContextualSuggestionsSource provideContextualSuggestionsSource(Profile profile) {
-            mNumberOfSourcesCreated++;
-            return mFakeSource;
-        }
-    }
-
-    private class ChromeAppModuleForTest extends ChromeAppModule {
-        @Override
-        public EnabledStateMonitor provideEnabledStateMonitor() {
-            return new EmptyEnabledStateMonitor() {
-                @Override
-                public boolean getSettingsEnabled() {
-                    return true;
-                }
-
-                @Override
-                public boolean getEnabledState() {
-                    return true;
-                }
-            };
-        }
-    }
-
-    @Before
-    public void setUp() throws Exception {
-        FetchHelper.setDisableDelayForTesting(true);
-        ContextualSuggestionsMediator.setOverrideIPHTimeoutForTesting(true);
-
-        mFakeTracker = new FakeTracker(FeatureConstants.CONTEXTUAL_SUGGESTIONS_FEATURE);
-        TrackerFactory.setTrackerForTests(mFakeTracker);
-
-        mTestServer = EmbeddedTestServer.createAndStartServer(InstrumentationRegistry.getContext());
-        mActivityTestRule.startMainActivityWithURL(mTestServer.getURL(TEST_PAGE));
-        final CallbackHelper waitForSuggestionsHelper = new CallbackHelper();
-
-        ThreadUtils.runOnUiThreadBlocking(() -> {
-            mCoordinator = mActivityTestRule.getActivity()
-                                   .getComponent()
-                                   .resolveContextualSuggestionsCoordinator();
-            mMediator = mCoordinator.getMediatorForTesting();
-            mModel = mCoordinator.getModelForTesting();
-
-            if (mModel.getTitle() != null) {
-                waitForSuggestionsHelper.notifyCalled();
-            } else {
-                mModel.addObserver((source, propertyKey) -> {
-                    if (propertyKey == PropertyKey.TITLE && mModel.getTitle() != null) {
-                        waitForSuggestionsHelper.notifyCalled();
-                    }
-                });
-            }
-        });
-
-        waitForSuggestionsHelper.waitForCallback(0);
-        mBottomSheet = mActivityTestRule.getActivity().getBottomSheet();
-    }
-
-    @After
-    public void tearDown() {
-        mTestServer.stopAndDestroyServer();
-        FetchHelper.setDisableDelayForTesting(false);
-        ContextualSuggestionsMediator.setOverrideIPHTimeoutForTesting(false);
-    }
-
-    @Test
-    @MediumTest
-    @Feature({"ContextualSuggestions"})
-    @EnableFeatures(ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_BUTTON)
-    public void testRepeatedOpen() throws Exception {
-        View toolbarButton = getToolbarButton();
-        assertEquals(
-                "Toolbar button should be visible", View.VISIBLE, toolbarButton.getVisibility());
-
-        clickToolbarButton();
-        simulateClickOnCloseButton();
-
-        assertEquals(
-                "Toolbar button should be visible", View.VISIBLE, toolbarButton.getVisibility());
-
-        clickToolbarButton();
-        testOpenFirstSuggestion();
-
-        assertEquals("Toolbar button should be visible", View.GONE, toolbarButton.getVisibility());
-    }
-
-    @Test
-    @MediumTest
-    @Feature({"ContextualSuggestions"})
-    @EnableFeatures(ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_BUTTON)
-    public void testOpenSuggestion() throws Exception {
-        clickToolbarButton();
-        testOpenFirstSuggestion();
-    }
-
-    @Test
-    @MediumTest
-    @Feature({"ContextualSuggestions"})
-    @EnableFeatures(ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_BUTTON)
-    public void testOpenArticleInNewTab() throws Exception {
-        clickToolbarButton();
-
-        SnippetArticleViewHolder holder = getFirstSuggestionViewHolder();
-        String expectedUrl = holder.getUrl();
-
-        ChromeTabUtils.invokeContextMenuAndOpenInANewTab(mActivityTestRule, holder.itemView,
-                ContextMenuManager.ContextMenuItemId.OPEN_IN_NEW_TAB, false, expectedUrl);
-
-        assertEquals("Sheet should still be opened.", BottomSheet.SheetState.HALF,
-                mBottomSheet.getSheetState());
-    }
-
-    @Test
-    @MediumTest
-    @Feature({"ContextualSuggestions"})
-    @EnableFeatures(ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_BUTTON)
-    public void testOpenSuggestionInNewTabIncognito() throws Exception {
-        clickToolbarButton();
-
-        SnippetArticleViewHolder holder = getFirstSuggestionViewHolder();
-        String expectedUrl = holder.getUrl();
-
-        ChromeTabUtils.invokeContextMenuAndOpenInANewTab(mActivityTestRule, holder.itemView,
-                ContextMenuManager.ContextMenuItemId.OPEN_IN_INCOGNITO_TAB, true, expectedUrl);
-
-        ThreadUtils.runOnUiThreadBlocking(() -> mBottomSheet.endAnimations());
-
-        assertFalse("Sheet should be closed.", mBottomSheet.isSheetOpen());
-    }
-
-    @Test
-    @MediumTest
-    @Feature({"ContextualSuggestions"})
-    @EnableFeatures(ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_BUTTON)
-    public void testShadowVisibleOnScroll() throws Exception {
-        clickToolbarButton();
-
-        assertFalse("Shadow shouldn't be visible.", mModel.getToolbarShadowVisibility());
-
-        CallbackHelper shadowVisibilityCallback = new CallbackHelper();
-
-        mModel.addObserver((source, propertyKey) -> {
-            if (propertyKey == PropertyKey.TOOLBAR_SHADOW_VISIBILITY
-                    && mModel.getToolbarShadowVisibility()) {
-                shadowVisibilityCallback.notifyCalled();
-            }
-        });
-
-        ThreadUtils.runOnUiThreadBlocking(() -> {
-            RecyclerView view =
-                    (RecyclerView) mBottomSheet.getCurrentSheetContent().getContentView();
-            view.smoothScrollToPosition(5);
-        });
-
-        shadowVisibilityCallback.waitForCallback("Shadow should be visible");
-    }
-
-    @Test
-    @MediumTest
-    @Feature({"ContextualSuggestions"})
-    @EnableFeatures(ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_BUTTON)
-    public void testCanShowInProductHelp_DefaultConfidenceThreshold() {
-        // Check fieldtrial setup.
-        Assert.assertEquals(0.d,
-                ChromeFeatureList.getFieldTrialParamByFeatureAsDouble(
-                        ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_BUTTON,
-                        ContextualSuggestionsMediator.IPH_CONFIDENCE_THRESHOLD_PARAM, 0.d),
-                0.d);
-        assertTrue(mMediator.getCanShowIphForCurrentResults());
-    }
-
-    @Test
-    @MediumTest
-    @Feature({"ContextualSuggestions"})
-    @CommandLineFlags.Add({"enable-features=" + ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_BUTTON
-            + "<FakeEoCStudyName", "force-fieldtrials=FakeEoCStudyName/Enabled",
-            "force-fieldtrial-params=FakeEoCStudyName.Enabled:"
-                    + ContextualSuggestionsMediator.IPH_CONFIDENCE_THRESHOLD_PARAM + "/0.5"})
-    public void testCanShowInProductHelp_ResultsConfidenceAboveThreshold() {
-        // Check fieldtrial setup.
-        Assert.assertEquals(0.5d,
-                ChromeFeatureList.getFieldTrialParamByFeatureAsDouble(
-                        ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_BUTTON,
-                        ContextualSuggestionsMediator.IPH_CONFIDENCE_THRESHOLD_PARAM, 0.d),
-                0.d);
-        assertTrue(mMediator.getCanShowIphForCurrentResults());
-    }
-
-    @Test
-    @MediumTest
-    @Feature({"ContextualSuggestions"})
-    @CommandLineFlags.Add({"enable-features=" + ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_BUTTON
-            + "<FakeEoCStudyName", "force-fieldtrials=FakeEoCStudyName/Enabled",
-            "force-fieldtrial-params=FakeEoCStudyName.Enabled:"
-                    + ContextualSuggestionsMediator.IPH_CONFIDENCE_THRESHOLD_PARAM + "/0.75"})
-    public void testCanShowInProductHelp_ResultsConfidenceAtThreshold() {
-        // Check fieldtrial setup.
-        Assert.assertEquals(FakeContextualSuggestionsSource.TEST_PEEK_CONFIDENCE,
-                ChromeFeatureList.getFieldTrialParamByFeatureAsDouble(
-                        ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_BUTTON,
-                        ContextualSuggestionsMediator.IPH_CONFIDENCE_THRESHOLD_PARAM, 0.d),
-                0.d);
-        assertTrue(mMediator.getCanShowIphForCurrentResults());
-    }
-
-    @Test
-    @MediumTest
-    @Feature({"ContextualSuggestions"})
-    @CommandLineFlags.Add({"enable-features=" + ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_BUTTON
-            + "<FakeEoCStudyName", "force-fieldtrials=FakeEoCStudyName/Enabled",
-            "force-fieldtrial-params=FakeEoCStudyName.Enabled:"
-                    + ContextualSuggestionsMediator.IPH_CONFIDENCE_THRESHOLD_PARAM + "/0.8"})
-    public void testCanShowInProductHelp_ResultsConfidenceBelowThreshold() {
-        // Check fieldtrial setup.
-        Assert.assertEquals(0.8d,
-                ChromeFeatureList.getFieldTrialParamByFeatureAsDouble(
-                        ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_BUTTON,
-                        ContextualSuggestionsMediator.IPH_CONFIDENCE_THRESHOLD_PARAM, 0.d),
-                0.d);
-        assertFalse(mMediator.getCanShowIphForCurrentResults());
-    }
-
-    @Test
-    @MediumTest
-    @Feature({"ContextualSuggestions"})
-    @EnableFeatures(ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_BUTTON)
-    @DisableFeatures(ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_IPH_REVERSE_SCROLL)
-    public void testInProductHelp_DontRequireReverseScroll() throws Exception {
-        // IPH can only be shown after the animation completes.
-        ThreadUtils.runOnUiThreadBlocking(
-                () -> getToolbarPhone().endExperimentalButtonAnimationForTesting());
-
-        CriteriaHelper.pollUiThread(() -> mMediator.getHelpBubbleForTesting() != null &&
-                            mMediator.getHelpBubbleForTesting().isShowing(),
-                "Help bubble never shown.");
-
-        ThreadUtils.runOnUiThreadBlocking(() -> mMediator.getHelpBubbleForTesting().dismiss());
-
-        Assert.assertEquals("Help bubble should be dimissed.", 1,
-                mFakeTracker.mDimissedCallbackHelper.getCallCount());
-    }
-
-    @Test
-    @MediumTest
-    @Feature({"ContextualSuggestions"})
-    @EnableFeatures({ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_BUTTON,
-            ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_IPH_REVERSE_SCROLL})
-    @DisabledTest(message = "https://crbug.com/890947")
-    public void testInProductHelp_RequireReverseScroll() throws Exception {
-        // IPH can only be shown after the animation to show the toolbar button completes.
-        ThreadUtils.runOnUiThreadBlocking(
-                () -> getToolbarPhone().endExperimentalButtonAnimationForTesting());
-
-        Assert.assertNull("Help bubble should not be shown yet.",
-                mMediator.getHelpBubbleForTesting());
-
-        // Scroll the base page, hiding then reshowing the browser controls.
-        FullscreenManagerTestUtils.disableBrowserOverrides();
-        FullscreenManagerTestUtils.waitForBrowserControlsToBeMoveable(
-                mActivityTestRule, mActivityTestRule.getActivity().getActivityTab());
-
-        CriteriaHelper.pollUiThread(() -> mMediator.getHelpBubbleForTesting() != null &&
-                            mMediator.getHelpBubbleForTesting().isShowing(),
-                "Help bubble never shown.");
-
-        ThreadUtils.runOnUiThreadBlocking(() -> mMediator.getHelpBubbleForTesting().dismiss());
-
-        Assert.assertEquals("Help bubble should be dimissed.", 1,
-                mFakeTracker.mDimissedCallbackHelper.getCallCount());
-    }
-
-    @Test
-    @LargeTest
-    @Feature({"ContextualSuggestions"})
-    @EnableFeatures(ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_BUTTON)
-    public void testMultiInstanceMode() throws Exception {
-        ChromeTabbedActivity activity1 = mActivityTestRule.getActivity();
-        clickToolbarButton();
-
-        MultiWindowUtils.getInstance().setIsInMultiWindowModeForTesting(true);
-        ChromeTabbedActivity activity2 = MultiWindowTestHelper.createSecondChromeTabbedActivity(
-                activity1, new LoadUrlParams(mTestServer.getURL(TEST_PAGE)));
-        ChromeActivityTestRule.waitForActivityNativeInitializationComplete(activity2);
-
-        CallbackHelper allItemsInsertedCallback = new CallbackHelper();
-        ThreadUtils.runOnUiThreadBlocking(() -> {
-            mCoordinator2 = activity2.getComponent().resolveContextualSuggestionsCoordinator();
-            mMediator2 = mCoordinator2.getMediatorForTesting();
-            mModel2 = mCoordinator2.getModelForTesting();
-            mBottomSheet2 = activity2.getBottomSheet();
-
-            mModel2.getClusterList().addObserver(new ListObserver<PartialBindCallback>() {
-                @Override
-                public void onItemRangeInserted(ListObservable source, int index, int count) {
-                    // There will be two calls to this method, one for each cluster that is added
-                    // to the list. Wait for the expected number of items to ensure the model
-                    // is finished updating.
-                    if (mModel2.getClusterList().getItemCount()
-                            == FakeContextualSuggestionsSource.TOTAL_ITEM_COUNT) {
-                        allItemsInsertedCallback.notifyCalled();
-                    }
-                }
-            });
-        });
-
-        assertNotEquals("There should be two coordinators.", mCoordinator, mCoordinator2);
-        assertNotEquals("There should be two mediators.", mMediator, mMediator2);
-        assertNotEquals("There should be two models.", mModel, mModel2);
-        assertEquals("There should have been two requests to create a ContextualSuggestionsSource",
-                2, mNumberOfSourcesCreated);
-
-        allItemsInsertedCallback.waitForCallback(0);
-
-        int itemCount = ThreadUtils.runOnUiThreadBlocking(
-                () -> { return mModel.getClusterList().getItemCount(); });
-        assertEquals("Second model has incorrect number of items.",
-                (int) FakeContextualSuggestionsSource.TOTAL_ITEM_COUNT, itemCount);
-
-        clickToolbarButton(activity2);
-        BottomSheetTestRule.waitForWindowUpdates();
-        ThreadUtils.runOnUiThreadBlocking(() -> {
-            ContextualSuggestionsBottomSheetContent content1 =
-                    (ContextualSuggestionsBottomSheetContent) mBottomSheet.getCurrentSheetContent();
-            ContextualSuggestionsBottomSheetContent content2 =
-                    (ContextualSuggestionsBottomSheetContent)
-                            mBottomSheet2.getCurrentSheetContent();
-            assertNotEquals("There should be two bottom sheet contents", content1, content2);
-        });
-
-        assertEquals("Sheet in the second activity should be peeked.", BottomSheet.SheetState.HALF,
-                mBottomSheet2.getSheetState());
-        assertEquals("Sheet in the first activity should be open.", BottomSheet.SheetState.HALF,
-                mBottomSheet.getSheetState());
-
-        ThreadUtils.runOnUiThreadBlocking(
-                () -> mBottomSheet2.setSheetState(BottomSheet.SheetState.FULL, false));
-
-        SnippetArticleViewHolder holder = getFirstSuggestionViewHolder(mBottomSheet2);
-        String expectedUrl = holder.getUrl();
-        ChromeTabUtils.invokeContextMenuAndOpenInOtherWindow(activity2, activity1, holder.itemView,
-                ContextMenuManager.ContextMenuItemId.OPEN_IN_NEW_WINDOW, false, expectedUrl);
-
-        ThreadUtils.runOnUiThreadBlocking(() -> {
-            mBottomSheet.endAnimations();
-            mBottomSheet2.endAnimations();
-        });
-
-        assertTrue("Sheet in second activity should be opened.", mBottomSheet2.isSheetOpen());
-        assertFalse("Sheet in first activity should be closed.", mBottomSheet.isSheetOpen());
-    }
-
-    @Test
-    @MediumTest
-    @Feature({"ContextualSuggestions", "UiCatalogue"})
-    @EnableFeatures(ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_BUTTON)
-    public void testCaptureContextualSuggestionsBottomSheet() throws Exception {
-        dismissHelpBubble();
-
-        mScreenShooter.shoot("Contextual suggestions: toolbar button");
-
-        clickToolbarButton();
-        BottomSheetTestRule.waitForWindowUpdates();
-
-        ThreadUtils.runOnUiThreadBlocking(
-                () -> mBottomSheet.setSheetState(BottomSheet.SheetState.HALF, false));
-        BottomSheetTestRule.waitForWindowUpdates();
-        mScreenShooter.shoot("Contextual suggestions: half height, images loading");
-
-        ThreadUtils.runOnUiThreadBlocking(() -> mFakeSource.runImageFetchCallbacks());
-        BottomSheetTestRule.waitForWindowUpdates();
-        mScreenShooter.shoot("Contextual suggestions: half height, images loaded");
-
-        ThreadUtils.runOnUiThreadBlocking(
-                () -> mBottomSheet.setSheetState(BottomSheet.SheetState.FULL, false));
-        BottomSheetTestRule.waitForWindowUpdates();
-        mScreenShooter.shoot("Contextual suggestions: full height");
-
-        ThreadUtils.runOnUiThreadBlocking(() -> {
-            RecyclerView view =
-                    (RecyclerView) mBottomSheet.getCurrentSheetContent().getContentView();
-            view.scrollToPosition(5);
-        });
-        BottomSheetTestRule.waitForWindowUpdates();
-        mScreenShooter.shoot("Contextual suggestions: scrolled");
-    }
-
-    @Test
-    @MediumTest
-    @Feature({"ContextualSuggestions", "RenderTest"})
-    @EnableFeatures(ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_BUTTON)
-    public void testRender() throws Exception {
-        dismissHelpBubble();
-
-        // Open the sheet to cause the suggestions to be bound in the RecyclerView, then capture
-        // a suggestion with its thumbnail loading.
-        clickToolbarButton();
-        ThreadUtils.runOnUiThreadBlocking(
-                () -> mBottomSheet.setSheetState(BottomSheet.SheetState.FULL, false));
-
-        BottomSheetTestRule.waitForWindowUpdates();
-        mRenderTestRule.render(getFirstSuggestionViewHolder().itemView, "suggestion_image_loading");
-
-        // Run the image fetch callback so images load, then capture a suggestion with its
-        // thumbnail loaded.
-        ThreadUtils.runOnUiThreadBlocking(() -> mFakeSource.runImageFetchCallbacks());
-        BottomSheetTestRule.waitForWindowUpdates();
-        mRenderTestRule.render(getFirstSuggestionViewHolder().itemView, "suggestion_image_loaded");
-
-        // Render a thumbnail with an offline badge.
-        ThreadUtils.runOnUiThreadBlocking(
-                () -> getSuggestionViewHolder(2).setOfflineBadgeVisibilityForTesting(true));
-        mRenderTestRule.render(getSuggestionViewHolder(2).itemView, "suggestion_offline");
-
-        // Render the full suggestions sheet.
-        mRenderTestRule.render(mBottomSheet, "full_height");
-
-        // Scroll the suggestions and render the full suggestions sheet.
-        ThreadUtils.runOnUiThreadBlocking(() -> {
-            RecyclerView view =
-                    (RecyclerView) mBottomSheet.getCurrentSheetContent().getContentView();
-            view.scrollToPosition(5);
-        });
-        BottomSheetTestRule.waitForWindowUpdates();
-        mRenderTestRule.render(mBottomSheet, "full_height_scrolled");
-    }
-
-    // Re-enable if peek delay condition is hooked up to toolbar button.
-    @Test
-    @MediumTest
-    @Feature({"ContextualSuggestions"})
-    @DisabledTest
-    @EnableFeatures(ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_BUTTON)
-    public void testPeekDelay() throws Exception {
-        // Close the suggestions from setUp().
-        ThreadUtils.runOnUiThreadBlocking(() -> {
-            mMediator.clearState();
-            mBottomSheet.endAnimations();
-        });
-
-        // Request suggestions with fetch time baseline set for testing.
-        long startTime = SystemClock.uptimeMillis();
-        FetchHelper.setFetchTimeBaselineMillisForTesting(startTime);
-        ThreadUtils.runOnUiThreadBlocking(
-                () -> mMediator.requestSuggestions("http://www.testurl.com"));
-        assertEquals("Bottom sheet should be hidden before delay.", BottomSheet.SheetState.HIDDEN,
-                mBottomSheet.getSheetState());
-
-        // Simulate user scroll by calling showContentInSheet until the sheet is peeked.
-        CriteriaHelper.pollUiThread(() -> {
-            mMediator.showContentInSheetForTesting(true);
-            mBottomSheet.endAnimations();
-            return mBottomSheet.getSheetState() == BottomSheet.SheetState.PEEK;
-        });
-
-        // Verify that suggestions is shown after the expected delay.
-        long duration = SystemClock.uptimeMillis() - startTime;
-        long expected = FakeContextualSuggestionsSource.TEST_PEEK_DELAY_SECONDS * 1000;
-        assertTrue(String.format(Locale.US,
-                        "The peek delay should be greater than %d ms, but was %d ms.",
-                        expected, duration),
-                duration >= expected);
-    }
-
-    @Test
-    @MediumTest
-    @Feature({"ContextualSuggestions"})
-    @EnableFeatures(ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_BUTTON)
-    public void testToolbarButton_SwitchTabs() throws Exception {
-        View toolbarButton = getToolbarButton();
-
-        assertEquals(
-                "Toolbar button should be visible", View.VISIBLE, toolbarButton.getVisibility());
-
-        final TabModel currentModel =
-                mActivityTestRule.getActivity().getTabModelSelector().getCurrentModel();
-        int currentIndex = currentModel.index();
-        ChromeTabUtils.newTabFromMenu(
-                InstrumentationRegistry.getInstrumentation(), mActivityTestRule.getActivity());
-
-        assertEquals("Toolbar button should be gone", View.GONE, toolbarButton.getVisibility());
-
-        ThreadUtils.runOnUiThreadBlocking(
-                () -> currentModel.setIndex(currentIndex, TabSelectionType.FROM_USER));
-
-        CriteriaHelper.pollUiThread(
-                () -> { return toolbarButton.getVisibility() == View.VISIBLE; });
-    }
-
-    @Test
-    @MediumTest
-    @Feature({"ContextualSuggestions"})
-    @EnableFeatures(ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_BUTTON)
-    public void testToolbarButton_ResponseInTabSwitcher() throws Exception {
-        View toolbarButton = getToolbarButton();
-
-        assertEquals("Toolbar button should be visible before resetting suggestions", View.VISIBLE,
-                toolbarButton.getVisibility());
-
-        // Simulate suggestions being cleared.
-        ThreadUtils.runOnUiThreadBlocking(() -> {
-            mMediator.clearState();
-            getToolbarPhone().endExperimentalButtonAnimationForTesting();
-        });
-        assertEquals("Toolbar button should be gone", View.GONE, toolbarButton.getVisibility());
-        assertEquals("Suggestions should be cleared", 0, mModel.getClusterList().getItemCount());
-
-        // Enter tab switcher.
-        ThreadUtils.runOnUiThreadBlocking(
-                () -> { mActivityTestRule.getActivity().getLayoutManager().showOverview(false); });
-
-        // Simulate a new suggestions request.
-        ThreadUtils.runOnUiThreadBlocking(
-                () -> mMediator.requestSuggestions("https://www.google.com"));
-        CriteriaHelper.pollUiThread(new Criteria() {
-            @Override
-            public boolean isSatisfied() {
-                return mModel.getClusterList().getItemCount()
-                        == FakeContextualSuggestionsSource.TOTAL_ITEM_COUNT;
-            }
-        });
-
-        assertEquals("Toolbar button should be visible after response received", View.VISIBLE,
-                toolbarButton.getVisibility());
-
-        ThreadUtils.runOnUiThreadBlocking(
-                () -> { mActivityTestRule.getActivity().getLayoutManager().hideOverview(false); });
-
-        assertEquals("Toolbar button should still be visible after exiting tab switcher",
-                View.VISIBLE, toolbarButton.getVisibility());
-    }
-
-    @Test
-    @MediumTest
-    @Feature({"ContextualSuggestions"})
-    @EnableFeatures(ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_BUTTON)
-    public void testSuggestionRanking() throws Exception {
-        ClusterList clusters = mModel.getClusterList();
-
-        ContextualSuggestionsCluster cluster1 = clusters.getClusterForTesting(0);
-        for (int i = 0; i < cluster1.getSuggestions().size(); i++) {
-            SnippetArticle article = cluster1.getSuggestions().get(i);
-            assertEquals("Cluster rank incorrect for item " + i + " in cluster1", i,
-                    article.getPerSectionRank());
-            assertEquals("Global rank incorrect for item " + i + " in cluster1", i,
-                    article.getGlobalRank());
-        }
-
-        ContextualSuggestionsCluster cluster2 = clusters.getClusterForTesting(1);
-        for (int i = 0; i < cluster2.getSuggestions().size(); i++) {
-            SnippetArticle article = cluster2.getSuggestions().get(i);
-            assertEquals("Cluster rank incorrect for item " + i + " in cluster2", i,
-                    article.getPerSectionRank());
-            assertEquals("Global rank incorrect for item " + i + " in cluster2",
-                    i + cluster1.getSuggestions().size(), article.getGlobalRank());
-        }
-    }
-
-    private void simulateClickOnCloseButton() {
-        ThreadUtils.runOnUiThreadBlocking(() -> {
-            mBottomSheet.getCurrentSheetContent()
-                    .getToolbarView()
-                    .findViewById(R.id.close_button)
-                    .performClick();
-            mBottomSheet.endAnimations();
-        });
-
-        assertEquals("Sheet should be hidden.", BottomSheet.SheetState.HIDDEN,
-                mBottomSheet.getSheetState());
-        assertNull("Bottom sheet contents should be null.", mBottomSheet.getCurrentSheetContent());
-    }
-
-    private SnippetArticleViewHolder getFirstSuggestionViewHolder() {
-        return getFirstSuggestionViewHolder(mBottomSheet);
-    }
-
-    private SnippetArticleViewHolder getFirstSuggestionViewHolder(BottomSheet bottomSheet) {
-        return getSuggestionViewHolder(bottomSheet, 0);
-    }
-
-    private SnippetArticleViewHolder getSuggestionViewHolder(int index) {
-        return getSuggestionViewHolder(mBottomSheet, index);
-    }
-
-    private SnippetArticleViewHolder getSuggestionViewHolder(BottomSheet bottomSheet, int index) {
-        ContextualSuggestionsBottomSheetContent content =
-                (ContextualSuggestionsBottomSheetContent) bottomSheet.getCurrentSheetContent();
-        RecyclerView recyclerView = (RecyclerView) content.getContentView();
-
-        RecyclerViewTestUtils.waitForStableRecyclerView(recyclerView);
-
-        return (SnippetArticleViewHolder) recyclerView.findViewHolderForAdapterPosition(index);
-    }
-
-    private View getToolbarButton() throws ExecutionException {
-        return getToolbarButton(mActivityTestRule.getActivity());
-    }
-
-    private View getToolbarButton(ChromeActivity activity) throws ExecutionException {
-        return ThreadUtils.runOnUiThreadBlocking(
-                () -> { return getToolbarPhone(activity).getExperimentalButtonForTesting(); });
-    }
-
-    private void clickToolbarButton() throws ExecutionException {
-        clickToolbarButton(mActivityTestRule.getActivity());
-    }
-
-    private void clickToolbarButton(ChromeActivity activity) throws ExecutionException {
-        View toolbarButton = getToolbarButton(activity);
-        assertEquals(
-                "Toolbar button should be visible", View.VISIBLE, toolbarButton.getVisibility());
-
-        ThreadUtils.runOnUiThreadBlocking(() -> {
-            toolbarButton.performClick();
-            mBottomSheet.endAnimations();
-        });
-        assertTrue("Sheet should be open.", mBottomSheet.isSheetOpen());
-    }
-
-    private void testOpenFirstSuggestion() throws InterruptedException, TimeoutException {
-        SnippetArticleViewHolder holder = getFirstSuggestionViewHolder();
-        String expectedUrl = holder.getUrl();
-
-        TestWebContentsObserver webContentsObserver = new TestWebContentsObserver(
-                mActivityTestRule.getActivity().getActivityTab().getWebContents());
-
-        int callCount = webContentsObserver.getOnPageStartedHelper().getCallCount();
-
-        ThreadUtils.runOnUiThreadBlocking(() -> { holder.itemView.performClick(); });
-
-        webContentsObserver.getOnPageStartedHelper().waitForCallback(callCount);
-
-        ThreadUtils.runOnUiThreadBlocking(() -> mBottomSheet.endAnimations());
-
-        assertFalse("Sheet should be closed.", mBottomSheet.isSheetOpen());
-
-        // URL may not have been updated yet when WebContentsObserver#didStartLoading is called.
-        CriteriaHelper.pollUiThread(() -> {
-            return mActivityTestRule.getActivity().getActivityTab().getUrl().equals(expectedUrl);
-        });
-
-        ThreadUtils.runOnUiThreadBlocking(
-                () -> getToolbarPhone().endExperimentalButtonAnimationForTesting());
-    }
-
-    private void dismissHelpBubble() {
-        ThreadUtils.runOnUiThreadBlocking(() -> {
-            if (mMediator.getHelpBubbleForTesting() != null) {
-                mMediator.getHelpBubbleForTesting().dismiss();
-            }
-        });
-    }
-
-    private ToolbarPhone getToolbarPhone() {
-        return getToolbarPhone(mActivityTestRule.getActivity());
-    }
-
-    private ToolbarPhone getToolbarPhone(ChromeActivity activity) {
-        return (ToolbarPhone) activity.getToolbarManager().getToolbarLayoutForTesting();
-    }
-}
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/vr/WebXrArSessionTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/vr/WebXrArSessionTest.java
index 017d51f..1c3577e 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/vr/WebXrArSessionTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/vr/WebXrArSessionTest.java
@@ -5,12 +5,10 @@
 package org.chromium.chrome.browser.vr;
 
 import static org.chromium.chrome.browser.vr.WebXrArTestFramework.PAGE_LOAD_TIMEOUT_S;
-import static org.chromium.chrome.browser.vr.WebXrArTestFramework.POLL_TIMEOUT_LONG_MS;
 
 import android.os.Build;
 import android.support.test.filters.MediumTest;
 
-import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -28,6 +26,7 @@
 import org.chromium.chrome.browser.vr.util.XrTestRuleUtils;
 import org.chromium.chrome.test.ChromeActivityTestRule;
 import org.chromium.chrome.test.ChromeJUnit4RunnerDelegate;
+import org.chromium.content_public.browser.WebContents;
 
 import java.util.List;
 import java.util.concurrent.Callable;
@@ -76,6 +75,33 @@
     }
 
     /**
+     * Tests that AR session consent can be declined or granted per session.
+     */
+    @Test
+    @MediumTest
+    @XrActivityRestriction({XrActivityRestriction.SupportedActivity.ALL})
+    public void testArVaryingPerSessionConsent() throws InterruptedException {
+        mWebXrArTestFramework.loadUrlAndAwaitInitialization(
+                mWebXrArTestFramework.getEmbeddedServerUrlForHtmlTestFile(
+                        "test_ar_request_session_succeeds"),
+                PAGE_LOAD_TIMEOUT_S);
+        WebContents contents = mWebXrArTestFramework.getCurrentWebContents();
+
+        // Start session, decline consent prompt.
+        mWebXrArTestFramework.enterSessionWithUserGestureAndDeclineConsentOrFail(contents);
+        mWebXrArTestFramework.assertNoJavaScriptErrors();
+
+        // Start new session, accept consent prompt this time.
+        mWebXrArTestFramework.enterSessionWithUserGestureOrFail(contents);
+        mWebXrArTestFramework.endSession();
+        mWebXrArTestFramework.assertNoJavaScriptErrors();
+
+        // Start yet another session, decline consent prompt again.
+        mWebXrArTestFramework.enterSessionWithUserGestureAndDeclineConsentOrFail(contents);
+        mWebXrArTestFramework.assertNoJavaScriptErrors();
+    }
+
+    /**
      * Tests that repeatedly starting and stopping AR sessions does not cause any unexpected
      * behavior. Regression test for https://crbug.com/837894.
      */
@@ -93,30 +119,4 @@
         }
         mWebXrArTestFramework.assertNoJavaScriptErrors();
     }
-
-    /**
-     * Tests that repeated calls to requestSession on the same page only prompts the user for
-     * camera permissions once.
-     */
-    @Test
-    @MediumTest
-    @XrActivityRestriction({XrActivityRestriction.SupportedActivity.ALL})
-    public void testRepeatedArSessionsOnlyPromptPermissionsOnce() throws InterruptedException {
-        mWebXrArTestFramework.loadUrlAndAwaitInitialization(
-                mWebXrArTestFramework.getEmbeddedServerUrlForHtmlTestFile(
-                        "test_ar_request_session_succeeds"),
-                PAGE_LOAD_TIMEOUT_S);
-        Assert.assertTrue("First AR session request did not trigger permission prompt",
-                mWebXrArTestFramework.permissionRequestWouldTriggerPrompt("camera"));
-        mWebXrArTestFramework.enterSessionWithUserGestureOrFail();
-        mWebXrArTestFramework.endSession();
-        // Manually run through the same steps as enterArSessionOrFail so that we don't trigger
-        // its automatic permission acceptance.
-        Assert.assertFalse("Second AR session request triggered permission prompt",
-                mWebXrArTestFramework.permissionRequestWouldTriggerPrompt("camera"));
-        mWebXrArTestFramework.enterSessionWithUserGesture();
-        mWebXrArTestFramework.pollJavaScriptBooleanOrFail(
-                "sessionInfos[sessionTypes.AR].currentSession != null", POLL_TIMEOUT_LONG_MS);
-        mWebXrArTestFramework.assertNoJavaScriptErrors();
-    }
-}
\ No newline at end of file
+}
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/vr/WebXrArTestFramework.java b/chrome/android/javatests/src/org/chromium/chrome/browser/vr/WebXrArTestFramework.java
index c790d59..cb9b20e 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/vr/WebXrArTestFramework.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/vr/WebXrArTestFramework.java
@@ -21,8 +21,8 @@
     }
 
     /**
-     * Requests an AR session, automatically accepting the Camera permission prompt if necessary.
-     * Causes a test failure if it is unable to do so.
+     * Requests an AR session, automatically giving consent when prompted.
+     * Causes a test failure if it is unable to do so, or if the consent prompt is missing.
      *
      * @param webContents The Webcontents to start the AR session in.
      */
@@ -30,21 +30,42 @@
     public void enterSessionWithUserGestureOrFail(WebContents webContents) {
         runJavaScriptOrFail(
                 "sessionTypeToRequest = sessionTypes.AR", POLL_TIMEOUT_LONG_MS, webContents);
-        // Requesting an AR session for the first time on a page will always prompt for camera
-        // permissions, but not on subsequent requests, so check to see if we'll need to accept it
-        // after requesting the session.
-        boolean expectPermissionPrompt = permissionRequestWouldTriggerPrompt("camera", webContents);
-        // TODO(bsheedy): Rename enterPresentation since it's used for both presentation and AR?
+
         enterSessionWithUserGesture(webContents);
-        if (expectPermissionPrompt) {
-            PermissionUtils.waitForPermissionPrompt();
-            PermissionUtils.acceptPermissionPrompt();
-        }
+
+        // We expect the AR-specific AR session consent prompt but should not get
+        // prompted for page camera permission.
+        PermissionUtils.waitForArConsentPrompt(getRule().getActivity());
+        PermissionUtils.acceptArConsentPrompt(getRule().getActivity());
+
         pollJavaScriptBooleanOrFail("sessionInfos[sessionTypes.AR].currentSession != null",
                 POLL_TIMEOUT_LONG_MS, webContents);
     }
 
     /**
+     * Requests an AR session, then declines consent when prompted.
+     * Causes a test failure if there was no prompt, or if the session started without consent.
+     *
+     * @param webContents The Webcontents to start the AR session in.
+     */
+    public void enterSessionWithUserGestureAndDeclineConsentOrFail(WebContents webContents) {
+        runJavaScriptOrFail(
+                "sessionTypeToRequest = sessionTypes.AR", POLL_TIMEOUT_LONG_MS, webContents);
+
+        enterSessionWithUserGesture(webContents);
+
+        // We expect the AR-specific AR session consent prompt but should not get
+        // prompted for page camera permission.
+        PermissionUtils.waitForArConsentPrompt(getRule().getActivity());
+        PermissionUtils.declineArConsentPrompt(getRule().getActivity());
+
+        pollJavaScriptBooleanOrFail(
+                "sessionInfos[sessionTypes.AR].error != null", POLL_TIMEOUT_LONG_MS, webContents);
+        pollJavaScriptBooleanOrFail("sessionInfos[sessionTypes.AR].currentSession == null",
+                POLL_TIMEOUT_LONG_MS, webContents);
+    }
+
+    /**
      * Exits a WebXR AR session.
      *
      * @param webcontents The WebContents to exit the AR session in
@@ -54,4 +75,4 @@
         runJavaScriptOrFail("sessionInfos[sessionTypes.AR].currentSession.end()",
                 POLL_TIMEOUT_SHORT_MS, webContents);
     }
-}
\ No newline at end of file
+}
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/vr/WebXrVrDeviceTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/vr/WebXrVrDeviceTest.java
index 229132d..a620ef6 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/vr/WebXrVrDeviceTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/vr/WebXrVrDeviceTest.java
@@ -25,6 +25,7 @@
 import org.chromium.base.test.params.ParameterSet;
 import org.chromium.base.test.params.ParameterizedRunner;
 import org.chromium.base.test.util.CommandLineFlags;
+import org.chromium.base.test.util.DisabledTest;
 import org.chromium.base.test.util.MinAndroidSdkLevel;
 import org.chromium.base.test.util.Restriction;
 import org.chromium.chrome.browser.ChromeSwitches;
@@ -176,6 +177,7 @@
             .Remove({"enable-webvr"})
             @CommandLineFlags.Add({"enable-features=WebXR"})
             @Restriction(RESTRICTION_TYPE_VIEWER_DAYDREAM)
+            @DisabledTest(message = "crbug.com/958432")
             public void testForNullPosesInInlineVrFromNfc() throws InterruptedException {
         mWebXrVrTestFramework.loadUrlAndAwaitInitialization(
                 WebXrVrTestFramework.getFileUrlForHtmlTestFile("test_inline_vr_poses"),
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/vr/util/PermissionUtils.java b/chrome/android/javatests/src/org/chromium/chrome/browser/vr/util/PermissionUtils.java
index 8f08242c..3c1afeb 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/vr/util/PermissionUtils.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/vr/util/PermissionUtils.java
@@ -4,10 +4,14 @@
 
 package org.chromium.chrome.browser.vr.util;
 
+import org.chromium.chrome.browser.ChromeActivity;
 import org.chromium.chrome.browser.permissions.PermissionDialogController;
+import org.chromium.chrome.browser.vr.ArConsentDialog;
 import org.chromium.content_public.browser.test.util.CriteriaHelper;
 import org.chromium.content_public.browser.test.util.TestThreadUtils;
+import org.chromium.ui.modaldialog.ModalDialogManager;
 import org.chromium.ui.modaldialog.ModalDialogProperties;
+import org.chromium.ui.modelutil.PropertyModel;
 
 /**
  * Utility class for interacting with permission prompts outside of the VR Browser. For interaction
@@ -42,4 +46,51 @@
                     ModalDialogProperties.ButtonType.NEGATIVE);
         });
     }
+
+    /**
+     * Blocks until the AR session consent prompt appears.
+     */
+    public static void waitForArConsentPrompt(ChromeActivity activity) {
+        CriteriaHelper.pollUiThread(() -> {
+            return isArConsentDialogShown(activity);
+        }, "AR consent prompt did not appear in allotted time");
+    }
+
+    /**
+     * Accepts the currently displayed AR session consent prompt.
+     */
+    public static void acceptArConsentPrompt(ChromeActivity activity) {
+        TestThreadUtils.runOnUiThreadBlocking(() -> {
+            clickArConsentDialogButton(activity, ModalDialogProperties.ButtonType.POSITIVE);
+        });
+    }
+
+    /**
+     * Declines the currently displayed AR session consent prompt.
+     */
+    public static void declineArConsentPrompt(ChromeActivity activity) {
+        TestThreadUtils.runOnUiThreadBlocking(() -> {
+            clickArConsentDialogButton(activity, ModalDialogProperties.ButtonType.NEGATIVE);
+        });
+    }
+
+    /**
+     * Helper function to check if the AR consent dialog is being shown.
+     */
+    public static boolean isArConsentDialogShown(ChromeActivity activity) {
+        ModalDialogManager manager = activity.getModalDialogManager();
+        PropertyModel model = manager.getCurrentDialogForTest();
+        if (model == null) return false;
+        return model.get(ModalDialogProperties.CONTROLLER) instanceof ArConsentDialog;
+    }
+
+    /**
+     * Helper function to click a button in the AR consent dialog.
+     */
+    public static void clickArConsentDialogButton(ChromeActivity activity, int buttonType) {
+        ModalDialogManager manager = activity.getModalDialogManager();
+        PropertyModel model = manager.getCurrentDialogForTest();
+        ArConsentDialog dialog = (ArConsentDialog) (model.get(ModalDialogProperties.CONTROLLER));
+        dialog.onClick(model, buttonType);
+    }
 }
diff --git a/chrome/android/touchless/java/res/drawable-hdpi/progress_bar_shadow.9.png b/chrome/android/touchless/java/res/drawable-hdpi/progress_bar_shadow.9.png
new file mode 100644
index 0000000..427ff726
--- /dev/null
+++ b/chrome/android/touchless/java/res/drawable-hdpi/progress_bar_shadow.9.png
Binary files differ
diff --git a/chrome/android/touchless/java/res/drawable-mdpi/progress_bar_shadow.9.png b/chrome/android/touchless/java/res/drawable-mdpi/progress_bar_shadow.9.png
new file mode 100644
index 0000000..f6bf182
--- /dev/null
+++ b/chrome/android/touchless/java/res/drawable-mdpi/progress_bar_shadow.9.png
Binary files differ
diff --git a/chrome/android/touchless/java/res/drawable-xhdpi/progress_bar_shadow.9.png b/chrome/android/touchless/java/res/drawable-xhdpi/progress_bar_shadow.9.png
new file mode 100644
index 0000000..4d611a10
--- /dev/null
+++ b/chrome/android/touchless/java/res/drawable-xhdpi/progress_bar_shadow.9.png
Binary files differ
diff --git a/chrome/android/touchless/java/res/drawable-xxhdpi/progress_bar_shadow.9.png b/chrome/android/touchless/java/res/drawable-xxhdpi/progress_bar_shadow.9.png
new file mode 100644
index 0000000..e7e2884
--- /dev/null
+++ b/chrome/android/touchless/java/res/drawable-xxhdpi/progress_bar_shadow.9.png
Binary files differ
diff --git a/chrome/android/touchless/java/res/drawable-xxxhdpi/progress_bar_shadow.9.png b/chrome/android/touchless/java/res/drawable-xxxhdpi/progress_bar_shadow.9.png
new file mode 100644
index 0000000..fcb1c69
--- /dev/null
+++ b/chrome/android/touchless/java/res/drawable-xxxhdpi/progress_bar_shadow.9.png
Binary files differ
diff --git a/chrome/android/touchless/java/res/drawable/notouch_progress_bar_drawable.xml b/chrome/android/touchless/java/res/drawable/notouch_progress_bar_drawable.xml
index 200cd437..66a6797 100644
--- a/chrome/android/touchless/java/res/drawable/notouch_progress_bar_drawable.xml
+++ b/chrome/android/touchless/java/res/drawable/notouch_progress_bar_drawable.xml
@@ -1,9 +1,13 @@
 <?xml version="1.0" encoding="utf-8"?>
+<!-- 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. -->
+
 <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
     <item android:id="@android:id/background">
         <shape>
             <corners android:radius="16dp" />
-            <solid android:color="@android:color/white" />
+            <solid android:color="@android:color/transparent" />
         </shape>
     </item>
 
diff --git a/chrome/android/touchless/java/res/layout/notouch_progress_bar_view.xml b/chrome/android/touchless/java/res/layout/notouch_progress_bar_view.xml
index 38983ef..250a4442 100644
--- a/chrome/android/touchless/java/res/layout/notouch_progress_bar_view.xml
+++ b/chrome/android/touchless/java/res/layout/notouch_progress_bar_view.xml
@@ -1,14 +1,20 @@
 <?xml version="1.0" encoding="utf-8"?>
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="180dp"
-    android:layout_height="32dp"
-    android:layout_marginTop="11dp">
+<!-- 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. -->
+
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_gravity="center_horizontal"
+    android:background="@drawable/progress_bar_shadow">
 
     <ProgressBar
         android:id="@+id/notouch_progress_bar_view"
         style="@android:style/Widget.ProgressBar.Horizontal"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
+        android:layout_width="192dp"
+        android:layout_height="36dp"
         android:progressDrawable="@drawable/notouch_progress_bar_drawable" />
 
     <TextView
@@ -16,10 +22,9 @@
         style="@style/TextAppearance.NoTouchProgressBar"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:ellipsize="start"
+        android:layout_gravity="center"
         android:gravity="center"
-        android:paddingEnd="16dp"
-        android:paddingStart="16dp"
+        android:ellipsize="start"
         android:singleLine="true" />
 
-</RelativeLayout>
\ No newline at end of file
+</FrameLayout>
\ No newline at end of file
diff --git a/chrome/android/touchless/java/src/org/chromium/chrome/browser/touchless/NoTouchActivity.java b/chrome/android/touchless/java/src/org/chromium/chrome/browser/touchless/NoTouchActivity.java
index f303a2d..1bf5522 100644
--- a/chrome/android/touchless/java/src/org/chromium/chrome/browser/touchless/NoTouchActivity.java
+++ b/chrome/android/touchless/java/src/org/chromium/chrome/browser/touchless/NoTouchActivity.java
@@ -27,10 +27,12 @@
 import org.chromium.chrome.browser.compositor.layouts.LayoutManager;
 import org.chromium.chrome.browser.fullscreen.ChromeFullscreenManager;
 import org.chromium.chrome.browser.preferences.ChromePreferenceManager;
+import org.chromium.chrome.browser.snackbar.SnackbarManager;
 import org.chromium.chrome.browser.tab.Tab;
 import org.chromium.chrome.browser.tab.TabRedirectHandler;
 import org.chromium.chrome.browser.tab.TabState;
 import org.chromium.chrome.browser.touchless.dialog.TouchlessDialogPresenter;
+import org.chromium.chrome.browser.touchless.snackbar.BlackHoleSnackbarManager;
 import org.chromium.chrome.browser.touchless.ui.iph.KeyFunctionsIPHCoordinator;
 import org.chromium.chrome.browser.touchless.ui.progressbar.ProgressBarCoordinator;
 import org.chromium.chrome.browser.touchless.ui.progressbar.ProgressBarView;
@@ -71,6 +73,9 @@
     /** Tab observer that tracks media state. */
     private TouchlessTabObserver mTabObserver;
 
+    /** The snackbar manager for this activity that drops all snackbar requests. */
+    private BlackHoleSnackbarManager mSnackbarManager;
+
     /**
      * Internal class which performs the intent handling operations delegated by IntentHandler.
      */
@@ -156,6 +161,7 @@
         if (launchNtpDueToInactivity) resetSavedInstanceState();
         super.initializeState();
 
+        mSnackbarManager = new BlackHoleSnackbarManager(this);
         mKeyFunctionsIPHCoordinator =
                 new KeyFunctionsIPHCoordinator(mTooltipView, getActivityTabProvider());
         mProgressBarCoordinator =
@@ -315,4 +321,9 @@
     public TouchlessUiController getTouchlessUiController() {
         return mUiController;
     }
+
+    @Override
+    public SnackbarManager getSnackbarManager() {
+        return mSnackbarManager;
+    }
 }
diff --git a/chrome/android/touchless/java/src/org/chromium/chrome/browser/touchless/dialog/TouchlessDialogPresenter.java b/chrome/android/touchless/java/src/org/chromium/chrome/browser/touchless/dialog/TouchlessDialogPresenter.java
index 99b35444..c4eada2 100644
--- a/chrome/android/touchless/java/src/org/chromium/chrome/browser/touchless/dialog/TouchlessDialogPresenter.java
+++ b/chrome/android/touchless/java/src/org/chromium/chrome/browser/touchless/dialog/TouchlessDialogPresenter.java
@@ -4,7 +4,6 @@
 
 package org.chromium.chrome.browser.touchless.dialog;
 
-import android.app.Activity;
 import android.app.Dialog;
 import android.graphics.PorterDuff;
 import android.graphics.PorterDuffColorFilter;
@@ -39,7 +38,7 @@
 /** A modal dialog presenter that is specific to touchless dialogs. */
 public class TouchlessDialogPresenter extends Presenter {
     /** An activity to attach dialogs to. */
-    private final Activity mActivity;
+    private final ChromeActivity mActivity;
 
     /** The dialog this class abstracts. */
     private Dialog mDialog;
@@ -48,7 +47,7 @@
     private PropertyModelChangeProcessor<PropertyModel, Pair<ViewGroup, ModelListAdapter>,
             PropertyKey> mModelChangeProcessor;
 
-    public TouchlessDialogPresenter(Activity activity) {
+    public TouchlessDialogPresenter(ChromeActivity activity) {
         mActivity = activity;
     }
 
@@ -76,17 +75,19 @@
         mDialog.setOnCancelListener(dialogInterface
                 -> dismissCurrentDialog(DialogDismissalCause.NAVIGATE_BACK_OR_TOUCH_OUTSIDE));
         mDialog.setOnShowListener(dialog
-                -> AppHooks.get().getTouchlessUiControllerForActivity((ChromeActivity) mActivity)
-                .addModelToQueue(model));
+                -> AppHooks.get().getTouchlessUiControllerForActivity(mActivity).addModelToQueue(
+                        model));
         mDialog.setOnDismissListener(dialog
-                -> AppHooks.get().getTouchlessUiControllerForActivity((ChromeActivity) mActivity)
-                .removeModelFromQueue(model));
+                -> AppHooks.get()
+                           .getTouchlessUiControllerForActivity(mActivity)
+                           .removeModelFromQueue(model));
         // Cancel on touch outside should be disabled by default. The ModelChangeProcessor wouldn't
         // notify change if the property is not set during initialization.
         mDialog.setCanceledOnTouchOutside(false);
-        mDialog.setOnKeyListener((dialog, keyCode, event) ->
-                AppHooks.get().getTouchlessUiControllerForActivity((ChromeActivity) mActivity)
-                .onKeyEvent(event));
+        mDialog.setOnKeyListener(
+                (dialog, keyCode, event)
+                        -> AppHooks.get().getTouchlessUiControllerForActivity(mActivity).onKeyEvent(
+                                event));
         ViewGroup dialogView = (ViewGroup) LayoutInflater.from(mDialog.getContext())
                 .inflate(R.layout.touchless_dialog_view, null);
         ModelListAdapter adapter = new ModelListAdapter(mActivity);
diff --git a/chrome/android/touchless/java/src/org/chromium/chrome/browser/touchless/snackbar/BlackHoleSnackbarManager.java b/chrome/android/touchless/java/src/org/chromium/chrome/browser/touchless/snackbar/BlackHoleSnackbarManager.java
new file mode 100644
index 0000000..fb4c55d
--- /dev/null
+++ b/chrome/android/touchless/java/src/org/chromium/chrome/browser/touchless/snackbar/BlackHoleSnackbarManager.java
@@ -0,0 +1,29 @@
+// 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.touchless.snackbar;
+
+import android.app.Activity;
+import android.view.ViewGroup;
+
+import org.chromium.chrome.browser.snackbar.Snackbar;
+import org.chromium.chrome.browser.snackbar.SnackbarManager;
+
+/** A snackbar manager that consumes all incoming requests. */
+public class BlackHoleSnackbarManager extends SnackbarManager {
+    /**
+     * @param activity The embedding activity.
+     */
+    public BlackHoleSnackbarManager(Activity activity) {
+        super(activity, new ViewGroup(activity) {
+            @Override
+            protected void onLayout(boolean changed, int left, int top, int right, int bottom) {}
+        });
+    }
+
+    @Override
+    public void showSnackbar(Snackbar snackbar) {
+        // Intentional noop.
+    }
+}
diff --git a/chrome/android/touchless/touchless_java_sources.gni b/chrome/android/touchless/touchless_java_sources.gni
index 9074b299..69640629 100644
--- a/chrome/android/touchless/touchless_java_sources.gni
+++ b/chrome/android/touchless/touchless_java_sources.gni
@@ -41,6 +41,7 @@
   "touchless/java/src/org/chromium/chrome/browser/touchless/TouchlessZoomHelper.java",
   "touchless/java/src/org/chromium/chrome/browser/touchless/dialog/TouchlessDialogPresenter.java",
   "touchless/java/src/org/chromium/chrome/browser/touchless/dialog/TouchlessDialogProperties.java",
+  "touchless/java/src/org/chromium/chrome/browser/touchless/snackbar/BlackHoleSnackbarManager.java",
   "touchless/java/src/org/chromium/chrome/browser/touchless/ui/iph/KeyFunctionsIPHCoordinator.java",
   "touchless/java/src/org/chromium/chrome/browser/touchless/ui/iph/KeyFunctionsIPHMediator.java",
   "touchless/java/src/org/chromium/chrome/browser/touchless/ui/iph/KeyFunctionsIPHProperties.java",
diff --git a/chrome/app/settings_strings.grdp b/chrome/app/settings_strings.grdp
index 7d02b8b..89da853 100644
--- a/chrome/app/settings_strings.grdp
+++ b/chrome/app/settings_strings.grdp
@@ -549,7 +549,7 @@
       Available USB devices will appear here.
     </message>
     <message name="IDS_SETTINGS_CROSTINI_SHARED_USB_DEVICES_DESCRIPTION" desc="Description for managing shared USB devices.">
-      Give Linux permission to access USB devices. Linux won't remember a USB device after it's removed.
+      Give Linux apps permission to access USB devices. Linux won't remember a USB device after it's removed.
     </message>
     <message name="IDS_SETTINGS_CROSTINI_SHARED_USB_DEVICES_EXTRA_DESCRIPTION" desc="Extra description for managing shared USB devices.">
        Only Android devices are currently supported.
diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
index 9663be8..e727a763 100644
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -2603,6 +2603,13 @@
                                     kOmniboxUIVerticalMarginVariations,
                                     "OmniboxUIVerticalMarginVariations")},
 
+    {"omnibox-ui-vertical-margin-limit-to-non-touch-only",
+     flag_descriptions::kOmniboxUIVerticalMarginLimitToNonTouchOnlyName,
+     flag_descriptions::kOmniboxUIVerticalMarginLimitToNonTouchOnlyDescription,
+     kOsDesktop,
+     FEATURE_VALUE_TYPE(
+         omnibox::kUIExperimentVerticalMarginLimitToNonTouchOnly)},
+
     {"omnibox-ui-show-suggestion-favicons",
      flag_descriptions::kOmniboxUIShowSuggestionFaviconsName,
      flag_descriptions::kOmniboxUIShowSuggestionFaviconsDescription,
@@ -3067,6 +3074,11 @@
      flag_descriptions::kTabGroupsAndroidDescription, kOsAndroid,
      FEATURE_VALUE_TYPE(chrome::android::kTabGroupsAndroid)},
 
+    {"enable-tab-groups-ui-improvements",
+     flag_descriptions::kTabGroupsUiImprovementsAndroidName,
+     flag_descriptions::kTabGroupsUiImprovementsAndroidDescription, kOsAndroid,
+     FEATURE_VALUE_TYPE(chrome::android::kTabGroupsUiImprovementsAndroid)},
+
     {"enable-tab-switcher-on-return",
      flag_descriptions::kTabSwitcherOnReturnName,
      flag_descriptions::kTabSwitcherOnReturnDescription, kOsAndroid,
diff --git a/chrome/browser/android/chrome_feature_list.cc b/chrome/browser/android/chrome_feature_list.cc
index c4269ed..5c84557 100644
--- a/chrome/browser/android/chrome_feature_list.cc
+++ b/chrome/browser/android/chrome_feature_list.cc
@@ -170,6 +170,7 @@
     &kSpecialUserDecision,
     &kTabEngagementReportingAndroid,
     &kTabGroupsAndroid,
+    &kTabGroupsUiImprovementsAndroid,
     &kTabGridLayoutAndroid,
     &kTabPersistentStoreTaskRunner,
     &kTabReparenting,
@@ -499,6 +500,9 @@
 const base::Feature kTabGroupsAndroid{"TabGroupsAndroid",
                                       base::FEATURE_DISABLED_BY_DEFAULT};
 
+const base::Feature kTabGroupsUiImprovementsAndroid{
+    "TabGroupsUiImprovementsAndroid", base::FEATURE_DISABLED_BY_DEFAULT};
+
 const base::Feature kTabGridLayoutAndroid{"TabGridLayoutAndroid",
                                           base::FEATURE_DISABLED_BY_DEFAULT};
 
diff --git a/chrome/browser/android/chrome_feature_list.h b/chrome/browser/android/chrome_feature_list.h
index 4296287..0d6c4d1 100644
--- a/chrome/browser/android/chrome_feature_list.h
+++ b/chrome/browser/android/chrome_feature_list.h
@@ -101,6 +101,7 @@
 extern const base::Feature kSpecialUserDecision;
 extern const base::Feature kTabEngagementReportingAndroid;
 extern const base::Feature kTabGroupsAndroid;
+extern const base::Feature kTabGroupsUiImprovementsAndroid;
 extern const base::Feature kTabGridLayoutAndroid;
 extern const base::Feature kTabPersistentStoreTaskRunner;
 extern const base::Feature kTabReparenting;
diff --git a/chrome/browser/android/service_tab_launcher.cc b/chrome/browser/android/service_tab_launcher.cc
index 12b9e5ed..4c39017 100644
--- a/chrome/browser/android/service_tab_launcher.cc
+++ b/chrome/browser/android/service_tab_launcher.cc
@@ -40,7 +40,7 @@
 
 void ServiceTabLauncher::LaunchTab(content::BrowserContext* browser_context,
                                    const content::OpenURLParams& params,
-                                   const TabLaunchedCallback& callback) {
+                                   TabLaunchedCallback callback) {
   WindowOpenDisposition disposition = params.disposition;
   if (disposition != WindowOpenDisposition::NEW_WINDOW &&
       disposition != WindowOpenDisposition::NEW_POPUP &&
@@ -61,8 +61,9 @@
 
   ScopedJavaLocalRef<jobject> post_data;
 
+  // IDMap requires a pointer, so we move |callback| into a heap pointer.
   int request_id = tab_launched_callbacks_.Add(
-      std::make_unique<TabLaunchedCallback>(callback));
+      std::make_unique<TabLaunchedCallback>(std::move(callback)));
   DCHECK_GE(request_id, 1);
 
   Java_ServiceTabLauncher_launchTab(
@@ -75,9 +76,6 @@
                                        content::WebContents* web_contents) {
   TabLaunchedCallback* callback = tab_launched_callbacks_.Lookup(request_id);
   DCHECK(callback);
-
-  if (callback)
-    callback->Run(web_contents);
-
+  std::move(*callback).Run(web_contents);
   tab_launched_callbacks_.Remove(request_id);
 }
diff --git a/chrome/browser/android/service_tab_launcher.h b/chrome/browser/android/service_tab_launcher.h
index 888f417..e42cb29 100644
--- a/chrome/browser/android/service_tab_launcher.h
+++ b/chrome/browser/android/service_tab_launcher.h
@@ -22,7 +22,7 @@
 // tab has been launched, the user of this class will be informed with the
 // content::WebContents instance associated with the tab.
 class ServiceTabLauncher {
-  using TabLaunchedCallback = base::Callback<void(content::WebContents*)>;
+  using TabLaunchedCallback = base::OnceCallback<void(content::WebContents*)>;
 
  public:
   // Returns the singleton instance of the service tab launcher.
@@ -33,7 +33,7 @@
   // the tab is avialable. This method must only be called from the UI thread.
   void LaunchTab(content::BrowserContext* browser_context,
                  const content::OpenURLParams& params,
-                 const TabLaunchedCallback& callback);
+                 TabLaunchedCallback callback);
 
   // To be called when the tab for |request_id| has launched, with the
   // associated |web_contents|. The WebContents must not yet have started
diff --git a/chrome/browser/android/vr/BUILD.gn b/chrome/browser/android/vr/BUILD.gn
index dcb6b17..8b87c02 100644
--- a/chrome/browser/android/vr/BUILD.gn
+++ b/chrome/browser/android/vr/BUILD.gn
@@ -82,8 +82,6 @@
       "arcore_device/arcore_install_utils.h",
       "arcore_device/arcore_java_utils.cc",
       "arcore_device/arcore_java_utils.h",
-      "arcore_device/arcore_permission_helper.cc",
-      "arcore_device/arcore_permission_helper.h",
       "arcore_device/arcore_shim.cc",
       "arcore_device/arcore_shim.h",
     ]
diff --git a/chrome/browser/android/vr/arcore_device/ar_image_transport.cc b/chrome/browser/android/vr/arcore_device/ar_image_transport.cc
index 53f6538f..f7708ca 100644
--- a/chrome/browser/android/vr/arcore_device/ar_image_transport.cc
+++ b/chrome/browser/android/vr/arcore_device/ar_image_transport.cc
@@ -9,6 +9,7 @@
 #include "base/containers/queue.h"
 #include "base/trace_event/traced_value.h"
 #include "chrome/browser/android/vr/mailbox_to_surface_bridge.h"
+#include "chrome/browser/android/vr/web_xr_presentation_state.h"
 #include "gpu/command_buffer/common/shared_image_usage.h"
 #include "gpu/ipc/common/gpu_memory_buffer_impl_android_hardware_buffer.h"
 #include "ui/gfx/gpu_fence.h"
@@ -21,69 +22,36 @@
 
 namespace device {
 
-namespace {
-
-// Number of shared buffers to use in rotation. Two would be sufficient if
-// strictly sequenced, but use an extra one since we currently don't know
-// exactly when the Renderer is done with it.
-constexpr int kSharedBufferSwapChainSize = 3;
-
-}  // namespace
-
-// TODO(klausw): share this with WebXrPresentationState.
-struct SharedFrameBuffer {
-  SharedFrameBuffer() = default;
-  ~SharedFrameBuffer() = default;
-
-  gfx::Size size;
-
-  std::unique_ptr<gpu::GpuMemoryBufferImplAndroidHardwareBuffer>
-      shared_gpu_memory_buffer;
-
-  // Resources in the remote GPU process command buffer context
-  gpu::MailboxHolder mailbox_holder;
-
-  // Resources in the local GL context
-  GLuint local_texture_id = 0;
-  // This refptr keeps the image alive while processing a frame. That's
-  // required because it owns underlying resources and must still be
-  // alive when the mailbox texture backed by this image is used.
-  scoped_refptr<gl::GLImageEGL> local_glimage;
-};
-
-struct SharedFrameBufferSwapChain {
-  SharedFrameBufferSwapChain() = default;
-  ~SharedFrameBufferSwapChain() = default;
-
-  base::queue<std::unique_ptr<SharedFrameBuffer>> buffers;
-  int next_memory_buffer_id = 0;
-};
-
 ArImageTransport::ArImageTransport(
     std::unique_ptr<vr::MailboxToSurfaceBridge> mailbox_bridge)
     : gl_thread_task_runner_(base::ThreadTaskRunnerHandle::Get()),
-      mailbox_bridge_(std::move(mailbox_bridge)),
-      swap_chain_(std::make_unique<SharedFrameBufferSwapChain>()) {}
+      mailbox_bridge_(std::move(mailbox_bridge)) {}
 
 ArImageTransport::~ArImageTransport() {
   DCHECK(IsOnGlThread());
-  while (!swap_chain_->buffers.empty()) {
-    std::unique_ptr<SharedFrameBuffer> buffer =
-        std::move(swap_chain_->buffers.front());
-    swap_chain_->buffers.pop();
-    if (!buffer->mailbox_holder.mailbox.IsZero()) {
-      DVLOG(2) << ": DestroySharedImage, mailbox="
-               << buffer->mailbox_holder.mailbox.ToDebugString();
-      // Note: the sync token in mailbox_holder may not be accurate. See comment
-      // in TransferFrame below.
-      mailbox_bridge_->DestroySharedImage(buffer->mailbox_holder);
+
+  if (webxr_) {
+    std::vector<std::unique_ptr<vr::WebXrSharedBuffer>> buffers =
+        webxr_->TakeSharedBuffers();
+    for (auto& buffer : buffers) {
+      if (!buffer->mailbox_holder.mailbox.IsZero()) {
+        DCHECK(mailbox_bridge_);
+        DVLOG(2) << ": DestroySharedImage, mailbox="
+                 << buffer->mailbox_holder.mailbox.ToDebugString();
+        // Note: the sync token in mailbox_holder may not be accurate. See
+        // comment in TransferFrame below.
+        mailbox_bridge_->DestroySharedImage(buffer->mailbox_holder);
+      }
     }
   }
 }
 
-bool ArImageTransport::Initialize() {
+bool ArImageTransport::Initialize(vr::WebXrPresentationState* webxr) {
+  DVLOG(1) << __func__;
   DCHECK(IsOnGlThread());
 
+  webxr_ = webxr;
+
   mailbox_bridge_->BindContextProviderToCurrentThread();
 
   glDisable(GL_DEPTH_TEST);
@@ -91,7 +59,7 @@
   ar_renderer_ = std::make_unique<ArRenderer>();
   glGenTextures(1, &camera_texture_id_arcore_);
 
-  SetupHardwareBuffers();
+  glGenFramebuffersEXT(1, &camera_fbo_);
 
   return true;
 }
@@ -101,7 +69,7 @@
 }
 
 void ArImageTransport::ResizeSharedBuffer(const gfx::Size& size,
-                                          SharedFrameBuffer* buffer) {
+                                          vr::WebXrSharedBuffer* buffer) {
   DCHECK(IsOnGlThread());
 
   if (buffer->size == size)
@@ -125,31 +93,29 @@
   static constexpr gfx::BufferFormat format = gfx::BufferFormat::RGBA_8888;
   static constexpr gfx::BufferUsage usage = gfx::BufferUsage::SCANOUT;
 
-  gfx::GpuMemoryBufferId kBufferId(swap_chain_->next_memory_buffer_id++);
-  buffer->shared_gpu_memory_buffer =
-      gpu::GpuMemoryBufferImplAndroidHardwareBuffer::Create(
-          kBufferId, size, format, usage,
-          gpu::GpuMemoryBufferImpl::DestructionCallback());
+  gfx::GpuMemoryBufferId kBufferId(webxr_->next_memory_buffer_id++);
+  buffer->gmb = gpu::GpuMemoryBufferImplAndroidHardwareBuffer::Create(
+      kBufferId, size, format, usage,
+      gpu::GpuMemoryBufferImpl::DestructionCallback());
 
   uint32_t shared_image_usage = gpu::SHARED_IMAGE_USAGE_SCANOUT |
                                 gpu::SHARED_IMAGE_USAGE_DISPLAY |
                                 gpu::SHARED_IMAGE_USAGE_GLES2;
-  buffer->mailbox_holder =
-      mailbox_bridge_->CreateSharedImage(buffer->shared_gpu_memory_buffer.get(),
-                                         gfx::ColorSpace(), shared_image_usage);
+  buffer->mailbox_holder = mailbox_bridge_->CreateSharedImage(
+      buffer->gmb.get(), gfx::ColorSpace(), shared_image_usage);
   DVLOG(2) << ": CreateSharedImage, mailbox="
            << buffer->mailbox_holder.mailbox.ToDebugString();
 
   auto img = base::MakeRefCounted<gl::GLImageAHardwareBuffer>(size);
 
   base::android::ScopedHardwareBufferHandle ahb =
-      buffer->shared_gpu_memory_buffer->CloneHandle().android_hardware_buffer;
+      buffer->gmb->CloneHandle().android_hardware_buffer;
   bool ret = img->Initialize(ahb.get(), false /* preserved */);
   if (!ret) {
     DLOG(WARNING) << __FUNCTION__ << ": ERROR: failed to initialize image!";
     return;
   }
-  glBindTexture(GL_TEXTURE_EXTERNAL_OES, buffer->local_texture_id);
+  glBindTexture(GL_TEXTURE_EXTERNAL_OES, buffer->local_texture);
   img->BindTexImage(GL_TEXTURE_EXTERNAL_OES);
   buffer->local_glimage = std::move(img);
 
@@ -159,87 +125,81 @@
   buffer->size = size;
 }
 
-void ArImageTransport::SetupHardwareBuffers() {
-  DCHECK(IsOnGlThread());
-
-  glGenFramebuffersEXT(1, &camera_fbo_);
-
-  for (int i = 0; i < kSharedBufferSwapChainSize; ++i) {
-    std::unique_ptr<SharedFrameBuffer> buffer =
-        std::make_unique<SharedFrameBuffer>();
-
-    // Local resources
-    glGenTextures(1, &buffer->local_texture_id);
-
-    // Add to swap chain
-    swap_chain_->buffers.push(std::move(buffer));
-  }
-
-  glGenFramebuffersEXT(1, &transfer_fbo_);
+std::unique_ptr<vr::WebXrSharedBuffer> ArImageTransport::CreateBuffer() {
+  std::unique_ptr<vr::WebXrSharedBuffer> buffer =
+      std::make_unique<vr::WebXrSharedBuffer>();
+  // Local resources
+  glGenTextures(1, &buffer->local_texture);
+  return buffer;
 }
 
 gpu::MailboxHolder ArImageTransport::TransferFrame(
     const gfx::Size& frame_size,
     const gfx::Transform& uv_transform) {
   DCHECK(IsOnGlThread());
-  // TODO(klausw): find out when a buffer is actually done being used
-  // including by GL so we can know if we are overwriting one.
-  // A sync token needs to be returned by the client and stashed into
-  // shared_buffer->mailbox_holder.sync_token, then waited upon before reusing
-  // the buffer.
-  DCHECK(swap_chain_->buffers.size() > 0);
 
-  glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER, transfer_fbo_);
-
-  std::unique_ptr<SharedFrameBuffer> shared_buffer =
-      std::move(swap_chain_->buffers.front());
-  swap_chain_->buffers.pop();
-  ResizeSharedBuffer(frame_size, shared_buffer.get());
-
-  glFramebufferTexture2DEXT(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
-                            GL_TEXTURE_EXTERNAL_OES,
-                            shared_buffer->local_texture_id, 0);
-
-  if (!transfer_fbo_completeness_checked_) {
-    auto status = glCheckFramebufferStatusEXT(GL_DRAW_FRAMEBUFFER);
-    DVLOG(1) << __FUNCTION__ << ": framebuffer status=" << std::hex << status;
-    DCHECK(status == GL_FRAMEBUFFER_COMPLETE);
-    transfer_fbo_completeness_checked_ = true;
+  if (!webxr_->GetAnimatingFrame()->shared_buffer) {
+    webxr_->GetAnimatingFrame()->shared_buffer = CreateBuffer();
   }
+  vr::WebXrSharedBuffer* shared_buffer =
+      webxr_->GetAnimatingFrame()->shared_buffer.get();
+  ResizeSharedBuffer(frame_size, shared_buffer);
 
+  mailbox_bridge_->GenSyncToken(&shared_buffer->mailbox_holder.sync_token);
+  return shared_buffer->mailbox_holder;
+}
+
+void ArImageTransport::CreateGpuFenceForSyncToken(
+    const gpu::SyncToken& sync_token,
+    base::OnceCallback<void(std::unique_ptr<gfx::GpuFence>)> callback) {
+  DVLOG(2) << __func__;
+  mailbox_bridge_->CreateGpuFence(sync_token, std::move(callback));
+}
+
+void ArImageTransport::WaitSyncToken(const gpu::SyncToken& sync_token) {
+  mailbox_bridge_->WaitSyncToken(sync_token);
+}
+
+void ArImageTransport::CopyCameraImageToFramebuffer(
+    const gfx::Size& frame_size,
+    const gfx::Transform& uv_transform) {
+  glDisable(GL_BLEND);
+  CopyTextureToFramebuffer(camera_texture_id_arcore_, frame_size, uv_transform);
+}
+
+void ArImageTransport::CopyDrawnImageToFramebuffer(
+    const gfx::Size& frame_size,
+    const gfx::Transform& uv_transform) {
+  DVLOG(2) << __func__;
+
+  vr::WebXrSharedBuffer* shared_buffer =
+      webxr_->GetRenderingFrame()->shared_buffer.get();
+
+  glEnable(GL_BLEND);
+  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+  glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER, 0);
+  CopyTextureToFramebuffer(shared_buffer->local_texture, frame_size,
+                           uv_transform);
+}
+
+void ArImageTransport::CopyTextureToFramebuffer(
+    GLuint texture,
+    const gfx::Size& frame_size,
+    const gfx::Transform& uv_transform) {
+  DVLOG(2) << __func__;
   // Don't need face culling, depth testing, blending, etc. Turn it all off.
   // TODO(klausw): see if we can do this one time on initialization. That would
   // be a tiny bit more efficient, but is only safe if ARCore and ArRenderer
   // don't modify these states.
   glDisable(GL_CULL_FACE);
   glDisable(GL_SCISSOR_TEST);
-  glDisable(GL_BLEND);
   glDisable(GL_POLYGON_OFFSET_FILL);
   glViewport(0, 0, frame_size.width(), frame_size.height());
 
   // Draw the ARCore texture!
   float uv_transform_floats[16];
   uv_transform.matrix().asColMajorf(uv_transform_floats);
-  ar_renderer_->Draw(camera_texture_id_arcore_, uv_transform_floats, 0, 0);
-
-  glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER, 0);
-
-  // Make a GpuFence and place it in the GPU stream for sequencing.
-  std::unique_ptr<gl::GLFence> gl_fence = gl::GLFence::CreateForGpuFence();
-  std::unique_ptr<gfx::GpuFence> gpu_fence = gl_fence->GetGpuFence();
-
-  // Have GL wait on both the client fence and the creation/return sync token.
-  // TODO(piman): this should probably do an UpdateSharedImage.
-  mailbox_bridge_->WaitSyncToken(shared_buffer->mailbox_holder.sync_token);
-  mailbox_bridge_->WaitForClientGpuFence(gpu_fence.get());
-  mailbox_bridge_->GenSyncToken(&shared_buffer->mailbox_holder.sync_token);
-
-  gpu::MailboxHolder rendered_frame_holder = shared_buffer->mailbox_holder;
-
-  // Done with the shared buffer.
-  swap_chain_->buffers.push(std::move(shared_buffer));
-
-  return rendered_frame_holder;
+  ar_renderer_->Draw(texture, uv_transform_floats, 0, 0);
 }
 
 bool ArImageTransport::IsOnGlThread() const {
diff --git a/chrome/browser/android/vr/arcore_device/ar_image_transport.h b/chrome/browser/android/vr/arcore_device/ar_image_transport.h
index 6703c43..747e2f9 100644
--- a/chrome/browser/android/vr/arcore_device/ar_image_transport.h
+++ b/chrome/browser/android/vr/arcore_device/ar_image_transport.h
@@ -12,19 +12,23 @@
 #include "device/vr/public/mojom/vr_service.mojom.h"
 #include "ui/gfx/geometry/size_f.h"
 
+namespace gfx {
+class GpuFence;
+}  // namespace gfx
+
 namespace gpu {
 struct MailboxHolder;
+struct SyncToken;
 }  // namespace gpu
 
 namespace vr {
 class MailboxToSurfaceBridge;
+class WebXrPresentationState;
+struct WebXrSharedBuffer;
 }  // namespace vr
 
 namespace device {
 
-struct SharedFrameBuffer;
-struct SharedFrameBufferSwapChain;
-
 // This class copies the camera texture to a shared image and returns a mailbox
 // holder which is suitable for mojo transport to the Renderer.
 class ArImageTransport {
@@ -34,7 +38,7 @@
   virtual ~ArImageTransport();
 
   // Initialize() must be called on a valid GL thread.
-  virtual bool Initialize();
+  virtual bool Initialize(vr::WebXrPresentationState* webxr);
 
   virtual GLuint GetCameraTextureId();
 
@@ -43,22 +47,32 @@
   // a gpu::MailboxHolder with that texture copied to a shared buffer.
   virtual gpu::MailboxHolder TransferFrame(const gfx::Size& frame_size,
                                            const gfx::Transform& uv_transform);
+  virtual void CreateGpuFenceForSyncToken(
+      const gpu::SyncToken& sync_token,
+      base::OnceCallback<void(std::unique_ptr<gfx::GpuFence>)>);
+  virtual void CopyCameraImageToFramebuffer(const gfx::Size& frame_size,
+                                            const gfx::Transform& uv_transform);
+  virtual void CopyDrawnImageToFramebuffer(const gfx::Size& frame_size,
+                                           const gfx::Transform& uv_transform);
+  virtual void CopyTextureToFramebuffer(GLuint texture,
+                                        const gfx::Size& frame_size,
+                                        const gfx::Transform& uv_transform);
+  virtual void WaitSyncToken(const gpu::SyncToken& sync_token);
 
  private:
-  void SetupHardwareBuffers();
-  void ResizeSharedBuffer(const gfx::Size& size, SharedFrameBuffer* buffer);
+  std::unique_ptr<vr::WebXrSharedBuffer> CreateBuffer();
+  void ResizeSharedBuffer(const gfx::Size& size, vr::WebXrSharedBuffer* buffer);
   bool IsOnGlThread() const;
   std::unique_ptr<ArRenderer> ar_renderer_;
   // samplerExternalOES texture data for WebXR content image.
   GLuint camera_texture_id_arcore_ = 0;
   GLuint camera_fbo_ = 0;
-  GLuint transfer_fbo_ = 0;
-  bool transfer_fbo_completeness_checked_ = false;
 
   scoped_refptr<base::SingleThreadTaskRunner> gl_thread_task_runner_;
 
   std::unique_ptr<vr::MailboxToSurfaceBridge> mailbox_bridge_;
-  std::unique_ptr<SharedFrameBufferSwapChain> swap_chain_;
+
+  vr::WebXrPresentationState* webxr_ = nullptr;
 
   DISALLOW_COPY_AND_ASSIGN(ArImageTransport);
 };
diff --git a/chrome/browser/android/vr/arcore_device/arcore_device.cc b/chrome/browser/android/vr/arcore_device/arcore_device.cc
index fdc03ed..796129fc 100644
--- a/chrome/browser/android/vr/arcore_device/arcore_device.cc
+++ b/chrome/browser/android/vr/arcore_device/arcore_device.cc
@@ -16,7 +16,6 @@
 #include "chrome/browser/android/vr/arcore_device/arcore_impl.h"
 #include "chrome/browser/android/vr/arcore_device/arcore_install_utils.h"
 #include "chrome/browser/android/vr/arcore_device/arcore_java_utils.h"
-#include "chrome/browser/android/vr/arcore_device/arcore_permission_helper.h"
 #include "chrome/browser/android/vr/mailbox_to_surface_bridge.h"
 #include "chrome/browser/permissions/permission_manager.h"
 #include "chrome/browser/permissions/permission_result.h"
@@ -34,10 +33,12 @@
 
 namespace {
 
-mojom::VRDisplayInfoPtr CreateVRDisplayInfo(mojom::XRDeviceId device_id) {
+mojom::VRDisplayInfoPtr CreateVRDisplayInfo(mojom::XRDeviceId device_id,
+                                            const gfx::Size& frame_size) {
   mojom::VRDisplayInfoPtr device = mojom::VRDisplayInfo::New();
   device->id = device_id;
   device->displayName = "ARCore VR Device";
+  device->webxr_default_framebuffer_scale = 1.0;
   device->capabilities = mojom::VRDisplayCapabilities::New();
   device->capabilities->hasPosition = true;
   device->capabilities->hasExternalDisplay = false;
@@ -48,11 +49,11 @@
   mojom::VREyeParametersPtr& left_eye = device->leftEye;
   left_eye->fieldOfView = mojom::VRFieldOfView::New();
   // TODO(lincolnfrog): get these values for real (see gvr device).
-  uint width = 1080;
-  uint height = 1795;
   double fov_x = 1437.387;
   double fov_y = 1438.074;
   // TODO(lincolnfrog): get real camera intrinsics.
+  int width = frame_size.width();
+  int height = frame_size.height();
   float horizontal_degrees = atan(width / (2.0 * fov_x)) * kDegreesPerRadian;
   float vertical_degrees = atan(height / (2.0 * fov_y)) * kDegreesPerRadian;
   left_eye->fieldOfView->leftDegrees = horizontal_degrees;
@@ -67,21 +68,27 @@
 
 }  // namespace
 
+ArCoreDevice::SessionState::SessionState() = default;
+ArCoreDevice::SessionState::~SessionState() = default;
+
 ArCoreDevice::ArCoreDevice(
     std::unique_ptr<ArCoreFactory> arcore_factory,
     std::unique_ptr<ArImageTransportFactory> ar_image_transport_factory,
     std::unique_ptr<vr::MailboxToSurfaceBridge> mailbox_to_surface_bridge,
-    std::unique_ptr<vr::ArCoreInstallUtils> arcore_install_utils,
-    std::unique_ptr<ArCorePermissionHelper> arcore_permission_helper)
+    std::unique_ptr<vr::ArCoreInstallUtils> arcore_install_utils)
     : VRDeviceBase(mojom::XRDeviceId::ARCORE_DEVICE_ID),
       main_thread_task_runner_(base::ThreadTaskRunnerHandle::Get()),
       arcore_factory_(std::move(arcore_factory)),
       ar_image_transport_factory_(std::move(ar_image_transport_factory)),
       mailbox_bridge_(std::move(mailbox_to_surface_bridge)),
       arcore_install_utils_(std::move(arcore_install_utils)),
-      arcore_permission_helper_(std::move(arcore_permission_helper)),
+      session_state_(std::make_unique<ArCoreDevice::SessionState>()),
       weak_ptr_factory_(this) {
-  SetVRDisplayInfo(CreateVRDisplayInfo(GetId()));
+  // Ensure display_info_ is set to avoid crash in CallDeferredSessionCallback
+  // if initialization fails. Use an arbitrary but really low resolution to make
+  // it obvious if we're using this data instead of the actual values we get
+  // from the output drawing surface.
+  SetVRDisplayInfo(CreateVRDisplayInfo(GetId(), {16, 16}));
 
   // TODO(https://crbug.com/836524) clean up usage of mailbox bridge
   // and extract the methods in this class that interact with ARCore API
@@ -91,178 +98,253 @@
 }
 
 ArCoreDevice::ArCoreDevice()
-    : ArCoreDevice(std::make_unique<ArCoreImplFactory>(),
-                   std::make_unique<ArImageTransportFactory>(),
-                   std::make_unique<vr::MailboxToSurfaceBridge>(),
-                   std::make_unique<vr::ArCoreJavaUtils>(
-                       base::BindRepeating(
-                           &ArCoreDevice::OnRequestInstallArModuleResult,
-                           base::Unretained(
-                               this)),  // unretained is fine since ArCoreDevice
-                                        // owns the ArCoreJavaUtils instance
-                       base::BindRepeating(
-                           &ArCoreDevice::OnRequestInstallSupportedArCoreResult,
-                           base::Unretained(this))),  // ditto
-                   std::make_unique<ArCorePermissionHelper>()) {}
+    : ArCoreDevice(
+          std::make_unique<ArCoreImplFactory>(),
+          std::make_unique<ArImageTransportFactory>(),
+          std::make_unique<vr::MailboxToSurfaceBridge>(),
+          std::make_unique<vr::ArCoreJavaUtils>(
+              base::BindRepeating(
+                  &ArCoreDevice::OnRequestInstallArModuleResult,
+                  base::Unretained(this)),  // unretained is fine for callbacks
+                                            // since ArCoreDevice owns the
+                                            // ArCoreJavaUtils instance
+              base::BindRepeating(
+                  &ArCoreDevice::OnRequestInstallSupportedArCoreResult,
+                  base::Unretained(this)))) {}
 
 ArCoreDevice::~ArCoreDevice() {
-  CallDeferredRequestSessionCallbacks(/*success=*/false);
+  CallDeferredRequestSessionCallback(/*success=*/false);
   // The GL thread must be terminated since it uses our members. For example,
   // there might still be a posted Initialize() call in flight that uses
   // arcore_install_utils_ and arcore_factory_. Ensure that the thread is
   // stopped before other members get destructed. Don't call Stop() here,
   // destruction calls Stop() and doing so twice is illegal (null pointer
   // dereference).
-  arcore_gl_thread_ = nullptr;
+  session_state_->arcore_gl_thread_ = nullptr;
 }
 
 void ArCoreDevice::OnMailboxBridgeReady() {
+  DVLOG(1) << __func__;
   DCHECK(IsOnMainThread());
-  DCHECK(!arcore_gl_thread_);
+  DCHECK(!session_state_->arcore_gl_thread_);
   // MailboxToSurfaceBridge's destructor's call to DestroyContext must
   // happen on the GL thread, so transferring it to that thread is appropriate.
   // TODO(https://crbug.com/836553): use same GL thread as GVR.
-  arcore_gl_thread_ = std::make_unique<ArCoreGlThread>(
+  session_state_->arcore_gl_thread_ = std::make_unique<ArCoreGlThread>(
       std::move(ar_image_transport_factory_), std::move(mailbox_bridge_),
       CreateMainThreadCallback(base::BindOnce(
           &ArCoreDevice::OnArCoreGlThreadInitialized, GetWeakPtr())));
-  arcore_gl_thread_->Start();
+  session_state_->arcore_gl_thread_->Start();
 }
 
 void ArCoreDevice::OnArCoreGlThreadInitialized() {
+  DVLOG(1) << __func__;
   DCHECK(IsOnMainThread());
 
-  is_arcore_gl_thread_initialized_ = true;
+  session_state_->is_arcore_gl_thread_initialized_ = true;
 
-  if (pending_request_ar_module_callback_) {
-    std::move(pending_request_ar_module_callback_).Run();
+  if (session_state_->pending_request_session_after_gl_thread_initialized_) {
+    std::move(
+        session_state_->pending_request_session_after_gl_thread_initialized_)
+        .Run();
   }
 }
 
 void ArCoreDevice::RequestSession(
     mojom::XRRuntimeSessionOptionsPtr options,
     mojom::XRRuntime::RequestSessionCallback callback) {
+  DVLOG(1) << __func__;
   DCHECK(IsOnMainThread());
 
-  // If we are currently handling another request defer this request. All
-  // deferred requests will be processed once handling is complete.
-  deferred_request_session_callbacks_.push_back(std::move(callback));
-  if (deferred_request_session_callbacks_.size() > 1) {
+  if (session_state_->pending_request_session_callback_) {
+    DVLOG(1) << __func__ << ": Rejecting additional session request";
+    std::move(callback).Run(nullptr, nullptr);
     return;
   }
+  session_state_->pending_request_session_callback_ = std::move(callback);
+
+  if (session_state_->is_arcore_gl_thread_initialized_) {
+    // First session on a new ArCoreDevice, and it's ready to proceed now.
+    RequestSessionAfterInitialization(options->render_process_id,
+                                      options->render_frame_id);
+  } else {
+    if (mailbox_bridge_) {
+      // This is a new ArCoreDevice, but its mailbox_bridge_ hasn't finished
+      // initialization yet.
+    } else {
+      // We're reusing a previously constructed ArCoreDevice for a new session.
+      // Restart initialization.
+      mailbox_bridge_ = std::make_unique<vr::MailboxToSurfaceBridge>();
+      mailbox_bridge_->CreateUnboundContextProvider(
+          base::BindOnce(&ArCoreDevice::OnMailboxBridgeReady, GetWeakPtr()));
+    }
+
+    // We're now expecting a call to OnMailboxBridgeReady() which will create
+    // a new GL thread, and at some point after that GL thread initialization
+    // will complete which calls OnArCoreGlThreadInitialized().
+    session_state_->pending_request_session_after_gl_thread_initialized_ =
+        base::BindOnce(&ArCoreDevice::RequestSessionAfterInitialization,
+                       GetWeakPtr(), options->render_process_id,
+                       options->render_frame_id);
+  }
+}
+
+void ArCoreDevice::RequestSessionAfterInitialization(int render_process_id,
+                                                     int render_frame_id) {
+  session_state_->start_immersive_activity_callback_ =
+      base::BindOnce(&ArCoreDevice::RequestArSessionConsent, GetWeakPtr(),
+                     render_process_id, render_frame_id);
+
+  RequestArModule(render_process_id, render_frame_id);
+}
+
+void ArCoreDevice::RequestArSessionConsent(int render_process_id,
+                                           int render_frame_id) {
+  auto ready_callback =
+      base::BindRepeating(&ArCoreDevice::OnDrawingSurfaceReady, GetWeakPtr());
+  auto touch_callback =
+      base::BindRepeating(&ArCoreDevice::OnDrawingSurfaceTouch, GetWeakPtr());
+  auto destroyed_callback =
+      base::BindOnce(&ArCoreDevice::OnDrawingSurfaceDestroyed, GetWeakPtr());
+
+  arcore_install_utils_->RequestArSession(
+      render_process_id, render_frame_id, std::move(ready_callback),
+      std::move(touch_callback), std::move(destroyed_callback));
+}
+
+void ArCoreDevice::OnDrawingSurfaceReady(gfx::AcceleratedWidget window,
+                                         display::Display::Rotation rotation,
+                                         const gfx::Size& frame_size) {
+  DVLOG(1) << __func__ << ": size=" << frame_size.width() << "x"
+           << frame_size.height() << " rotation=" << static_cast<int>(rotation);
+  DCHECK(!session_state_->is_arcore_gl_initialized_);
+
+  auto display_info = CreateVRDisplayInfo(GetId(), frame_size);
+  SetVRDisplayInfo(std::move(display_info));
+
+  RequestArCoreGlInitialization(window, rotation, frame_size);
+}
+
+void ArCoreDevice::OnDrawingSurfaceTouch(bool touching,
+                                         const gfx::PointF& location) {
+  DVLOG(2) << __func__ << ": touching=" << touching;
+
+  if (!session_state_->is_arcore_gl_initialized_ ||
+      !session_state_->arcore_gl_thread_)
+    return;
+
+  PostTaskToGlThread(base::BindOnce(
+      &ArCoreGl::OnScreenTouch,
+      session_state_->arcore_gl_thread_->GetArCoreGl()->GetWeakPtr(), touching,
+      location));
+}
+
+void ArCoreDevice::OnDrawingSurfaceDestroyed() {
+  DVLOG(1) << __func__;
+
+  CallDeferredRequestSessionCallback(/*success=*/false);
+
+  OnSessionEnded();
+}
+
+void ArCoreDevice::OnSessionEnded() {
+  DVLOG(1) << __func__;
+
+  // This may be a no-op in case it's destroyed already.
+  arcore_install_utils_->DestroyDrawingSurface();
+
+  // The GL thread had initialized its context with a drawing_widget based on
+  // the ArImmersiveOverlay's Surface, and the one it has is no longer valid.
+  // For now, just destroy the GL thread so that it is recreated for the next
+  // session with fresh associated resources. Also go through these steps in
+  // case the GL thread hadn't completed, or had initialized partially, to
+  // ensure consistent state.
 
   // TODO(https://crbug.com/849568): Instead of splitting the initialization
   // of this class between construction and RequestSession, perform all the
   // initialization at once on the first successful RequestSession call.
-  if (!is_arcore_gl_thread_initialized_) {
-    pending_request_ar_module_callback_ =
-        base::BindOnce(&ArCoreDevice::RequestArModule, GetWeakPtr(),
-                       options->render_process_id, options->render_frame_id,
-                       options->has_user_activation);
-    return;
-  }
 
-  RequestArModule(options->render_process_id, options->render_frame_id,
-                  options->has_user_activation);
+  // Reset per-session members to initial values.
+  session_state_ = std::make_unique<ArCoreDevice::SessionState>();
+
+  // The image transport factory should be reusable, but we've std::moved it
+  // to the GL thread. Make a new one for next time. (This is cheap, it's
+  // just a factory.)
+  ar_image_transport_factory_ = std::make_unique<ArImageTransportFactory>();
+
+  // Shut down the mailbox bridge, this has the side effect of also destroying
+  // GL resources in the GPU process.
+  mailbox_bridge_ = nullptr;
 }
 
-void ArCoreDevice::RequestArModule(int render_process_id,
-                                   int render_frame_id,
-                                   bool has_user_activation) {
+void ArCoreDevice::RequestArModule(int render_process_id, int render_frame_id) {
+  DVLOG(1) << __func__;
   if (arcore_install_utils_->ShouldRequestInstallArModule()) {
     if (!arcore_install_utils_->CanRequestInstallArModule()) {
-      OnRequestArModuleResult(render_process_id, render_frame_id,
-                              has_user_activation, false);
+      OnRequestArModuleResult(render_process_id, render_frame_id, false);
       return;
     }
 
     on_request_ar_module_result_callback_ =
         base::BindOnce(&ArCoreDevice::OnRequestArModuleResult, GetWeakPtr(),
-                       render_process_id, render_frame_id, has_user_activation);
+                       render_process_id, render_frame_id);
     arcore_install_utils_->RequestInstallArModule(render_process_id,
                                                   render_frame_id);
     return;
   }
 
-  OnRequestArModuleResult(render_process_id, render_frame_id,
-                          has_user_activation, true);
+  OnRequestArModuleResult(render_process_id, render_frame_id, true);
 }
 
 void ArCoreDevice::OnRequestArModuleResult(int render_process_id,
                                            int render_frame_id,
-                                           bool has_user_activation,
                                            bool success) {
-  DVLOG(3) << __func__ << ": has_user_activation=" << has_user_activation
-           << ", success=" << success;
+  DVLOG(3) << __func__ << ": success=" << success;
 
   if (!success) {
-    CallDeferredRequestSessionCallbacks(/*success=*/false);
+    CallDeferredRequestSessionCallback(/*success=*/false);
     return;
   }
 
-  RequestArCoreInstallOrUpdate(render_process_id, render_frame_id,
-                               has_user_activation);
+  RequestArCoreInstallOrUpdate(render_process_id, render_frame_id);
 }
 
 void ArCoreDevice::RequestArCoreInstallOrUpdate(int render_process_id,
-                                                int render_frame_id,
-                                                bool has_user_activation) {
+                                                int render_frame_id) {
+  DVLOG(1) << __func__;
   DCHECK(IsOnMainThread());
-  DCHECK(is_arcore_gl_thread_initialized_);
   DCHECK(!on_request_arcore_install_or_update_result_callback_);
 
   if (arcore_install_utils_->ShouldRequestInstallSupportedArCore()) {
     // ARCore is not installed or requires an update. Store the callback to be
     // processed later once installation/update is complete or got cancelled.
     on_request_arcore_install_or_update_result_callback_ = base::BindOnce(
-        &ArCoreDevice::OnRequestArCoreInstallOrUpdateResult, GetWeakPtr(),
-        render_process_id, render_frame_id, has_user_activation);
+        &ArCoreDevice::OnRequestArCoreInstallOrUpdateResult, GetWeakPtr());
 
     arcore_install_utils_->RequestInstallSupportedArCore(render_process_id,
                                                          render_frame_id);
     return;
   }
 
-  OnRequestArCoreInstallOrUpdateResult(render_process_id, render_frame_id,
-                                       has_user_activation, true);
+  OnRequestArCoreInstallOrUpdateResult(true);
 }
 
-void ArCoreDevice::OnRequestArCoreInstallOrUpdateResult(
-    int render_process_id,
-    int render_frame_id,
-    bool has_user_activation,
-    bool success) {
+void ArCoreDevice::OnRequestArCoreInstallOrUpdateResult(bool success) {
+  DVLOG(1) << __func__;
   DCHECK(IsOnMainThread());
-  DCHECK(is_arcore_gl_thread_initialized_);
 
   if (!success) {
-    CallDeferredRequestSessionCallbacks(/*success=*/false);
+    CallDeferredRequestSessionCallback(/*success=*/false);
     return;
   }
 
-  // TODO(https://crbug.com/845792): Consider calling a method to ask for the
-  // appropriate permissions.
-  // ARCore sessions require camera permission.
-  arcore_permission_helper_->RequestCameraPermission(
-      render_process_id, render_frame_id, has_user_activation,
-      base::BindOnce(&ArCoreDevice::OnRequestCameraPermissionComplete,
-                     GetWeakPtr()));
-}
-
-void ArCoreDevice::OnRequestCameraPermissionComplete(bool success) {
-  DCHECK(IsOnMainThread());
-  DCHECK(is_arcore_gl_thread_initialized_);
-
-  if (!success) {
-    CallDeferredRequestSessionCallbacks(/*success=*/false);
-    return;
-  }
-
-  // By this point ARCore has already been set up, so continue handling request.
-  RequestArCoreGlInitialization();
+  DCHECK(session_state_->start_immersive_activity_callback_);
+  base::ResetAndReturn(&session_state_->start_immersive_activity_callback_)
+      .Run();
 }
 
 void ArCoreDevice::OnRequestInstallArModuleResult(bool success) {
+  DVLOG(1) << __func__;
   DCHECK(IsOnMainThread());
 
   if (on_request_ar_module_result_callback_) {
@@ -271,48 +353,60 @@
 }
 
 void ArCoreDevice::OnRequestInstallSupportedArCoreResult(bool success) {
+  DVLOG(1) << __func__;
   DCHECK(IsOnMainThread());
-  DCHECK(is_arcore_gl_thread_initialized_);
   DCHECK(on_request_arcore_install_or_update_result_callback_);
 
   std::move(on_request_arcore_install_or_update_result_callback_).Run(success);
 }
 
-void ArCoreDevice::CallDeferredRequestSessionCallbacks(bool success) {
+void ArCoreDevice::CallDeferredRequestSessionCallback(bool success) {
+  DVLOG(1) << __func__ << " success=" << success;
   DCHECK(IsOnMainThread());
-  DCHECK(!success || is_arcore_gl_thread_initialized_);
 
-  for (auto& deferred_callback : deferred_request_session_callbacks_) {
-    // We don't expect this call to alter deferred_request_session_callbacks_.
-    // The call may request another session, which should be handled right here
-    // in this loop as well.
+  // We might not have any pending session requests, i.e. if destroyed
+  // immediately after construction.
+  if (!session_state_->pending_request_session_callback_)
+    return;
 
-    if (!success) {
-      std::move(deferred_callback).Run(nullptr, nullptr);
-      continue;
-    }
+  mojom::XRRuntime::RequestSessionCallback deferred_callback =
+      base::ResetAndReturn(&session_state_->pending_request_session_callback_);
 
-    auto callback = base::BindOnce(&ArCoreDevice::OnCreateSessionCallback,
-                                   GetWeakPtr(), std::move(deferred_callback));
-
-    PostTaskToGlThread(base::BindOnce(
-        &ArCoreGl::CreateSession,
-        arcore_gl_thread_->GetArCoreGl()->GetWeakPtr(), display_info_->Clone(),
-        CreateMainThreadCallback(std::move(callback))));
+  if (!success) {
+    std::move(deferred_callback).Run(nullptr, nullptr);
+    return;
   }
-  deferred_request_session_callbacks_.clear();
+
+  // Success case should only happen after GL thread is ready.
+  DCHECK(session_state_->is_arcore_gl_thread_initialized_);
+  auto create_callback =
+      base::BindOnce(&ArCoreDevice::OnCreateSessionCallback, GetWeakPtr(),
+                     std::move(deferred_callback));
+
+  auto shutdown_callback =
+      base::BindOnce(&ArCoreDevice::OnSessionEnded, GetWeakPtr());
+
+  PostTaskToGlThread(base::BindOnce(
+      &ArCoreGl::CreateSession,
+      session_state_->arcore_gl_thread_->GetArCoreGl()->GetWeakPtr(),
+      display_info_->Clone(),
+      CreateMainThreadCallback(std::move(create_callback)),
+      CreateMainThreadCallback(std::move(shutdown_callback))));
 }
 
 void ArCoreDevice::OnCreateSessionCallback(
     mojom::XRRuntime::RequestSessionCallback deferred_callback,
     mojom::XRFrameDataProviderPtrInfo frame_data_provider_info,
     mojom::VRDisplayInfoPtr display_info,
-    mojom::XRSessionControllerPtrInfo session_controller_info) {
+    mojom::XRSessionControllerPtrInfo session_controller_info,
+    mojom::XRPresentationConnectionPtr presentation_connection) {
+  DVLOG(2) << __func__;
   DCHECK(IsOnMainThread());
 
   mojom::XRSessionPtr session = mojom::XRSession::New();
   session->data_provider = std::move(frame_data_provider_info);
   session->display_info = std::move(display_info);
+  session->submit_frame_sink = std::move(presentation_connection);
 
   mojom::XRSessionControllerPtr controller(std::move(session_controller_info));
 
@@ -321,17 +415,22 @@
 
 void ArCoreDevice::PostTaskToGlThread(base::OnceClosure task) {
   DCHECK(IsOnMainThread());
-  arcore_gl_thread_->GetArCoreGl()->GetGlThreadTaskRunner()->PostTask(
-      FROM_HERE, std::move(task));
+  session_state_->arcore_gl_thread_->GetArCoreGl()
+      ->GetGlThreadTaskRunner()
+      ->PostTask(FROM_HERE, std::move(task));
 }
 
 bool ArCoreDevice::IsOnMainThread() {
   return main_thread_task_runner_->BelongsToCurrentThread();
 }
 
-void ArCoreDevice::RequestArCoreGlInitialization() {
+void ArCoreDevice::RequestArCoreGlInitialization(
+    gfx::AcceleratedWidget drawing_widget,
+    int drawing_rotation,
+    const gfx::Size& frame_size) {
+  DVLOG(1) << __func__;
   DCHECK(IsOnMainThread());
-  DCHECK(is_arcore_gl_thread_initialized_);
+  DCHECK(session_state_->is_arcore_gl_thread_initialized_);
 
   if (!arcore_install_utils_->EnsureLoaded()) {
     DLOG(ERROR) << "ARCore was not loaded properly.";
@@ -339,14 +438,17 @@
     return;
   }
 
-  if (!is_arcore_gl_initialized_) {
+  if (!session_state_->is_arcore_gl_initialized_) {
     // We will only try to initialize ArCoreGl once, at the end of the
     // permission sequence, and will resolve pending requests that have queued
     // up once that initialization completes. We set is_arcore_gl_initialized_
     // in the callback to block operations that require it to be ready.
+    auto rotation = static_cast<display::Display::Rotation>(drawing_rotation);
     PostTaskToGlThread(base::BindOnce(
-        &ArCoreGl::Initialize, arcore_gl_thread_->GetArCoreGl()->GetWeakPtr(),
-        arcore_install_utils_.get(), arcore_factory_.get(),
+        &ArCoreGl::Initialize,
+        session_state_->arcore_gl_thread_->GetArCoreGl()->GetWeakPtr(),
+        arcore_install_utils_.get(), arcore_factory_.get(), drawing_widget,
+        frame_size, rotation,
         CreateMainThreadCallback(base::BindOnce(
             &ArCoreDevice::OnArCoreGlInitializationComplete, GetWeakPtr()))));
     return;
@@ -356,17 +458,20 @@
 }
 
 void ArCoreDevice::OnArCoreGlInitializationComplete(bool success) {
+  DVLOG(1) << __func__;
   DCHECK(IsOnMainThread());
-  DCHECK(is_arcore_gl_thread_initialized_);
+  DCHECK(session_state_->is_arcore_gl_thread_initialized_);
 
   if (!success) {
-    CallDeferredRequestSessionCallbacks(/*success=*/false);
+    CallDeferredRequestSessionCallback(/*success=*/false);
     return;
   }
 
-  is_arcore_gl_initialized_ = true;
+  session_state_->is_arcore_gl_initialized_ = true;
 
-  CallDeferredRequestSessionCallbacks(/*success=*/true);
+  // We only start GL initialization after the user has granted consent, so we
+  // can now start the session.
+  CallDeferredRequestSessionCallback(/*success=*/true);
 }
 
 }  // namespace device
diff --git a/chrome/browser/android/vr/arcore_device/arcore_device.h b/chrome/browser/android/vr/arcore_device/arcore_device.h
index 583e74a6..fc2560b 100644
--- a/chrome/browser/android/vr/arcore_device/arcore_device.h
+++ b/chrome/browser/android/vr/arcore_device/arcore_device.h
@@ -12,10 +12,13 @@
 
 #include "base/android/jni_android.h"
 #include "base/bind.h"
+#include "base/callback.h"
 #include "base/macros.h"
 #include "base/optional.h"
 #include "device/vr/vr_device.h"
 #include "device/vr/vr_device_base.h"
+#include "ui/gfx/geometry/size_f.h"
+#include "ui/gfx/native_widget_types.h"
 
 namespace vr {
 class MailboxToSurfaceBridge;
@@ -27,7 +30,6 @@
 class ArImageTransportFactory;
 class ArCoreFactory;
 class ArCoreGlThread;
-class ArCorePermissionHelper;
 
 class ArCoreDevice : public VRDeviceBase {
  public:
@@ -35,8 +37,7 @@
       std::unique_ptr<ArCoreFactory> arcore_factory,
       std::unique_ptr<ArImageTransportFactory> ar_image_transport_factory,
       std::unique_ptr<vr::MailboxToSurfaceBridge> mailbox_to_surface_bridge,
-      std::unique_ptr<vr::ArCoreInstallUtils> arcore_install_utils,
-      std::unique_ptr<ArCorePermissionHelper> arcore_permission_helper);
+      std::unique_ptr<vr::ArCoreInstallUtils> arcore_install_utils);
   ArCoreDevice();
   ~ArCoreDevice() override;
 
@@ -58,6 +59,13 @@
   void OnArCoreGlThreadInitialized();
   void OnRequestCameraPermissionComplete(bool success);
 
+  void OnDrawingSurfaceReady(gfx::AcceleratedWidget window,
+                             display::Display::Rotation rotation,
+                             const gfx::Size& frame_size);
+  void OnDrawingSurfaceTouch(bool touching, const gfx::PointF& location);
+  void OnDrawingSurfaceDestroyed();
+  void OnSessionEnded();
+
   template <typename... Args>
   static void RunCallbackOnTaskRunner(
       const scoped_refptr<base::TaskRunner>& task_runner,
@@ -78,51 +86,59 @@
 
   bool IsOnMainThread();
 
-  void RequestArModule(int render_process_id,
-                       int render_frame_id,
-                       bool has_user_activation);
+  void RequestSessionAfterInitialization(int render_process_id,
+                                         int render_frame_id);
+  void RequestArModule(int render_process_id, int render_frame_id);
   void OnRequestArModuleResult(int render_process_id,
                                int render_frame_id,
-                               bool has_user_activation,
                                bool success);
-  void RequestArCoreInstallOrUpdate(int render_process_id,
-                                    int render_frame_id,
-                                    bool has_user_activation);
-  void OnRequestArCoreInstallOrUpdateResult(int render_process_id,
-                                            int render_frame_id,
-                                            bool has_user_activation,
-                                            bool success);
-  void CallDeferredRequestSessionCallbacks(bool success);
+  void RequestArCoreInstallOrUpdate(int render_process_id, int render_frame_id);
+  void OnRequestArCoreInstallOrUpdateResult(bool success);
+  void CallDeferredRequestSessionCallback(bool success);
   void OnRequestAndroidCameraPermissionResult(
       base::OnceCallback<void(bool)> callback,
       bool was_android_camera_permission_granted);
-  void RequestArCoreGlInitialization();
+  void RequestArCoreGlInitialization(gfx::AcceleratedWidget window,
+                                     int rotation,
+                                     const gfx::Size& size);
   void OnArCoreGlInitializationComplete(bool success);
+  void RequestArSessionConsent(int render_process_id, int render_frame_id);
 
   void OnCreateSessionCallback(
       mojom::XRRuntime::RequestSessionCallback deferred_callback,
       mojom::XRFrameDataProviderPtrInfo frame_data_provider_info,
       mojom::VRDisplayInfoPtr display_info,
-      mojom::XRSessionControllerPtrInfo session_controller_info);
+      mojom::XRSessionControllerPtrInfo session_controller_info,
+      mojom::XRPresentationConnectionPtr presentation_connection);
 
   scoped_refptr<base::SingleThreadTaskRunner> main_thread_task_runner_;
   std::unique_ptr<ArCoreFactory> arcore_factory_;
   std::unique_ptr<ArImageTransportFactory> ar_image_transport_factory_;
   std::unique_ptr<vr::MailboxToSurfaceBridge> mailbox_bridge_;
-  std::unique_ptr<ArCoreGlThread> arcore_gl_thread_;
   std::unique_ptr<vr::ArCoreInstallUtils> arcore_install_utils_;
-  std::unique_ptr<ArCorePermissionHelper> arcore_permission_helper_;
 
-  bool is_arcore_gl_thread_initialized_ = false;
-  bool is_arcore_gl_initialized_ = false;
+  // Encapsulates data with session lifetime.
+  struct SessionState {
+    SessionState();
+    ~SessionState();
 
-  // If we get a requestSession before we are completely initialized, store a
-  // callback to requesting the AR module since that is the next step that needs
-  // to be taken.
-  base::OnceClosure pending_request_ar_module_callback_;
+    std::unique_ptr<ArCoreGlThread> arcore_gl_thread_;
+    bool is_arcore_gl_thread_initialized_ = false;
+    bool is_arcore_gl_initialized_ = false;
 
-  std::vector<mojom::XRRuntime::RequestSessionCallback>
-      deferred_request_session_callbacks_;
+    base::OnceClosure start_immersive_activity_callback_;
+
+    // The initial requestSession triggers the initialization sequence, store
+    // the callback for replying once that initialization completes. Only one
+    // concurrent session is supported, other requests are rejected.
+    mojom::XRRuntime::RequestSessionCallback pending_request_session_callback_;
+
+    base::OnceClosure pending_request_session_after_gl_thread_initialized_;
+  };
+
+  // This object is reset to initial values when ending a session. This helps
+  // ensure that each session has consistent per-session state.
+  std::unique_ptr<SessionState> session_state_;
 
   base::OnceCallback<void(bool)>
       on_request_arcore_install_or_update_result_callback_;
diff --git a/chrome/browser/android/vr/arcore_device/arcore_device_unittest.cc b/chrome/browser/android/vr/arcore_device/arcore_device_unittest.cc
index 0aacf466..cc514c7 100644
--- a/chrome/browser/android/vr/arcore_device/arcore_device_unittest.cc
+++ b/chrome/browser/android/vr/arcore_device/arcore_device_unittest.cc
@@ -14,7 +14,6 @@
 #include "chrome/browser/android/vr/arcore_device/arcore_device.h"
 #include "chrome/browser/android/vr/arcore_device/arcore_gl.h"
 #include "chrome/browser/android/vr/arcore_device/arcore_install_utils.h"
-#include "chrome/browser/android/vr/arcore_device/arcore_permission_helper.h"
 #include "chrome/browser/android/vr/arcore_device/fake_arcore.h"
 #include "chrome/browser/android/vr/mailbox_to_surface_bridge.h"
 #include "device/vr/public/mojom/vr_service.mojom.h"
@@ -33,7 +32,7 @@
 
   // TODO(lincolnfrog): verify this gets called on GL thread.
   // TODO(lincolnfrog): test what happens if this returns false.
-  bool Initialize() override { return true; }
+  bool Initialize(vr::WebXrPresentationState*) override { return true; }
 
   // TODO(lincolnfrog): test verify this somehow.
   GLuint GetCameraTextureId() override { return CAMERA_TEXTURE_ID; }
@@ -95,6 +94,19 @@
   bool ShouldRequestInstallSupportedArCore() override { return false; }
   void RequestInstallSupportedArCore(int render_process_id,
                                      int render_frame_id) override {}
+  void RequestArSession(
+      int render_process_id,
+      int render_frame_id,
+      vr::SurfaceReadyCallback ready_callback,
+      vr::SurfaceTouchCallback touch_callback,
+      vr::SurfaceDestroyedCallback destroyed_callback) override {
+    // Return arbitrary screen geometry as stand-in for the expected
+    // drawing surface. It's not actually a surface, hence the nullptr
+    // instead of a WindowAndroid.
+    std::move(ready_callback)
+        .Run(nullptr, display::Display::Rotation::ROTATE_0, {1024, 512});
+  }
+  void DestroyDrawingSurface() override {}
 
   bool EnsureLoaded() override { return true; }
 
@@ -113,43 +125,14 @@
   }
 };
 
-class StubArCorePermissionHelper : public ArCorePermissionHelper {
- public:
-  StubArCorePermissionHelper() = default;
-
-  MOCK_METHOD4(DoRequestCameraPermission,
-               void(int render_process_id,
-                    int render_frame_id,
-                    bool has_user_activation,
-                    base::OnceCallback<void(bool)> callback));
-  void RequestCameraPermission(
-      int render_process_id,
-      int render_frame_id,
-      bool has_user_activation,
-      base::OnceCallback<void(bool)> callback) override {
-    callback_ = std::move(callback);
-    if (request_camera_permission_quit_closure) {
-      std::move(request_camera_permission_quit_closure).Run();
-    }
-  }
-
-  void CallCallback(bool result) { std::move(callback_).Run(result); }
-
-  base::OnceClosure request_camera_permission_quit_closure;
-
- private:
-  base::OnceCallback<void(bool)> callback_;
-};
-
 class ArCoreDeviceTest : public testing::Test {
  public:
   ArCoreDeviceTest() {}
   ~ArCoreDeviceTest() override {}
 
-  static const gfx::Size kTestFrameSize;
-
   void OnSessionCreated(mojom::XRSessionPtr session,
                         mojom::XRSessionControllerPtr controller) {
+    DVLOG(1) << __func__;
     session_ = std::move(session);
     controller_ = std::move(controller);
     // TODO(crbug.com/837834): verify that things fail if restricted.
@@ -166,7 +149,6 @@
 
   StubMailboxToSurfaceBridge* bridge;
   StubArCoreInstallUtils* install_utils;
-  StubArCorePermissionHelper* permission_helper;
   mojom::XRFrameDataProviderPtr frame_provider;
   mojom::XREnvironmentIntegrationProviderAssociatedPtr environment_provider;
   std::unique_ptr<base::RunLoop> run_loop;
@@ -180,21 +162,17 @@
     std::unique_ptr<StubArCoreInstallUtils> install_utils_ptr =
         std::make_unique<StubArCoreInstallUtils>();
     install_utils = install_utils_ptr.get();
-    std::unique_ptr<StubArCorePermissionHelper> permission_helper_ptr =
-        std::make_unique<StubArCorePermissionHelper>();
-    permission_helper = permission_helper_ptr.get();
     device_ = std::make_unique<ArCoreDevice>(
         std::make_unique<FakeArCoreFactory>(),
         std::make_unique<StubArImageTransportFactory>(), std::move(bridge_ptr),
-        std::move(install_utils_ptr), std::move(permission_helper_ptr));
+        std::move(install_utils_ptr));
   }
 
   void CreateSession() {
     mojom::XRRuntimeSessionOptionsPtr options =
         mojom::XRRuntimeSessionOptions::New();
-    options->immersive = false;
-    // TODO(crbug.com/837834): ensure request fails without user activation?
-    options->has_user_activation = true;
+    options->environment_integration = true;
+    options->immersive = true;
     device()->RequestSession(std::move(options),
                              base::BindOnce(&ArCoreDeviceTest::OnSessionCreated,
                                             base::Unretained(this)));
@@ -204,25 +182,12 @@
     // DoCreateUnboundContextProvider(testing::_)).Times(1);
 
     run_loop = std::make_unique<base::RunLoop>();
-    permission_helper->request_camera_permission_quit_closure =
-        run_loop->QuitClosure();
-    bridge->CallCallback();
-    run_loop->Run();
-
-    // TODO(https://crbug.com/837834): figure out how to make this work.
-    // EXPECT_CALL(*permission_helper, DoRequestCameraPermission(testing::_,
-    // testing::_, testing::_, testing::_)).Times(1);
-
-    run_loop = std::make_unique<base::RunLoop>();
     quit_closure = run_loop->QuitClosure();
-    permission_helper->CallCallback(true);
+    bridge->CallCallback();
     run_loop->Run();
 
     EXPECT_TRUE(environment_provider);
     EXPECT_TRUE(session_);
-
-    environment_provider->UpdateSessionGeometry(kTestFrameSize,
-                                                display::Display::ROTATE_0);
   }
 
   mojom::XRFrameDataPtr GetFrameData() {
@@ -256,9 +221,6 @@
   mojom::XRSessionControllerPtr controller_;
 };
 
-// The default screen size for portrait mode on the Pixel Android phone.
-const gfx::Size ArCoreDeviceTest::kTestFrameSize = {1080, 1795};
-
 TEST_F(ArCoreDeviceTest, RequestSession) {
   CreateSession();
 }
@@ -268,20 +230,6 @@
   GetFrameData();
 }
 
-TEST_F(ArCoreDeviceTest, SetDisplayGeometry) {
-  CreateSession();
-
-  auto frame_data = GetFrameData();
-  EXPECT_TRUE(frame_data->buffer_size.value() == kTestFrameSize);
-
-  gfx::Size new_size = {100, 200};
-  environment_provider->UpdateSessionGeometry(new_size,
-                                              display::Display::ROTATE_90);
-
-  frame_data = GetFrameData();
-  EXPECT_TRUE(frame_data->buffer_size.value() == new_size);
-}
-
 TEST_F(ArCoreDeviceTest, RequestHitTest) {
   CreateSession();
 
diff --git a/chrome/browser/android/vr/arcore_device/arcore_gl.cc b/chrome/browser/android/vr/arcore_device/arcore_gl.cc
index a05b998..2357e987 100644
--- a/chrome/browser/android/vr/arcore_device/arcore_gl.cc
+++ b/chrome/browser/android/vr/arcore_device/arcore_gl.cc
@@ -5,11 +5,13 @@
 #include "chrome/browser/android/vr/arcore_device/arcore_gl.h"
 
 #include <algorithm>
+#include <iomanip>
 #include <limits>
 #include <utility>
 #include "base/android/android_hardware_buffer_compat.h"
 #include "base/android/jni_android.h"
 #include "base/bind.h"
+#include "base/bind_helpers.h"
 #include "base/callback_helpers.h"
 #include "base/containers/queue.h"
 #include "base/memory/ptr_util.h"
@@ -20,7 +22,7 @@
 #include "chrome/browser/android/vr/arcore_device/ar_image_transport.h"
 #include "chrome/browser/android/vr/arcore_device/arcore_impl.h"
 #include "chrome/browser/android/vr/arcore_device/arcore_install_utils.h"
-#include "chrome/browser/android/vr/mailbox_to_surface_bridge.h"
+#include "chrome/browser/android/vr/web_xr_presentation_state.h"
 #include "device/vr/public/mojom/vr_service.mojom.h"
 #include "gpu/ipc/common/gpu_memory_buffer_impl_android_hardware_buffer.h"
 #include "ui/display/display.h"
@@ -76,6 +78,11 @@
   return result;
 }
 
+gfx::Transform WebXRImageTransformMatrix() {
+  gfx::Transform result;
+  return result;
+}
+
 const gfx::Size kDefaultFrameSize = {1, 1};
 const display::Display::Rotation kDefaultRotation = display::Display::ROTATE_0;
 
@@ -96,34 +103,39 @@
 ArCoreGl::ArCoreGl(std::unique_ptr<ArImageTransport> ar_image_transport)
     : gl_thread_task_runner_(base::ThreadTaskRunnerHandle::Get()),
       ar_image_transport_(std::move(ar_image_transport)),
+      webxr_(std::make_unique<vr::WebXrPresentationState>()),
       frame_data_binding_(this),
       session_controller_binding_(this),
       environment_binding_(this),
-      weak_ptr_factory_(this) {}
+      presentation_binding_(this),
+      weak_ptr_factory_(this) {
+  DVLOG(1) << __func__;
+  webxr_transform_ = WebXRImageTransformMatrix();
+}
 
 ArCoreGl::~ArCoreGl() {
+  DVLOG(1) << __func__;
   DCHECK(IsOnGlThread());
   ar_image_transport_.reset();
+  CloseBindingsIfOpen();
 }
 
 void ArCoreGl::Initialize(vr::ArCoreInstallUtils* install_utils,
                           ArCoreFactory* arcore_factory,
+                          gfx::AcceleratedWidget drawing_widget,
+                          const gfx::Size& frame_size,
+                          display::Display::Rotation display_rotation,
                           base::OnceCallback<void(bool)> callback) {
   DVLOG(3) << __func__;
 
   DCHECK(IsOnGlThread());
+  DCHECK(!is_initialized_);
 
-  // Do not DCHECK !is_initialized to allow multiple calls to correctly
-  // proceed. This method may be called multiple times if a subsequent session
-  // request occurs before the first one completes and the callback is called.
-  // TODO(https://crbug.com/849568): This may not be necessary after
-  // addressing this issue.
-  if (is_initialized_) {
-    std::move(callback).Run(true);
-    return;
-  }
+  transfer_size_ = frame_size;
+  display_rotation_ = display_rotation;
+  should_update_display_geometry_ = true;
 
-  if (!InitializeGl()) {
+  if (!InitializeGl(drawing_widget)) {
     std::move(callback).Run(false);
     return;
   }
@@ -155,12 +167,15 @@
 }
 
 void ArCoreGl::CreateSession(mojom::VRDisplayInfoPtr display_info,
-                             ArCoreGlCreateSessionCallback callback) {
+                             ArCoreGlCreateSessionCallback create_callback,
+                             base::OnceClosure shutdown_callback) {
   DVLOG(3) << __func__;
 
   DCHECK(IsOnGlThread());
   DCHECK(is_initialized_);
 
+  session_shutdown_callback_ = std::move(shutdown_callback);
+
   CloseBindingsIfOpen();
 
   mojom::XRFrameDataProviderPtrInfo frame_data_provider_info;
@@ -173,11 +188,33 @@
   session_controller_binding_.set_connection_error_handler(base::BindOnce(
       &ArCoreGl::OnBindingDisconnect, weak_ptr_factory_.GetWeakPtr()));
 
-  std::move(callback).Run(std::move(frame_data_provider_info),
-                          std::move(display_info), std::move(controller_info));
+  device::mojom::XRPresentationProviderPtr presentation_provider;
+  presentation_binding_.Bind(mojo::MakeRequest(&presentation_provider));
+
+  device::mojom::XRPresentationTransportOptionsPtr transport_options =
+      device::mojom::XRPresentationTransportOptions::New();
+  transport_options->wait_for_gpu_fence = true;
+
+  // Currently, AR mode only supports Android O+ due to requiring
+  // AHardwareBuffer-backed GpuMemoryBuffer shared images. This could be
+  // extended back to Android N by using the SUBMIT_AS_MAILBOX_HOLDER method
+  // that uses Surface/SurfaceTexture.
+  transport_options->transport_method =
+      device::mojom::XRPresentationTransportMethod::DRAW_INTO_TEXTURE_MAILBOX;
+
+  auto submit_frame_sink = device::mojom::XRPresentationConnection::New();
+  submit_frame_sink->client_request = mojo::MakeRequest(&submit_client_);
+  submit_frame_sink->provider = presentation_provider.PassInterface();
+  submit_frame_sink->transport_options = std::move(transport_options);
+
+  display_info_ = std::move(display_info);
+
+  std::move(create_callback)
+      .Run(std::move(frame_data_provider_info), display_info_->Clone(),
+           std::move(controller_info), std::move(submit_frame_sink));
 }
 
-bool ArCoreGl::InitializeGl() {
+bool ArCoreGl::InitializeGl(gfx::AcceleratedWidget drawing_widget) {
   DVLOG(3) << __func__;
 
   DCHECK(IsOnGlThread());
@@ -190,9 +227,10 @@
   }
 
   scoped_refptr<gl::GLSurface> surface =
-      gl::init::CreateOffscreenGLSurface(gfx::Size());
+      gl::init::CreateViewGLSurface(drawing_widget);
+  DVLOG(3) << "surface=" << surface.get();
   if (!surface.get()) {
-    DLOG(ERROR) << "gl::init::CreateOffscreenGLSurface failed";
+    DLOG(ERROR) << "gl::init::CreateViewGLSurface failed";
     return false;
   }
 
@@ -207,7 +245,8 @@
     return false;
   }
 
-  if (!ar_image_transport_->Initialize()) {
+  DVLOG(3) << "ar_image_transport_->Initialize()...";
+  if (!ar_image_transport_->Initialize(webxr_.get())) {
     DLOG(ERROR) << "ARImageTransport failed to initialize";
     return false;
   }
@@ -217,6 +256,7 @@
   surface_ = std::move(surface);
   context_ = std::move(context);
 
+  DVLOG(3) << "done";
   return true;
 }
 
@@ -225,6 +265,14 @@
     mojom::XRFrameDataProvider::GetFrameDataCallback callback) {
   TRACE_EVENT0("gpu", __FUNCTION__);
 
+  if (webxr_->HaveAnimatingFrame()) {
+    DVLOG(3) << __func__ << ": deferring, HaveAnimatingFrame";
+    pending_getframedata_ =
+        base::BindOnce(&ArCoreGl::GetFrameData, GetWeakPtr(),
+                       std::move(options), std::move(callback));
+    return;
+  }
+
   DVLOG(3) << __func__ << ": should_update_display_geometry_="
            << should_update_display_geometry_
            << ", transfer_size_=" << transfer_size_.ToString()
@@ -238,6 +286,11 @@
     return;
   }
 
+  if (is_paused_) {
+    DVLOG(2) << __func__ << ": paused but frame data not restricted. Resuming.";
+    Resume();
+  }
+
   // Check if the frame_size and display_rotation updated last frame. If yes,
   // apply the update for this frame.
   if (should_recalculate_uvs_) {
@@ -252,6 +305,26 @@
     constexpr float depth_near = 0.1f;
     constexpr float depth_far = 1000.f;
     projection_ = arcore_->GetProjectionMatrix(depth_near, depth_far);
+    auto m = projection_.matrix();
+    float left = depth_near * (m.get(2, 0) - 1.f) / m.get(0, 0);
+    float right = depth_near * (m.get(2, 0) + 1.f) / m.get(0, 0);
+    float bottom = depth_near * (m.get(2, 1) - 1.f) / m.get(1, 1);
+    float top = depth_near * (m.get(2, 1) + 1.f) / m.get(1, 1);
+
+    // VRFieldOfView wants positive angles.
+    mojom::VRFieldOfViewPtr field_of_view = mojom::VRFieldOfView::New();
+    field_of_view->leftDegrees = gfx::RadToDeg(atanf(-left / depth_near));
+    field_of_view->rightDegrees = gfx::RadToDeg(atanf(right / depth_near));
+    field_of_view->downDegrees = gfx::RadToDeg(atanf(-bottom / depth_near));
+    field_of_view->upDegrees = gfx::RadToDeg(atanf(top / depth_near));
+    DVLOG(3) << " fov degrees up=" << field_of_view->upDegrees
+             << " down=" << field_of_view->downDegrees
+             << " left=" << field_of_view->leftDegrees
+             << " right=" << field_of_view->rightDegrees;
+
+    display_info_->leftEye->fieldOfView = std::move(field_of_view);
+    display_info_changed_ = true;
+
     should_recalculate_uvs_ = false;
   }
 
@@ -278,6 +351,7 @@
   if (!camera_updated) {
     DVLOG(1) << "arcore_->Update() failed";
     std::move(callback).Run(nullptr);
+    have_camera_image_ = false;
     return;
   }
 
@@ -286,23 +360,37 @@
   if (transfer_size_.IsEmpty()) {
     DLOG(ERROR) << "No valid AR frame size provided!";
     std::move(callback).Run(nullptr);
+    have_camera_image_ = false;
     return;
   }
 
-  // Transfer the camera image texture to a MailboxHolder for transport to
-  // the renderer process.
+  have_camera_image_ = true;
+  mojom::XRFrameDataPtr frame_data = mojom::XRFrameData::New();
+
+  frame_data->frame_id = webxr_->StartFrameAnimating();
+  DVLOG(2) << __func__ << " frame=" << frame_data->frame_id;
+
+  if (display_info_changed_) {
+    frame_data->left_eye = display_info_->leftEye.Clone();
+    display_info_changed_ = false;
+  }
+  // Set up a shared buffer for the renderer to draw into, it'll be sent
+  // alongside the frame pose.
   gpu::MailboxHolder buffer_holder =
       ar_image_transport_->TransferFrame(transfer_size_, uv_transform_);
 
+  if (pose) {
+    mojom::XRInputSourceStatePtr input_state = GetInputSourceState();
+    if (input_state) {
+      input_states_.push_back(std::move(input_state));
+      pose->input_state = std::move(input_states_);
+    }
+  }
+
   // Create the frame data to return to the renderer.
-  mojom::XRFrameDataPtr frame_data = mojom::XRFrameData::New();
   frame_data->pose = std::move(pose);
   frame_data->buffer_holder = buffer_holder;
-  frame_data->buffer_size = transfer_size_;
   frame_data->time_delta = base::TimeTicks::Now() - base::TimeTicks();
-  // Convert the Transform's 4x4 matrix to 16 floats in column-major order.
-  frame_data->projection_matrix.emplace(16);
-  projection_.matrix().asColMajorf(frame_data->projection_matrix->data());
 
   fps_meter_.AddFrame(base::TimeTicks::Now());
   TRACE_COUNTER1("gpu", "WebXR FPS", fps_meter_.GetFPS());
@@ -316,6 +404,133 @@
                      base::Passed(&frame_data), base::Passed(&callback)));
 }
 
+bool ArCoreGl::IsSubmitFrameExpected(int16_t frame_index) {
+  // submit_client_ could be null when we exit presentation, if there were
+  // pending SubmitFrame messages queued.  XRSessionClient::OnExitPresent
+  // will clean up state in blink, so it doesn't wait for
+  // OnSubmitFrameTransferred or OnSubmitFrameRendered. Similarly,
+  // the animating frame state is cleared when exiting presentation,
+  // and we should ignore a leftover queued SubmitFrame.
+  if (!submit_client_.get() || !webxr_->HaveAnimatingFrame())
+    return false;
+
+  vr::WebXrFrame* animating_frame = webxr_->GetAnimatingFrame();
+
+  if (animating_frame->index != frame_index) {
+    DVLOG(1) << __func__ << ": wrong frame index, got " << frame_index
+             << ", expected " << animating_frame->index;
+    mojo::ReportBadMessage("SubmitFrame called with wrong frame index");
+    CloseBindingsIfOpen();
+    return false;
+  }
+
+  // Frame looks valid.
+  return true;
+}
+
+void ArCoreGl::SubmitFrameMissing(int16_t frame_index,
+                                  const gpu::SyncToken& sync_token) {
+  DVLOG(2) << __func__;
+
+  if (!IsSubmitFrameExpected(frame_index))
+    return;
+
+  webxr_->RecycleUnusedAnimatingFrame();
+  ar_image_transport_->WaitSyncToken(sync_token);
+
+  // Draw the current camera texture to the output default framebuffer now.
+  if (have_camera_image_) {
+    glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER, 0);
+    ar_image_transport_->CopyCameraImageToFramebuffer(transfer_size_,
+                                                      uv_transform_);
+    have_camera_image_ = false;
+  }
+
+  // We're done with the camera image for this frame, start the next ARCore
+  // update if we had deferred it. This will get the next frame's camera image
+  // and pose in parallel while we're waiting for this frame's rendered image.
+  if (pending_getframedata_) {
+    base::ResetAndReturn(&pending_getframedata_).Run();
+  }
+
+  surface_->SwapBuffers(base::DoNothing());
+  DVLOG(3) << __func__ << ": frame=" << frame_index << " SwapBuffers";
+}
+
+void ArCoreGl::SubmitFrame(int16_t frame_index,
+                           const gpu::MailboxHolder& mailbox,
+                           base::TimeDelta time_waited) {
+  NOTIMPLEMENTED();
+}
+
+void ArCoreGl::SubmitFrameWithTextureHandle(int16_t frame_index,
+                                            mojo::ScopedHandle texture_handle) {
+  NOTIMPLEMENTED();
+}
+
+void ArCoreGl::SubmitFrameDrawnIntoTexture(int16_t frame_index,
+                                           const gpu::SyncToken& sync_token,
+                                           base::TimeDelta time_waited) {
+  DVLOG(2) << __func__ << ": frame=" << frame_index;
+
+  if (!IsSubmitFrameExpected(frame_index))
+    return;
+
+  webxr_->TransitionFrameAnimatingToProcessing();
+
+  TRACE_EVENT0("gpu", "ArCore SubmitFrame");
+
+  // Draw the current camera texture to the output default framebuffer now.
+  if (have_camera_image_) {
+    glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER, 0);
+    ar_image_transport_->CopyCameraImageToFramebuffer(transfer_size_,
+                                                      uv_transform_);
+    have_camera_image_ = false;
+  }
+
+  // We're done with the camera image for this frame, start the next ARCore
+  // update if we had deferred it. This will get the next frame's camera image
+  // and pose in parallel while we're waiting for this frame's rendered image.
+  if (pending_getframedata_) {
+    base::ResetAndReturn(&pending_getframedata_).Run();
+  }
+
+  ar_image_transport_->CreateGpuFenceForSyncToken(
+      sync_token, base::BindOnce(&ArCoreGl::OnWebXrTokenSignaled, GetWeakPtr(),
+                                 frame_index));
+}
+
+void ArCoreGl::OnWebXrTokenSignaled(int16_t frame_index,
+                                    std::unique_ptr<gfx::GpuFence> gpu_fence) {
+  DVLOG(3) << __func__ << ": frame=" << frame_index;
+
+  webxr_->TransitionFrameProcessingToRendering();
+
+  glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER, 0);
+  ar_image_transport_->CopyDrawnImageToFramebuffer(transfer_size_,
+                                                   webxr_transform_);
+  surface_->SwapBuffers(base::DoNothing());
+  DVLOG(3) << __func__ << ": frame=" << frame_index << " SwapBuffers";
+
+  webxr_->EndFrameRendering();
+
+  if (submit_client_) {
+    // Create a local GpuFence and pass it to the Renderer via IPC.
+    std::unique_ptr<gl::GLFence> gl_fence = gl::GLFence::CreateForGpuFence();
+    std::unique_ptr<gfx::GpuFence> gpu_fence2 = gl_fence->GetGpuFence();
+    submit_client_->OnSubmitFrameGpuFence(
+        gfx::CloneHandleForIPC(gpu_fence2->GetGpuFenceHandle()));
+  }
+}
+
+void ArCoreGl::UpdateLayerBounds(int16_t frame_index,
+                                 const gfx::RectF& left_bounds,
+                                 const gfx::RectF& right_bounds,
+                                 const gfx::Size& source_size) {
+  DVLOG(2) << __func__;
+  // Nothing to do
+}
+
 void ArCoreGl::GetEnvironmentIntegrationProvider(
     device::mojom::XREnvironmentIntegrationProviderAssociatedRequest
         environment_request) {
@@ -329,21 +544,6 @@
       &ArCoreGl::OnBindingDisconnect, weak_ptr_factory_.GetWeakPtr()));
 }
 
-void ArCoreGl::UpdateSessionGeometry(
-    const gfx::Size& frame_size,
-    display::Display::Rotation display_rotation) {
-  DVLOG(3) << __func__ << ": frame_size=" << frame_size.ToString()
-           << ", display_rotation=" << display_rotation;
-
-  DCHECK(IsOnGlThread());
-  DCHECK(is_initialized_);
-
-  transfer_size_ = frame_size;
-  display_rotation_ = display_rotation;
-
-  should_update_display_geometry_ = true;
-}
-
 void ArCoreGl::RequestHitTest(
     mojom::XRRayPtr ray,
     mojom::XREnvironmentIntegrationProvider::RequestHitTestCallback callback) {
@@ -369,6 +569,7 @@
   DCHECK(IsOnGlThread());
   DCHECK(is_initialized_);
 
+  DVLOG(3) << __func__ << ": frame_data_restricted=" << frame_data_restricted;
   restrict_frame_data_ = frame_data_restricted;
   if (restrict_frame_data_) {
     Pause();
@@ -380,7 +581,7 @@
 void ArCoreGl::ProcessFrame(
     mojom::XRFrameDataPtr frame_data,
     mojom::XRFrameDataProvider::GetFrameDataCallback callback) {
-  DVLOG(3) << __func__;
+  DVLOG(3) << __func__ << " frame=" << frame_data->frame_id;
 
   DCHECK(IsOnGlThread());
   DCHECK(is_initialized_);
@@ -416,24 +617,73 @@
   std::move(callback).Run(std::move(frame_data));
 }
 
+void ArCoreGl::OnScreenTouch(bool touching, const gfx::PointF& touch_point) {
+  DVLOG(2) << __func__ << ": touching=" << touching;
+  screen_last_touch_ = touch_point;
+  screen_touch_active_ = touching;
+  if (touching)
+    screen_touch_pending_ = true;
+}
+
+mojom::XRInputSourceStatePtr ArCoreGl::GetInputSourceState() {
+  DVLOG(3) << __func__;
+
+  // If there's no active screen touch, and no unreported past click event,
+  // don't report a device.
+  if (!screen_touch_pending_ && !screen_touch_active_)
+    return nullptr;
+
+  device::mojom::XRInputSourceStatePtr state =
+      device::mojom::XRInputSourceState::New();
+
+  // Only one controller is supported, so the source id can be static.
+  state->source_id = 1;
+
+  state->primary_input_pressed = screen_touch_active_;
+  if (!screen_touch_active_ && screen_touch_pending_) {
+    state->primary_input_clicked = true;
+    screen_touch_pending_ = false;
+  }
+
+  state->description = device::mojom::XRInputSourceDescription::New();
+
+  state->description->handedness = device::mojom::XRHandedness::NONE;
+
+  state->description->target_ray_mode = device::mojom::XRTargetRayMode::TAPPING;
+
+  // Controller doesn't have a measured position.
+  state->description->emulated_position = true;
+
+  // TODO(klausw): fill in state->grip and state->description->pointer_offset
+  // by unprojecting the screen coordinates into a world space ray.
+
+  return state;
+}
+
 void ArCoreGl::Pause() {
   DCHECK(IsOnGlThread());
   DCHECK(is_initialized_);
+  DVLOG(1) << __func__;
 
   arcore_->Pause();
+  is_paused_ = true;
 }
 
 void ArCoreGl::Resume() {
   DCHECK(IsOnGlThread());
   DCHECK(is_initialized_);
+  DVLOG(1) << __func__;
 
   arcore_->Resume();
+  is_paused_ = false;
 }
 
 void ArCoreGl::OnBindingDisconnect() {
   DVLOG(3) << __func__;
 
   CloseBindingsIfOpen();
+
+  base::ResetAndReturn(&session_shutdown_callback_).Run();
 }
 
 void ArCoreGl::CloseBindingsIfOpen() {
@@ -442,6 +692,7 @@
   environment_binding_.Close();
   frame_data_binding_.Close();
   session_controller_binding_.Close();
+  presentation_binding_.Close();
 }
 
 bool ArCoreGl::IsOnGlThread() const {
diff --git a/chrome/browser/android/vr/arcore_device/arcore_gl.h b/chrome/browser/android/vr/arcore_device/arcore_gl.h
index ae3b85c..5a3756c1 100644
--- a/chrome/browser/android/vr/arcore_device/arcore_gl.h
+++ b/chrome/browser/android/vr/arcore_device/arcore_gl.h
@@ -20,11 +20,16 @@
 #include "mojo/public/cpp/bindings/associated_binding.h"
 #include "mojo/public/cpp/bindings/binding.h"
 #include "ui/display/display.h"
+#include "ui/gfx/geometry/point_f.h"
 #include "ui/gfx/geometry/quaternion.h"
 #include "ui/gfx/geometry/rect_f.h"
 #include "ui/gfx/geometry/size_f.h"
 #include "ui/gfx/native_widget_types.h"
 
+namespace gfx {
+class GpuFence;
+}  // namespace gfx
+
 namespace gl {
 class GLContext;
 class GLSurface;
@@ -32,6 +37,7 @@
 
 namespace vr {
 class ArCoreInstallUtils;
+class WebXrPresentationState;
 }  // namespace vr
 
 namespace device {
@@ -44,11 +50,13 @@
 using ArCoreGlCreateSessionCallback = base::OnceCallback<void(
     mojom::XRFrameDataProviderPtrInfo frame_data_provider_info,
     mojom::VRDisplayInfoPtr display_info,
-    mojom::XRSessionControllerPtrInfo session_controller_info)>;
+    mojom::XRSessionControllerPtrInfo session_controller_info,
+    mojom::XRPresentationConnectionPtr presentation_connection)>;
 
 // All of this class's methods must be called on the same valid GL thread with
 // the exception of GetGlThreadTaskRunner() and GetWeakPtr().
 class ArCoreGl : public mojom::XRFrameDataProvider,
+                 public mojom::XRPresentationProvider,
                  public mojom::XREnvironmentIntegrationProvider,
                  public mojom::XRSessionController {
  public:
@@ -57,10 +65,14 @@
 
   void Initialize(vr::ArCoreInstallUtils* install_utils,
                   ArCoreFactory* arcore_factory,
+                  gfx::AcceleratedWidget drawing_widget,
+                  const gfx::Size& frame_size,
+                  display::Display::Rotation display_rotation,
                   base::OnceCallback<void(bool)> callback);
 
   void CreateSession(mojom::VRDisplayInfoPtr display_info,
-                     ArCoreGlCreateSessionCallback callback);
+                     ArCoreGlCreateSessionCallback create_callback,
+                     base::OnceClosure shutdown_callback);
 
   const scoped_refptr<base::SingleThreadTaskRunner>& GetGlThreadTaskRunner() {
     return gl_thread_task_runner_;
@@ -74,10 +86,20 @@
       mojom::XREnvironmentIntegrationProviderAssociatedRequest
           environment_provider) override;
 
-  // mojom::XREnvironmentIntegrationProvider
-  void UpdateSessionGeometry(
-      const gfx::Size& frame_size,
-      display::Display::Rotation display_rotation) override;
+  // XRPresentationProvider
+  void SubmitFrameMissing(int16_t frame_index, const gpu::SyncToken&) override;
+  void SubmitFrame(int16_t frame_index,
+                   const gpu::MailboxHolder& mailbox,
+                   base::TimeDelta time_waited) override;
+  void SubmitFrameWithTextureHandle(int16_t frame_index,
+                                    mojo::ScopedHandle texture_handle) override;
+  void SubmitFrameDrawnIntoTexture(int16_t frame_index,
+                                   const gpu::SyncToken&,
+                                   base::TimeDelta time_waited) override;
+  void UpdateLayerBounds(int16_t frame_index,
+                         const gfx::RectF& left_bounds,
+                         const gfx::RectF& right_bounds,
+                         const gfx::Size& source_size) override;
 
   void RequestHitTest(
       mojom::XRRayPtr,
@@ -86,18 +108,27 @@
   // mojom::XRSessionController
   void SetFrameDataRestricted(bool restricted) override;
 
+  void OnWebXrTokenSignaled(int16_t frame_index,
+                            std::unique_ptr<gfx::GpuFence> gpu_fence);
+
+  void OnScreenTouch(bool touching, const gfx::PointF& touch_point);
+  mojom::XRInputSourceStatePtr GetInputSourceState();
+
   base::WeakPtr<ArCoreGl> GetWeakPtr();
 
  private:
   void Pause();
   void Resume();
 
+  bool IsSubmitFrameExpected(int16_t frame_index);
   void ProcessFrame(mojom::XRFrameDataPtr frame_data,
                     mojom::XRFrameDataProvider::GetFrameDataCallback callback);
 
-  bool InitializeGl();
+  bool InitializeGl(gfx::AcceleratedWidget drawing_widget);
   bool IsOnGlThread() const;
 
+  base::OnceClosure session_shutdown_callback_;
+
   scoped_refptr<gl::GLSurface> surface_;
   scoped_refptr<gl::GLContext> context_;
   scoped_refptr<base::SingleThreadTaskRunner> gl_thread_task_runner_;
@@ -106,18 +137,41 @@
   std::unique_ptr<ArCore> arcore_;
   std::unique_ptr<ArImageTransport> ar_image_transport_;
 
+  // This class uses the same overall presentation state logic
+  // as GvrGraphicsDelegate, with some difference due to drawing
+  // camera images even on frames with no pose and therefore
+  // no blink-generated rendered image.
+  //
+  // Rough sequence is:
+  //
+  // SubmitFrame N                 N animating->processing
+  //   draw camera N
+  //   waitForToken
+  // GetFrameData N+1              N+1 start animating
+  //   update ARCore N to N+1
+  // OnToken N                     N processing->rendering
+  //   draw rendered N
+  //   swap                        N rendering done
+  // SubmitFrame N+1               N+1 animating->processing
+  //   draw camera N+1
+  //   waitForToken
+  std::unique_ptr<vr::WebXrPresentationState> webxr_;
+
   // Default dummy values to ensure consistent behaviour.
   gfx::Size transfer_size_ = gfx::Size(0, 0);
   display::Display::Rotation display_rotation_ = display::Display::ROTATE_0;
   bool should_update_display_geometry_ = true;
 
   gfx::Transform uv_transform_;
+  gfx::Transform webxr_transform_;
   gfx::Transform projection_;
   // The first run of ProduceFrame should set uv_transform_ and projection_
   // using the default settings in ArCore.
   bool should_recalculate_uvs_ = true;
+  bool have_camera_image_ = false;
 
   bool is_initialized_ = false;
+  bool is_paused_ = true;
 
   bool restrict_frame_data_ = false;
 
@@ -133,6 +187,37 @@
   void OnBindingDisconnect();
   void CloseBindingsIfOpen();
 
+  mojo::Binding<device::mojom::XRPresentationProvider> presentation_binding_;
+  device::mojom::XRPresentationClientPtr submit_client_;
+
+  base::OnceClosure pending_getframedata_;
+
+  mojom::VRDisplayInfoPtr display_info_;
+  bool display_info_changed_ = false;
+
+  std::vector<device::mojom::XRInputSourceStatePtr> input_states_;
+  gfx::PointF screen_last_touch_;
+
+  // Screen touch start/end events get reported asynchronously. We want to
+  // report at least one "clicked" event even if start and end happen within a
+  // single frame. The "active" state corresponds to the current state and is
+  // updated asynchronously. The "pending" state is set to true whenever the
+  // screen is touched, but only gets cleared by the input source handler.
+  //
+  //    active   pending    event
+  //         0         0
+  //         1         1
+  //         1         1    pressed=true (selectstart)
+  //         1         1    pressed=true
+  //         0         1->0 pressed=false clicked=true (selectend, click)
+  //
+  //         0         0
+  //         1         1
+  //         0         1
+  //         0         1->0 pressed=false clicked=true (selectend, click)
+  float screen_touch_pending_ = false;
+  float screen_touch_active_ = false;
+
   // Must be last.
   base::WeakPtrFactory<ArCoreGl> weak_ptr_factory_;
   DISALLOW_COPY_AND_ASSIGN(ArCoreGl);
diff --git a/chrome/browser/android/vr/arcore_device/arcore_install_utils.h b/chrome/browser/android/vr/arcore_device/arcore_install_utils.h
index 58be55ff4..ae16038 100644
--- a/chrome/browser/android/vr/arcore_device/arcore_install_utils.h
+++ b/chrome/browser/android/vr/arcore_device/arcore_install_utils.h
@@ -7,9 +7,34 @@
 
 #include "base/android/scoped_java_ref.h"
 #include "base/memory/weak_ptr.h"
+#include "ui/display/display.h"
+#include "ui/gfx/geometry/point_f.h"
+#include "ui/gfx/geometry/size.h"
+#include "ui/gfx/native_widget_types.h"
 
 namespace vr {
 
+// Immersive AR sessions use callbacks in the following sequence:
+//
+// RequestArSession
+// [show consent prompt]
+// if consent declined, or if camera permission refused after consent:
+//   DestroyedCallback
+//
+// if accepted:
+//   SurfaceReadyCallback
+//   SurfaceTouchCallback (repeated for each touch)
+//   [exit session via "back" button, or via JS session exit]
+//   DestroyedCallback
+//
+using SurfaceReadyCallback =
+    base::RepeatingCallback<void(gfx::AcceleratedWidget window,
+                                 display::Display::Rotation rotation,
+                                 const gfx::Size& size)>;
+using SurfaceTouchCallback =
+    base::RepeatingCallback<void(bool touching, const gfx::PointF& location)>;
+using SurfaceDestroyedCallback = base::OnceClosure;
+
 class ArCoreInstallUtils {
  public:
   virtual ~ArCoreInstallUtils() = default;
@@ -25,6 +50,13 @@
   virtual bool EnsureLoaded() = 0;
   virtual base::android::ScopedJavaLocalRef<jobject>
   GetApplicationContext() = 0;
+  virtual void RequestArSession(
+      int render_process_id,
+      int render_frame_id,
+      SurfaceReadyCallback ready_callback,
+      SurfaceTouchCallback touch_callback,
+      SurfaceDestroyedCallback destroyed_callback) = 0;
+  virtual void DestroyDrawingSurface() = 0;
 };
 
 }  // namespace vr
diff --git a/chrome/browser/android/vr/arcore_device/arcore_java_utils.cc b/chrome/browser/android/vr/arcore_device/arcore_java_utils.cc
index c461e2d..eb9d09d 100644
--- a/chrome/browser/android/vr/arcore_device/arcore_java_utils.cc
+++ b/chrome/browser/android/vr/arcore_device/arcore_java_utils.cc
@@ -98,6 +98,64 @@
       getTabFromRenderer(render_process_id, render_frame_id));
 }
 
+void ArCoreJavaUtils::RequestArSession(
+    int render_process_id,
+    int render_frame_id,
+    SurfaceReadyCallback ready_callback,
+    SurfaceTouchCallback touch_callback,
+    SurfaceDestroyedCallback destroyed_callback) {
+  DVLOG(1) << __func__;
+  JNIEnv* env = AttachCurrentThread();
+
+  Java_ArCoreJavaUtils_launchArConsentDialog(
+      env, j_arcore_java_utils_,
+      getTabFromRenderer(render_process_id, render_frame_id));
+
+  surface_ready_callback_ = std::move(ready_callback);
+  surface_touch_callback_ = std::move(touch_callback);
+  surface_destroyed_callback_ = std::move(destroyed_callback);
+}
+
+void ArCoreJavaUtils::DestroyDrawingSurface() {
+  DVLOG(1) << __func__;
+  JNIEnv* env = AttachCurrentThread();
+
+  Java_ArCoreJavaUtils_destroyArImmersiveOverlay(env, j_arcore_java_utils_);
+}
+
+void ArCoreJavaUtils::OnDrawingSurfaceReady(
+    JNIEnv* env,
+    const base::android::JavaParamRef<jobject>& obj,
+    const base::android::JavaParamRef<jobject>& surface,
+    int rotation,
+    int width,
+    int height) {
+  DVLOG(1) << __func__ << ": width=" << width << " height=" << height
+           << " rotation=" << rotation;
+  gfx::AcceleratedWidget window =
+      ANativeWindow_fromSurface(base::android::AttachCurrentThread(), surface);
+  display::Display::Rotation display_rotation =
+      static_cast<display::Display::Rotation>(rotation);
+  surface_ready_callback_.Run(window, display_rotation, {width, height});
+}
+
+void ArCoreJavaUtils::OnDrawingSurfaceTouch(
+    JNIEnv* env,
+    const base::android::JavaParamRef<jobject>& obj,
+    bool touching,
+    float x,
+    float y) {
+  DVLOG(3) << __func__ << ": touching=" << touching;
+  surface_touch_callback_.Run(touching, {x, y});
+}
+
+void ArCoreJavaUtils::OnDrawingSurfaceDestroyed(
+    JNIEnv* env,
+    const base::android::JavaParamRef<jobject>& obj) {
+  DVLOG(1) << __func__ << ":::";
+  base::ResetAndReturn(&surface_destroyed_callback_).Run();
+}
+
 void ArCoreJavaUtils::OnRequestInstallArModuleResult(
     JNIEnv* env,
     const base::android::JavaParamRef<jobject>& obj,
diff --git a/chrome/browser/android/vr/arcore_device/arcore_java_utils.h b/chrome/browser/android/vr/arcore_device/arcore_java_utils.h
index 5920f64..5a5afe5 100644
--- a/chrome/browser/android/vr/arcore_device/arcore_java_utils.h
+++ b/chrome/browser/android/vr/arcore_device/arcore_java_utils.h
@@ -5,7 +5,9 @@
 #ifndef CHROME_BROWSER_ANDROID_VR_ARCORE_DEVICE_ARCORE_JAVA_UTILS_H_
 #define CHROME_BROWSER_ANDROID_VR_ARCORE_DEVICE_ARCORE_JAVA_UTILS_H_
 
+#include <android/native_window_jni.h>
 #include <jni.h>
+
 #include "base/android/scoped_java_ref.h"
 #include "base/callback.h"
 #include "base/memory/weak_ptr.h"
@@ -26,6 +28,12 @@
   bool ShouldRequestInstallSupportedArCore() override;
   void RequestInstallSupportedArCore(int render_process_id,
                                      int render_frame_id) override;
+  void RequestArSession(int render_process_id,
+                        int render_frame_id,
+                        SurfaceReadyCallback ready_callback,
+                        SurfaceTouchCallback touch_callback,
+                        SurfaceDestroyedCallback destroyed_callback) override;
+  void DestroyDrawingSurface() override;
 
   // Methods called from the Java side.
   void OnRequestInstallArModuleResult(
@@ -36,6 +44,21 @@
       JNIEnv* env,
       const base::android::JavaParamRef<jobject>& obj,
       bool success);
+  void OnDrawingSurfaceReady(
+      JNIEnv* env,
+      const base::android::JavaParamRef<jobject>& obj,
+      const base::android::JavaParamRef<jobject>& surface,
+      int rotation,
+      int width,
+      int height);
+  void OnDrawingSurfaceTouch(JNIEnv* env,
+                             const base::android::JavaParamRef<jobject>& obj,
+                             bool touching,
+                             float x,
+                             float y);
+  void OnDrawingSurfaceDestroyed(
+      JNIEnv* env,
+      const base::android::JavaParamRef<jobject>& obj);
 
   bool EnsureLoaded() override;
   base::android::ScopedJavaLocalRef<jobject> GetApplicationContext() override;
@@ -49,6 +72,10 @@
   base::RepeatingCallback<void(bool)> ar_core_installation_callback_;
 
   base::android::ScopedJavaGlobalRef<jobject> j_arcore_java_utils_;
+
+  SurfaceReadyCallback surface_ready_callback_;
+  SurfaceTouchCallback surface_touch_callback_;
+  SurfaceDestroyedCallback surface_destroyed_callback_;
 };
 
 }  // namespace vr
diff --git a/chrome/browser/android/vr/arcore_device/arcore_permission_helper.cc b/chrome/browser/android/vr/arcore_device/arcore_permission_helper.cc
deleted file mode 100644
index 5791ed11..0000000
--- a/chrome/browser/android/vr/arcore_device/arcore_permission_helper.cc
+++ /dev/null
@@ -1,100 +0,0 @@
-// Copyright 2018 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/android/vr/arcore_device/arcore_permission_helper.h"
-
-#include "base/bind.h"
-#include "chrome/browser/permissions/permission_manager.h"
-#include "chrome/browser/permissions/permission_result.h"
-#include "chrome/browser/permissions/permission_update_infobar_delegate_android.h"
-#include "chrome/browser/profiles/profile.h"
-#include "components/content_settings/core/common/content_settings_types.h"
-#include "content/public/browser/render_frame_host.h"
-#include "content/public/browser/web_contents.h"
-
-namespace device {
-
-ArCorePermissionHelper::ArCorePermissionHelper() : weak_ptr_factory_(this) {}
-
-ArCorePermissionHelper::~ArCorePermissionHelper() {}
-
-void ArCorePermissionHelper::RequestCameraPermission(
-    int render_process_id,
-    int render_frame_id,
-    bool has_user_activation,
-    base::OnceCallback<void(bool)> callback) {
-  content::RenderFrameHost* rfh =
-      content::RenderFrameHost::FromID(render_process_id, render_frame_id);
-
-  DCHECK(rfh);
-  // The RFH may have been destroyed by the time the request is processed.
-  // We have to do a runtime check in addition to the DCHECK as it doesn't
-  // trigger in release.
-  if (!rfh) {
-    DLOG(ERROR) << "The RenderFrameHost was destroyed prior to permission";
-    std::move(callback).Run(false);
-  }
-
-  content::WebContents* web_contents =
-      content::WebContents::FromRenderFrameHost(rfh);
-  DCHECK(web_contents);
-
-  Profile* profile =
-      Profile::FromBrowserContext(web_contents->GetBrowserContext());
-
-  PermissionManager* permission_manager = PermissionManager::Get(profile);
-
-  permission_manager->RequestPermission(
-      CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA, rfh, web_contents->GetURL(),
-      has_user_activation,
-      base::BindRepeating(
-          &ArCorePermissionHelper::OnRequestCameraPermissionResult,
-          GetWeakPtr(), web_contents, base::Passed(&callback)));
-}
-
-void ArCorePermissionHelper::OnRequestCameraPermissionResult(
-    content::WebContents* web_contents,
-    base::OnceCallback<void(bool)> callback,
-    ContentSetting content_setting) {
-  // If the camera permission is not allowed, abort the request.
-  if (content_setting != CONTENT_SETTING_ALLOW) {
-    std::move(callback).Run(false);
-    return;
-  }
-
-  // Even if the content setting stated that the camera access is allowed,
-  // the Android camera permission might still need to be requested, so check
-  // if the OS level permission infobar should be shown.
-  std::vector<ContentSettingsType> content_settings_types;
-  content_settings_types.push_back(CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA);
-  ShowPermissionInfoBarState show_permission_info_bar_state =
-      PermissionUpdateInfoBarDelegate::ShouldShowPermissionInfoBar(
-          web_contents, content_settings_types);
-  switch (show_permission_info_bar_state) {
-    case ShowPermissionInfoBarState::NO_NEED_TO_SHOW_PERMISSION_INFOBAR:
-      std::move(callback).Run(true);
-      return;
-    case ShowPermissionInfoBarState::SHOW_PERMISSION_INFOBAR:
-      // Show the Android camera permission info bar.
-      PermissionUpdateInfoBarDelegate::Create(
-          web_contents, content_settings_types,
-          base::BindOnce(
-              &ArCorePermissionHelper::OnRequestAndroidCameraPermissionResult,
-              GetWeakPtr(), base::Passed(&callback)));
-      return;
-    case ShowPermissionInfoBarState::CANNOT_SHOW_PERMISSION_INFOBAR:
-      std::move(callback).Run(false);
-      return;
-  }
-
-  NOTREACHED() << "Unknown show permission infobar state.";
-}
-
-void ArCorePermissionHelper::OnRequestAndroidCameraPermissionResult(
-    base::OnceCallback<void(bool)> callback,
-    bool was_android_camera_permission_granted) {
-  std::move(callback).Run(was_android_camera_permission_granted);
-}
-
-}  // namespace device
diff --git a/chrome/browser/android/vr/arcore_device/arcore_permission_helper.h b/chrome/browser/android/vr/arcore_device/arcore_permission_helper.h
deleted file mode 100644
index 20336de..0000000
--- a/chrome/browser/android/vr/arcore_device/arcore_permission_helper.h
+++ /dev/null
@@ -1,53 +0,0 @@
-// Copyright 2018 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_ANDROID_VR_ARCORE_DEVICE_ARCORE_PERMISSION_HELPER_H_
-#define CHROME_BROWSER_ANDROID_VR_ARCORE_DEVICE_ARCORE_PERMISSION_HELPER_H_
-
-#include <memory>
-#include "base/android/java_handler_thread.h"
-#include "base/macros.h"
-#include "base/memory/weak_ptr.h"
-#include "base/single_thread_task_runner.h"
-#include "chrome/browser/android/vr/mailbox_to_surface_bridge.h"
-#include "components/content_settings/core/common/content_settings.h"
-
-namespace content {
-class WebContents;
-}  // namespace content
-
-namespace device {
-
-class ArCorePermissionHelper {
- public:
-  ArCorePermissionHelper();
-  virtual ~ArCorePermissionHelper();
-
-  virtual void RequestCameraPermission(int render_process_id,
-                                       int render_frame_id,
-                                       bool has_user_activation,
-                                       base::OnceCallback<void(bool)> callback);
-
-  virtual void OnRequestCameraPermissionResult(
-      content::WebContents* web_contents,
-      base::OnceCallback<void(bool)> callback,
-      ContentSetting content_setting);
-
-  virtual void OnRequestAndroidCameraPermissionResult(
-      base::OnceCallback<void(bool)> callback,
-      bool was_android_camera_permission_granted);
-
- private:
-  base::WeakPtr<ArCorePermissionHelper> GetWeakPtr() {
-    return weak_ptr_factory_.GetWeakPtr();
-  }
-
-  // Must be last.
-  base::WeakPtrFactory<ArCorePermissionHelper> weak_ptr_factory_;
-  DISALLOW_COPY_AND_ASSIGN(ArCorePermissionHelper);
-};
-
-}  // namespace device
-
-#endif  // CHROME_BROWSER_ANDROID_VR_ARCORE_DEVICE_ARCORE_PERMISSION_HELPER_H_
diff --git a/chrome/browser/autofill/captured_sites_test_utils.cc b/chrome/browser/autofill/captured_sites_test_utils.cc
index a98694a..9068a74 100644
--- a/chrome/browser/autofill/captured_sites_test_utils.cc
+++ b/chrome/browser/autofill/captured_sites_test_utils.cc
@@ -426,7 +426,6 @@
                                                   .AppendASCII("catapult")
                                                   .AppendASCII("telemetry")
                                                   .AppendASCII("telemetry")
-                                                  .AppendASCII("internal")
                                                   .AppendASCII("bin");
   options.current_directory = web_page_replay_binary_dir;
 
diff --git a/chrome/browser/chrome_content_browser_client.cc b/chrome/browser/chrome_content_browser_client.cc
index c45606c..f07e481d 100644
--- a/chrome/browser/chrome_content_browser_client.cc
+++ b/chrome/browser/chrome_content_browser_client.cc
@@ -4155,7 +4155,7 @@
 void ChromeContentBrowserClient::OpenURL(
     content::SiteInstance* site_instance,
     const content::OpenURLParams& params,
-    const base::RepeatingCallback<void(content::WebContents*)>& callback) {
+    base::OnceCallback<void(content::WebContents*)> callback) {
   DCHECK_CURRENTLY_ON(BrowserThread::UI);
   DCHECK(ShouldAllowOpenURL(site_instance, params.url));
 
@@ -4163,7 +4163,7 @@
 
 #if defined(OS_ANDROID)
   ServiceTabLauncher::GetInstance()->LaunchTab(browser_context, params,
-                                               callback);
+                                               std::move(callback));
 #else
   NavigateParams nav_params(Profile::FromBrowserContext(browser_context),
                             params.url, params.transition);
@@ -4171,7 +4171,7 @@
   nav_params.user_gesture = params.user_gesture;
 
   Navigate(&nav_params);
-  callback.Run(nav_params.navigated_or_inserted_contents);
+  std::move(callback).Run(nav_params.navigated_or_inserted_contents);
 #endif
 }
 
diff --git a/chrome/browser/chrome_content_browser_client.h b/chrome/browser/chrome_content_browser_client.h
index 7926733..afc5f97 100644
--- a/chrome/browser/chrome_content_browser_client.h
+++ b/chrome/browser/chrome_content_browser_client.h
@@ -429,10 +429,10 @@
       base::StringPiece name) override;
   std::vector<service_manager::Manifest> GetExtraServiceManifests() override;
   std::vector<std::string> GetStartupServices() override;
-  void OpenURL(content::SiteInstance* site_instance,
-               const content::OpenURLParams& params,
-               const base::RepeatingCallback<void(content::WebContents*)>&
-                   callback) override;
+  void OpenURL(
+      content::SiteInstance* site_instance,
+      const content::OpenURLParams& params,
+      base::OnceCallback<void(content::WebContents*)> callback) override;
   content::ControllerPresentationServiceDelegate*
   GetControllerPresentationServiceDelegate(
       content::WebContents* web_contents) override;
diff --git a/chrome/browser/chromeos/login/quick_unlock/quick_unlock_storage_unittest.cc b/chrome/browser/chromeos/login/quick_unlock/quick_unlock_storage_unittest.cc
index 89d9d59..8f52880 100644
--- a/chrome/browser/chromeos/login/quick_unlock/quick_unlock_storage_unittest.cc
+++ b/chrome/browser/chromeos/login/quick_unlock/quick_unlock_storage_unittest.cc
@@ -113,7 +113,7 @@
   PrefService* pref_service = profile_->GetPrefs();
   QuickUnlockStorageTestApi test_api(quick_unlock_storage);
 
-  // The default is one day, so verify moving the last strong auth time back 12
+  // The default is two days, so verify moving the last strong auth time back 24
   // hours(half of the expiration time) should not request strong auth.
   quick_unlock_storage->MarkStrongAuth();
   base::TimeDelta expiration_time = GetExpirationTime(pref_service);
diff --git a/chrome/browser/chromeos/login/quick_unlock/quick_unlock_utils.cc b/chrome/browser/chromeos/login/quick_unlock/quick_unlock_utils.cc
index 2766e73..85afc3b 100644
--- a/chrome/browser/chromeos/login/quick_unlock/quick_unlock_utils.cc
+++ b/chrome/browser/chromeos/login/quick_unlock/quick_unlock_utils.cc
@@ -63,8 +63,8 @@
       return base::TimeDelta::FromHours(6);
     case PasswordConfirmationFrequency::TWELVE_HOURS:
       return base::TimeDelta::FromHours(12);
-    case PasswordConfirmationFrequency::DAY:
-      return base::TimeDelta::FromDays(1);
+    case PasswordConfirmationFrequency::TWO_DAYS:
+      return base::TimeDelta::FromDays(2);
     case PasswordConfirmationFrequency::WEEK:
       return base::TimeDelta::FromDays(7);
   }
@@ -80,7 +80,7 @@
       base::Value(std::move(quick_unlock_whitelist_default)));
   registry->RegisterIntegerPref(
       prefs::kQuickUnlockTimeout,
-      static_cast<int>(PasswordConfirmationFrequency::DAY));
+      static_cast<int>(PasswordConfirmationFrequency::TWO_DAYS));
 
   // Preferences related the lock screen pin unlock.
   registry->RegisterIntegerPref(prefs::kPinUnlockMinimumLength,
diff --git a/chrome/browser/chromeos/login/quick_unlock/quick_unlock_utils.h b/chrome/browser/chromeos/login/quick_unlock/quick_unlock_utils.h
index ffa95296..9c2581a 100644
--- a/chrome/browser/chromeos/login/quick_unlock/quick_unlock_utils.h
+++ b/chrome/browser/chromeos/login/quick_unlock/quick_unlock_utils.h
@@ -23,7 +23,7 @@
 enum class PasswordConfirmationFrequency {
   SIX_HOURS = 0,
   TWELVE_HOURS = 1,
-  DAY = 2,
+  TWO_DAYS = 2,
   WEEK = 3
 };
 
diff --git a/chrome/browser/chromeos/login/session/user_session_manager.cc b/chrome/browser/chromeos/login/session/user_session_manager.cc
index c0a62d6..38f0461 100644
--- a/chrome/browser/chromeos/login/session/user_session_manager.cc
+++ b/chrome/browser/chromeos/login/session/user_session_manager.cc
@@ -102,6 +102,7 @@
 #include "chrome/browser/ui/startup/startup_browser_creator.h"
 #include "chrome/browser/ui/webui/chromeos/login/discover/discover_manager.h"
 #include "chrome/browser/ui/webui/chromeos/login/discover/modules/discover_module_pin_setup.h"
+#include "chrome/browser/ui/zoom/chrome_zoom_level_prefs.h"
 #include "chrome/common/channel_info.h"
 #include "chrome/common/chrome_features.h"
 #include "chrome/common/chrome_switches.h"
@@ -146,6 +147,7 @@
 #include "content/public/browser/notification_service.h"
 #include "content/public/browser/storage_partition.h"
 #include "content/public/common/content_switches.h"
+#include "content/public/common/page_zoom.h"
 #include "extensions/common/features/feature_session_type.h"
 #include "rlz/buildflags/buildflags.h"
 #include "services/identity/public/cpp/accounts_mutator.h"
@@ -1485,6 +1487,14 @@
 }
 
 void UserSessionManager::FinalizePrepareProfile(Profile* profile) {
+  // Record each user's "Page zoom" setting for https://crbug.com/955071.
+  // This can be removed after M79.
+  double zoom_level = profile->GetZoomLevelPrefs()->GetDefaultZoomLevelPref();
+  double zoom_factor = content::ZoomLevelToZoomFactor(zoom_level);
+  int zoom_percent = std::floor(zoom_factor * 100);
+  // Zoom can be greater than 100%.
+  UMA_HISTOGRAM_COUNTS_1000("Login.DefaultPageZoom", zoom_percent);
+
   BootTimesRecorder::Get()->AddLoginTimeMarker("TPMOwn-End", false);
 
   user_manager::UserManager* user_manager = user_manager::UserManager::Get();
diff --git a/chrome/browser/extensions/extension_unload_browsertest.cc b/chrome/browser/extensions/extension_unload_browsertest.cc
index 45dfa255..687a51d 100644
--- a/chrome/browser/extensions/extension_unload_browsertest.cc
+++ b/chrome/browser/extensions/extension_unload_browsertest.cc
@@ -49,10 +49,6 @@
 // After an extension is uninstalled, network requests from its content scripts
 // should fail but not kill the renderer process.
 IN_PROC_BROWSER_TEST_F(ExtensionUnloadBrowserTest, UnloadWithContentScripts) {
-  // https://crbug.com/862176
-  if (base::FeatureList::IsEnabled(network::features::kNetworkService))
-    return;
-
   ASSERT_TRUE(embedded_test_server()->Start());
 
   // Load an extension with a content script that has a button to send XHRs.
diff --git a/chrome/browser/extensions/service_worker_apitest.cc b/chrome/browser/extensions/service_worker_apitest.cc
index e3d7ca4..9391ad5e 100644
--- a/chrome/browser/extensions/service_worker_apitest.cc
+++ b/chrome/browser/extensions/service_worker_apitest.cc
@@ -15,6 +15,7 @@
 #include "base/test/bind_test_util.h"
 #include "base/threading/thread_restrictions.h"
 #include "build/build_config.h"
+#include "chrome/browser/extensions/crx_installer.h"
 #include "chrome/browser/extensions/extension_apitest.h"
 #include "chrome/browser/extensions/extension_service.h"
 #include "chrome/browser/extensions/lazy_background_page_test_util.h"
@@ -58,6 +59,7 @@
 #include "extensions/browser/service_worker_task_queue.h"
 #include "extensions/common/api/test.h"
 #include "extensions/common/value_builder.h"
+#include "extensions/common/verifier_formats.h"
 #include "extensions/test/background_page_watcher.h"
 #include "extensions/test/extension_test_message_listener.h"
 #include "extensions/test/result_catcher.h"
@@ -1135,6 +1137,76 @@
   EXPECT_EQ(starting_tab_count, browser()->tab_strip_model()->count());
 }
 
+// Tests that updating a packed extension with modified scripts works
+// properly -- we expect that the new script will execute, rather than the
+// previous one.
+IN_PROC_BROWSER_TEST_F(ServiceWorkerTest, UpdatePackedExtension) {
+  // Extensions APIs from SW are only enabled on trunk.
+  ScopedCurrentChannel current_channel_override(version_info::Channel::UNKNOWN);
+  constexpr char kManifest1[] =
+      R"({
+           "name": "Test Extension",
+           "manifest_version": 2,
+           "version": "0.1",
+           "background": {"service_worker": "script.js"}
+         })";
+  // This script installs an event listener for updates to the extension with
+  // a callback that forces itself to reload.
+  constexpr char kScript[] =
+      R"(
+         chrome.runtime.onUpdateAvailable.addListener(function(details) {
+           chrome.runtime.reload();
+         });
+         chrome.test.sendMessage('ready1');
+        )";
+
+  std::string id;
+  TestExtensionDir test_dir;
+
+  // Write the manifest and script files and load the extension.
+  test_dir.WriteManifest(kManifest1);
+  test_dir.WriteFile(FILE_PATH_LITERAL("script.js"), kScript);
+
+  {
+    ExtensionTestMessageListener ready_listener("ready1", false);
+    base::FilePath path = test_dir.Pack();
+    const Extension* extension = LoadExtension(path);
+    ASSERT_TRUE(extension);
+
+    EXPECT_TRUE(ready_listener.WaitUntilSatisfied());
+    id = extension->id();
+  }
+
+  constexpr char kManifest2[] =
+      R"({
+           "name": "Test Extension",
+           "manifest_version": 2,
+           "version": "0.2",
+           "background": {"service_worker": "script.js"}
+         })";
+  // Rewrite the manifest and script files with a version change in the manifest
+  // file. After reloading the extension, the old version of the extension
+  // should detect the update, force the reload, and the new script should
+  // execute.
+  test_dir.WriteManifest(kManifest2);
+  test_dir.WriteFile(FILE_PATH_LITERAL("script.js"),
+                     "chrome.test.sendMessage('ready2');");
+  {
+    ExtensionTestMessageListener ready_listener("ready2", false);
+    base::FilePath path = test_dir.Pack();
+    ExtensionService* const extension_service =
+        ExtensionSystem::Get(profile())->extension_service();
+    EXPECT_TRUE(extension_service->UpdateExtension(
+        CRXFileInfo(id, GetTestVerifierFormat(), path), true, nullptr));
+    EXPECT_TRUE(ready_listener.WaitUntilSatisfied());
+    EXPECT_EQ("0.2", ExtensionRegistry::Get(profile())
+                         ->enabled_extensions()
+                         .GetByID(id)
+                         ->version()
+                         .GetString());
+  }
+}
+
 // Tests that updating an unpacked extension with modified scripts works
 // properly -- we expect that the new script will execute, rather than the
 // previous one.
diff --git a/chrome/browser/flag-metadata.json b/chrome/browser/flag-metadata.json
index c95018233..c19ce9b 100644
--- a/chrome/browser/flag-metadata.json
+++ b/chrome/browser/flag-metadata.json
@@ -1716,6 +1716,11 @@
     "expiry_milestone": 80
   },
   {
+    "name": "enable-tab-groups-ui-improvements",
+    "owners": [ "memex-team@google.com" ],
+    "expiry_milestone": 80
+  },
+  {
     "name": "enable-tab-switcher-on-return",
     "owners": [ "memex-team@google.com" ],
     "expiry_milestone": 76
@@ -2590,7 +2595,12 @@
   },
   {
     "name": "omnibox-ui-vertical-margin",
-    "owners": [ "chrome-omnibox-team@google.com" ],
+    "owners": [ "tommycli", "chrome-omnibox-team@google.com" ],
+    "expiry_milestone": 80
+  },
+  {
+    "name": "omnibox-ui-vertical-margin-limit-to-non-touch-only",
+    "owners": [ "tommycli", "chrome-omnibox-team@google.com" ],
     "expiry_milestone": 80
   },
   {
diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc
index d8197e9..e935e0f 100644
--- a/chrome/browser/flag_descriptions.cc
+++ b/chrome/browser/flag_descriptions.cc
@@ -1423,6 +1423,12 @@
 const char kOmniboxUIVerticalMarginDescription[] =
     "Changes the vertical margin in the Omnibox UI.";
 
+const char kOmniboxUIVerticalMarginLimitToNonTouchOnlyName[] =
+    "Omnibox UI Vertical Margin - Limit to Non-Touch Only";
+const char kOmniboxUIVerticalMarginLimitToNonTouchOnlyDescription[] =
+    "Limits the vertical margin UI experiment to non-touch devices only. Has "
+    "no effect if the Omnibox Vertical Margin experiment is not enabled.";
+
 const char kOmniboxUIWhiteBackgroundOnBlurName[] =
     "Omnibox UI White Background On Blur";
 const char kOmniboxUIWhiteBackgroundOnBlurDescription[] =
@@ -1845,6 +1851,10 @@
 const char kTabGroupsAndroidDescription[] =
     "Allows users to create groups to better organize their tabs.";
 
+const char kTabGroupsUiImprovementsAndroidName[] = "Tab Groups UI Improvements";
+const char kTabGroupsUiImprovementsAndroidDescription[] =
+    "Allows users to access new features in Tab Group UI.";
+
 const char kTabGroupsName[] = "Tab Groups";
 const char kTabGroupsDescription[] =
     "Allows users to organize tabs into visually distinct groups, e.g. to "
diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h
index 84c8694..8450a5b7 100644
--- a/chrome/browser/flag_descriptions.h
+++ b/chrome/browser/flag_descriptions.h
@@ -866,6 +866,9 @@
 extern const char kOmniboxUIVerticalMarginName[];
 extern const char kOmniboxUIVerticalMarginDescription[];
 
+extern const char kOmniboxUIVerticalMarginLimitToNonTouchOnlyName[];
+extern const char kOmniboxUIVerticalMarginLimitToNonTouchOnlyDescription[];
+
 extern const char kOmniboxUIWhiteBackgroundOnBlurName[];
 extern const char kOmniboxUIWhiteBackgroundOnBlurDescription[];
 
@@ -1101,6 +1104,9 @@
 extern const char kTabGroupsAndroidName[];
 extern const char kTabGroupsAndroidDescription[];
 
+extern const char kTabGroupsUiImprovementsAndroidName[];
+extern const char kTabGroupsUiImprovementsAndroidDescription[];
+
 extern const char kTabGroupsName[];
 extern const char kTabGroupsDescription[];
 
diff --git a/chrome/browser/metrics/process_memory_metrics_emitter_browsertest.cc b/chrome/browser/metrics/process_memory_metrics_emitter_browsertest.cc
index d9984262..f28fd867 100644
--- a/chrome/browser/metrics/process_memory_metrics_emitter_browsertest.cc
+++ b/chrome/browser/metrics/process_memory_metrics_emitter_browsertest.cc
@@ -775,9 +775,10 @@
 }
 
 // Test is flaky on chromeos and linux. https://crbug.com/938054.
-// Test is flaky on mac: https://crbug.com/948674.
-#if defined(ADDRESS_SANITIZER) || defined(MEMORY_SANITIZER) || \
-    defined(OS_CHROMEOS) || defined(OS_LINUX) || defined(OS_MACOSX)
+// Test is flaky on mac and win: https://crbug.com/948674.
+#if defined(ADDRESS_SANITIZER) || defined(MEMORY_SANITIZER) ||         \
+    defined(OS_CHROMEOS) || defined(OS_LINUX) || defined(OS_MACOSX) || \
+    defined(OS_WIN)
 #define MAYBE_ForegroundAndBackgroundPages DISABLED_ForegroundAndBackgroundPages
 #else
 #define MAYBE_ForegroundAndBackgroundPages ForegroundAndBackgroundPages
diff --git a/chrome/browser/resources/chromeos/switch_access/BUILD.gn b/chrome/browser/resources/chromeos/switch_access/BUILD.gn
index 911970a..f14456fb 100644
--- a/chrome/browser/resources/chromeos/switch_access/BUILD.gn
+++ b/chrome/browser/resources/chromeos/switch_access/BUILD.gn
@@ -59,6 +59,7 @@
     "options.html",
     "options.js",
     "preferences.js",
+    "rect_helper.js",
     "switch_access.js",
     "switch_access_constants.js",
     "switch_access_predicate.js",
@@ -168,6 +169,7 @@
     ":navigation_manager",
     ":options",
     ":preferences",
+    ":rect_helper",
     ":switch_access",
     ":switch_access_constants",
     ":switch_access_interface",
@@ -190,6 +192,7 @@
     ":back_button_manager",
     ":menu_manager",
     ":menu_panel_interface",
+    ":rect_helper",
     ":switch_access_constants",
     ":switch_access_predicate",
     ":text_input_manager",
@@ -263,6 +266,10 @@
   externs_list = [ "$externs_path/chrome_extensions.js" ]
 }
 
+js_library("rect_helper") {
+  externs_list = [ "$externs_path/accessibility_private.js" ]
+}
+
 js_library("switch_access") {
   deps = [
     ":auto_scan_manager",
diff --git a/chrome/browser/resources/chromeos/switch_access/back_button_manager.js b/chrome/browser/resources/chromeos/switch_access/back_button_manager.js
index ef3fc82..6647e02b 100644
--- a/chrome/browser/resources/chromeos/switch_access/back_button_manager.js
+++ b/chrome/browser/resources/chromeos/switch_access/back_button_manager.js
@@ -30,14 +30,14 @@
   /**
    * Shows the menu as just a back button in the upper right corner of the
    * node.
-   * @param {!chrome.automation.AutomationNode} node
+   * @param {chrome.accessibilityPrivate.ScreenRect=} nodeLocation
    */
-  show(node) {
-    if (!node.location)
+  show(nodeLocation) {
+    if (!nodeLocation)
       return;
     this.backButtonOpen_ = true;
     chrome.accessibilityPrivate.setSwitchAccessMenuState(
-        true, node.location, 0 /* num_actions */);
+        true, nodeLocation, 0 /* num_actions */);
     this.menuPanel_.setFocusRing(SAConstants.BACK_ID, true);
   }
 
diff --git a/chrome/browser/resources/chromeos/switch_access/manifest.json.jinja2 b/chrome/browser/resources/chromeos/switch_access/manifest.json.jinja2
index 5301ca3..418712a8 100644
--- a/chrome/browser/resources/chromeos/switch_access/manifest.json.jinja2
+++ b/chrome/browser/resources/chromeos/switch_access/manifest.json.jinja2
@@ -20,6 +20,7 @@
       "menu_manager.js",
       "navigation_manager.js",
       "preferences.js",
+      "rect_helper.js",
       "switch_access.js",
       "switch_access_constants.js",
       "switch_access_predicate.js",
diff --git a/chrome/browser/resources/chromeos/switch_access/menu_panel.css b/chrome/browser/resources/chromeos/switch_access/menu_panel.css
index b16ae4b0..cceefe8 100644
--- a/chrome/browser/resources/chromeos/switch_access/menu_panel.css
+++ b/chrome/browser/resources/chromeos/switch_access/menu_panel.css
@@ -37,7 +37,7 @@
 }
 .focus,
 #back.focus {
-  outline: solid 3px rgb(138, 180, 248);  /** GB300 from SAConstants */
+  outline: solid 3px rgb(26, 115, 232);  /** Focus color from SAConstants */
 }
 img {
   display: block;
diff --git a/chrome/browser/resources/chromeos/switch_access/navigation_manager.js b/chrome/browser/resources/chromeos/switch_access/navigation_manager.js
index 48f8fad..835beb22 100644
--- a/chrome/browser/resources/chromeos/switch_access/navigation_manager.js
+++ b/chrome/browser/resources/chromeos/switch_access/navigation_manager.js
@@ -88,6 +88,12 @@
       secondaryColor: SAConstants.Focus.SECONDARY_COLOR
     };
 
+    /**
+     * The currently highlighted scope object. Tracked for comparison purposes.
+     * @private {chrome.automation.AutomationNode}
+     */
+    this.focusedScope_;
+
     this.init_();
   }
 
@@ -524,16 +530,23 @@
 
   /**
    * Set the focus ring for the current node and scope.
-   * @param {chrome.accessibilityPrivate.ScreenRect=} opt_focusRect Optionally
-   *     set where the focus should be. Prevents back button from being shown.
    * @private
    */
-  updateFocusRings_(opt_focusRect) {
+  updateFocusRings_() {
+    const focusRect = this.node_.location;
+    // If the scope element has not changed, we want to use the previously
+    // calculated rect as the current scope rect.
+    let scopeRect = this.scope_ === this.focusedScope_ ?
+        this.scopeFocusRing_.rects[0] :
+        this.scope_.location;
+    this.focusedScope_ = this.scope_;
+
     if (this.node_ === this.backButtonManager_.buttonNode()) {
-      this.backButtonManager_.show(this.scope_);
+      this.backButtonManager_.show(scopeRect);
 
       this.primaryFocusRing_.rects = [];
-      this.scopeFocusRing_.rects = [this.scope_.location];
+      this.scopeFocusRing_.rects = [scopeRect];
+
       chrome.accessibilityPrivate.setFocusRings(
           [this.primaryFocusRing_, this.scopeFocusRing_]);
 
@@ -541,11 +554,10 @@
     }
     this.backButtonManager_.hide();
 
-    const focusRect = opt_focusRect || this.node_.location;
-    const scopeRect = this.scope_.location;
-
-    // TODO(anastasi): Make adjustments to scope rect so it draws entirely
-    // outside the focus rect.
+    // If the current element is not the back button, the scope rect should
+    // expand to contain the focus rect.
+    scopeRect = RectHelper.expandToFitWithPadding(
+        SAConstants.Focus.SCOPE_BUFFER, scopeRect, focusRect);
 
     this.primaryFocusRing_.rects = [focusRect];
     this.scopeFocusRing_.rects = [scopeRect];
diff --git a/chrome/browser/resources/chromeos/switch_access/rect_helper.js b/chrome/browser/resources/chromeos/switch_access/rect_helper.js
new file mode 100644
index 0000000..0243165
--- /dev/null
+++ b/chrome/browser/resources/chromeos/switch_access/rect_helper.js
@@ -0,0 +1,64 @@
+// 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.
+
+/** A collection of helper functions when dealing with rects. */
+const RectHelper = {
+  /**
+   * Finds the bottom of a rect.
+   * @param {!chrome.accessibilityPrivate.ScreenRect} rect
+   * @return {number}
+   */
+  bottom: (rect) => rect.top + rect.height,
+
+  /**
+   * Finds the right of a rect.
+   * @param {!chrome.accessibilityPrivate.ScreenRect} rect
+   * @return {number}
+   */
+  right: (rect) => rect.left + rect.width,
+
+  /**
+   * Increases the size of |outer| to entirely enclose |inner|, with |padding|
+   * buffer on each side.
+   * @param {number} padding
+   * @param {chrome.accessibilityPrivate.ScreenRect=} outer
+   * @param {chrome.accessibilityPrivate.ScreenRect=} inner
+   * @return {chrome.accessibilityPrivate.ScreenRect|undefined}
+   */
+  expandToFitWithPadding: (padding, outer, inner) => {
+    if (!outer || !inner)
+      return outer;
+
+    let newOuter = RectHelper.deepCopy(outer);
+
+    if (newOuter.top > inner.top - padding) {
+      newOuter.top = inner.top - padding;
+      // The height should be the original bottom point less the new top point.
+      newOuter.height = RectHelper.bottom(outer) - newOuter.top;
+    }
+    if (newOuter.left > inner.left - padding) {
+      newOuter.left = inner.left - padding;
+      // The new width should be the original right point less the new left.
+      newOuter.width = RectHelper.right(outer) - newOuter.left;
+    }
+    if (RectHelper.bottom(newOuter) < RectHelper.bottom(inner) + padding) {
+      newOuter.height = RectHelper.bottom(inner) + padding - newOuter.top;
+    }
+    if (RectHelper.right(newOuter) < RectHelper.right(inner) + padding) {
+      newOuter.width = RectHelper.right(inner) + padding - newOuter.left;
+    }
+
+    return newOuter;
+  },
+
+  /**
+   * @param {!chrome.accessibilityPrivate.ScreenRect} rect
+   * @return {!chrome.accessibilityPrivate.ScreenRect}
+   */
+  deepCopy: (rect) => {
+    const copy = (Object.assign({}, rect));
+    return /** @type {!chrome.accessibilityPrivate.ScreenRect} */ (copy);
+  }
+
+};
diff --git a/chrome/browser/resources/chromeos/switch_access/rect_helper_unittest.gtestjs b/chrome/browser/resources/chromeos/switch_access/rect_helper_unittest.gtestjs
new file mode 100644
index 0000000..5346efe
--- /dev/null
+++ b/chrome/browser/resources/chromeos/switch_access/rect_helper_unittest.gtestjs
@@ -0,0 +1,81 @@
+// 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.
+
+/**
+ * Test fixture for rect_helper.js.
+ * @constructor
+ * @extends {testing.Test}
+ */
+function SwitchAccessRectHelperUnitTest() {
+  testing.Test.call(this);
+}
+
+SwitchAccessRectHelperUnitTest.prototype = {
+  __proto__: testing.Test.prototype,
+
+  /** @override */
+  extraLibraries: [
+    'rect_helper.js',
+  ],
+
+  equal: (rect1, rect2) => rect1.left === rect2.left &&
+                           rect1.top === rect2.top &&
+                           rect1.width === rect2.width &&
+                           rect1.height === rect2.height,
+};
+
+TEST_F('SwitchAccessRectHelperUnitTest',
+       'ExpandToFitWithPadding_OuterContainedInInner', function() {
+  const padding = 5;
+  const inner = {left: 100, top: 100, width: 100, height: 100};
+
+  const outer = {left: 120, top: 120, width: 20, height: 20};
+  let expected = {left: 95, top: 95, width: 110, height: 110};
+
+  assertTrue(this.equal(expected,
+      RectHelper.expandToFitWithPadding(padding, outer, inner)));
+});
+
+TEST_F('SwitchAccessRectHelperUnitTest',
+       'ExpandToFitWithPadding_OuterContainsInner', function() {
+  const padding = 5;
+  const inner = {left: 100, top: 100, width: 100, height: 100};
+  const outer = {left: 50, top: 50, width: 200, height: 200};
+
+  assertTrue(this.equal(outer,
+      RectHelper.expandToFitWithPadding(padding, outer, inner)));
+});
+
+TEST_F('SwitchAccessRectHelperUnitTest', 'ExpandToFitWithPadding_NoOverlap',
+       function() {
+  const padding = 5;
+  const inner = {left: 100, top: 100, width: 100, height: 100};
+  const outer = {left: 10, top: 10, width: 10, height: 10};
+  expected = {left: 10, top: 10, width: 195, height: 195};
+
+  assertTrue(this.equal(expected,
+      RectHelper.expandToFitWithPadding(padding, outer, inner)));
+});
+
+TEST_F('SwitchAccessRectHelperUnitTest', 'ExpandToFitWithPadding_Overlap',
+       function() {
+  const padding = 5;
+  const inner = {left: 100, top: 100, width: 100, height: 100};
+  const outer = {left: 120, top: 50, width: 200, height: 200};
+  expected = {left: 95, top: 50, width: 225, height: 200};
+
+  assertTrue(this.equal(expected,
+      RectHelper.expandToFitWithPadding(padding, outer, inner)));
+});
+
+TEST_F('SwitchAccessRectHelperUnitTest', 'ExpandToFitWithPadding_WithinPadding',
+       function() {
+  const padding = 5;
+  const inner = {left: 100, top: 100, width: 100, height: 100};
+  const outer = {left: 97, top: 95, width: 108, height: 110};
+  expected = {left: 95, top: 95, width: 110, height: 110};
+
+  assertTrue(this.equal(expected,
+      RectHelper.expandToFitWithPadding(padding, outer, inner)));
+});
diff --git a/chrome/browser/resources/chromeos/switch_access/switch_access_constants.js b/chrome/browser/resources/chromeos/switch_access/switch_access_constants.js
index 09571c0..1420d6d9 100644
--- a/chrome/browser/resources/chromeos/switch_access/switch_access_constants.js
+++ b/chrome/browser/resources/chromeos/switch_access/switch_access_constants.js
@@ -46,6 +46,13 @@
 SAConstants.Focus.SCOPE_ID = 'scope';
 
 /**
+ * The buffer (in dip) between the primary focus ring and the scope focus ring.
+ * @type {number}
+ * @const
+ */
+SAConstants.Focus.SCOPE_BUFFER = 2;
+
+/**
  * The ID used for the focus ring around the active text input.
  * @type {string}
  * @const
@@ -57,14 +64,14 @@
  * @type {string}
  * @const
  */
-SAConstants.Focus.PRIMARY_COLOR = '#8ab4f8b8';
+SAConstants.Focus.PRIMARY_COLOR = '#1A73E8FF';
 
 /**
  * The outer color of the focus rings.
  * @type {string}
  * @const
  */
-SAConstants.Focus.SECONDARY_COLOR = '#0003';
+SAConstants.Focus.SECONDARY_COLOR = '#0006';
 
 /**
  * The amount of space (in px) needed to fit a focus ring around an element.
diff --git a/chrome/browser/resources/chromeos/switch_access/switch_access_predicate.js b/chrome/browser/resources/chromeos/switch_access/switch_access_predicate.js
index e0a8e97..a17d2c0 100644
--- a/chrome/browser/resources/chromeos/switch_access/switch_access_predicate.js
+++ b/chrome/browser/resources/chromeos/switch_access/switch_access_predicate.js
@@ -60,7 +60,7 @@
     }
 
     // Check various indicators that the node is actionable.
-    if (role === RoleType.BUTTON)
+    if (role === RoleType.BUTTON || role === RoleType.SLIDER)
       return true;
 
     if (SwitchAccessPredicate.isTextInput(node))
diff --git a/chrome/browser/resources/chromeos/switch_access/switch_access_predicate_test.extjs b/chrome/browser/resources/chromeos/switch_access/switch_access_predicate_test.extjs
index 4b0d0bfa..73ceedc 100644
--- a/chrome/browser/resources/chromeos/switch_access/switch_access_predicate_test.extjs
+++ b/chrome/browser/resources/chromeos/switch_access/switch_access_predicate_test.extjs
@@ -169,6 +169,7 @@
       '<a href="https://www.google.com/" aria-label="link1">link1</a>' +
       '<input type="text" aria-label="input1">input1</input>' +
       '<button>button3</button>' +
+      '<input type="range" aria-label="slider" value="5" min="0" max="10">' +
       '<div aria-label="listitem" role="listitem" onclick="2+2"></div>' +
       '<div aria-label="div1"><p>p1</p></div>';
   this.runWithLoadedTree('data:text/html;charset=utf-8,' + treeString,
@@ -202,6 +203,10 @@
         const button3 = this.getNodeByName('button3');
         assertTrue(SwitchAccessPredicate.isActionable(button3));
 
+        // Sliders are generally actionable.
+        const slider = this.getNodeByName('slider');
+        assertTrue(SwitchAccessPredicate.isActionable(slider));
+
         // List items with a default action of click are actionable.
         const listitem = this.getNodeByName('listitem');
         assertTrue(SwitchAccessPredicate.isActionable(listitem));
diff --git a/chrome/browser/resources/discards/graph_doc.js b/chrome/browser/resources/discards/graph_doc.js
index 1ff2ae5..1cfcfe5 100644
--- a/chrome/browser/resources/discards/graph_doc.js
+++ b/chrome/browser/resources/discards/graph_doc.js
@@ -43,11 +43,25 @@
   }
 
   /**
+   * Sets the initial x and y position of this node, also resets
+   * vx and vy.
+   * @param {number} graph_width: Width of the graph view (svg).
+   * @param {number} graph_height: Height of the graph view (svg).
+   */
+  setInitialPosition(graph_width, graph_height) {
+    this.x = graph_width / 2;
+    this.y = this.targetYPosition(graph_height);
+    this.vx = 0;
+    this.vy = 0;
+  }
+
+  /**
    * @param {number} graph_height: Height of the graph view (svg).
    * @return {number}
    */
   targetYPosition(graph_height) {
-    return 0;
+    const bounds = this.allowedYRange(graph_height);
+    return (bounds[0] + bounds[1]) / 2;
   }
 
   /**
@@ -63,8 +77,8 @@
    * @return {!Array<number>}
    */
   allowedYRange(graph_height) {
-    // By default, there is no hard constraint on the y position of a node.
-    return [-Infinity, Infinity];
+    // By default, nodes just need to be in bounds of the graph.
+    return [0, graph_height];
   }
 
   /** @return {number}: The strength of the repulsion force with other nodes. */
@@ -101,11 +115,6 @@
     return this.page.mainFrameUrl.length > 0 ? this.page.mainFrameUrl : 'Page';
   }
 
-  /** override */
-  targetYPosition(graph_height) {
-    return kPageNodesTargetY;
-  }
-
   /** @override */
   targetYPositionStrength() {
     return 10;
@@ -172,11 +181,6 @@
     return `PID: ${this.process.pid.pid}`;
   }
 
-  /** override */
-  targetYPosition(graph_height) {
-    return graph_height - (kProcessNodesYRange / 2);
-  }
-
   /** @return {number} */
   targetYPositionStrength() {
     return 10;
@@ -193,6 +197,40 @@
   }
 }
 
+/**
+ * A force that bounds GraphNodes |allowedYRange| in Y.
+ * @param {number} graph_height
+ */
+function bounding_force(graph_height) {
+  /** @type {!Array<!GraphNode>} */
+  let nodes = [];
+  /** @type {!Array<!Array>} */
+  let bounds = [];
+
+  /** @param {number} alpha */
+  function force(alpha) {
+    const n = nodes.length;
+    for (let i = 0; i < n; ++i) {
+      const bound = bounds[i];
+      const node = nodes[i];
+      const yOld = node.y;
+      const yNew = Math.max(bound[0], Math.min(yOld, bound[1]));
+      if (yOld != yNew) {
+        node.y = yNew;
+        // Zero the velocity of clamped nodes.
+        node.vy = 0;
+      }
+    }
+  }
+
+  /** @param {!Array<!GraphNode>} n */
+  force.initialize = function(n) {
+    nodes = n;
+    bounds = nodes.map(node => node.allowedYRange(graph_height));
+  };
+
+  return force;
+}
 
 class Graph {
   /**
@@ -207,10 +245,13 @@
      */
     this.svg_ = svg;
 
+    /** @private {boolean} */
+    this.wasResized_ = false;
+
     /** @private {number} */
-    this.width_ = 100;
+    this.width_ = 0;
     /** @private {number} */
-    this.height_ = 100;
+    this.height_ = 0;
 
     /** @private {d3.ForceSimulation} */
     this.simulation_ = null;
@@ -339,8 +380,7 @@
   /** @private */
   onTick_() {
     const circles = this.nodeGroup_.selectAll('circle');
-    circles.attr('cx', this.getClampedXPosition_.bind(this))
-        .attr('cy', this.getClampedYPosition_.bind(this));
+    circles.attr('cx', d => d.x).attr('cy', d => d.y);
 
     const lines = this.linkGroup_.selectAll('line');
     lines.attr('x1', d => d.source.x)
@@ -363,6 +403,7 @@
       node.page = page;
     } else {
       node = new PageNode(page);
+      node.setInitialPosition(this.width_, this.height_);
     }
 
     this.nodes_.set(page.id, node);
@@ -382,6 +423,7 @@
       node.frame = frame;
     } else {
       node = new FrameNode(frame);
+      node.setInitialPosition(this.width_, this.height_);
     }
 
     this.nodes_.set(frame.id, node);
@@ -401,6 +443,7 @@
       node.process = process;
     } else {
       node = new ProcessNode(process);
+      node.setInitialPosition(this.width_, this.height_);
     }
 
     this.nodes_.set(process.id, node);
@@ -501,25 +544,6 @@
    * @param {!d3.ForceNode} d The node to position.
    * @private
    */
-  getClampedYPosition_(d) {
-    const range = d.allowedYRange(this.height_);
-    d.y = Math.max(range[0], Math.min(d.y, range[1]));
-    return d.y;
-  }
-
-  /**
-   * @param {!d3.ForceNode} d The node to position.
-   * @private
-   */
-  getClampedXPosition_(d) {
-    d.x = Math.max(10, Math.min(d.x, this.width_ - 10));
-    return d.x;
-  }
-
-  /**
-   * @param {!d3.ForceNode} d The node to position.
-   * @private
-   */
   getTargetYPositionStrength_(d) {
     return d.targetYPositionStrength();
   }
@@ -553,6 +577,20 @@
                        .strength(this.getTargetYPositionStrength_.bind(this));
     this.simulation_.force('x_pos', xForce);
     this.simulation_.force('y_pos', yForce);
+    this.simulation_.force('y_bound', bounding_force(this.height_));
+
+    if (!this.wasResized_) {
+      this.wasResized_ = true;
+
+      // Reinitialize all node positions on first resize.
+      this.nodes_.forEach(
+          node => node.setInitialPosition(this.width_, this.height_));
+
+      // Allow the simulation to settle by running it for a bit.
+      for (let i = 0; i < 200; ++i) {
+        this.simulation_.tick();
+      }
+    }
 
     this.restartSimulation_();
   }
diff --git a/chrome/browser/themes/theme_service.cc b/chrome/browser/themes/theme_service.cc
index b9acd99..03204f05 100644
--- a/chrome/browser/themes/theme_service.cc
+++ b/chrome/browser/themes/theme_service.cc
@@ -314,6 +314,8 @@
 // the "Use Classic" button.
 const char ThemeService::kDefaultThemeID[] = "";
 
+const char ThemeService::kAutogeneratedThemeID[] = "autogenerated_theme_id";
+
 ThemeService::ThemeService()
     : ready_(false),
       rb_(ui::ResourceBundle::GetSharedInstance()),
@@ -450,14 +452,18 @@
 
 bool ThemeService::UsingDefaultTheme() const {
   std::string id = GetThemeID();
-  return id == ThemeService::kDefaultThemeID ||
-      id == kDefaultThemeGalleryID;
+  return id == kDefaultThemeID || id == kDefaultThemeGalleryID;
 }
 
 bool ThemeService::UsingSystemTheme() const {
   return UsingDefaultTheme();
 }
 
+bool ThemeService::UsingExtensionTheme() const {
+  return get_theme_supplier() && get_theme_supplier()->get_theme_type() ==
+                                     CustomThemeSupplier::ThemeType::EXTENSION;
+}
+
 std::string ThemeService::GetThemeID() const {
   return profile_->GetPrefs()->GetString(prefs::kCurrentThemeID);
 }
@@ -553,7 +559,12 @@
 }
 
 bool ThemeService::UsingAutogenerated() const {
-  return profile_->GetPrefs()->HasPrefPath(prefs::kAutogeneratedThemeColor);
+  bool autogenerated =
+      get_theme_supplier() && get_theme_supplier()->get_theme_type() ==
+                                  CustomThemeSupplier::ThemeType::AUTOGENERATED;
+  DCHECK_EQ(autogenerated,
+            profile_->GetPrefs()->HasPrefPath(prefs::kAutogeneratedThemeColor));
+  return autogenerated;
 }
 
 SkColor ThemeService::GetThemeColor() const {
@@ -682,26 +693,28 @@
 
 void ThemeService::InitFromPrefs() {
   FixInconsistentPreferencesIfNeeded();
-  PrefService* prefs = profile_->GetPrefs();
 
   std::string current_id = GetThemeID();
   if (current_id == kDefaultThemeID) {
-    if (UsingAutogenerated()) {
-      BuildFromColor(GetThemeColor());
-    } else if (ShouldInitWithSystemTheme()) {
+    if (ShouldInitWithSystemTheme())
       UseSystemTheme();
-    } else {
+    else
       UseDefaultTheme();
-    }
+    set_ready();
+    return;
+  }
 
+  if (current_id == kAutogeneratedThemeID) {
+    BuildFromColor(GetThemeColor());
     set_ready();
     return;
   }
 
   bool loaded_pack = false;
 
-  // If we don't have a file pack, we're updating from an old version.
+  PrefService* prefs = profile_->GetPrefs();
   base::FilePath path = prefs->GetFilePath(prefs::kCurrentThemePackFilename);
+  // If we don't have a file pack, we're updating from an old version.
   if (!path.empty()) {
     path = path.Append(chrome::kThemePackFilename);
     SwapThemeSupplier(BrowserThemePack::BuildFromDataPack(path, current_id));
@@ -1051,4 +1064,6 @@
 void ThemeService::SetThemePrefsForColor(SkColor color) {
   ClearThemePrefs();
   profile_->GetPrefs()->SetInteger(prefs::kAutogeneratedThemeColor, color);
+  profile_->GetPrefs()->SetString(prefs::kCurrentThemeID,
+                                  kAutogeneratedThemeID);
 }
diff --git a/chrome/browser/themes/theme_service.h b/chrome/browser/themes/theme_service.h
index f3bc8749..6c79c72 100644
--- a/chrome/browser/themes/theme_service.h
+++ b/chrome/browser/themes/theme_service.h
@@ -59,6 +59,9 @@
   // Public constants used in ThemeService and its subclasses:
   static const char kDefaultThemeID[];
 
+  // Constant ID to use for all autogenerated themes.
+  static const char kAutogeneratedThemeID[];
+
   ThemeService();
   ~ThemeService() override;
 
@@ -102,6 +105,12 @@
   // theme, not the "Classic" theme.
   virtual bool UsingSystemTheme() const;
 
+  // Whether we are using theme installed through extensions.
+  // |UsingExtensionTheme| and |UsingDefaultTheme| are not mutually exclusive as
+  // default theme can be installed through extensions. Cannot be called before
+  // theme is loaded.
+  virtual bool UsingExtensionTheme() const;
+
   // Gets the id of the last installed theme. (The theme may have been further
   // locally customized.)
   virtual std::string GetThemeID() const;
@@ -136,6 +145,8 @@
   // Builds a theme from a given |color|.
   virtual void BuildFromColor(SkColor color);
 
+  // Whether using autogenerated theme. Cannot be called before theme is
+  // loaded.
   virtual bool UsingAutogenerated() const;
   virtual SkColor GetThemeColor() const;
 
diff --git a/chrome/browser/themes/theme_service_factory.cc b/chrome/browser/themes/theme_service_factory.cc
index 6ea6811..feb337b 100644
--- a/chrome/browser/themes/theme_service_factory.cc
+++ b/chrome/browser/themes/theme_service_factory.cc
@@ -32,12 +32,13 @@
 // static
 const extensions::Extension* ThemeServiceFactory::GetThemeForProfile(
     Profile* profile) {
-  std::string id = GetForProfile(profile)->GetThemeID();
-  if (id == ThemeService::kDefaultThemeID)
-    return NULL;
+  ThemeService* theme_service = GetForProfile(profile);
+  if (!theme_service->UsingExtensionTheme())
+    return nullptr;
 
-  return extensions::ExtensionRegistry::Get(
-      profile)->enabled_extensions().GetByID(id);
+  return extensions::ExtensionRegistry::Get(profile)
+      ->enabled_extensions()
+      .GetByID(theme_service->GetThemeID());
 }
 
 // static
diff --git a/chrome/browser/themes/theme_service_unittest.cc b/chrome/browser/themes/theme_service_unittest.cc
index dbd6e01..de97d59 100644
--- a/chrome/browser/themes/theme_service_unittest.cc
+++ b/chrome/browser/themes/theme_service_unittest.cc
@@ -146,6 +146,7 @@
   const std::string& extension_id =
       LoadUnpackedMinimalThemeAt(temp_dir.GetPath());
   EXPECT_FALSE(theme_service->UsingDefaultTheme());
+  EXPECT_TRUE(theme_service->UsingExtensionTheme());
   EXPECT_EQ(extension_id, theme_service->GetThemeID());
 
   // Now uninstall the extension, should revert to the default theme.
@@ -153,6 +154,7 @@
                                extensions::UNINSTALL_REASON_FOR_TESTING,
                                NULL);
   EXPECT_TRUE(theme_service->UsingDefaultTheme());
+  EXPECT_FALSE(theme_service->UsingExtensionTheme());
 }
 
 // Test that a theme extension is disabled when not in use. A theme may be
@@ -428,8 +430,10 @@
   ThemeService* theme_service =
       ThemeServiceFactory::GetForProfile(profile_.get());
   theme_service->UseDefaultTheme();
+  EXPECT_TRUE(theme_service->UsingDefaultTheme());
   EXPECT_FALSE(theme_service->UsingAutogenerated());
   theme_service->BuildFromColor(SkColorSetRGB(100, 100, 100));
+  EXPECT_FALSE(theme_service->UsingDefaultTheme());
   EXPECT_TRUE(theme_service->UsingAutogenerated());
 
   // Set theme from data pack and then override it with theme from color.
@@ -438,14 +442,16 @@
   const std::string& extension1_id =
       LoadUnpackedMinimalThemeAt(temp_dir1.GetPath());
   EXPECT_EQ(extension1_id, theme_service->GetThemeID());
+  EXPECT_FALSE(theme_service->UsingDefaultTheme());
   EXPECT_FALSE(theme_service->UsingAutogenerated());
   base::FilePath path =
       profile_->GetPrefs()->GetFilePath(prefs::kCurrentThemePackFilename);
   EXPECT_FALSE(path.empty());
 
   theme_service->BuildFromColor(SkColorSetRGB(100, 100, 100));
+  EXPECT_FALSE(theme_service->UsingDefaultTheme());
   EXPECT_TRUE(theme_service->UsingAutogenerated());
-  EXPECT_EQ(ThemeService::kDefaultThemeID, theme_service->GetThemeID());
+  EXPECT_EQ(ThemeService::kAutogeneratedThemeID, theme_service->GetThemeID());
   path = profile_->GetPrefs()->GetFilePath(prefs::kCurrentThemePackFilename);
   EXPECT_TRUE(path.empty());
 }
diff --git a/chrome/browser/themes/theme_syncable_service.cc b/chrome/browser/themes/theme_syncable_service.cc
index ea065fe..cf65501 100644
--- a/chrome/browser/themes/theme_syncable_service.cc
+++ b/chrome/browser/themes/theme_syncable_service.cc
@@ -265,28 +265,33 @@
 
 bool ThemeSyncableService::GetThemeSpecificsFromCurrentTheme(
     sync_pb::ThemeSpecifics* theme_specifics) const {
-  const extensions::Extension* current_theme =
-      theme_service_->UsingDefaultTheme() ?
-          NULL :
-          extensions::ExtensionSystem::Get(profile_)->extension_service()->
-              GetExtensionById(theme_service_->GetThemeID(), false);
-  if (current_theme && !extensions::sync_helper::IsSyncable(current_theme)) {
-    DVLOG(1) << "Ignoring non-syncable extension: " << current_theme->id();
+  const extensions::Extension* current_extension =
+      theme_service_->UsingExtensionTheme() &&
+              !theme_service_->UsingDefaultTheme()
+          ? extensions::ExtensionSystem::Get(profile_)
+                ->extension_service()
+                ->GetExtensionById(theme_service_->GetThemeID(), false)
+          : nullptr;
+  if (current_extension &&
+      !extensions::sync_helper::IsSyncable(current_extension)) {
+    DVLOG(1) << "Ignoring non-syncable extension: " << current_extension->id();
     return false;
   }
 
   theme_specifics->Clear();
   theme_specifics->set_use_custom_theme(false);
 
-  if (current_theme) {
+  if (current_extension) {
     // Using custom theme and it's an extension.
-    DCHECK(current_theme->is_theme());
+    DCHECK(current_extension->is_theme());
     theme_specifics->set_use_custom_theme(true);
-    theme_specifics->set_custom_theme_name(current_theme->name());
-    theme_specifics->set_custom_theme_id(current_theme->id());
+    theme_specifics->set_custom_theme_name(current_extension->name());
+    theme_specifics->set_custom_theme_id(current_extension->id());
     theme_specifics->set_custom_theme_update_url(
-        extensions::ManifestURL::GetUpdateURL(current_theme).spec());
-  } else if (theme_service_->UsingAutogenerated()) {
+        extensions::ManifestURL::GetUpdateURL(current_extension).spec());
+  }
+
+  if (theme_service_->UsingAutogenerated()) {
     // Using custom theme and it's autogenerated from color.
     theme_specifics->set_use_custom_theme(false);
     theme_specifics->mutable_autogenerated_theme()->set_color(
diff --git a/chrome/browser/themes/theme_syncable_service_unittest.cc b/chrome/browser/themes/theme_syncable_service_unittest.cc
index 3bcb144..74543546f 100644
--- a/chrome/browser/themes/theme_syncable_service_unittest.cc
+++ b/chrome/browser/themes/theme_syncable_service_unittest.cc
@@ -110,6 +110,8 @@
 
   bool UsingAutogenerated() const override { return color_ != 0; }
 
+  bool UsingExtensionTheme() const override { return !!theme_extension_; }
+
   string GetThemeID() const override {
     if (theme_extension_.get())
       return theme_extension_->id();
diff --git a/chrome/browser/ui/views/global_error_bubble_view_unittest.cc b/chrome/browser/ui/views/global_error_bubble_view_unittest.cc
index d7af629..b1030197 100644
--- a/chrome/browser/ui/views/global_error_bubble_view_unittest.cc
+++ b/chrome/browser/ui/views/global_error_bubble_view_unittest.cc
@@ -70,8 +70,8 @@
         button_(nullptr, base::string16()),
         view_(std::make_unique<GlobalErrorBubbleView>(
             &arg_view_,
-            gfx::Rect(anchor_point_, gfx::Size()),
-            arrow_,
+            gfx::Rect(gfx::Point(), gfx::Size()),
+            views::BubbleBorder::NONE,
             nullptr,
             mock_global_error_with_standard_bubble_->AsWeakPtr())) {}
 
@@ -80,8 +80,6 @@
   std::unique_ptr<StrictMock<MockGlobalErrorWithStandardBubble>>
       mock_global_error_with_standard_bubble_;
   views::View arg_view_;
-  const gfx::Point anchor_point_;
-  views::BubbleBorder::Arrow arrow_;
   views::LabelButton button_;
   std::unique_ptr<GlobalErrorBubbleView> view_;
 
diff --git a/chrome/browser/ui/views/page_info/page_info_bubble_view_browsertest.cc b/chrome/browser/ui/views/page_info/page_info_bubble_view_browsertest.cc
index 73bdc3c..efa1147 100644
--- a/chrome/browser/ui/views/page_info/page_info_bubble_view_browsertest.cc
+++ b/chrome/browser/ui/views/page_info/page_info_bubble_view_browsertest.cc
@@ -26,6 +26,7 @@
 #include "components/content_settings/core/common/content_settings_types.h"
 #include "components/safe_browsing/features.h"
 #include "components/safe_browsing/password_protection/metrics_util.h"
+#include "components/strings/grit/components_strings.h"
 #include "content/public/browser/navigation_handle.h"
 #include "content/public/browser/render_frame_host.h"
 #include "content/public/browser/web_contents.h"
@@ -40,16 +41,11 @@
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "ui/accessibility/ax_action_data.h"
+#include "ui/base/l10n/l10n_util.h"
 #include "ui/events/event_constants.h"
 
 namespace {
 
-const auto kPageInfoNotSecureTitle =
-    base::ASCIIToUTF16("Your connection to this site is not secure");
-const auto kPageInfoMixedTitle =
-    base::ASCIIToUTF16("Your connection to this site is not fully secure");
-const auto kPageInfoSecureTitle = base::ASCIIToUTF16("Connection is secure");
-
 class ClickEvent : public ui::Event {
  public:
   ClickEvent() : ui::Event(ui::ET_UNKNOWN, base::TimeTicks(), 0) {}
@@ -627,7 +623,7 @@
             PageInfoBubbleView::GetShownBubbleType());
 }
 
-// Ensure that changes to security state are reflected in open PageInfo bubble.
+// Ensure changes to security state are reflected in an open PageInfo bubble.
 IN_PROC_BROWSER_TEST_F(PageInfoBubbleViewBrowserTest,
                        UpdatesOnSecurityStateChange) {
   net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
@@ -642,8 +638,10 @@
   views::BubbleDialogDelegateView* page_info =
       PageInfoBubbleView::GetPageInfoBubble();
 
-  EXPECT_EQ(page_info->GetWindowTitle(), kPageInfoSecureTitle);
+  EXPECT_EQ(page_info->GetWindowTitle(),
+            l10n_util::GetStringUTF16(IDS_PAGE_INFO_SECURE_SUMMARY));
 
   ExecuteJavaScriptForTests("load_mixed();");
-  EXPECT_EQ(page_info->GetWindowTitle(), kPageInfoMixedTitle);
+  EXPECT_EQ(page_info->GetWindowTitle(),
+            l10n_util::GetStringUTF16(IDS_PAGE_INFO_MIXED_CONTENT_SUMMARY));
 }
diff --git a/chrome/browser/ui/views/payments/cvc_unmask_view_controller.cc b/chrome/browser/ui/views/payments/cvc_unmask_view_controller.cc
index d32ab86..75c7e63 100644
--- a/chrome/browser/ui/views/payments/cvc_unmask_view_controller.cc
+++ b/chrome/browser/ui/views/payments/cvc_unmask_view_controller.cc
@@ -273,6 +273,11 @@
   return button;
 }
 
+bool CvcUnmaskViewController::ShouldShowSecondaryButton() {
+  // Do not show the "Cancel Payment" button.
+  return false;
+}
+
 void CvcUnmaskViewController::ButtonPressed(views::Button* sender,
                                             const ui::Event& event) {
   if (!dialog()->IsInteractive())
diff --git a/chrome/browser/ui/views/payments/cvc_unmask_view_controller.h b/chrome/browser/ui/views/payments/cvc_unmask_view_controller.h
index af2c08f..2530fb0 100644
--- a/chrome/browser/ui/views/payments/cvc_unmask_view_controller.h
+++ b/chrome/browser/ui/views/payments/cvc_unmask_view_controller.h
@@ -70,6 +70,7 @@
   base::string16 GetSheetTitle() override;
   void FillContentView(views::View* content_view) override;
   std::unique_ptr<views::Button> CreatePrimaryButton() override;
+  bool ShouldShowSecondaryButton() override;
   void ButtonPressed(views::Button* sender, const ui::Event& event) override;
 
  private:
diff --git a/chrome/browser/ui/views/payments/editor_view_controller.cc b/chrome/browser/ui/views/payments/editor_view_controller.cc
index 5730a15c..a90a3bba 100644
--- a/chrome/browser/ui/views/payments/editor_view_controller.cc
+++ b/chrome/browser/ui/views/payments/editor_view_controller.cc
@@ -138,6 +138,11 @@
   return button;
 }
 
+bool EditorViewController::ShouldShowSecondaryButton() {
+  // Do not show the "Cancel Payment" button.
+  return false;
+}
+
 void EditorViewController::FillContentView(views::View* content_view) {
   auto layout = std::make_unique<views::BoxLayout>(views::BoxLayout::kVertical);
   layout->set_main_axis_alignment(views::BoxLayout::MAIN_AXIS_ALIGNMENT_START);
diff --git a/chrome/browser/ui/views/payments/editor_view_controller.h b/chrome/browser/ui/views/payments/editor_view_controller.h
index f3aba89..0bcb91a 100644
--- a/chrome/browser/ui/views/payments/editor_view_controller.h
+++ b/chrome/browser/ui/views/payments/editor_view_controller.h
@@ -149,6 +149,7 @@
 
   // PaymentRequestSheetController;
   std::unique_ptr<views::Button> CreatePrimaryButton() override;
+  bool ShouldShowSecondaryButton() override;
   void FillContentView(views::View* content_view) override;
 
   // views::ComboboxListener:
diff --git a/chrome/browser/ui/views/payments/profile_list_view_controller.cc b/chrome/browser/ui/views/payments/profile_list_view_controller.cc
index 59475b1..86aa87f 100644
--- a/chrome/browser/ui/views/payments/profile_list_view_controller.cc
+++ b/chrome/browser/ui/views/payments/profile_list_view_controller.cc
@@ -188,16 +188,16 @@
     return GetShippingAddressSectionString(spec()->shipping_type());
   }
 
-  int GetExtraFooterViewButtonTextId() override {
-    return IDS_PAYMENTS_ADD_ADDRESS;
+  base::string16 GetSecondaryButtonLabel() override {
+    return l10n_util::GetStringUTF16(IDS_PAYMENTS_ADD_ADDRESS);
   }
 
-  int GetExtraFooterViewButtonTag() override {
+  int GetSecondaryButtonTag() override {
     return static_cast<int>(
         ProfileListViewControllerTags::ADD_SHIPPING_ADDRESS_BUTTON);
   }
 
-  int GetExtraFooterViewButtonViewId() override {
+  int GetSecondaryButtonId() override {
     return static_cast<int>(DialogViewID::PAYMENT_METHOD_ADD_SHIPPING_BUTTON);
   }
 
@@ -288,15 +288,15 @@
         IDS_PAYMENT_REQUEST_CONTACT_INFO_SECTION_NAME);
   }
 
-  int GetExtraFooterViewButtonTextId() override {
-    return IDS_PAYMENTS_ADD_CONTACT;
+  base::string16 GetSecondaryButtonLabel() override {
+    return l10n_util::GetStringUTF16(IDS_PAYMENTS_ADD_CONTACT);
   }
 
-  int GetExtraFooterViewButtonTag() override {
+  int GetSecondaryButtonTag() override {
     return static_cast<int>(ProfileListViewControllerTags::ADD_CONTACT_BUTTON);
   }
 
-  int GetExtraFooterViewButtonViewId() override {
+  int GetSecondaryButtonId() override {
     return static_cast<int>(DialogViewID::PAYMENT_METHOD_ADD_CONTACT_BUTTON);
   }
 
@@ -366,27 +366,9 @@
   content_view->AddChildView(list_view.release());
 }
 
-std::unique_ptr<views::View>
-ProfileListViewController::CreateExtraFooterView() {
-  auto extra_view = std::make_unique<views::View>();
-
-  extra_view->SetLayoutManager(std::make_unique<views::BoxLayout>(
-      views::BoxLayout::kHorizontal, gfx::Insets(),
-      kPaymentRequestButtonSpacing));
-
-  views::LabelButton* button = views::MdTextButton::CreateSecondaryUiButton(
-      this, l10n_util::GetStringUTF16(GetExtraFooterViewButtonTextId()));
-  button->set_tag(GetExtraFooterViewButtonTag());
-  button->set_id(GetExtraFooterViewButtonViewId());
-  button->SetFocusBehavior(views::View::FocusBehavior::ALWAYS);
-  extra_view->AddChildView(button);
-
-  return extra_view;
-}
-
 void ProfileListViewController::ButtonPressed(views::Button* sender,
                                               const ui::Event& event) {
-  if (sender->tag() == GetExtraFooterViewButtonTag())
+  if (sender->tag() == GetSecondaryButtonTag())
     ShowEditor(nullptr);
   else
     PaymentRequestSheetController::ButtonPressed(sender, event);
diff --git a/chrome/browser/ui/views/payments/profile_list_view_controller.h b/chrome/browser/ui/views/payments/profile_list_view_controller.h
index fe4e44a..1ced64aa 100644
--- a/chrome/browser/ui/views/payments/profile_list_view_controller.h
+++ b/chrome/browser/ui/views/payments/profile_list_view_controller.h
@@ -49,7 +49,6 @@
                                   PaymentRequestDialogView* dialog);
 
   // PaymentRequestSheetController:
-  std::unique_ptr<views::View> CreateExtraFooterView() override;
   void ButtonPressed(views::Button* sender, const ui::Event& event) override;
 
   // Returns a representation of the given profile appropriate for display
@@ -93,15 +92,6 @@
   // PaymentRequestSheetController:
   void FillContentView(views::View* content_view) override;
 
-  // Settings and events related to the button in the extra view area of the
-  // footer.
-  // +------------------------------------------------------------+
-  // | EXTRA VIEW | PAY(primary button)| CANCEL(secondary button) |
-  // +------------------------------------------------------------+
-  virtual int GetExtraFooterViewButtonTextId() = 0;
-  virtual int GetExtraFooterViewButtonTag() = 0;
-  virtual int GetExtraFooterViewButtonViewId() = 0;
-
  private:
   std::unique_ptr<views::Button> CreateRow(autofill::AutofillProfile* profile);
   PaymentRequestItemList list_;
diff --git a/chrome/browser/ui/views/payments/shipping_option_view_controller.cc b/chrome/browser/ui/views/payments/shipping_option_view_controller.cc
index 3ca2b47..3faccb6 100644
--- a/chrome/browser/ui/views/payments/shipping_option_view_controller.cc
+++ b/chrome/browser/ui/views/payments/shipping_option_view_controller.cc
@@ -121,4 +121,9 @@
   return nullptr;
 }
 
+bool ShippingOptionViewController::ShouldShowSecondaryButton() {
+  // Do not show the "Cancel Payment" button.
+  return false;
+}
+
 }  // namespace payments
diff --git a/chrome/browser/ui/views/payments/shipping_option_view_controller.h b/chrome/browser/ui/views/payments/shipping_option_view_controller.h
index 2cfe104..5c7d09e 100644
--- a/chrome/browser/ui/views/payments/shipping_option_view_controller.h
+++ b/chrome/browser/ui/views/payments/shipping_option_view_controller.h
@@ -30,6 +30,7 @@
   base::string16 GetSheetTitle() override;
   void FillContentView(views::View* content_view) override;
   std::unique_ptr<views::View> CreateExtraFooterView() override;
+  bool ShouldShowSecondaryButton() override;
 
   PaymentRequestItemList shipping_option_list_;
 
diff --git a/chrome/browser/ui/webauthn/sheet_models.cc b/chrome/browser/ui/webauthn/sheet_models.cc
index 270ec2c..0e44541 100644
--- a/chrome/browser/ui/webauthn/sheet_models.cc
+++ b/chrome/browser/ui/webauthn/sheet_models.cc
@@ -1028,11 +1028,11 @@
 }
 
 bool AuthenticatorSelectAccountSheetModel::IsAcceptButtonVisible() const {
-  return true;
+  return false;
 }
 
 bool AuthenticatorSelectAccountSheetModel::IsAcceptButtonEnabled() const {
-  return true;
+  return false;
 }
 
 base::string16 AuthenticatorSelectAccountSheetModel::GetAcceptButtonLabel()
diff --git a/chrome/browser/ui/webui/chromeos/arc_graphics_tracing/arc_graphics_tracing_handler.cc b/chrome/browser/ui/webui/chromeos/arc_graphics_tracing/arc_graphics_tracing_handler.cc
index caa66b5..00328b9e 100644
--- a/chrome/browser/ui/webui/chromeos/arc_graphics_tracing/arc_graphics_tracing_handler.cc
+++ b/chrome/browser/ui/webui/chromeos/arc_graphics_tracing/arc_graphics_tracing_handler.cc
@@ -28,6 +28,7 @@
 #include "chrome/browser/chromeos/arc/tracing/arc_tracing_graphics_model.h"
 #include "chrome/browser/chromeos/arc/tracing/arc_tracing_model.h"
 #include "chrome/browser/chromeos/file_manager/path_util.h"
+#include "chrome/browser/platform_util.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/ash/launcher/arc_app_window_launcher_controller.h"
 #include "components/arc/arc_prefs.h"
@@ -37,6 +38,7 @@
 #include "components/exo/wm_helper.h"
 #include "content/public/browser/browser_thread.h"
 #include "content/public/browser/tracing_controller.h"
+#include "content/public/browser/web_contents.h"
 #include "content/public/browser/web_ui.h"
 #include "ui/aura/client/aura_constants.h"
 #include "ui/base/ui_base_features.h"
@@ -280,8 +282,10 @@
 
 void ArcGraphicsTracingHandler::OnJankDetected(const base::Time& timestamp) {
   VLOG(1) << "Jank detected " << timestamp;
-  if (tracing_active_ && stop_on_jank_)
+  if (tracing_active_ && stop_on_jank_) {
     StopTracing();
+    Activate();
+  }
 }
 
 void ArcGraphicsTracingHandler::OnWindowPropertyChanged(aura::Window* window,
@@ -305,10 +309,12 @@
       !event->IsControlDown() || !event->IsShiftDown()) {
     return;
   }
-  if (tracing_active_)
+  if (tracing_active_) {
     StopTracing();
-  else
+    Activate();
+  } else {
     StartTracing();
+  }
 }
 
 void ArcGraphicsTracingHandler::UpdateActiveArcWindowInfo() {
@@ -350,6 +356,17 @@
   arc_active_window_ = nullptr;
 }
 
+void ArcGraphicsTracingHandler::Activate() {
+  aura::Window* const window =
+      web_ui()->GetWebContents()->GetTopLevelNativeWindow();
+  if (!window) {
+    LOG(ERROR) << "Failed to activate, no top level window.";
+    return;
+  }
+
+  platform_util::ActivateWindow(window);
+}
+
 void ArcGraphicsTracingHandler::StartTracing() {
   SetStatus("Collecting samples...");
 
diff --git a/chrome/browser/ui/webui/chromeos/arc_graphics_tracing/arc_graphics_tracing_handler.h b/chrome/browser/ui/webui/chromeos/arc_graphics_tracing/arc_graphics_tracing_handler.h
index d986704..dc37db9 100644
--- a/chrome/browser/ui/webui/chromeos/arc_graphics_tracing/arc_graphics_tracing_handler.h
+++ b/chrome/browser/ui/webui/chromeos/arc_graphics_tracing/arc_graphics_tracing_handler.h
@@ -59,6 +59,7 @@
   void OnKeyEvent(ui::KeyEvent* event) override;
 
  private:
+  void Activate();
   void StartTracing();
   void StopTracing();
   void SetStatus(const std::string& status);
diff --git a/chrome/browser/vr/service/xr_device_impl.cc b/chrome/browser/vr/service/xr_device_impl.cc
index 9dec13d..3d0be01 100644
--- a/chrome/browser/vr/service/xr_device_impl.cc
+++ b/chrome/browser/vr/service/xr_device_impl.cc
@@ -44,7 +44,6 @@
   device::mojom::XRRuntimeSessionOptionsPtr runtime_options =
       device::mojom::XRRuntimeSessionOptions::New();
   runtime_options->immersive = options->immersive;
-  runtime_options->has_user_activation = options->has_user_activation;
   runtime_options->environment_integration = options->environment_integration;
   runtime_options->use_legacy_webvr_render_path =
       options->use_legacy_webvr_render_path;
diff --git a/chrome/browser/vr/service/xr_runtime_manager.cc b/chrome/browser/vr/service/xr_runtime_manager.cc
index 08fd87b..8729fea 100644
--- a/chrome/browser/vr/service/xr_runtime_manager.cc
+++ b/chrome/browser/vr/service/xr_runtime_manager.cc
@@ -172,8 +172,8 @@
 
   // AR requested.
   if (options->environment_integration) {
-    if (options->immersive) {
-      // No support for immersive AR.
+    if (!options->immersive) {
+      DVLOG(1) << __func__ << ": non-immersive AR mode is unsupported";
       return nullptr;
     }
     // Return the ARCore device.
@@ -227,6 +227,7 @@
 
 device::mojom::VRDisplayInfoPtr XRRuntimeManager::GetCurrentVRDisplayInfo(
     XRDeviceImpl* device) {
+  DVLOG(1) << __func__;
   // Get an immersive_runtime device if there is one.
   auto* immersive_runtime = GetImmersiveRuntime();
   if (immersive_runtime) {
diff --git a/chrome/browser/vr/service/xr_runtime_manager_unittest.cc b/chrome/browser/vr/service/xr_runtime_manager_unittest.cc
index 628f83f..a1afa975 100644
--- a/chrome/browser/vr/service/xr_runtime_manager_unittest.cc
+++ b/chrome/browser/vr/service/xr_runtime_manager_unittest.cc
@@ -142,6 +142,7 @@
 
   device::mojom::XRSessionOptions options = {};
   options.environment_integration = true;
+  options.immersive = true;
   EXPECT_TRUE(DeviceManager()->GetRuntimeForOptions(&options));
   Provider()->RemoveDevice(device->GetId());
   EXPECT_TRUE(!DeviceManager()->GetRuntimeForOptions(&options));
diff --git a/chrome/credential_provider/gaiacp/associated_user_validator.cc b/chrome/credential_provider/gaiacp/associated_user_validator.cc
index dfb8c70..e51e638 100644
--- a/chrome/credential_provider/gaiacp/associated_user_validator.cc
+++ b/chrome/credential_provider/gaiacp/associated_user_validator.cc
@@ -518,10 +518,20 @@
   return block_deny_access_update_ > 0;
 }
 
-bool AssociatedUserValidator::IsUserAccessBlocked(
+bool AssociatedUserValidator::IsUserAccessBlockedForTesting(
     const base::string16& sid) const {
   base::AutoLock locker(validator_lock_);
   return locked_user_sids_.find(sid) != locked_user_sids_.end();
 }
 
+void AssociatedUserValidator::ForceRefreshTokenHandlesForTesting() {
+  base::AutoLock locker(validator_lock_);
+  for (const auto& user_info : user_to_token_handle_info_) {
+    // Make the last update time outside the validity lifetime of the token
+    // handle.
+    user_info.second->last_update =
+        base::Time::Now() - kTokenHandleValidityLifetime;
+  }
+}
+
 }  // namespace credential_provider
diff --git a/chrome/credential_provider/gaiacp/associated_user_validator.h b/chrome/credential_provider/gaiacp/associated_user_validator.h
index 0f5cb9d..da28902 100644
--- a/chrome/credential_provider/gaiacp/associated_user_validator.h
+++ b/chrome/credential_provider/gaiacp/associated_user_validator.h
@@ -139,7 +139,11 @@
 
   // Returns whether the user should be locked out of sign in (only used in
   // tests).
-  bool IsUserAccessBlocked(const base::string16& sid) const;
+  bool IsUserAccessBlockedForTesting(const base::string16& sid) const;
+
+  // Forces a refresh of all token handles the next time they are queried.
+  // This function should only be called in tests.
+  void ForceRefreshTokenHandlesForTesting();
 
  private:
   bool IsTokenHandleValidForUserInternal(const base::string16& sid);
diff --git a/chrome/credential_provider/gaiacp/associated_user_validator_unittests.cc b/chrome/credential_provider/gaiacp/associated_user_validator_unittests.cc
index 6f12e4c..96dfd57 100644
--- a/chrome/credential_provider/gaiacp/associated_user_validator_unittests.cc
+++ b/chrome/credential_provider/gaiacp/associated_user_validator_unittests.cc
@@ -281,16 +281,16 @@
           &validator);
       EXPECT_FALSE(
           validator.DenySigninForUsersWithInvalidTokenHandles(CPUS_LOGON));
-      EXPECT_FALSE(validator.IsUserAccessBlocked(OLE2W(sid)));
+      EXPECT_FALSE(validator.IsUserAccessBlockedForTesting(OLE2W(sid)));
     }
 
     EXPECT_FALSE(
         validator.DenySigninForUsersWithInvalidTokenHandles(CPUS_LOGON));
-    EXPECT_FALSE(validator.IsUserAccessBlocked(OLE2W(sid)));
+    EXPECT_FALSE(validator.IsUserAccessBlockedForTesting(OLE2W(sid)));
   }
   // Unblock deny access. User should not be blocked.
   EXPECT_TRUE(validator.DenySigninForUsersWithInvalidTokenHandles(CPUS_LOGON));
-  EXPECT_TRUE(validator.IsUserAccessBlocked(OLE2W(sid)));
+  EXPECT_TRUE(validator.IsUserAccessBlockedForTesting(OLE2W(sid)));
 
   EXPECT_EQ(1u, fake_http_url_fetcher_factory()->requests_created());
 }
@@ -360,12 +360,13 @@
   EXPECT_EQ(!internet_available || (!mdm_url_set && token_handle_valid) ||
                 (mdm_url_set && mdm_enrolled && token_handle_valid),
             validator.IsTokenHandleValidForUser(OLE2W(sid)));
-  EXPECT_EQ(should_user_be_blocked, validator.IsUserAccessBlocked(OLE2W(sid)));
+  EXPECT_EQ(should_user_be_blocked,
+            validator.IsUserAccessBlockedForTesting(OLE2W(sid)));
 
   // Unlock the user.
   validator.AllowSigninForUsersWithInvalidTokenHandles();
 
-  EXPECT_EQ(false, validator.IsUserAccessBlocked(OLE2W(sid)));
+  EXPECT_EQ(false, validator.IsUserAccessBlockedForTesting(OLE2W(sid)));
   EXPECT_NE(S_OK,
             GetMachineRegDWORD(kWinlogonUserListRegKey, username, &reg_value));
 }
diff --git a/chrome/credential_provider/gaiacp/gaia_credential_base.cc b/chrome/credential_provider/gaiacp/gaia_credential_base.cc
index 66c6b2f..fee4613 100644
--- a/chrome/credential_provider/gaiacp/gaia_credential_base.cc
+++ b/chrome/credential_provider/gaiacp/gaia_credential_base.cc
@@ -1692,6 +1692,11 @@
                  << "'. Maximum attempts reached.";
     *error_text = AllocErrorString(IDS_INTERNAL_ERROR_BASE);
     return hr;
+  } else if (FAILED(hr)) {
+    LOGFN(ERROR) << "Failed to create user '" << found_domain << "\\"
+                 << found_username << "'. hr=" << putHR(hr);
+    *error_text = AllocErrorString(IDS_INTERNAL_ERROR_BASE);
+    return hr;
   }
 
   *domain = ::SysAllocString(found_domain);
@@ -1793,6 +1798,12 @@
   USES_CONVERSION;
   LOGFN(INFO);
 
+  // Provider may be unset if the GLS process ended as a result of a kill
+  // request coming from Terminate() which would release the |provider_|
+  // reference.
+  if (!provider_)
+    return S_OK;
+
   result_status_ = status;
 
   // If the user cancelled out of the logon, the process may be already
diff --git a/chrome/credential_provider/gaiacp/gaia_credential_base.h b/chrome/credential_provider/gaiacp/gaia_credential_base.h
index d4848a4..a79a41c 100644
--- a/chrome/credential_provider/gaiacp/gaia_credential_base.h
+++ b/chrome/credential_provider/gaiacp/gaia_credential_base.h
@@ -87,9 +87,6 @@
   const base::Optional<base::Value>& get_authentication_results() const {
     return authentication_results_;
   }
-  void set_current_windows_password(BSTR password) {
-    current_windows_password_ = password;
-  }
 
   // Returns true if the current credentials stored in |username_| and
   // |password_| are valid and should succeed a local Windows logon. This
diff --git a/chrome/credential_provider/gaiacp/gaia_credential_base_unittests.cc b/chrome/credential_provider/gaiacp/gaia_credential_base_unittests.cc
index 0ede0ea..6ea8685 100644
--- a/chrome/credential_provider/gaiacp/gaia_credential_base_unittests.cc
+++ b/chrome/credential_provider/gaiacp/gaia_credential_base_unittests.cc
@@ -16,72 +16,27 @@
 
 namespace testing {
 
-// This class is used to implement a test credential based off only
-// CGaiaCredentialBase which requires certain functions be implemented.
-class ATL_NO_VTABLE CTestCredentialForBase
-    : public CTestCredentialBase<CGaiaCredentialBase>,
-      public CComObjectRootEx<CComMultiThreadModel> {
- public:
-  DECLARE_NO_REGISTRY()
-
-  CTestCredentialForBase();
-  ~CTestCredentialForBase();
-
-  HRESULT FinalConstruct() { return S_OK; }
-  void FinalRelease() {}
-
- private:
-  BEGIN_COM_MAP(CTestCredentialForBase)
-  COM_INTERFACE_ENTRY(IGaiaCredential)
-  COM_INTERFACE_ENTRY(ICredentialProviderCredential)
-  COM_INTERFACE_ENTRY(ITestCredential)
-  END_COM_MAP()
-
-  DECLARE_PROTECT_FINAL_CONSTRUCT()
-};
-
-CTestCredentialForBase::CTestCredentialForBase() = default;
-
-CTestCredentialForBase::~CTestCredentialForBase() = default;
-
-namespace {
-
-HRESULT CreateCredential(ICredentialProviderCredential** credential) {
-  return CComCreator<CComObject<CTestCredentialForBase>>::CreateInstance(
-      nullptr, IID_ICredentialProviderCredential,
-      reinterpret_cast<void**>(credential));
-}
-
-HRESULT CreateCredentialWithProvider(
-    IGaiaCredentialProvider* provider,
-    IGaiaCredential** gaia_credential,
-    ICredentialProviderCredential** credential) {
-  HRESULT hr = CreateCredential(credential);
-  if (SUCCEEDED(hr)) {
-    hr = (*credential)
-             ->QueryInterface(IID_IGaiaCredential,
-                              reinterpret_cast<void**>(gaia_credential));
-    if (SUCCEEDED(hr))
-      hr = (*gaia_credential)->Initialize(provider);
-  }
-  return hr;
-}
-
-}  // namespace
-
 class GcpGaiaCredentialBaseTest : public GlsRunnerTestBase {};
 
 TEST_F(GcpGaiaCredentialBaseTest, Advise) {
+  // Create provider with credentials. This should Advise the credential.
   CComPtr<ICredentialProviderCredential> cred;
-  ASSERT_EQ(S_OK, CreateCredential(&cred));
 
-  ASSERT_EQ(S_OK, cred->Advise(nullptr));
-  ASSERT_EQ(S_OK, cred->UnAdvise());
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred));
+
+  // Release ref count so the credential can be deleted by the call to
+  // ReleaseProvider.
+  cred.Release();
+
+  // Release the provider. This should unadvise the credential.
+  ASSERT_EQ(S_OK, ReleaseProvider());
 }
 
 TEST_F(GcpGaiaCredentialBaseTest, SetSelected) {
+  // Create provider and credential only.
   CComPtr<ICredentialProviderCredential> cred;
-  ASSERT_EQ(S_OK, CreateCredential(&cred));
+
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred));
 
   // A credential that has not attempted to sign in a user yet should return
   // false for |auto_login|.
@@ -91,65 +46,38 @@
 }
 
 TEST_F(GcpGaiaCredentialBaseTest, GetSerialization_NoInternet) {
-  FakeGaiaCredentialProvider provider;
   FakeInternetAvailabilityChecker internet_checker(
       FakeInternetAvailabilityChecker::kHicForceNo);
 
-  CComPtr<IGaiaCredential> gaia_cred;
+  // Create provider and start logon.
   CComPtr<ICredentialProviderCredential> cred;
-  ASSERT_EQ(S_OK, CreateCredentialWithProvider(&provider, &gaia_cred, &cred));
 
-  CComPtr<ITestCredential> test;
-  ASSERT_EQ(S_OK, cred.QueryInterface(&test));
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred));
 
-  ASSERT_EQ(S_OK, run_helper()->StartLogonProcess(cred, /*succeeds=*/false));
-
-  ASSERT_EQ(S_OK, gaia_cred->Terminate());
+  ASSERT_EQ(S_OK, StartLogonProcess(/*succeeds=*/false));
 }
 
 TEST_F(GcpGaiaCredentialBaseTest, GetSerialization_Start) {
-  FakeGaiaCredentialProvider provider;
-
-  CComPtr<IGaiaCredential> gaia_cred;
+  // Create provider and start logon.
   CComPtr<ICredentialProviderCredential> cred;
-  ASSERT_EQ(S_OK, CreateCredentialWithProvider(&provider, &gaia_cred, &cred));
 
-  CComPtr<ITestCredential> test;
-  ASSERT_EQ(S_OK, cred.QueryInterface(&test));
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred));
 
-  ASSERT_EQ(S_OK, run_helper()->StartLogonProcessAndWait(cred));
-
-  ASSERT_EQ(S_OK, gaia_cred->Terminate());
+  ASSERT_EQ(S_OK, StartLogonProcessAndWait());
 }
 
 TEST_F(GcpGaiaCredentialBaseTest, GetSerialization_Finish) {
-  FakeGaiaCredentialProvider provider;
-
-  // Start logon.
-  CComPtr<IGaiaCredential> gaia_cred;
+  // Create provider and start logon.
   CComPtr<ICredentialProviderCredential> cred;
-  ASSERT_EQ(S_OK, CreateCredentialWithProvider(&provider, &gaia_cred, &cred));
 
-  ASSERT_EQ(S_OK, run_helper()->StartLogonProcessAndWait(cred));
-
-  // Now finish the logon.
-  CREDENTIAL_PROVIDER_GET_SERIALIZATION_RESPONSE cpgsr;
-  CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION cpcs;
-  wchar_t* status_text;
-  CREDENTIAL_PROVIDER_STATUS_ICON status_icon;
-  ASSERT_EQ(S_OK,
-            cred->GetSerialization(&cpgsr, &cpcs, &status_text, &status_icon));
-  EXPECT_EQ(nullptr, status_text);
-  EXPECT_EQ(CPSI_SUCCESS, status_icon);
-  EXPECT_EQ(CPGSR_RETURN_CREDENTIAL_FINISHED, cpgsr);
-  EXPECT_LT(0u, cpcs.cbSerialization);
-  EXPECT_NE(nullptr, cpcs.rgbSerialization);
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred));
 
   CComPtr<ITestCredential> test;
   ASSERT_EQ(S_OK, cred.QueryInterface(&test));
 
-  // State was not reset.
-  EXPECT_TRUE(test->AreCredentialsValid());
+  ASSERT_EQ(S_OK, StartLogonProcessAndWait());
+
+  EXPECT_EQ(test->GetFinalEmail(), kDefaultEmail);
 
   // Make sure a "foo" user was created.
   PSID sid;
@@ -157,43 +85,26 @@
                       OSUserManager::GetLocalDomain().c_str(), kDefaultUsername,
                       &sid));
   ::LocalFree(sid);
-  EXPECT_EQ(test->GetFinalEmail(), kDefaultEmail);
-
-  wchar_t* report_status_text = nullptr;
-  CREDENTIAL_PROVIDER_STATUS_ICON report_icon;
-  EXPECT_EQ(S_OK, cred->ReportResult(0, 0, &report_status_text, &report_icon));
-  // State was reset.
-  EXPECT_FALSE(test->AreCredentialsValid());
-
-  EXPECT_EQ(S_OK, gaia_cred->Terminate());
 
   // New user should be created.
   EXPECT_EQ(2ul, fake_os_user_manager()->GetUserCount());
 }
 
 TEST_F(GcpGaiaCredentialBaseTest, GetSerialization_Abort) {
-  FakeGaiaCredentialProvider provider;
-
-  // Start logon.
-  CComPtr<IGaiaCredential> gaia_cred;
+  // Create provider and start logon.
   CComPtr<ICredentialProviderCredential> cred;
-  ASSERT_EQ(S_OK, CreateCredentialWithProvider(&provider, &gaia_cred, &cred));
+
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred));
 
   CComPtr<ITestCredential> test;
   ASSERT_EQ(S_OK, cred.QueryInterface(&test));
   ASSERT_EQ(S_OK, test->SetDefaultExitCode(kUiecAbort));
 
-  ASSERT_EQ(S_OK, run_helper()->StartLogonProcessAndWait(cred));
+  ASSERT_EQ(S_OK, StartLogonProcessAndWait());
 
-  // Nothing should have been propagated to the provider, but also no
-  // error should be reported.
-  EXPECT_EQ(0u, provider.username().Length());
-  EXPECT_EQ(0u, provider.password().Length());
-  EXPECT_EQ(0u, provider.sid().Length());
-  EXPECT_EQ(FALSE, provider.credentials_changed_fired());
-  EXPECT_EQ(nullptr, test->GetErrorText());
-
-  EXPECT_EQ(S_OK, gaia_cred->Terminate());
+  // Logon process should not signal credentials change or raise an error
+  // message.
+  ASSERT_EQ(S_OK, FinishLogonProcess(false, false, 0));
 }
 
 TEST_F(GcpGaiaCredentialBaseTest,
@@ -207,57 +118,30 @@
                       base::UTF8ToUTF16(kDefaultGaiaId), base::string16(),
                       &first_sid));
   ASSERT_EQ(2ul, fake_os_user_manager()->GetUserCount());
-  FakeGaiaCredentialProvider provider;
-
-  // Start logon.
-  CComPtr<IGaiaCredential> gaia_cred;
+  // Create provider and start logon.
   CComPtr<ICredentialProviderCredential> cred;
-  ASSERT_EQ(S_OK, CreateCredentialWithProvider(&provider, &gaia_cred, &cred));
+
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred));
 
   CComPtr<ITestCredential> test;
   ASSERT_EQ(S_OK, cred.QueryInterface(&test));
 
-  ASSERT_EQ(S_OK, run_helper()->StartLogonProcessAndWait(cred));
-
-  // Now finish the logon.
-  CREDENTIAL_PROVIDER_GET_SERIALIZATION_RESPONSE cpgsr;
-  CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION cpcs;
-  wchar_t* status_text;
-  CREDENTIAL_PROVIDER_STATUS_ICON status_icon;
-  ASSERT_EQ(S_OK,
-            cred->GetSerialization(&cpgsr, &cpcs, &status_text, &status_icon));
-  EXPECT_EQ(nullptr, status_text);
-  EXPECT_EQ(CPSI_SUCCESS, status_icon);
-  EXPECT_EQ(CPGSR_RETURN_CREDENTIAL_FINISHED, cpgsr);
-  EXPECT_LT(0u, cpcs.cbSerialization);
-  EXPECT_NE(nullptr, cpcs.rgbSerialization);
-
-  // State was not reset.
-  EXPECT_TRUE(test->AreCredentialsValid());
+  ASSERT_EQ(S_OK, StartLogonProcessAndWait());
 
   // User should have been associated.
   EXPECT_EQ(test->GetFinalUsername(), username);
   // Email should be the same as the default one.
   EXPECT_EQ(test->GetFinalEmail(), kDefaultEmail);
 
-  wchar_t* report_status_text = nullptr;
-  CREDENTIAL_PROVIDER_STATUS_ICON report_icon;
-  EXPECT_EQ(S_OK, cred->ReportResult(0, 0, &report_status_text, &report_icon));
-  // State was reset.
-  EXPECT_FALSE(test->AreCredentialsValid());
-
-  EXPECT_EQ(S_OK, gaia_cred->Terminate());
-
   // No new user should be created.
   EXPECT_EQ(2ul, fake_os_user_manager()->GetUserCount());
 }
 
 TEST_F(GcpGaiaCredentialBaseTest, GetSerialization_MultipleCalls) {
-  FakeGaiaCredentialProvider provider;
-
-  CComPtr<IGaiaCredential> gaia_cred;
+  // Create provider and start logon.
   CComPtr<ICredentialProviderCredential> cred;
-  ASSERT_EQ(S_OK, CreateCredentialWithProvider(&provider, &gaia_cred, &cred));
+
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred));
 
   CComPtr<ITestCredential> test;
   ASSERT_EQ(S_OK, cred.QueryInterface(&test));
@@ -270,7 +154,7 @@
   ASSERT_EQ(S_OK, test->SetStartGlsEventName(kStartGlsEventName));
   base::WaitableEvent start_event(std::move(start_event_handle));
 
-  ASSERT_EQ(S_OK, run_helper()->StartLogonProcess(cred, /*succeeds=*/true));
+  ASSERT_EQ(S_OK, StartLogonProcess(/*succeeds=*/true));
 
   // Calling GetSerialization again while the credential is waiting for the
   // logon process should yield CPGSR_NO_CREDENTIAL_NOT_FINISHED as a
@@ -288,14 +172,12 @@
   // Signal that the gls process can finish.
   start_event.Signal();
 
-  ASSERT_EQ(S_OK, run_helper()->WaitForLogonProcess(cred));
-  ASSERT_EQ(S_OK, gaia_cred->Terminate());
+  ASSERT_EQ(S_OK, WaitForLogonProcess());
 }
 
 TEST_F(GcpGaiaCredentialBaseTest,
        GetSerialization_PasswordChangedForAssociatedUser) {
   USES_CONVERSION;
-  FakeGaiaCredentialProvider provider;
 
   // Create a fake user for which the windows password does not match the gaia
   // password supplied by the test gls process.
@@ -306,15 +188,15 @@
                 L"foo", (BSTR)windows_password, L"Full Name", L"comment",
                 base::UTF8ToUTF16(kDefaultGaiaId), base::string16(), &sid));
 
-  // Start logon.
-  CComPtr<IGaiaCredential> gaia_cred;
+  // Create provider and start logon.
   CComPtr<ICredentialProviderCredential> cred;
-  ASSERT_EQ(S_OK, CreateCredentialWithProvider(&provider, &gaia_cred, &cred));
+
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred));
 
   CComPtr<ITestCredential> test;
   ASSERT_EQ(S_OK, cred.QueryInterface(&test));
 
-  ASSERT_EQ(S_OK, run_helper()->StartLogonProcessAndWait(cred));
+  ASSERT_EQ(S_OK, StartLogonProcessAndWait());
 
   EXPECT_TRUE(test->CanAttemptWindowsLogon());
   EXPECT_EQ(S_OK, test->IsWindowsPasswordValidForStoredUser());
@@ -336,14 +218,14 @@
   // Set an invalid password and try to get serialization again. Credentials
   // should still be valid but serialization is not complete.
   CComBSTR invalid_windows_password = L"a";
-  test->SetWindowsPassword(invalid_windows_password);
+  cred->SetStringValue(FID_CURRENT_PASSWORD_FIELD, invalid_windows_password);
   EXPECT_EQ(nullptr, status_text);
   ASSERT_EQ(S_OK,
             cred->GetSerialization(&cpgsr, &cpcs, &status_text, &status_icon));
   EXPECT_EQ(CPGSR_NO_CREDENTIAL_NOT_FINISHED, cpgsr);
 
   // Update the Windows password to be the real password created for the user.
-  test->SetWindowsPassword(windows_password);
+  cred->SetStringValue(FID_CURRENT_PASSWORD_FIELD, windows_password);
   // Sign in information should still be available.
   EXPECT_TRUE(test->GetFinalEmail().length());
 
@@ -351,30 +233,13 @@
   EXPECT_TRUE(test->CanAttemptWindowsLogon());
   EXPECT_EQ(S_FALSE, test->IsWindowsPasswordValidForStoredUser());
 
-  // Serialization should complete without any errors.
-  ASSERT_EQ(S_OK,
-            cred->GetSerialization(&cpgsr, &cpcs, &status_text, &status_icon));
-  EXPECT_EQ(nullptr, status_text);
-  EXPECT_EQ(CPSI_SUCCESS, status_icon);
-  EXPECT_EQ(CPGSR_RETURN_CREDENTIAL_FINISHED, cpgsr);
-  EXPECT_LT(0u, cpcs.cbSerialization);
-  EXPECT_NE(nullptr, cpcs.rgbSerialization);
-
-  // State was not reset.
-  EXPECT_TRUE(test->AreCredentialsValid());
-  wchar_t* report_status_text = nullptr;
-  CREDENTIAL_PROVIDER_STATUS_ICON report_icon;
-  EXPECT_EQ(S_OK, cred->ReportResult(0, 0, &report_status_text, &report_icon));
-  // State was reset.
-  EXPECT_FALSE(test->AreCredentialsValid());
-
-  EXPECT_EQ(S_OK, gaia_cred->Terminate());
+  // Finish logon successfully but with no credential changed event.
+  ASSERT_EQ(S_OK, FinishLogonProcess(true, false, 0));
 }
 
 TEST_F(GcpGaiaCredentialBaseTest,
        GetSerialization_ForgotPasswordForAssociatedUser) {
   USES_CONVERSION;
-  FakeGaiaCredentialProvider provider;
 
   // Create a fake user for which the windows password does not match the gaia
   // password supplied by the test gls process.
@@ -385,15 +250,15 @@
                 L"foo", (BSTR)windows_password, L"Full Name", L"comment",
                 base::UTF8ToUTF16(kDefaultGaiaId), base::string16(), &sid));
 
-  // Start logon.
-  CComPtr<IGaiaCredential> gaia_cred;
+  // Create provider and start logon.
   CComPtr<ICredentialProviderCredential> cred;
-  ASSERT_EQ(S_OK, CreateCredentialWithProvider(&provider, &gaia_cred, &cred));
+
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred));
 
   CComPtr<ITestCredential> test;
   ASSERT_EQ(S_OK, cred.QueryInterface(&test));
 
-  ASSERT_EQ(S_OK, run_helper()->StartLogonProcessAndWait(cred));
+  ASSERT_EQ(S_OK, StartLogonProcessAndWait());
 
   EXPECT_TRUE(test->CanAttemptWindowsLogon());
   EXPECT_EQ(S_OK, test->IsWindowsPasswordValidForStoredUser());
@@ -415,35 +280,13 @@
   // Simulate a click on the "Forgot Password" link.
   cred->CommandLinkClicked(FID_FORGOT_PASSWORD_LINK);
 
-  // Serialization should complete without any errors.
-  ASSERT_EQ(S_OK,
-            cred->GetSerialization(&cpgsr, &cpcs, &status_text, &status_icon));
-  EXPECT_EQ(nullptr, status_text);
-  EXPECT_EQ(CPSI_SUCCESS, status_icon);
-  EXPECT_EQ(CPGSR_RETURN_CREDENTIAL_FINISHED, cpgsr);
-  EXPECT_LT(0u, cpcs.cbSerialization);
-  EXPECT_NE(nullptr, cpcs.rgbSerialization);
-
-  // State was not reset.
-  EXPECT_TRUE(test->AreCredentialsValid());
-  wchar_t* report_status_text = nullptr;
-  CREDENTIAL_PROVIDER_STATUS_ICON report_icon;
-  EXPECT_EQ(S_OK, cred->ReportResult(0, 0, &report_status_text, &report_icon));
-  // State was reset.
-  EXPECT_FALSE(test->AreCredentialsValid());
-
-  // User password should be force changed to the one from gaia.
-  EXPECT_EQ(S_OK,
-            fake_os_user_manager()->IsWindowsPasswordValid(
-                OSUserManager::GetLocalDomain().c_str(), L"foo", L"password"));
-
-  EXPECT_EQ(S_OK, gaia_cred->Terminate());
+  // Finish logon successfully but with no credential changed event.
+  ASSERT_EQ(S_OK, FinishLogonProcess(true, false, 0));
 }
 
 TEST_F(GcpGaiaCredentialBaseTest,
        GetSerialization_AlternateForgotPasswordAssociatedUser) {
   USES_CONVERSION;
-  FakeGaiaCredentialProvider provider;
 
   // Create a fake user for which the windows password does not match the gaia
   // password supplied by the test gls process.
@@ -454,15 +297,15 @@
                 L"foo", (BSTR)windows_password, L"Full Name", L"comment",
                 base::UTF8ToUTF16(kDefaultGaiaId), base::string16(), &sid));
 
-  // Start logon.
-  CComPtr<IGaiaCredential> gaia_cred;
+  // Create provider and start logon.
   CComPtr<ICredentialProviderCredential> cred;
-  ASSERT_EQ(S_OK, CreateCredentialWithProvider(&provider, &gaia_cred, &cred));
+
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred));
 
   CComPtr<ITestCredential> test;
   ASSERT_EQ(S_OK, cred.QueryInterface(&test));
 
-  ASSERT_EQ(S_OK, run_helper()->StartLogonProcessAndWait(cred));
+  ASSERT_EQ(S_OK, StartLogonProcessAndWait());
 
   EXPECT_TRUE(test->CanAttemptWindowsLogon());
   EXPECT_EQ(S_OK, test->IsWindowsPasswordValidForStoredUser());
@@ -490,14 +333,14 @@
   // Set an invalid password and try to get serialization again. Credentials
   // should still be valid but serialization is not complete.
   CComBSTR invalid_windows_password = L"a";
-  test->SetWindowsPassword(invalid_windows_password);
+  cred->SetStringValue(FID_CURRENT_PASSWORD_FIELD, invalid_windows_password);
   EXPECT_EQ(nullptr, status_text);
   ASSERT_EQ(S_OK,
             cred->GetSerialization(&cpgsr, &cpcs, &status_text, &status_icon));
   EXPECT_EQ(CPGSR_NO_CREDENTIAL_NOT_FINISHED, cpgsr);
 
   // Update the Windows password to be the real password created for the user.
-  test->SetWindowsPassword(windows_password);
+  cred->SetStringValue(FID_CURRENT_PASSWORD_FIELD, windows_password);
   // Sign in information should still be available.
   EXPECT_TRUE(test->GetFinalEmail().length());
 
@@ -505,32 +348,15 @@
   EXPECT_TRUE(test->CanAttemptWindowsLogon());
   EXPECT_EQ(S_FALSE, test->IsWindowsPasswordValidForStoredUser());
 
-  // Serialization should complete without any errors.
-  ASSERT_EQ(S_OK,
-            cred->GetSerialization(&cpgsr, &cpcs, &status_text, &status_icon));
-  EXPECT_EQ(nullptr, status_text);
-  EXPECT_EQ(CPSI_SUCCESS, status_icon);
-  EXPECT_EQ(CPGSR_RETURN_CREDENTIAL_FINISHED, cpgsr);
-  EXPECT_LT(0u, cpcs.cbSerialization);
-  EXPECT_NE(nullptr, cpcs.rgbSerialization);
-
-  // State was not reset.
-  EXPECT_TRUE(test->AreCredentialsValid());
-  wchar_t* report_status_text = nullptr;
-  CREDENTIAL_PROVIDER_STATUS_ICON report_icon;
-  EXPECT_EQ(S_OK, cred->ReportResult(0, 0, &report_status_text, &report_icon));
-  // State was reset.
-  EXPECT_FALSE(test->AreCredentialsValid());
-
-  EXPECT_EQ(S_OK, gaia_cred->Terminate());
+  // Finish logon successfully but with no credential changed event.
+  ASSERT_EQ(S_OK, FinishLogonProcess(true, false, 0));
 }
 
 TEST_F(GcpGaiaCredentialBaseTest, GetSerialization_Cancel) {
-  FakeGaiaCredentialProvider provider;
-
-  CComPtr<IGaiaCredential> gaia_cred;
+  // Create provider and start logon.
   CComPtr<ICredentialProviderCredential> cred;
-  ASSERT_EQ(S_OK, CreateCredentialWithProvider(&provider, &gaia_cred, &cred));
+
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred));
 
   CComPtr<ITestCredential> test;
   ASSERT_EQ(S_OK, cred.QueryInterface(&test));
@@ -544,73 +370,78 @@
   ASSERT_EQ(S_OK, test->SetStartGlsEventName(kStartGlsEventName));
   base::WaitableEvent start_event(std::move(start_event_handle));
 
-  ASSERT_EQ(S_OK, run_helper()->StartLogonProcess(cred, /*succeeds=*/true));
+  ASSERT_EQ(S_OK, StartLogonProcess(/*succeeds=*/true));
 
   // Deselect the credential provider so that it cancels the GLS process and
   // returns.
   ASSERT_EQ(S_OK, cred->SetDeselected());
 
-  ASSERT_EQ(S_OK, run_helper()->WaitForLogonProcess(cred));
-  ASSERT_EQ(S_OK, gaia_cred->Terminate());
+  ASSERT_EQ(S_OK, WaitForLogonProcess());
+
+  // Logon process should not signal credentials change or raise an error
+  // message.
+  ASSERT_EQ(S_OK, FinishLogonProcess(false, false, 0));
+}
+
+TEST_F(GcpGaiaCredentialBaseTest, FailedUserCreation) {
+  // Create provider and start logon.
+  CComPtr<ICredentialProviderCredential> cred;
+
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred));
+
+  // Fail user creation.
+  fake_os_user_manager()->SetShouldFailUserCreation(true);
+
+  ASSERT_EQ(S_OK, StartLogonProcessAndWait());
+
+  // Logon process should fail with an internal error.
+  ASSERT_EQ(S_OK, FinishLogonProcess(false, false, IDS_INTERNAL_ERROR_BASE));
 }
 
 TEST_F(GcpGaiaCredentialBaseTest, StripEmailTLD) {
   USES_CONVERSION;
-  FakeGaiaCredentialProvider provider;
-
-  CComPtr<IGaiaCredential> gaia_cred;
+  // Create provider and start logon.
   CComPtr<ICredentialProviderCredential> cred;
-  ASSERT_EQ(S_OK, CreateCredentialWithProvider(&provider, &gaia_cred, &cred));
 
-  constexpr char email[] = "foo@imfl.info";
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred));
 
   CComPtr<ITestCredential> test;
   ASSERT_EQ(S_OK, cred.QueryInterface(&test));
+
+  constexpr char email[] = "foo@imfl.info";
+
   ASSERT_EQ(S_OK, test->SetGlsEmailAddress(email));
 
-  ASSERT_EQ(S_OK, run_helper()->StartLogonProcessAndWait(cred));
+  ASSERT_EQ(S_OK, StartLogonProcessAndWait());
 
   ASSERT_STREQ(W2COLE(L"foo_imfl"), test->GetFinalUsername());
   EXPECT_EQ(test->GetFinalEmail(), email);
-
-  ASSERT_EQ(S_OK, gaia_cred->Terminate());
 }
 
 TEST_F(GcpGaiaCredentialBaseTest, NewUserDisabledThroughUsageScenario) {
   USES_CONVERSION;
-  FakeGaiaCredentialProvider provider;
-
-  // Start logon.
-  CComPtr<IGaiaCredential> gaia_cred;
+  // Create provider and start logon.
   CComPtr<ICredentialProviderCredential> cred;
-  ASSERT_EQ(S_OK, CreateCredentialWithProvider(&provider, &gaia_cred, &cred));
 
-  provider.SetUsageScenario(CPUS_UNLOCK_WORKSTATION);
+  // Set the other user tile so that we can get the anonymous credential
+  // that may try create a new user.
+  fake_user_array()->SetAccountOptions(CPAO_EMPTY_LOCAL);
+
+  SetUsageScenario(CPUS_UNLOCK_WORKSTATION);
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred));
 
   CComPtr<ITestCredential> test;
   ASSERT_EQ(S_OK, cred.QueryInterface(&test));
 
-  ASSERT_EQ(S_OK, run_helper()->StartLogonProcessAndWait(cred));
-
-  ASSERT_EQ(S_OK, gaia_cred->Terminate());
-
-  // Check that values were not propagated to the provider.
-  EXPECT_EQ(0u, provider.username().Length());
-  EXPECT_EQ(0u, provider.password().Length());
-  EXPECT_EQ(0u, provider.sid().Length());
-  EXPECT_EQ(FALSE, provider.credentials_changed_fired());
+  ASSERT_EQ(S_OK, StartLogonProcessAndWait());
 
   // Sign in should fail with an error stating that no new users can be created.
-  ASSERT_STREQ(
-      test->GetErrorText(),
-      GetStringResource(IDS_INVALID_UNLOCK_WORKSTATION_USER_BASE).c_str());
+  ASSERT_EQ(S_OK, FinishLogonProcess(false, false,
+                                     IDS_INVALID_UNLOCK_WORKSTATION_USER_BASE));
 }
 
 TEST_F(GcpGaiaCredentialBaseTest, NewUserDisabledThroughMdm) {
   USES_CONVERSION;
-  FakeAssociatedUserValidator validator;
-  FakeInternetAvailabilityChecker internet_checker;
-
   // Enforce single user mode for MDM.
   ASSERT_EQ(S_OK, SetGlobalFlagForTesting(kRegMdmUrl, L"https://mdm.com"));
   ASSERT_EQ(S_OK, SetGlobalFlagForTesting(kRegMdmAllowConsumerAccounts, 1));
@@ -624,40 +455,32 @@
                       L"foo_registered", L"password", L"name", L"comment",
                       L"gaia-id-registered", base::string16(), &sid));
 
-  FakeGaiaCredentialProvider provider;
+  // Populate the associated users list. The created user's token handle
+  // should be valid so that no reauth credential is created.
+  fake_associated_user_validator()->StartRefreshingTokenHandleValidity();
 
-  // Populate the associated users list, token handle validity does not matter
-  // in this test.
-  validator.StartRefreshingTokenHandleValidity();
+  // Set the other user tile so that we can get the anonymous credential
+  // that may try to sign in a user.
+  fake_user_array()->SetAccountOptions(CPAO_EMPTY_LOCAL);
 
-  // Start logon.
-  CComPtr<IGaiaCredential> gaia_cred;
+  // Create provider and start logon.
   CComPtr<ICredentialProviderCredential> cred;
-  ASSERT_EQ(S_OK, CreateCredentialWithProvider(&provider, &gaia_cred, &cred));
+
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred));
 
   CComPtr<ITestCredential> test;
   ASSERT_EQ(S_OK, cred.QueryInterface(&test));
 
-  ASSERT_EQ(S_OK, run_helper()->StartLogonProcessAndWait(cred));
-
-  ASSERT_EQ(S_OK, gaia_cred->Terminate());
-
-  // Check that values were not propagated to the provider.
-  EXPECT_EQ(0u, provider.username().Length());
-  EXPECT_EQ(0u, provider.password().Length());
-  EXPECT_EQ(0u, provider.sid().Length());
-  EXPECT_EQ(FALSE, provider.credentials_changed_fired());
+  ASSERT_EQ(S_OK, StartLogonProcessAndWait());
 
   // Sign in should fail with an error stating that no new users can be created.
-  ASSERT_STREQ(test->GetErrorText(),
-               GetStringResource(IDS_ADD_USER_DISALLOWED_BASE).c_str());
+  ASSERT_EQ(S_OK,
+            FinishLogonProcess(false, false, IDS_ADD_USER_DISALLOWED_BASE));
 }
 
 TEST_F(GcpGaiaCredentialBaseTest, InvalidUserUnlockedAfterSignin) {
   // Enforce token handle verification with user locking when the token handle
   // is not valid.
-  FakeAssociatedUserValidator validator;
-  FakeInternetAvailabilityChecker internet_checker;
   ASSERT_EQ(S_OK, SetGlobalFlagForTesting(kRegMdmUrl, L"https://mdm.com"));
   ASSERT_EQ(S_OK, SetGlobalFlagForTesting(kRegMdmAllowConsumerAccounts, 1));
   GoogleMdmEnrollmentStatusForTesting force_success(true);
@@ -672,52 +495,36 @@
                 base::UTF8ToUTF16(kDefaultGaiaId), base::string16(), &sid));
   ASSERT_EQ(2ul, fake_os_user_manager()->GetUserCount());
 
-  // Invalid token fetch result.
-  fake_http_url_fetcher_factory()->SetFakeResponse(
-      GURL(AssociatedUserValidator::kTokenInfoUrl),
-      FakeWinHttpUrlFetcher::Headers(), "{}");
-
-  // Lock the user through their token handle.
-  validator.StartRefreshingTokenHandleValidity();
-  validator.DenySigninForUsersWithInvalidTokenHandles(CPUS_LOGON);
-
-  // User should have invalid token handle and be locked.
-  EXPECT_FALSE(validator.IsTokenHandleValidForUser(OLE2W(sid)));
-  EXPECT_EQ(true, validator.IsUserAccessBlocked(OLE2W(sid)));
-
-  FakeGaiaCredentialProvider provider;
-
-  // Start logon.
-  CComPtr<IGaiaCredential> gaia_cred;
+  // Create provider and start logon.
   CComPtr<ICredentialProviderCredential> cred;
-  ASSERT_EQ(S_OK, CreateCredentialWithProvider(&provider, &gaia_cred, &cred));
+
+  // Create with invalid token handle response.
+  SetDefaultTokenHandleResponse(kDefaultInvalidTokenHandleResponse);
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred));
 
   CComPtr<ITestCredential> test;
   ASSERT_EQ(S_OK, cred.QueryInterface(&test));
 
-  ASSERT_EQ(S_OK, run_helper()->StartLogonProcessAndWait(cred));
+  // User should have invalid token handle and be locked.
+  EXPECT_FALSE(
+      fake_associated_user_validator()->IsTokenHandleValidForUser(OLE2W(sid)));
+  EXPECT_EQ(true,
+            fake_associated_user_validator()->IsUserAccessBlockedForTesting(
+                OLE2W(sid)));
 
-  // Now finish the logon, this should unlock the user.
-  CREDENTIAL_PROVIDER_GET_SERIALIZATION_RESPONSE cpgsr;
-  CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION cpcs;
-  wchar_t* status_text;
-  CREDENTIAL_PROVIDER_STATUS_ICON status_icon;
-  ASSERT_EQ(S_OK,
-            cred->GetSerialization(&cpgsr, &cpcs, &status_text, &status_icon));
-  EXPECT_EQ(nullptr, status_text);
-  EXPECT_EQ(CPSI_SUCCESS, status_icon);
-  EXPECT_EQ(CPGSR_RETURN_CREDENTIAL_FINISHED, cpgsr);
-  EXPECT_LT(0u, cpcs.cbSerialization);
-  EXPECT_NE(nullptr, cpcs.rgbSerialization);
+  ASSERT_EQ(S_OK, StartLogonProcessAndWait());
 
   // User should have been associated.
   EXPECT_EQ(test->GetFinalUsername(), username);
   // Email should be the same as the default one.
   EXPECT_EQ(test->GetFinalEmail(), kDefaultEmail);
 
-  EXPECT_EQ(false, validator.IsUserAccessBlocked(OLE2W(sid)));
+  // Now finish the logon, this should unlock the user.
+  ASSERT_EQ(S_OK, FinishLogonProcess(true, true, 0));
 
-  ASSERT_EQ(S_OK, gaia_cred->Terminate());
+  EXPECT_EQ(false,
+            fake_associated_user_validator()->IsUserAccessBlockedForTesting(
+                OLE2W(sid)));
 
   // No new user should be created.
   EXPECT_EQ(2ul, fake_os_user_manager()->GetUserCount());
@@ -726,11 +533,10 @@
 TEST_F(GcpGaiaCredentialBaseTest, DenySigninBlockedDuringSignin) {
   USES_CONVERSION;
 
-  FakeAssociatedUserValidator validator;
-  FakeInternetAvailabilityChecker internet_checker;
   ASSERT_EQ(S_OK, SetGlobalFlagForTesting(kRegMdmUrl, L"https://mdm.com"));
+  ASSERT_EQ(S_OK, SetGlobalFlagForTesting(kRegMdmSupportsMultiUser, 1));
   ASSERT_EQ(S_OK, SetGlobalFlagForTesting(kRegMdmAllowConsumerAccounts, 1));
-  GoogleMdmEnrollmentStatusForTesting force_success(true);
+  GoogleMdmEnrolledStatusForTesting force_success(true);
 
   // Create a fake user that has the same gaia id as the test gaia id.
   CComBSTR first_sid;
@@ -740,45 +546,36 @@
                       base::UTF8ToUTF16(kDefaultGaiaId), base::string16(),
                       &first_sid));
   ASSERT_EQ(2ul, fake_os_user_manager()->GetUserCount());
-  FakeGaiaCredentialProvider provider;
 
-  // Invalid token fetch result.
-  fake_http_url_fetcher_factory()->SetFakeResponse(
-      GURL(AssociatedUserValidator::kTokenInfoUrl),
-      FakeWinHttpUrlFetcher::Headers(), "{}");
-
-  validator.StartRefreshingTokenHandleValidity();
-
-  // Start logon.
-  CComPtr<IGaiaCredential> gaia_cred;
+  // Create provider and start logon.
   CComPtr<ICredentialProviderCredential> cred;
-  ASSERT_EQ(S_OK, CreateCredentialWithProvider(&provider, &gaia_cred, &cred));
+
+  // Create with valid token handle response and sign in the anonymous
+  // credential with the user that should still be valid.
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred));
 
   CComPtr<ITestCredential> test;
   ASSERT_EQ(S_OK, cred.QueryInterface(&test));
 
-  ASSERT_EQ(S_OK, run_helper()->StartLogonProcessAndWait(cred));
+  ASSERT_EQ(S_OK, StartLogonProcessAndWait());
+
+  // Change token response to an invalid one.
+  fake_http_url_fetcher_factory()->SetFakeResponse(
+      GURL(AssociatedUserValidator::kTokenInfoUrl),
+      FakeWinHttpUrlFetcher::Headers(), "{}");
+
+  // Force refresh of all token handles on the next query.
+  fake_associated_user_validator()->ForceRefreshTokenHandlesForTesting();
 
   // Signin process has already started. User should not be locked even if their
   // token handle is invalid.
-  EXPECT_FALSE(validator.DenySigninForUsersWithInvalidTokenHandles(CPUS_LOGON));
-  EXPECT_FALSE(validator.IsUserAccessBlocked(OLE2W(first_sid)));
+  EXPECT_FALSE(fake_associated_user_validator()
+                   ->DenySigninForUsersWithInvalidTokenHandles(CPUS_LOGON));
+  EXPECT_FALSE(fake_associated_user_validator()->IsUserAccessBlockedForTesting(
+      OLE2W(first_sid)));
 
   // Now finish the logon.
-  CREDENTIAL_PROVIDER_GET_SERIALIZATION_RESPONSE cpgsr;
-  CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION cpcs;
-  wchar_t* status_text;
-  CREDENTIAL_PROVIDER_STATUS_ICON status_icon;
-  ASSERT_EQ(S_OK,
-            cred->GetSerialization(&cpgsr, &cpcs, &status_text, &status_icon));
-  EXPECT_EQ(nullptr, status_text);
-  EXPECT_EQ(CPSI_SUCCESS, status_icon);
-  EXPECT_EQ(CPGSR_RETURN_CREDENTIAL_FINISHED, cpgsr);
-  EXPECT_LT(0u, cpcs.cbSerialization);
-  EXPECT_NE(nullptr, cpcs.rgbSerialization);
-
-  // State was not reset.
-  EXPECT_TRUE(test->AreCredentialsValid());
+  ASSERT_EQ(S_OK, FinishLogonProcessWithCred(true, true, 0, cred));
 
   // User should have been associated.
   EXPECT_EQ(test->GetFinalUsername(), username);
@@ -786,20 +583,18 @@
   EXPECT_EQ(test->GetFinalEmail(), kDefaultEmail);
 
   // Result has not been reported yet, user signin should still not be denied.
-  EXPECT_FALSE(validator.DenySigninForUsersWithInvalidTokenHandles(CPUS_LOGON));
-  EXPECT_FALSE(validator.IsUserAccessBlocked(OLE2W(first_sid)));
+  EXPECT_FALSE(fake_associated_user_validator()
+                   ->DenySigninForUsersWithInvalidTokenHandles(CPUS_LOGON));
+  EXPECT_FALSE(fake_associated_user_validator()->IsUserAccessBlockedForTesting(
+      OLE2W(first_sid)));
 
-  wchar_t* report_status_text = nullptr;
-  CREDENTIAL_PROVIDER_STATUS_ICON report_icon;
-  EXPECT_EQ(S_OK, cred->ReportResult(0, 0, &report_status_text, &report_icon));
-  // State was reset.
-  EXPECT_FALSE(test->AreCredentialsValid());
-
-  EXPECT_EQ(S_OK, gaia_cred->Terminate());
+  ReportLogonProcessResult(cred);
 
   // Now signin can be denied for the user if their token handle is invalid.
-  EXPECT_TRUE(validator.DenySigninForUsersWithInvalidTokenHandles(CPUS_LOGON));
-  EXPECT_TRUE(validator.IsUserAccessBlocked(OLE2W(first_sid)));
+  EXPECT_TRUE(fake_associated_user_validator()
+                  ->DenySigninForUsersWithInvalidTokenHandles(CPUS_LOGON));
+  EXPECT_TRUE(fake_associated_user_validator()->IsUserAccessBlockedForTesting(
+      OLE2W(first_sid)));
 
   // No new user should be created.
   EXPECT_EQ(2ul, fake_os_user_manager()->GetUserCount());
@@ -807,178 +602,167 @@
 
 TEST_F(GcpGaiaCredentialBaseTest, StripEmailTLD_Gmail) {
   USES_CONVERSION;
-  FakeGaiaCredentialProvider provider;
 
-  CComPtr<IGaiaCredential> gaia_cred;
+  // Create provider and start logon.
   CComPtr<ICredentialProviderCredential> cred;
-  ASSERT_EQ(S_OK, CreateCredentialWithProvider(&provider, &gaia_cred, &cred));
 
-  constexpr char email[] = "bar@gmail.com";
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred));
 
   CComPtr<ITestCredential> test;
   ASSERT_EQ(S_OK, cred.QueryInterface(&test));
+
+  constexpr char email[] = "bar@gmail.com";
+
   ASSERT_EQ(S_OK, test->SetGlsEmailAddress(email));
 
-  ASSERT_EQ(S_OK, run_helper()->StartLogonProcessAndWait(cred));
+  ASSERT_EQ(S_OK, StartLogonProcessAndWait());
 
   ASSERT_STREQ(W2COLE(L"bar"), test->GetFinalUsername());
   EXPECT_EQ(test->GetFinalEmail(), email);
-
-  ASSERT_EQ(S_OK, gaia_cred->Terminate());
 }
 
 TEST_F(GcpGaiaCredentialBaseTest, StripEmailTLD_Googlemail) {
   USES_CONVERSION;
-  FakeGaiaCredentialProvider provider;
 
-  CComPtr<IGaiaCredential> gaia_cred;
+  // Create provider and start logon.
   CComPtr<ICredentialProviderCredential> cred;
-  ASSERT_EQ(S_OK, CreateCredentialWithProvider(&provider, &gaia_cred, &cred));
 
-  constexpr char email[] = "toto@googlemail.com";
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred));
 
   CComPtr<ITestCredential> test;
   ASSERT_EQ(S_OK, cred.QueryInterface(&test));
+
+  constexpr char email[] = "toto@googlemail.com";
+
   ASSERT_EQ(S_OK, test->SetGlsEmailAddress(email));
 
-  ASSERT_EQ(S_OK, run_helper()->StartLogonProcessAndWait(cred));
+  ASSERT_EQ(S_OK, StartLogonProcessAndWait());
 
   ASSERT_STREQ(W2COLE(L"toto"), test->GetFinalUsername());
   EXPECT_EQ(test->GetFinalEmail(), email);
-
-  ASSERT_EQ(S_OK, gaia_cred->Terminate());
 }
 
 TEST_F(GcpGaiaCredentialBaseTest, InvalidUsernameCharacters) {
   USES_CONVERSION;
-  FakeGaiaCredentialProvider provider;
-
-  CComPtr<IGaiaCredential> gaia_cred;
+  // Create provider and start logon.
   CComPtr<ICredentialProviderCredential> cred;
-  ASSERT_EQ(S_OK, CreateCredentialWithProvider(&provider, &gaia_cred, &cred));
 
-  constexpr char email[] = "a\\[]:|<>+=;?*z@gmail.com";
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred));
 
   CComPtr<ITestCredential> test;
   ASSERT_EQ(S_OK, cred.QueryInterface(&test));
+
+  constexpr char email[] = "a\\[]:|<>+=;?*z@gmail.com";
+
   ASSERT_EQ(S_OK, test->SetGlsEmailAddress(email));
 
-  ASSERT_EQ(S_OK, run_helper()->StartLogonProcessAndWait(cred));
+  ASSERT_EQ(S_OK, StartLogonProcessAndWait());
 
   ASSERT_STREQ(W2COLE(L"a____________z"), test->GetFinalUsername());
   EXPECT_EQ(test->GetFinalEmail(), email);
-
-  ASSERT_EQ(S_OK, gaia_cred->Terminate());
 }
 
 TEST_F(GcpGaiaCredentialBaseTest, EmailTooLong) {
   USES_CONVERSION;
-  FakeGaiaCredentialProvider provider;
 
-  CComPtr<IGaiaCredential> gaia_cred;
+  // Create provider and start logon.
   CComPtr<ICredentialProviderCredential> cred;
-  ASSERT_EQ(S_OK, CreateCredentialWithProvider(&provider, &gaia_cred, &cred));
 
-  constexpr char email[] = "areallylongemailadressdude@gmail.com";
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred));
 
   CComPtr<ITestCredential> test;
   ASSERT_EQ(S_OK, cred.QueryInterface(&test));
+
+  constexpr char email[] = "areallylongemailadressdude@gmail.com";
+
   ASSERT_EQ(S_OK, test->SetGlsEmailAddress(email));
 
-  ASSERT_EQ(S_OK, run_helper()->StartLogonProcessAndWait(cred));
+  ASSERT_EQ(S_OK, StartLogonProcessAndWait());
 
   ASSERT_STREQ(W2COLE(L"areallylongemailadre"), test->GetFinalUsername());
   EXPECT_EQ(test->GetFinalEmail(), email);
-
-  ASSERT_EQ(S_OK, gaia_cred->Terminate());
 }
 
 TEST_F(GcpGaiaCredentialBaseTest, EmailTooLong2) {
   USES_CONVERSION;
-  FakeGaiaCredentialProvider provider;
-
-  CComPtr<IGaiaCredential> gaia_cred;
+  // Create provider and start logon.
   CComPtr<ICredentialProviderCredential> cred;
-  ASSERT_EQ(S_OK, CreateCredentialWithProvider(&provider, &gaia_cred, &cred));
 
-  constexpr char email[] = "foo@areallylongdomaindude.com";
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred));
 
   CComPtr<ITestCredential> test;
   ASSERT_EQ(S_OK, cred.QueryInterface(&test));
+
+  constexpr char email[] = "foo@areallylongdomaindude.com";
+
   ASSERT_EQ(S_OK, test->SetGlsEmailAddress(email));
 
-  ASSERT_EQ(S_OK, run_helper()->StartLogonProcessAndWait(cred));
+  ASSERT_EQ(S_OK, StartLogonProcessAndWait());
 
   ASSERT_STREQ(W2COLE(L"foo_areallylongdomai"), test->GetFinalUsername());
   EXPECT_EQ(test->GetFinalEmail(), email);
-
-  ASSERT_EQ(S_OK, gaia_cred->Terminate());
 }
 
 TEST_F(GcpGaiaCredentialBaseTest, EmailIsNoAt) {
   USES_CONVERSION;
-  FakeGaiaCredentialProvider provider;
-
-  CComPtr<IGaiaCredential> gaia_cred;
-  CComPtr<ICredentialProviderCredential> cred;
-  ASSERT_EQ(S_OK, CreateCredentialWithProvider(&provider, &gaia_cred, &cred));
-
   constexpr char email[] = "foo";
 
+  // Create provider and start logon.
+  CComPtr<ICredentialProviderCredential> cred;
+
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred));
+
   CComPtr<ITestCredential> test;
   ASSERT_EQ(S_OK, cred.QueryInterface(&test));
+
   ASSERT_EQ(S_OK, test->SetGlsEmailAddress(email));
 
-  ASSERT_EQ(S_OK, run_helper()->StartLogonProcessAndWait(cred));
+  ASSERT_EQ(S_OK, StartLogonProcessAndWait());
 
   ASSERT_STREQ(W2COLE(L"foo_gmail"), test->GetFinalUsername());
   EXPECT_EQ(test->GetFinalEmail(), email);
-
-  ASSERT_EQ(S_OK, gaia_cred->Terminate());
 }
 
 TEST_F(GcpGaiaCredentialBaseTest, EmailIsAtCom) {
   USES_CONVERSION;
-  FakeGaiaCredentialProvider provider;
-
-  CComPtr<IGaiaCredential> gaia_cred;
-  CComPtr<ICredentialProviderCredential> cred;
-  ASSERT_EQ(S_OK, CreateCredentialWithProvider(&provider, &gaia_cred, &cred));
 
   constexpr char email[] = "@com";
 
+  // Create provider and start logon.
+  CComPtr<ICredentialProviderCredential> cred;
+
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred));
+
   CComPtr<ITestCredential> test;
   ASSERT_EQ(S_OK, cred.QueryInterface(&test));
+
   ASSERT_EQ(S_OK, test->SetGlsEmailAddress(email));
 
-  ASSERT_EQ(S_OK, run_helper()->StartLogonProcessAndWait(cred));
+  ASSERT_EQ(S_OK, StartLogonProcessAndWait());
 
   ASSERT_STREQ(W2COLE(L"_com"), test->GetFinalUsername());
   EXPECT_EQ(test->GetFinalEmail(), email);
-
-  ASSERT_EQ(S_OK, gaia_cred->Terminate());
 }
 
 TEST_F(GcpGaiaCredentialBaseTest, EmailIsAtDotCom) {
   USES_CONVERSION;
-  FakeGaiaCredentialProvider provider;
-
-  CComPtr<IGaiaCredential> gaia_cred;
-  CComPtr<ICredentialProviderCredential> cred;
-  ASSERT_EQ(S_OK, CreateCredentialWithProvider(&provider, &gaia_cred, &cred));
 
   constexpr char email[] = "@.com";
 
+  // Create provider and start logon.
+  CComPtr<ICredentialProviderCredential> cred;
+
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred));
+
   CComPtr<ITestCredential> test;
   ASSERT_EQ(S_OK, cred.QueryInterface(&test));
+
   ASSERT_EQ(S_OK, test->SetGlsEmailAddress(email));
 
-  ASSERT_EQ(S_OK, run_helper()->StartLogonProcessAndWait(cred));
+  ASSERT_EQ(S_OK, StartLogonProcessAndWait());
 
   ASSERT_STREQ(W2COLE(L"_.com"), test->GetFinalUsername());
   EXPECT_EQ(test->GetFinalEmail(), email);
-
-  ASSERT_EQ(S_OK, gaia_cred->Terminate());
 }
 
 // Tests various sign in scenarios with consumer and non-consumer domains.
@@ -1033,19 +817,17 @@
     ASSERT_EQ(2ul, fake_os_user_manager()->GetUserCount());
   }
 
-  FakeGaiaCredentialProvider provider;
-
-  // Start logon.
-  CComPtr<IGaiaCredential> gaia_cred;
+  // Create provider and start logon.
   CComPtr<ICredentialProviderCredential> cred;
-  ASSERT_EQ(S_OK, CreateCredentialWithProvider(&provider, &gaia_cred, &cred));
+
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred));
 
   CComPtr<ITestCredential> test;
   ASSERT_EQ(S_OK, cred.QueryInterface(&test));
 
   test->SetGlsEmailAddress(user_email);
 
-  ASSERT_EQ(S_OK, run_helper()->StartLogonProcessAndWait(cred));
+  ASSERT_EQ(S_OK, StartLogonProcessAndWait());
 
   bool should_signin_succeed = !mdm_enabled ||
                                (mdm_consumer_accounts_reg_key_set &&
@@ -1054,36 +836,18 @@
 
   // Sign in success.
   if (should_signin_succeed) {
-    CREDENTIAL_PROVIDER_GET_SERIALIZATION_RESPONSE cpgsr;
-    CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION cpcs;
-    wchar_t* status_text;
-    CREDENTIAL_PROVIDER_STATUS_ICON status_icon;
-    ASSERT_EQ(S_OK, cred->GetSerialization(&cpgsr, &cpcs, &status_text,
-                                           &status_icon));
-    EXPECT_EQ(nullptr, status_text);
-    EXPECT_EQ(CPSI_SUCCESS, status_icon);
-    EXPECT_EQ(CPGSR_RETURN_CREDENTIAL_FINISHED, cpgsr);
-    EXPECT_LT(0u, cpcs.cbSerialization);
-    EXPECT_NE(nullptr, cpcs.rgbSerialization);
-
     // User should have been associated.
     EXPECT_EQ(test->GetFinalUsername(), username);
     // Email should be the same as the default one.
     EXPECT_EQ(test->GetFinalEmail(), user_email);
+
+    ASSERT_EQ(S_OK, FinishLogonProcess(true, true, 0));
   } else {
-    // Nothing was propagated to the provider.
-    EXPECT_EQ(0u, provider.username().Length());
-    EXPECT_EQ(0u, provider.password().Length());
-    EXPECT_EQ(0u, provider.sid().Length());
-    EXPECT_EQ(FALSE, provider.credentials_changed_fired());
-
     // Error message concerning invalid domain is sent.
-    EXPECT_STREQ(test->GetErrorText(),
-                 GetStringResource(IDS_INVALID_EMAIL_DOMAIN_BASE).c_str());
+    ASSERT_EQ(S_OK,
+              FinishLogonProcess(false, false, IDS_INVALID_EMAIL_DOMAIN_BASE));
   }
 
-  gaia_cred->Terminate();
-
   if (user_created) {
     // No new user should be created.
     EXPECT_EQ(2ul, fake_os_user_manager()->GetUserCount());
diff --git a/chrome/credential_provider/gaiacp/gaia_credential_provider.cc b/chrome/credential_provider/gaiacp/gaia_credential_provider.cc
index a08a5d73..9032fe2 100644
--- a/chrome/credential_provider/gaiacp/gaia_credential_provider.cc
+++ b/chrome/credential_provider/gaiacp/gaia_credential_provider.cc
@@ -95,6 +95,19 @@
   return S_OK;
 }
 
+template <class CredentialT>
+HRESULT CreateCredentialObject(
+    CGaiaCredentialProvider::CredentialCreatorFn creator_fn,
+    CGaiaCredentialProvider::GaiaCredentialComPtrStorage* credential_com_ptr) {
+  if (creator_fn) {
+    return creator_fn(credential_com_ptr);
+  }
+
+  return CComCreator<CComObject<CredentialT>>::CreateInstance(
+      nullptr, IID_IGaiaCredential,
+      reinterpret_cast<void**>(&credential_com_ptr->gaia_cred));
+}
+
 }  // namespace
 
 // Class that when constructed automatically starts a thread that tries
@@ -173,8 +186,10 @@
   return 0;
 }
 
-CGaiaCredentialProvider::ComPtrStorage::ComPtrStorage() = default;
-CGaiaCredentialProvider::ComPtrStorage::~ComPtrStorage() = default;
+CGaiaCredentialProvider::GaiaCredentialComPtrStorage::
+    GaiaCredentialComPtrStorage() = default;
+CGaiaCredentialProvider::GaiaCredentialComPtrStorage::
+    ~GaiaCredentialComPtrStorage() = default;
 
 CGaiaCredentialProvider::ProviderConcurrentState::ProviderConcurrentState() =
     default;
@@ -230,7 +245,7 @@
 
 void CGaiaCredentialProvider::ProviderConcurrentState::GetUpdatedState(
     bool* needs_to_refresh_users,
-    ComPtrStorage* auto_logon_credential) {
+    GaiaCredentialComPtrStorage* auto_logon_credential) {
   DCHECK(needs_to_refresh_users);
   DCHECK(auto_logon_credential);
   base::AutoLock locker(state_update_lock_);
@@ -314,22 +329,22 @@
 
 HRESULT CGaiaCredentialProvider::CreateAnonymousCredentialIfNeeded(
     bool showing_other_user) {
-  CComPtr<IGaiaCredential> cred;
+  GaiaCredentialComPtrStorage cred;
   HRESULT hr = E_FAIL;
   if (showing_other_user) {
-    hr = CComCreator<CComObject<COtherUserGaiaCredential>>::CreateInstance(
-        nullptr, IID_IGaiaCredential, reinterpret_cast<void**>(&cred));
+    hr = CreateCredentialObject<COtherUserGaiaCredential>(
+        other_user_cred_creator_, &cred);
   } else if (CanNewUsersBeCreated(cpus_)) {
-    hr = CComCreator<CComObject<CGaiaCredential>>::CreateInstance(
-        nullptr, IID_IGaiaCredential, reinterpret_cast<void**>(&cred));
+    hr =
+        CreateCredentialObject<CGaiaCredential>(anonymous_cred_creator_, &cred);
   } else {
     return S_OK;
   }
 
   if (SUCCEEDED(hr)) {
-    hr = cred->Initialize(this);
+    hr = cred.gaia_cred->Initialize(this);
     if (SUCCEEDED(hr)) {
-      AddCredentialAndCheckAutoLogon(cred, base::string16(), nullptr);
+      AddCredentialAndCheckAutoLogon(cred.gaia_cred, base::string16(), nullptr);
     } else {
       LOG(ERROR) << "Could not create credential hr=" << putHR(hr);
     }
@@ -340,7 +355,7 @@
 
 HRESULT CGaiaCredentialProvider::CreateReauthCredentials(
     ICredentialProviderUserArray* users,
-    ComPtrStorage* auto_logon_credential) {
+    GaiaCredentialComPtrStorage* auto_logon_credential) {
   std::map<base::string16, std::pair<base::string16, base::string16>>
       sid_to_username;
 
@@ -401,21 +416,22 @@
     if (AssociatedUserValidator::Get()->IsTokenHandleValidForUser(sid))
       continue;
 
-    CComPtr<IGaiaCredential> cred;
-    HRESULT hr = CComCreator<CComObject<CReauthCredential>>::CreateInstance(
-        nullptr, IID_IGaiaCredential, reinterpret_cast<void**>(&cred));
+    GaiaCredentialComPtrStorage cred;
+    HRESULT hr =
+        CreateCredentialObject<CReauthCredential>(reauth_cred_creator_, &cred);
     if (FAILED(hr)) {
       LOG(ERROR) << "Could not create credential hr=" << putHR(hr);
       return hr;
     }
 
-    hr = InitializeReauthCredential(this, sid, domain, username, cred);
+    hr =
+        InitializeReauthCredential(this, sid, domain, username, cred.gaia_cred);
     if (FAILED(hr)) {
       LOG(ERROR) << "InitializeReauthCredential hr=" << putHR(hr);
       return hr;
     }
 
-    AddCredentialAndCheckAutoLogon(cred, sid, auto_logon_credential);
+    AddCredentialAndCheckAutoLogon(cred.gaia_cred, sid, auto_logon_credential);
   }
 
   return S_OK;
@@ -424,7 +440,7 @@
 void CGaiaCredentialProvider::AddCredentialAndCheckAutoLogon(
     const CComPtr<IGaiaCredential>& cred,
     const base::string16& sid,
-    ComPtrStorage* auto_logon_credential) {
+    GaiaCredentialComPtrStorage* auto_logon_credential) {
   USES_CONVERSION;
   users_.emplace_back(cred);
 
@@ -451,7 +467,7 @@
 }
 
 void CGaiaCredentialProvider::RecreateCredentials(
-    ComPtrStorage* auto_logon_credential) {
+    GaiaCredentialComPtrStorage* auto_logon_credential) {
   LOGFN(INFO);
   DCHECK(user_array_);
 
@@ -470,6 +486,55 @@
     LOG(ERROR) << "CreateReauthCredentials hr=" << putHR(hr);
 }
 
+void CGaiaCredentialProvider::SetCredentialCreatorFunctionsForTesting(
+    CredentialCreatorFn anonymous_cred_creator,
+    CredentialCreatorFn other_user_cred_creator,
+    CredentialCreatorFn reauth_cred_creator) {
+  DCHECK(!anonymous_cred_creator_);
+  DCHECK(!other_user_cred_creator_);
+  DCHECK(!reauth_cred_creator_);
+
+  anonymous_cred_creator_ = anonymous_cred_creator;
+  other_user_cred_creator_ = other_user_cred_creator;
+  reauth_cred_creator_ = reauth_cred_creator;
+}
+
+HRESULT CGaiaCredentialProvider::OnUserAuthenticatedImpl(
+    IUnknown* credential,
+    BSTR /*username*/,
+    BSTR /*password*/,
+    BSTR sid,
+    BOOL fire_credentials_changed) {
+  DCHECK(!credential || sid);
+
+  if (!fire_credentials_changed)
+    return S_OK;
+
+  // Ensure that user access cannot be denied at this time so that the user
+  // that is about to sign in won't be locked. If a ScopedLockDenyAccessUpdate
+  // is created before calling this function this should guarantee that
+  // situation because the call to BlockDenyAccessUpdate is locked with the
+  // same lock that is used in DenySigninForUsersWithInvalidTokenHandles.
+  // So either the call to Deny has finished and no new deny will occur
+  // afterwards or the Deny will be disabled because the block has been
+  // incremented first.
+  CHECK(!credential ||
+        AssociatedUserValidator::Get()->IsDenyAccessUpdateBlocked());
+
+  CComPtr<IGaiaCredential> gaia_credential;
+  if (credential->QueryInterface(IID_IGaiaCredential,
+                                 reinterpret_cast<void**>(&gaia_credential)) ==
+      S_OK) {
+    // Try to set the auto logon credential. If it succeeds we can raise a
+    // credential changed event.
+    if (concurrent_state_.SetAutoLogonCredential(gaia_credential) && events_)
+      events_->CredentialsChanged(advise_context_);
+  }
+
+  LOGFN(INFO) << "Signing in authenticated sid=" << OLE2CW(sid);
+  return S_OK;
+}
+
 // Static.
 bool CGaiaCredentialProvider::IsUsageScenarioSupported(
     CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus) {
@@ -527,38 +592,12 @@
 
 HRESULT CGaiaCredentialProvider::OnUserAuthenticated(
     IUnknown* credential,
-    BSTR /*username*/,
-    BSTR /*password*/,
+    BSTR username,
+    BSTR password,
     BSTR sid,
     BOOL fire_credentials_changed) {
-  DCHECK(!credential || sid);
-
-  if (!fire_credentials_changed)
-    return S_OK;
-
-  // Ensure that user access cannot be denied at this time so that the user
-  // that is about to sign in won't be locked. If a ScopedLockDenyAccessUpdate
-  // is created before calling this function this should guarantee that
-  // situation because the call to BlockDenyAccessUpdate is locked with the
-  // same lock that is used in DenySigninForUsersWithInvalidTokenHandles.
-  // So either the call to Deny has finished and no new deny will occur
-  // afterwards or the Deny will be disabled because the block has been
-  // incremented first.
-  CHECK(!credential ||
-        AssociatedUserValidator::Get()->IsDenyAccessUpdateBlocked());
-
-  CComPtr<IGaiaCredential> gaia_credential;
-  if (credential->QueryInterface(IID_IGaiaCredential,
-                                 reinterpret_cast<void**>(&gaia_credential)) ==
-      S_OK) {
-    // Try to set the auto logon credential. If it succeeds we can raise a
-    // credential changed event.
-    if (concurrent_state_.SetAutoLogonCredential(gaia_credential) && events_)
-      events_->CredentialsChanged(advise_context_);
-  }
-
-  LOGFN(INFO) << "Signing in authenticated sid=" << OLE2CW(sid);
-  return S_OK;
+  return OnUserAuthenticatedImpl(credential, username, password, sid,
+                                 fire_credentials_changed);
 }
 
 // ICredentialProviderSetUserArray ////////////////////////////////////////////
@@ -710,7 +749,7 @@
     DWORD* default_index,
     BOOL* autologin_with_default) {
   bool needs_to_refresh_users = false;
-  ComPtrStorage local_auto_logon_credential;
+  GaiaCredentialComPtrStorage local_auto_logon_credential;
 
   // Get the mutually exclusive state of the provider so that we can
   // determine the correct next step (recreate credentials or auto logon).
diff --git a/chrome/credential_provider/gaiacp/gaia_credential_provider.h b/chrome/credential_provider/gaiacp/gaia_credential_provider.h
index 2a6db40..54edb10 100644
--- a/chrome/credential_provider/gaiacp/gaia_credential_provider.h
+++ b/chrome/credential_provider/gaiacp/gaia_credential_provider.h
@@ -24,7 +24,8 @@
 
 // Event handler that can be notified when a user's access has been revoked,
 // allowing the credential provider to update the list of available credentials.
-class ICredentialUpdateEventsHandler {
+class DECLSPEC_UUID("fc2c889b-b468-4eb9-a61c-c984be8cc496")
+    ICredentialUpdateEventsHandler : public IUnknown {
  public:
   virtual ~ICredentialUpdateEventsHandler() = default;
   virtual void UpdateCredentialsIfNeeded(bool user_access_changed) = 0;
@@ -51,6 +52,7 @@
   COM_INTERFACE_ENTRY(IGaiaCredentialProvider)
   COM_INTERFACE_ENTRY(ICredentialProviderSetUserArray)
   COM_INTERFACE_ENTRY(ICredentialProvider)
+  COM_INTERFACE_ENTRY(ICredentialUpdateEventsHandler)
   END_COM_MAP()
 
   DECLARE_PROTECT_FINAL_CONSTRUCT()
@@ -70,17 +72,31 @@
   // determine the result of this query.
   static bool CanNewUsersBeCreated(CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus);
 
- private:
-  HRESULT DestroyCredentials();
-
   // Struct to allow passing CComPtr by pointer without the implicit conversion
   // to ** version of the CComPtr
-  struct ComPtrStorage {
-    ComPtrStorage();
-    ~ComPtrStorage();
+  struct GaiaCredentialComPtrStorage {
+    GaiaCredentialComPtrStorage();
+    ~GaiaCredentialComPtrStorage();
     CComPtr<IGaiaCredential> gaia_cred;
   };
 
+  typedef HRESULT (*CredentialCreatorFn)(GaiaCredentialComPtrStorage*);
+
+ protected:
+  void SetCredentialCreatorFunctionsForTesting(
+      CredentialCreatorFn anonymous_cred_creator,
+      CredentialCreatorFn other_user_cred_creator,
+      CredentialCreatorFn reauth_cred_creator);
+
+  virtual HRESULT OnUserAuthenticatedImpl(IUnknown* credential,
+                                          BSTR username,
+                                          BSTR password,
+                                          BSTR sid,
+                                          BOOL fire_credentials_changed);
+
+ private:
+  HRESULT DestroyCredentials();
+
   // Class used to store state information for the provider that may be accessed
   // concurrently. This class is thread safe and ensures that the correct state
   // can be set / queried at any moment. When modifying the state, the modifying
@@ -116,7 +132,7 @@
     // |needs_to_refresh_users| and |auto_logon_credential| can be set to a non
     // default value.
     void GetUpdatedState(bool* needs_to_refresh_users,
-                         ComPtrStorage* auto_logon_credential);
+                         GaiaCredentialComPtrStorage* auto_logon_credential);
 
     // Resets the state of the provider to be default one. On the next call to
     // GetCredentialCount no auto logon should be performed and no refresh of
@@ -158,24 +174,26 @@
   // Creates all the reauth credentials from the users that is returned from
   // |users|. Fills the |gaia_cred| in |auto_logon_credential| with a reference
   // to the credential that needs to perform auto logon (if any).
-  HRESULT CreateReauthCredentials(ICredentialProviderUserArray* users,
-                                  ComPtrStorage* auto_logon_credential);
+  HRESULT CreateReauthCredentials(
+      ICredentialProviderUserArray* users,
+      GaiaCredentialComPtrStorage* auto_logon_credential);
 
   // This function will always add |cred| to |users_| and will also try to check
   // if the |sid| matches the one set in |set_serialization_sid_| to allow auto
   // logon of remote connections. Fills the |gaia_cred| in
   // |auto_logon_credential| with a reference to the credential that needs to
   // perform auto logon (if any).
-  void AddCredentialAndCheckAutoLogon(const CComPtr<IGaiaCredential>& cred,
-                                      const base::string16& sid,
-                                      ComPtrStorage* auto_logon_credential);
+  void AddCredentialAndCheckAutoLogon(
+      const CComPtr<IGaiaCredential>& cred,
+      const base::string16& sid,
+      GaiaCredentialComPtrStorage* auto_logon_credential);
 
   // Destroys existing credentials and recreates them based on the contents of
   // |user_array_|. This member must be set via a call to SetUserArray before
   // RecreateCredentials is called. Fills the |gaia_cred| in
   // |auto_logon_credential| with a reference to the credential that needs to
   // perform auto logon (if any).
-  void RecreateCredentials(ComPtrStorage* auto_logon_credential);
+  void RecreateCredentials(GaiaCredentialComPtrStorage* auto_logon_credential);
 
   void ClearTransient();
   void CleanupOlderVersions();
@@ -235,6 +253,10 @@
   base::string16 set_serialization_sid_;
 
   ProviderConcurrentState concurrent_state_;
+
+  CredentialCreatorFn anonymous_cred_creator_ = nullptr;
+  CredentialCreatorFn other_user_cred_creator_ = nullptr;
+  CredentialCreatorFn reauth_cred_creator_ = nullptr;
 };
 
 // OBJECT_ENTRY_AUTO() contains an extra semicolon.
diff --git a/chrome/credential_provider/gaiacp/gaia_credential_provider_filter.cc b/chrome/credential_provider/gaiacp/gaia_credential_provider_filter.cc
index 486dd40..6506180a 100644
--- a/chrome/credential_provider/gaiacp/gaia_credential_provider_filter.cc
+++ b/chrome/credential_provider/gaiacp/gaia_credential_provider_filter.cc
@@ -82,6 +82,8 @@
          pcpcs_in->cbSerialization);
   pcpcs_out->cbSerialization = pcpcs_in->cbSerialization;
   pcpcs_out->clsidCredentialProvider = CLSID_GaiaCredentialProvider;
+  pcpcs_out->ulAuthenticationPackage = pcpcs_in->ulAuthenticationPackage;
+
   return S_OK;
 }
 
diff --git a/chrome/credential_provider/gaiacp/gaia_credential_provider_unittests.cc b/chrome/credential_provider/gaiacp/gaia_credential_provider_unittests.cc
index f69e873..fd92c70 100644
--- a/chrome/credential_provider/gaiacp/gaia_credential_provider_unittests.cc
+++ b/chrome/credential_provider/gaiacp/gaia_credential_provider_unittests.cc
@@ -10,10 +10,8 @@
 #include <tuple>
 
 #include "base/synchronization/waitable_event.h"
-#include "base/win/registry.h"
 #include "base/win/win_util.h"
 #include "chrome/credential_provider/common/gcp_strings.h"
-#include "chrome/credential_provider/gaiacp/associated_user_validator.h"
 #include "chrome/credential_provider/gaiacp/auth_utils.h"
 #include "chrome/credential_provider/gaiacp/gaia_credential_provider.h"
 #include "chrome/credential_provider/gaiacp/gaia_credential_provider_i.h"
@@ -21,41 +19,14 @@
 #include "chrome/credential_provider/gaiacp/reg_utils.h"
 #include "chrome/credential_provider/test/com_fakes.h"
 #include "chrome/credential_provider/test/gcp_fakes.h"
-#include "testing/gtest/include/gtest/gtest.h"
+#include "chrome/credential_provider/test/gls_runner_test_base.h"
+#include "chrome/credential_provider/test/test_credential.h"
 
 namespace credential_provider {
 
-class GcpCredentialProviderTest : public ::testing::Test {
- protected:
-  void CreateGCPWUser(const wchar_t* username,
-                      const wchar_t* email,
-                      const wchar_t* password,
-                      const wchar_t* fullname,
-                      const wchar_t* comment,
-                      const wchar_t* gaia_id,
-                      BSTR* sid) {
-    ASSERT_EQ(S_OK,
-              fake_os_user_manager_.CreateTestOSUser(
-                  username, password, fullname, comment, gaia_id, email, sid));
-  }
+namespace testing {
 
-  FakeOSUserManager* fake_os_user_manager() { return &fake_os_user_manager_; }
-  FakeWinHttpUrlFetcherFactory* fake_http_url_fetcher_factory() {
-    return &fake_http_url_fetcher_factory_;
-  }
-
-  void SetUp() override;
-
- private:
-  registry_util::RegistryOverrideManager registry_override_;
-  FakeOSUserManager fake_os_user_manager_;
-  FakeScopedLsaPolicyFactory fake_scoped_lsa_policy_factory_;
-  FakeWinHttpUrlFetcherFactory fake_http_url_fetcher_factory_;
-};
-
-void GcpCredentialProviderTest::SetUp() {
-  InitializeRegistryOverrideForTesting(&registry_override_);
-}
+class GcpCredentialProviderTest : public GlsRunnerTestBase {};
 
 TEST_F(GcpCredentialProviderTest, Basic) {
   CComPtr<IGaiaCredentialProvider> provider;
@@ -65,164 +36,92 @@
 }
 
 TEST_F(GcpCredentialProviderTest, SetUserArray_NoGaiaUsers) {
-  CComPtr<ICredentialProviderSetUserArray> user_array;
-  ASSERT_EQ(
-      S_OK,
-      CComCreator<CComObject<CGaiaCredentialProvider>>::CreateInstance(
-          nullptr, IID_ICredentialProviderSetUserArray, (void**)&user_array));
-
-  FakeCredentialProviderUserArray array;
-  array.AddUser(L"sid", L"username");
-  ASSERT_EQ(S_OK, user_array->SetUserArray(&array));
+  CComBSTR sid;
+  ASSERT_EQ(S_OK, fake_os_user_manager()->CreateTestOSUser(
+                      L"username", L"password", L"full name", L"comment", L"",
+                      L"", &sid));
 
   CComPtr<ICredentialProvider> provider;
-  ASSERT_EQ(S_OK, user_array.QueryInterface(&provider));
+  DWORD count = 0;
+  ASSERT_EQ(S_OK, InitializeProviderWithCredentials(&count, &provider));
 
-  // There should be no credentials. Only users with the requisite registry
-  // entry will be counted.
-  DWORD count;
-  DWORD default_index;
-  BOOL autologon;
-  ASSERT_EQ(S_OK,
-            provider->GetCredentialCount(&count, &default_index, &autologon));
-  EXPECT_EQ(0u, count);
-  EXPECT_EQ(CREDENTIAL_PROVIDER_NO_DEFAULT, default_index);
-  EXPECT_FALSE(autologon);
+  // There should only be the anonymous credential. Only users with the
+  // requisite registry entry will be counted.
+  EXPECT_EQ(1u, count);
+
+  CComPtr<ICredentialProviderCredential> cred;
+  ASSERT_EQ(S_OK, provider->GetCredentialAt(0, &cred));
+
+  CComPtr<ICredentialProviderCredential2> cred2;
+  ASSERT_NE(S_OK, cred.QueryInterface(&cred2));
+
+  CComPtr<IReauthCredential> reauth_cred;
+  ASSERT_NE(S_OK, cred.QueryInterface(&reauth_cred));
 }
 
 TEST_F(GcpCredentialProviderTest, CpusLogon) {
+  CComBSTR sid;
+  ASSERT_EQ(S_OK, fake_os_user_manager()->CreateTestOSUser(
+                      L"username", L"password", L"full name", L"comment", L"",
+                      L"", &sid));
+
   CComPtr<ICredentialProvider> provider;
-  ASSERT_EQ(S_OK,
-            CComCreator<CComObject<CGaiaCredentialProvider>>::CreateInstance(
-                nullptr, IID_ICredentialProvider, (void**)&provider));
+  DWORD count = 0;
+  ASSERT_EQ(S_OK, InitializeProviderWithCredentials(&count, &provider));
 
-  // Start process for logon screen.
-  ASSERT_EQ(S_OK, provider->SetUsageScenario(CPUS_LOGON, 0));
+  // There should only be the anonymous credential. Only users with the
+  // requisite registry entry will be counted.
+  EXPECT_EQ(1u, count);
 
-  // Give list of users visible on welcome screen.
-  CComPtr<ICredentialProviderSetUserArray> user_array;
-  ASSERT_EQ(S_OK, provider.QueryInterface(&user_array));
-  FakeCredentialProviderUserArray array;
-  array.AddUser(L"sid1", L"username1");
-  ASSERT_EQ(S_OK, user_array->SetUserArray(&array));
-
-  // Activate the CP.
-  FakeCredentialProviderEvents events;
-  ASSERT_EQ(S_OK, provider->Advise(&events, 0));
-
-  // Check credentials.
-  DWORD count;
-  DWORD default_index;
-  BOOL autologon;
-  ASSERT_EQ(S_OK,
-            provider->GetCredentialCount(&count, &default_index, &autologon));
-  ASSERT_EQ(1u, count);
-  EXPECT_EQ(CREDENTIAL_PROVIDER_NO_DEFAULT, default_index);
-  EXPECT_FALSE(autologon);
   CComPtr<ICredentialProviderCredential> cred;
   ASSERT_EQ(S_OK, provider->GetCredentialAt(0, &cred));
-  CComPtr<IGaiaCredential> gaia_cred;
-  EXPECT_EQ(S_OK, cred.QueryInterface(&gaia_cred));
 
-  // Get fields.
-  DWORD field_count;
-  ASSERT_EQ(S_OK, provider->GetFieldDescriptorCount(&field_count));
-  EXPECT_EQ(FIELD_COUNT, field_count);
+  CComPtr<ICredentialProviderCredential2> cred2;
+  ASSERT_NE(S_OK, cred.QueryInterface(&cred2));
 
-  // Deactivate the CP.
-  ASSERT_EQ(S_OK, provider->UnAdvise());
+  CComPtr<IReauthCredential> reauth_cred;
+  ASSERT_NE(S_OK, cred.QueryInterface(&reauth_cred));
 }
 
 TEST_F(GcpCredentialProviderTest, CpusUnlock) {
+  CComBSTR sid;
+  ASSERT_EQ(S_OK, fake_os_user_manager()->CreateTestOSUser(
+                      L"username", L"password", L"full name", L"comment", L"",
+                      L"", &sid));
+
   CComPtr<ICredentialProvider> provider;
-  ASSERT_EQ(S_OK,
-            CComCreator<CComObject<CGaiaCredentialProvider>>::CreateInstance(
-                nullptr, IID_ICredentialProvider, (void**)&provider));
-
-  // Start process for logon screen.
-  ASSERT_EQ(S_OK, provider->SetUsageScenario(CPUS_UNLOCK_WORKSTATION, 0));
-
-  // Give list of users visible on welcome screen.
-  CComPtr<ICredentialProviderSetUserArray> user_array;
-  ASSERT_EQ(S_OK, provider.QueryInterface(&user_array));
-  FakeCredentialProviderUserArray array;
-  array.AddUser(L"sid1", L"username1");
-  ASSERT_EQ(S_OK, user_array->SetUserArray(&array));
-
-  // Activate the CP.
-  FakeCredentialProviderEvents events;
-  ASSERT_EQ(S_OK, provider->Advise(&events, 0));
+  DWORD count = 0;
+  SetUsageScenario(CPUS_UNLOCK_WORKSTATION);
+  ASSERT_EQ(S_OK, InitializeProviderWithCredentials(&count, &provider));
 
   // Check credentials. None should be available because the anonymous
   // credential is not allowed during an unlock scenario.
-  DWORD count;
-  DWORD default_index;
-  BOOL autologon;
-  ASSERT_EQ(S_OK,
-            provider->GetCredentialCount(&count, &default_index, &autologon));
   ASSERT_EQ(0u, count);
-  EXPECT_EQ(CREDENTIAL_PROVIDER_NO_DEFAULT, default_index);
-  EXPECT_FALSE(autologon);
-
-  // Get fields.
-  DWORD field_count;
-  ASSERT_EQ(S_OK, provider->GetFieldDescriptorCount(&field_count));
-  EXPECT_EQ(FIELD_COUNT, field_count);
-
-  // Deactivate the CP.
-  ASSERT_EQ(S_OK, provider->UnAdvise());
 }
 
 TEST_F(GcpCredentialProviderTest, AutoLogonAfterUserRefresh) {
   USES_CONVERSION;
-  CComPtr<ICredentialProvider> provider;
-  ASSERT_EQ(S_OK,
-            CComCreator<CComObject<CGaiaCredentialProvider>>::CreateInstance(
-                nullptr, IID_ICredentialProvider, (void**)&provider));
+  CComBSTR sid;
+  ASSERT_EQ(S_OK, fake_os_user_manager()->CreateTestOSUser(
+                      L"username", L"password", L"full name", L"comment", L"",
+                      L"", &sid));
+
+  CComPtr<ICredentialProviderCredential> cred;
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred));
+
+  CComPtr<ICredentialProvider> provider = created_provider();
 
   CComPtr<IGaiaCredentialProvider> gaia_provider;
   ASSERT_EQ(S_OK, provider.QueryInterface(&gaia_provider));
 
-  CComBSTR sid;
-  ASSERT_EQ(S_OK, fake_os_user_manager()->CreateTestOSUser(
-                      L"username", L"passowrd", L"Full Name", L"Comment", L"",
-                      L"", &sid));
-  // Start process for logon screen.
-  ASSERT_EQ(S_OK, provider->SetUsageScenario(CPUS_LOGON, 0));
-
-  // Give empty list of users so that only the anonymous credential is created.
-  CComPtr<ICredentialProviderSetUserArray> user_array;
-  ASSERT_EQ(S_OK, provider.QueryInterface(&user_array));
-  FakeCredentialProviderUserArray array;
-  ASSERT_EQ(S_OK, user_array->SetUserArray(&array));
-
-  // Activate the CP.
-  FakeCredentialProviderEvents events;
-  ASSERT_EQ(S_OK, provider->Advise(&events, 0));
-
-  // Only the anonymous credential should exist.
-  DWORD count;
-  DWORD default_index;
-  BOOL autologon;
-  ASSERT_EQ(S_OK,
-            provider->GetCredentialCount(&count, &default_index, &autologon));
-  ASSERT_EQ(1u, count);
-  EXPECT_EQ(CREDENTIAL_PROVIDER_NO_DEFAULT, default_index);
-  EXPECT_FALSE(autologon);
-
-  // Get the anonymous credential.
-  CComPtr<ICredentialProviderCredential> cred;
-  ASSERT_EQ(S_OK, provider->GetCredentialAt(0, &cred));
-
   // Notify that user access is denied to fake a forced recreation of the users.
-  ICredentialUpdateEventsHandler* update_handler =
-      static_cast<ICredentialUpdateEventsHandler*>(
-          static_cast<CGaiaCredentialProvider*>(provider.p));
+  CComPtr<ICredentialUpdateEventsHandler> update_handler;
+  ASSERT_EQ(S_OK, provider.QueryInterface(&update_handler));
   update_handler->UpdateCredentialsIfNeeded(true);
 
   // Credential changed event should have been received.
-  EXPECT_TRUE(events.CredentialsChangedReceived());
-  events.ResetCredentialsChangedReceived();
+  EXPECT_TRUE(fake_provider_events()->CredentialsChangedReceived());
+  fake_provider_events()->ResetCredentialsChangedReceived();
 
   // At the same time notify that a user has authenticated and requires a
   // sign in.
@@ -236,10 +135,14 @@
   }
 
   // No credential changed should have been signalled here.
-  EXPECT_FALSE(events.CredentialsChangedReceived());
+  EXPECT_FALSE(fake_provider_events()->CredentialsChangedReceived());
 
   // GetCredentialCount should return back the same credential that was just
   // auto logged on.
+
+  DWORD count;
+  DWORD default_index;
+  BOOL autologon;
   ASSERT_EQ(S_OK,
             provider->GetCredentialCount(&count, &default_index, &autologon));
   ASSERT_EQ(1u, count);
@@ -257,7 +160,7 @@
   update_handler->UpdateCredentialsIfNeeded(false);
 
   // Credential changed event should have been received.
-  EXPECT_TRUE(events.CredentialsChangedReceived());
+  EXPECT_TRUE(fake_provider_events()->CredentialsChangedReceived());
 
   // GetCredentialCount should return new credentials with no auto logon.
   ASSERT_EQ(S_OK,
@@ -272,12 +175,12 @@
 
   // Another request to refresh the credentials should yield no credential
   // changed event or refresh of credentials.
-  events.ResetCredentialsChangedReceived();
+  fake_provider_events()->ResetCredentialsChangedReceived();
 
   update_handler->UpdateCredentialsIfNeeded(false);
 
   // No credential changed event should have been received.
-  EXPECT_FALSE(events.CredentialsChangedReceived());
+  EXPECT_FALSE(fake_provider_events()->CredentialsChangedReceived());
 
   // GetCredentialCount should return the same credentials with no change.
   ASSERT_EQ(S_OK,
@@ -289,55 +192,24 @@
   CComPtr<ICredentialProviderCredential> unchanged_cred;
   ASSERT_EQ(S_OK, provider->GetCredentialAt(0, &unchanged_cred));
   EXPECT_TRUE(new_cred.IsEqualObject(unchanged_cred));
-
-  // Deactivate the CP.
-  ASSERT_EQ(S_OK, provider->UnAdvise());
 }
 
 TEST_F(GcpCredentialProviderTest, AutoLogonBeforeUserRefresh) {
   USES_CONVERSION;
-  CComPtr<ICredentialProvider> provider;
-  ASSERT_EQ(S_OK,
-            CComCreator<CComObject<CGaiaCredentialProvider>>::CreateInstance(
-                nullptr, IID_ICredentialProvider, (void**)&provider));
+  CComBSTR sid;
+  ASSERT_EQ(S_OK, fake_os_user_manager()->CreateTestOSUser(
+                      L"username", L"password", L"full name", L"comment", L"",
+                      L"", &sid));
 
+  CComPtr<ICredentialProviderCredential> cred;
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred));
+
+  CComPtr<ICredentialProvider> provider = created_provider();
   CComPtr<IGaiaCredentialProvider> gaia_provider;
   ASSERT_EQ(S_OK, provider.QueryInterface(&gaia_provider));
 
-  CComBSTR sid;
-  ASSERT_EQ(S_OK, fake_os_user_manager()->CreateTestOSUser(
-                      L"username", L"passowrd", L"Full Name", L"Comment", L"",
-                      L"", &sid));
-  // Start process for logon screen.
-  ASSERT_EQ(S_OK, provider->SetUsageScenario(CPUS_LOGON, 0));
-
-  // Give empty list of users so that only the anonymous credential is created.
-  CComPtr<ICredentialProviderSetUserArray> user_array;
-  ASSERT_EQ(S_OK, provider.QueryInterface(&user_array));
-  FakeCredentialProviderUserArray array;
-  ASSERT_EQ(S_OK, user_array->SetUserArray(&array));
-
-  // Activate the CP.
-  FakeCredentialProviderEvents events;
-  ASSERT_EQ(S_OK, provider->Advise(&events, 0));
-
-  // Only the anonymous credential should exist.
-  DWORD count;
-  DWORD default_index;
-  BOOL autologon;
-  ASSERT_EQ(S_OK,
-            provider->GetCredentialCount(&count, &default_index, &autologon));
-  ASSERT_EQ(1u, count);
-  EXPECT_EQ(CREDENTIAL_PROVIDER_NO_DEFAULT, default_index);
-  EXPECT_FALSE(autologon);
-
-  // Get the anonymous credential.
-  CComPtr<ICredentialProviderCredential> cred;
-  ASSERT_EQ(S_OK, provider->GetCredentialAt(0, &cred));
-
-  ICredentialUpdateEventsHandler* update_handler =
-      static_cast<ICredentialUpdateEventsHandler*>(
-          static_cast<CGaiaCredentialProvider*>(provider.p));
+  CComPtr<ICredentialUpdateEventsHandler> update_handler;
+  ASSERT_EQ(S_OK, provider.QueryInterface(&update_handler));
 
   // Notify user auto logon first and then notify user access denied to ensure
   // that auto logon always has precedence over user access denied.
@@ -351,18 +223,22 @@
   }
 
   // Credential changed event should have been received.
-  EXPECT_TRUE(events.CredentialsChangedReceived());
-  events.ResetCredentialsChangedReceived();
+  EXPECT_TRUE(fake_provider_events()->CredentialsChangedReceived());
+  fake_provider_events()->ResetCredentialsChangedReceived();
 
   // Notify that user access is denied. This should not cause a credential
   // changed since an event was already processed.
   update_handler->UpdateCredentialsIfNeeded(true);
 
   // No credential changed should have been signalled here.
-  EXPECT_FALSE(events.CredentialsChangedReceived());
+  EXPECT_FALSE(fake_provider_events()->CredentialsChangedReceived());
 
   // GetCredentialCount should return back the same credential that was just
   // auto logged on.
+  DWORD count;
+  DWORD default_index;
+  BOOL autologon;
+
   ASSERT_EQ(S_OK,
             provider->GetCredentialCount(&count, &default_index, &autologon));
   ASSERT_EQ(1u, count);
@@ -380,7 +256,7 @@
   update_handler->UpdateCredentialsIfNeeded(false);
 
   // Credential changed event should have been received.
-  EXPECT_TRUE(events.CredentialsChangedReceived());
+  EXPECT_TRUE(fake_provider_events()->CredentialsChangedReceived());
 
   // GetCredentialCount should return new credentials with no auto logon.
   ASSERT_EQ(S_OK,
@@ -398,9 +274,6 @@
 }
 
 TEST_F(GcpCredentialProviderTest, AddPersonAfterUserRemove) {
-  FakeAssociatedUserValidator associated_user_validator;
-  FakeInternetAvailabilityChecker internet_checker;
-
   // Set up such that MDM is enabled, mulit-users is not, and a user already
   // exists.
   ASSERT_EQ(S_OK, SetGlobalFlagForTesting(kRegMdmUrl, L"https://mdm.com"));
@@ -410,39 +283,21 @@
   const wchar_t kDummyUsername[] = L"username";
   const wchar_t kDummyPassword[] = L"password";
   CComBSTR sid;
-  CreateGCPWUser(kDummyUsername, L"foo@gmail.com", kDummyPassword, L"Full Name",
-                 L"Comment", L"gaia-id", &sid);
-
-  // Start token handle refresh threads.
-  associated_user_validator.StartRefreshingTokenHandleValidity();
+  ASSERT_EQ(S_OK, fake_os_user_manager()->CreateTestOSUser(
+                      kDummyUsername, kDummyPassword, L"full name", L"comment",
+                      L"gaia-id", L"foo@gmail.com", &sid));
 
   {
+    CComPtr<ICredentialProviderCredential> cred;
     CComPtr<ICredentialProvider> provider;
-    ASSERT_EQ(S_OK,
-              CComCreator<CComObject<CGaiaCredentialProvider>>::CreateInstance(
-                  nullptr, IID_ICredentialProvider, (void**)&provider));
-    ASSERT_EQ(S_OK, provider->SetUsageScenario(CPUS_LOGON, 0));
-
-    // Empty user array.
-    CComPtr<ICredentialProviderSetUserArray> user_array;
-    ASSERT_EQ(S_OK, provider.QueryInterface(&user_array));
-    FakeCredentialProviderUserArray array;
-    ASSERT_EQ(S_OK, user_array->SetUserArray(&array));
-
-    // Activate the CP.
-    FakeCredentialProviderEvents events;
-    ASSERT_EQ(S_OK, provider->Advise(&events, 0));
+    DWORD count = 0;
+    ASSERT_EQ(S_OK, InitializeProviderWithCredentials(&count, &provider));
 
     // In this case no credential should be returned.
-    DWORD count;
-    DWORD default_index;
-    BOOL autologon;
-    ASSERT_EQ(S_OK,
-              provider->GetCredentialCount(&count, &default_index, &autologon));
     ASSERT_EQ(0u, count);
 
-    // Deactivate the CP.
-    ASSERT_EQ(S_OK, provider->UnAdvise());
+    // Release the CP so we can create another one.
+    ASSERT_EQ(S_OK, ReleaseProvider());
   }
 
   // Delete the OS user.  At this point, info in the HKLM registry about this
@@ -453,47 +308,64 @@
 
   {
     CComPtr<ICredentialProvider> provider;
-    ASSERT_EQ(S_OK,
-              CComCreator<CComObject<CGaiaCredentialProvider>>::CreateInstance(
-                  nullptr, IID_ICredentialProvider, (void**)&provider));
-    ASSERT_EQ(S_OK, provider->SetUsageScenario(CPUS_LOGON, 0));
-
-    // Empty user array.
-    CComPtr<ICredentialProviderSetUserArray> user_array;
-    ASSERT_EQ(S_OK, provider.QueryInterface(&user_array));
-    FakeCredentialProviderUserArray array;
-    ASSERT_EQ(S_OK, user_array->SetUserArray(&array));
-
-    // Activate the CP.
-    FakeCredentialProviderEvents events;
-    ASSERT_EQ(S_OK, provider->Advise(&events, 0));
+    DWORD count = 0;
+    ASSERT_EQ(S_OK, InitializeProviderWithCredentials(&count, &provider));
 
     // This time a credential should be returned.
-    DWORD count;
-    DWORD default_index;
-    BOOL autologon;
-    ASSERT_EQ(S_OK,
-              provider->GetCredentialCount(&count, &default_index, &autologon));
     ASSERT_EQ(1u, count);
 
-    // Deactivate the CP.
+    // And this credential should be the anonymous one.
+    CComPtr<ICredentialProviderCredential> cred;
+    ASSERT_EQ(S_OK, provider->GetCredentialAt(0, &cred));
+
+    CComPtr<ICredentialProviderCredential2> cred2;
+    ASSERT_NE(S_OK, cred.QueryInterface(&cred2));
+
+    CComPtr<IReauthCredential> reauth_cred;
+    ASSERT_NE(S_OK, cred.QueryInterface(&reauth_cred));
+
+    // Release the CP.
     ASSERT_EQ(S_OK, provider->UnAdvise());
   }
 }
 
+class GcpCredentialProviderExecutionTest : public GlsRunnerTestBase {};
+
+TEST_F(GcpCredentialProviderExecutionTest, UnAdviseDuringGls) {
+  USES_CONVERSION;
+
+  CComPtr<ICredentialProviderCredential> cred;
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred));
+
+  CComPtr<ITestCredential> test;
+  ASSERT_EQ(S_OK, cred.QueryInterface(&test));
+
+  // This event is merely used to keep the gls running while it is killed by
+  // Terminate().
+  constexpr wchar_t kStartGlsEventName[] = L"UnAdviseDuringGls_Signal";
+  base::win::ScopedHandle start_event_handle(
+      ::CreateEvent(nullptr, false, false, kStartGlsEventName));
+  ASSERT_TRUE(start_event_handle.IsValid());
+  ASSERT_EQ(S_OK, test->SetStartGlsEventName(kStartGlsEventName));
+  base::WaitableEvent start_event(std::move(start_event_handle));
+
+  ASSERT_EQ(S_OK, StartLogonProcess(/*succeeds=*/true));
+
+  // Release the provider which should also Terminate the credential that
+  // was created.
+  ReleaseProvider();
+}
+
 // Tests auto logon enabled when set serialization is called.
 // Parameters:
 // 1. bool: are the users' token handles still valid.
 // 2. CREDENTIAL_PROVIDER_USAGE_SCENARIO - the usage scenario.
 class GcpCredentialProviderSetSerializationTest
     : public GcpCredentialProviderTest,
-      public testing::WithParamInterface<
+      public ::testing::WithParamInterface<
           std::tuple<bool, CREDENTIAL_PROVIDER_USAGE_SCENARIO>> {};
 
 TEST_P(GcpCredentialProviderSetSerializationTest, CheckAutoLogon) {
-  FakeAssociatedUserValidator associated_user_validator;
-  FakeInternetAvailabilityChecker internet_checker;
-
   const bool valid_token_handles = std::get<0>(GetParam());
   const CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus = std::get<1>(GetParam());
 
@@ -502,25 +374,15 @@
 
   CComBSTR first_sid;
   constexpr wchar_t first_username[] = L"username";
-  CreateGCPWUser(first_username, L"foo@gmail.com", L"password", L"Full Name",
-                 L"Comment", L"gaia-id", &first_sid);
+  ASSERT_EQ(S_OK, fake_os_user_manager()->CreateTestOSUser(
+                      first_username, L"password", L"full name", L"comment",
+                      L"gaia-id", L"foo@gmail.com", &first_sid));
 
   CComBSTR second_sid;
   constexpr wchar_t second_username[] = L"username2";
-  CreateGCPWUser(second_username, L"foo2@gmail.com", L"password", L"Full Name",
-                 L"Comment", L"gaia-id2", &second_sid);
-
-  // Token fetch result.
-  fake_http_url_fetcher_factory()->SetFakeResponse(
-      GURL(AssociatedUserValidator::kTokenInfoUrl),
-      FakeWinHttpUrlFetcher::Headers(),
-      valid_token_handles ? "{\"expires_in\":1}" : "{}");
-
-  // Start token handle refresh threads.
-  associated_user_validator.StartRefreshingTokenHandleValidity();
-
-  // Lock users as needed based on the validity of their token handles.
-  associated_user_validator.DenySigninForUsersWithInvalidTokenHandles(cpus);
+  ASSERT_EQ(S_OK, fake_os_user_manager()->CreateTestOSUser(
+                      second_username, L"password", L"Full Name", L"Comment",
+                      L"gaia-id2", L"foo2@gmail.com", &second_sid));
 
   // Build a dummy authentication buffer that can be passed to SetSerialization.
   CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION cpcs;
@@ -539,32 +401,18 @@
                       &dummy_domain[0], &dummy_username[0], &dummy_password[0],
                       cpus, &cpcs));
 
+  GetAuthenticationPackageId(&cpcs.ulAuthenticationPackage);
   cpcs.clsidCredentialProvider = CLSID_GaiaCredentialProvider;
 
-  CComPtr<ICredentialProviderSetUserArray> user_array;
-  ASSERT_EQ(
-      S_OK,
-      CComCreator<CComObject<CGaiaCredentialProvider>>::CreateInstance(
-          nullptr, IID_ICredentialProviderSetUserArray, (void**)&user_array));
+  CComPtr<ICredentialProviderCredential> cred;
   CComPtr<ICredentialProvider> provider;
-  ASSERT_EQ(S_OK, user_array.QueryInterface(&provider));
-
-  ASSERT_EQ(S_OK, provider->SetUsageScenario(cpus, 0));
-
-  ASSERT_EQ(S_OK, provider->SetSerialization(&cpcs));
+  SetDefaultTokenHandleResponse(valid_token_handles
+                                    ? kDefaultValidTokenHandleResponse
+                                    : kDefaultInvalidTokenHandleResponse);
+  ASSERT_EQ(S_OK, InitializeProviderWithRemoteCredentials(&cpcs, &provider));
 
   ::CoTaskMemFree(cpcs.rgbSerialization);
 
-  FakeCredentialProviderUserArray array;
-  array.AddUser(OLE2CW(first_sid), first_username);
-  array.AddUser(OLE2CW(second_sid), second_username);
-
-  ASSERT_EQ(S_OK, user_array->SetUserArray(&array));
-
-  // Activate the CP.
-  FakeCredentialProviderEvents events;
-  ASSERT_EQ(S_OK, provider->Advise(&events, 0));
-
   // Check the correct number of credentials are created and whether autologon
   // is enabled based on the token handle validity.
   DWORD count;
@@ -578,9 +426,6 @@
   EXPECT_EQ(autologon, should_autologon);
   EXPECT_EQ(default_index,
             should_autologon ? 1 : CREDENTIAL_PROVIDER_NO_DEFAULT);
-
-  // Deactivate the CP.
-  ASSERT_EQ(S_OK, provider->UnAdvise());
 }
 
 INSTANTIATE_TEST_SUITE_P(
@@ -599,12 +444,9 @@
 //    bool: whether an existing user exists.
 class GcpCredentialProviderMdmTest
     : public GcpCredentialProviderTest,
-      public testing::WithParamInterface<std::tuple<bool, int, bool>> {};
+      public ::testing::WithParamInterface<std::tuple<bool, int, bool>> {};
 
 TEST_P(GcpCredentialProviderMdmTest, Basic) {
-  FakeAssociatedUserValidator associated_user_validator;
-  FakeInternetAvailabilityChecker internet_checker;
-
   const bool config_mdm_url = std::get<0>(GetParam());
   const int supports_multi_users = std::get<1>(GetParam());
   const bool user_exists = std::get<2>(GetParam());
@@ -626,40 +468,16 @@
 
   if (user_exists) {
     CComBSTR sid;
-    CreateGCPWUser(L"username", L"foo@gmail.com", L"password", L"Full Name",
-                   L"Comment", L"gaia-id", &sid);
+    ASSERT_EQ(S_OK, fake_os_user_manager()->CreateTestOSUser(
+                        L"username", L"password", L"full name", L"comment",
+                        L"gaia-id", L"foo@gmail.com", &sid));
   }
 
-  // Valid token fetch result.
-  fake_http_url_fetcher_factory()->SetFakeResponse(
-      GURL(AssociatedUserValidator::kTokenInfoUrl),
-      FakeWinHttpUrlFetcher::Headers(), "{\"expires_in\":1}");
-
-  associated_user_validator.StartRefreshingTokenHandleValidity();
-
+  CComPtr<ICredentialProviderCredential> cred;
   CComPtr<ICredentialProvider> provider;
-  ASSERT_EQ(S_OK,
-            CComCreator<CComObject<CGaiaCredentialProvider>>::CreateInstance(
-                nullptr, IID_ICredentialProvider, (void**)&provider));
+  DWORD count = 0;
+  ASSERT_EQ(S_OK, InitializeProviderWithCredentials(&count, &provider));
 
-  // Start process for logon screen.
-  ASSERT_EQ(S_OK, provider->SetUsageScenario(CPUS_LOGON, 0));
-
-  // Empty user array.
-  CComPtr<ICredentialProviderSetUserArray> user_array;
-  ASSERT_EQ(S_OK, provider.QueryInterface(&user_array));
-  FakeCredentialProviderUserArray array;
-  ASSERT_EQ(S_OK, user_array->SetUserArray(&array));
-
-  // Activate the CP.
-  FakeCredentialProviderEvents events;
-  ASSERT_EQ(S_OK, provider->Advise(&events, 0));
-
-  DWORD count;
-  DWORD default_index;
-  BOOL autologon;
-  ASSERT_EQ(S_OK,
-            provider->GetCredentialCount(&count, &default_index, &autologon));
   ASSERT_EQ(expected_credential_count, count);
 
   // Deactivate the CP.
@@ -668,9 +486,9 @@
 
 INSTANTIATE_TEST_SUITE_P(GcpCredentialProviderMdmTest,
                          GcpCredentialProviderMdmTest,
-                         ::testing::Combine(testing::Bool(),
-                                            testing::Range(0, 3),
-                                            testing::Bool()));
+                         ::testing::Combine(::testing::Bool(),
+                                            ::testing::Range(0, 3),
+                                            ::testing::Bool()));
 
 // Check that reauth credentials only exist when the token handle for the
 // associated user is no longer valid and internet is available.
@@ -688,61 +506,32 @@
   const bool has_token_handle = std::get<0>(GetParam());
   const bool valid_token_handle = std::get<1>(GetParam());
   const bool has_internet = std::get<2>(GetParam());
-  FakeAssociatedUserValidator associated_user_validator;
-  FakeInternetAvailabilityChecker internet_checker(
+  fake_internet_checker()->SetHasInternetConnection(
       has_internet ? FakeInternetAvailabilityChecker::kHicForceYes
                    : FakeInternetAvailabilityChecker::kHicForceNo);
 
   CComBSTR sid;
-  CreateGCPWUser(L"username", L"foo@gmail.com", L"password", L"Full Name",
-                 L"Comment", L"gaia-id", &sid);
+  ASSERT_EQ(S_OK, fake_os_user_manager()->CreateTestOSUser(
+                      L"username", L"password", L"full name", L"comment",
+                      L"gaia-id", L"foo@gmail.com", &sid));
 
   if (!has_token_handle)
     ASSERT_EQ(S_OK, SetUserProperty((BSTR)sid, kUserTokenHandle, L""));
 
-  // Token fetch result.
-  fake_http_url_fetcher_factory()->SetFakeResponse(
-      GURL(AssociatedUserValidator::kTokenInfoUrl),
-      FakeWinHttpUrlFetcher::Headers(),
-      valid_token_handle ? "{\"expires_in\":1}" : "{}");
-
-  // Start token handle refresh threads.
-  associated_user_validator.StartRefreshingTokenHandleValidity();
-
-  CComPtr<ICredentialProviderSetUserArray> user_array;
-  ASSERT_EQ(
-      S_OK,
-      CComCreator<CComObject<CGaiaCredentialProvider>>::CreateInstance(
-          nullptr, IID_ICredentialProviderSetUserArray, (void**)&user_array));
-
+  CComPtr<ICredentialProviderCredential> cred;
   CComPtr<ICredentialProvider> provider;
-  ASSERT_EQ(S_OK, user_array.QueryInterface(&provider));
-
-  ASSERT_EQ(S_OK, provider->SetUsageScenario(CPUS_LOGON, 0));
-
-  FakeCredentialProviderUserArray array;
-  array.AddUser(OLE2CW(sid), L"username");
-  ASSERT_EQ(S_OK, user_array->SetUserArray(&array));
-
-  // Activate the CP.
-  FakeCredentialProviderEvents events;
-  ASSERT_EQ(S_OK, provider->Advise(&events, 0));
+  DWORD count = 0;
+  SetDefaultTokenHandleResponse(valid_token_handle
+                                    ? kDefaultValidTokenHandleResponse
+                                    : kDefaultInvalidTokenHandleResponse);
+  ASSERT_EQ(S_OK, InitializeProviderWithCredentials(&count, &provider));
 
   bool should_reauth_user =
       has_internet && (!has_token_handle || !valid_token_handle);
 
   // Check if there is a IReauthCredential depending on the state of the token
   // handle.
-  DWORD count;
-  DWORD default_index;
-  BOOL autologon;
-  // There should always be the anonymous credential and potentially a reauth
-  // credential.
-  ASSERT_EQ(S_OK,
-            provider->GetCredentialCount(&count, &default_index, &autologon));
   ASSERT_EQ(should_reauth_user ? 2u : 1u, count);
-  EXPECT_EQ(CREDENTIAL_PROVIDER_NO_DEFAULT, default_index);
-  EXPECT_FALSE(autologon);
 
   if (should_reauth_user) {
     CComPtr<ICredentialProviderCredential> cred;
@@ -750,9 +539,6 @@
     CComPtr<IReauthCredential> reauth;
     EXPECT_EQ(S_OK, cred.QueryInterface(&reauth));
   }
-
-  // Deactivate the CP.
-  ASSERT_EQ(S_OK, provider->UnAdvise());
 }
 
 INSTANTIATE_TEST_SUITE_P(,
@@ -788,9 +574,7 @@
 }
 
 TEST_P(GcpCredentialProviderAvailableCredentialsTest, AvailableCredentials) {
-  FakeAssociatedUserValidator associated_user_validator;
-  FakeInternetAvailabilityChecker internet_checker;
-  FakeCredentialProviderUserArray array;
+  USES_CONVERSION;
 
   const bool valid_token_handles = std::get<0>(GetParam());
   const CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus = std::get<1>(GetParam());
@@ -801,67 +585,33 @@
   GoogleMdmEnrolledStatusForTesting forced_status(enrolled_to_mdm);
 
   if (other_user_tile_available)
-    array.SetAccountOptions(CPAO_EMPTY_LOCAL);
+    fake_user_array()->SetAccountOptions(CPAO_EMPTY_LOCAL);
 
   CComBSTR first_sid;
   constexpr wchar_t first_username[] = L"username";
-  CreateGCPWUser(first_username, L"foo@gmail.com", L"password", L"Full Name",
-                 L"Comment", L"gaia-id", &first_sid);
+  ASSERT_EQ(S_OK, fake_os_user_manager()->CreateTestOSUser(
+                      first_username, L"password", L"full name", L"comment",
+                      L"gaia-id", L"foo@gmail.com", &first_sid));
 
   CComBSTR second_sid;
   constexpr wchar_t second_username[] = L"username2";
-  CreateGCPWUser(second_username, L"foo2@gmail.com", L"password", L"Full Name",
-                 L"Comment", L"gaia-id2", &second_sid);
+  ASSERT_EQ(S_OK, fake_os_user_manager()->CreateTestOSUser(
+                      second_username, L"password", L"Full Name", L"Comment",
+                      L"gaia-id2", L"foo2@gmail.com", &second_sid));
 
-  // Token fetch result.
-  fake_http_url_fetcher_factory()->SetFakeResponse(
-      GURL(AssociatedUserValidator::kTokenInfoUrl),
-      FakeWinHttpUrlFetcher::Headers(),
-      valid_token_handles ? "{\"expires_in\":1}" : "{}");
+  // Set the user locking the system.
+  SetSidLockingWorkstation(second_user_locking_system ? OLE2CW(second_sid)
+                                                      : OLE2CW(first_sid));
 
-  // Start token handle refresh threads.
-  associated_user_validator.StartRefreshingTokenHandleValidity();
-
-  // Lock users as needed based on the validity of their token handles.
-  associated_user_validator.DenySigninForUsersWithInvalidTokenHandles(cpus);
-
-  CComPtr<ICredentialProviderSetUserArray> user_array;
-  ASSERT_EQ(
-      S_OK,
-      CComCreator<CComObject<CGaiaCredentialProvider>>::CreateInstance(
-          nullptr, IID_ICredentialProviderSetUserArray, (void**)&user_array));
   CComPtr<ICredentialProvider> provider;
-  ASSERT_EQ(S_OK, user_array.QueryInterface(&provider));
-
-  ASSERT_EQ(S_OK, provider->SetUsageScenario(cpus, 0));
-
-  // All users are shown if the usage is not for unlocking the workstation.
-  bool all_users_shown = cpus != CPUS_UNLOCK_WORKSTATION;
-  // If not all the users are shown, the user that locked the system is
-  // the only one that is in the user array (if the other user tile is
-  // not available).
-  if (all_users_shown ||
-      (!second_user_locking_system && !other_user_tile_available)) {
-    array.AddUser(OLE2CW(first_sid), first_username);
-  }
-  if (all_users_shown ||
-      (second_user_locking_system && !other_user_tile_available)) {
-    array.AddUser(OLE2CW(second_sid), second_username);
-  }
-
-  ASSERT_EQ(S_OK, user_array->SetUserArray(&array));
-
-  // Activate the CP.
-  FakeCredentialProviderEvents events;
-  ASSERT_EQ(S_OK, provider->Advise(&events, 0));
+  DWORD count = 0;
+  SetUsageScenario(cpus);
+  SetDefaultTokenHandleResponse(valid_token_handles
+                                    ? kDefaultValidTokenHandleResponse
+                                    : kDefaultInvalidTokenHandleResponse);
+  ASSERT_EQ(S_OK, InitializeProviderWithCredentials(&count, &provider));
 
   // Check the correct number of credentials are created.
-  DWORD count;
-  DWORD default_index;
-  BOOL autologon;
-  ASSERT_EQ(S_OK,
-            provider->GetCredentialCount(&count, &default_index, &autologon));
-
   DWORD expected_credentials = 0;
   if (cpus != CPUS_UNLOCK_WORKSTATION) {
     expected_credentials = valid_token_handles && enrolled_to_mdm ? 0 : 2;
@@ -876,14 +626,10 @@
   }
 
   ASSERT_EQ(expected_credentials, count);
-  EXPECT_EQ(CREDENTIAL_PROVIDER_NO_DEFAULT, default_index);
-  EXPECT_FALSE(autologon);
 
-  if (expected_credentials == 0) {
-    // Deactivate the CP.
-    ASSERT_EQ(S_OK, provider->UnAdvise());
+  // No credentials to verify.
+  if (expected_credentials == 0)
     return;
-  }
 
   CComPtr<ICredentialProviderCredential> cred;
   CComPtr<ICredentialProviderCredential2> cred2;
@@ -942,9 +688,6 @@
     EXPECT_EQ(guid_string, base::string16(guid_in_registry));
     ::CoTaskMemFree(sid);
   }
-
-  // Deactivate the CP.
-  ASSERT_EQ(S_OK, provider->UnAdvise());
 }
 
 INSTANTIATE_TEST_SUITE_P(
@@ -956,4 +699,6 @@
                        ::testing::Bool(),
                        ::testing::Bool()));
 
+}  // namespace testing
+
 }  // namespace credential_provider
diff --git a/chrome/credential_provider/gaiacp/gaia_credential_unittests.cc b/chrome/credential_provider/gaiacp/gaia_credential_unittests.cc
index a89c408..51fa2c8 100644
--- a/chrome/credential_provider/gaiacp/gaia_credential_unittests.cc
+++ b/chrome/credential_provider/gaiacp/gaia_credential_unittests.cc
@@ -25,29 +25,15 @@
 
 namespace testing {
 
-namespace {
-
-HRESULT CreateGaiaCredentialWithProvider(
-    IGaiaCredentialProvider* provider,
-    IGaiaCredential** gaia_credential,
-    ICredentialProviderCredential** credential) {
-  return CreateBaseInheritedCredentialWithProvider<CGaiaCredential>(
-      provider, gaia_credential, credential);
-}
-
-}  // namespace
-
 class GcpGaiaCredentialTest : public GlsRunnerTestBase {
  protected:
   GcpGaiaCredentialTest();
 
-  FakeGaiaCredentialProvider* provider() { return &provider_; }
   BSTR signin_result() { return signin_result_; }
 
   CComBSTR MakeSigninResult(const std::string& password);
 
  private:
-  FakeGaiaCredentialProvider provider_;
   CComBSTR signin_result_;
 };
 
@@ -69,28 +55,36 @@
 TEST_F(GcpGaiaCredentialTest, OnUserAuthenticated) {
   USES_CONVERSION;
 
-  CComPtr<IGaiaCredential> gaia_cred;
   CComPtr<ICredentialProviderCredential> cred;
-  ASSERT_EQ(S_OK,
-            CreateGaiaCredentialWithProvider(provider(), &gaia_cred, &cred));
+
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred));
+
+  CComPtr<IGaiaCredential> gaia_cred;
+  ASSERT_EQ(S_OK, cred.QueryInterface(&gaia_cred));
 
   CComBSTR error;
   ASSERT_EQ(S_OK, gaia_cred->OnUserAuthenticated(signin_result(), &error));
-  EXPECT_TRUE(provider()->credentials_changed_fired());
+  CComPtr<ITestCredentialProvider> test_provider;
+  ASSERT_EQ(S_OK, created_provider().QueryInterface(&test_provider));
+  EXPECT_TRUE(test_provider->credentials_changed_fired());
 }
 
 TEST_F(GcpGaiaCredentialTest, OnUserAuthenticated_SamePassword) {
   USES_CONVERSION;
 
-  CComPtr<IGaiaCredential> gaia_cred;
   CComPtr<ICredentialProviderCredential> cred;
-  ASSERT_EQ(S_OK,
-            CreateGaiaCredentialWithProvider(provider(), &gaia_cred, &cred));
+
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred));
+
+  CComPtr<IGaiaCredential> gaia_cred;
+  ASSERT_EQ(S_OK, cred.QueryInterface(&gaia_cred));
 
   CComBSTR error;
   ASSERT_EQ(S_OK, gaia_cred->OnUserAuthenticated(signin_result(), &error));
 
-  CComBSTR first_sid = provider()->sid();
+  CComPtr<ITestCredentialProvider> test_provider;
+  ASSERT_EQ(S_OK, created_provider().QueryInterface(&test_provider));
+  CComBSTR first_sid = test_provider->sid();
 
   // Report to register the user.
   wchar_t* report_status_text = nullptr;
@@ -100,8 +94,9 @@
   // Finishing with the same username+password should succeed.
   CComBSTR error2;
   ASSERT_EQ(S_OK, gaia_cred->OnUserAuthenticated(signin_result(), &error2));
-  EXPECT_TRUE(provider()->credentials_changed_fired());
-  EXPECT_EQ(first_sid, provider()->sid());
+
+  EXPECT_TRUE(test_provider->credentials_changed_fired());
+  EXPECT_EQ(first_sid, test_provider->sid());
 }
 
 TEST_F(GcpGaiaCredentialTest, OnUserAuthenticated_DiffPassword) {
@@ -120,23 +115,28 @@
           base::UTF8ToUTF16(test_data_storage.GetSuccessId()).c_str(),
           base::UTF8ToUTF16(test_data_storage.GetSuccessEmail()).c_str(),
           &sid));
-  CComPtr<IGaiaCredential> cred;
-  ASSERT_EQ(S_OK, CComCreator<CComObject<CGaiaCredential>>::CreateInstance(
-                      nullptr, IID_IGaiaCredential, (void**)&cred));
-  ASSERT_EQ(S_OK, cred->Initialize(provider()));
+  CComPtr<ICredentialProviderCredential> cred;
+
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred));
+
+  CComPtr<IGaiaCredential> gaia_cred;
+  ASSERT_EQ(S_OK, cred.QueryInterface(&gaia_cred));
 
   CComBSTR error;
-  ASSERT_EQ(S_OK, cred->OnUserAuthenticated(signin_result(), &error));
-  EXPECT_TRUE(provider()->credentials_changed_fired());
+  ASSERT_EQ(S_OK, gaia_cred->OnUserAuthenticated(signin_result(), &error));
 
-  provider()->ResetCredentialsChangedFired();
+  CComPtr<ITestCredentialProvider> test_provider;
+  ASSERT_EQ(S_OK, created_provider().QueryInterface(&test_provider));
+  EXPECT_TRUE(test_provider->credentials_changed_fired());
+
+  test_provider->ResetCredentialsChangedFired();
 
   CComBSTR new_signin_result = MakeSigninResult("password2");
 
   // Finishing with the same username but different password should mark
   // the password as stale and not fire the credentials changed event.
-  EXPECT_EQ(S_FALSE, cred->OnUserAuthenticated(new_signin_result, &error));
-  EXPECT_FALSE(provider()->credentials_changed_fired());
+  EXPECT_EQ(S_FALSE, gaia_cred->OnUserAuthenticated(new_signin_result, &error));
+  EXPECT_FALSE(test_provider->credentials_changed_fired());
 }
 
 class GcpGaiaCredentialGlsRunnerTest : public GlsRunnerTestBase {};
@@ -155,34 +155,29 @@
                       base_gaia_id, base::string16(), &sid));
 
   ASSERT_EQ(2u, fake_os_user_manager()->GetUserCount());
-  FakeGaiaCredentialProvider provider;
 
   // Start logon.
-  CComPtr<IGaiaCredential> gaia_cred;
   CComPtr<ICredentialProviderCredential> cred;
-  ASSERT_EQ(S_OK,
-            CreateGaiaCredentialWithProvider(&provider, &gaia_cred, &cred));
+
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred));
+
+  CComPtr<IGaiaCredential> gaia_cred;
+  ASSERT_EQ(S_OK, cred.QueryInterface(&gaia_cred));
 
   CComPtr<ITestCredential> test;
   ASSERT_EQ(S_OK, cred.QueryInterface(&test));
   ASSERT_EQ(S_OK, test->SetGlsEmailAddress(base::UTF16ToUTF8(base_username) +
                                            "@gmail.com"));
-  ASSERT_EQ(S_OK, run_helper()->StartLogonProcessAndWait(cred));
+  ASSERT_EQ(S_OK, StartLogonProcessAndWait());
 
   // New username should be truncated at the end and have the last character
   // replaced with a new index
   EXPECT_STREQ((base_username.substr(0, base_username.size() - 1) +
                 base::NumberToString16(kInitialDuplicateUsernameIndex))
                    .c_str(),
-               provider.username());
-  EXPECT_NE(0u, provider.password().Length());
-  EXPECT_NE(0u, provider.sid().Length());
-  EXPECT_STREQ(test->GetErrorText(), nullptr);
-  EXPECT_EQ(TRUE, provider.credentials_changed_fired());
+               test->GetFinalUsername());
   // New user should be created.
   EXPECT_EQ(3u, fake_os_user_manager()->GetUserCount());
-
-  EXPECT_EQ(S_OK, gaia_cred->Terminate());
 }
 
 // This test checks the expected success / failure of user creation when
@@ -230,45 +225,38 @@
 
   ASSERT_EQ(static_cast<size_t>(1 + last_user_index + 1),
             fake_os_user_manager()->GetUserCount());
-  FakeGaiaCredentialProvider provider;
 
-  // Start logon.
-  CComPtr<IGaiaCredential> gaia_cred;
+  // Create provider.
   CComPtr<ICredentialProviderCredential> cred;
-  ASSERT_EQ(S_OK,
-            CreateGaiaCredentialWithProvider(&provider, &gaia_cred, &cred));
 
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(0, &cred));
+
+  CComPtr<IGaiaCredential> gaia_cred;
+  ASSERT_EQ(S_OK, cred.QueryInterface(&gaia_cred));
   CComPtr<ITestCredential> test;
   ASSERT_EQ(S_OK, cred.QueryInterface(&test));
 
-  ASSERT_EQ(S_OK, run_helper()->StartLogonProcessAndWait(cred));
+  // Start logon.
+  ASSERT_EQ(S_OK, StartLogonProcessAndWait());
 
   if (should_succeed) {
     EXPECT_STREQ(
         (base_username + base::NumberToString16(last_user_index +
                                                 kInitialDuplicateUsernameIndex))
             .c_str(),
-        provider.username());
-    EXPECT_NE(0u, provider.password().Length());
-    EXPECT_NE(0u, provider.sid().Length());
-    EXPECT_STREQ(test->GetErrorText(), nullptr);
-    EXPECT_EQ(TRUE, provider.credentials_changed_fired());
+        OLE2CW(test->GetFinalUsername()));
     // New user should be created.
     EXPECT_EQ(static_cast<size_t>(last_user_index + 2 + 1),
               fake_os_user_manager()->GetUserCount());
+
+    ASSERT_EQ(S_OK, FinishLogonProcess(true, true, 0));
+
   } else {
-    EXPECT_EQ(0u, provider.username().Length());
-    EXPECT_EQ(0u, provider.password().Length());
-    EXPECT_EQ(0u, provider.sid().Length());
-    EXPECT_STREQ(test->GetErrorText(),
-                 GetStringResource(IDS_INTERNAL_ERROR_BASE).c_str());
-    EXPECT_EQ(FALSE, provider.credentials_changed_fired());
     // No new user should be created.
     EXPECT_EQ(static_cast<size_t>(last_user_index + 1 + 1),
               fake_os_user_manager()->GetUserCount());
+    ASSERT_EQ(S_OK, FinishLogonProcess(false, false, IDS_INTERNAL_ERROR_BASE));
   }
-  // Expect a different user name with the suffix added.
-  EXPECT_EQ(S_OK, gaia_cred->Terminate());
 }
 
 // For a max retry of 10, it is possible to create users 'username',
diff --git a/chrome/credential_provider/gaiacp/gcp_utils.cc b/chrome/credential_provider/gaiacp/gcp_utils.cc
index bdd4aad..43836a5 100644
--- a/chrome/credential_provider/gaiacp/gcp_utils.cc
+++ b/chrome/credential_provider/gaiacp/gcp_utils.cc
@@ -578,6 +578,62 @@
   return hr;
 }
 
+HRESULT LookupLocalizedNameBySid(PSID sid, base::string16* localized_name) {
+  DCHECK(localized_name);
+  std::vector<wchar_t> localized_name_buffer;
+  DWORD group_name_size = 0;
+  std::vector<wchar_t> domain_buffer;
+  DWORD domain_size = 0;
+  SID_NAME_USE use;
+
+  // Get the localized name of the local users group. The function
+  // NetLocalGroupAddMembers only accepts the name of the group and it
+  // may be localized on the system.
+  if (!::LookupAccountSidW(nullptr, sid, nullptr, &group_name_size, nullptr,
+                           &domain_size, &use)) {
+    if (::GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
+      HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
+      LOGFN(ERROR) << "LookupAccountSidW hr=" << putHR(hr);
+      return hr;
+    }
+
+    localized_name_buffer.resize(group_name_size);
+    domain_buffer.resize(domain_size);
+    if (!::LookupAccountSidW(nullptr, sid, localized_name_buffer.data(),
+                             &group_name_size, domain_buffer.data(),
+                             &domain_size, &use)) {
+      HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
+      LOGFN(ERROR) << "LookupAccountSidW hr=" << putHR(hr);
+      return hr;
+    }
+  }
+
+  if (localized_name_buffer.empty()) {
+    LOGFN(ERROR) << "Empty localized name";
+    return E_UNEXPECTED;
+  }
+  *localized_name = base::string16(localized_name_buffer.data(),
+                                   localized_name_buffer.size() - 1);
+
+  return S_OK;
+}
+
+HRESULT LookupLocalizedNameForWellKnownSid(WELL_KNOWN_SID_TYPE sid_type,
+                                           base::string16* localized_name) {
+  BYTE well_known_sid[SECURITY_MAX_SID_SIZE];
+  DWORD size_local_users_group_sid = base::size(well_known_sid);
+
+  // Get the sid for the well known local users group.
+  if (!::CreateWellKnownSid(sid_type, nullptr, well_known_sid,
+                            &size_local_users_group_sid)) {
+    HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
+    LOGFN(ERROR) << "CreateWellKnownSid hr=" << putHR(hr);
+    return hr;
+  }
+
+  return LookupLocalizedNameBySid(well_known_sid, localized_name);
+}
+
 bool VerifyStartupSentinel() {
   // Always try to write to the startup sentinel file. If writing or opening
   // fails for any reason (file locked, no access etc) consider this a failure.
diff --git a/chrome/credential_provider/gaiacp/gcp_utils.h b/chrome/credential_provider/gaiacp/gcp_utils.h
index cc784da4..dd32b01 100644
--- a/chrome/credential_provider/gaiacp/gcp_utils.h
+++ b/chrome/credential_provider/gaiacp/gcp_utils.h
@@ -196,6 +196,15 @@
                                     const wchar_t* entrypoint,
                                     base::CommandLine* command_line);
 
+// Looks up the name associated to the |sid| (if any). Returns an error on any
+// failure or no name is associated with the |sid|.
+HRESULT LookupLocalizedNameBySid(PSID sid, base::string16* localized_name);
+
+// Looks up the name associated to the well known |sid_type| (if any). Returns
+// an error on any failure or no name is associated with the |sid_type|.
+HRESULT LookupLocalizedNameForWellKnownSid(WELL_KNOWN_SID_TYPE sid_type,
+                                           base::string16* localized_name);
+
 // Handles the writing and deletion of a startup sentinel file used to ensure
 // that the GCPW does not crash continuously on startup and render the
 // winlogon process unusable.
diff --git a/chrome/credential_provider/gaiacp/os_user_manager.cc b/chrome/credential_provider/gaiacp/os_user_manager.cc
index 541f039..0d2ce8c 100644
--- a/chrome/credential_provider/gaiacp/os_user_manager.cc
+++ b/chrome/credential_provider/gaiacp/os_user_manager.cc
@@ -234,6 +234,18 @@
                                DWORD* error) {
   DCHECK(sid);
 
+  base::string16 local_users_group_name;
+  // If adding to the local users group, make sure we can get the localized
+  // name for the group before proceeding.
+  if (add_to_users_group) {
+    HRESULT hr = LookupLocalizedNameForWellKnownSid(WinBuiltinUsersSid,
+                                                    &local_users_group_name);
+    if (FAILED(hr)) {
+      LOGFN(ERROR) << "LookupLocalizedNameForWellKnownSid hr=" << putHR(hr);
+      return hr;
+    }
+  }
+
   USER_INFO_1 info;
   memset(&info, 0, sizeof(info));
   info.usri1_comment = _wcsdup(comment);
@@ -279,12 +291,14 @@
     }
 
     if (nsts == NERR_Success && add_to_users_group) {
-      // Add to the "Users" group so that it appears on login screen.
+      // Add to the well known local users group so that it appears on login
+      // screen.
       LOCALGROUP_MEMBERS_INFO_0 member_info;
       memset(&member_info, 0, sizeof(member_info));
       member_info.lgrmi0_sid = user_info->usri4_user_sid;
-      nsts = ::NetLocalGroupAddMembers(
-          nullptr, L"Users", 0, reinterpret_cast<LPBYTE>(&member_info), 1);
+      nsts =
+          ::NetLocalGroupAddMembers(nullptr, local_users_group_name.c_str(), 0,
+                                    reinterpret_cast<LPBYTE>(&member_info), 1);
       if (nsts != NERR_Success && nsts != ERROR_MEMBER_IN_ALIAS) {
         LOGFN(ERROR) << "NetLocalGroupAddMembers nsts=" << nsts;
       } else {
diff --git a/chrome/credential_provider/gaiacp/reauth_credential_unittests.cc b/chrome/credential_provider/gaiacp/reauth_credential_unittests.cc
index 7bbd799..f1a2767c 100644
--- a/chrome/credential_provider/gaiacp/reauth_credential_unittests.cc
+++ b/chrome/credential_provider/gaiacp/reauth_credential_unittests.cc
@@ -25,19 +25,6 @@
 
 namespace testing {
 
-namespace {
-
-HRESULT CreateReauthCredentialWithProvider(
-    IGaiaCredentialProvider* provider,
-    IGaiaCredential** gaia_credential,
-    ICredentialProviderCredential** credential) {
-  return CreateInheritedCredentialWithProvider<CReauthCredential,
-                                               IReauthCredential>(
-      provider, gaia_credential, credential);
-}
-
-}  // namespace
-
 class GcpReauthCredentialTest : public ::testing::Test {
  protected:
   FakeOSUserManager* fake_os_user_manager() { return &fake_os_user_manager_; }
@@ -80,7 +67,9 @@
   ::CoTaskMemFree(sid);
 }
 
-TEST_F(GcpReauthCredentialTest, UserGaiaIdMismatch) {
+class GcpReauthCredentialGlsRunnerTest : public GlsRunnerTestBase {};
+
+TEST_F(GcpReauthCredentialGlsRunnerTest, UserGaiaIdMismatch) {
   USES_CONVERSION;
 
   CredentialProviderSigninDialogTestDataStorage test_data_storage;
@@ -96,10 +85,6 @@
       base::JSONWriter::Write(unexpected_full_result, &signin_result_utf8));
   CComBSTR unexpected_signin_result = A2COLE(signin_result_utf8.c_str());
 
-  // Create a fake credential provider.  This object must outlive the reauth
-  // credential so it should be declared first.
-  FakeGaiaCredentialProvider provider;
-
   CComBSTR username = L"foo_bar";
   CComBSTR full_name = A2COLE(test_data_storage.GetSuccessFullName().c_str());
   CComBSTR password = A2COLE(test_data_storage.GetSuccessPassword().c_str());
@@ -120,43 +105,31 @@
                       base::UTF8ToUTF16(unexpected_gaia_id), base::string16(),
                       &second_sid));
 
-  // Initialize a reauth credential for the valid gaia id.
-  CComPtr<IReauthCredential> reauth;
-  ASSERT_EQ(S_OK, CComCreator<CComObject<CReauthCredential>>::CreateInstance(
-                      nullptr, IID_IReauthCredential, (void**)&reauth));
+  // Create provider and start logon.
+  CComPtr<ICredentialProviderCredential> cred;
 
-  CComPtr<IGaiaCredential> gaia_cred;
-  gaia_cred = reauth;
-  ASSERT_TRUE(!!gaia_cred);
+  // Create with invalid token handle response so that a reauth occurs.
+  SetDefaultTokenHandleResponse(kDefaultInvalidTokenHandleResponse);
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(1, &cred));
 
-  ASSERT_EQ(S_OK, gaia_cred->Initialize(&provider));
+  CComPtr<ITestCredential> test;
+  ASSERT_EQ(S_OK, cred.QueryInterface(&test));
 
-  ASSERT_EQ(S_OK,
-            reauth->SetOSUserInfo(
-                first_sid, CComBSTR(OSUserManager::GetLocalDomain().c_str()),
-                username));
-  ASSERT_EQ(S_OK, reauth->SetEmailForReauth(email));
+  // Force the GLS to return an invalid Gaia Id without reporting the usual
+  // kUiecEMailMissmatch exit code when this happens. This will test whether
+  // the credential can perform necessary validation in case the GLS ever
+  // does not do the validation for us.
+  test->SetGaiaIdOverride(unexpected_gaia_id, /*ignore_expected_gaia_id=*/true);
 
-  // Finishing reauth with an unexpected gaia id should fail.
-  CComBSTR error2;
-  ASSERT_NE(S_OK,
-            gaia_cred->OnUserAuthenticated(unexpected_signin_result, &error2));
-
-  ASSERT_EQ(S_OK, gaia_cred->Terminate());
-
-  EXPECT_EQ(0u, provider.username().Length());
-  EXPECT_EQ(0u, provider.password().Length());
-  EXPECT_EQ(0u, provider.sid().Length());
-  ASSERT_STREQ((BSTR)error2,
-               GetStringResource(IDS_ACCOUNT_IN_USE_BASE).c_str());
-  EXPECT_EQ(FALSE, provider.credentials_changed_fired());
+  // The logon should have failed with an error about another user already
+  // associated to this Google account.
+  ASSERT_EQ(S_OK, FinishLogonProcess(false, false, IDS_ACCOUNT_IN_USE_BASE));
 }
 
-class GcpReauthCredentialGlsRunnerTest : public GlsRunnerTestBase {};
-
 TEST_F(GcpReauthCredentialGlsRunnerTest, NormalReauth) {
   USES_CONVERSION;
   CredentialProviderSigninDialogTestDataStorage test_data_storage;
+
   CComBSTR username = L"foo_bar";
   CComBSTR full_name = A2COLE(test_data_storage.GetSuccessFullName().c_str());
   CComBSTR password = A2COLE(test_data_storage.GetSuccessPassword().c_str());
@@ -170,48 +143,33 @@
                 L"comment", base::UTF8ToUTF16(test_data_storage.GetSuccessId()),
                 OLE2CW(email), &sid));
 
-  FakeGaiaCredentialProvider provider;
-
-  CComPtr<IGaiaCredential> gaia_cred;
+  // Create provider and start logon.
   CComPtr<ICredentialProviderCredential> cred;
-  ASSERT_EQ(S_OK,
-            CreateReauthCredentialWithProvider(&provider, &gaia_cred, &cred));
 
-  CComPtr<IReauthCredential> reauth;
-  reauth = cred;
-  ASSERT_TRUE(!!reauth);
-
-  ASSERT_EQ(S_OK, reauth->SetOSUserInfo(
-                      sid, CComBSTR(OSUserManager::GetLocalDomain().c_str()),
-                      username));
-  ASSERT_EQ(S_OK, reauth->SetEmailForReauth(email));
+  // Create with invalid token handle response so that a reauth occurs.
+  SetDefaultTokenHandleResponse(kDefaultInvalidTokenHandleResponse);
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(1, &cred));
 
   CComPtr<ITestCredential> test;
   ASSERT_EQ(S_OK, cred.QueryInterface(&test));
+
   ASSERT_EQ(S_OK, test->SetGlsEmailAddress(std::string()));
 
-  ASSERT_EQ(S_OK, run_helper()->StartLogonProcessAndWait(cred));
+  ASSERT_EQ(S_OK, StartLogonProcessAndWait());
 
-  ASSERT_EQ(S_OK, gaia_cred->Terminate());
-
-  // Check that values were propagated to the provider.
-  EXPECT_EQ(username, provider.username());
-  EXPECT_EQ(password, provider.password());
-  EXPECT_EQ(sid, provider.sid());
-  EXPECT_EQ(TRUE, provider.credentials_changed_fired());
-
-  ASSERT_STREQ(test->GetErrorText(), NULL);
+  // Teardown of the test should confirm that the logon was successful.
 }
 
 TEST_F(GcpReauthCredentialGlsRunnerTest, NormalReauthWithoutEmail) {
   USES_CONVERSION;
   CredentialProviderSigninDialogTestDataStorage test_data_storage;
+
   CComBSTR username = L"foo_bar";
   CComBSTR full_name = A2COLE(test_data_storage.GetSuccessFullName().c_str());
   CComBSTR password = A2COLE(test_data_storage.GetSuccessPassword().c_str());
   CComBSTR email = A2COLE(test_data_storage.GetSuccessEmail().c_str());
 
-  // Create a fake user to reauth.
+  // Create a fake user to reauth with no e-mail specified.
   CComBSTR sid;
   ASSERT_EQ(S_OK,
             fake_os_user_manager()->CreateTestOSUser(
@@ -219,86 +177,28 @@
                 L"comment", base::UTF8ToUTF16(test_data_storage.GetSuccessId()),
                 base::string16(), &sid));
 
-  FakeGaiaCredentialProvider provider;
-
-  CComPtr<IGaiaCredential> gaia_cred;
+  // Create provider and start logon.
   CComPtr<ICredentialProviderCredential> cred;
-  ASSERT_EQ(S_OK,
-            CreateReauthCredentialWithProvider(&provider, &gaia_cred, &cred));
 
-  CComPtr<IReauthCredential> reauth;
-  reauth = cred;
-  ASSERT_TRUE(!!reauth);
-
-  ASSERT_EQ(S_OK, reauth->SetOSUserInfo(
-                      sid, CComBSTR(OSUserManager::GetLocalDomain().c_str()),
-                      username));
+  // Create with invalid token handle response so that a reauth occurs.
+  SetDefaultTokenHandleResponse(kDefaultInvalidTokenHandleResponse);
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(1, &cred));
 
   CComPtr<ITestCredential> test;
   ASSERT_EQ(S_OK, cred.QueryInterface(&test));
 
-  ASSERT_EQ(S_OK, run_helper()->StartLogonProcessAndWait(cred));
+  ASSERT_EQ(S_OK, StartLogonProcessAndWait());
 
   // Email associated should be the default one
   EXPECT_EQ(test->GetFinalEmail(), kDefaultEmail);
 
-  ASSERT_EQ(S_OK, gaia_cred->Terminate());
-
-  // Check that values were propagated to the provider.
-  EXPECT_EQ(username, provider.username());
-  EXPECT_EQ(password, provider.password());
-  EXPECT_EQ(sid, provider.sid());
-  EXPECT_EQ(TRUE, provider.credentials_changed_fired());
-
-  ASSERT_STREQ(test->GetErrorText(), NULL);
-}
-
-TEST_F(GcpReauthCredentialGlsRunnerTest, NoGaiaIdAssociatedToCredential) {
-  USES_CONVERSION;
-  CredentialProviderSigninDialogTestDataStorage test_data_storage;
-  CComBSTR username = L"foo_bar";
-  CComBSTR full_name = A2COLE(test_data_storage.GetSuccessFullName().c_str());
-  CComBSTR password = A2COLE(test_data_storage.GetSuccessPassword().c_str());
-  CComBSTR email = A2COLE(test_data_storage.GetSuccessEmail().c_str());
-
-  // Create a fake user to reauth.
-  CComBSTR sid;
-  ASSERT_EQ(S_OK, fake_os_user_manager()->CreateTestOSUser(
-                      OLE2CW(username), OLE2CW(password), OLE2CW(full_name),
-                      L"comment", base::string16(), base::string16(), &sid));
-
-  FakeGaiaCredentialProvider provider;
-
-  CComPtr<IGaiaCredential> gaia_cred;
-  CComPtr<ICredentialProviderCredential> cred;
-  ASSERT_EQ(S_OK,
-            CreateReauthCredentialWithProvider(&provider, &gaia_cred, &cred));
-
-  CComPtr<IReauthCredential> reauth;
-  reauth = cred;
-  ASSERT_TRUE(!!reauth);
-
-  ASSERT_EQ(S_OK, reauth->SetOSUserInfo(
-                      sid, CComBSTR(OSUserManager::GetLocalDomain().c_str()),
-                      username));
-  ASSERT_EQ(S_OK, reauth->SetEmailForReauth(email));
-
-  CComPtr<ITestCredential> test;
-  ASSERT_EQ(S_OK, cred.QueryInterface(&test));
-  ASSERT_EQ(S_OK, test->SetGlsEmailAddress(std::string()));
-
-  // This call should fail
-  CREDENTIAL_PROVIDER_GET_SERIALIZATION_RESPONSE cpgsr;
-  CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION cpcs;
-  wchar_t* status_text;
-  CREDENTIAL_PROVIDER_STATUS_ICON status_icon;
-  EXPECT_EQ(E_UNEXPECTED,
-            cred->GetSerialization(&cpgsr, &cpcs, &status_text, &status_icon));
+  // Teardown of the test should confirm that the logon was successful.
 }
 
 TEST_F(GcpReauthCredentialGlsRunnerTest, GaiaIdMismatch) {
   USES_CONVERSION;
   CredentialProviderSigninDialogTestDataStorage test_data_storage;
+
   CComBSTR username = L"foo_bar";
   CComBSTR full_name = A2COLE(test_data_storage.GetSuccessFullName().c_str());
   CComBSTR password = A2COLE(test_data_storage.GetSuccessPassword().c_str());
@@ -310,42 +210,28 @@
             fake_os_user_manager()->CreateTestOSUser(
                 OLE2CW(username), OLE2CW(password), OLE2CW(full_name),
                 L"comment", base::UTF8ToUTF16(test_data_storage.GetSuccessId()),
-                base::string16(), &sid));
+                OLE2CW(email), &sid));
 
   std::string unexpected_gaia_id = "unexpected-gaia-id";
-  FakeGaiaCredentialProvider provider;
 
-  CComPtr<IGaiaCredential> gaia_cred;
+  // Create provider and start logon.
   CComPtr<ICredentialProviderCredential> cred;
-  ASSERT_EQ(S_OK,
-            CreateReauthCredentialWithProvider(&provider, &gaia_cred, &cred));
 
-  CComPtr<IReauthCredential> reauth;
-  reauth = cred;
-  ASSERT_TRUE(!!reauth);
+  // Create with invalid token handle response so that a reauth occurs.
+  SetDefaultTokenHandleResponse(kDefaultInvalidTokenHandleResponse);
+  ASSERT_EQ(S_OK, InitializeProviderAndGetCredential(1, &cred));
 
-  ASSERT_EQ(S_OK, reauth->SetOSUserInfo(
-                      sid, CComBSTR(OSUserManager::GetLocalDomain().c_str()),
-                      username));
-  ASSERT_EQ(S_OK, reauth->SetEmailForReauth(email));
-
-  CComPtr<testing::ITestCredential> test;
+  CComPtr<ITestCredential> test;
   ASSERT_EQ(S_OK, cred.QueryInterface(&test));
+
   ASSERT_EQ(S_OK, test->SetGlsEmailAddress(std::string()));
-  ASSERT_EQ(S_OK, test->SetGaiaIdOverride(unexpected_gaia_id));
+  ASSERT_EQ(S_OK, test->SetGaiaIdOverride(unexpected_gaia_id,
+                                          /*ignore_expected_gaia_id=*/false));
 
-  ASSERT_EQ(S_OK, run_helper()->StartLogonProcessAndWait(cred));
+  ASSERT_EQ(S_OK, StartLogonProcessAndWait());
 
-  ASSERT_EQ(S_OK, gaia_cred->Terminate());
-
-  // Check that values were not propagated to the provider.
-  EXPECT_EQ(0u, provider.username().Length());
-  EXPECT_EQ(0u, provider.password().Length());
-  EXPECT_EQ(0u, provider.sid().Length());
-  EXPECT_EQ(FALSE, provider.credentials_changed_fired());
-
-  ASSERT_STREQ(test->GetErrorText(),
-               GetStringResource(IDS_EMAIL_MISMATCH_BASE).c_str());
+  // The logon should have failed with an email mismatch error.
+  ASSERT_EQ(S_OK, FinishLogonProcess(false, false, IDS_EMAIL_MISMATCH_BASE));
 }
 
 }  // namespace testing
diff --git a/chrome/credential_provider/test/BUILD.gn b/chrome/credential_provider/test/BUILD.gn
index 8cf6fe0..f6dfac8 100644
--- a/chrome/credential_provider/test/BUILD.gn
+++ b/chrome/credential_provider/test/BUILD.gn
@@ -14,8 +14,6 @@
     "../gaiacp/reauth_credential_unittests.cc",
     "com_fakes.cc",
     "com_fakes.h",
-    "fake_gls_run_helper.cc",
-    "fake_gls_run_helper.h",
     "gcp_fakes.cc",
     "gcp_fakes.h",
     "gcp_gls_output_unittest.cc",
@@ -24,6 +22,7 @@
     "gls_runner_test_base.cc",
     "gls_runner_test_base.h",
     "test_credential.h",
+    "test_credential_provider.h",
   ]
 
   deps = [
diff --git a/chrome/credential_provider/test/com_fakes.cc b/chrome/credential_provider/test/com_fakes.cc
index 80fe137f..b98631c8 100644
--- a/chrome/credential_provider/test/com_fakes.cc
+++ b/chrome/credential_provider/test/com_fakes.cc
@@ -7,13 +7,63 @@
 #include <sddl.h>  // For ConvertSidToStringSid()
 
 #include "base/logging.h"
+#include "chrome/credential_provider/gaiacp/gaia_credential.h"
+#include "chrome/credential_provider/gaiacp/gaia_credential_other_user.h"
 #include "chrome/credential_provider/gaiacp/os_user_manager.h"
+#include "chrome/credential_provider/gaiacp/reauth_credential.h"
 #include "chrome/credential_provider/gaiacp/stdafx.h"
 #include "chrome/credential_provider/test/test_credential.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 namespace credential_provider {
 
+namespace testing {
+
+// This class is used to implement a test credential based off a
+// CGaiaCredential.
+class ATL_NO_VTABLE CTestGaiaCredential
+    : public CTestCredentialBase<CGaiaCredential> {
+ public:
+  DECLARE_NO_REGISTRY()
+
+  CTestGaiaCredential();
+  ~CTestGaiaCredential();
+
+ private:
+  BEGIN_COM_MAP(CTestGaiaCredential)
+  COM_INTERFACE_ENTRY(IGaiaCredential)
+  COM_INTERFACE_ENTRY(ICredentialProviderCredential)
+  COM_INTERFACE_ENTRY(ITestCredential)
+  END_COM_MAP()
+};
+
+CTestGaiaCredential::CTestGaiaCredential() = default;
+
+CTestGaiaCredential::~CTestGaiaCredential() = default;
+
+// This class is used to implement a test credential based off a
+// COtherUserGaiaCredential.
+class ATL_NO_VTABLE CTestOtherUserGaiaCredential
+    : public CTestCredentialBase<COtherUserGaiaCredential> {
+ public:
+  DECLARE_NO_REGISTRY()
+
+  CTestOtherUserGaiaCredential();
+  ~CTestOtherUserGaiaCredential();
+
+ private:
+  BEGIN_COM_MAP(CTestOtherUserGaiaCredential)
+  COM_INTERFACE_ENTRY(IGaiaCredential)
+  COM_INTERFACE_ENTRY(ICredentialProviderCredential)
+  COM_INTERFACE_ENTRY(ICredentialProviderCredential2)
+  COM_INTERFACE_ENTRY(ITestCredential)
+  END_COM_MAP()
+};
+
+CTestOtherUserGaiaCredential::CTestOtherUserGaiaCredential() = default;
+
+CTestOtherUserGaiaCredential::~CTestOtherUserGaiaCredential() = default;
+
 #define IMPL_IUNKOWN_NOQI_WITH_REF(cls)                               \
   IFACEMETHODIMP cls::QueryInterface(REFIID riid, void** ppv) {       \
     return E_NOTIMPL;                                                 \
@@ -34,21 +84,19 @@
   EXPECT_EQ(ref_count_, 1u);
 }
 
-HRESULT STDMETHODCALLTYPE FakeCredentialProviderUser::GetSid(wchar_t** sid) {
+HRESULT FakeCredentialProviderUser::GetSid(wchar_t** sid) {
   DWORD length = sid_.length() + 1;
   *sid = static_cast<wchar_t*>(::CoTaskMemAlloc(length * sizeof(wchar_t)));
   EXPECT_EQ(0, wcscpy_s(*sid, length, sid_.c_str()));
   return S_OK;
 }
 
-HRESULT STDMETHODCALLTYPE
-FakeCredentialProviderUser::GetProviderID(GUID* providerID) {
+HRESULT FakeCredentialProviderUser::GetProviderID(GUID* providerID) {
   return E_NOTIMPL;
 }
 
-HRESULT STDMETHODCALLTYPE
-FakeCredentialProviderUser::GetStringValue(REFPROPERTYKEY key,
-                                           wchar_t** value) {
+HRESULT FakeCredentialProviderUser::GetStringValue(REFPROPERTYKEY key,
+                                                   wchar_t** value) {
   if (key != PKEY_Identity_UserName)
     return E_INVALIDARG;
 
@@ -58,8 +106,8 @@
   return S_OK;
 }
 
-HRESULT STDMETHODCALLTYPE
-FakeCredentialProviderUser::GetValue(REFPROPERTYKEY key, PROPVARIANT* value) {
+HRESULT FakeCredentialProviderUser::GetValue(REFPROPERTYKEY key,
+                                             PROPVARIANT* value) {
   return E_NOTIMPL;
 }
 
@@ -118,19 +166,55 @@
 
 ///////////////////////////////////////////////////////////////////////////////
 
-FakeGaiaCredentialProvider::FakeGaiaCredentialProvider() {}
-
-FakeGaiaCredentialProvider::~FakeGaiaCredentialProvider() {
-  EXPECT_EQ(ref_count_, 1u);
+CTestGaiaCredentialProvider::CTestGaiaCredentialProvider() {
+  // Set functions for creating test credentials of all types.
+  SetCredentialCreatorFunctionsForTesting(
+      [](CGaiaCredentialProvider::GaiaCredentialComPtrStorage*
+             cred_ptr_storage) {
+        return CComCreator<CComObject<CTestGaiaCredential>>::CreateInstance(
+            nullptr, IID_IGaiaCredential,
+            reinterpret_cast<void**>(&cred_ptr_storage->gaia_cred));
+      },
+      [](CGaiaCredentialProvider::GaiaCredentialComPtrStorage*
+             cred_ptr_storage) {
+        return CComCreator<CComObject<CTestOtherUserGaiaCredential>>::
+            CreateInstance(
+                nullptr, IID_IGaiaCredential,
+                reinterpret_cast<void**>(&cred_ptr_storage->gaia_cred));
+      },
+      [](CGaiaCredentialProvider::GaiaCredentialComPtrStorage*
+             cred_ptr_storage) {
+        return CComCreator<CComObject<testing::CTestCredentialForInherited<
+            CReauthCredential, IReauthCredential>>>::
+            CreateInstance(
+                nullptr, IID_IGaiaCredential,
+                reinterpret_cast<void**>(&cred_ptr_storage->gaia_cred));
+      });
 }
 
-HRESULT FakeGaiaCredentialProvider::GetUsageScenario(DWORD* cpus) {
-  DCHECK(cpus);
-  *cpus = static_cast<DWORD>(cpus_);
-  return S_OK;
+CTestGaiaCredentialProvider::~CTestGaiaCredentialProvider() {}
+
+const CComBSTR& CTestGaiaCredentialProvider::username() const {
+  return username_;
 }
 
-HRESULT FakeGaiaCredentialProvider::OnUserAuthenticated(
+const CComBSTR& CTestGaiaCredentialProvider::password() const {
+  return password_;
+}
+
+const CComBSTR& CTestGaiaCredentialProvider::sid() const {
+  return sid_;
+}
+
+bool CTestGaiaCredentialProvider::credentials_changed_fired() const {
+  return credentials_changed_fired_;
+}
+
+void CTestGaiaCredentialProvider::ResetCredentialsChangedFired() {
+  credentials_changed_fired_ = FALSE;
+}
+
+HRESULT CTestGaiaCredentialProvider::OnUserAuthenticatedImpl(
     IUnknown* credential,
     BSTR username,
     BSTR password,
@@ -140,9 +224,10 @@
   password_ = password;
   sid_ = sid;
   credentials_changed_fired_ = fire_credentials_changed;
-  return S_OK;
+  return CGaiaCredentialProvider::OnUserAuthenticatedImpl(
+      credential, username, password, sid, fire_credentials_changed);
 }
 
-IMPL_IUNKOWN_NOQI_WITH_REF(FakeGaiaCredentialProvider)
+}  // namespace testing
 
 }  // namespace credential_provider
diff --git a/chrome/credential_provider/test/com_fakes.h b/chrome/credential_provider/test/com_fakes.h
index f9bcdbb..4a8823b 100644
--- a/chrome/credential_provider/test/com_fakes.h
+++ b/chrome/credential_provider/test/com_fakes.h
@@ -12,10 +12,14 @@
 #include <vector>
 
 #include "base/strings/string16.h"
+#include "chrome/credential_provider/gaiacp/gaia_credential_provider.h"
 #include "chrome/credential_provider/gaiacp/gaia_credential_provider_i.h"
+#include "chrome/credential_provider/test/test_credential_provider.h"
 
 namespace credential_provider {
 
+namespace testing {
+
 #define DECLARE_IUNKOWN_NOQI_WITH_REF()                            \
   IFACEMETHODIMP QueryInterface(REFIID riid, void** ppv) override; \
   ULONG STDMETHODCALLTYPE AddRef() override;                       \
@@ -94,38 +98,45 @@
 
 ///////////////////////////////////////////////////////////////////////////////
 
-// Fake the GaiaCredentialProvider COM object.
-class FakeGaiaCredentialProvider : public IGaiaCredentialProvider {
+// Test implementation of GaiaCredentialProvider that stores information from
+// OnUserAuthenticatedImpl.
+class CTestGaiaCredentialProvider : public CGaiaCredentialProvider,
+                                    public ITestCredentialProvider {
  public:
-  FakeGaiaCredentialProvider();
-  virtual ~FakeGaiaCredentialProvider();
+  const CComBSTR& STDMETHODCALLTYPE username() const override;
+  const CComBSTR& STDMETHODCALLTYPE password() const override;
+  const CComBSTR& STDMETHODCALLTYPE sid() const override;
+  bool STDMETHODCALLTYPE credentials_changed_fired() const override;
+  void STDMETHODCALLTYPE ResetCredentialsChangedFired() override;
 
-  const CComBSTR& username() const { return username_; }
-  const CComBSTR& password() const { return password_; }
-  const CComBSTR& sid() const { return sid_; }
-  bool credentials_changed_fired() const { return credentials_changed_fired_; }
-  void ResetCredentialsChangedFired() { credentials_changed_fired_ = FALSE; }
-  void SetUsageScenario(CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus) {
-    cpus_ = cpus;
-  }
+  BEGIN_COM_MAP(CTestGaiaCredentialProvider)
+  COM_INTERFACE_ENTRY(IGaiaCredentialProvider)
+  COM_INTERFACE_ENTRY(ICredentialProviderSetUserArray)
+  COM_INTERFACE_ENTRY(ICredentialProvider)
+  COM_INTERFACE_ENTRY(ICredentialUpdateEventsHandler)
+  COM_INTERFACE_ENTRY(ITestCredentialProvider)
+  END_COM_MAP()
 
-  // IGaiaCredentialProvider
-  IFACEMETHODIMP GetUsageScenario(DWORD* cpus) override;
-  DECLARE_IUNKOWN_NOQI_WITH_REF()
-  IFACEMETHODIMP OnUserAuthenticated(IUnknown* credential,
-                                     BSTR username,
-                                     BSTR password,
-                                     BSTR sid,
-                                     BOOL fire_credentials_changed) override;
+ protected:
+  // CGaiaCredentialProvider
+  HRESULT OnUserAuthenticatedImpl(IUnknown* credential,
+                                  BSTR username,
+                                  BSTR password,
+                                  BSTR sid,
+                                  BOOL fire_credentials_changed) override;
+
+  CTestGaiaCredentialProvider();
+  ~CTestGaiaCredentialProvider() override;
 
  private:
   CComBSTR username_;
   CComBSTR password_;
   CComBSTR sid_;
   BOOL credentials_changed_fired_ = FALSE;
-  CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus_ = CPUS_LOGON;
 };
 
+}  // namespace testing
+
 }  // namespace credential_provider
 
 #endif  // CHROME_CREDENTIAL_PROVIDER_TEST_COM_FAKES_H_
diff --git a/chrome/credential_provider/test/fake_gls_run_helper.cc b/chrome/credential_provider/test/fake_gls_run_helper.cc
deleted file mode 100644
index 70f2f06b..0000000
--- a/chrome/credential_provider/test/fake_gls_run_helper.cc
+++ /dev/null
@@ -1,197 +0,0 @@
-// Copyright 2018 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 "fake_gls_run_helper.h"
-
-#include "chrome/credential_provider/gaiacp/stdafx.h"
-
-#include "base/base_switches.h"
-#include "base/command_line.h"
-#include "base/json/json_writer.h"
-#include "base/strings/string_number_conversions.h"
-#include "base/strings/utf_string_conversions.h"
-#include "base/test/multiprocess_test.h"
-#include "chrome/credential_provider/gaiacp/gaia_credential_provider_i.h"
-#include "chrome/credential_provider/gaiacp/scoped_lsa_policy.h"
-#include "chrome/credential_provider/test/gcp_fakes.h"
-#include "chrome/credential_provider/test/test_credential.h"
-#include "testing/gtest/include/gtest/gtest.h"
-#include "testing/multiprocess_func_list.h"
-
-namespace credential_provider {
-
-namespace switches {
-
-constexpr char kDefaultExitCode[] = "default-exit-code";
-constexpr char kGlsUserEmail[] = "gls-user-email";
-constexpr char kStartGlsEventName[] = "start-gls-event-name";
-constexpr char kOverrideGaiaId[] = "override-gaia-id";
-
-}  // namespace switches
-
-namespace testing {
-
-// Corresponding default email and username for tests that don't override them.
-const char kDefaultEmail[] = "foo@gmail.com";
-const char kDefaultGaiaId[] = "test-gaia-id";
-const wchar_t kDefaultUsername[] = L"foo";
-
-namespace {
-
-// Generates a common signin result given an email pass through the command
-// line and writes this result to stdout.  This is used as a fake GLS process
-// for testing.
-MULTIPROCESS_TEST_MAIN(gls_main) {
-  base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
-
-  // If a start event name is specified, the process waits for an event from the
-  // tester telling it that it can start running.
-  if (command_line->HasSwitch(switches::kStartGlsEventName)) {
-    base::string16 start_event_name =
-        command_line->GetSwitchValueNative(switches::kStartGlsEventName);
-    if (!start_event_name.empty()) {
-      base::win::ScopedHandle start_event_handle(::CreateEvent(
-          nullptr, false, false, base::UTF16ToWide(start_event_name).c_str()));
-      if (start_event_handle.IsValid()) {
-        base::WaitableEvent start_event(std::move(start_event_handle));
-        start_event.Wait();
-      }
-    }
-  }
-
-  int default_exit_code = kUiecSuccess;
-  EXPECT_TRUE(base::StringToInt(
-      command_line->GetSwitchValueASCII(switches::kDefaultExitCode),
-      &default_exit_code));
-  std::string gls_email =
-      command_line->GetSwitchValueASCII(switches::kGlsUserEmail);
-  std::string gaia_id_override =
-      command_line->GetSwitchValueASCII(switches::kOverrideGaiaId);
-  std::string expected_gaia_id =
-      command_line->GetSwitchValueASCII(kGaiaIdSwitch);
-  std::string expected_email =
-      command_line->GetSwitchValueASCII(kPrefillEmailSwitch);
-  if (expected_email.empty()) {
-    expected_email = gls_email;
-  } else {
-    EXPECT_EQ(gls_email, std::string());
-  }
-  if (expected_gaia_id.empty())
-    expected_gaia_id = kDefaultGaiaId;
-  base::Value dict(base::Value::Type::DICTIONARY);
-  if (!gaia_id_override.empty() && gaia_id_override != expected_gaia_id) {
-    dict.SetIntKey(kKeyExitCode, kUiecEMailMissmatch);
-  } else {
-    dict.SetIntKey(kKeyExitCode, static_cast<UiExitCodes>(default_exit_code));
-    dict.SetStringKey(kKeyEmail, expected_email);
-    dict.SetStringKey(kKeyFullname, "Full Name");
-    dict.SetStringKey(kKeyId, expected_gaia_id);
-    dict.SetStringKey(kKeyMdmIdToken, "idt-123456");
-    dict.SetStringKey(kKeyPassword, "password");
-    dict.SetStringKey(kKeyRefreshToken, "rt-123456");
-    dict.SetStringKey(kKeyTokenHandle, "th-123456");
-  }
-
-  std::string json;
-  if (!base::JSONWriter::Write(dict, &json))
-    return -1;
-
-  HANDLE hstdout = ::GetStdHandle(STD_OUTPUT_HANDLE);
-  DWORD written;
-  if (::WriteFile(hstdout, json.c_str(), json.length(), &written, nullptr)) {
-    return 0;
-  }
-
-  return -1;
-}
-}  // namespace
-
-FakeGlsRunHelper::FakeGlsRunHelper(FakeOSUserManager* fake_os_user_manager) {
-  // Create the special gaia account used to run GLS and save its password.
-
-  BSTR sid;
-  DWORD error;
-  EXPECT_EQ(S_OK, fake_os_user_manager->AddUser(
-                      kDefaultGaiaAccountName, L"password", L"fullname",
-                      L"comment", true, &sid, &error));
-
-  auto policy = ScopedLsaPolicy::Create(POLICY_ALL_ACCESS);
-  EXPECT_EQ(S_OK, policy->StorePrivateData(kLsaKeyGaiaUsername,
-                                           kDefaultGaiaAccountName));
-  EXPECT_EQ(S_OK, policy->StorePrivateData(kLsaKeyGaiaPassword, L"password"));
-}
-
-FakeGlsRunHelper::~FakeGlsRunHelper() = default;
-
-HRESULT FakeGlsRunHelper::StartLogonProcess(ICredentialProviderCredential* cred,
-                                            bool succeeds) {
-  BOOL auto_login;
-  EXPECT_EQ(S_OK, cred->SetSelected(&auto_login));
-
-  // Logging on is an async process, so the call to GetSerialization() starts
-  // the process, but when it returns it has not completed.
-  CREDENTIAL_PROVIDER_GET_SERIALIZATION_RESPONSE cpgsr;
-  CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION cpcs;
-  wchar_t* status_text;
-  CREDENTIAL_PROVIDER_STATUS_ICON status_icon;
-  EXPECT_EQ(S_OK,
-            cred->GetSerialization(&cpgsr, &cpcs, &status_text, &status_icon));
-  EXPECT_EQ(CPSI_NONE, status_icon);
-  if (succeeds) {
-    EXPECT_EQ(nullptr, status_text);
-    EXPECT_EQ(CPGSR_NO_CREDENTIAL_NOT_FINISHED, cpgsr);
-  } else {
-    EXPECT_NE(nullptr, status_text);
-    EXPECT_EQ(CPGSR_NO_CREDENTIAL_FINISHED, cpgsr);
-  }
-  return S_OK;
-}
-
-HRESULT FakeGlsRunHelper::WaitForLogonProcess(
-    ICredentialProviderCredential* cred) {
-  CComPtr<testing::ITestCredential> test;
-  HRESULT hr = cred->QueryInterface(__uuidof(testing::ITestCredential),
-                                    reinterpret_cast<void**>(&test));
-  if (FAILED(hr))
-    return hr;
-  return test->WaitForGls();
-}
-
-HRESULT FakeGlsRunHelper::StartLogonProcessAndWait(
-    ICredentialProviderCredential* cred) {
-  HRESULT hr = StartLogonProcess(cred, /*succeeds=*/true);
-  if (FAILED(hr))
-    return hr;
-  return WaitForLogonProcess(cred);
-}
-
-// static
-HRESULT FakeGlsRunHelper::GetFakeGlsCommandline(
-    UiExitCodes default_exit_code,
-    const std::string& gls_email,
-    const std::string& gaia_id_override,
-    const base::string16& start_gls_event_name,
-    base::CommandLine* command_line) {
-  *command_line = base::GetMultiProcessTestChildBaseCommandLine();
-  command_line->AppendSwitchASCII(::switches::kTestChildProcess, "gls_main");
-  command_line->AppendSwitchASCII(switches::kGlsUserEmail, gls_email);
-  command_line->AppendSwitchNative(switches::kDefaultExitCode,
-                                   base::NumberToString16(default_exit_code));
-
-  if (!gaia_id_override.empty()) {
-    command_line->AppendSwitchASCII(switches::kOverrideGaiaId,
-                                    gaia_id_override);
-  }
-
-  if (!start_gls_event_name.empty()) {
-    command_line->AppendSwitchNative(switches::kStartGlsEventName,
-                                     start_gls_event_name);
-  }
-
-  return S_OK;
-}
-
-}  // namespace testing
-
-}  // namespace credential_provider
diff --git a/chrome/credential_provider/test/fake_gls_run_helper.h b/chrome/credential_provider/test/fake_gls_run_helper.h
deleted file mode 100644
index f870adf8..0000000
--- a/chrome/credential_provider/test/fake_gls_run_helper.h
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright 2018 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_CREDENTIAL_PROVIDER_TEST_FAKE_GLS_RUN_HELPER_H_
-#define CHROME_CREDENTIAL_PROVIDER_TEST_FAKE_GLS_RUN_HELPER_H_
-
-#include <atlcomcli.h>
-
-#include "base/strings/string16.h"
-#include "chrome/credential_provider/common/gcp_strings.h"
-
-struct ICredentialProviderCredential;
-
-namespace base {
-class CommandLine;
-}
-
-namespace credential_provider {
-
-class FakeOSUserManager;
-
-namespace testing {
-
-extern const char kDefaultEmail[];
-extern const char kDefaultGaiaId[];
-extern const wchar_t kDefaultUsername[];
-
-// Helper class used to run and wait for a fake GLS process to validate the
-// functionality of a GCPW credential.
-class FakeGlsRunHelper {
- public:
-  explicit FakeGlsRunHelper(FakeOSUserManager* fake_os_user_manager);
-  ~FakeGlsRunHelper();
-
-  HRESULT StartLogonProcess(ICredentialProviderCredential* cred, bool succeeds);
-  HRESULT WaitForLogonProcess(ICredentialProviderCredential* cred);
-  HRESULT StartLogonProcessAndWait(ICredentialProviderCredential* cred);
-
-  // Gets a command line that runs a fake GLS that produces the desired output.
-  // |default_exit_code| is the default value that will be written unless the
-  // other command line arguments require a specific error code to be returned.
-  static HRESULT GetFakeGlsCommandline(
-      UiExitCodes default_exit_code,
-      const std::string& gls_email,
-      const std::string& gaia_id_override,
-      const base::string16& start_gls_event_name,
-      base::CommandLine* command_line);
-};
-
-}  // namespace testing
-
-}  // namespace credential_provider
-
-#endif  // CHROME_CREDENTIAL_PROVIDER_TEST_FAKE_GLS_RUN_HELPER_H_
diff --git a/chrome/credential_provider/test/gcp_fakes.cc b/chrome/credential_provider/test/gcp_fakes.cc
index f26f199..613664d 100644
--- a/chrome/credential_provider/test/gcp_fakes.cc
+++ b/chrome/credential_provider/test/gcp_fakes.cc
@@ -143,6 +143,9 @@
   if (error)
     *error = 0;
 
+  if (should_fail_user_creation_)
+    return E_FAIL;
+
   bool user_found = username_to_info_.count(username) > 0;
 
   if (user_found) {
@@ -388,6 +391,16 @@
   return S_OK;
 }
 
+std::vector<std::pair<base::string16, base::string16>>
+FakeOSUserManager::GetUsers() const {
+  std::vector<std::pair<base::string16, base::string16>> users;
+
+  for (auto& kv : username_to_info_)
+    users.emplace_back(std::make_pair(kv.second.sid, kv.first));
+
+  return users;
+}
+
 ///////////////////////////////////////////////////////////////////////////////
 
 FakeScopedLsaPolicyFactory::FakeScopedLsaPolicyFactory()
diff --git a/chrome/credential_provider/test/gcp_fakes.h b/chrome/credential_provider/test/gcp_fakes.h
index d471c05c..653db567 100644
--- a/chrome/credential_provider/test/gcp_fakes.h
+++ b/chrome/credential_provider/test/gcp_fakes.h
@@ -101,6 +101,11 @@
   HRESULT ModifyUserAccessWithLogonHours(const wchar_t* domain,
                                          const wchar_t* username,
                                          bool allow) override;
+
+  void SetShouldFailUserCreation(bool should_fail) {
+    should_fail_user_creation_ = should_fail;
+  }
+
   struct UserInfo {
     UserInfo(const wchar_t* domain,
              const wchar_t* password,
@@ -138,11 +143,13 @@
                            BSTR* sid);
 
   size_t GetUserCount() const { return username_to_info_.size(); }
+  std::vector<std::pair<base::string16, base::string16>> GetUsers() const;
 
  private:
   OSUserManager* original_manager_;
   DWORD next_rid_ = 0;
   std::map<base::string16, UserInfo> username_to_info_;
+  bool should_fail_user_creation_ = false;
 };
 
 ///////////////////////////////////////////////////////////////////////////////
@@ -294,7 +301,8 @@
   explicit FakeAssociatedUserValidator(base::TimeDelta validation_timeout);
   ~FakeAssociatedUserValidator() override;
 
-  using AssociatedUserValidator::IsUserAccessBlocked;
+  using AssociatedUserValidator::ForceRefreshTokenHandlesForTesting;
+  using AssociatedUserValidator::IsUserAccessBlockedForTesting;
 
  private:
   AssociatedUserValidator* original_validator_ = nullptr;
diff --git a/chrome/credential_provider/test/gls_runner_test_base.cc b/chrome/credential_provider/test/gls_runner_test_base.cc
index 6cc03139..c3857eb 100644
--- a/chrome/credential_provider/test/gls_runner_test_base.cc
+++ b/chrome/credential_provider/test/gls_runner_test_base.cc
@@ -4,20 +4,589 @@
 
 #include "gls_runner_test_base.h"
 
+#include "base/base_switches.h"
+#include "base/command_line.h"
+#include "base/json/json_writer.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/utf_string_conversions.h"
+#include "base/test/multiprocess_test.h"
+#include "chrome/credential_provider/gaiacp/gaia_credential_provider_filter.h"
+#include "chrome/credential_provider/gaiacp/scoped_lsa_policy.h"
+#include "chrome/credential_provider/test/test_credential.h"
+#include "testing/multiprocess_func_list.h"
+
 namespace credential_provider {
 
+namespace switches {
+
+constexpr char kDefaultExitCode[] = "default-exit-code";
+constexpr char kIgnoreExpectedGaiaId[] = "ignore-expected-gaia-id";
+constexpr char kGlsUserEmail[] = "gls-user-email";
+constexpr char kStartGlsEventName[] = "start-gls-event-name";
+constexpr char kOverrideGaiaId[] = "override-gaia-id";
+
+}  // namespace switches
+
 namespace testing {
 
-GlsRunnerTestBase::GlsRunnerTestBase() : run_helper_(&fake_os_user_manager_) {}
+// Corresponding default email and username for tests that don't override them.
+const char kDefaultEmail[] = "foo@gmail.com";
+const char kDefaultGaiaId[] = "test-gaia-id";
+const wchar_t kDefaultUsername[] = L"foo";
+const char kDefaultInvalidTokenHandleResponse[] = "{}";
+const char kDefaultValidTokenHandleResponse[] = "{\"expires_in\":1}";
+
+namespace {
+
+// Generates a common signin result given an email pass through the command
+// line and writes this result to stdout.  This is used as a fake GLS process
+// for testing.
+MULTIPROCESS_TEST_MAIN(gls_main) {
+  base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
+
+  // If a start event name is specified, the process waits for an event from the
+  // tester telling it that it can start running.
+  if (command_line->HasSwitch(switches::kStartGlsEventName)) {
+    base::string16 start_event_name =
+        command_line->GetSwitchValueNative(switches::kStartGlsEventName);
+    if (!start_event_name.empty()) {
+      base::win::ScopedHandle start_event_handle(::CreateEvent(
+          nullptr, false, false, base::UTF16ToWide(start_event_name).c_str()));
+      if (start_event_handle.IsValid()) {
+        base::WaitableEvent start_event(std::move(start_event_handle));
+        start_event.Wait();
+      }
+    }
+  }
+
+  int default_exit_code = kUiecSuccess;
+  EXPECT_TRUE(base::StringToInt(
+      command_line->GetSwitchValueASCII(switches::kDefaultExitCode),
+      &default_exit_code));
+  std::string gls_email =
+      command_line->GetSwitchValueASCII(switches::kGlsUserEmail);
+  std::string gaia_id_override =
+      command_line->GetSwitchValueASCII(switches::kOverrideGaiaId);
+  std::string expected_gaia_id =
+      command_line->GetSwitchValueASCII(kGaiaIdSwitch);
+  std::string expected_email =
+      command_line->GetSwitchValueASCII(kPrefillEmailSwitch);
+  if (expected_email.empty()) {
+    expected_email = gls_email;
+  } else {
+    EXPECT_EQ(gls_email, std::string());
+  }
+  if (expected_gaia_id.empty())
+    expected_gaia_id = kDefaultGaiaId;
+
+  if (command_line->HasSwitch(switches::kIgnoreExpectedGaiaId)) {
+    DCHECK(!gaia_id_override.empty());
+    expected_gaia_id = gaia_id_override;
+  }
+
+  base::Value dict(base::Value::Type::DICTIONARY);
+  if (!gaia_id_override.empty() && gaia_id_override != expected_gaia_id) {
+    dict.SetIntKey(kKeyExitCode, kUiecEMailMissmatch);
+  } else {
+    dict.SetIntKey(kKeyExitCode, static_cast<UiExitCodes>(default_exit_code));
+    dict.SetStringKey(kKeyEmail, expected_email);
+    dict.SetStringKey(kKeyFullname, "Full Name");
+    dict.SetStringKey(kKeyId, expected_gaia_id);
+    dict.SetStringKey(kKeyMdmIdToken, "idt-123456");
+    dict.SetStringKey(kKeyPassword, "password");
+    dict.SetStringKey(kKeyRefreshToken, "rt-123456");
+    dict.SetStringKey(kKeyTokenHandle, "th-123456");
+  }
+
+  std::string json;
+  if (!base::JSONWriter::Write(dict, &json))
+    return -1;
+
+  HANDLE hstdout = ::GetStdHandle(STD_OUTPUT_HANDLE);
+  DWORD written;
+  if (::WriteFile(hstdout, json.c_str(), json.length(), &written, nullptr)) {
+    return 0;
+  }
+
+  return -1;
+}
+
+}  // namespace
+
+GlsRunnerTestBase::GlsRunnerTestBase()
+    : cpus_(CPUS_LOGON),
+      default_token_handle_response_(kDefaultValidTokenHandleResponse) {}
 
 GlsRunnerTestBase::~GlsRunnerTestBase() = default;
 
 void GlsRunnerTestBase::SetUp() {
+  // Create the special gaia account used to run GLS and save its password.
+  BSTR sid;
+  DWORD error;
+  EXPECT_EQ(S_OK, fake_os_user_manager()->AddUser(
+                      kDefaultGaiaAccountName, L"password", L"fullname",
+                      L"comment", true, &sid, &error));
+
+  auto policy = ScopedLsaPolicy::Create(POLICY_ALL_ACCESS);
+  EXPECT_EQ(S_OK, policy->StorePrivateData(kLsaKeyGaiaUsername,
+                                           kDefaultGaiaAccountName));
+  EXPECT_EQ(S_OK, policy->StorePrivateData(kLsaKeyGaiaPassword, L"password"));
+
   // Make sure not to read random GCPW settings from the machine that is running
   // the tests.
   InitializeRegistryOverrideForTesting(&registry_override_);
 }
 
+void GlsRunnerTestBase::TearDown() {
+  // If credential has not been explicitly completed and the logon process
+  // was started, then complete here under the assumption that it will
+  // complete successfully.
+  ASSERT_EQ(S_OK, FinishLogonProcess(true, true, 0));
+
+  ASSERT_EQ(S_OK, ReleaseProvider());
+}
+
+HRESULT GlsRunnerTestBase::ReleaseProvider() {
+  if (!gaia_provider_)
+    return S_OK;
+
+  // If any logon are still pending, they are about to be killed now.
+  logon_process_started_successfully_ = false;
+
+  HRESULT hr = S_OK;
+  // Complete the release of the provider.
+  // Unadvise all the credentials.
+  DWORD count;
+  DWORD default_index;
+  BOOL autologon;
+  HRESULT get_count_hr =
+      gaia_provider_->GetCredentialCount(&count, &default_index, &autologon);
+  if (SUCCEEDED(get_count_hr)) {
+    for (DWORD i = 0; i < count; ++i) {
+      CComPtr<ICredentialProviderCredential> credential;
+      HRESULT get_hr = gaia_provider_->GetCredentialAt(i, &credential);
+      EXPECT_EQ(get_hr, S_OK);
+      if (SUCCEEDED(get_hr)) {
+        get_hr = credential->UnAdvise();
+        if (FAILED(get_hr))
+          hr = get_hr;
+      } else {
+        hr = get_hr;
+      }
+    }
+  } else {
+    hr = get_count_hr;
+  }
+
+  // Unadvise the provider.
+  HRESULT unadvise_hr = gaia_provider_->UnAdvise();
+  if (FAILED(unadvise_hr))
+    hr = unadvise_hr;
+  gaia_provider_.Release();
+
+  return hr;
+}
+
+HRESULT
+GlsRunnerTestBase::InitializeProviderWithCredentials(
+    DWORD* credential_count,
+    ICredentialProvider** provider) {
+  HRESULT hr = InternalInitializeProvider(nullptr, credential_count);
+  if (FAILED(hr))
+    return hr;
+
+  return gaia_provider_.QueryInterface(provider);
+}
+
+HRESULT GlsRunnerTestBase::InitializeProviderWithRemoteCredentials(
+    const CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION* pcpcs_in,
+    ICredentialProvider** provider) {
+  HRESULT hr = InternalInitializeProvider(pcpcs_in, nullptr);
+  if (FAILED(hr))
+    return hr;
+
+  return gaia_provider_.QueryInterface(provider);
+}
+
+HRESULT GlsRunnerTestBase::InitializeProviderAndGetCredential(
+    DWORD index,
+    ICredentialProviderCredential** credential) {
+  DCHECK(credential);
+
+  *credential = nullptr;
+  DWORD count = 0;
+  HRESULT hr = InternalInitializeProvider(nullptr, &count);
+  if (FAILED(hr))
+    return hr;
+
+  if (index >= count)
+    return E_FAIL;
+
+  // Reference specific credential that was requested.
+  hr = gaia_provider_->GetCredentialAt(index, &testing_cred_);
+  if (FAILED(hr))
+    return hr;
+
+  EXPECT_EQ(S_OK, testing_cred_.QueryInterface(credential));
+  return S_OK;
+}
+
+HRESULT GlsRunnerTestBase::InternalInitializeProvider(
+    const CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION* pcpcs_in,
+    DWORD* count) {
+  if (count)
+    *count = 0;
+
+  CComPtr<ICredentialProvider> provider;
+
+  HRESULT hr =
+      CComCreator<CComObject<CTestGaiaCredentialProvider>>::CreateInstance(
+          nullptr, IID_ICredentialProvider,
+          reinterpret_cast<void**>(&provider));
+  if (FAILED(hr))
+    return hr;
+
+  // Apply the filter and get remote credentials as needed.
+  {
+    CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION remote_credentials;
+    HRESULT update_remote_credentials_hr;
+    hr = ApplyProviderFilter(provider, pcpcs_in, &remote_credentials,
+                             &update_remote_credentials_hr);
+    if (FAILED(hr))
+      return hr;
+
+    // Start process for logon screen.
+    hr = provider->SetUsageScenario(cpus_, 0);
+    if (FAILED(hr))
+      return hr;
+
+    if (SUCCEEDED(update_remote_credentials_hr) &&
+        remote_credentials.rgbSerialization) {
+      // Apply remote credentials if any.
+      if (remote_credentials.clsidCredentialProvider ==
+          CLSID_GaiaCredentialProvider) {
+        provider->SetSerialization(&remote_credentials);
+      }
+      ::CoTaskMemFree(remote_credentials.rgbSerialization);
+    }
+  }
+
+  // Give list of users visible on welcome screen.
+  CComPtr<ICredentialProviderSetUserArray> provider_user_array;
+  hr = provider.QueryInterface(&provider_user_array);
+  if (FAILED(hr))
+    return hr;
+
+  // All users are shown if the usage is not for unlocking the workstation.
+  bool all_users_shown = cpus_ != CPUS_UNLOCK_WORKSTATION;
+
+  CREDENTIAL_PROVIDER_ACCOUNT_OPTIONS cpao;
+  ICredentialProviderUserArray* user_array = fake_user_array();
+  hr = user_array->GetAccountOptions(&cpao);
+  if (FAILED(hr))
+    return hr;
+
+  bool other_user_tile_available = cpao == CPAO_EMPTY_LOCAL;
+
+  for (auto& sid_and_username : fake_os_user_manager_.GetUsers()) {
+    // If not all the users are shown, the user that locked the system is
+    // the only one that is in the user array (if the other user tile is
+    // not available).
+    if (!all_users_shown &&
+        ((!sid_locking_workstation_.empty() &&
+          sid_locking_workstation_ != sid_and_username.first) ||
+         other_user_tile_available)) {
+      continue;
+    }
+    fake_user_array_.AddUser(sid_and_username.first.c_str(),
+                             sid_and_username.second.c_str());
+  }
+
+  hr = provider_user_array->SetUserArray(&fake_user_array_);
+  if (FAILED(hr))
+    return hr;
+
+  // Activate the CP.
+  FakeCredentialProviderEvents events;
+  hr = provider->Advise(&fake_provider_events_, 0);
+  if (FAILED(hr))
+    return hr;
+
+  // This class can now take ownership of the provider so that it can
+  // be correctly uninitialized later.
+  gaia_provider_ = provider;
+
+  // GetCredentialCount must be called to initialize the credentials (if
+  // desired).
+  if (count) {
+    DWORD default_index;
+    BOOL autologon;
+    hr = gaia_provider_->GetCredentialCount(count, &default_index, &autologon);
+    if (FAILED(hr))
+      return hr;
+
+    EXPECT_EQ(CREDENTIAL_PROVIDER_NO_DEFAULT, default_index);
+    EXPECT_FALSE(autologon);
+
+    // Advise all the credentials
+    for (DWORD i = 0; i < *count; ++i) {
+      CComPtr<ICredentialProviderCredential> current_credential;
+      hr = gaia_provider_->GetCredentialAt(i, &current_credential);
+      if (FAILED(hr))
+        break;
+
+      hr = current_credential->Advise(nullptr);
+      if (FAILED(hr))
+        break;
+    }
+  }
+
+  // Verify fields.
+  DWORD field_count;
+  hr = gaia_provider_->GetFieldDescriptorCount(&field_count);
+  if (FAILED(hr))
+    return hr;
+
+  for (DWORD i = 0; i < field_count; ++i) {
+    CREDENTIAL_PROVIDER_FIELD_DESCRIPTOR* ppcpfd;
+    hr = gaia_provider_->GetFieldDescriptorAt(i, &ppcpfd);
+    if (FAILED(hr))
+      return hr;
+  }
+
+  return S_OK;
+}
+
+HRESULT GlsRunnerTestBase::ApplyProviderFilter(
+    const CComPtr<ICredentialProvider>& provider,
+    const CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION* pcpcs_in,
+    CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION* pcpcs_out,
+    HRESULT* update_remote_credentials_hr) {
+  if (update_remote_credentials_hr)
+    *update_remote_credentials_hr = E_NOTIMPL;
+
+  // Filter only lives long enough to apply filter and get serialization
+  // credentials.
+  CComPtr<ICredentialProviderFilter> filter;
+  HRESULT hr =
+      CComCreator<CComObject<CGaiaCredentialProviderFilter>>::CreateInstance(
+          nullptr, IID_ICredentialProviderFilter, (void**)&filter);
+  if (FAILED(hr))
+    return hr;
+
+  // Set token fetch result before starting the filter.
+  fake_http_url_fetcher_factory_.SetFakeResponse(
+      GURL(AssociatedUserValidator::kTokenInfoUrl),
+      FakeWinHttpUrlFetcher::Headers(), default_token_handle_response_);
+
+  // Start initial refresh of token handles. The filter will apply user access
+  // restrictions as needed.
+  fake_associated_user_validator_.StartRefreshingTokenHandleValidity();
+
+  // Perform initial filter code.
+  hr = filter->Filter(cpus_, 0, nullptr, nullptr, 0);
+  if (FAILED(hr))
+    return hr;
+
+  // Apply remote credentials if any.
+  if (pcpcs_in && pcpcs_out && update_remote_credentials_hr)
+    *update_remote_credentials_hr =
+        filter->UpdateRemoteCredential(pcpcs_in, pcpcs_out);
+
+  return S_OK;
+}
+
+HRESULT GlsRunnerTestBase::StartLogonProcess(bool succeeds) {
+  DCHECK(testing_cred_);
+  DCHECK(!logon_process_started_successfully_);
+  BOOL auto_login;
+  EXPECT_EQ(S_OK, testing_cred_->SetSelected(&auto_login));
+
+  // Logging on is an async process, so the call to GetSerialization() starts
+  // the process, but when it returns it has not completed.
+  CREDENTIAL_PROVIDER_GET_SERIALIZATION_RESPONSE cpgsr;
+  CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION cpcs;
+  wchar_t* status_text;
+  CREDENTIAL_PROVIDER_STATUS_ICON status_icon;
+  EXPECT_EQ(S_OK, testing_cred_->GetSerialization(&cpgsr, &cpcs, &status_text,
+                                                  &status_icon));
+  EXPECT_EQ(CPSI_NONE, status_icon);
+  if (succeeds) {
+    EXPECT_EQ(nullptr, status_text);
+    EXPECT_EQ(CPGSR_NO_CREDENTIAL_NOT_FINISHED, cpgsr);
+    logon_process_started_successfully_ = true;
+  } else {
+    EXPECT_NE(nullptr, status_text);
+    EXPECT_EQ(CPGSR_NO_CREDENTIAL_FINISHED, cpgsr);
+  }
+  return S_OK;
+}
+
+HRESULT GlsRunnerTestBase::WaitForLogonProcess() {
+  CComPtr<testing::ITestCredential> test;
+  HRESULT hr = testing_cred_->QueryInterface(&test);
+  if (FAILED(hr))
+    return hr;
+  return test->WaitForGls();
+}
+
+HRESULT GlsRunnerTestBase::StartLogonProcessAndWait() {
+  HRESULT hr = StartLogonProcess(/*succeeds=*/true);
+  if (FAILED(hr))
+    return hr;
+  return WaitForLogonProcess();
+}
+
+// static
+HRESULT GlsRunnerTestBase::GetFakeGlsCommandline(
+    UiExitCodes default_exit_code,
+    const std::string& gls_email,
+    const std::string& gaia_id_override,
+    const base::string16& start_gls_event_name,
+    bool ignore_expected_gaia_id,
+    base::CommandLine* command_line) {
+  *command_line = base::GetMultiProcessTestChildBaseCommandLine();
+  command_line->AppendSwitchASCII(::switches::kTestChildProcess, "gls_main");
+  command_line->AppendSwitchASCII(switches::kGlsUserEmail, gls_email);
+  command_line->AppendSwitchNative(switches::kDefaultExitCode,
+                                   base::NumberToString16(default_exit_code));
+
+  if (ignore_expected_gaia_id)
+    command_line->AppendSwitch(switches::kIgnoreExpectedGaiaId);
+
+  if (!gaia_id_override.empty()) {
+    command_line->AppendSwitchASCII(switches::kOverrideGaiaId,
+                                    gaia_id_override);
+  }
+
+  if (!start_gls_event_name.empty()) {
+    command_line->AppendSwitchNative(switches::kStartGlsEventName,
+                                     start_gls_event_name);
+  }
+
+  return S_OK;
+}
+
+HRESULT GlsRunnerTestBase::FinishLogonProcess(
+    bool expected_success,
+    bool expected_credentials_change_fired,
+    int expected_error_message) {
+  // If no logon process was started, there is nothing to finish.
+  if (!logon_process_started_successfully_)
+    return S_OK;
+
+  CComPtr<ICredentialProviderCredential> local_testing_cred = testing_cred_;
+  // Release ownership on the testing_cred_ which should be finishing.
+  testing_cred_.Release();
+
+  HRESULT hr = FinishLogonProcessWithCred(
+      expected_success, expected_credentials_change_fired,
+      expected_error_message, local_testing_cred);
+
+  EXPECT_EQ(hr, S_OK);
+  if (FAILED(hr))
+    return hr;
+
+  if (expected_credentials_change_fired) {
+    hr = ReportLogonProcessResult(local_testing_cred);
+    EXPECT_EQ(hr, S_OK);
+    return hr;
+  }
+
+  return S_OK;
+}
+
+HRESULT GlsRunnerTestBase::FinishLogonProcessWithCred(
+    bool expected_success,
+    bool expected_credentials_change_fired,
+    int expected_error_message,
+    const CComPtr<ICredentialProviderCredential>& local_testing_cred) {
+  // If no logon process was started, there is nothing to finish.
+  if (!logon_process_started_successfully_)
+    return S_OK;
+
+  logon_process_started_successfully_ = false;
+  DCHECK(gaia_provider_);
+
+  CComPtr<ITestCredential> test_cred;
+  HRESULT hr = local_testing_cred.QueryInterface(&test_cred);
+  if (FAILED(hr))
+    return hr;
+
+  CComPtr<ITestCredentialProvider> test_provider;
+  hr = gaia_provider_.QueryInterface(&test_provider);
+  if (FAILED(hr))
+    return hr;
+
+  EXPECT_EQ(test_provider->credentials_changed_fired(),
+            expected_success && expected_credentials_change_fired);
+
+  if (!expected_success) {
+    // Check that values were not propagated to the provider.
+    EXPECT_EQ(0u, test_provider->username().Length());
+    EXPECT_EQ(0u, test_provider->password().Length());
+    EXPECT_EQ(0u, test_provider->sid().Length());
+
+    if (expected_error_message) {
+      EXPECT_STREQ(test_cred->GetErrorText(),
+                   GetStringResource(expected_error_message).c_str());
+    } else {
+      EXPECT_EQ(test_cred->GetErrorText(), nullptr);
+    }
+    return S_OK;
+  }
+
+  // Call final GetSerialization and expect it to be finished.
+  CREDENTIAL_PROVIDER_GET_SERIALIZATION_RESPONSE cpgsr;
+  CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION cpcs;
+  wchar_t* status_text;
+  CREDENTIAL_PROVIDER_STATUS_ICON status_icon;
+  hr = local_testing_cred->GetSerialization(&cpgsr, &cpcs, &status_text,
+                                            &status_icon);
+
+  if (FAILED(hr))
+    return hr;
+
+  EXPECT_EQ(nullptr, status_text);
+  EXPECT_EQ(CPSI_SUCCESS, status_icon);
+  EXPECT_EQ(CPGSR_RETURN_CREDENTIAL_FINISHED, cpgsr);
+  EXPECT_LT(0u, cpcs.cbSerialization);
+  EXPECT_NE(nullptr, cpcs.rgbSerialization);
+
+  // Check that values were propagated to the provider.
+  if (expected_credentials_change_fired) {
+    EXPECT_NE(0u, test_provider->username().Length());
+    EXPECT_NE(0u, test_provider->password().Length());
+    EXPECT_NE(0u, test_provider->sid().Length());
+  }
+
+  EXPECT_EQ(test_cred->GetErrorText(), nullptr);
+
+  return S_OK;
+}
+
+HRESULT GlsRunnerTestBase::ReportLogonProcessResult(
+    const CComPtr<ICredentialProviderCredential>& local_testing_cred) {
+  CComPtr<ITestCredential> test_cred;
+  HRESULT hr = local_testing_cred.QueryInterface(&test_cred);
+  if (FAILED(hr))
+    return hr;
+
+  // State was not reset.
+  EXPECT_TRUE(test_cred->AreCredentialsValid());
+  wchar_t* report_status_text = nullptr;
+  CREDENTIAL_PROVIDER_STATUS_ICON report_icon;
+  hr =
+      local_testing_cred->ReportResult(0, 0, &report_status_text, &report_icon);
+  if (FAILED(hr))
+    return hr;
+
+  // State was reset.
+  EXPECT_FALSE(test_cred->AreCredentialsValid());
+
+  return S_OK;
+}
+
 }  // namespace testing
 
 }  // namespace credential_provider
diff --git a/chrome/credential_provider/test/gls_runner_test_base.h b/chrome/credential_provider/test/gls_runner_test_base.h
index 63f1124c..66955ad 100644
--- a/chrome/credential_provider/test/gls_runner_test_base.h
+++ b/chrome/credential_provider/test/gls_runner_test_base.h
@@ -6,8 +6,9 @@
 #define CHROME_CREDENTIAL_PROVIDER_TEST_GLS_RUNNER_TEST_BASE_H_
 
 #include "base/test/test_reg_util_win.h"
+#include "chrome/credential_provider/common/gcp_strings.h"
+#include "chrome/credential_provider/gaiacp/gaia_credential_provider.h"
 #include "chrome/credential_provider/test/com_fakes.h"
-#include "chrome/credential_provider/test/fake_gls_run_helper.h"
 #include "chrome/credential_provider/test/gcp_fakes.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
@@ -15,31 +16,188 @@
 
 namespace testing {
 
-// Helper class used to run and wait for a fake GLS process to validate the
-// functionality of a GCPW credential.
+extern const char kDefaultEmail[];
+extern const char kDefaultGaiaId[];
+extern const wchar_t kDefaultUsername[];
+extern const char kDefaultInvalidTokenHandleResponse[];
+extern const char kDefaultValidTokenHandleResponse[];
+
+// Helper class used to test the full call sequence of a credential provider by
+// LoginUI. This includes creation of a credential provider filter and
+// application of remote credentials if specified. There are default token
+// handle responses (always valid token handles) and usage scenarios
+// (CPUS_LOGON) that can be overridden before starting the call sequence for the
+// credential provider.
 class GlsRunnerTestBase : public ::testing::Test {
+ public:
+  // Gets a command line that runs a fake GLS that produces the desired output.
+  // |default_exit_code| is the default value that will be written unless the
+  // other command line arguments require a specific error code to be returned.
+  static HRESULT GetFakeGlsCommandline(
+      UiExitCodes default_exit_code,
+      const std::string& gls_email,
+      const std::string& gaia_id_override,
+      const base::string16& start_gls_event_name,
+      bool ignore_expected_gaia_id,
+      base::CommandLine* command_line);
+
  protected:
   GlsRunnerTestBase();
   ~GlsRunnerTestBase() override;
 
   void SetUp() override;
-  FakeGlsRunHelper* run_helper() { return &run_helper_; }
+  void TearDown() override;
 
   FakeOSUserManager* fake_os_user_manager() { return &fake_os_user_manager_; }
   FakeWinHttpUrlFetcherFactory* fake_http_url_fetcher_factory() {
     return &fake_http_url_fetcher_factory_;
   }
+  FakeCredentialProviderUserArray* fake_user_array() {
+    return &fake_user_array_;
+  }
+  FakeAssociatedUserValidator* fake_associated_user_validator() {
+    return &fake_associated_user_validator_;
+  }
+  FakeCredentialProviderEvents* fake_provider_events() {
+    return &fake_provider_events_;
+  }
+  FakeInternetAvailabilityChecker* fake_internet_checker() {
+    return &fake_internet_checker_;
+  }
+
+  const CComPtr<ICredentialProvider>& created_provider() const {
+    return gaia_provider_;
+  }
+
+  void SetSidLockingWorkstation(const base::string16& sid) {
+    sid_locking_workstation_ = sid;
+  }
+
+  void SetDefaultTokenHandleResponse(const std::string& response) {
+    default_token_handle_response_ = response;
+  }
+
+  void SetUsageScenario(CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus) {
+    // Must be called before creating the provide. The usage does not normally
+    // change during the execution of a provider.
+    DCHECK(!gaia_provider_);
+    cpus_ = cpus;
+  }
+
+  // Creates the provider and also all the credentials associated to users that
+  // are already created before this call. Fills |credential_count| with the
+  // number of credentials in the provider and |provider| with a pointer to the
+  // created provider (this also correctly adds a reference to the provider).
+  HRESULT
+  InitializeProviderWithCredentials(DWORD* credential_count,
+                                    ICredentialProvider** provider);
+
+  // Creates the provider and also all the credentials associated to users that
+  // are already created before this call. If |pcps_in| is non null then it will
+  // pass this information as remote credentials to the credential provider
+  // filter and provider. Fills |provider| with a pointer to the created
+  // provider (this also correctly adds a reference to the provider).
+  HRESULT
+  InitializeProviderWithRemoteCredentials(
+      const CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION* pcpcs_in,
+      ICredentialProvider** provider);
+
+  // Creates the provider and also all the credentials associated to users that
+  // are already created before this call. Once credentials are created, the
+  // function tries to fill |credential| with the credential at index |index|.
+  HRESULT InitializeProviderAndGetCredential(
+      DWORD index,
+      ICredentialProviderCredential** credential);
+
+  // Used to release the provider before normal TearDown to test certain
+  // cancellation scenarios. No other references should be held on the provider
+  // to ensure that the provider can actually be released.
+  HRESULT ReleaseProvider();
+
+  // Initiates the logon process on the current |testing_credential_| that
+  // is selected by a call to InitializeProviderAndGetCredential.
+  // |succeeds| specifies whether we expect the first call to GetSerialization
+  // on |testing_credential_| to succeed and start a GLS process or not.
+  // If false, we will check that an appropriate error has been returned.
+  HRESULT StartLogonProcess(bool succeeds);
+
+  // Waits for the GLS process that was started in StartLogonProcess to
+  // complete and returns.
+  HRESULT WaitForLogonProcess();
+
+  // Combines StartLogonProcess and WaitForLogonProcess.
+  HRESULT StartLogonProcessAndWait();
+
+  // Calls the final GetSerialization on the |testing_credential_| to complete
+  // the logon process. |expected_success| specifies whether the final
+  // GetSerialization is expected to succeed.
+  // |expected_credentials_change_fired| specifies if a credential changed fired
+  // event should have been detected by the provider. |expected_error_message|
+  // is the error message that is expected. This message only applies if
+  // |expected_success| is false.
+  // This function combines the calls to FinishLogonProcessWithPred and
+  // ReportLogonProcessResult which can be called separately to perform extra
+  // operations between the last GetSerialization and the call to ReportResult.
+  HRESULT FinishLogonProcess(bool expected_success,
+                             bool expected_credentials_change_fired,
+                             int expected_error_message);
+  HRESULT FinishLogonProcessWithCred(
+      bool expected_success,
+      bool expected_credentials_change_fired,
+      int expected_error_message,
+      const CComPtr<ICredentialProviderCredential>& local_testing_cred);
+  HRESULT ReportLogonProcessResult(
+      const CComPtr<ICredentialProviderCredential>& local_testing_cred);
 
  private:
+  HRESULT InternalInitializeProvider(
+      const CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION* pcpcs_in,
+      DWORD* count);
+  HRESULT ApplyProviderFilter(
+      const CComPtr<ICredentialProvider>& provider,
+      const CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION* pcpcs_in,
+      CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION* pcpcs_out,
+      HRESULT* update_remote_credentials_hr);
+
+  registry_util::RegistryOverrideManager registry_override_;
+
   FakeOSProcessManager fake_os_process_manager_;
   FakeOSUserManager fake_os_user_manager_;
   FakeScopedLsaPolicyFactory fake_scoped_lsa_policy_factory_;
   FakeScopedUserProfileFactory fake_scoped_user_profile_factory_;
-  registry_util::RegistryOverrideManager registry_override_;
-  FakeGlsRunHelper run_helper_;
   FakeInternetAvailabilityChecker fake_internet_checker_;
   FakeAssociatedUserValidator fake_associated_user_validator_;
   FakeWinHttpUrlFetcherFactory fake_http_url_fetcher_factory_;
+  FakeCredentialProviderEvents fake_provider_events_;
+  FakeCredentialProviderUserArray fake_user_array_;
+
+  // SID of the user that is considered to be locking the workstation. This is
+  // only relevant for CPUS_UNLOCK_WORKSTATION usage.
+  base::string16 sid_locking_workstation_;
+
+  // Reference to the provider that is created and owned by this class.
+  CComPtr<ICredentialProvider> gaia_provider_;
+
+  // Reference to the credential in provider that is being tested by this class.
+  // This member is kept so that it can be automatically released on destruction
+  // of the test if the test did not explicitly release it. This allows us to
+  // write less boiler plate test code and ensures that proper destruction order
+  // of the credentials is respected.
+  CComPtr<ICredentialProviderCredential> testing_cred_;
+
+  // Keeps track of whether a logon process has started for |testing_cred_|.
+  // Testers who do not explicitly call FinishLogonProcess before the end of
+  // their test will leave the completion of the logon process to the TearDown
+  // of this class.
+  bool logon_process_started_successfully_ = false;
+
+  // The current usage scenario that this test is running. This should be
+  // set before |gaia_provider_| is set.
+  CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus_;
+
+  // Default response returned by |fake_http_url_fetcher_factory_| when checking
+  // for token handle validity.
+  std::string default_token_handle_response_;
 };
 
 }  // namespace testing
diff --git a/chrome/credential_provider/test/test_credential.h b/chrome/credential_provider/test/test_credential.h
index c72858e..0061a3c 100644
--- a/chrome/credential_provider/test/test_credential.h
+++ b/chrome/credential_provider/test/test_credential.h
@@ -17,7 +17,7 @@
 #include "base/synchronization/waitable_event.h"
 #include "chrome/credential_provider/common/gcp_strings.h"
 #include "chrome/credential_provider/gaiacp/gaia_credential_base.h"
-#include "chrome/credential_provider/test/fake_gls_run_helper.h"
+#include "chrome/credential_provider/test/gls_runner_test_base.h"
 
 namespace base {
 class CommandLine;
@@ -35,7 +35,8 @@
   virtual HRESULT STDMETHODCALLTYPE
   SetGlsEmailAddress(const std::string& email) = 0;
   virtual HRESULT STDMETHODCALLTYPE
-  SetGaiaIdOverride(const std::string& gaia_id) = 0;
+  SetGaiaIdOverride(const std::string& gaia_id,
+                    bool ignore_expected_gaia_id) = 0;
   virtual HRESULT STDMETHODCALLTYPE WaitForGls() = 0;
   virtual HRESULT STDMETHODCALLTYPE
   SetStartGlsEventName(const base::string16& event_name) = 0;
@@ -45,8 +46,6 @@
   virtual bool STDMETHODCALLTYPE AreCredentialsValid() = 0;
   virtual bool STDMETHODCALLTYPE CanAttemptWindowsLogon() = 0;
   virtual bool STDMETHODCALLTYPE IsWindowsPasswordValidForStoredUser() = 0;
-  virtual void STDMETHODCALLTYPE
-  SetWindowsPassword(const CComBSTR& windows_password) = 0;
   virtual bool STDMETHODCALLTYPE IsGlsRunning() = 0;
 };
 
@@ -71,7 +70,8 @@
   // ITestCredential.
   IFACEMETHODIMP SetDefaultExitCode(UiExitCodes default_exit_code) override;
   IFACEMETHODIMP SetGlsEmailAddress(const std::string& email) override;
-  IFACEMETHODIMP SetGaiaIdOverride(const std::string& gaia_id) override;
+  IFACEMETHODIMP SetGaiaIdOverride(const std::string& gaia_id,
+                                   bool ignore_expected_gaia_id) override;
   IFACEMETHODIMP WaitForGls() override;
   IFACEMETHODIMP SetStartGlsEventName(
       const base::string16& event_name) override;
@@ -81,8 +81,6 @@
   bool STDMETHODCALLTYPE AreCredentialsValid() override;
   bool STDMETHODCALLTYPE CanAttemptWindowsLogon() override;
   bool STDMETHODCALLTYPE IsWindowsPasswordValidForStoredUser() override;
-  void STDMETHODCALLTYPE
-  SetWindowsPassword(const CComBSTR& windows_password) override;
   bool STDMETHODCALLTYPE IsGlsRunning() override;
 
   void SignalGlsCompletion();
@@ -120,6 +118,7 @@
   base::string16 start_gls_event_name_;
   CComBSTR error_text_;
   bool gls_process_started_ = false;
+  bool ignore_expected_gaia_id_ = false;
 };
 
 template <class T>
@@ -145,7 +144,10 @@
 }
 
 template <class T>
-HRESULT CTestCredentialBase<T>::SetGaiaIdOverride(const std::string& gaia_id) {
+HRESULT CTestCredentialBase<T>::SetGaiaIdOverride(
+    const std::string& gaia_id,
+    bool ignore_expected_gaia_id) {
+  ignore_expected_gaia_id_ = ignore_expected_gaia_id;
   gaia_id_override_ = gaia_id;
   return S_OK;
 }
@@ -208,12 +210,6 @@
 }
 
 template <class T>
-void CTestCredentialBase<T>::SetWindowsPassword(
-    const CComBSTR& windows_password) {
-  this->set_current_windows_password(windows_password);
-}
-
-template <class T>
 bool CTestCredentialBase<T>::IsGlsRunning() {
   return this->IsGaiaLogonStubRunning();
 }
@@ -226,9 +222,9 @@
 template <class T>
 HRESULT CTestCredentialBase<T>::GetBaseGlsCommandline(
     base::CommandLine* command_line) {
-  return FakeGlsRunHelper::GetFakeGlsCommandline(
+  return GlsRunnerTestBase::GetFakeGlsCommandline(
       default_exit_code_, gls_email_, gaia_id_override_, start_gls_event_name_,
-      command_line);
+      ignore_expected_gaia_id_, command_line);
 }
 
 template <class T>
@@ -287,7 +283,8 @@
 }
 
 // This class is used to implement a test credential based off a fully
-// implemented CGaiaCredentialBase class.
+// implemented CGaiaCredentialBase class that does not expose
+// ICredentialProviderCredential2.
 template <class T>
 class ATL_NO_VTABLE CTestCredentialForBaseInherited
     : public CTestCredentialBase<T> {
@@ -301,7 +298,6 @@
   BEGIN_COM_MAP(CTestCredentialForBaseInherited)
   COM_INTERFACE_ENTRY(IGaiaCredential)
   COM_INTERFACE_ENTRY(ICredentialProviderCredential)
-  COM_INTERFACE_ENTRY(ICredentialProviderCredential2)
   COM_INTERFACE_ENTRY(ITestCredential)
   END_COM_MAP()
 };
@@ -313,30 +309,6 @@
 CTestCredentialForBaseInherited<T>::~CTestCredentialForBaseInherited() =
     default;
 
-template <class T>
-HRESULT CreateBaseInheritedCredential(
-    ICredentialProviderCredential** credential) {
-  return CComCreator<CComObject<testing::CTestCredentialForBaseInherited<T>>>::
-      CreateInstance(nullptr, IID_ICredentialProviderCredential,
-                     reinterpret_cast<void**>(credential));
-}
-
-template <class T>
-HRESULT CreateBaseInheritedCredentialWithProvider(
-    IGaiaCredentialProvider* provider,
-    IGaiaCredential** gaia_credential,
-    ICredentialProviderCredential** credential) {
-  HRESULT hr = CreateBaseInheritedCredential<T>(credential);
-  if (SUCCEEDED(hr)) {
-    hr = (*credential)
-             ->QueryInterface(IID_IGaiaCredential,
-                              reinterpret_cast<void**>(gaia_credential));
-    if (SUCCEEDED(hr))
-      hr = (*gaia_credential)->Initialize(provider);
-  }
-  return hr;
-}
-
 // This class is used to implement a test credential based off a fully
 // implemented CGaiaCredentialBase class. The additional InterfaceT parameter
 // is used to specify any additional interfaces that should be registerd for
@@ -370,30 +342,6 @@
 CTestCredentialForInherited<T, InterfaceT>::~CTestCredentialForInherited() =
     default;
 
-template <class T, class InterfaceT>
-HRESULT CreateInheritedCredential(ICredentialProviderCredential** credential) {
-  return CComCreator<CComObject<testing::CTestCredentialForInherited<
-      T, InterfaceT>>>::CreateInstance(nullptr,
-                                       IID_ICredentialProviderCredential,
-                                       reinterpret_cast<void**>(credential));
-}
-
-template <class T, class InterfaceT>
-HRESULT CreateInheritedCredentialWithProvider(
-    IGaiaCredentialProvider* provider,
-    IGaiaCredential** gaia_credential,
-    ICredentialProviderCredential** credential) {
-  HRESULT hr = CreateInheritedCredential<T, InterfaceT>(credential);
-  if (SUCCEEDED(hr)) {
-    hr = (*credential)
-             ->QueryInterface(IID_IGaiaCredential,
-                              reinterpret_cast<void**>(gaia_credential));
-    if (SUCCEEDED(hr))
-      hr = (*gaia_credential)->Initialize(provider);
-  }
-  return hr;
-}
-
 }  // namespace testing
 }  // namespace credential_provider
 
diff --git a/chrome/credential_provider/test/test_credential_provider.h b/chrome/credential_provider/test/test_credential_provider.h
new file mode 100644
index 0000000..a8ef146
--- /dev/null
+++ b/chrome/credential_provider/test/test_credential_provider.h
@@ -0,0 +1,29 @@
+// Copyright 2018 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_CREDENTIAL_PROVIDER_TEST_TEST_CREDENTIAL_PROVIDER_H_
+#define CHROME_CREDENTIAL_PROVIDER_TEST_TEST_CREDENTIAL_PROVIDER_H_
+
+#include <atlbase.h>
+#include <atlcom.h>
+#include <atlcomcli.h>
+
+namespace credential_provider {
+
+namespace testing {
+
+class DECLSPEC_UUID("d8108fd0-1e0d-4853-9a8a-1f6aed8bf64d")
+    ITestCredentialProvider : public IUnknown {
+ public:
+  virtual const CComBSTR& STDMETHODCALLTYPE username() const = 0;
+  virtual const CComBSTR& STDMETHODCALLTYPE password() const = 0;
+  virtual const CComBSTR& STDMETHODCALLTYPE sid() const = 0;
+  virtual bool STDMETHODCALLTYPE credentials_changed_fired() const = 0;
+  virtual void STDMETHODCALLTYPE ResetCredentialsChangedFired() = 0;
+};
+
+}  // namespace testing
+}  // namespace credential_provider
+
+#endif  // CHROME_CREDENTIAL_PROVIDER_TEST_TEST_CREDENTIAL_PROVIDER_H_
diff --git a/chrome/renderer/extensions/chrome_extensions_renderer_client.cc b/chrome/renderer/extensions/chrome_extensions_renderer_client.cc
index 04cd438..486c5a65 100644
--- a/chrome/renderer/extensions/chrome_extensions_renderer_client.cc
+++ b/chrome/renderer/extensions/chrome_extensions_renderer_client.cc
@@ -39,6 +39,7 @@
 #include "extensions/renderer/guest_view/mime_handler_view/mime_handler_view_container.h"
 #include "extensions/renderer/guest_view/mime_handler_view/mime_handler_view_container_manager.h"
 #include "extensions/renderer/script_context.h"
+#include "services/network/public/cpp/features.h"
 #include "third_party/blink/public/platform/web_url.h"
 #include "third_party/blink/public/web/web_document.h"
 #include "third_party/blink/public/web/web_local_frame.h"
@@ -241,6 +242,13 @@
               extensions::PermissionsData::PageAccess::kAllowed) {
         *attach_same_site_cookies = true;
       }
+    } else if (base::FeatureList::IsEnabled(
+                   network::features::kNetworkService)) {
+      // If there is no extension installed for the origin, it may be from a
+      // recently uninstalled extension.  The tabs of such extensions are
+      // automatically closed, but subframes and content scripts may stick
+      // around. Fail such requests without killing the process.
+      *new_url = GURL(chrome::kExtensionInvalidRequestURL);
     }
   }
 
diff --git a/chrome/test/chromedriver/key_converter.cc b/chrome/test/chromedriver/key_converter.cc
index e0d0eec..d372b0a 100644
--- a/chrome/test/chromedriver/key_converter.cc
+++ b/chrome/test/chromedriver/key_converter.cc
@@ -35,7 +35,7 @@
 // points defined by the Unicode standard.
 const ui::KeyboardCode kSpecialWebDriverKeys[] = {
     ui::VKEY_UNKNOWN,   // \uE000
-    ui::VKEY_UNKNOWN,
+    ui::VKEY_CANCEL,  // \uE001
     ui::VKEY_HELP,
     ui::VKEY_BACK,
     ui::VKEY_TAB,
diff --git a/chrome/test/chromedriver/key_converter_unittest.cc b/chrome/test/chromedriver/key_converter_unittest.cc
index dc83a00..2e0908e 100644
--- a/chrome/test/chromedriver/key_converter_unittest.cc
+++ b/chrome/test/chromedriver/key_converter_unittest.cc
@@ -376,29 +376,21 @@
     int modifiers = 0;
     keys.push_back(0xE000U + i);
     std::list<KeyEvent> events;
-    if (i == 1) {
-      EXPECT_NE(kOk, ConvertKeysToKeyEvents(keys,
-                                            true /* release_modifiers*/,
-                                            &modifiers, &events).code())
-          << "Index: " << i;
+    EXPECT_EQ(kOk, ConvertKeysToKeyEvents(keys,
+                                          true /* release_modifiers */,
+                                          &modifiers, &events).code())
+        << "Index: " << i;
+    if (i == 0) {
       EXPECT_EQ(0u, events.size()) << "Index: " << i;
+    } else if (i >= base::size(kTextForKeys) || kTextForKeys[i] == 0) {
+      EXPECT_EQ(2u, events.size()) << "Index: " << i;
     } else {
-      EXPECT_EQ(kOk, ConvertKeysToKeyEvents(keys,
-                                            true /* release_modifiers */,
-                                            &modifiers, &events).code())
+      ASSERT_EQ(3u, events.size()) << "Index: " << i;
+      std::list<KeyEvent>::const_iterator it = events.begin();
+      ++it;  // Move to the second event.
+      ASSERT_EQ(1u, it->unmodified_text.length()) << "Index: " << i;
+      EXPECT_EQ(kTextForKeys[i], it->unmodified_text[0])
           << "Index: " << i;
-      if (i == 0) {
-        EXPECT_EQ(0u, events.size()) << "Index: " << i;
-      } else if (i >= base::size(kTextForKeys) || kTextForKeys[i] == 0) {
-        EXPECT_EQ(2u, events.size()) << "Index: " << i;
-      } else {
-        ASSERT_EQ(3u, events.size()) << "Index: " << i;
-        std::list<KeyEvent>::const_iterator it = events.begin();
-        ++it;  // Move to the second event.
-        ASSERT_EQ(1u, it->unmodified_text.length()) << "Index: " << i;
-        EXPECT_EQ(kTextForKeys[i], it->unmodified_text[0])
-            << "Index: " << i;
-      }
     }
   }
 }
diff --git a/chrome/test/data/webui/BUILD.gn b/chrome/test/data/webui/BUILD.gn
index 2871ba5e..6e1a50df 100644
--- a/chrome/test/data/webui/BUILD.gn
+++ b/chrome/test/data/webui/BUILD.gn
@@ -186,6 +186,7 @@
       "../../../browser/resources/chromeos/select_to_speak/rect_utils_unittest.gtestjs",
       "../../../browser/resources/chromeos/select_to_speak/select_to_speak_unittest.gtestjs",
       "../../../browser/resources/chromeos/select_to_speak/word_utils_unittest.gtestjs",
+      "../../../browser/resources/chromeos/switch_access/rect_helper_unittest.gtestjs",
     ]
     extra_js_files += [
       "../../../browser/resources/chromeos/braille_ime/braille_ime.js",
@@ -195,6 +196,7 @@
       "../../../browser/resources/chromeos/select_to_speak/test_support.js",
       "../../../browser/resources/chromeos/select_to_speak/word_utils.js",
       "../../../browser/resources/chromeos/select_to_speak/node_utils.js",
+      "../../../browser/resources/chromeos/switch_access/rect_helper.js",
     ]
   }
 }
diff --git a/chrome/test/data/webui/app_management/dom_switch_test.js b/chrome/test/data/webui/app_management/dom_switch_test.js
index 830f990..1905dc77 100644
--- a/chrome/test/data/webui/app_management/dom_switch_test.js
+++ b/chrome/test/data/webui/app_management/dom_switch_test.js
@@ -13,7 +13,7 @@
 
     const template = `
         <dom-bind>
-          <template is="dom-bind">
+          <template>
             <app-management-dom-switch id="switch">
               <template>
                 <div id='child1' route-id='1'>[[property.x]]</div>
@@ -26,9 +26,7 @@
     document.body.innerHTML = template;
 
     domSwitch = document.getElementById('switch');
-    // TODO(dpapad): Remove conditional when Polymer 2 migration has completed.
-    domBind = document.querySelector(
-        Polymer.DomBind ? 'dom-bind' : 'template[is=\'dom-bind\']');
+    domBind = document.querySelector('dom-bind');
   });
 
   test('children are attached/detached when the route changes', function() {
diff --git a/chrome/test/data/webui/cr_elements/cr_lazy_render_tests.js b/chrome/test/data/webui/cr_elements/cr_lazy_render_tests.js
index 94ed2b74..b8eb86ac 100644
--- a/chrome/test/data/webui/cr_elements/cr_lazy_render_tests.js
+++ b/chrome/test/data/webui/cr_elements/cr_lazy_render_tests.js
@@ -15,7 +15,7 @@
     PolymerTest.clearBody();
     const template = `
         <dom-bind>
-          <template is="dom-bind">
+          <template>
             <cr-lazy-render id="lazy">
               <template>
                 <h1>
@@ -28,9 +28,7 @@
         </dom-bind>`;
     document.body.innerHTML = template;
     lazy = document.getElementById('lazy');
-    // TODO(dpapad): Remove conditional when Polymer 2 migration has completed.
-    bind = document.querySelector(
-        Polymer.DomBind ? 'dom-bind' : 'template[is=\'dom-bind\']');
+    bind = document.querySelector('dom-bind');
   });
 
   test('stamps after get()', function() {
diff --git a/chrome/test/data/webui/cr_elements/cr_search_field_tests.js b/chrome/test/data/webui/cr_elements/cr_search_field_tests.js
index 1f0578f..ca978d2a 100644
--- a/chrome/test/data/webui/cr_elements/cr_search_field_tests.js
+++ b/chrome/test/data/webui/cr_elements/cr_search_field_tests.js
@@ -79,7 +79,7 @@
     field.setValue('foo');
 
     field.setValue('');
-    assertEquals(['query1', '', 'query2', 'foo', ''].join(), searches.join());
+    assertDeepEquals(['query1', '', 'query2', 'foo', ''], searches);
   });
 
   test('does not notify on setValue with noEvent=true', function() {
@@ -87,6 +87,24 @@
     field.setValue('foo', true);
     field.setValue('bar');
     field.setValue('baz', true);
-    assertEquals(['bar'].join(), searches.join());
+    assertDeepEquals(['bar'], searches);
+  });
+
+  test('setValue will return early if the query has not changed', () => {
+    // Need a space at the end, since the effective query will strip the spaces
+    // at the beginning, but not at the end of the query.
+    const value = 'test ';
+    assertNotEquals(value, field.getValue());
+    let calledSetValue = false;
+    field.onSearchTermInput = () => {
+      if (!calledSetValue) {
+        calledSetValue = true;
+        field.setValue(value);
+      }
+    };
+    field.setValue(value, true);
+    field.setValue(`  ${value}  `);
+    assertTrue(calledSetValue);
+    assertEquals(0, searches.length);
   });
 });
diff --git a/chrome/test/data/webui/settings/languages_page_tests.js b/chrome/test/data/webui/settings/languages_page_tests.js
index 8faca078..49029ce 100644
--- a/chrome/test/data/webui/settings/languages_page_tests.js
+++ b/chrome/test/data/webui/settings/languages_page_tests.js
@@ -317,8 +317,7 @@
         let item = null;
 
         let listItems = languagesCollapse.querySelectorAll('.list-item');
-        let domRepeat = assert(languagesCollapse.querySelector(
-            Polymer.DomRepeat ? 'dom-repeat' : 'template[is="dom-repeat"]'));
+        let domRepeat = assert(languagesCollapse.querySelector('dom-repeat'));
 
         let num_visibles = 0;
         Array.from(listItems).forEach(function(el) {
@@ -407,8 +406,7 @@
 
         // Find the new language item.
         const items = languagesCollapse.querySelectorAll('.list-item');
-        const domRepeat = assert(languagesCollapse.querySelector(
-            Polymer.DomRepeat ? 'dom-repeat' : 'template[is="dom-repeat"]'));
+        const domRepeat = assert(languagesCollapse.querySelector('dom-repeat'));
         const item = Array.from(items).find(function(el) {
           return domRepeat.itemForElement(el) &&
               domRepeat.itemForElement(el).language.code == 'no';
@@ -435,8 +433,7 @@
             ['en-US'], languageHelper.prefs.translate_blocked_languages.value);
 
         const items = languagesCollapse.querySelectorAll('.list-item');
-        const domRepeat = assert(languagesCollapse.querySelector(
-            Polymer.DomRepeat ? 'dom-repeat' : 'template[is="dom-repeat"]'));
+        const domRepeat = assert(languagesCollapse.querySelector('dom-repeat'));
         const item = Array.from(items).find(function(el) {
           return domRepeat.itemForElement(el) &&
               domRepeat.itemForElement(el).language.code == 'en-US';
@@ -451,8 +448,7 @@
 
       test('remove language when starting with 2 languages', function() {
         const items = languagesCollapse.querySelectorAll('.list-item');
-        const domRepeat = assert(languagesCollapse.querySelector(
-            Polymer.DomRepeat ? 'dom-repeat' : 'template[is="dom-repeat"]'));
+        const domRepeat = assert(languagesCollapse.querySelector('dom-repeat'));
         const item = Array.from(items).find(function(el) {
           return domRepeat.itemForElement(el) &&
               domRepeat.itemForElement(el).language.code == 'sw';
diff --git a/chrome/test/data/webui/settings/test_util.js b/chrome/test/data/webui/settings/test_util.js
index 8affff3..cfdce28 100644
--- a/chrome/test/data/webui/settings/test_util.js
+++ b/chrome/test/data/webui/settings/test_util.js
@@ -269,35 +269,10 @@
    */
   function waitForRender(element) {
     return new Promise(resolve => {
-      // TODO(dpapad): Remove early return once Polymer 2 migration is complete.
-      if (!Polymer.DomIf) {
-        resolve();
-        return;
-      }
-
       Polymer.RenderStatus.beforeNextRender(element, resolve);
     });
   }
 
-  /**
-   * Similar to waitForRender(), but resolves after setTimeout() for Polymer 1.
-   * TODO (rbpotter): Delete this function when the Polymer 2 migration is
-   * complete, and update callers to use waitForRender().
-   * @param {!Element} element
-   * @return {!Promise}
-   */
-  function waitForRenderOrTimeout0(element) {
-    return new Promise(resolve => {
-      if (Polymer.DomIf) {
-        Polymer.RenderStatus.beforeNextRender(element, resolve);
-      } else {
-        setTimeout(() => {
-          resolve();
-        });
-      }
-    });
-  }
-
   return {
     createContentSettingTypeToValuePair: createContentSettingTypeToValuePair,
     createDefaultContentSetting: createDefaultContentSetting,
@@ -311,8 +286,6 @@
     getContentSettingsTypeFromChooserType:
         getContentSettingsTypeFromChooserType,
     waitForRender: waitForRender,
-    waitForRenderOrTimeout0: waitForRenderOrTimeout0,
     whenAttributeIs: whenAttributeIs,
   };
-
 });
diff --git a/chrome/test/data/webui/welcome/welcome_app_test.js b/chrome/test/data/webui/welcome/welcome_app_test.js
index bb284d77..cd17f3d 100644
--- a/chrome/test/data/webui/welcome/welcome_app_test.js
+++ b/chrome/test/data/webui/welcome/welcome_app_test.js
@@ -71,7 +71,7 @@
     test('new user route (can set default)', function() {
       simulateCanSetDefault();
       welcome.navigateTo(welcome.Routes.NEW_USER, 1);
-      return test_util.waitForRenderOrTimeout0(testElement).then(() => {
+      return test_util.waitForRender(testElement).then(() => {
         const views = testElement.shadowRoot.querySelectorAll('[slot=view]');
         assertEquals(views.length, 5);
         ['LANDING-VIEW',
@@ -88,7 +88,7 @@
     test('new user route (cannot set default)', function() {
       simulateCannotSetDefault();
       welcome.navigateTo(welcome.Routes.NEW_USER, 1);
-      return test_util.waitForRenderOrTimeout0(testElement).then(() => {
+      return test_util.waitForRender(testElement).then(() => {
         const views = testElement.shadowRoot.querySelectorAll('[slot=view]');
         assertEquals(views.length, 4);
         ['LANDING-VIEW',
@@ -104,7 +104,7 @@
     test('returning user route (can set default)', function() {
       simulateCanSetDefault();
       welcome.navigateTo(welcome.Routes.RETURNING_USER, 1);
-      return test_util.waitForRenderOrTimeout0(testElement).then(() => {
+      return test_util.waitForRender(testElement).then(() => {
         const views = testElement.shadowRoot.querySelectorAll('[slot=view]');
         assertEquals(views.length, 2);
         assertEquals(views[0].tagName, 'LANDING-VIEW');
@@ -134,7 +134,7 @@
         // Use the new-user route to test if nux-set-as-default module gets
         // initialized.
         welcome.navigateTo(welcome.Routes.NEW_USER, 1);
-        return test_util.waitForRenderOrTimeout0(testElement).then(() => {
+        return test_util.waitForRender(testElement).then(() => {
           // Use the existence of the nux-set-as-default as indication of
           // whether or not the promise is resolved with the expected result.
           assertEquals(
diff --git a/chrome/test/data/xr/e2e_test_files/resources/webxr_boilerplate.js b/chrome/test/data/xr/e2e_test_files/resources/webxr_boilerplate.js
index ea89e4e..24534fd 100644
--- a/chrome/test/data/xr/e2e_test_files/resources/webxr_boilerplate.js
+++ b/chrome/test/data/xr/e2e_test_files/resources/webxr_boilerplate.js
@@ -41,6 +41,7 @@
   constructor() {
     this.session = null;
     this.frameOfRef = null;
+    this.error = null;
   }
 
   get currentSession() {
@@ -62,6 +63,7 @@
   clearSession() {
     this.session = null;
     this.frameOfRef = null;
+    this.error = null;
   }
 }
 
@@ -73,8 +75,7 @@
 function getSessionType(session) {
   if (session.mode == 'immersive-vr') {
     return sessionTypes.IMMERSIVE;
-  } else if (session.mode == 'immersive-ar' ||
-             session.mode == 'legacy-inline-ar') {
+  } else if (session.mode == 'immersive-ar') {
     return sessionTypes.AR;
   } else {
     return sessionTypes.MAGIC_WINDOW;
@@ -91,6 +92,7 @@
         sessionInfos[sessionTypes.IMMERSIVE].currentSession = session;
         onSessionStarted(session);
       }, (error) => {
+        sessionInfos[sessionTypes.IMMERSIVE].error = error;
         console.info('Immersive VR session request rejected with: ' + error);
       });
       break;
@@ -102,21 +104,9 @@
         sessionInfos[sessionTypes.AR].currentSession = session;
         onSessionStarted(session);
       }, (error) => {
+        sessionInfos[sessionTypes.AR].error = error;
         console.info('Immersive AR session request rejected with: ' + error);
-        console.info('Attempting to fall back to legacy AR mode');
-        navigator.xr.requestSession('legacy-inline-ar').then(
-            (session) => {
-          session.mode = 'legacy-inline-ar';
-          session.updateRenderState({
-              outputContext: webglCanvas.getContext('xrpresent')
-          });
-          console.info('Legacy AR session request succeeded');
-          sessionInfos[sessionTypes.AR].currentSession = session;
-          onSessionStarted(session);
-        }, (error) => {
-          console.info('Legacy AR session request rejected with: ' + error);
-        });
-      });
+     });
       break;
     default:
       throw 'Given unsupported WebXR session type enum ' + sessionTypeToRequest;
diff --git a/chromecast/external_mojo/external_service_support/external_connector.cc b/chromecast/external_mojo/external_service_support/external_connector.cc
index 388d3416..e92a3d1 100644
--- a/chromecast/external_mojo/external_service_support/external_connector.cc
+++ b/chromecast/external_mojo/external_service_support/external_connector.cc
@@ -84,6 +84,15 @@
   }
 }
 
+void ExternalConnector::QueryServiceList(
+    base::OnceCallback<void(
+        std::vector<chromecast::external_mojo::mojom::ExternalServiceInfoPtr>)>
+        callback) {
+  if (BindConnectorIfNecessary()) {
+    connector_->QueryServiceList(std::move(callback));
+  }
+}
+
 void ExternalConnector::BindInterface(
     const std::string& service_name,
     const std::string& interface_name,
diff --git a/chromecast/external_mojo/external_service_support/external_connector.h b/chromecast/external_mojo/external_service_support/external_connector.h
index e5f0f77..ac41af79 100644
--- a/chromecast/external_mojo/external_service_support/external_connector.h
+++ b/chromecast/external_mojo/external_service_support/external_connector.h
@@ -8,6 +8,7 @@
 #include <memory>
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "base/callback.h"
 #include "base/macros.h"
@@ -78,6 +79,13 @@
   // Sends a request for a Chromium ServiceManager connector.
   void SendChromiumConnectorRequest(mojo::ScopedMessagePipeHandle request);
 
+  // Query the list of available services from this connector.
+  void QueryServiceList(
+      base::OnceCallback<
+          void(std::vector<
+               chromecast::external_mojo::mojom::ExternalServiceInfoPtr>)>
+          callback);
+
  private:
   void OnConnectionError();
   bool BindConnectorIfNecessary();
diff --git a/chromecast/external_mojo/public/cpp/external_mojo_broker.cc b/chromecast/external_mojo/public/cpp/external_mojo_broker.cc
index bad226d..6f7d8f98 100644
--- a/chromecast/external_mojo/public/cpp/external_mojo_broker.cc
+++ b/chromecast/external_mojo/public/cpp/external_mojo_broker.cc
@@ -235,6 +235,11 @@
       }
       pending_bind_requests_.erase(p);
     }
+
+    auto& info_entry = services_info_[service_name];
+    info_entry.name = service_name;
+    info_entry.connect_time = base::TimeTicks::Now();
+    info_entry.disconnect_time = base::TimeTicks();
   }
 
   void BindInterface(const std::string& service_name,
@@ -276,6 +281,14 @@
         service_manager::mojom::ConnectorRequest(std::move(interface_pipe)));
   }
 
+  void QueryServiceList(QueryServiceListCallback callback) override {
+    std::vector<chromecast::external_mojo::mojom::ExternalServiceInfoPtr> infos;
+    for (const auto& it : services_info_) {
+      infos.emplace_back(it.second.Clone());
+    }
+    std::move(callback).Run(std::move(infos));
+  }
+
   void OnQueryResult(const std::string& service_name,
                      const std::string& interface_name,
                      mojo::ScopedMessagePipeHandle interface_pipe,
@@ -305,6 +318,7 @@
   void OnServiceLost(const std::string& service_name) {
     LOG(INFO) << service_name << " disconnected";
     services_.erase(service_name);
+    services_info_[service_name].disconnect_time = base::TimeTicks::Now();
   }
 
   ServiceManagerConnectorFacade connector_facade_;
@@ -316,6 +330,7 @@
 
   std::map<std::string, mojom::ExternalServicePtr> services_;
   std::map<std::string, std::vector<PendingBindRequest>> pending_bind_requests_;
+  std::map<std::string, mojom::ExternalServiceInfo> services_info_;
 
   DISALLOW_COPY_AND_ASSIGN(ConnectorImpl);
 };
diff --git a/chromecast/external_mojo/public/mojom/BUILD.gn b/chromecast/external_mojo/public/mojom/BUILD.gn
index 0e60fb8..0cc2050 100644
--- a/chromecast/external_mojo/public/mojom/BUILD.gn
+++ b/chromecast/external_mojo/public/mojom/BUILD.gn
@@ -8,4 +8,8 @@
   sources = [
     "connector.mojom",
   ]
+
+  public_deps = [
+    "//mojo/public/mojom/base",
+  ]
 }
diff --git a/chromecast/external_mojo/public/mojom/connector.mojom b/chromecast/external_mojo/public/mojom/connector.mojom
index 9939a4b..189721a7 100644
--- a/chromecast/external_mojo/public/mojom/connector.mojom
+++ b/chromecast/external_mojo/public/mojom/connector.mojom
@@ -4,6 +4,19 @@
 
 module chromecast.external_mojo.mojom;
 
+import "mojo/public/mojom/base/time.mojom";
+
+// Struct for information provided when calling QueryServiceList().
+struct ExternalServiceInfo {
+  // Service name provided to RegisterServiceInstance.
+  string name;
+  // TimeTicks value when the service was last registered.
+  mojo_base.mojom.TimeTicks connect_time;
+  // TimeTicks value when the service was disconnected.
+  // This value is set to TimeTicks() if the service is currently connected.
+  mojo_base.mojom.TimeTicks disconnect_time;
+};
+
 // Interface for external (non-Chromium process) Mojo services to receive Mojo
 // binding requests from other processes/services.
 interface ExternalService {
@@ -36,4 +49,7 @@
 
   // Binds to a Chromium service_manager::Connector instance, if possible.
   BindChromiumConnector(handle<message_pipe> interface_pipe);
+
+  // Query services that are available from this connector.
+  QueryServiceList() => (array<ExternalServiceInfo> services);
 };
diff --git a/chromecast/media/cma/base/demuxer_stream_for_test.cc b/chromecast/media/cma/base/demuxer_stream_for_test.cc
index 983fe22..4e4c027d 100644
--- a/chromecast/media/cma/base/demuxer_stream_for_test.cc
+++ b/chromecast/media/cma/base/demuxer_stream_for_test.cc
@@ -58,7 +58,7 @@
   return ::media::VideoDecoderConfig(
       ::media::kCodecH264, ::media::VIDEO_CODEC_PROFILE_UNKNOWN,
       ::media::PIXEL_FORMAT_YV12, ::media::VideoColorSpace(),
-      ::media::VIDEO_ROTATION_0, coded_size, visible_rect, natural_size,
+      ::media::kNoTransformation, coded_size, visible_rect, natural_size,
       ::media::EmptyExtraData(), ::media::Unencrypted());
 }
 
diff --git a/chromecast/media/cma/pipeline/audio_video_pipeline_impl_unittest.cc b/chromecast/media/cma/pipeline/audio_video_pipeline_impl_unittest.cc
index 847a567d..73de848f 100644
--- a/chromecast/media/cma/pipeline/audio_video_pipeline_impl_unittest.cc
+++ b/chromecast/media/cma/pipeline/audio_video_pipeline_impl_unittest.cc
@@ -155,7 +155,7 @@
       video_configs.push_back(::media::VideoDecoderConfig(
           ::media::kCodecH264, ::media::H264PROFILE_MAIN,
           ::media::PIXEL_FORMAT_I420, ::media::VideoColorSpace(),
-          ::media::VIDEO_ROTATION_0, gfx::Size(640, 480),
+          ::media::kNoTransformation, gfx::Size(640, 480),
           gfx::Rect(0, 0, 640, 480), gfx::Size(640, 480),
           ::media::EmptyExtraData(), ::media::EncryptionScheme()));
       VideoPipelineClient client;
diff --git a/chromecast/media/cma/test/mock_frame_provider.cc b/chromecast/media/cma/test/mock_frame_provider.cc
index 83f3593b..7a23e77 100644
--- a/chromecast/media/cma/test/mock_frame_provider.cc
+++ b/chromecast/media/cma/test/mock_frame_provider.cc
@@ -82,7 +82,7 @@
     video_config = ::media::VideoDecoderConfig(
         ::media::kCodecH264, ::media::VIDEO_CODEC_PROFILE_UNKNOWN,
         ::media::PIXEL_FORMAT_YV12, ::media::VideoColorSpace(),
-        ::media::VIDEO_ROTATION_0, coded_size, visible_rect, natural_size,
+        ::media::kNoTransformation, coded_size, visible_rect, natural_size,
         ::media::EmptyExtraData(), ::media::Unencrypted());
 
     audio_config = ::media::AudioDecoderConfig(
diff --git a/components/cronet/native/engine.cc b/components/cronet/native/engine.cc
index 299af9d6..59698194 100644
--- a/components/cronet/native/engine.cc
+++ b/components/cronet/native/engine.cc
@@ -18,6 +18,7 @@
 #include "components/cronet/cronet_url_request_context.h"
 #include "components/cronet/native/generated/cronet.idl_impl_struct.h"
 #include "components/cronet/native/include/cronet_c.h"
+#include "components/cronet/native/runnables.h"
 #include "components/cronet/url_request_context_config.h"
 #include "components/cronet/version.h"
 #include "components/grpc_support/include/bidirectional_stream_c.h"
@@ -288,6 +289,35 @@
   }
 }
 
+using RequestInfo = base::RefCountedData<Cronet_RequestFinishedInfo>;
+
+void Cronet_EngineImpl::ReportRequestFinished(
+    scoped_refptr<RequestInfo> request_info) {
+  base::flat_map<Cronet_RequestFinishedInfoListenerPtr, Cronet_ExecutorPtr>
+      registrations;
+  {
+    base::AutoLock lock(lock_);
+    // We copy under to avoid calling callbacks (which may run on direct
+    // executors and call Engine methods) with the lock held.
+    //
+    // The map holds only pointers and shouldn't be very large.
+    registrations = request_finished_registrations_;
+  }
+  for (auto& pair : registrations) {
+    auto* request_finished_listener = pair.first;
+    auto* request_finished_executor = pair.second;
+
+    request_finished_executor->Execute(
+        new cronet::OnceClosureRunnable(base::BindOnce(
+            [](scoped_refptr<RequestInfo> request_info,
+               Cronet_RequestFinishedInfoListenerPtr
+                   request_finished_listener) {
+              request_finished_listener->OnRequestFinished(&request_info->data);
+            },
+            request_info, request_finished_listener)));
+  }
+}
+
 Cronet_RESULT Cronet_EngineImpl::CheckResult(Cronet_RESULT result) {
   if (enable_check_result_)
     CHECK_EQ(Cronet_RESULT_SUCCESS, result);
diff --git a/components/cronet/native/engine.h b/components/cronet/native/engine.h
index 14ead83..2075d9c1 100644
--- a/components/cronet/native/engine.h
+++ b/components/cronet/native/engine.h
@@ -10,6 +10,8 @@
 
 #include "base/containers/flat_map.h"
 #include "base/macros.h"
+#include "base/memory/ref_counted.h"
+#include "base/memory/scoped_refptr.h"
 #include "base/synchronization/lock.h"
 #include "base/synchronization/waitable_event.h"
 #include "base/thread_annotations.h"
@@ -65,6 +67,11 @@
   // AddRequestFinishedListener()), and false otherwise.
   bool HasRequestFinishedListener();
 
+  // Provide |request_info| to all registered RequestFinishedListeners.
+  void ReportRequestFinished(
+      scoped_refptr<base::RefCountedData<Cronet_RequestFinishedInfo>>
+          request_info);
+
  private:
   class StreamEngineImpl;
   class Callback;
diff --git a/components/cronet/native/engine_unittest.cc b/components/cronet/native/engine_unittest.cc
index 9b2e3769..86532934 100644
--- a/components/cronet/native/engine_unittest.cc
+++ b/components/cronet/native/engine_unittest.cc
@@ -13,6 +13,9 @@
 
 namespace {
 
+// Fake sent byte count for metrics testing.
+constexpr int64_t kSentByteCount = 12345;
+
 // App implementation of Cronet_Executor methods.
 void TestExecutor_Execute(Cronet_ExecutorPtr self, Cronet_RunnablePtr command) {
   CHECK(self);
@@ -20,11 +23,24 @@
   Cronet_Runnable_Destroy(command);
 }
 
+// Context for TestRequestInfoListener_OnRequestFinished().
+using TestOnRequestFinishedClientContext = int;
+
 // App implementation of Cronet_RequestFinishedInfoListener methods.
+//
+// Expects a client context of type TestOnRequestFinishedClientContext -- will
+// increment this value.
 void TestRequestInfoListener_OnRequestFinished(
     Cronet_RequestFinishedInfoListenerPtr self,
     Cronet_RequestFinishedInfoPtr request_info) {
   CHECK(self);
+  Cronet_ClientContext context =
+      Cronet_RequestFinishedInfoListener_GetClientContext(self);
+  auto* listener_run_count =
+      static_cast<TestOnRequestFinishedClientContext*>(context);
+  ++(*listener_run_count);
+  auto* metrics = Cronet_RequestFinishedInfo_metrics_get(request_info);
+  EXPECT_EQ(kSentByteCount, Cronet_Metrics_sent_byte_count_get(metrics));
 }
 
 TEST(EngineUnitTest, HasNoRequestFinishedInfoListener) {
@@ -56,6 +72,43 @@
   Cronet_Engine_Destroy(engine);
 }
 
+TEST(EngineUnitTest, RequestFinishedInfoListeners) {
+  using RequestInfo = base::RefCountedData<Cronet_RequestFinishedInfo>;
+  constexpr int kNumListeners = 5;
+  TestOnRequestFinishedClientContext listener_run_count = 0;
+
+  Cronet_EnginePtr engine = Cronet_Engine_Create();
+  Cronet_EngineParamsPtr engine_params = Cronet_EngineParams_Create();
+
+  Cronet_RequestFinishedInfoListenerPtr listeners[kNumListeners];
+  Cronet_ExecutorPtr executor =
+      Cronet_Executor_CreateWith(TestExecutor_Execute);
+  for (int i = 0; i < kNumListeners; ++i) {
+    listeners[i] = Cronet_RequestFinishedInfoListener_CreateWith(
+        TestRequestInfoListener_OnRequestFinished);
+    Cronet_RequestFinishedInfoListener_SetClientContext(listeners[i],
+                                                        &listener_run_count);
+    Cronet_Engine_AddRequestFinishedListener(engine, listeners[i], executor);
+  }
+
+  // Simulate the UrlRequest reporting metrics to the engine.
+  auto* engine_impl = static_cast<Cronet_EngineImpl*>(engine);
+  auto request_info = base::MakeRefCounted<RequestInfo>();
+  auto metrics = std::make_unique<Cronet_Metrics>();
+  metrics->sent_byte_count = kSentByteCount;
+  request_info->data.metrics.emplace(*metrics);
+  engine_impl->ReportRequestFinished(request_info);
+  EXPECT_EQ(kNumListeners, listener_run_count);
+
+  for (auto* listener : listeners) {
+    Cronet_RequestFinishedInfoListener_Destroy(listener);
+    Cronet_Engine_RemoveRequestFinishedListener(engine, listener);
+  }
+  Cronet_Executor_Destroy(executor);
+  Cronet_Engine_Destroy(engine);
+  Cronet_EngineParams_Destroy(engine_params);
+}
+
 // EXPECT_DEBUG_DEATH(), used by the tests below, isn't available on iOS.
 #if !defined(OS_IOS)
 
diff --git a/components/cronet/native/test/BUILD.gn b/components/cronet/native/test/BUILD.gn
index bcd4e08..023157d 100644
--- a/components/cronet/native/test/BUILD.gn
+++ b/components/cronet/native/test/BUILD.gn
@@ -37,6 +37,7 @@
     "//components/grpc_support:bidirectional_stream_test",
     "//components/grpc_support/test:get_stream_engine_header",
     "//net:test_support",
+    "//testing/gmock",
     "//testing/gtest",
   ]
 
diff --git a/components/cronet/native/test/url_request_test.cc b/components/cronet/native/test/url_request_test.cc
index 29b70b2..ee4ecccd 100644
--- a/components/cronet/native/test/url_request_test.cc
+++ b/components/cronet/native/test/url_request_test.cc
@@ -18,12 +18,15 @@
 #include "components/cronet/native/test/test_util.h"
 #include "components/cronet/test/test_server.h"
 #include "cronet_c.h"
+#include "net/test/embedded_test_server/default_handlers.h"
 #include "net/test/embedded_test_server/embedded_test_server.h"
+#include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "url/gurl.h"
 
 using cronet::test::TestUploadDataProvider;
 using cronet::test::TestUrlRequestCallback;
+using ::testing::HasSubstr;
 
 namespace {
 
@@ -134,8 +137,9 @@
       const std::string& url,
       std::unique_ptr<TestUrlRequestCallback> test_callback,
       const std::string& http_method,
-      TestUploadDataProvider* test_upload_data_provider) {
-    Cronet_EnginePtr engine = cronet::test::CreateTestEngine(0);
+      TestUploadDataProvider* test_upload_data_provider,
+      int remapped_port) {
+    Cronet_EnginePtr engine = cronet::test::CreateTestEngine(remapped_port);
     Cronet_UrlRequestPtr request = Cronet_UrlRequest_Create();
     Cronet_UrlRequestParamsPtr request_params =
         Cronet_UrlRequestParams_Create();
@@ -189,6 +193,16 @@
 
   std::unique_ptr<TestUrlRequestCallback> StartAndWaitForComplete(
       const std::string& url,
+      std::unique_ptr<TestUrlRequestCallback> test_callback,
+      const std::string& http_method,
+      TestUploadDataProvider* test_upload_data_provider) {
+    return StartAndWaitForComplete(url, std::move(test_callback), http_method,
+                                   test_upload_data_provider,
+                                   /* remapped_port = */ 0);
+  }
+
+  std::unique_ptr<TestUrlRequestCallback> StartAndWaitForComplete(
+      const std::string& url,
       std::unique_ptr<TestUrlRequestCallback> test_callback) {
     return StartAndWaitForComplete(url, std::move(test_callback),
                                    /* http_method =  */ std::string(),
@@ -519,6 +533,28 @@
   EXPECT_EQ("net::ERR_CERT_INVALID", callback->last_error_message_);
 }
 
+TEST_P(UrlRequestTest, SSLUpload) {
+  net::EmbeddedTestServer ssl_server(net::EmbeddedTestServer::TYPE_HTTPS);
+  net::test_server::RegisterDefaultHandlers(&ssl_server);
+  ASSERT_TRUE(ssl_server.Start());
+
+  constexpr char kUrl[] = "https://test.example.com/echoall";
+  constexpr char kUploadString[] =
+      "The quick brown fox jumps over the lazy dog.";
+  TestUploadDataProvider data_provider(TestUploadDataProvider::SYNC,
+                                       /* executor = */ nullptr);
+  data_provider.AddRead(kUploadString);
+  auto callback =
+      std::make_unique<TestUrlRequestCallback>(GetDirectExecutorParam());
+  callback = StartAndWaitForComplete(kUrl, std::move(callback), std::string(),
+                                     &data_provider, ssl_server.port());
+  data_provider.AssertClosed();
+  EXPECT_NE(nullptr, callback->response_info_);
+  EXPECT_EQ("", callback->last_error_message_);
+  EXPECT_EQ(200, callback->response_info_->http_status_code);
+  EXPECT_THAT(callback->response_as_string_, HasSubstr(kUploadString));
+}
+
 TEST_P(UrlRequestTest, UploadMultiplePiecesSync) {
   const std::string url = cronet::TestServer::GetEchoRequestBodyURL();
   auto callback =
diff --git a/components/dom_distiller/core/css/distilledpage.css b/components/dom_distiller/core/css/distilledpage.css
index 9880e2cd..9a28e9b 100644
--- a/components/dom_distiller/core/css/distilledpage.css
+++ b/components/dom_distiller/core/css/distilledpage.css
@@ -2,6 +2,10 @@
  * Use of this source code is governed by a BSD-style license that can be
  * found in the LICENSE file. */
 
+/* This file contains style used across ALL platforms. Platform-specific styling
+ * should be placed in the corresponding file (e.g. desktop style goes in
+ * distilledpage_desktop.css).*/
+
 /* Set the global 'box-sizing' state to 'border-box'.
  * *::after and *::before used to select pseudo-elements not selectable by *. */
 
@@ -287,12 +291,6 @@
   width: 100%;
 }
 
-@media screen {
-  #mainContent {
-    max-width: 35em;
-  }
-}
-
 #articleHeader {
   margin-top: 24px;
   width: 100%;
@@ -402,4 +400,3 @@
   top: 0px;
   width: 100%;
 }
-
diff --git a/components/dom_distiller/core/css/distilledpage_desktop.css b/components/dom_distiller/core/css/distilledpage_desktop.css
new file mode 100644
index 0000000..a41abde5
--- /dev/null
+++ b/components/dom_distiller/core/css/distilledpage_desktop.css
@@ -0,0 +1,13 @@
+/* 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. */
+
+/* This file should contain style used on desktop but not Android or iOS. */
+
+#mainContent {
+  width: 75%;
+  box-shadow: 0 0 5px 1px rgba(0, 0, 0, 0.4);
+  padding: 1em;
+  margin-top: 1em;
+  margin-bottom: 1em;
+}
diff --git a/components/dom_distiller/core/css/distilledpage_mobile.css b/components/dom_distiller/core/css/distilledpage_mobile.css
new file mode 100644
index 0000000..73e8b23
--- /dev/null
+++ b/components/dom_distiller/core/css/distilledpage_mobile.css
@@ -0,0 +1,9 @@
+/* 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. */
+
+/* This file should contain style shared by Android and iOS but not desktop. */
+
+#mainContent {
+  max-width: 35em;
+}
diff --git a/components/dom_distiller/core/viewer.cc b/components/dom_distiller/core/viewer.cc
index 8c3f533..9bdc077c 100644
--- a/components/dom_distiller/core/viewer.cc
+++ b/components/dom_distiller/core/viewer.cc
@@ -10,6 +10,7 @@
 
 #include "base/json/json_writer.h"
 #include "base/metrics/histogram_macros.h"
+#include "base/strings/strcat.h"
 #include "base/strings/string_number_conversions.h"
 #include "base/strings/string_util.h"
 #include "build/build_config.h"
@@ -54,6 +55,23 @@
 const char kSansSerifCssClass[] = "sans-serif";
 const char kMonospaceCssClass[] = "monospace";
 
+std::string GetCssFromResourceId(int id) {
+  return ui::ResourceBundle::GetSharedInstance()
+      .GetRawDataResource(id)
+      .as_string();
+}
+
+std::string GetPlatformSpecificCss() {
+#if defined(OS_IOS)
+  return base::StrCat({GetCssFromResourceId(IDR_DISTILLER_MOBILE_CSS),
+                       GetCssFromResourceId(IDR_DISTILLER_IOS_CSS)});
+#elif defined(OS_ANDROID)
+  return GetCssFromResourceId(IDR_DISTILLER_MOBILE_CSS);
+#else  // Desktop
+  return GetCssFromResourceId(IDR_DISTILLER_DESKTOP_CSS);
+#endif
+}
+
 // Maps themes to JS themes.
 const std::string GetJsTheme(DistilledPagePrefs::Theme theme) {
   if (theme == DistilledPagePrefs::THEME_DARK)
@@ -112,7 +130,7 @@
 #if defined(OS_IOS)
   // On iOS the content is inlined as there is no API to detect those requests
   // and return the local data once a page is loaded.
-  css << "<style>" << viewer::GetCss() << viewer::GetIOSCss() << "</style>";
+  css << "<style>" << viewer::GetCss() << "</style>";
   svg << viewer::GetLoadingImage();
 #else
   css << "<link rel=\"stylesheet\" href=\"/" << kViewerCssPath << "\">";
@@ -215,9 +233,8 @@
 }
 
 const std::string GetCss() {
-  return ui::ResourceBundle::GetSharedInstance()
-      .GetRawDataResource(IDR_DISTILLER_CSS)
-      .as_string();
+  return base::StrCat(
+      {GetCssFromResourceId(IDR_DISTILLER_CSS), GetPlatformSpecificCss()});
 }
 
 const std::string GetLoadingImage() {
@@ -226,12 +243,6 @@
       .as_string();
 }
 
-const std::string GetIOSCss() {
-  return ui::ResourceBundle::GetSharedInstance()
-      .GetRawDataResource(IDR_DISTILLER_IOS_CSS)
-      .as_string();
-}
-
 const std::string GetJavaScript() {
   return ui::ResourceBundle::GetSharedInstance()
       .GetRawDataResource(IDR_DOM_DISTILLER_VIEWER_JS)
diff --git a/components/dom_distiller/core/viewer.h b/components/dom_distiller/core/viewer.h
index eef5b17..724e95a 100644
--- a/components/dom_distiller/core/viewer.h
+++ b/components/dom_distiller/core/viewer.h
@@ -62,15 +62,12 @@
 // the last page of the article (i.e. loading indicator should be removed).
 const std::string GetToggleLoadingIndicatorJs(bool is_last_page);
 
-// Returns the default CSS to be used for a viewer.
+// Returns the CSS to use for a viewer.
 const std::string GetCss();
 
 // Returns the animated SVG loading image for a viewer.
 const std::string GetLoadingImage();
 
-// Returns the iOS specific CSS to be used for the distiller viewer.
-const std::string GetIOSCss();
-
 // Returns the default JS to be used for a viewer.
 const std::string GetJavaScript();
 
diff --git a/components/password_manager/content/common/credential_manager_mojom_traits.cc b/components/password_manager/content/common/credential_manager_mojom_traits.cc
index dff9cb42..97a463e8 100644
--- a/components/password_manager/content/common/credential_manager_mojom_traits.cc
+++ b/components/password_manager/content/common/credential_manager_mojom_traits.cc
@@ -96,6 +96,7 @@
     case blink::mojom::CredentialManagerError::NOT_IMPLEMENTED:
     case blink::mojom::CredentialManagerError::NOT_FOCUSED:
     case blink::mojom::CredentialManagerError::RESIDENT_CREDENTIALS_UNSUPPORTED:
+    case blink::mojom::CredentialManagerError::PROTECTION_POLICY_INCONSISTENT:
     case blink::mojom::CredentialManagerError::UNKNOWN:
       *output = password_manager::CredentialManagerError::UNKNOWN;
       return true;
diff --git a/components/policy/resources/policy_templates.json b/components/policy/resources/policy_templates.json
index 644310a..998043c 100644
--- a/components/policy/resources/policy_templates.json
+++ b/components/policy/resources/policy_templates.json
@@ -11944,9 +11944,9 @@
           'caption': '''Password entry is required every twelve hours''',
         },
         {
-          'name': 'Day',
+          'name': 'TwoDays',
           'value': 2,
-          'caption': '''Password entry is required every day (24 hours)''',
+          'caption': '''Password entry is required every two days (48 hours)''',
         },
         {
           'name': 'Week',
diff --git a/components/resources/dom_distiller_resources.grdp b/components/resources/dom_distiller_resources.grdp
index 3c3cc1a..a274979e 100644
--- a/components/resources/dom_distiller_resources.grdp
+++ b/components/resources/dom_distiller_resources.grdp
@@ -7,7 +7,9 @@
   <include name="IDR_DOM_DISTILLER_VIEWER_JS" file="../dom_distiller/core/javascript/dom_distiller_viewer.js" type="BINDATA" />
   <include name="IDR_DISTILLER_JS" file="../dom_distiller/core/javascript/domdistiller.js" flattenhtml="true" type="BINDATA" />
   <include name="IDR_DISTILLER_CSS" file="../dom_distiller/core/css/distilledpage.css" type="BINDATA" />
+  <include name="IDR_DISTILLER_DESKTOP_CSS" file="../dom_distiller/core/css/distilledpage_desktop.css" type="BINDATA" />
   <include name="IDR_DISTILLER_IOS_CSS" file="../dom_distiller/core/css/distilledpage_ios.css" type="BINDATA" />
+  <include name="IDR_DISTILLER_MOBILE_CSS" file="../dom_distiller/core/css/distilledpage_mobile.css" type="BINDATA" />
   <include name="IDR_DISTILLER_LOADING_IMAGE" file="../dom_distiller/core/images/dom_distiller_material_spinner.svg" type="BINDATA" />
   <include name="IDR_EXTRACT_PAGE_FEATURES_JS" file="../dom_distiller/core/javascript/extract_features.js" type="BINDATA" />
   <include name="IDR_DISTILLABLE_PAGE_SERIALIZED_MODEL_NEW" file="../dom_distiller/core/data/distillable_page_model_new.bin" type="BINDATA" />
diff --git a/components/test/data/vr_browser_video/render_tests/VrBrowserWebInputEditingTest.fullscreen_video_paused_browser_content.Pixel_XL-25.png.sha1 b/components/test/data/vr_browser_video/render_tests/VrBrowserWebInputEditingTest.fullscreen_video_paused_browser_content.Pixel_XL-25.png.sha1
index 5714e3f9..f46fb206 100644
--- a/components/test/data/vr_browser_video/render_tests/VrBrowserWebInputEditingTest.fullscreen_video_paused_browser_content.Pixel_XL-25.png.sha1
+++ b/components/test/data/vr_browser_video/render_tests/VrBrowserWebInputEditingTest.fullscreen_video_paused_browser_content.Pixel_XL-25.png.sha1
@@ -1 +1 @@
-9d6c493f9dfe09fce6eaca19349a88d149cb2898
\ No newline at end of file
+9ef5447ea3d00d9978e8389ab1f25eb12d012396
\ No newline at end of file
diff --git a/components/test/data/vr_browser_video/render_tests/VrBrowserWebInputEditingTest.fullscreen_video_paused_browser_content.Pixel_XL-26.png.sha1 b/components/test/data/vr_browser_video/render_tests/VrBrowserWebInputEditingTest.fullscreen_video_paused_browser_content.Pixel_XL-26.png.sha1
index 609f6d8..63d588f5 100644
--- a/components/test/data/vr_browser_video/render_tests/VrBrowserWebInputEditingTest.fullscreen_video_paused_browser_content.Pixel_XL-26.png.sha1
+++ b/components/test/data/vr_browser_video/render_tests/VrBrowserWebInputEditingTest.fullscreen_video_paused_browser_content.Pixel_XL-26.png.sha1
@@ -1 +1 @@
-6076800c2aa6ed0cd77881abfe553230a2529cd1
\ No newline at end of file
+3a6e79cb8d4e001ebb370274022286eff523dee9
\ No newline at end of file
diff --git a/components/viz/common/features.cc b/components/viz/common/features.cc
index 0cdf417..1643390 100644
--- a/components/viz/common/features.cc
+++ b/components/viz/common/features.cc
@@ -64,19 +64,11 @@
 }
 
 bool IsVizDisplayCompositorEnabled() {
-#if defined(OS_MACOSX) || defined(OS_WIN) || \
-    (defined(OS_LINUX) && !defined(OS_CHROMEOS))
-  // We can't remove the feature switch yet because OOP-D isn't enabled on all
-  // platforms but turning it off on Mac, Windows and Linux is broken. Don't
-  // check the feature switch for these platforms anymore.
-  return true;
-#else
 #if defined(OS_ANDROID)
   if (features::IsAndroidSurfaceControlEnabled())
     return true;
 #endif
   return base::FeatureList::IsEnabled(kVizDisplayCompositor);
-#endif
 }
 
 bool IsVizHitTestingDebugEnabled() {
diff --git a/components/viz/service/BUILD.gn b/components/viz/service/BUILD.gn
index 2134574..6ad1ea5 100644
--- a/components/viz/service/BUILD.gn
+++ b/components/viz/service/BUILD.gn
@@ -322,6 +322,8 @@
     "display_embedder/skia_output_device_gl.h",
     "display_embedder/skia_output_device_offscreen.cc",
     "display_embedder/skia_output_device_offscreen.h",
+    "display_embedder/skia_output_surface_base.cc",
+    "display_embedder/skia_output_surface_base.h",
     "display_embedder/skia_output_surface_impl.cc",
     "display_embedder/skia_output_surface_impl.h",
     "display_embedder/skia_output_surface_impl_non_ddl.cc",
diff --git a/components/viz/service/display_embedder/skia_output_surface_base.cc b/components/viz/service/display_embedder/skia_output_surface_base.cc
new file mode 100644
index 0000000..b11dcce
--- /dev/null
+++ b/components/viz/service/display_embedder/skia_output_surface_base.cc
@@ -0,0 +1,147 @@
+// 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/viz/service/display_embedder/skia_output_surface_base.h"
+
+#include "build/build_config.h"
+#include "components/viz/service/display/output_surface_frame.h"
+#include "components/viz/service/display/resource_metadata.h"
+#include "components/viz/service/display_embedder/image_context.h"
+#include "components/viz/service/gl/gpu_service_impl.h"
+#include "ui/gl/gl_bindings.h"
+
+namespace viz {
+
+SkiaOutputSurfaceBase::SkiaOutputSurfaceBase() {
+  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
+}
+
+SkiaOutputSurfaceBase::~SkiaOutputSurfaceBase() {
+  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
+}
+
+void SkiaOutputSurfaceBase::BindToClient(OutputSurfaceClient* client) {
+  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
+  DCHECK(client);
+  DCHECK(!client_);
+  client_ = client;
+}
+
+void SkiaOutputSurfaceBase::BindFramebuffer() {
+  // TODO(penghuang): remove this method when GLRenderer is removed.
+}
+
+void SkiaOutputSurfaceBase::SetDrawRectangle(const gfx::Rect& draw_rectangle) {
+  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
+
+  // This GLSurface::SetDrawRectangle is a no-op for all GLSurface subclasses
+  // except DirectCompositionSurfaceWin.
+#if defined(OS_WIN)
+  NOTIMPLEMENTED();
+#endif
+}
+
+void SkiaOutputSurfaceBase::SwapBuffers(OutputSurfaceFrame frame) {
+  NOTREACHED();
+}
+
+uint32_t SkiaOutputSurfaceBase::GetFramebufferCopyTextureFormat() {
+  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
+
+  return GL_RGB;
+}
+
+OverlayCandidateValidator* SkiaOutputSurfaceBase::GetOverlayCandidateValidator()
+    const {
+  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
+  return nullptr;
+}
+
+bool SkiaOutputSurfaceBase::IsDisplayedAsOverlayPlane() const {
+  return false;
+}
+
+unsigned SkiaOutputSurfaceBase::GetOverlayTextureId() const {
+  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
+  return 0;
+}
+
+gfx::BufferFormat SkiaOutputSurfaceBase::GetOverlayBufferFormat() const {
+  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
+  return gfx::BufferFormat::RGBX_8888;
+}
+
+bool SkiaOutputSurfaceBase::HasExternalStencilTest() const {
+  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
+
+  return false;
+}
+
+void SkiaOutputSurfaceBase::ApplyExternalStencil() {
+  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
+}
+
+unsigned SkiaOutputSurfaceBase::UpdateGpuFence() {
+  return 0;
+}
+
+void SkiaOutputSurfaceBase::SetNeedsSwapSizeNotifications(
+    bool needs_swap_size_notifications) {
+  needs_swap_size_notifications_ = needs_swap_size_notifications;
+}
+
+void SkiaOutputSurfaceBase::SetUpdateVSyncParametersCallback(
+    UpdateVSyncParametersCallback callback) {}
+
+void SkiaOutputSurfaceBase::AddContextLostObserver(
+    ContextLostObserver* observer) {
+  observers_.AddObserver(observer);
+}
+
+void SkiaOutputSurfaceBase::RemoveContextLostObserver(
+    ContextLostObserver* observer) {
+  observers_.RemoveObserver(observer);
+}
+
+void SkiaOutputSurfaceBase::PrepareYUVATextureIndices(
+    const std::vector<ResourceMetadata>& metadatas,
+    bool has_alpha,
+    SkYUVAIndex indices[4]) {
+  DCHECK((has_alpha && (metadatas.size() == 3 || metadatas.size() == 4)) ||
+         (!has_alpha && (metadatas.size() == 2 || metadatas.size() == 3)));
+
+  bool uv_interleaved =
+      has_alpha ? metadatas.size() == 3 : metadatas.size() == 2;
+
+  indices[SkYUVAIndex::kY_Index].fIndex = 0;
+  indices[SkYUVAIndex::kY_Index].fChannel = SkColorChannel::kR;
+
+  if (uv_interleaved) {
+    indices[SkYUVAIndex::kU_Index].fIndex = 1;
+    indices[SkYUVAIndex::kU_Index].fChannel = SkColorChannel::kR;
+
+    indices[SkYUVAIndex::kV_Index].fIndex = 1;
+    indices[SkYUVAIndex::kV_Index].fChannel = SkColorChannel::kG;
+
+    indices[SkYUVAIndex::kA_Index].fIndex = has_alpha ? 2 : -1;
+    indices[SkYUVAIndex::kA_Index].fChannel = SkColorChannel::kR;
+  } else {
+    indices[SkYUVAIndex::kU_Index].fIndex = 1;
+    indices[SkYUVAIndex::kU_Index].fChannel = SkColorChannel::kR;
+
+    indices[SkYUVAIndex::kV_Index].fIndex = 2;
+    indices[SkYUVAIndex::kV_Index].fChannel = SkColorChannel::kR;
+
+    indices[SkYUVAIndex::kA_Index].fIndex = has_alpha ? 3 : -1;
+    indices[SkYUVAIndex::kA_Index].fChannel = SkColorChannel::kR;
+  }
+}
+
+void SkiaOutputSurfaceBase::ContextLost() {
+  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
+  for (auto& observer : observers_)
+    observer.OnContextLost();
+}
+
+}  // namespace viz
diff --git a/components/viz/service/display_embedder/skia_output_surface_base.h b/components/viz/service/display_embedder/skia_output_surface_base.h
new file mode 100644
index 0000000..7eba081
--- /dev/null
+++ b/components/viz/service/display_embedder/skia_output_surface_base.h
@@ -0,0 +1,75 @@
+// 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 COMPONENTS_VIZ_SERVICE_DISPLAY_EMBEDDER_SKIA_OUTPUT_SURFACE_BASE_H_
+#define COMPONENTS_VIZ_SERVICE_DISPLAY_EMBEDDER_SKIA_OUTPUT_SURFACE_BASE_H_
+
+#include <vector>
+
+#include "base/macros.h"
+#include "base/observer_list.h"
+#include "base/threading/thread_checker.h"
+#include "components/viz/service/display/skia_output_surface.h"
+#include "components/viz/service/viz_service_export.h"
+#include "third_party/skia/include/core/SkYUVAIndex.h"
+
+namespace viz {
+
+struct ImageContext;
+
+class VIZ_SERVICE_EXPORT SkiaOutputSurfaceBase : public SkiaOutputSurface {
+ public:
+  // OutputSurface implementation:
+  void BindToClient(OutputSurfaceClient* client) override;
+  void BindFramebuffer() override;
+  void SetDrawRectangle(const gfx::Rect& draw_rectangle) override;
+  void SwapBuffers(OutputSurfaceFrame frame) override;
+  uint32_t GetFramebufferCopyTextureFormat() override;
+  OverlayCandidateValidator* GetOverlayCandidateValidator() const override;
+  bool IsDisplayedAsOverlayPlane() const override;
+  unsigned GetOverlayTextureId() const override;
+  gfx::BufferFormat GetOverlayBufferFormat() const override;
+  bool HasExternalStencilTest() const override;
+  void ApplyExternalStencil() override;
+  unsigned UpdateGpuFence() override;
+  void SetNeedsSwapSizeNotifications(
+      bool needs_swap_size_notifications) override;
+  void SetUpdateVSyncParametersCallback(
+      UpdateVSyncParametersCallback callback) override;
+
+  // SkiaOutputSurface implementation:
+  void AddContextLostObserver(ContextLostObserver* observer) override;
+  void RemoveContextLostObserver(ContextLostObserver* observer) override;
+
+ protected:
+  SkiaOutputSurfaceBase();
+  ~SkiaOutputSurfaceBase() override;
+
+  void PrepareYUVATextureIndices(const std::vector<ResourceMetadata>& metadatas,
+                                 bool has_alpha,
+                                 SkYUVAIndex indices[4]);
+  void ContextLost();
+
+  OutputSurfaceClient* client_ = nullptr;
+  bool needs_swap_size_notifications_ = false;
+
+  // Cached promise image.
+  base::flat_map<ResourceId, std::unique_ptr<ImageContext>>
+      promise_image_cache_;
+
+  // Images for current frame or render pass.
+  std::vector<ImageContext*> images_in_current_paint_;
+
+  THREAD_CHECKER(thread_checker_);
+
+ private:
+  // Observers for context lost.
+  base::ObserverList<ContextLostObserver>::Unchecked observers_;
+
+  DISALLOW_COPY_AND_ASSIGN(SkiaOutputSurfaceBase);
+};
+
+}  // namespace viz
+
+#endif  // COMPONENTS_VIZ_SERVICE_DISPLAY_EMBEDDER_SKIA_OUTPUT_SURFACE_BASE_H_
diff --git a/components/viz/service/display_embedder/skia_output_surface_impl.cc b/components/viz/service/display_embedder/skia_output_surface_impl.cc
index 45f09fc2..13b89f2 100644
--- a/components/viz/service/display_embedder/skia_output_surface_impl.cc
+++ b/components/viz/service/display_embedder/skia_output_surface_impl.cc
@@ -17,7 +17,6 @@
 #include "components/viz/common/frame_sinks/begin_frame_source.h"
 #include "components/viz/common/frame_sinks/copy_output_request.h"
 #include "components/viz/common/frame_sinks/copy_output_util.h"
-#include "components/viz/common/gpu/context_lost_observer.h"
 #include "components/viz/common/resources/resource_format_utils.h"
 #include "components/viz/service/display/output_surface_client.h"
 #include "components/viz/service/display/output_surface_frame.h"
@@ -29,9 +28,7 @@
 #include "gpu/command_buffer/service/scheduler.h"
 #include "gpu/command_buffer/service/shared_image_representation.h"
 #include "gpu/vulkan/buildflags.h"
-#include "third_party/skia/include/core/SkYUVAIndex.h"
 #include "ui/gfx/skia_util.h"
-#include "ui/gl/gl_bindings.h"
 #include "ui/gl/gl_context.h"
 #include "ui/gl/gl_gl_api_implementation.h"
 
@@ -71,7 +68,7 @@
     gpu::SurfaceHandle surface_handle,
     const RendererSettings& renderer_settings)
     : gpu_service_(gpu_service),
-      is_using_vulkan_(gpu_service->is_using_vulkan()),
+      is_using_vulkan_(gpu_service_->is_using_vulkan()),
       surface_handle_(surface_handle),
       renderer_settings_(renderer_settings),
       weak_ptr_factory_(this) {
@@ -118,10 +115,7 @@
 
 void SkiaOutputSurfaceImpl::BindToClient(OutputSurfaceClient* client) {
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
-  DCHECK(client);
-  DCHECK(!client_);
-
-  client_ = client;
+  SkiaOutputSurfaceBase::BindToClient(client);
   weak_ptr_ = weak_ptr_factory_.GetWeakPtr();
   client_thread_task_runner_ = base::ThreadTaskRunnerHandle::Get();
   base::WaitableEvent event(base::WaitableEvent::ResetPolicy::MANUAL,
@@ -150,20 +144,6 @@
   ScheduleGpuTask(std::move(callback), std::vector<gpu::SyncToken>());
 }
 
-void SkiaOutputSurfaceImpl::BindFramebuffer() {
-  // TODO(penghuang): remove this method when GLRenderer is removed.
-}
-
-void SkiaOutputSurfaceImpl::SetDrawRectangle(const gfx::Rect& draw_rectangle) {
-  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
-
-  // This GLSurface::SetDrawRectangle is a no-op for all GLSurface subclasses
-  // except DirectCompositionSurfaceWin.
-#if defined(OS_WIN)
-  NOTIMPLEMENTED();
-#endif
-}
-
 void SkiaOutputSurfaceImpl::Reshape(const gfx::Size& size,
                                     float device_scale_factor,
                                     const gfx::ColorSpace& color_space,
@@ -197,55 +177,6 @@
   ScheduleGpuTask(std::move(callback), std::vector<gpu::SyncToken>());
 }
 
-void SkiaOutputSurfaceImpl::SwapBuffers(OutputSurfaceFrame frame) {
-  NOTREACHED();
-}
-
-uint32_t SkiaOutputSurfaceImpl::GetFramebufferCopyTextureFormat() {
-  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
-
-  return GL_RGB;
-}
-
-OverlayCandidateValidator* SkiaOutputSurfaceImpl::GetOverlayCandidateValidator()
-    const {
-  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
-  return nullptr;
-}
-
-bool SkiaOutputSurfaceImpl::IsDisplayedAsOverlayPlane() const {
-  return false;
-}
-
-unsigned SkiaOutputSurfaceImpl::GetOverlayTextureId() const {
-  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
-  return 0;
-}
-
-gfx::BufferFormat SkiaOutputSurfaceImpl::GetOverlayBufferFormat() const {
-  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
-  return gfx::BufferFormat::RGBX_8888;
-}
-
-bool SkiaOutputSurfaceImpl::HasExternalStencilTest() const {
-  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
-
-  return false;
-}
-
-void SkiaOutputSurfaceImpl::ApplyExternalStencil() {
-  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
-}
-
-unsigned SkiaOutputSurfaceImpl::UpdateGpuFence() {
-  return 0;
-}
-
-void SkiaOutputSurfaceImpl::SetNeedsSwapSizeNotifications(
-    bool needs_swap_size_notifications) {
-  needs_swap_size_notifications_ = needs_swap_size_notifications;
-}
-
 void SkiaOutputSurfaceImpl::SetUpdateVSyncParametersCallback(
     UpdateVSyncParametersCallback callback) {
   update_vsync_parameters_callback_ = std::move(callback);
@@ -322,23 +253,15 @@
   DCHECK((has_alpha && (metadatas.size() == 3 || metadatas.size() == 4)) ||
          (!has_alpha && (metadatas.size() == 2 || metadatas.size() == 3)));
 
-  bool uv_interleaved =
-      has_alpha ? metadatas.size() == 3 : metadatas.size() == 2;
+  SkYUVAIndex indices[4];
+  PrepareYUVATextureIndices(metadatas, has_alpha, indices);
 
-  GrBackendFormat formats[4];
-  SkYUVAIndex indices[4] = {
-      {-1, SkColorChannel::kR},
-      {-1, SkColorChannel::kR},
-      {-1, SkColorChannel::kR},
-      {-1, SkColorChannel::kR},
-  };
+  GrBackendFormat formats[4] = {};
   SkISize yuva_sizes[4] = {};
   SkDeferredDisplayListRecorder::PromiseImageTextureContext
-      texture_contexts[4] = {nullptr, nullptr, nullptr, nullptr};
-
-  std::vector<std::unique_ptr<ImageContext>> image_contexts(metadatas.size());
-  const auto process_planar = [&](size_t i) {
-    auto metadata = metadatas[i];
+      texture_contexts[4] = {};
+  for (size_t i = 0; i < metadatas.size(); ++i) {
+    const auto& metadata = metadatas[i];
     DCHECK(metadata.origin == kTopLeft_GrSurfaceOrigin);
     formats[i] = GetGrBackendFormatForTexture(
         metadata.resource_format, metadata.mailbox_holder.texture_target);
@@ -359,42 +282,8 @@
     }
     images_in_current_paint_.push_back(image_context.get());
     texture_contexts[i] = image_context.get();
-  };
-
-  if (uv_interleaved) {
-    process_planar(0);
-    indices[SkYUVAIndex::kY_Index].fIndex = 0;
-    indices[SkYUVAIndex::kY_Index].fChannel = SkColorChannel::kR;
-
-    process_planar(1);
-    indices[SkYUVAIndex::kU_Index].fIndex = 1;
-    indices[SkYUVAIndex::kU_Index].fChannel = SkColorChannel::kR;
-
-    indices[SkYUVAIndex::kV_Index].fIndex = 1;
-    indices[SkYUVAIndex::kV_Index].fChannel = SkColorChannel::kG;
-    if (has_alpha) {
-      process_planar(2);
-      indices[SkYUVAIndex::kA_Index].fIndex = 2;
-      indices[SkYUVAIndex::kA_Index].fChannel = SkColorChannel::kR;
-    }
-  } else {
-    process_planar(0);
-    indices[SkYUVAIndex::kY_Index].fIndex = 0;
-    indices[SkYUVAIndex::kY_Index].fChannel = SkColorChannel::kR;
-
-    process_planar(1);
-    indices[SkYUVAIndex::kU_Index].fIndex = 1;
-    indices[SkYUVAIndex::kU_Index].fChannel = SkColorChannel::kR;
-
-    process_planar(2);
-    indices[SkYUVAIndex::kV_Index].fIndex = 2;
-    indices[SkYUVAIndex::kV_Index].fChannel = SkColorChannel::kR;
-    if (has_alpha) {
-      process_planar(3);
-      indices[SkYUVAIndex::kA_Index].fIndex = 3;
-      indices[SkYUVAIndex::kA_Index].fChannel = SkColorChannel::kR;
-    }
   }
+
   auto image = recorder_->makeYUVAPromiseTexture(
       yuv_color_space, formats, yuva_sizes, indices, yuva_sizes[0].width(),
       yuva_sizes[0].height(), kTopLeft_GrSurfaceOrigin, dst_color_space,
@@ -585,16 +474,6 @@
   ScheduleGpuTask(std::move(callback), std::vector<gpu::SyncToken>());
 }
 
-void SkiaOutputSurfaceImpl::AddContextLostObserver(
-    ContextLostObserver* observer) {
-  observers_.AddObserver(observer);
-}
-
-void SkiaOutputSurfaceImpl::RemoveContextLostObserver(
-    ContextLostObserver* observer) {
-  observers_.RemoveObserver(observer);
-}
-
 void SkiaOutputSurfaceImpl::SetCapabilitiesForTesting(
     bool flipped_output_surface) {
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
@@ -703,12 +582,6 @@
   }
 }
 
-void SkiaOutputSurfaceImpl::ContextLost() {
-  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
-  for (auto& observer : observers_)
-    observer.OnContextLost();
-}
-
 void SkiaOutputSurfaceImpl::ScheduleGpuTask(
     base::OnceClosure callback,
     std::vector<gpu::SyncToken> sync_tokens) {
diff --git a/components/viz/service/display_embedder/skia_output_surface_impl.h b/components/viz/service/display_embedder/skia_output_surface_impl.h
index 08ff06ef..6f9b2c27 100644
--- a/components/viz/service/display_embedder/skia_output_surface_impl.h
+++ b/components/viz/service/display_embedder/skia_output_surface_impl.h
@@ -9,13 +9,10 @@
 #include <vector>
 
 #include "base/macros.h"
-#include "base/observer_list.h"
 #include "base/optional.h"
-#include "base/threading/thread_checker.h"
 #include "components/viz/common/display/renderer_settings.h"
-#include "components/viz/common/display/update_vsync_parameters_callback.h"
 #include "components/viz/common/resources/resource_id.h"
-#include "components/viz/service/display/skia_output_surface.h"
+#include "components/viz/service/display_embedder/skia_output_surface_base.h"
 #include "components/viz/service/viz_service_export.h"
 #include "gpu/command_buffer/common/sync_token.h"
 #include "gpu/ipc/common/surface_handle.h"
@@ -30,7 +27,6 @@
 
 namespace viz {
 
-struct ImageContext;
 class GpuServiceImpl;
 class SkiaOutputSurfaceImplOnGpu;
 
@@ -44,7 +40,7 @@
 // render into. In SwapBuffers, it detaches a SkDeferredDisplayList from the
 // recorder and plays it back on the framebuffer SkSurface on the GPU thread
 // through SkiaOutputSurfaceImpleOnGpu.
-class VIZ_SERVICE_EXPORT SkiaOutputSurfaceImpl : public SkiaOutputSurface {
+class VIZ_SERVICE_EXPORT SkiaOutputSurfaceImpl : public SkiaOutputSurfaceBase {
  public:
   SkiaOutputSurfaceImpl(GpuServiceImpl* gpu_service,
                         gpu::SurfaceHandle surface_handle,
@@ -55,24 +51,11 @@
   void BindToClient(OutputSurfaceClient* client) override;
   void EnsureBackbuffer() override;
   void DiscardBackbuffer() override;
-  void BindFramebuffer() override;
-  void SetDrawRectangle(const gfx::Rect& draw_rectangle) override;
   void Reshape(const gfx::Size& size,
                float device_scale_factor,
                const gfx::ColorSpace& color_space,
                bool has_alpha,
                bool use_stencil) override;
-  void SwapBuffers(OutputSurfaceFrame frame) override;
-  uint32_t GetFramebufferCopyTextureFormat() override;
-  OverlayCandidateValidator* GetOverlayCandidateValidator() const override;
-  bool IsDisplayedAsOverlayPlane() const override;
-  unsigned GetOverlayTextureId() const override;
-  gfx::BufferFormat GetOverlayBufferFormat() const override;
-  bool HasExternalStencilTest() const override;
-  void ApplyExternalStencil() override;
-  unsigned UpdateGpuFence() override;
-  void SetNeedsSwapSizeNotifications(
-      bool needs_swap_size_notifications) override;
   void SetUpdateVSyncParametersCallback(
       UpdateVSyncParametersCallback callback) override;
 
@@ -103,8 +86,6 @@
                   const copy_output::RenderPassGeometry& geometry,
                   const gfx::ColorSpace& color_space,
                   std::unique_ptr<CopyOutputRequest> request) override;
-  void AddContextLostObserver(ContextLostObserver* observer) override;
-  void RemoveContextLostObserver(ContextLostObserver* observer) override;
 
   // ExternalUseClient implementation:
   void ReleaseCachedResources(const std::vector<ResourceId>& ids) override;
@@ -124,20 +105,16 @@
   void DidSwapBuffersComplete(gpu::SwapBuffersCompleteParams params,
                               const gfx::Size& pixel_size);
   void BufferPresented(const gfx::PresentationFeedback& feedback);
-  void ContextLost();
   void ScheduleGpuTask(base::OnceClosure callback,
                        std::vector<gpu::SyncToken> sync_tokens);
   GrBackendFormat GetGrBackendFormatForTexture(ResourceFormat resource_format,
                                                uint32_t gl_texture_target);
 
   uint64_t sync_fence_release_ = 0;
-
   GpuServiceImpl* const gpu_service_;
-
   const bool is_using_vulkan_;
   const gpu::SurfaceHandle surface_handle_;
   UpdateVSyncParametersCallback update_vsync_parameters_callback_;
-  OutputSurfaceClient* client_ = nullptr;
 
   std::unique_ptr<base::WaitableEvent> initialize_waitable_event_;
   SkSurfaceCharacterization characterization_;
@@ -157,17 +134,10 @@
   // |nway_canvas_| contains |overdraw_canvas_| and root canvas.
   base::Optional<SkNWayCanvas> nway_canvas_;
 
-  // The cached for promise images indexed by resource id.
-  base::flat_map<ResourceId, std::unique_ptr<ImageContext>>
-      promise_image_cache_;
-
   // The cache for promise image created from render passes.
   base::flat_map<RenderPassId, std::unique_ptr<ImageContext>>
       render_pass_image_cache_;
 
-  // Image contexts which are used for the current frame or render pass.
-  std::vector<ImageContext*> images_in_current_paint_;
-
   // Sync tokens for resources which are used for the current frame or render
   // pass.
   std::vector<gpu::SyncToken> resource_sync_tokens_;
@@ -180,14 +150,6 @@
   // |impl_on_gpu| is created and destroyed on the GPU thread.
   std::unique_ptr<SkiaOutputSurfaceImplOnGpu> impl_on_gpu_;
 
-  // Whether to send OutputSurfaceClient::DidSwapWithSize notifications.
-  bool needs_swap_size_notifications_ = false;
-
-  // Observers for context lost.
-  base::ObserverList<ContextLostObserver>::Unchecked observers_;
-
-  THREAD_CHECKER(thread_checker_);
-
   base::WeakPtr<SkiaOutputSurfaceImpl> weak_ptr_;
   base::WeakPtrFactory<SkiaOutputSurfaceImpl> weak_ptr_factory_;
 
diff --git a/components/viz/service/display_embedder/skia_output_surface_impl_non_ddl.cc b/components/viz/service/display_embedder/skia_output_surface_impl_non_ddl.cc
index e4e197c..32c62d39 100644
--- a/components/viz/service/display_embedder/skia_output_surface_impl_non_ddl.cc
+++ b/components/viz/service/display_embedder/skia_output_surface_impl_non_ddl.cc
@@ -13,7 +13,6 @@
 #include "base/synchronization/waitable_event.h"
 #include "components/viz/common/frame_sinks/begin_frame_source.h"
 #include "components/viz/common/frame_sinks/copy_output_request.h"
-#include "components/viz/common/gpu/context_lost_observer.h"
 #include "components/viz/common/gpu/vulkan_context_provider.h"
 #include "components/viz/common/resources/resource_format_utils.h"
 #include "components/viz/service/display/output_surface_client.h"
@@ -30,7 +29,6 @@
 #include "gpu/command_buffer/service/texture_base.h"
 #include "gpu/vulkan/buildflags.h"
 #include "third_party/skia/include/core/SkPromiseImageTexture.h"
-#include "third_party/skia/include/core/SkYUVAIndex.h"
 #include "third_party/skia/include/gpu/GrBackendSemaphore.h"
 #include "ui/gfx/skia_util.h"
 #include "ui/gl/color_space_utils.h"
@@ -93,13 +91,6 @@
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
 }
 
-void SkiaOutputSurfaceImplNonDDL::BindToClient(OutputSurfaceClient* client) {
-  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
-  DCHECK(client);
-  DCHECK(!client_);
-  client_ = client;
-}
-
 void SkiaOutputSurfaceImplNonDDL::EnsureBackbuffer() {
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
   NOTIMPLEMENTED();
@@ -110,16 +101,6 @@
   NOTIMPLEMENTED();
 }
 
-void SkiaOutputSurfaceImplNonDDL::BindFramebuffer() {
-  // TODO(penghuang): remove this method when GLRenderer is removed.
-}
-
-void SkiaOutputSurfaceImplNonDDL::SetDrawRectangle(
-    const gfx::Rect& draw_rectangle) {
-  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
-  NOTIMPLEMENTED();
-}
-
 void SkiaOutputSurfaceImplNonDDL::Reshape(const gfx::Size& size,
                                           float device_scale_factor,
                                           const gfx::ColorSpace& color_space,
@@ -161,58 +142,6 @@
   }
 }
 
-void SkiaOutputSurfaceImplNonDDL::SwapBuffers(OutputSurfaceFrame frame) {
-  NOTIMPLEMENTED();
-}
-
-uint32_t SkiaOutputSurfaceImplNonDDL::GetFramebufferCopyTextureFormat() {
-  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
-  return GL_RGB;
-}
-
-OverlayCandidateValidator*
-SkiaOutputSurfaceImplNonDDL::GetOverlayCandidateValidator() const {
-  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
-  return nullptr;
-}
-
-bool SkiaOutputSurfaceImplNonDDL::IsDisplayedAsOverlayPlane() const {
-  return false;
-}
-
-unsigned SkiaOutputSurfaceImplNonDDL::GetOverlayTextureId() const {
-  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
-  return 0;
-}
-
-gfx::BufferFormat SkiaOutputSurfaceImplNonDDL::GetOverlayBufferFormat() const {
-  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
-  return gfx::BufferFormat::RGBX_8888;
-}
-
-bool SkiaOutputSurfaceImplNonDDL::HasExternalStencilTest() const {
-  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
-  return false;
-}
-
-void SkiaOutputSurfaceImplNonDDL::ApplyExternalStencil() {
-  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
-}
-
-unsigned SkiaOutputSurfaceImplNonDDL::UpdateGpuFence() {
-  return 0;
-}
-
-void SkiaOutputSurfaceImplNonDDL::SetNeedsSwapSizeNotifications(
-    bool needs_swap_size_notifications) {
-  NOTIMPLEMENTED();
-}
-
-void SkiaOutputSurfaceImplNonDDL::SetUpdateVSyncParametersCallback(
-    UpdateVSyncParametersCallback callback) {
-  NOTIMPLEMENTED();
-}
-
 SkCanvas* SkiaOutputSurfaceImplNonDDL::BeginPaintCurrentFrame() {
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
   DCHECK_EQ(current_render_pass_id_, 0u);
@@ -292,56 +221,14 @@
   DCHECK((has_alpha && (metadatas.size() == 3 || metadatas.size() == 4)) ||
          (!has_alpha && (metadatas.size() == 2 || metadatas.size() == 3)));
 
-  bool is_i420 = has_alpha ? metadatas.size() == 4 : metadatas.size() == 3;
+  SkYUVAIndex indices[4] = {};
+  PrepareYUVATextureIndices(metadatas, has_alpha, indices);
 
-  GrBackendFormat formats[4];
-  SkYUVAIndex indices[4] = {
-      {-1, SkColorChannel::kR},
-      {-1, SkColorChannel::kR},
-      {-1, SkColorChannel::kR},
-      {-1, SkColorChannel::kR},
-  };
   GrBackendTexture yuva_textures[4] = {};
-  const auto process_planar = [&](size_t i, ResourceFormat resource_format) {
-    auto metadata = metadatas[i];
-    metadata.resource_format = resource_format;
+  for (size_t i = 0; i < metadatas.size(); ++i) {
+    const auto& metadata = metadatas[i];
     if (!GetGrBackendTexture(metadata, &yuva_textures[i]))
       DLOG(ERROR) << "Failed to GetGrBackendTexture from a mailbox.";
-  };
-
-  if (is_i420) {
-    process_planar(0, RED_8);
-    indices[SkYUVAIndex::kY_Index].fIndex = 0;
-    indices[SkYUVAIndex::kY_Index].fChannel = SkColorChannel::kR;
-
-    process_planar(1, RED_8);
-    indices[SkYUVAIndex::kU_Index].fIndex = 1;
-    indices[SkYUVAIndex::kU_Index].fChannel = SkColorChannel::kR;
-
-    process_planar(2, RED_8);
-    indices[SkYUVAIndex::kV_Index].fIndex = 2;
-    indices[SkYUVAIndex::kV_Index].fChannel = SkColorChannel::kR;
-    if (has_alpha) {
-      process_planar(3, RED_8);
-      indices[SkYUVAIndex::kA_Index].fIndex = 3;
-      indices[SkYUVAIndex::kA_Index].fChannel = SkColorChannel::kR;
-    }
-  } else {
-    process_planar(0, RED_8);
-    indices[SkYUVAIndex::kY_Index].fIndex = 0;
-    indices[SkYUVAIndex::kY_Index].fChannel = SkColorChannel::kR;
-
-    process_planar(1, RG_88);
-    indices[SkYUVAIndex::kU_Index].fIndex = 1;
-    indices[SkYUVAIndex::kU_Index].fChannel = SkColorChannel::kR;
-
-    indices[SkYUVAIndex::kV_Index].fIndex = 1;
-    indices[SkYUVAIndex::kV_Index].fChannel = SkColorChannel::kG;
-    if (has_alpha) {
-      process_planar(2, RED_8);
-      indices[SkYUVAIndex::kA_Index].fIndex = 2;
-      indices[SkYUVAIndex::kA_Index].fChannel = SkColorChannel::kR;
-    }
   }
 
   return SkImage::MakeFromYUVATextures(
@@ -492,16 +379,6 @@
   NOTIMPLEMENTED();
 }
 
-void SkiaOutputSurfaceImplNonDDL::AddContextLostObserver(
-    ContextLostObserver* observer) {
-  observers_.AddObserver(observer);
-}
-
-void SkiaOutputSurfaceImplNonDDL::RemoveContextLostObserver(
-    ContextLostObserver* observer) {
-  observers_.RemoveObserver(observer);
-}
-
 bool SkiaOutputSurfaceImplNonDDL::WaitSyncToken(
     const gpu::SyncToken& sync_token) {
   base::WaitableEvent event;
@@ -615,12 +492,6 @@
   client_->DidReceivePresentationFeedback(feedback);
 }
 
-void SkiaOutputSurfaceImplNonDDL::ContextLost() {
-  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
-  for (auto& observer : observers_)
-    observer.OnContextLost();
-}
-
 void SkiaOutputSurfaceImplNonDDL::WaitSemaphores(
     std::vector<GrBackendSemaphore> semaphores) {
   if (semaphores.empty())
diff --git a/components/viz/service/display_embedder/skia_output_surface_impl_non_ddl.h b/components/viz/service/display_embedder/skia_output_surface_impl_non_ddl.h
index e0511bc..763fb22 100644
--- a/components/viz/service/display_embedder/skia_output_surface_impl_non_ddl.h
+++ b/components/viz/service/display_embedder/skia_output_surface_impl_non_ddl.h
@@ -11,10 +11,8 @@
 #include "base/containers/flat_map.h"
 #include "base/macros.h"
 #include "base/memory/weak_ptr.h"
-#include "base/observer_list.h"
 #include "base/optional.h"
-#include "base/threading/thread_checker.h"
-#include "components/viz/service/display/skia_output_surface.h"
+#include "components/viz/service/display_embedder/skia_output_surface_base.h"
 #include "components/viz/service/viz_service_export.h"
 #include "gpu/command_buffer/common/sync_token.h"
 #include "gpu/command_buffer/service/shared_context_state.h"
@@ -43,14 +41,12 @@
 
 namespace viz {
 
-struct ImageContext;
-
 // A SkiaOutputSurface implementation for running SkiaRenderer on GpuThread.
 // Comparing to SkiaOutputSurfaceImpl, it will issue skia draw operations
 // against OS graphics API (GL, Vulkan, etc) instead of recording deferred
 // display list first.
 class VIZ_SERVICE_EXPORT SkiaOutputSurfaceImplNonDDL
-    : public SkiaOutputSurface {
+    : public SkiaOutputSurfaceBase {
  public:
   SkiaOutputSurfaceImplNonDDL(
       scoped_refptr<gl::GLSurface> gl_surface,
@@ -62,29 +58,13 @@
   ~SkiaOutputSurfaceImplNonDDL() override;
 
   // OutputSurface implementation:
-  void BindToClient(OutputSurfaceClient* client) override;
   void EnsureBackbuffer() override;
   void DiscardBackbuffer() override;
-  void BindFramebuffer() override;
-  void SetDrawRectangle(const gfx::Rect& draw_rectangle) override;
   void Reshape(const gfx::Size& size,
                float device_scale_factor,
                const gfx::ColorSpace& color_space,
                bool has_alpha,
                bool use_stencil) override;
-  void SwapBuffers(OutputSurfaceFrame frame) override;
-  uint32_t GetFramebufferCopyTextureFormat() override;
-  OverlayCandidateValidator* GetOverlayCandidateValidator() const override;
-  bool IsDisplayedAsOverlayPlane() const override;
-  unsigned GetOverlayTextureId() const override;
-  gfx::BufferFormat GetOverlayBufferFormat() const override;
-  bool HasExternalStencilTest() const override;
-  void ApplyExternalStencil() override;
-  unsigned UpdateGpuFence() override;
-  void SetNeedsSwapSizeNotifications(
-      bool needs_swap_size_notifications) override;
-  void SetUpdateVSyncParametersCallback(
-      UpdateVSyncParametersCallback callback) override;
 
   // SkiaOutputSurface implementation:
   SkCanvas* BeginPaintCurrentFrame() override;
@@ -112,8 +92,6 @@
                   const copy_output::RenderPassGeometry& geometry,
                   const gfx::ColorSpace& color_space,
                   std::unique_ptr<CopyOutputRequest> request) override;
-  void AddContextLostObserver(ContextLostObserver* observer) override;
-  void RemoveContextLostObserver(ContextLostObserver* observer) override;
 
   // ExternalUseClient implementation:
   void ReleaseCachedResources(const std::vector<ResourceId>& ids) override;
@@ -140,7 +118,6 @@
                            GrBackendTexture* backend_texture);
   void FinishPaint(uint64_t sync_fence_release);
   void BufferPresented(const gfx::PresentationFeedback& feedback);
-  void ContextLost();
   void WaitSemaphores(std::vector<GrBackendSemaphore> semaphores);
 
   uint64_t sync_fence_release_ = 0;
@@ -155,8 +132,6 @@
   const bool need_swapbuffers_ack_;
   base::Optional<ScopedGpuTask> scoped_gpu_task_;
 
-  OutputSurfaceClient* client_ = nullptr;
-
   unsigned int backing_framebuffer_object_ = 0;
   gfx::Size reshape_surface_size_;
   float reshape_device_scale_factor_ = 0.f;
@@ -167,9 +142,6 @@
   // The current render pass id set by BeginPaintRenderPass.
   RenderPassId current_render_pass_id_ = 0;
 
-  // Observers for context lost.
-  base::ObserverList<ContextLostObserver>::Unchecked observers_;
-
   // The SkSurface for the framebuffer.
   sk_sp<SkSurface> sk_surface_;
 
@@ -183,18 +155,9 @@
   // Offscreen SkSurfaces for render passes.
   base::flat_map<RenderPassId, sk_sp<SkSurface>> offscreen_sk_surfaces_;
 
-  // Cached promise image.
-  base::flat_map<ResourceId, std::unique_ptr<ImageContext>>
-      promise_image_cache_;
-
-  // Images for current frame or render pass.
-  std::vector<ImageContext*> images_in_current_paint_;
-
   // Semaphores which need to be signalled for the current paint.
   std::vector<GrBackendSemaphore> pending_semaphores_;
 
-  THREAD_CHECKER(thread_checker_);
-
   base::WeakPtrFactory<SkiaOutputSurfaceImplNonDDL> weak_ptr_factory_;
 
   DISALLOW_COPY_AND_ASSIGN(SkiaOutputSurfaceImplNonDDL);
diff --git a/content/browser/renderer_host/render_widget_host_view_base.cc b/content/browser/renderer_host/render_widget_host_view_base.cc
index 6986a9b..534eb18 100644
--- a/content/browser/renderer_host/render_widget_host_view_base.cc
+++ b/content/browser/renderer_host/render_widget_host_view_base.cc
@@ -908,12 +908,6 @@
 
 #endif
 
-#if defined(OS_MACOSX)
-bool RenderWidgetHostViewBase::ShouldContinueToPauseForFrame() {
-  return false;
-}
-#endif
-
 void RenderWidgetHostViewBase::DidNavigate() {
   if (host())
     host()->SynchronizeVisualProperties();
diff --git a/content/browser/renderer_host/render_widget_host_view_base.h b/content/browser/renderer_host/render_widget_host_view_base.h
index e8dadd6..ce353fb 100644
--- a/content/browser/renderer_host/render_widget_host_view_base.h
+++ b/content/browser/renderer_host/render_widget_host_view_base.h
@@ -590,12 +590,6 @@
   void OnChildFrameDestroyed(int routing_id);
 #endif
 
-#if defined(OS_MACOSX)
-  // Use only for resize on macOS. Returns true if there is not currently a
-  // frame of the view's size being displayed.
-  virtual bool ShouldContinueToPauseForFrame();
-#endif
-
   virtual void DidNavigate();
 
   // Called when the RenderWidgetHostImpl has be initialized.
diff --git a/content/browser/service_worker/service_worker_clients_api_browsertest.cc b/content/browser/service_worker/service_worker_clients_api_browsertest.cc
index e24d7bd..1a4425a 100644
--- a/content/browser/service_worker/service_worker_clients_api_browsertest.cc
+++ b/content/browser/service_worker/service_worker_clients_api_browsertest.cc
@@ -94,12 +94,11 @@
     return allow_open_url_;
   }
 
-  void OpenURL(
-      SiteInstance* site_instance,
-      const OpenURLParams& params,
-      const base::RepeatingCallback<void(WebContents*)>& callback) override {
+  void OpenURL(SiteInstance* site_instance,
+               const OpenURLParams& params,
+               base::OnceCallback<void(WebContents*)> callback) override {
     opened_url_ = params.url;
-    callback.Run(nullptr);
+    std::move(callback).Run(nullptr);
     if (opened_url_callback_)
       std::move(opened_url_callback_).Run();
   }
diff --git a/content/browser/web_contents/web_contents_view_aura.cc b/content/browser/web_contents/web_contents_view_aura.cc
index 2ff912c1..73f03a6 100644
--- a/content/browser/web_contents/web_contents_view_aura.cc
+++ b/content/browser/web_contents/web_contents_view_aura.cc
@@ -1370,7 +1370,19 @@
 
   const int key_modifiers = ui::EventFlagsToWebEventModifiers(event.flags());
 #if defined(OS_WIN)
-  if (event.data().HasVirtualFilenames()) {
+  // As with real files, only add virtual files if the drag did not originate in
+  // the renderer process. Without this, if an anchor element is dragged and
+  // then dropped on the same page, the browser will navigate to the URL
+  // referenced by the anchor. That is because virtual ".url" file data
+  // (internet shortcut) is added to the data object on drag start, and if
+  // script doesn't handle the drop, the browser behaves just as if a .url file
+  // were dragged in from the desktop. Filtering out virtual files if the drag
+  // is renderer tainted also prevents the possibility of a compromised renderer
+  // gaining access to the backing temp file paths.
+  // TODO(https://crbug.com/958273): DragDrop: Extend virtual filename support
+  // to DropData, for parity with real filename support.
+  if (!current_drop_data_->did_originate_from_renderer &&
+      event.data().HasVirtualFilenames()) {
     // Asynchronously retrieve the actual content of any virtual files now (this
     // step is not needed for "real" files already on the file system, e.g.
     // those dropped on Chromium from the desktop). When all content has been
diff --git a/content/browser/web_contents/web_contents_view_aura.h b/content/browser/web_contents/web_contents_view_aura.h
index 22d8ee98..1c20db5 100644
--- a/content/browser/web_contents/web_contents_view_aura.h
+++ b/content/browser/web_contents/web_contents_view_aura.h
@@ -67,7 +67,11 @@
   friend class WebContentsViewAuraTest;
   FRIEND_TEST_ALL_PREFIXES(WebContentsViewAuraTest, EnableDisableOverscroll);
   FRIEND_TEST_ALL_PREFIXES(WebContentsViewAuraTest, DragDropFiles);
+  FRIEND_TEST_ALL_PREFIXES(WebContentsViewAuraTest,
+                           DragDropFilesOriginateFromRenderer);
   FRIEND_TEST_ALL_PREFIXES(WebContentsViewAuraTest, DragDropVirtualFiles);
+  FRIEND_TEST_ALL_PREFIXES(WebContentsViewAuraTest,
+                           DragDropVirtualFilesOriginateFromRenderer);
 
   class WindowObserver;
 
diff --git a/content/browser/web_contents/web_contents_view_aura_unittest.cc b/content/browser/web_contents/web_contents_view_aura_unittest.cc
index fb8f274..43a74ec 100644
--- a/content/browser/web_contents/web_contents_view_aura_unittest.cc
+++ b/content/browser/web_contents/web_contents_view_aura_unittest.cc
@@ -77,18 +77,18 @@
     RenderViewHostTestHarness::TearDown();
   }
 
-  WebContentsViewAura* view() {
+  WebContentsViewAura* GetView() {
     WebContentsImpl* contents = static_cast<WebContentsImpl*>(web_contents());
     return static_cast<WebContentsViewAura*>(contents->GetView());
   }
 
   aura::Window* GetNativeView() { return web_contents()->GetNativeView(); }
 
-  void CheckDropData(WebContentsViewAura* wcva) const {
-    EXPECT_EQ(nullptr, wcva->current_drop_data_);
+  void CheckDropData(WebContentsViewAura* view) const {
+    EXPECT_EQ(nullptr, view->current_drop_data_);
     ASSERT_NE(nullptr, drop_complete_data_);
     EXPECT_TRUE(drop_complete_data_->drop_allowed);
-    EXPECT_EQ(wcva->current_rwh_for_drag_.get(),
+    EXPECT_EQ(view->current_rwh_for_drag_.get(),
               drop_complete_data_->target_rwh.get());
     EXPECT_EQ(kClientPt, drop_complete_data_->client_pt);
     // Screen point of event is ignored, instead cursor position used.
@@ -130,11 +130,11 @@
 };
 
 TEST_F(WebContentsViewAuraTest, EnableDisableOverscroll) {
-  WebContentsViewAura* wcva = view();
-  wcva->SetOverscrollControllerEnabled(false);
-  EXPECT_FALSE(wcva->gesture_nav_simple_);
-  wcva->SetOverscrollControllerEnabled(true);
-  EXPECT_TRUE(wcva->gesture_nav_simple_);
+  WebContentsViewAura* view = GetView();
+  view->SetOverscrollControllerEnabled(false);
+  EXPECT_FALSE(view->gesture_nav_simple_);
+  view->SetOverscrollControllerEnabled(true);
+  EXPECT_TRUE(view->gesture_nav_simple_);
 }
 
 TEST_F(WebContentsViewAuraTest, ShowHideParent) {
@@ -157,7 +157,7 @@
 }
 
 TEST_F(WebContentsViewAuraTest, DragDropFiles) {
-  WebContentsViewAura* wcva = view();
+  WebContentsViewAura* view = GetView();
   ui::OSExchangeData data;
 
   const base::string16 string_data = base::ASCIIToUTF16("Some string data");
@@ -187,20 +187,20 @@
                             ui::DragDropTypes::DRAG_COPY);
 
   // Simulate drag enter.
-  EXPECT_EQ(nullptr, wcva->current_drop_data_);
-  wcva->OnDragEntered(event);
-  ASSERT_NE(nullptr, wcva->current_drop_data_);
+  EXPECT_EQ(nullptr, view->current_drop_data_);
+  view->OnDragEntered(event);
+  ASSERT_NE(nullptr, view->current_drop_data_);
 
 #if defined(USE_X11)
   // By design, OSExchangeDataProviderAuraX11::GetString returns an empty string
   // if file data is also present.
-  EXPECT_TRUE(wcva->current_drop_data_->text.string().empty());
+  EXPECT_TRUE(view->current_drop_data_->text.string().empty());
 #else
-  EXPECT_EQ(string_data, wcva->current_drop_data_->text.string());
+  EXPECT_EQ(string_data, view->current_drop_data_->text.string());
 #endif
 
   std::vector<ui::FileInfo> retrieved_file_infos =
-      wcva->current_drop_data_->filenames;
+      view->current_drop_data_->filenames;
   ASSERT_EQ(test_file_infos.size(), retrieved_file_infos.size());
   for (size_t i = 0; i < retrieved_file_infos.size(); i++) {
     EXPECT_EQ(test_file_infos[i].path, retrieved_file_infos[i].path);
@@ -211,15 +211,15 @@
   // Simulate drop.
   auto callback = base::BindOnce(&WebContentsViewAuraTest::OnDropComplete,
                                  base::Unretained(this));
-  wcva->RegisterDropCallbackForTesting(std::move(callback));
+  view->RegisterDropCallbackForTesting(std::move(callback));
 
   base::RunLoop run_loop;
   async_drop_closure_ = run_loop.QuitClosure();
 
-  wcva->OnPerformDrop(event);
+  view->OnPerformDrop(event);
   run_loop.Run();
 
-  CheckDropData(wcva);
+  CheckDropData(view);
 
 #if defined(USE_X11)
   // By design, OSExchangeDataProviderAuraX11::GetString returns an empty string
@@ -238,9 +238,84 @@
   }
 }
 
+#if defined(OS_WIN) || defined(USE_X11)
+TEST_F(WebContentsViewAuraTest, DragDropFilesOriginateFromRenderer) {
+  WebContentsViewAura* view = GetView();
+  ui::OSExchangeData data;
+
+  const base::string16 string_data = base::ASCIIToUTF16("Some string data");
+  data.SetString(string_data);
+
+#if defined(OS_WIN)
+  const std::vector<ui::FileInfo> test_file_infos = {
+      {base::FilePath(FILE_PATH_LITERAL("C:\\tmp\\test_file1")),
+       base::FilePath()},
+      {base::FilePath(FILE_PATH_LITERAL("C:\\tmp\\test_file2")),
+       base::FilePath()},
+      {
+          base::FilePath(FILE_PATH_LITERAL("C:\\tmp\\test_file3")),
+          base::FilePath(),
+      },
+  };
+#else
+  const std::vector<ui::FileInfo> test_file_infos = {
+      {base::FilePath(FILE_PATH_LITERAL("/tmp/test_file1")), base::FilePath()},
+      {base::FilePath(FILE_PATH_LITERAL("/tmp/test_file2")), base::FilePath()},
+      {base::FilePath(FILE_PATH_LITERAL("/tmp/test_file3")), base::FilePath()},
+  };
+#endif
+  data.SetFilenames(test_file_infos);
+
+  // Simulate the drag originating in the renderer process, in which case
+  // any file data should be filtered out (anchor drag scenario).
+  data.MarkOriginatedFromRenderer();
+
+  ui::DropTargetEvent event(data, kClientPt, kScreenPt,
+                            ui::DragDropTypes::DRAG_COPY);
+
+  // Simulate drag enter.
+  EXPECT_EQ(nullptr, view->current_drop_data_);
+  view->OnDragEntered(event);
+  ASSERT_NE(nullptr, view->current_drop_data_);
+
+#if defined(USE_X11)
+  // By design, OSExchangeDataProviderAuraX11::GetString returns an empty string
+  // if file data is also present.
+  EXPECT_TRUE(view->current_drop_data_->text.string().empty());
+#else
+  EXPECT_EQ(string_data, view->current_drop_data_->text.string());
+#endif
+
+  ASSERT_TRUE(view->current_drop_data_->filenames.empty());
+
+  // Simulate drop.
+  auto callback = base::BindOnce(&WebContentsViewAuraTest::OnDropComplete,
+                                 base::Unretained(this));
+  view->RegisterDropCallbackForTesting(std::move(callback));
+
+  base::RunLoop run_loop;
+  async_drop_closure_ = run_loop.QuitClosure();
+
+  view->OnPerformDrop(event);
+  run_loop.Run();
+
+  CheckDropData(view);
+
+#if defined(USE_X11)
+  // By design, OSExchangeDataProviderAuraX11::GetString returns an empty string
+  // if file data is also present.
+  EXPECT_TRUE(drop_complete_data_->drop_data.text.string().empty());
+#else
+  EXPECT_EQ(string_data, drop_complete_data_->drop_data.text.string());
+#endif
+
+  ASSERT_TRUE(drop_complete_data_->drop_data.filenames.empty());
+}
+#endif
+
 #if defined(OS_WIN)
 TEST_F(WebContentsViewAuraTest, DragDropVirtualFiles) {
-  WebContentsViewAura* wcva = view();
+  WebContentsViewAura* view = GetView();
   ui::OSExchangeData data;
 
   const base::string16 string_data = base::ASCIIToUTF16("Some string data");
@@ -263,15 +338,15 @@
                             ui::DragDropTypes::DRAG_COPY);
 
   // Simulate drag enter.
-  EXPECT_EQ(nullptr, wcva->current_drop_data_);
-  wcva->OnDragEntered(event);
-  ASSERT_NE(nullptr, wcva->current_drop_data_);
+  EXPECT_EQ(nullptr, view->current_drop_data_);
+  view->OnDragEntered(event);
+  ASSERT_NE(nullptr, view->current_drop_data_);
 
-  EXPECT_EQ(string_data, wcva->current_drop_data_->text.string());
+  EXPECT_EQ(string_data, view->current_drop_data_->text.string());
 
   const base::FilePath path_placeholder(FILE_PATH_LITERAL("temp.tmp"));
   std::vector<ui::FileInfo> retrieved_file_infos =
-      wcva->current_drop_data_->filenames;
+      view->current_drop_data_->filenames;
   ASSERT_EQ(test_filenames_and_contents.size(), retrieved_file_infos.size());
   for (size_t i = 0; i < retrieved_file_infos.size(); i++) {
     EXPECT_EQ(test_filenames_and_contents[i].first,
@@ -283,15 +358,15 @@
   // present).
   auto callback = base::BindOnce(&WebContentsViewAuraTest::OnDropComplete,
                                  base::Unretained(this));
-  wcva->RegisterDropCallbackForTesting(std::move(callback));
+  view->RegisterDropCallbackForTesting(std::move(callback));
 
   base::RunLoop run_loop;
   async_drop_closure_ = run_loop.QuitClosure();
 
-  wcva->OnPerformDrop(event);
+  view->OnPerformDrop(event);
   run_loop.Run();
 
-  CheckDropData(wcva);
+  CheckDropData(view);
 
   EXPECT_EQ(string_data, drop_complete_data_->drop_data.text.string());
 
@@ -312,6 +387,61 @@
     EXPECT_EQ(test_filenames_and_contents[i].second, read_contents);
   }
 }
+
+TEST_F(WebContentsViewAuraTest, DragDropVirtualFilesOriginateFromRenderer) {
+  WebContentsViewAura* view = GetView();
+  ui::OSExchangeData data;
+
+  const base::string16 string_data = base::ASCIIToUTF16("Some string data");
+  data.SetString(string_data);
+
+  const std::vector<std::pair<base::FilePath, std::string>>
+      test_filenames_and_contents = {
+          {base::FilePath(FILE_PATH_LITERAL("filename.txt")),
+           std::string("just some data")},
+          {base::FilePath(FILE_PATH_LITERAL("another filename.txt")),
+           std::string("just some data\0with\0nulls", 25)},
+          {base::FilePath(FILE_PATH_LITERAL("and another filename.txt")),
+           std::string("just some more data")},
+      };
+
+  data.provider().SetVirtualFileContentsForTesting(test_filenames_and_contents,
+                                                   TYMED_ISTREAM);
+
+  // Simulate the drag originating in the renderer process, in which case
+  // any file data should be filtered out (anchor drag scenario).
+  data.MarkOriginatedFromRenderer();
+
+  ui::DropTargetEvent event(data, kClientPt, kScreenPt,
+                            ui::DragDropTypes::DRAG_COPY);
+
+  // Simulate drag enter.
+  EXPECT_EQ(nullptr, view->current_drop_data_);
+  view->OnDragEntered(event);
+  ASSERT_NE(nullptr, view->current_drop_data_);
+
+  EXPECT_EQ(string_data, view->current_drop_data_->text.string());
+
+  ASSERT_TRUE(view->current_drop_data_->filenames.empty());
+
+  // Simulate drop (completes asynchronously since virtual file data is
+  // present).
+  auto callback = base::BindOnce(&WebContentsViewAuraTest::OnDropComplete,
+                                 base::Unretained(this));
+  view->RegisterDropCallbackForTesting(std::move(callback));
+
+  base::RunLoop run_loop;
+  async_drop_closure_ = run_loop.QuitClosure();
+
+  view->OnPerformDrop(event);
+  run_loop.Run();
+
+  CheckDropData(view);
+
+  EXPECT_EQ(string_data, drop_complete_data_->drop_data.text.string());
+
+  ASSERT_TRUE(drop_complete_data_->drop_data.filenames.empty());
+}
 #endif
 
 }  // namespace content
diff --git a/content/browser/webauth/authenticator_common.cc b/content/browser/webauth/authenticator_common.cc
index c9d549dc..038739b 100644
--- a/content/browser/webauth/authenticator_common.cc
+++ b/content/browser/webauth/authenticator_common.cc
@@ -639,8 +639,10 @@
     return;
   }
 
-  if (options->authenticator_selection &&
-      options->authenticator_selection->require_resident_key &&
+  const bool resident_key =
+      options->authenticator_selection &&
+      options->authenticator_selection->require_resident_key;
+  if (resident_key &&
       (!base::FeatureList::IsEnabled(device::kWebAuthResidentKeys) ||
        !request_delegate_->SupportsResidentKeys())) {
     // Disallow the creation of resident credentials.
@@ -650,6 +652,43 @@
     return;
   }
 
+  auto authenticator_selection_criteria =
+      options->authenticator_selection
+          ? mojo::ConvertTo<device::AuthenticatorSelectionCriteria>(
+                options->authenticator_selection)
+          : device::AuthenticatorSelectionCriteria();
+
+  // Reject any non-sensical credProtect extension values.
+  if (  // Can't require the default policy (or no policy).
+      (options->enforce_protection_policy &&
+       (options->protection_policy ==
+            blink::mojom::ProtectionPolicy::UNSPECIFIED ||
+        options->protection_policy == blink::mojom::ProtectionPolicy::NONE)) ||
+      // For non-resident keys the only protection that makes sense is
+      // UV_REQUIRED (or UNSPECIFIED).
+      (!resident_key &&
+       (options->protection_policy == blink::mojom::ProtectionPolicy::NONE ||
+        options->protection_policy ==
+            blink::mojom::ProtectionPolicy::UV_OR_CRED_ID_REQUIRED)) ||
+      // UV_REQUIRED only makes sense if UV is required overall.
+      (options->protection_policy ==
+           blink::mojom::ProtectionPolicy::UV_REQUIRED &&
+       authenticator_selection_criteria.user_verification_requirement() !=
+           device::UserVerificationRequirement::kRequired)) {
+    InvokeCallbackAndCleanup(
+        std::move(callback),
+        blink::mojom::AuthenticatorStatus::PROTECTION_POLICY_INCONSISTENT);
+    return;
+  }
+
+  if (options->protection_policy ==
+          blink::mojom::ProtectionPolicy::UNSPECIFIED &&
+      resident_key) {
+    // If not specified, UV_OR_CRED_ID_REQUIRED is made the default.
+    options->protection_policy =
+        blink::mojom::ProtectionPolicy::UV_OR_CRED_ID_REQUIRED;
+  }
+
   DCHECK(make_credential_response_callback_.is_null());
   make_credential_response_callback_ = std::move(callback);
 
@@ -690,12 +729,6 @@
                 {device::FidoTransportProtocol::kUsbHumanInterfaceDevice})
           : transports_;
 
-  auto authenticator_selection_criteria =
-      options->authenticator_selection
-          ? mojo::ConvertTo<device::AuthenticatorSelectionCriteria>(
-                options->authenticator_selection)
-          : device::AuthenticatorSelectionCriteria();
-
   auto ctap_request = CreateCtapMakeCredentialRequest(
       client_data_json_, options, browser_context()->IsOffTheRecord());
   // On dual protocol CTAP2/U2F devices, force credential creation over U2F.
@@ -714,6 +747,21 @@
   attestation_requested_ =
       attestation != ::device::AttestationConveyancePreference::NONE;
 
+  switch (options->protection_policy) {
+    case blink::mojom::ProtectionPolicy::UNSPECIFIED:
+    case blink::mojom::ProtectionPolicy::NONE:
+      break;
+    case blink::mojom::ProtectionPolicy::UV_OR_CRED_ID_REQUIRED:
+      ctap_request.cred_protect =
+          std::make_pair(device::CredProtect::kUVOrCredIDRequired,
+                         options->enforce_protection_policy);
+      break;
+    case blink::mojom::ProtectionPolicy::UV_REQUIRED:
+      ctap_request.cred_protect = std::make_pair(
+          device::CredProtect::kUVRequired, options->enforce_protection_policy);
+      break;
+  }
+
   request_ = std::make_unique<device::MakeCredentialRequestHandler>(
       connector_, transports, std::move(ctap_request),
       std::move(authenticator_selection_criteria),
@@ -732,8 +780,7 @@
       base::BindRepeating(
           &device::FidoRequestHandlerBase::InitiatePairingWithDevice,
           request_->GetWeakPtr()) /* ble_pairing_callback */);
-  if (options->authenticator_selection &&
-      options->authenticator_selection->require_resident_key) {
+  if (resident_key) {
     request_delegate_->SetMightCreateResidentCredential(true);
   }
   request_->set_observer(request_delegate_.get());
diff --git a/content/browser/webauth/authenticator_impl_unittest.cc b/content/browser/webauth/authenticator_impl_unittest.cc
index 694278f9..a172033 100644
--- a/content/browser/webauth/authenticator_impl_unittest.cc
+++ b/content/browser/webauth/authenticator_impl_unittest.cc
@@ -3452,6 +3452,8 @@
   }
 }
 
+// TODO(agl): test resident-key storage exhaustion.
+
 TEST_F(ResidentKeyAuthenticatorImplTest, GetAssertionSingle) {
   ASSERT_TRUE(virtual_device_.mutable_state()->InjectResidentKey(
       /*credential_id=*/{{4, 3, 2, 1}}, kTestRelyingPartyId,
@@ -3491,6 +3493,180 @@
   EXPECT_TRUE(HasUV(callback_receiver));
 }
 
-// TODO(agl): test resident-key storage exhaustion.
+static const char* ProtectionPolicyDescription(
+    blink::mojom::ProtectionPolicy p) {
+  switch (p) {
+    case blink::mojom::ProtectionPolicy::UNSPECIFIED:
+      return "UNSPECIFIED";
+    case blink::mojom::ProtectionPolicy::NONE:
+      return "NONE";
+    case blink::mojom::ProtectionPolicy::UV_OR_CRED_ID_REQUIRED:
+      return "UV_OR_CRED_ID_REQUIRED";
+    case blink::mojom::ProtectionPolicy::UV_REQUIRED:
+      return "UV_REQUIRED";
+  }
+}
+
+TEST_F(ResidentKeyAuthenticatorImplTest, CredProtectRegistration) {
+  TestServiceManagerContext smc;
+  AuthenticatorPtr authenticator = ConnectToAuthenticator();
+
+  const auto UNSPECIFIED = blink::mojom::ProtectionPolicy::UNSPECIFIED;
+  const auto NONE = blink::mojom::ProtectionPolicy::NONE;
+  const auto UV_OR_CRED =
+      blink::mojom::ProtectionPolicy::UV_OR_CRED_ID_REQUIRED;
+  const auto UV_REQ = blink::mojom::ProtectionPolicy::UV_REQUIRED;
+  const int kOk = 0;
+  const int kNonsense = 1;
+  const int kNotAllow = 2;
+
+  const struct {
+    bool supported_by_authenticator;
+    bool is_resident;
+    blink::mojom::ProtectionPolicy protection;
+    bool enforce;
+    bool uv;
+    int expected_outcome;
+    blink::mojom::ProtectionPolicy resulting_policy;
+  } kExpectations[] = {
+      // clang-format off
+    // Support | Resdnt | Level      | Enf  |  UV  || Result   | Prot level
+    {  false,   false,   UNSPECIFIED, false, false,   kOk,       NONE},
+    {  false,   false,   UNSPECIFIED, true,  false,   kNonsense, UNSPECIFIED},
+    {  false,   false,   NONE,        false, false,   kNonsense, UNSPECIFIED},
+    {  false,   false,   NONE,        true,  false,   kNonsense, UNSPECIFIED},
+    {  false,   false,   UV_OR_CRED,  false, false,   kNonsense, UNSPECIFIED},
+    {  false,   false,   UV_OR_CRED,  true,  false,   kNonsense, UNSPECIFIED},
+    {  false,   false,   UV_REQ,      false, false,   kNonsense, UNSPECIFIED},
+    {  false,   false,   UV_REQ,      false, true,    kOk,       NONE},
+    {  false,   false,   UV_REQ,      true,  false,   kNonsense, UNSPECIFIED},
+    {  false,   false,   UV_REQ,      true,  true,    kNotAllow, UNSPECIFIED},
+    {  false,   true,    UNSPECIFIED, false, false,   kOk,       NONE},
+    {  false,   true,    UNSPECIFIED, true,  false,   kNonsense, UNSPECIFIED},
+    {  false,   true,    NONE,        false, false,   kOk,       NONE},
+    {  false,   true,    NONE,        true,  false,   kNonsense, UNSPECIFIED},
+    {  false,   true,    UV_OR_CRED,  false, false,   kOk,       NONE},
+    {  false,   true,    UV_OR_CRED,  true,  false,   kNotAllow, UNSPECIFIED},
+    {  false,   true,    UV_REQ,      false, false,   kNonsense, UNSPECIFIED},
+    {  false,   true,    UV_REQ,      false, true,    kOk,       NONE},
+    {  false,   true,    UV_REQ,      true,  false,   kNonsense, UNSPECIFIED},
+    {  false,   true,    UV_REQ,      true,  true,    kNotAllow, UNSPECIFIED},
+
+    // For the case where the authenticator supports credProtect we do not
+    // repeat the cases above that are |kNonsense| on the assumption that
+    // authenticator support is irrelevant. Therefore these are just the non-
+    // kNonsense cases from the prior block.
+    {  true,    false,   UNSPECIFIED, false, false,   kOk,       NONE},
+    {  true,    false,   UV_REQ,      false, true,    kOk,       UV_REQ},
+    {  true,    false,   UV_REQ,      true,  true,    kOk,       UV_REQ},
+    {  true,    true,    UNSPECIFIED, false, false,   kOk,       UV_OR_CRED},
+    {  true,    true,    NONE,        false, false,   kOk,       NONE},
+    {  true,    true,    UV_OR_CRED,  false, false,   kOk,       UV_OR_CRED},
+    {  true,    true,    UV_OR_CRED,  true,  false,   kOk,       UV_OR_CRED},
+    {  true,    true,    UV_REQ,      false, true,    kOk,       UV_REQ},
+    {  true,    true,    UV_REQ,      true,  true,    kOk,       UV_REQ},
+      // clang-format on
+  };
+
+  for (const auto& test : kExpectations) {
+    device::VirtualCtap2Device::Config config;
+    config.pin_support = true;
+    config.resident_key_support = true;
+    config.cred_protect_support = test.supported_by_authenticator;
+    virtual_device_.SetCtap2Config(config);
+    virtual_device_.mutable_state()->registrations.clear();
+
+    SCOPED_TRACE(::testing::Message() << "uv=" << test.uv);
+    SCOPED_TRACE(::testing::Message() << "enforce=" << test.enforce);
+    SCOPED_TRACE(::testing::Message()
+                 << "level=" << ProtectionPolicyDescription(test.protection));
+    SCOPED_TRACE(::testing::Message() << "resident=" << test.is_resident);
+    SCOPED_TRACE(::testing::Message()
+                 << "support=" << test.supported_by_authenticator);
+
+    PublicKeyCredentialCreationOptionsPtr options = make_credential_options();
+    options->authenticator_selection->require_resident_key = test.is_resident;
+    options->protection_policy = test.protection;
+    options->enforce_protection_policy = test.enforce;
+    options->authenticator_selection->user_verification =
+        test.uv ? blink::mojom::UserVerificationRequirement::REQUIRED
+                : blink::mojom::UserVerificationRequirement::DISCOURAGED;
+
+    TestMakeCredentialCallback callback_receiver;
+    authenticator->MakeCredential(std::move(options),
+                                  callback_receiver.callback());
+    callback_receiver.WaitForCallback();
+
+    switch (test.expected_outcome) {
+      case kOk: {
+        EXPECT_EQ(AuthenticatorStatus::SUCCESS, callback_receiver.status());
+        ASSERT_EQ(1u, virtual_device_.mutable_state()->registrations.size());
+        const base::Optional<device::CredProtect> result =
+            virtual_device_.mutable_state()
+                ->registrations.begin()
+                ->second.protection;
+
+        switch (test.resulting_policy) {
+          case UNSPECIFIED:
+            NOTREACHED();
+            break;
+          case NONE:
+            EXPECT_FALSE(result);
+            break;
+          case UV_OR_CRED:
+            ASSERT_TRUE(result);
+            EXPECT_EQ(device::CredProtect::kUVOrCredIDRequired, *result);
+            break;
+          case UV_REQ:
+            ASSERT_TRUE(result);
+            EXPECT_EQ(device::CredProtect::kUVRequired, *result);
+            break;
+        }
+        break;
+      }
+      case kNonsense:
+        EXPECT_EQ(AuthenticatorStatus::PROTECTION_POLICY_INCONSISTENT,
+                  callback_receiver.status());
+        break;
+      case kNotAllow:
+        EXPECT_EQ(AuthenticatorStatus::NOT_ALLOWED_ERROR,
+                  callback_receiver.status());
+        break;
+      default:
+        NOTREACHED();
+    }
+  }
+}
+
+TEST_F(ResidentKeyAuthenticatorImplTest, ProtectedNonResidentCreds) {
+  // Until we have UVToken, there's a danger that we'll preflight UV-required
+  // credential IDs such that the authenticator denies knowledge of all of them
+  // for silent requests and then we fail the whole request.
+  device::VirtualCtap2Device::Config config;
+  config.pin_support = true;
+  config.resident_key_support = true;
+  config.cred_protect_support = true;
+  virtual_device_.SetCtap2Config(config);
+  ASSERT_TRUE(virtual_device_.mutable_state()->InjectRegistration(
+      /*credential_id=*/{{4, 3, 2, 1}}, kTestRelyingPartyId));
+  ASSERT_EQ(1u, virtual_device_.mutable_state()->registrations.size());
+  virtual_device_.mutable_state()->registrations.begin()->second.protection =
+      device::CredProtect::kUVRequired;
+
+  TestServiceManagerContext smc;
+  AuthenticatorPtr authenticator = ConnectToAuthenticator();
+  TestGetAssertionCallback callback_receiver;
+  // |SelectAccount| should not be called when there's only a single response.
+  test_client_.expected_accounts = "<invalid>";
+
+  PublicKeyCredentialRequestOptionsPtr options = get_credential_options();
+  options->allow_credentials = GetTestCredentials(5);
+  options->allow_credentials[0]->id = {4, 3, 2, 1};
+
+  authenticator->GetAssertion(std::move(options), callback_receiver.callback());
+  callback_receiver.WaitForCallback();
+  EXPECT_EQ(AuthenticatorStatus::SUCCESS, callback_receiver.status());
+  EXPECT_TRUE(HasUV(callback_receiver));
+}
 
 }  // namespace content
diff --git a/content/browser/webauth/webauth_browsertest.cc b/content/browser/webauth/webauth_browsertest.cc
index 8c00e09..b932edb4 100644
--- a/content/browser/webauth/webauth_browsertest.cc
+++ b/content/browser/webauth/webauth_browsertest.cc
@@ -400,7 +400,8 @@
         base::TimeDelta::FromSeconds(30),
         std::vector<blink::mojom::PublicKeyCredentialDescriptorPtr>(), nullptr,
         blink::mojom::AttestationConveyancePreference::NONE, nullptr,
-        false /* no hmac_secret */);
+        false /* no hmac_secret */, blink::mojom::ProtectionPolicy::UNSPECIFIED,
+        false /* protection policy not enforced */);
 
     return mojo_options;
   }
diff --git a/content/public/browser/content_browser_client.cc b/content/public/browser/content_browser_client.cc
index bf0bbe2..23b7c63 100644
--- a/content/public/browser/content_browser_client.cc
+++ b/content/public/browser/content_browser_client.cc
@@ -651,9 +651,9 @@
 void ContentBrowserClient::OpenURL(
     content::SiteInstance* site_instance,
     const content::OpenURLParams& params,
-    const base::Callback<void(content::WebContents*)>& callback) {
+    base::OnceCallback<void(content::WebContents*)> callback) {
   DCHECK(site_instance);
-  callback.Run(nullptr);
+  std::move(callback).Run(nullptr);
 }
 
 std::string ContentBrowserClient::GetMetricSuffixForURL(const GURL& url) {
diff --git a/content/public/browser/content_browser_client.h b/content/public/browser/content_browser_client.h
index e295f5a9..1041df5 100644
--- a/content/public/browser/content_browser_client.h
+++ b/content/public/browser/content_browser_client.h
@@ -1097,7 +1097,7 @@
   // invoked with the appropriate WebContents* when available.
   virtual void OpenURL(SiteInstance* site_instance,
                        const OpenURLParams& params,
-                       const base::Callback<void(WebContents*)>& callback);
+                       base::OnceCallback<void(WebContents*)> callback);
 
   // Allows the embedder to record |metric| for a specific |url|.
   virtual void RecordURLMetric(const std::string& metric, const GURL& url) {}
diff --git a/content/public/test/content_browser_test_utils.cc b/content/public/test/content_browser_test_utils.cc
index 233c60c..48138adad 100644
--- a/content/public/test/content_browser_test_utils.cc
+++ b/content/public/test/content_browser_test_utils.cc
@@ -30,7 +30,6 @@
 #include "content/public/test/browser_test_utils.h"
 #include "content/public/test/test_frame_navigation_observer.h"
 #include "content/public/test/test_navigation_observer.h"
-#include "content/public/test/test_utils.h"
 #include "content/shell/browser/shell.h"
 #include "content/shell/browser/shell_javascript_dialog_manager.h"
 #include "net/base/filename_util.h"
@@ -121,9 +120,9 @@
       static_cast<ShellJavaScriptDialogManager*>(
           window->GetJavaScriptDialogManager(window->web_contents()));
 
-  scoped_refptr<MessageLoopRunner> runner = new MessageLoopRunner();
-  dialog_manager->set_dialog_request_callback(runner->QuitClosure());
-  runner->Run();
+  base::RunLoop runner;
+  dialog_manager->set_dialog_request_callback(runner.QuitClosure());
+  runner.Run();
 }
 
 RenderFrameHost* ConvertToRenderFrameHost(Shell* shell) {
@@ -160,18 +159,18 @@
   run_loop.Run();
 }
 
-ShellAddedObserver::ShellAddedObserver() : shell_(nullptr) {
-  Shell::SetShellCreatedCallback(
-      base::Bind(&ShellAddedObserver::ShellCreated, base::Unretained(this)));
+ShellAddedObserver::ShellAddedObserver() {
+  Shell::SetShellCreatedCallback(base::BindOnce(
+      &ShellAddedObserver::ShellCreated, base::Unretained(this)));
 }
 
-ShellAddedObserver::~ShellAddedObserver() {}
+ShellAddedObserver::~ShellAddedObserver() = default;
 
 Shell* ShellAddedObserver::GetShell() {
   if (shell_)
     return shell_;
 
-  runner_ = new MessageLoopRunner();
+  runner_ = std::make_unique<base::RunLoop>();
   runner_->Run();
   return shell_;
 }
@@ -179,8 +178,8 @@
 void ShellAddedObserver::ShellCreated(Shell* shell) {
   DCHECK(!shell_);
   shell_ = shell;
-  if (runner_.get())
-    runner_->QuitClosure().Run();
+  if (runner_)
+    runner_->Quit();
 }
 
 void IsolateOriginsForTesting(
diff --git a/content/public/test/content_browser_test_utils.h b/content/public/test/content_browser_test_utils.h
index d0a77d4..cce4d63 100644
--- a/content/public/test/content_browser_test_utils.h
+++ b/content/public/test/content_browser_test_utils.h
@@ -10,7 +10,7 @@
 
 #include "base/callback.h"
 #include "base/macros.h"
-#include "base/memory/ref_counted.h"
+#include "base/run_loop.h"
 #include "content/public/common/page_type.h"
 #include "ui/gfx/native_widget_types.h"
 #include "url/gurl.h"
@@ -41,8 +41,6 @@
 // content\public\test\browser_test_utils.h
 
 namespace content {
-
-class MessageLoopRunner;
 class RenderFrameHost;
 class RenderWidgetHost;
 class Shell;
@@ -134,8 +132,8 @@
  private:
   void ShellCreated(Shell* shell);
 
-  Shell* shell_;
-  scoped_refptr<MessageLoopRunner> runner_;
+  Shell* shell_ = nullptr;
+  std::unique_ptr<base::RunLoop> runner_;
 
   DISALLOW_COPY_AND_ASSIGN(ShellAddedObserver);
 };
diff --git a/content/renderer/media/stream/webmediaplayer_ms.cc b/content/renderer/media/stream/webmediaplayer_ms.cc
index 79f56e1..a91b08c 100644
--- a/content/renderer/media/stream/webmediaplayer_ms.cc
+++ b/content/renderer/media/stream/webmediaplayer_ms.cc
@@ -24,7 +24,7 @@
 #include "media/base/media_content_type.h"
 #include "media/base/media_log.h"
 #include "media/base/video_frame.h"
-#include "media/base/video_rotation.h"
+#include "media/base/video_transformation.h"
 #include "media/base/video_types.h"
 #include "media/blink/webmediaplayer_util.h"
 #include "media/video/gpu_memory_buffer_video_frame_pool.h"
@@ -250,7 +250,7 @@
       delegate_(delegate),
       delegate_id_(0),
       paused_(true),
-      video_rotation_(media::VIDEO_ROTATION_0),
+      video_transformation_(media::kNoTransformation),
       media_log_(std::move(media_log)),
       renderer_factory_(std::move(factory)),
       main_render_task_runner_(std::move(main_render_task_runner)),
@@ -685,8 +685,8 @@
   if (!video_frame_provider_)
     return blink::WebSize();
 
-  if (video_rotation_ == media::VIDEO_ROTATION_90 ||
-      video_rotation_ == media::VideoRotation::VIDEO_ROTATION_270) {
+  if (video_transformation_.rotation == media::VIDEO_ROTATION_90 ||
+      video_transformation_.rotation == media::VIDEO_ROTATION_270) {
     const gfx::Size& current_size = compositor_->GetCurrentSize();
     return blink::WebSize(current_size.height(), current_size.width());
   }
@@ -700,8 +700,8 @@
     return blink::WebSize();
 
   const gfx::Rect& visible_rect = video_frame->visible_rect();
-  if (video_rotation_ == media::VIDEO_ROTATION_90 ||
-      video_rotation_ == media::VideoRotation::VIDEO_ROTATION_270) {
+  if (video_transformation_.rotation == media::VIDEO_ROTATION_90 ||
+      video_transformation_.rotation == media::VIDEO_ROTATION_270) {
     return blink::WebSize(visible_rect.height(), visible_rect.width());
   }
   return blink::WebSize(visible_rect.width(), visible_rect.height());
@@ -787,7 +787,7 @@
       return;
   }
   const gfx::RectF dest_rect(rect.x, rect.y, rect.width, rect.height);
-  video_renderer_.Paint(frame, canvas, dest_rect, flags, video_rotation_,
+  video_renderer_.Paint(frame, canvas, dest_rect, flags, video_transformation_,
                         provider);
 }
 
@@ -1054,7 +1054,7 @@
       FROM_HERE, base::BindOnce(&WebMediaPlayerMSCompositor::EnableSubmission,
                                 compositor_, bridge_->GetSurfaceId(),
                                 bridge_->GetLocalSurfaceIdAllocationTime(),
-                                video_rotation_, IsInPictureInPicture()));
+                                video_transformation_, IsInPictureInPicture()));
 
   // If the element is already in Picture-in-Picture mode, it means that it
   // was set in this mode prior to this load, with a different
@@ -1110,7 +1110,7 @@
 void WebMediaPlayerMS::OnRotationChanged(media::VideoRotation video_rotation) {
   DVLOG(1) << __func__;
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
-  video_rotation_ = video_rotation;
+  video_transformation_ = {video_rotation, 0};
 
   if (!bridge_) {
     // Keep the old |video_layer_| alive until SetCcLayer() is called with a new
diff --git a/content/renderer/media/stream/webmediaplayer_ms.h b/content/renderer/media/stream/webmediaplayer_ms.h
index 891deea..138c138 100644
--- a/content/renderer/media/stream/webmediaplayer_ms.h
+++ b/content/renderer/media/stream/webmediaplayer_ms.h
@@ -306,7 +306,7 @@
   media::PaintCanvasVideoRenderer video_renderer_;
 
   bool paused_;
-  media::VideoRotation video_rotation_;
+  media::VideoTransformation video_transformation_;
 
   std::unique_ptr<media::MediaLog> media_log_;
 
diff --git a/content/renderer/media/stream/webmediaplayer_ms_compositor.cc b/content/renderer/media/stream/webmediaplayer_ms_compositor.cc
index f9d85a5d..53ff571 100644
--- a/content/renderer/media/stream/webmediaplayer_ms_compositor.cc
+++ b/content/renderer/media/stream/webmediaplayer_ms_compositor.cc
@@ -211,7 +211,7 @@
 void WebMediaPlayerMSCompositor::EnableSubmission(
     const viz::SurfaceId& id,
     base::TimeTicks local_surface_id_allocation_time,
-    media::VideoRotation rotation,
+    media::VideoTransformation transformation,
     bool force_submit) {
   DCHECK(video_frame_compositor_task_runner_->BelongsToCurrentThread());
 
@@ -221,7 +221,7 @@
     video_frame_provider_client_->StopUsingProvider();
   }
 
-  submitter_->SetRotation(rotation);
+  submitter_->SetRotation(transformation.rotation);
   submitter_->SetForceSubmit(force_submit);
   submitter_->EnableSubmission(id, local_surface_id_allocation_time);
   video_frame_provider_client_ = submitter_.get();
diff --git a/content/renderer/media/stream/webmediaplayer_ms_compositor.h b/content/renderer/media/stream/webmediaplayer_ms_compositor.h
index cf66a20f..d7e2520 100644
--- a/content/renderer/media/stream/webmediaplayer_ms_compositor.h
+++ b/content/renderer/media/stream/webmediaplayer_ms_compositor.h
@@ -84,7 +84,7 @@
   virtual void EnableSubmission(
       const viz::SurfaceId& id,
       base::TimeTicks local_surface_id_allocation_time,
-      media::VideoRotation rotation,
+      media::VideoTransformation transformation,
       bool force_submit);
 
   // Notifies the |submitter_| that the frames must be submitted.
diff --git a/content/renderer/media/webrtc/rtc_video_decoder_adapter.cc b/content/renderer/media/webrtc/rtc_video_decoder_adapter.cc
index f26a722..26f0c91 100644
--- a/content/renderer/media/webrtc/rtc_video_decoder_adapter.cc
+++ b/content/renderer/media/webrtc/rtc_video_decoder_adapter.cc
@@ -159,7 +159,7 @@
   media::VideoDecoderConfig config(
       ToVideoCodec(webrtc::PayloadStringToCodecType(format.name)),
       GuessVideoCodecProfile(format), kDefaultPixelFormat,
-      media::VideoColorSpace(), media::VIDEO_ROTATION_0, kDefaultSize,
+      media::VideoColorSpace(), media::kNoTransformation, kDefaultSize,
       gfx::Rect(kDefaultSize), kDefaultSize, media::EmptyExtraData(),
       media::Unencrypted());
   if (!gpu_factories->IsDecoderConfigSupported(kImplementation, config))
diff --git a/content/renderer/media/webrtc/webrtc_video_utils.h b/content/renderer/media/webrtc/webrtc_video_utils.h
index 1d91b2a4..5bcfcf0 100644
--- a/content/renderer/media/webrtc/webrtc_video_utils.h
+++ b/content/renderer/media/webrtc/webrtc_video_utils.h
@@ -6,7 +6,7 @@
 #define CONTENT_RENDERER_MEDIA_WEBRTC_WEBRTC_VIDEO_UTILS_H_
 
 #include "media/base/video_color_space.h"
-#include "media/base/video_rotation.h"
+#include "media/base/video_transformation.h"
 #include "third_party/webrtc/api/video/color_space.h"
 #include "third_party/webrtc/api/video/video_rotation.h"
 
diff --git a/content/renderer/pepper/video_decoder_shim.cc b/content/renderer/pepper/video_decoder_shim.cc
index 5c75dd4c..5163608 100644
--- a/content/renderer/pepper/video_decoder_shim.cc
+++ b/content/renderer/pepper/video_decoder_shim.cc
@@ -894,7 +894,7 @@
 
   media::VideoDecoderConfig video_decoder_config(
       codec, vda_config.profile, media::PIXEL_FORMAT_I420,
-      media::VideoColorSpace(), media::VIDEO_ROTATION_0,
+      media::VideoColorSpace(), media::kNoTransformation,
       gfx::Size(32, 24),  // Small sizes that won't fail.
       gfx::Rect(32, 24), gfx::Size(32, 24),
       // TODO(bbudge): Verify extra data isn't needed.
diff --git a/content/shell/browser/shell.cc b/content/shell/browser/shell.cc
index ad1d7259..0da00e47 100644
--- a/content/shell/browser/shell.cc
+++ b/content/shell/browser/shell.cc
@@ -55,7 +55,7 @@
 const int kDefaultTestWindowHeightDip = 600;
 
 std::vector<Shell*> Shell::windows_;
-base::Callback<void(Shell*)> Shell::shell_created_callback_;
+base::OnceCallback<void(Shell*)> Shell::shell_created_callback_;
 
 class Shell::DevToolsWebContentsObserver : public WebContentsObserver {
  public:
@@ -107,10 +107,8 @@
 
   windows_.push_back(this);
 
-  if (!shell_created_callback_.is_null()) {
-    shell_created_callback_.Run(this);
-    shell_created_callback_.Reset();
-  }
+  if (shell_created_callback_)
+    std::move(shell_created_callback_).Run(this);
 }
 
 Shell::~Shell() {
@@ -197,8 +195,8 @@
 }
 
 void Shell::SetShellCreatedCallback(
-    base::Callback<void(Shell*)> shell_created_callback) {
-  DCHECK(shell_created_callback_.is_null());
+    base::OnceCallback<void(Shell*)> shell_created_callback) {
+  DCHECK(!shell_created_callback_);
   shell_created_callback_ = std::move(shell_created_callback);
 }
 
diff --git a/content/shell/browser/shell.h b/content/shell/browser/shell.h
index 354a936..9080bde 100644
--- a/content/shell/browser/shell.h
+++ b/content/shell/browser/shell.h
@@ -124,7 +124,7 @@
 
   // Used for content_browsertests. Called once.
   static void SetShellCreatedCallback(
-      base::Callback<void(Shell*)> shell_created_callback);
+      base::OnceCallback<void(Shell*)> shell_created_callback);
 
   WebContents* web_contents() const { return web_contents_.get(); }
   gfx::NativeWindow window() { return window_; }
@@ -310,7 +310,7 @@
   // of ordering.
   static std::vector<Shell*> windows_;
 
-  static base::Callback<void(Shell*)> shell_created_callback_;
+  static base::OnceCallback<void(Shell*)> shell_created_callback_;
 };
 
 }  // namespace content
diff --git a/content/shell/browser/shell_content_browser_client.cc b/content/shell/browser/shell_content_browser_client.cc
index efc1902..e47f78b 100644
--- a/content/shell/browser/shell_content_browser_client.cc
+++ b/content/shell/browser/shell_content_browser_client.cc
@@ -483,10 +483,11 @@
 void ShellContentBrowserClient::OpenURL(
     SiteInstance* site_instance,
     const OpenURLParams& params,
-    const base::Callback<void(WebContents*)>& callback) {
-  callback.Run(Shell::CreateNewWindow(site_instance->GetBrowserContext(),
-                                      params.url, nullptr, gfx::Size())
-                   ->web_contents());
+    base::OnceCallback<void(WebContents*)> callback) {
+  std::move(callback).Run(
+      Shell::CreateNewWindow(site_instance->GetBrowserContext(), params.url,
+                             nullptr, gfx::Size())
+          ->web_contents());
 }
 
 std::unique_ptr<LoginDelegate> ShellContentBrowserClient::CreateLoginDelegate(
diff --git a/content/shell/browser/shell_content_browser_client.h b/content/shell/browser/shell_content_browser_client.h
index 6d58d2dff..bc13ac56 100644
--- a/content/shell/browser/shell_content_browser_client.h
+++ b/content/shell/browser/shell_content_browser_client.h
@@ -78,7 +78,7 @@
   DevToolsManagerDelegate* GetDevToolsManagerDelegate() override;
   void OpenURL(SiteInstance* site_instance,
                const OpenURLParams& params,
-               const base::Callback<void(WebContents*)>& callback) override;
+               base::OnceCallback<void(WebContents*)> callback) override;
   std::unique_ptr<LoginDelegate> CreateLoginDelegate(
       const net::AuthChallengeInfo& auth_info,
       content::WebContents* web_contents,
diff --git a/content/shell/browser/shell_download_manager_delegate.cc b/content/shell/browser/shell_download_manager_delegate.cc
index 2ca93dab..af9b2f3 100644
--- a/content/shell/browser/shell_download_manager_delegate.cc
+++ b/content/shell/browser/shell_download_manager_delegate.cc
@@ -81,11 +81,9 @@
     return true;
   }
 
-  FilenameDeterminedCallback filename_determined_callback =
-      base::Bind(&ShellDownloadManagerDelegate::OnDownloadPathGenerated,
-                 weak_ptr_factory_.GetWeakPtr(),
-                 download->GetId(),
-                 callback);
+  FilenameDeterminedCallback filename_determined_callback = base::BindOnce(
+      &ShellDownloadManagerDelegate::OnDownloadPathGenerated,
+      weak_ptr_factory_.GetWeakPtr(), download->GetId(), callback);
 
   PostTaskWithTraits(
       FROM_HERE,
@@ -118,7 +116,7 @@
     const std::string& suggested_filename,
     const std::string& mime_type,
     const base::FilePath& suggested_directory,
-    const FilenameDeterminedCallback& callback) {
+    FilenameDeterminedCallback callback) {
   base::FilePath generated_name = net::GenerateFileName(url,
                                                         content_disposition,
                                                         std::string(),
@@ -131,7 +129,7 @@
 
   base::FilePath suggested_path(suggested_directory.Append(generated_name));
   base::PostTaskWithTraits(FROM_HERE, {BrowserThread::UI},
-                           base::BindOnce(callback, suggested_path));
+                           base::BindOnce(std::move(callback), suggested_path));
 }
 
 void ShellDownloadManagerDelegate::OnDownloadPathGenerated(
diff --git a/content/shell/browser/shell_download_manager_delegate.h b/content/shell/browser/shell_download_manager_delegate.h
index 6086da5..3e4922d 100644
--- a/content/shell/browser/shell_download_manager_delegate.h
+++ b/content/shell/browser/shell_download_manager_delegate.h
@@ -38,15 +38,15 @@
  private:
   friend class base::RefCountedThreadSafe<ShellDownloadManagerDelegate>;
 
-  typedef base::Callback<void(const base::FilePath&)>
-      FilenameDeterminedCallback;
+  using FilenameDeterminedCallback =
+      base::OnceCallback<void(const base::FilePath&)>;
 
   static void GenerateFilename(const GURL& url,
                                const std::string& content_disposition,
                                const std::string& suggested_filename,
                                const std::string& mime_type,
                                const base::FilePath& suggested_directory,
-                               const FilenameDeterminedCallback& callback);
+                               FilenameDeterminedCallback callback);
   void OnDownloadPathGenerated(uint32_t download_id,
                                const DownloadTargetCallback& callback,
                                const base::FilePath& suggested_path);
diff --git a/content/shell/browser/web_test/scoped_android_configuration.cc b/content/shell/browser/web_test/scoped_android_configuration.cc
index ec1b9722..f5c3bae 100644
--- a/content/shell/browser/web_test/scoped_android_configuration.cc
+++ b/content/shell/browser/web_test/scoped_android_configuration.cc
@@ -102,28 +102,27 @@
 }
 
 void FinishRedirection(
-    const base::Callback<void(int)>& redirect,
-    const base::Callback<void(std::unique_ptr<net::SocketPosix>)>&
-        transfer_socket,
+    base::OnceCallback<void(int)> redirect,
+    base::OnceCallback<void(std::unique_ptr<net::SocketPosix>)> transfer_socket,
     base::WaitableEvent* event,
     std::unique_ptr<net::SocketPosix> socket) {
-  redirect.Run(socket->socket_fd());
-  transfer_socket.Run(std::move(socket));
+  std::move(redirect).Run(socket->socket_fd());
+  std::move(transfer_socket).Run(std::move(socket));
   event->Signal();
 }
 
-void RedirectStream(
-    uint16_t port,
-    const base::Callback<void(base::WaitableEvent*,
-                              std::unique_ptr<net::SocketPosix>)>&
-        finish_redirection) {
+void RedirectStream(uint16_t port,
+                    base::OnceCallback<void(base::WaitableEvent*,
+                                            std::unique_ptr<net::SocketPosix>)>
+                        finish_redirection) {
   base::WaitableEvent redirected(
       base::WaitableEvent::ResetPolicy::MANUAL,
       base::WaitableEvent::InitialState::NOT_SIGNALED);
   base::PostTaskWithTraits(
       FROM_HERE, {BrowserThread::IO},
-      base::BindOnce(&CreateAndConnectSocket, port,
-                     base::BindOnce(finish_redirection, &redirected)));
+      base::BindOnce(
+          &CreateAndConnectSocket, port,
+          base::BindOnce(std::move(finish_redirection), &redirected)));
   base::ScopedAllowBaseSyncPrimitivesForTesting allow_wait;
   while (!redirected.IsSignaled())
     redirected.Wait();
@@ -138,20 +137,20 @@
 ScopedAndroidConfiguration::~ScopedAndroidConfiguration() = default;
 
 void ScopedAndroidConfiguration::RedirectStreams() {
-  // Unretained is safe here because all executions of add_socket finish
-  // before this function returns.
-  base::Callback<void(std::unique_ptr<net::SocketPosix>)> add_socket =
-      base::Bind(&ScopedAndroidConfiguration::AddSocket,
-                 base::Unretained(this));
-
   std::string stdout_port_str =
       base::CommandLine::ForCurrentProcess()->GetSwitchValueNative(
           switches::kAndroidStdoutPort);
   unsigned stdout_port = 0;
   if (base::StringToUint(stdout_port_str, &stdout_port)) {
-    RedirectStream(base::checked_cast<uint16_t>(stdout_port),
-                   base::Bind(&FinishRedirection, base::Bind(&RedirectStdout),
-                              add_socket));
+    auto redirect_callback = base::BindOnce(&RedirectStdout);
+    // Unretained is safe here because all executions of transfer_callback
+    // finish before this function returns.
+    auto transfer_callback = base::BindOnce(
+        &ScopedAndroidConfiguration::AddSocket, base::Unretained(this));
+    RedirectStream(
+        base::checked_cast<uint16_t>(stdout_port),
+        base::BindOnce(&FinishRedirection, std::move(redirect_callback),
+                       std::move(transfer_callback)));
   }
 
   std::string stdin_port_str =
@@ -159,9 +158,15 @@
           switches::kAndroidStdinPort);
   unsigned stdin_port = 0;
   if (base::StringToUint(stdin_port_str, &stdin_port)) {
+    auto redirect_callback = base::BindOnce(&RedirectStdin);
+    // Unretained is safe here because all executions of transfer_callback
+    // finish before this function returns.
+    auto transfer_callback = base::BindOnce(
+        &ScopedAndroidConfiguration::AddSocket, base::Unretained(this));
     RedirectStream(
         base::checked_cast<uint16_t>(stdin_port),
-        base::Bind(&FinishRedirection, base::Bind(&RedirectStdin), add_socket));
+        base::BindOnce(&FinishRedirection, std::move(redirect_callback),
+                       std::move(transfer_callback)));
   }
 
   std::string stderr_port_str =
@@ -169,9 +174,15 @@
           switches::kAndroidStderrPort);
   unsigned stderr_port = 0;
   if (base::StringToUint(stderr_port_str, &stderr_port)) {
-    RedirectStream(base::checked_cast<uint16_t>(stderr_port),
-                   base::Bind(&FinishRedirection, base::Bind(&RedirectStderr),
-                              add_socket));
+    auto redirect_callback = base::BindOnce(&RedirectStderr);
+    // Unretained is safe here because all executions of transfer_callback
+    // finish before this function returns.
+    auto transfer_callback = base::BindOnce(
+        &ScopedAndroidConfiguration::AddSocket, base::Unretained(this));
+    RedirectStream(
+        base::checked_cast<uint16_t>(stderr_port),
+        base::BindOnce(&FinishRedirection, std::move(redirect_callback),
+                       std::move(transfer_callback)));
   }
 }
 
diff --git a/device/fido/authenticator_supported_options.h b/device/fido/authenticator_supported_options.h
index 65e6fea..b0ba9137 100644
--- a/device/fido/authenticator_supported_options.h
+++ b/device/fido/authenticator_supported_options.h
@@ -60,6 +60,9 @@
   // Indicates whether the authenticator supports the vendor-specific preview of
   // the CTAP2 authenticatorCredentialManagement command.
   bool supports_credential_management_preview = false;
+  // supports_cred_protect is true if the authenticator supports the
+  // `credProtect` extension. See CTAP2 draft for details.
+  bool supports_cred_protect = false;
   // Represents whether client pin is set and stored in authenticator. Set as
   // null optional if client pin capability is not supported by the
   // authenticator.
diff --git a/device/fido/ctap_make_credential_request.cc b/device/fido/ctap_make_credential_request.cc
index ef8a693..e2c6d7c4 100644
--- a/device/fido/ctap_make_credential_request.cc
+++ b/device/fido/ctap_make_credential_request.cc
@@ -60,9 +60,26 @@
     cbor_map[cbor::Value(5)] = cbor::Value(std::move(exclude_list_array));
   }
 
+  cbor::Value::MapValue extensions;
+
   if (request.hmac_secret) {
-    cbor::Value::MapValue extensions;
     extensions[cbor::Value(kExtensionHmacSecret)] = cbor::Value(true);
+  }
+
+  if (request.cred_protect) {
+    int value;
+    switch (request.cred_protect->first) {
+      case CredProtect::kUVOrCredIDRequired:
+        value = 2;
+        break;
+      case CredProtect::kUVRequired:
+        value = 3;
+        break;
+    }
+    extensions.emplace(kExtensionCredProtect, value);
+  }
+
+  if (!extensions.empty()) {
     cbor_map[cbor::Value(6)] = cbor::Value(std::move(extensions));
   }
 
diff --git a/device/fido/ctap_make_credential_request.h b/device/fido/ctap_make_credential_request.h
index 25dda96..387bbfac 100644
--- a/device/fido/ctap_make_credential_request.h
+++ b/device/fido/ctap_make_credential_request.h
@@ -75,6 +75,13 @@
   base::Optional<uint8_t> pin_protocol;
   AttestationConveyancePreference attestation_preference =
       AttestationConveyancePreference::NONE;
+
+  // cred_protect indicates the level of protection afforded to a credential.
+  // This depends on a CTAP2 extension that not all authenticators will support.
+  // The second element is true if the indicated protection level must be
+  // provided by the target authenticator for the MakeCredential request to be
+  // sent.
+  base::Optional<std::pair<CredProtect, bool>> cred_protect;
 };
 
 }  // namespace device
diff --git a/device/fido/device_response_converter.cc b/device/fido/device_response_converter.cc
index 9ca4abe0..d351270 100644
--- a/device/fido/device_response_converter.cc
+++ b/device/fido/device_response_converter.cc
@@ -206,6 +206,7 @@
       std::move(protocol_versions),
       base::make_span<kAaguidLength>(it->second.GetBytestring()));
 
+  AuthenticatorSupportedOptions options;
   it = response_map.find(CBOR(2));
   if (it != response_map.end()) {
     if (!it->second.is_array())
@@ -216,12 +217,15 @@
       if (!extension.is_string())
         return base::nullopt;
 
-      extensions.push_back(extension.GetString());
+      const std::string& extension_str = extension.GetString();
+      if (extension_str == kExtensionCredProtect) {
+        options.supports_cred_protect = true;
+      }
+      extensions.push_back(extension_str);
     }
     response.extensions = std::move(extensions);
   }
 
-  AuthenticatorSupportedOptions options;
   it = response_map.find(CBOR(4));
   if (it != response_map.end()) {
     if (!it->second.is_map())
diff --git a/device/fido/fido_constants.cc b/device/fido/fido_constants.cc
index fa78ae3..77088a7 100644
--- a/device/fido/fido_constants.cc
+++ b/device/fido/fido_constants.cc
@@ -58,6 +58,7 @@
 const char kU2fVersion[] = "U2F_V2";
 
 const char kExtensionHmacSecret[] = "hmac-secret";
+const char kExtensionCredProtect[] = "credProtect";
 
 const base::TimeDelta kBleDevicePairingModeWaitingInterval =
     base::TimeDelta::FromSeconds(2);
diff --git a/device/fido/fido_constants.h b/device/fido/fido_constants.h
index 62e0b12..a2727a6 100644
--- a/device/fido/fido_constants.h
+++ b/device/fido/fido_constants.h
@@ -385,6 +385,7 @@
 COMPONENT_EXPORT(DEVICE_FIDO) extern const char kU2fVersion[];
 
 COMPONENT_EXPORT(DEVICE_FIDO) extern const char kExtensionHmacSecret[];
+COMPONENT_EXPORT(DEVICE_FIDO) extern const char kExtensionCredProtect[];
 
 // Maximum number of seconds the browser waits for Bluetooth authenticator to
 // send packets that advertises that the device is in pairing mode before
@@ -405,6 +406,13 @@
   ENTERPRISE,
 };
 
+// CredProtect enumerates the levels of credential protection specified by the
+// `credProtect` CTAP2 extension.
+enum class CredProtect : uint8_t {
+  kUVOrCredIDRequired = 1,
+  kUVRequired = 2,
+};
+
 }  // namespace device
 
 #endif  // DEVICE_FIDO_FIDO_CONSTANTS_H_
diff --git a/device/fido/get_assertion_task.cc b/device/fido/get_assertion_task.cc
index 26e0ca0..6bba57c 100644
--- a/device/fido/get_assertion_task.cc
+++ b/device/fido/get_assertion_task.cc
@@ -91,12 +91,19 @@
   // Silently probe each credential in the allow list to work around
   // authenticators rejecting lists over a certain size. Also probe silently if
   // the request may fall back to U2F and the authenticator doesn't recognize
-  // any of the provided credential IDs. (caBLE devices, however, might not
-  // support silent probing so don't do it with them.)
-  if (device()->DeviceTransport() !=
+  // any of the provided credential IDs.
+  if ((request_.allow_list.size() > 1 ||
+       MayFallbackToU2fWithAppIdExtension(*device(), request_)) &&
+      // caBLE devices might not support silent probing so don't do it with
+      // them.
+      device()->DeviceTransport() !=
           FidoTransportProtocol::kCloudAssistedBluetoothLowEnergy &&
-      (request_.allow_list.size() > 1 ||
-       MayFallbackToU2fWithAppIdExtension(*device(), request_))) {
+      // If the device supports credProtect then it might have UV-required
+      // credentials which it'll pretend don't exist for silent requests.
+      // TODO(agl): should support batching of, and filtering over-long,
+      // credentials based on GetInfo data. Also should support
+      // PIN-authenticated silent requests.
+      !device()->device_info()->options.supports_cred_protect) {
     sign_operation_ = std::make_unique<Ctap2DeviceOperation<
         CtapGetAssertionRequest, AuthenticatorGetAssertionResponse>>(
         device(), NextSilentRequest(),
diff --git a/device/fido/make_credential_request_handler.cc b/device/fido/make_credential_request_handler.cc
index 181fd94..37dd6b5 100644
--- a/device/fido/make_credential_request_handler.cc
+++ b/device/fido/make_credential_request_handler.cc
@@ -76,6 +76,15 @@
     return FidoReturnCode::kAuthenticatorMissingResidentKeys;
   }
 
+  // TODO(martinkr): the Windows integration needs to be able to pass the
+  // credProtect information to the DLL, and to fail if the DLL version is too
+  // low to support credProtect.
+  if (request.cred_protect && request.cred_protect->second &&
+      (!authenticator->Options() ||
+       !authenticator->Options()->supports_cred_protect)) {
+    return FidoReturnCode::kAuthenticatorMissingResidentKeys;
+  }
+
   if (authenticator->WillNeedPINToMakeCredential(request, observer) ==
       MakeCredentialPINDisposition::kUnsatisfiable) {
     return FidoReturnCode::kAuthenticatorMissingUserVerification;
@@ -225,6 +234,11 @@
     } else {
       request.user_verification = UserVerificationRequirement::kDiscouraged;
     }
+
+    if (request.cred_protect &&
+        !authenticator->Options()->supports_cred_protect) {
+      request.cred_protect.reset();
+    }
   }
 
   ReportMakeCredentialRequestTransport(authenticator);
@@ -507,6 +521,10 @@
   // If doing a PIN operation then we don't ask the authenticator to also do
   // internal UV.
   request.user_verification = UserVerificationRequirement::kDiscouraged;
+  if (request.cred_protect && authenticator_->Options() &&
+      !authenticator_->Options()->supports_cred_protect) {
+    request.cred_protect.reset();
+  }
 
   ReportMakeCredentialRequestTransport(authenticator_);
 
diff --git a/device/fido/public_key_credential_user_entity.cc b/device/fido/public_key_credential_user_entity.cc
index 990bc5d4..056b648349 100644
--- a/device/fido/public_key_credential_user_entity.cc
+++ b/device/fido/public_key_credential_user_entity.cc
@@ -61,7 +61,8 @@
   user_map.emplace(kEntityIdMapKey, user.id);
   if (user.name)
     user_map.emplace(kEntityNameMapKey, *user.name);
-  if (user.icon_url)
+  // Empty icon URLs result in CTAP1_ERR_INVALID_LENGTH on some security keys.
+  if (user.icon_url && !user.icon_url->is_empty())
     user_map.emplace(kIconUrlMapKey, user.icon_url->spec());
   if (user.display_name)
     user_map.emplace(kDisplayNameMapKey, *user.display_name);
diff --git a/device/fido/virtual_ctap2_device.cc b/device/fido/virtual_ctap2_device.cc
index 734b5edb..eeb9a0c 100644
--- a/device/fido/virtual_ctap2_device.cc
+++ b/device/fido/virtual_ctap2_device.cc
@@ -496,6 +496,11 @@
   if (options_updated) {
     device_info_->options = std::move(options);
   }
+
+  if (config.cred_protect_support) {
+    device_info_->extensions.emplace(
+        {std::string(device::kExtensionCredProtect)});
+  }
 }
 
 VirtualCtap2Device::~VirtualCtap2Device() = default;
@@ -608,7 +613,15 @@
     }
 
     for (const auto& excluded_credential : *request.exclude_list) {
-      if (FindRegistrationData(excluded_credential.id(), rp_id_hash)) {
+      const RegistrationData* found =
+          FindRegistrationData(excluded_credential.id(), rp_id_hash);
+      if (found) {
+        if (found->protection == device::CredProtect::kUVRequired &&
+            !user_verified) {
+          // Cannot disclose the existence of this credential without UV. If
+          // a credentials ends up being created it'll overwrite this one.
+          continue;
+        }
         if (mutable_state()->simulate_press_callback) {
           mutable_state()->simulate_press_callback.Run();
         }
@@ -648,10 +661,19 @@
   std::vector<uint8_t> key_handle(hash.begin(), hash.end());
 
   base::Optional<cbor::Value> extensions;
+  cbor::Value::MapValue extensions_map;
   if (request.hmac_secret) {
-    cbor::Value::MapValue extensions_map;
     extensions_map.emplace(cbor::Value(kExtensionHmacSecret),
                            cbor::Value(true));
+  }
+
+  if (request.cred_protect) {
+    extensions_map.emplace(
+        cbor::Value(kExtensionCredProtect),
+        cbor::Value(
+            request.cred_protect->first == CredProtect::kUVRequired ? 3 : 2));
+  }
+  if (!extensions_map.empty()) {
     extensions = cbor::Value(std::move(extensions_map));
   }
 
@@ -713,6 +735,10 @@
     registration.user = request.user;
   }
 
+  if (request.cred_protect) {
+    registration.protection = request.cred_protect->first;
+  }
+
   StoreNewKey(key_handle, std::move(registration));
   return CtapDeviceResponseCode::kSuccess;
 }
@@ -793,6 +819,25 @@
     }
   }
 
+  // Enforce credProtect semantics.
+  found_registrations.erase(
+      std::remove_if(
+          found_registrations.begin(), found_registrations.end(),
+          [user_verified, &request](
+              const std::pair<base::span<const uint8_t>, RegistrationData*>&
+                  candidate) -> bool {
+            if (!candidate.second->protection) {
+              return false;
+            }
+            switch (*candidate.second->protection) {
+              case CredProtect::kUVOrCredIDRequired:
+                return request.allow_list.empty() && !user_verified;
+              case CredProtect::kUVRequired:
+                return !user_verified;
+            }
+          }),
+      found_registrations.end());
+
   if (config_.return_immediate_invalid_credential_error &&
       found_registrations.empty()) {
     return CtapDeviceResponseCode::kCtap2ErrInvalidCredential;
@@ -1439,6 +1484,29 @@
       }
       request.hmac_secret = hmac_secret_it->second.GetBool();
     }
+
+    const auto cred_protect_it =
+        extensions.find(cbor::Value(device::kExtensionCredProtect));
+    if (cred_protect_it != extensions.end()) {
+      if (!cred_protect_it->second.is_unsigned()) {
+        return base::nullopt;
+      }
+      switch (cred_protect_it->second.GetUnsigned()) {
+        case 1:
+          // Default behaviour.
+          break;
+        case 2:
+          request.cred_protect =
+              std::make_pair(device::CredProtect::kUVOrCredIDRequired, false);
+          break;
+        case 3:
+          request.cred_protect =
+              std::make_pair(device::CredProtect::kUVRequired, false);
+          break;
+        default:
+          return base::nullopt;
+      }
+    }
   }
 
   const auto option_it = request_map.find(cbor::Value(7));
diff --git a/device/fido/virtual_ctap2_device.h b/device/fido/virtual_ctap2_device.h
index 7fe53ba8..23277298 100644
--- a/device/fido/virtual_ctap2_device.h
+++ b/device/fido/virtual_ctap2_device.h
@@ -44,6 +44,7 @@
     bool internal_uv_support = false;
     bool resident_key_support = false;
     bool credential_management_support = false;
+    bool cred_protect_support = false;
     // resident_credential_storage is the number of resident credentials that
     // the device will store before returning KEY_STORE_FULL.
     size_t resident_credential_storage = 3;
diff --git a/device/fido/virtual_fido_device.h b/device/fido/virtual_fido_device.h
index bfcd28f..f5d63b4a 100644
--- a/device/fido/virtual_fido_device.h
+++ b/device/fido/virtual_fido_device.h
@@ -55,6 +55,7 @@
     bool is_resident = false;
     // is_u2f is true if the credential was created via a U2F interface.
     bool is_u2f = false;
+    base::Optional<device::CredProtect> protection;
 
     // user is only valid if |is_resident| is true.
     base::Optional<device::PublicKeyCredentialUserEntity> user;
diff --git a/device/vr/public/mojom/isolated_xr_service.mojom b/device/vr/public/mojom/isolated_xr_service.mojom
index 2685841e..652b623 100644
--- a/device/vr/public/mojom/isolated_xr_service.mojom
+++ b/device/vr/public/mojom/isolated_xr_service.mojom
@@ -50,10 +50,6 @@
   int32 render_process_id;
   int32 render_frame_id;
 
-  // A flag to indicate if there has been a user activation when the request
-  // session is made.
-  bool has_user_activation;
-
   // This flag ensures that render path's that are only supported in WebXR are
   // not used for WebVR 1.1.
   bool use_legacy_webvr_render_path;
diff --git a/device/vr/public/mojom/vr_service.mojom b/device/vr/public/mojom/vr_service.mojom
index 1f1e6b1..b6b0bd3 100644
--- a/device/vr/public/mojom/vr_service.mojom
+++ b/device/vr/public/mojom/vr_service.mojom
@@ -46,10 +46,6 @@
   bool immersive;
   bool environment_integration;
 
-  // A flag to indicate if there has been a user activation when the request
-  // session is made.
-  bool has_user_activation;
-
   // This flag ensures that render paths that are only supported in WebXR are
   // not used for WebVR 1.1.
   bool use_legacy_webvr_render_path;
@@ -126,6 +122,8 @@
 //
 
 // A field of view, given by 4 degrees describing the view from a center point.
+// For a typical field of view that contains the center point, all angles are
+// positive.
 struct VRFieldOfView {
   float upDegrees;
   float downDegrees;
@@ -266,10 +264,6 @@
   // imply no mapping.
   int16 frame_id;
 
-  // Pass through camera values
-  gfx.mojom.Size? buffer_size;
-  array<float, 16>? projection_matrix;
-
   // Eye parameters may be provided per-frame for some runtimes.  If both of
   // these are null, it indicates that there was no change since the previous
   // frame.  If either are non-null, it indicates that data has changed. If only
@@ -348,14 +342,6 @@
 // XRSession. For example, some AR sessions would implement hit test to allow
 // developers to get the information about the world that its sensors supply.
 interface XREnvironmentIntegrationProvider {
-  // Different devices can have different native orientations - 0 is the native
-  // orientation, and then increments of 90 degrees from there. Session geometry
-  // is needed by the device when integrating environment image data, i.e.
-  // camera feeds, into a session.
-  UpdateSessionGeometry(
-      gfx.mojom.Size frame_size,
-      display.mojom.Rotation display_rotation);
-
   // Performs a raycast into the scene and returns a list of XRHitResults sorted
   // from closest to furthest hit from the ray. Each hit result contains a
   // hit_matrix containing the transform of the hit where the rotation
diff --git a/device/vr/windows_mixed_reality/mixed_reality_input_helper.cc b/device/vr/windows_mixed_reality/mixed_reality_input_helper.cc
index 7af9904..959b85c 100644
--- a/device/vr/windows_mixed_reality/mixed_reality_input_helper.cc
+++ b/device/vr/windows_mixed_reality/mixed_reality_input_helper.cc
@@ -13,8 +13,11 @@
 #include <unordered_map>
 #include <vector>
 
+#include "base/strings/string16.h"
+#include "base/strings/utf_string_conversions.h"
 #include "device/gamepad/public/cpp/gamepads.h"
 #include "device/vr/public/mojom/isolated_xr_service.mojom.h"
+#include "device/vr/util/copy_to_ustring.h"
 #include "device/vr/windows_mixed_reality/wrappers/wmr_input_location.h"
 #include "device/vr/windows_mixed_reality/wrappers/wmr_input_manager.h"
 #include "device/vr/windows_mixed_reality/wrappers/wmr_input_source.h"
@@ -45,6 +48,10 @@
 namespace {
 constexpr double kDeadzoneMinimum = 0.1;
 
+double ApplyAxisDeadzone(double value) {
+  return std::fabs(value) < kDeadzoneMinimum ? 0 : value;
+}
+
 void AddButton(mojom::XRGamepadPtr& gamepad, ButtonData* data) {
   if (data) {
     auto button = mojom::XRGamepadButton::New();
@@ -61,13 +68,11 @@
 // These methods are only called for the thumbstick and touchpad, which both
 // have an X and Y.
 void AddAxes(mojom::XRGamepadPtr& gamepad, ButtonData data) {
-  gamepad->axes.push_back(
-      std::fabs(data.x_axis) < kDeadzoneMinimum ? 0 : data.x_axis);
-  gamepad->axes.push_back(
-      std::fabs(data.y_axis) < kDeadzoneMinimum ? 0 : data.y_axis);
+  gamepad->axes.push_back(ApplyAxisDeadzone(data.x_axis));
+  gamepad->axes.push_back(ApplyAxisDeadzone(data.y_axis));
 }
 
-void AddButtonAndAxes(mojom::XRGamepadPtr& gamepad, ButtonData data) {
+void AddButtonWithAxes(mojom::XRGamepadPtr& gamepad, ButtonData data) {
   AddButton(gamepad, &data);
   AddAxes(gamepad, data);
 }
@@ -133,11 +138,11 @@
   // use the polled button state for select here.  Voice (which we cannot get
   // via polling), lacks enough data to be considered a "Gamepad", and if we
   // used eventing the pressed state may be inconsistent.
-  AddButtonAndAxes(gamepad, input_state.button_data[ButtonName::kThumbstick]);
+  AddButtonWithAxes(gamepad, input_state.button_data[ButtonName::kThumbstick]);
   AddButton(gamepad, &input_state.button_data[ButtonName::kSelect]);
   AddButton(gamepad, &input_state.button_data[ButtonName::kGrip]);
   AddButton(gamepad, nullptr);  // Nothing seems to trigger this button in Edge.
-  AddButtonAndAxes(gamepad, input_state.button_data[ButtonName::kTouchpad]);
+  AddButtonWithAxes(gamepad, input_state.button_data[ButtonName::kTouchpad]);
 
   gamepad->pose = ConvertToVRPose(input_state.gamepad_pose);
   gamepad->hand = input_state.source_state->description->handedness;
@@ -149,6 +154,70 @@
   return gamepad;
 }
 
+GamepadHand MojoToGamepadHandedness(device::mojom::XRHandedness handedness) {
+  switch (handedness) {
+    case device::mojom::XRHandedness::LEFT:
+      return GamepadHand::kLeft;
+    case device::mojom::XRHandedness::RIGHT:
+      return GamepadHand::kRight;
+    case device::mojom::XRHandedness::NONE:
+      return GamepadHand::kNone;
+  }
+
+  NOTREACHED();
+}
+
+void AddButton(Gamepad& gamepad, ButtonData* data) {
+  DCHECK_LT(gamepad.buttons_length, Gamepad::kButtonsLengthCap);
+  if (data) {
+    gamepad.buttons[gamepad.buttons_length++] =
+        GamepadButton(data->pressed, data->touched, data->value);
+  } else {
+    gamepad.buttons[gamepad.buttons_length++] = GamepadButton();
+  }
+}
+
+void AddAxes(Gamepad& gamepad, ButtonData data) {
+  DCHECK_LT(gamepad.axes_length + 1, Gamepad::kAxesLengthCap);
+  gamepad.axes[gamepad.axes_length++] = ApplyAxisDeadzone(data.x_axis);
+  gamepad.axes[gamepad.axes_length++] = ApplyAxisDeadzone(data.y_axis);
+}
+
+void AddButtonWithAxes(Gamepad& gamepad, ButtonData data) {
+  AddButton(gamepad, &data);
+  AddAxes(gamepad, data);
+}
+
+Gamepad GetWebXRGamepad(ParsedInputState& input_state) {
+  Gamepad gamepad;
+  gamepad.connected = true;
+  gamepad.timestamp = base::TimeTicks::Now().since_origin().InMicroseconds();
+
+  // TODO(https://crbug.com/942201): Get correct ID string once WebXR spec issue
+  // #550 (https://github.com/immersive-web/webxr/issues/550) is resolved.
+  CopyToUString(base::UTF8ToUTF16("unknown"), gamepad.id,
+                base::size(gamepad.id));
+
+  CopyToUString(base::UTF8ToUTF16("xr-standard"), gamepad.mapping,
+                base::size(gamepad.mapping));
+
+  if (input_state.source_state && input_state.source_state->description) {
+    gamepad.hand = MojoToGamepadHandedness(
+        input_state.source_state->description->handedness);
+  } else {
+    gamepad.hand = GamepadHand::kNone;
+  }
+
+  // The order of these buttons is dictated by the xr-standard Gamepad mapping.
+  // Thumbstick is considered the primary 2D input axis, while the touchpad is
+  // the secondary 2D input axis.
+  AddButton(gamepad, &input_state.button_data[ButtonName::kSelect]);
+  AddButtonWithAxes(gamepad, input_state.button_data[ButtonName::kThumbstick]);
+  AddButton(gamepad, &input_state.button_data[ButtonName::kGrip]);
+  AddButtonWithAxes(gamepad, input_state.button_data[ButtonName::kTouchpad]);
+  return gamepad;
+}
+
 // Note that since this is built by polling, and so eventing changes are not
 // accounted for here.
 std::unordered_map<ButtonName, ButtonData> ParseButtonState(
@@ -348,8 +417,11 @@
   for (auto state : source_states) {
     auto parsed_source_state = LockedParseWindowsSourceState(state, origin);
 
-    if (parsed_source_state.source_state)
+    if (parsed_source_state.source_state) {
+      parsed_source_state.source_state->gamepad =
+          GetWebXRGamepad(parsed_source_state);
       input_states.push_back(std::move(parsed_source_state.source_state));
+    }
   }
 
   for (unsigned int i = 0; i < pending_voice_states_.size(); i++) {
diff --git a/extensions/browser/url_loader_factory_manager.cc b/extensions/browser/url_loader_factory_manager.cc
index 5d4b0ebb..fc242e5 100644
--- a/extensions/browser/url_loader_factory_manager.cc
+++ b/extensions/browser/url_loader_factory_manager.cc
@@ -155,6 +155,7 @@
     "7BFE588B209A15260DE12777B4BBB738DE98FE6C",
     "7C9DEE7EABBF6C722DC7C1B86460F0507E5AA561",
     "808FA9BB3CD501D7801D1CD6D5A3DBA088FDD46F",
+    "81FD24AF95679B900370DB857CE2EACADBE50A9B",
     "82FDBBF79F3517C3946BD89EAAF90C46DFDA4681",
     "83431421F759AE7A3BDAC00A4959D13095C65805",
     "834BD6E8E9F59D388DBB264453EB08A5DE45ED03",
@@ -200,6 +201,7 @@
     "AF0965B74237AFF383C981C05178732C9A05A140",
     "B3CF6C01796E8D03378FAA77AF507E27BB847E9D",
     "B4782AE831D849EFCC2AF4BE2012816EDDF8D908",
+    "B6903E9A5A8CC5D74A688DA0A67AFA2B0944F605",
     "BF5224FB246A6B67EA986EFF77A43F6C1BCA9672",
     "C0A30989F3717CE5B1B2FE462797951EA6D3922A",
     "C4A81852B9ACE6CE02DAB58BB77BDA0AD75716EC",
@@ -217,6 +219,7 @@
     "D572BE31227F6D0BE95B9430BE2D5F21D7D9CF9A",
     "D7C3879A8898618E3A23B0E6BFB6A38D01606246",
     "D9A97CD75380C697C65D37512E53DBECDFA45FB9",
+    "DC39837AC518B832FCB2D2DC1CE8BA148F54758E",
     "DC88B4C9E547F3E321B3E64CCDBD4B698116D2F4",
     "DDA21167F058A65D878DF84C3CF3FCC60B053E80",
     "E134BC4A0FF6C59CE42CC76BA6B2D6F5DC648EC4",
@@ -231,6 +234,7 @@
     "EC24668224116D19FF1A5FFAA61238B88773982C",
     "EC4A841BD03C8E5202043165188A9E060BF703A3",
     "EE4BE5F23D2E59E4713958465941EFB4A18166B7",
+    "EE711E704D4A365C4644EE4637076C81DF454EA6",
     "EF97543DC0DE66EF00D804A55DCF73E0BACB8773",
     "F1ACA279F460440E47078D91FE372212DD9B8709",
     "F273C23C616F5C56E8EDBAE24B21F5D408936A0D",
diff --git a/gpu/angle_end2end_tests_main.cc b/gpu/angle_end2end_tests_main.cc
index e6fde85..5d9353cb 100644
--- a/gpu/angle_end2end_tests_main.cc
+++ b/gpu/angle_end2end_tests_main.cc
@@ -24,8 +24,8 @@
 
 int main(int argc, char** argv) {
   base::CommandLine::Init(argc, argv);
-  testing::InitGoogleMock(&argc, argv);
   ANGLEProcessTestArgs(&argc, argv);
+  testing::InitGoogleMock(&argc, argv);
   base::TestSuite test_suite(argc, argv);
   int rt = base::LaunchUnitTestsWithOptions(
       argc, argv,
diff --git a/gpu/command_buffer/client/webgpu_implementation.cc b/gpu/command_buffer/client/webgpu_implementation.cc
index af47df4..7d875ed90 100644
--- a/gpu/command_buffer/client/webgpu_implementation.cc
+++ b/gpu/command_buffer/client/webgpu_implementation.cc
@@ -8,6 +8,7 @@
 #include <vector>
 
 #include "base/numerics/checked_math.h"
+#include "base/trace_event/trace_event.h"
 #include "gpu/command_buffer/client/gpu_control.h"
 #include "gpu/command_buffer/client/shared_memory_limits.h"
 
@@ -195,6 +196,14 @@
 void WebGPUImplementation::OnGpuControlReturnData(
     base::span<const uint8_t> data) {
 #if BUILDFLAG(USE_DAWN)
+
+  static uint32_t return_trace_id = 0;
+  TRACE_EVENT_FLOW_END0(TRACE_DISABLED_BY_DEFAULT("gpu.dawn"),
+                        "DawnReturnCommands", return_trace_id++);
+
+  TRACE_EVENT1(TRACE_DISABLED_BY_DEFAULT("gpu.dawn"),
+               "WebGPUImplementation::OnGpuControlReturnData", "bytes",
+               data.size());
   if (!wire_client_->HandleCommands(
       reinterpret_cast<const char*>(data.data()), data.size())) {
     // TODO(enga): Lose the context.
@@ -229,6 +238,8 @@
 
     uint32_t allocation_size =
         std::max(c2s_buffer_default_size_, static_cast<uint32_t>(size));
+    TRACE_EVENT1(TRACE_DISABLED_BY_DEFAULT("gpu.dawn"),
+                 "WebGPUImplementation::GetCmdSpace", "bytes", allocation_size);
     c2s_buffer_.Reset(allocation_size);
     c2s_put_offset_ = 0;
     next_offset = size;
@@ -248,6 +259,14 @@
 
 bool WebGPUImplementation::Flush() {
   if (c2s_buffer_.valid()) {
+    TRACE_EVENT1(TRACE_DISABLED_BY_DEFAULT("gpu.dawn"),
+                 "WebGPUImplementation::Flush", "bytes", c2s_put_offset_);
+
+    TRACE_EVENT_FLOW_BEGIN0(
+        TRACE_DISABLED_BY_DEFAULT("gpu.dawn"), "DawnCommands",
+        (static_cast<uint64_t>(c2s_buffer_.shm_id()) << 32) +
+            c2s_buffer_.offset());
+
     c2s_buffer_.Shrink(c2s_put_offset_);
     helper_->DawnCommands(c2s_buffer_.shm_id(), c2s_buffer_.offset(),
                           c2s_put_offset_);
diff --git a/gpu/command_buffer/service/webgpu_decoder_impl.cc b/gpu/command_buffer/service/webgpu_decoder_impl.cc
index 597d456..d5a1901 100644
--- a/gpu/command_buffer/service/webgpu_decoder_impl.cc
+++ b/gpu/command_buffer/service/webgpu_decoder_impl.cc
@@ -13,6 +13,7 @@
 
 #include "base/logging.h"
 #include "base/macros.h"
+#include "base/trace_event/trace_event.h"
 #include "gpu/command_buffer/common/mailbox.h"
 #include "gpu/command_buffer/common/webgpu_cmd_format.h"
 #include "gpu/command_buffer/common/webgpu_cmd_ids.h"
@@ -81,6 +82,13 @@
 
 bool WireServerCommandSerializer::Flush() {
   if (put_offset_ > 0) {
+    TRACE_EVENT1(TRACE_DISABLED_BY_DEFAULT("gpu.dawn"),
+                 "WireServerCommandSerializer::Flush", "bytes", put_offset_);
+
+    static uint32_t return_trace_id = 0;
+    TRACE_EVENT_FLOW_BEGIN0(TRACE_DISABLED_BY_DEFAULT("gpu.dawn"),
+                            "DawnReturnCommands", return_trace_id++);
+
     client_->HandleReturnData(base::make_span(buffer_.data(), put_offset_));
     put_offset_ = 0;
   }
@@ -171,6 +179,8 @@
   void PerformPollingWork() override {
     DCHECK(dawn_device_);
     DCHECK(wire_serializer_);
+    TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("gpu.dawn"),
+                 "WebGPUDecoderImpl::PerformPollingWork");
     dawn_procs_.deviceTick(dawn_device_);
     wire_serializer_->Flush();
   }
@@ -524,6 +534,12 @@
     return error::kOutOfBounds;
   }
 
+  TRACE_EVENT_FLOW_END0(
+      TRACE_DISABLED_BY_DEFAULT("gpu.dawn"), "DawnCommands",
+      (static_cast<uint64_t>(commands_shm_id) << 32) + commands_shm_offset);
+
+  TRACE_EVENT1(TRACE_DISABLED_BY_DEFAULT("gpu.dawn"),
+               "WebGPUDecoderImpl::HandleDawnCommands", "bytes", size);
   std::vector<char> commands(shm_commands, shm_commands + size);
   if (!wire_server_->HandleCommands(commands.data(), size)) {
     NOTREACHED();
diff --git a/gpu/vulkan/vulkan_fence_helper.cc b/gpu/vulkan/vulkan_fence_helper.cc
index 15df368..3bbafcf 100644
--- a/gpu/vulkan/vulkan_fence_helper.cc
+++ b/gpu/vulkan/vulkan_fence_helper.cc
@@ -43,7 +43,7 @@
 
 VulkanFenceHelper::FenceHandle VulkanFenceHelper::EnqueueFence(VkFence fence) {
   FenceHandle handle(fence, next_generation_++);
-  cleanup_tasks_.emplace(handle, std::move(tasks_pending_fence_));
+  cleanup_tasks_.emplace_back(handle, std::move(tasks_pending_fence_));
   tasks_pending_fence_ = std::vector<CleanupTask>();
 
   return handle;
@@ -82,12 +82,7 @@
   // |current_generation_| as far as possible. This assumes that fences pass in
   // order, which isn't a hard API guarantee, but should be close enough /
   // efficient enough for the purpose or processing cleanup tasks.
-  //
-  // Also runs any cleanup tasks for generations that have passed. Create a
-  // temporary vector of tasks to run to avoid reentrancy issues.
-  std::vector<CleanupTask> tasks_to_run;
-  while (!cleanup_tasks_.empty()) {
-    TasksForFence& tasks_for_fence = cleanup_tasks_.front();
+  for (const auto& tasks_for_fence : cleanup_tasks_) {
     VkResult result = vkGetFenceStatus(device, tasks_for_fence.handle.fence_);
     if (result == VK_NOT_READY)
       break;
@@ -96,12 +91,22 @@
       return;
     }
     current_generation_ = tasks_for_fence.handle.generation_id_;
-    vkDestroyFence(device, tasks_for_fence.handle.fence_, nullptr);
+  }
 
+  // Runs any cleanup tasks for generations that have passed. Create a temporary
+  // vector of tasks to run to avoid reentrancy issues.
+  std::vector<CleanupTask> tasks_to_run;
+  while (!cleanup_tasks_.empty()) {
+    TasksForFence& tasks_for_fence = cleanup_tasks_.front();
+    if (tasks_for_fence.handle.generation_id_ > current_generation_)
+      break;
+    DCHECK_EQ(vkGetFenceStatus(device, tasks_for_fence.handle.fence_),
+              VK_SUCCESS);
+    vkDestroyFence(device, tasks_for_fence.handle.fence_, nullptr);
     tasks_to_run.insert(tasks_to_run.end(),
                         std::make_move_iterator(tasks_for_fence.tasks.begin()),
                         std::make_move_iterator(tasks_for_fence.tasks.end()));
-    cleanup_tasks_.pop();
+    cleanup_tasks_.pop_front();
   }
 
   for (auto& task : tasks_to_run)
@@ -190,7 +195,7 @@
     tasks_to_run.insert(tasks_to_run.end(),
                         std::make_move_iterator(tasks_for_fence.tasks.begin()),
                         std::make_move_iterator(tasks_for_fence.tasks.end()));
-    cleanup_tasks_.pop();
+    cleanup_tasks_.pop_front();
   }
   tasks_to_run.insert(tasks_to_run.end(),
                       std::make_move_iterator(tasks_pending_fence_.begin()),
diff --git a/gpu/vulkan/vulkan_fence_helper.h b/gpu/vulkan/vulkan_fence_helper.h
index 5d64eee..36a7416 100644
--- a/gpu/vulkan/vulkan_fence_helper.h
+++ b/gpu/vulkan/vulkan_fence_helper.h
@@ -8,7 +8,7 @@
 #include <vulkan/vulkan.h>
 
 #include "base/callback.h"
-#include "base/containers/queue.h"
+#include "base/containers/circular_deque.h"
 #include "base/macros.h"
 #include "gpu/vulkan/vulkan_export.h"
 
@@ -115,7 +115,7 @@
     FenceHandle handle;
     std::vector<CleanupTask> tasks;
   };
-  base::queue<TasksForFence> cleanup_tasks_;
+  base::circular_deque<TasksForFence> cleanup_tasks_;
 
   DISALLOW_COPY_AND_ASSIGN(VulkanFenceHelper);
 };
diff --git a/infra/config/cr-buildbucket.cfg b/infra/config/cr-buildbucket.cfg
index 0dc291f5..0a21c593 100644
--- a/infra/config/cr-buildbucket.cfg
+++ b/infra/config/cr-buildbucket.cfg
@@ -751,12 +751,6 @@
     }
 
     builders {
-      name: "Android Cronet Builder"
-      mixins: "android-ci"
-      dimensions: "os:Ubuntu-14.04"
-    }
-
-    builders {
       name: "Android FYI 32 dEQP Vk Release (Pixel 2)"
       mixins: "android-gpu-fyi-ci"
     }
@@ -2147,12 +2141,6 @@
       mixins: "fuzz-ci"
     }
     builders {
-      name: "Android Cronet Marshmallow 64bit Builder"
-      dimensions: "os:Ubuntu-14.04"
-      dimensions: "device_os:MMB29Q"
-      mixins: "android-ci"
-    }
-    builders {
       name: "Win7"
       dimensions: "os:Windows-7"
       mixins: "chromedriver-ci"
@@ -2294,12 +2282,6 @@
       dimensions: "os:Ubuntu-14.04"
       mixins: "memory-ci"
     }
-    builders {
-      name: "Android Cronet Lollipop Builder"
-      dimensions: "os:Ubuntu-14.04"
-      dimensions: "device_os:LMY48I"
-      mixins: "android-ci"
-    }
     # TODO(crbug.com/888810): Remove once these bots are migrated.
     builders {
       name: "android-dbg"
@@ -2344,21 +2326,11 @@
       mixins: "fuzz-ci"
     }
     builders {
-      name: "Android Cronet x86 Builder"
-      dimensions: "os:Ubuntu-14.04"
-      mixins: "android-ci"
-    }
-    builders {
       name: "Linux remote_run Tester"
       dimensions: "os:Ubuntu-14.04"
       mixins: "fyi-ci"
     }
     builders {
-      name: "Android Cronet ARM64 Builder"
-      dimensions: "os:Ubuntu-14.04"
-      mixins: "android-ci"
-    }
-    builders {
       name: "WebKit Mac10.13 (retina)"
       dimensions: "os:Mac-10.13"
       mixins: "mac-ci"
@@ -2377,11 +2349,6 @@
       mixins: "fyi-ci"
     }
     builders {
-      name: "Android Cronet Builder Asan"
-      dimensions: "os:Ubuntu-14.04"
-      mixins: "android-ci"
-    }
-    builders {
       name: "Linux MSan Tests"
       dimensions: "os:Ubuntu-14.04"
       mixins: "memory-ci"
@@ -2500,11 +2467,6 @@
       dimensions: "cores:4"
       mixins: "fyi-ci"
     }
-    builders {
-      name: "Android Cronet KitKat Builder"
-      dimensions: "os:Ubuntu-14.04"
-      mixins: "android-fyi-ci"
-    }
     # TODO(crbug.com/888810): Remove once these bots are migrated.
     builders {
       name: "linux-dbg"
@@ -2589,11 +2551,6 @@
       mixins: "memory-ci"
     }
     builders {
-      name: "Android Cronet Marshmallow 64bit Perf"
-      dimensions: "os:Ubuntu-14.04"
-      mixins: "android-ci"
-    }
-    builders {
       name: "chromeos-amd64-generic-rel-goma-canary"
       dimensions: "os:Ubuntu-14.04"
       mixins: "fyi-ci"
@@ -2636,25 +2593,10 @@
       mixins: "fyi-ci"
     }
     builders {
-      name: "Android Cronet Builder (dbg)"
-      dimensions: "os:Ubuntu-14.04"
-      mixins: "android-fyi-ci"
-    }
-    builders {
-      name: "Android Cronet ARM64 Builder (dbg)"
-      dimensions: "os:Ubuntu-14.04"
-      mixins: "android-ci"
-    }
-    builders {
       name: "Win ASan Release Media"
       dimensions: "os:Windows-10"
       mixins: "fuzz-ci"
     }
-    builders {
-      name: "Android Cronet x86 Builder (dbg)"
-      dimensions: "os:Ubuntu-14.04"
-      mixins: "android-ci"
-    }
     # TODO(crbug.com/888810): Remove once these bots are migrated.
     builders {
       name: "mac-dbg"
diff --git a/infra/config/luci-milo.cfg b/infra/config/luci-milo.cfg
index 2d06be8..112fe1b 100644
--- a/infra/config/luci-milo.cfg
+++ b/infra/config/luci-milo.cfg
@@ -1430,120 +1430,60 @@
   refs: "refs/heads/master"
   manifest_name: "REVISION"
   builders {
-    name: "buildbucket/luci.chromium.ci/Android Cronet Builder"
-    category: "cronet"
-    short_name: "rel"
-  }
-  builders {
-    name: "buildbot/chromium.android/Android Cronet Builder (dbg)"
-    name: "buildbucket/luci.chromium.ci/Android Cronet Builder (dbg)"
-    category: "cronet"
-    short_name: "dbg"
-  }
-  builders {
-    name: "buildbot/chromium.android/Android Cronet Builder Asan"
-    name: "buildbucket/luci.chromium.ci/Android Cronet Builder Asan"
-    category: "cronet"
-    short_name: "asn"
-  }
-  builders {
-    name: "buildbot/chromium.android/Android Cronet KitKat Builder"
-    name: "buildbucket/luci.chromium.ci/Android Cronet KitKat Builder"
-    category: "cronet"
-    short_name: "kit"
-  }
-  builders {
-    name: "buildbot/chromium.android/Android Cronet Lollipop Builder"
-    name: "buildbucket/luci.chromium.ci/Android Cronet Lollipop Builder"
-    category: "cronet"
-    short_name: "lol"
-  }
-  builders {
-    name: "buildbot/chromium.android/Android Cronet Marshmallow 64bit Builder"
-    name: "buildbucket/luci.chromium.ci/Android Cronet Marshmallow 64bit Builder"
-    category: "cronet"
-    short_name: "mar"
-  }
-  builders {
-    name: "buildbot/chromium.android/Android Cronet Marshmallow 64bit Perf"
-    name: "buildbucket/luci.chromium.ci/Android Cronet Marshmallow 64bit Perf"
-    category: "cronet"
-    short_name: "prf"
-  }
-  builders {
-    name: "buildbot/chromium.android/Android Cronet ARM64 Builder"
-    name: "buildbucket/luci.chromium.ci/Android Cronet ARM64 Builder"
-    category: "cronet|arm64"
-    short_name: "rel"
-  }
-  builders {
-    name: "buildbot/chromium.android/Android Cronet ARM64 Builder (dbg)"
-    name: "buildbucket/luci.chromium.ci/Android Cronet ARM64 Builder (dbg)"
-    category: "cronet|arm64"
-    short_name: "dbg"
-  }
-  builders {
-    name: "buildbot/chromium.android/Android Cronet x86 Builder"
-    name: "buildbucket/luci.chromium.ci/Android Cronet x86 Builder"
-    category: "cronet|x86"
-    short_name: "rel"
-  }
-  builders {
-    name: "buildbot/chromium.android/Android Cronet x86 Builder (dbg)"
-    name: "buildbucket/luci.chromium.ci/Android Cronet x86 Builder (dbg)"
-    category: "cronet|x86"
-    short_name: "dbg"
-  }
-  builders {
     name: "buildbucket/luci.chromium.ci/android-cronet-arm-dbg"
-    category: "cronet|luci|arm"
+    category: "cronet|arm"
     short_name: "dbg"
   }
   builders {
     name: "buildbucket/luci.chromium.ci/android-cronet-arm-rel"
-    category: "cronet|luci|arm"
+    category: "cronet|arm"
     short_name: "rel"
   }
   builders {
     name: "buildbucket/luci.chromium.ci/android-cronet-arm64-dbg"
-    category: "cronet|luci|arm64"
+    category: "cronet|arm64"
     short_name: "dbg"
   }
   builders {
     name: "buildbucket/luci.chromium.ci/android-cronet-arm64-rel"
-    category: "cronet|luci|arm64"
+    category: "cronet|arm64"
     short_name: "rel"
   }
   builders {
-    name: "buildbucket/luci.chromium.ci/android-cronet-asan-arm-rel"
-    category: "cronet|luci|asan"
-  }
-  builders {
-    name: "buildbucket/luci.chromium.ci/android-cronet-kitkat-arm-rel"
-    category: "cronet|luci|test"
-    short_name: "k"
-  }
-  builders {
-    name: "buildbucket/luci.chromium.ci/android-cronet-lollipop-arm-rel"
-    category: "cronet|luci|test"
-    short_name: "l"
-  }
-  builders {
-    name: "buildbucket/luci.chromium.ci/android-cronet-marshmallow-arm64-rel"
-    category: "cronet|luci|test"
-    short_name: "m"
-  }
-  builders {
     name: "buildbucket/luci.chromium.ci/android-cronet-x86-dbg"
-    category: "cronet|luci|x86"
+    category: "cronet|x86"
     short_name: "dbg"
   }
   builders {
     name: "buildbucket/luci.chromium.ci/android-cronet-x86-rel"
-    category: "cronet|luci|x86"
+    category: "cronet|x86"
     short_name: "rel"
   }
   builders {
+    name: "buildbucket/luci.chromium.ci/android-cronet-asan-arm-rel"
+    category: "cronet|asan"
+  }
+  builders {
+    name: "buildbucket/luci.chromium.ci/android-cronet-kitkat-arm-rel"
+    category: "cronet|test"
+    short_name: "k"
+  }
+  builders {
+    name: "buildbucket/luci.chromium.ci/android-cronet-lollipop-arm-rel"
+    category: "cronet|test"
+    short_name: "l"
+  }
+  builders {
+    name: "buildbucket/luci.chromium.ci/android-cronet-marshmallow-arm64-rel"
+    category: "cronet|test"
+    short_name: "m"
+  }
+  builders {
+    name: "buildbot/chromium.android/Android Cronet Marshmallow 64bit Perf"
+    category: "cronet|buildbot"
+    short_name: "prf"
+  }
+  builders {
     name: "buildbucket/luci.chromium.ci/android-jumbo-rel"
     category: "builder"
   }
@@ -1691,18 +1631,6 @@
     category: "Memory"
   }
   builders {
-    name: "buildbot/chromium.android.fyi/Android Cronet Builder (dbg)"
-    name: "buildbucket/luci.chromium.ci/Android Cronet Builder (dbg)"
-    category: "Cronet"
-    short_name: "dbg"
-  }
-  builders {
-    name: "buildbot/chromium.android.fyi/Android Cronet KitKat Builder"
-    name: "buildbucket/luci.chromium.ci/Android Cronet KitKat Builder"
-    category: "Cronet"
-    short_name: "K"
-  }
-  builders {
     name: "buildbucket/luci.chromium.ci/Android Tests (trial)(dbg)"
   }
   builders {
diff --git a/infra/config/luci-scheduler.cfg b/infra/config/luci-scheduler.cfg
index 9b7a4f5..55dfb686 100644
--- a/infra/config/luci-scheduler.cfg
+++ b/infra/config/luci-scheduler.cfg
@@ -71,7 +71,6 @@
   triggers: "Android Builder (dbg) Goma Canary"
   triggers: "Android Builder (dbg) Goma Latest Client"
   triggers: "Android CFI"
-  triggers: "Android Cronet Builder"
   triggers: "Android FYI 32 Vk Release (Pixel 2)"
   triggers: "Android FYI 32 Vk Release (Pixel XL)"
   triggers: "Android FYI 32 dEQP Vk Release (Pixel 2)"
@@ -451,16 +450,6 @@
 }
 
 job {
-  id: "Android Cronet Builder"
-  acl_sets: "default"
-  buildbucket: {
-    server: "cr-buildbucket.appspot.com"
-    bucket: "luci.chromium.ci"
-    builder: "Android Cronet Builder"
-  }
-}
-
-job {
   id: "Android FYI 32 dEQP Vk Release (Pixel 2)"
   acl_sets: "default"
   buildbucket: {
@@ -3340,106 +3329,6 @@
 }
 
 job {
-  id: "Android Cronet ARM64 Builder"
-  acl_sets: "default"
-  buildbucket: {
-    server: "cr-buildbucket.appspot.com"
-    bucket: "luci.chromium.ci"
-    builder: "Android Cronet ARM64 Builder"
-  }
-}
-
-job {
-  id: "Android Cronet ARM64 Builder (dbg)"
-  acl_sets: "default"
-  buildbucket: {
-    server: "cr-buildbucket.appspot.com"
-    bucket: "luci.chromium.ci"
-    builder: "Android Cronet ARM64 Builder (dbg)"
-  }
-}
-
-job {
-  id: "Android Cronet Builder (dbg)"
-  acl_sets: "default"
-  buildbucket: {
-    server: "cr-buildbucket.appspot.com"
-    bucket: "luci.chromium.ci"
-    builder: "Android Cronet Builder (dbg)"
-  }
-}
-
-job {
-  id: "Android Cronet Builder Asan"
-  acl_sets: "default"
-  buildbucket: {
-    server: "cr-buildbucket.appspot.com"
-    bucket: "luci.chromium.ci"
-    builder: "Android Cronet Builder Asan"
-  }
-}
-
-job {
-  id: "Android Cronet KitKat Builder"
-  acl_sets: "default"
-  buildbucket: {
-    server: "cr-buildbucket.appspot.com"
-    bucket: "luci.chromium.ci"
-    builder: "Android Cronet KitKat Builder"
-  }
-}
-
-job {
-  id: "Android Cronet Lollipop Builder"
-  acl_sets: "default"
-  buildbucket: {
-    server: "cr-buildbucket.appspot.com"
-    bucket: "luci.chromium.ci"
-    builder: "Android Cronet Lollipop Builder"
-  }
-}
-
-job {
-  id: "Android Cronet Marshmallow 64bit Builder"
-  acl_sets: "default"
-  buildbucket: {
-    server: "cr-buildbucket.appspot.com"
-    bucket: "luci.chromium.ci"
-    builder: "Android Cronet Marshmallow 64bit Builder"
-  }
-}
-
-job {
-  id: "Android Cronet Marshmallow 64bit Perf"
-  acl_sets: "default"
-  buildbucket: {
-    server: "cr-buildbucket.appspot.com"
-    bucket: "luci.chromium.ci"
-    builder: "Android Cronet Marshmallow 64bit Perf"
-  }
-}
-
-job {
-  id: "Android Cronet x86 Builder"
-  acl_sets: "default"
-  buildbucket: {
-    server: "cr-buildbucket.appspot.com"
-    bucket: "luci.chromium.ci"
-    builder: "Android Cronet x86 Builder"
-  }
-}
-
-job {
-  id: "Android Cronet x86 Builder (dbg)"
-  acl_sets: "default"
-  buildbucket: {
-    server: "cr-buildbucket.appspot.com"
-    bucket: "luci.chromium.ci"
-    builder: "Android Cronet x86 Builder (dbg)"
-  }
-}
-
-job {
   id: "Chromium Mac 10.13"
   acl_sets: "default"
   buildbucket: {
diff --git a/ios/chrome/browser/ui/tab_grid/tab_grid_mediator.mm b/ios/chrome/browser/ui/tab_grid/tab_grid_mediator.mm
index bc4a91e..435b2f37f 100644
--- a/ios/chrome/browser/ui/tab_grid/tab_grid_mediator.mm
+++ b/ios/chrome/browser/ui/tab_grid/tab_grid_mediator.mm
@@ -64,11 +64,6 @@
 
 // Returns the ID of the active tab in |web_state_list|.
 NSString* GetActiveTabId(WebStateList* web_state_list) {
-  // TODO(crbug.com/877792) : Real-world crashes have been caused by
-  // |web_state_list| being nil in this function. Capture histogram to retain
-  // visibility of issue severity.
-  UMA_HISTOGRAM_BOOLEAN("IOS.TabGridMediator.GetActiveTabIDNilWebStateList",
-                        !web_state_list);
   if (!web_state_list)
     return nil;
 
@@ -214,11 +209,6 @@
 - (void)webStateList:(WebStateList*)webStateList
     didDetachWebState:(web::WebState*)webState
               atIndex:(int)index {
-  // TODO(crbug.com/877792) : Real-world crashes have been caused by
-  // |webStateList| being nil in this callback. Capture histogram to retain
-  // visibility of issue severity.
-  UMA_HISTOGRAM_BOOLEAN("IOS.TabGridMediator.DidDetachNilWebStateList",
-                        !webStateList);
   if (!webStateList)
     return;
   TabIdTabHelper* tabHelper = TabIdTabHelper::FromWebState(webState);
diff --git a/ios/web/net/BUILD.gn b/ios/web/net/BUILD.gn
index 2ebaf6a6..1d7f111 100644
--- a/ios/web/net/BUILD.gn
+++ b/ios/web/net/BUILD.gn
@@ -31,8 +31,6 @@
     "crw_ssl_status_updater.mm",
     "request_group_util.h",
     "request_group_util.mm",
-    "request_tracker_factory_impl.h",
-    "request_tracker_factory_impl.mm",
     "request_tracker_impl.h",
     "request_tracker_impl.mm",
   ]
diff --git a/ios/web/net/request_tracker_factory_impl.h b/ios/web/net/request_tracker_factory_impl.h
deleted file mode 100644
index 3ac0e37..0000000
--- a/ios/web/net/request_tracker_factory_impl.h
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright 2014 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef IOS_WEB_NET_REQUEST_TRACKER_FACTORY_IMPL_H_
-#define IOS_WEB_NET_REQUEST_TRACKER_FACTORY_IMPL_H_
-
-#include <string>
-
-#import "ios/net/request_tracker.h"
-
-namespace web {
-
-class RequestTrackerFactoryImpl
-    : public net::RequestTracker::RequestTrackerFactory {
- public:
-  explicit RequestTrackerFactoryImpl(const std::string& application_scheme);
-  ~RequestTrackerFactoryImpl() override;
-
- private:
-  // RequestTracker::RequestTrackerFactory implementation
-  bool GetRequestTracker(NSURLRequest* request,
-                         base::WeakPtr<net::RequestTracker>* tracker) override;
-
-  NSString* application_scheme_;
-};
-
-}  // namespace web
-
-#endif  // IOS_WEB_NET_REQUEST_TRACKER_FACTORY_IMPL_H_
diff --git a/ios/web/net/request_tracker_factory_impl.mm b/ios/web/net/request_tracker_factory_impl.mm
deleted file mode 100644
index 74b36f4..0000000
--- a/ios/web/net/request_tracker_factory_impl.mm
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright 2014 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#import "ios/web/net/request_tracker_factory_impl.h"
-
-#include "base/logging.h"
-#include "base/memory/weak_ptr.h"
-#include "base/strings/sys_string_conversions.h"
-#import "ios/web/net/request_group_util.h"
-#import "ios/web/net/request_tracker_impl.h"
-
-#if !defined(__has_feature) || !__has_feature(objc_arc)
-#error "This file requires ARC support."
-#endif
-
-namespace web {
-
-RequestTrackerFactoryImpl::RequestTrackerFactoryImpl(
-    const std::string& application_scheme) {
-  if (!application_scheme.empty()) {
-    application_scheme_ = [base::SysUTF8ToNSString(application_scheme) copy];
-    DCHECK(application_scheme_);
-  }
-}
-
-RequestTrackerFactoryImpl::~RequestTrackerFactoryImpl() {
-}
-
-bool RequestTrackerFactoryImpl::GetRequestTracker(
-    NSURLRequest* request,
-    base::WeakPtr<net::RequestTracker>* tracker) {
-  DCHECK(tracker);
-  DCHECK(!tracker->get());
-  NSString* request_group_id =
-      web::ExtractRequestGroupIDFromRequest(request, application_scheme_);
-  if (!request_group_id) {
-    // There was no request_group_id, so the request was from something like a
-    // data: or file: URL.
-    return true;
-  }
-  RequestTrackerImpl* tracker_impl =
-      RequestTrackerImpl::GetTrackerForRequestGroupID(request_group_id);
-  if (tracker_impl)
-    *tracker = tracker_impl->GetWeakPtr();
-  // If there is a request group ID, but no associated tracker, return false.
-  // This usually happens when the tab has been closed, but can maybe also
-  // happen in other cases (see http://crbug.com/228397).
-  return tracker->get() != nullptr;
-}
-
-}  // namespace web
diff --git a/media/base/BUILD.gn b/media/base/BUILD.gn
index e746dc1..229bcf0 100644
--- a/media/base/BUILD.gn
+++ b/media/base/BUILD.gn
@@ -285,10 +285,10 @@
     "video_frame_pool.h",
     "video_renderer.cc",
     "video_renderer.h",
-    "video_rotation.cc",
-    "video_rotation.h",
     "video_thumbnail_decoder.cc",
     "video_thumbnail_decoder.h",
+    "video_transformation.cc",
+    "video_transformation.h",
     "video_types.cc",
     "video_types.h",
     "video_util.cc",
diff --git a/media/base/demuxer_stream.h b/media/base/demuxer_stream.h
index 3888102a..fff2f3a 100644
--- a/media/base/demuxer_stream.h
+++ b/media/base/demuxer_stream.h
@@ -9,7 +9,7 @@
 #include "base/memory/ref_counted.h"
 #include "base/time/time.h"
 #include "media/base/media_export.h"
-#include "media/base/video_rotation.h"
+#include "media/base/video_transformation.h"
 
 namespace media {
 
diff --git a/media/base/fake_demuxer_stream.cc b/media/base/fake_demuxer_stream.cc
index 736f694..f71aff3 100644
--- a/media/base/fake_demuxer_stream.cc
+++ b/media/base/fake_demuxer_stream.cc
@@ -158,7 +158,7 @@
   const gfx::Rect kVisibleRect(kStartWidth, kStartHeight);
   video_decoder_config_.Initialize(
       kCodecVP8, VIDEO_CODEC_PROFILE_UNKNOWN, PIXEL_FORMAT_I420,
-      VideoColorSpace(), VIDEO_ROTATION_0, next_coded_size_, kVisibleRect,
+      VideoColorSpace(), kNoTransformation, next_coded_size_, kVisibleRect,
       next_coded_size_, EmptyExtraData(),
       is_encrypted_ ? AesCtrEncryptionScheme() : Unencrypted());
   next_coded_size_.Enlarge(kWidthDelta, kHeightDelta);
diff --git a/media/base/ipc/media_param_traits_macros.h b/media/base/ipc/media_param_traits_macros.h
index 6590f05..e093ed8 100644
--- a/media/base/ipc/media_param_traits_macros.h
+++ b/media/base/ipc/media_param_traits_macros.h
@@ -32,7 +32,7 @@
 #include "media/base/subsample_entry.h"
 #include "media/base/video_codecs.h"
 #include "media/base/video_color_space.h"
-#include "media/base/video_rotation.h"
+#include "media/base/video_transformation.h"
 #include "media/base/video_types.h"
 #include "media/base/waiting.h"
 #include "media/base/watch_time_keys.h"
diff --git a/media/base/pipeline.h b/media/base/pipeline.h
index 7b96cb2..da27057 100644
--- a/media/base/pipeline.h
+++ b/media/base/pipeline.h
@@ -20,7 +20,7 @@
 #include "media/base/ranges.h"
 #include "media/base/text_track.h"
 #include "media/base/video_decoder_config.h"
-#include "media/base/video_rotation.h"
+#include "media/base/video_transformation.h"
 #include "media/base/waiting.h"
 #include "ui/gfx/geometry/size.h"
 
diff --git a/media/base/pipeline_impl.cc b/media/base/pipeline_impl.cc
index 686b2131..6bf5f84 100644
--- a/media/base/pipeline_impl.cc
+++ b/media/base/pipeline_impl.cc
@@ -953,7 +953,7 @@
         if (stream->type() == DemuxerStream::VIDEO && !metadata.has_video) {
           metadata.has_video = true;
           metadata.natural_size = GetRotatedVideoSize(
-              stream->video_decoder_config().video_rotation(),
+              stream->video_decoder_config().video_transformation().rotation,
               stream->video_decoder_config().natural_size());
           metadata.video_decoder_config = stream->video_decoder_config();
         }
diff --git a/media/base/pipeline_metadata.h b/media/base/pipeline_metadata.h
index 1838222..4f34ff8 100644
--- a/media/base/pipeline_metadata.h
+++ b/media/base/pipeline_metadata.h
@@ -8,7 +8,7 @@
 #include "base/time/time.h"
 #include "media/base/audio_decoder_config.h"
 #include "media/base/video_decoder_config.h"
-#include "media/base/video_rotation.h"
+#include "media/base/video_transformation.h"
 #include "ui/gfx/geometry/size.h"
 
 namespace media {
diff --git a/media/base/test_helpers.cc b/media/base/test_helpers.cc
index dff8401..80a26ca 100644
--- a/media/base/test_helpers.cc
+++ b/media/base/test_helpers.cc
@@ -136,8 +136,9 @@
   gfx::Size natural_size = coded_size;
 
   return VideoDecoderConfig(
-      codec, profile, PIXEL_FORMAT_I420, color_space, rotation, coded_size,
-      visible_rect, natural_size, EmptyExtraData(),
+      codec, profile, PIXEL_FORMAT_I420, color_space,
+      VideoTransformation(rotation), coded_size, visible_rect, natural_size,
+      EmptyExtraData(),
       is_encrypted ? AesCtrEncryptionScheme() : Unencrypted());
 }
 
diff --git a/media/base/video_decoder_config.cc b/media/base/video_decoder_config.cc
index f514abe..23aa265e 100644
--- a/media/base/video_decoder_config.cc
+++ b/media/base/video_decoder_config.cc
@@ -62,14 +62,14 @@
     : codec_(kUnknownVideoCodec),
       profile_(VIDEO_CODEC_PROFILE_UNKNOWN),
       format_(PIXEL_FORMAT_UNKNOWN),
-      rotation_(VIDEO_ROTATION_0) {}
+      transformation_(kNoTransformation) {}
 
 VideoDecoderConfig::VideoDecoderConfig(
     VideoCodec codec,
     VideoCodecProfile profile,
     VideoPixelFormat format,
     const VideoColorSpace& color_space,
-    VideoRotation rotation,
+    VideoTransformation rotation,
     const gfx::Size& coded_size,
     const gfx::Rect& visible_rect,
     const gfx::Size& natural_size,
@@ -105,7 +105,7 @@
                                     VideoCodecProfile profile,
                                     VideoPixelFormat format,
                                     const VideoColorSpace& color_space,
-                                    VideoRotation rotation,
+                                    VideoTransformation transformation,
                                     const gfx::Size& coded_size,
                                     const gfx::Rect& visible_rect,
                                     const gfx::Size& natural_size,
@@ -114,7 +114,7 @@
   codec_ = codec;
   profile_ = profile;
   format_ = format;
-  rotation_ = rotation;
+  transformation_ = transformation;
   coded_size_ = coded_size;
   visible_rect_ = visible_rect;
   natural_size_ = natural_size;
@@ -133,7 +133,7 @@
 bool VideoDecoderConfig::Matches(const VideoDecoderConfig& config) const {
   return ((codec() == config.codec()) && (format() == config.format()) &&
           (profile() == config.profile()) &&
-          (video_rotation() == config.video_rotation()) &&
+          (video_transformation() == config.video_transformation()) &&
           (coded_size() == config.coded_size()) &&
           (visible_rect() == config.visible_rect()) &&
           (natural_size() == config.natural_size()) &&
@@ -154,7 +154,8 @@
     << natural_size().height() << "]"
     << ", has extra data: " << (extra_data().empty() ? "false" : "true")
     << ", encryption scheme: " << encryption_scheme()
-    << ", rotation: " << VideoRotationToString(video_rotation())
+    << ", rotation: " << VideoRotationToString(video_transformation().rotation)
+    << ", flipped: " << video_transformation().mirrored
     << ", color space: " << color_space_info().ToGfxColorSpace().ToString();
   if (hdr_metadata().has_value()) {
     s << std::setprecision(4) << ", luminance range: "
diff --git a/media/base/video_decoder_config.h b/media/base/video_decoder_config.h
index bd65211..ef65fee 100644
--- a/media/base/video_decoder_config.h
+++ b/media/base/video_decoder_config.h
@@ -17,7 +17,7 @@
 #include "media/base/media_export.h"
 #include "media/base/video_codecs.h"
 #include "media/base/video_color_space.h"
-#include "media/base/video_rotation.h"
+#include "media/base/video_transformation.h"
 #include "media/base/video_types.h"
 #include "ui/gfx/geometry/rect.h"
 #include "ui/gfx/geometry/size.h"
@@ -41,7 +41,7 @@
                      VideoCodecProfile profile,
                      VideoPixelFormat format,
                      const VideoColorSpace& color_space,
-                     VideoRotation rotation,
+                     VideoTransformation transformation,
                      const gfx::Size& coded_size,
                      const gfx::Rect& visible_rect,
                      const gfx::Size& natural_size,
@@ -57,7 +57,7 @@
                   VideoCodecProfile profile,
                   VideoPixelFormat format,
                   const VideoColorSpace& color_space,
-                  VideoRotation rotation,
+                  VideoTransformation transformation,
                   const gfx::Size& coded_size,
                   const gfx::Rect& visible_rect,
                   const gfx::Size& natural_size,
@@ -94,7 +94,7 @@
   // scaling to natural_size().
   //
   // TODO(sandersd): Which direction is orientation measured in?
-  VideoRotation video_rotation() const { return rotation_; }
+  VideoTransformation video_transformation() const { return transformation_; }
 
   // Deprecated. TODO(wolenetz): Remove. See https://crbug.com/665539.
   // Width and height of video frame immediately post-decode. Not all pixels
@@ -154,7 +154,7 @@
 
   VideoPixelFormat format_;
 
-  VideoRotation rotation_;
+  VideoTransformation transformation_;
 
   // Deprecated. TODO(wolenetz): Remove. See https://crbug.com/665539.
   gfx::Size coded_size_;
diff --git a/media/base/video_decoder_config_unittest.cc b/media/base/video_decoder_config_unittest.cc
index 33d849e..3c7b4e3 100644
--- a/media/base/video_decoder_config_unittest.cc
+++ b/media/base/video_decoder_config_unittest.cc
@@ -18,7 +18,7 @@
 TEST(VideoDecoderConfigTest, Invalid_UnsupportedPixelFormat) {
   VideoDecoderConfig config(kCodecVP8, VIDEO_CODEC_PROFILE_UNKNOWN,
                             PIXEL_FORMAT_UNKNOWN, VideoColorSpace(),
-                            VIDEO_ROTATION_0, kCodedSize, kVisibleRect,
+                            kNoTransformation, kCodedSize, kVisibleRect,
                             kNaturalSize, EmptyExtraData(), Unencrypted());
   EXPECT_FALSE(config.IsValidConfig());
 }
@@ -26,7 +26,7 @@
 TEST(VideoDecoderConfigTest, Invalid_AspectRatioNumeratorZero) {
   gfx::Size natural_size = GetNaturalSize(kVisibleRect.size(), 0, 1);
   VideoDecoderConfig config(kCodecVP8, VP8PROFILE_ANY, kVideoFormat,
-                            VideoColorSpace(), VIDEO_ROTATION_0, kCodedSize,
+                            VideoColorSpace(), kNoTransformation, kCodedSize,
                             kVisibleRect, natural_size, EmptyExtraData(),
                             Unencrypted());
   EXPECT_FALSE(config.IsValidConfig());
@@ -35,7 +35,7 @@
 TEST(VideoDecoderConfigTest, Invalid_AspectRatioDenominatorZero) {
   gfx::Size natural_size = GetNaturalSize(kVisibleRect.size(), 1, 0);
   VideoDecoderConfig config(kCodecVP8, VP8PROFILE_ANY, kVideoFormat,
-                            VideoColorSpace(), VIDEO_ROTATION_0, kCodedSize,
+                            VideoColorSpace(), kNoTransformation, kCodedSize,
                             kVisibleRect, natural_size, EmptyExtraData(),
                             Unencrypted());
   EXPECT_FALSE(config.IsValidConfig());
@@ -44,7 +44,7 @@
 TEST(VideoDecoderConfigTest, Invalid_AspectRatioNumeratorNegative) {
   gfx::Size natural_size = GetNaturalSize(kVisibleRect.size(), -1, 1);
   VideoDecoderConfig config(kCodecVP8, VP8PROFILE_ANY, kVideoFormat,
-                            VideoColorSpace(), VIDEO_ROTATION_0, kCodedSize,
+                            VideoColorSpace(), kNoTransformation, kCodedSize,
                             kVisibleRect, natural_size, EmptyExtraData(),
                             Unencrypted());
   EXPECT_FALSE(config.IsValidConfig());
@@ -53,7 +53,7 @@
 TEST(VideoDecoderConfigTest, Invalid_AspectRatioDenominatorNegative) {
   gfx::Size natural_size = GetNaturalSize(kVisibleRect.size(), 1, -1);
   VideoDecoderConfig config(kCodecVP8, VP8PROFILE_ANY, kVideoFormat,
-                            VideoColorSpace(), VIDEO_ROTATION_0, kCodedSize,
+                            VideoColorSpace(), kNoTransformation, kCodedSize,
                             kVisibleRect, natural_size, EmptyExtraData(),
                             Unencrypted());
   EXPECT_FALSE(config.IsValidConfig());
@@ -64,7 +64,7 @@
   int num = ceil(static_cast<double>(limits::kMaxDimension + 1) / width);
   gfx::Size natural_size = GetNaturalSize(kVisibleRect.size(), num, 1);
   VideoDecoderConfig config(kCodecVP8, VP8PROFILE_ANY, kVideoFormat,
-                            VideoColorSpace(), VIDEO_ROTATION_0, kCodedSize,
+                            VideoColorSpace(), kNoTransformation, kCodedSize,
                             kVisibleRect, natural_size, EmptyExtraData(),
                             Unencrypted());
   EXPECT_FALSE(config.IsValidConfig());
@@ -78,7 +78,7 @@
   EXPECT_EQ(320, natural_size.width());
   EXPECT_EQ(240 * 641, natural_size.height());
   VideoDecoderConfig config(kCodecVP8, VP8PROFILE_ANY, kVideoFormat,
-                            VideoColorSpace(), VIDEO_ROTATION_0, kCodedSize,
+                            VideoColorSpace(), kNoTransformation, kCodedSize,
                             kVisibleRect, natural_size, EmptyExtraData(),
                             Unencrypted());
   EXPECT_FALSE(config.IsValidConfig());
diff --git a/media/base/video_frame_metadata.h b/media/base/video_frame_metadata.h
index 4ba35f0..86207131 100644
--- a/media/base/video_frame_metadata.h
+++ b/media/base/video_frame_metadata.h
@@ -15,7 +15,7 @@
 #include "base/values.h"
 #include "build/build_config.h"
 #include "media/base/media_export.h"
-#include "media/base/video_rotation.h"
+#include "media/base/video_transformation.h"
 
 namespace gfx {
 class Rect;
diff --git a/media/base/video_rotation.cc b/media/base/video_rotation.cc
deleted file mode 100644
index accfd55..0000000
--- a/media/base/video_rotation.cc
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright 2017 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "media/base/video_rotation.h"
-
-#include "base/logging.h"
-
-namespace media {
-
-std::string VideoRotationToString(VideoRotation rotation) {
-  switch (rotation) {
-    case VIDEO_ROTATION_0:
-      return "0°";
-    case VIDEO_ROTATION_90:
-      return "90°";
-    case VIDEO_ROTATION_180:
-      return "180°";
-    case VIDEO_ROTATION_270:
-      return "270°";
-  }
-  NOTREACHED();
-  return "";
-}
-
-}  // namespace media
diff --git a/media/base/video_rotation.h b/media/base/video_rotation.h
deleted file mode 100644
index 05690ae..0000000
--- a/media/base/video_rotation.h
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright 2014 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef MEDIA_BASE_VIDEO_ROTATION_H_
-#define MEDIA_BASE_VIDEO_ROTATION_H_
-
-#include <string>
-
-namespace media {
-
-// Enumeration to represent 90 degree video rotation for MP4 videos
-// where it can be rotated by 90 degree intervals.
-enum VideoRotation : int {
-  VIDEO_ROTATION_0 = 0,
-  VIDEO_ROTATION_90,
-  VIDEO_ROTATION_180,
-  VIDEO_ROTATION_270,
-  VIDEO_ROTATION_MAX = VIDEO_ROTATION_270
-};
-
-std::string VideoRotationToString(VideoRotation rotation);
-
-}  // namespace media
-
-#endif  // MEDIA_BASE_VIDEO_ROTATION_H_
diff --git a/media/base/video_thumbnail_decoder_unittest.cc b/media/base/video_thumbnail_decoder_unittest.cc
index 1f64291..a898b1c 100644
--- a/media/base/video_thumbnail_decoder_unittest.cc
+++ b/media/base/video_thumbnail_decoder_unittest.cc
@@ -35,7 +35,7 @@
     mock_video_decoder_ = mock_video_decoder.get();
     VideoDecoderConfig valid_config(
         kCodecVP8, VP8PROFILE_ANY, PIXEL_FORMAT_I420, VideoColorSpace(),
-        VIDEO_ROTATION_0, gfx::Size(1, 1), gfx::Rect(1, 1), gfx::Size(1, 1),
+        kNoTransformation, gfx::Size(1, 1), gfx::Rect(1, 1), gfx::Size(1, 1),
         EmptyExtraData(), Unencrypted());
 
     thumbnail_decoder_ = std::make_unique<VideoThumbnailDecoder>(
diff --git a/media/base/video_transformation.cc b/media/base/video_transformation.cc
new file mode 100644
index 0000000..4ba3658
--- /dev/null
+++ b/media/base/video_transformation.cc
@@ -0,0 +1,95 @@
+// 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 "media/base/video_transformation.h"
+
+#include <math.h>
+#include <stddef.h>
+
+#include "base/logging.h"
+
+namespace media {
+namespace {
+
+double FixedToFloatingPoint(int32_t i) {
+  return static_cast<double>(i >> 16);
+}
+
+}  // namespace
+
+std::string VideoRotationToString(VideoRotation rotation) {
+  switch (rotation) {
+    case VIDEO_ROTATION_0:
+      return "0°";
+    case VIDEO_ROTATION_90:
+      return "90°";
+    case VIDEO_ROTATION_180:
+      return "180°";
+    case VIDEO_ROTATION_270:
+      return "270°";
+  }
+  NOTREACHED();
+  return "";
+}
+
+bool operator==(const struct VideoTransformation& first,
+                const struct VideoTransformation& second) {
+  return first.rotation == second.rotation && first.mirrored == second.mirrored;
+}
+
+VideoTransformation::VideoTransformation(int32_t matrix[4]) {
+  // Rotation by angle Θ is represented in the matrix as:
+  // [ cos(Θ), -sin(Θ)]
+  // [ sin(Θ),  cos(Θ)]
+  // A vertical flip is represented by the cosine's having opposite signs
+  // and a horizontal flip is represented by the sine's having the same sign.
+
+  // Check the matrix for validity
+  if (abs(matrix[0]) != abs(matrix[3]) || abs(matrix[1]) != abs(matrix[2])) {
+    rotation = VIDEO_ROTATION_0;
+    mirrored = false;
+    return;
+  }
+
+  double angle = acos(FixedToFloatingPoint(matrix[0])) * 180 / base::kPiDouble;
+
+  // Calculate angle offsets for rotation - rotating about the X axis
+  // can be expressed as a 180 degree rotation and a Y axis rotation
+  mirrored = false;
+  if (matrix[0] != matrix[3] && matrix[0] != 0) {
+    mirrored = !mirrored;
+    angle += 180;
+  }
+
+  if (matrix[1] == matrix[3] && matrix[1] != 0) {
+    mirrored = !mirrored;
+  }
+
+  // Normalize the angle
+  while (angle < 0)
+    angle += 360;
+
+  while (angle >= 360)
+    angle -= 360;
+
+  // 16 bits of fixed point decimal is enough to give 6 decimals of precision
+  // to cos(Θ). A delta of ±0.000001 causes acos(cos(Θ)) to differ by a minimum
+  // of 0.0002, which is why we only need to check that the angle is only
+  // accurate to within four decimal places. This is preferred to checking for
+  // a more precise accuracy, as the 'double' type is architecture dependent and
+  // there may be variance in floating point errors.
+  if (abs(angle - 0) < 1e-4) {
+    rotation = VIDEO_ROTATION_0;
+  } else if (abs(angle - 180) < 1e-4) {
+    rotation = VIDEO_ROTATION_180;
+  } else if (abs(angle - 90) < 1e-4) {
+    bool quadrant = asin(FixedToFloatingPoint(matrix[2])) < 0;
+    rotation = quadrant ? VIDEO_ROTATION_90 : VIDEO_ROTATION_270;
+  } else {
+    rotation = VIDEO_ROTATION_0;
+    mirrored = false;
+  }
+}
+
+}  // namespace media
diff --git a/media/base/video_transformation.h b/media/base/video_transformation.h
new file mode 100644
index 0000000..eff89cd
--- /dev/null
+++ b/media/base/video_transformation.h
@@ -0,0 +1,61 @@
+// 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 MEDIA_BASE_VIDEO_TRANSFORMATION_H_
+#define MEDIA_BASE_VIDEO_TRANSFORMATION_H_
+
+#include <string>
+
+#include "base/numerics/math_constants.h"
+#include "media/base/media_export.h"
+
+namespace media {
+
+// Enumeration to represent 90 degree video rotation for MP4 videos
+// where it can be rotated by 90 degree intervals.
+enum VideoRotation : int {
+  VIDEO_ROTATION_0 = 0,
+  VIDEO_ROTATION_90,
+  VIDEO_ROTATION_180,
+  VIDEO_ROTATION_270,
+  VIDEO_ROTATION_MAX = VIDEO_ROTATION_270
+};
+
+// Stores frame rotation & mirroring values. These are usually calculated from
+// a rotation matrix from a demuxer, and we only support 90 degree rotation
+// increments.
+struct MEDIA_EXPORT VideoTransformation {
+  constexpr VideoTransformation(VideoRotation rotation, bool mirrored)
+      : rotation(rotation), mirrored(mirrored) {}
+  constexpr VideoTransformation(VideoRotation r)
+      : VideoTransformation(r, false) {}
+  constexpr VideoTransformation()
+      : VideoTransformation(VIDEO_ROTATION_0, false) {}
+
+  // Rotation by angle Θ is represented in the matrix as:
+  // [ cos(Θ), -sin(Θ)]
+  // [ sin(Θ),  cos(Θ)]
+  // A vertical flip is represented by the cosine's having opposite signs
+  // and a horizontal flip is represented by the sine's having the same sign.
+  VideoTransformation(int32_t matrix[4]);
+
+  // The video rotation value, in 90 degree steps.
+  VideoRotation rotation;
+
+  // Whether the video should be flipped about its Y axis.
+  // This transformation takes place _after_ rotation, since they are not
+  // commutative.
+  bool mirrored;
+};
+
+MEDIA_EXPORT bool operator==(const struct VideoTransformation& first,
+                             const struct VideoTransformation& second);
+
+constexpr VideoTransformation kNoTransformation = VideoTransformation();
+
+std::string VideoRotationToString(VideoRotation rotation);
+
+}  // namespace media
+
+#endif  // MEDIA_BASE_VIDEO_TRANSFORMATION_H_
diff --git a/media/blink/video_decode_stats_reporter_unittest.cc b/media/blink/video_decode_stats_reporter_unittest.cc
index e9d11ca..bc41f4c 100644
--- a/media/blink/video_decode_stats_reporter_unittest.cc
+++ b/media/blink/video_decode_stats_reporter_unittest.cc
@@ -48,7 +48,7 @@
   gfx::Size coded_size = natural_size;
   gfx::Rect visible_rect(coded_size.width(), coded_size.height());
   return VideoDecoderConfig(codec, profile, PIXEL_FORMAT_I420,
-                            VideoColorSpace::JPEG(), VIDEO_ROTATION_0,
+                            VideoColorSpace::JPEG(), kNoTransformation,
                             coded_size, visible_rect, natural_size,
                             EmptyExtraData(), Unencrypted());
 }
diff --git a/media/blink/webmediaplayer_impl.cc b/media/blink/webmediaplayer_impl.cc
index 184b38eb4..a628daf 100644
--- a/media/blink/webmediaplayer_impl.cc
+++ b/media/blink/webmediaplayer_impl.cc
@@ -1214,7 +1214,7 @@
   }
   video_renderer_.Paint(
       video_frame, canvas, gfx::RectF(gfx_rect), flags,
-      pipeline_metadata_.video_decoder_config.video_rotation(),
+      pipeline_metadata_.video_decoder_config.video_transformation(),
       context_provider_.get());
 }
 
@@ -1776,9 +1776,10 @@
   pipeline_metadata_ = metadata;
 
   SetReadyState(WebMediaPlayer::kReadyStateHaveMetadata);
-  UMA_HISTOGRAM_ENUMERATION("Media.VideoRotation",
-                            metadata.video_decoder_config.video_rotation(),
-                            VIDEO_ROTATION_MAX + 1);
+  UMA_HISTOGRAM_ENUMERATION(
+      "Media.VideoRotation",
+      metadata.video_decoder_config.video_transformation().rotation,
+      VIDEO_ROTATION_MAX + 1);
 
   if (HasAudio()) {
     RecordEncryptionScheme("Audio",
@@ -1808,9 +1809,11 @@
       ActivateSurfaceLayerForVideo();
     } else {
       DCHECK(!video_layer_);
+      // TODO(tmathmeyer) does this need support for reflections as well?
       video_layer_ = cc::VideoLayer::Create(
           compositor_.get(),
-          pipeline_metadata_.video_decoder_config.video_rotation());
+          pipeline_metadata_.video_decoder_config.video_transformation()
+              .rotation);
       video_layer_->SetContentsOpaque(opaque_);
       client_->SetCcLayer(video_layer_.get());
     }
@@ -1844,14 +1847,17 @@
                 .Run(this, compositor_->GetUpdateSubmissionStateCallback());
   bridge_->CreateSurfaceLayer();
 
+  // TODO(tmathmeyer) does this need support for the reflection transformation
+  // as well?
   vfc_task_runner_->PostTask(
       FROM_HERE,
-      base::BindOnce(&VideoFrameCompositor::EnableSubmission,
-                     base::Unretained(compositor_.get()),
-                     bridge_->GetSurfaceId(),
-                     bridge_->GetLocalSurfaceIdAllocationTime(),
-                     pipeline_metadata_.video_decoder_config.video_rotation(),
-                     IsInPictureInPicture()));
+      base::BindOnce(
+          &VideoFrameCompositor::EnableSubmission,
+          base::Unretained(compositor_.get()), bridge_->GetSurfaceId(),
+          bridge_->GetLocalSurfaceIdAllocationTime(),
+          pipeline_metadata_.video_decoder_config.video_transformation()
+              .rotation,
+          IsInPictureInPicture()));
   bridge_->SetContentsOpaque(opaque_);
 
   // If the element is already in Picture-in-Picture mode, it means that it
@@ -2107,7 +2113,8 @@
   // The input |size| is from the decoded video frame, which is the original
   // natural size and need to be rotated accordingly.
   gfx::Size rotated_size = GetRotatedVideoSize(
-      pipeline_metadata_.video_decoder_config.video_rotation(), size);
+      pipeline_metadata_.video_decoder_config.video_transformation().rotation,
+      size);
 
   RecordVideoNaturalSize(rotated_size);
 
@@ -3114,8 +3121,8 @@
 }
 
 bool WebMediaPlayerImpl::DoesOverlaySupportMetadata() const {
-  return pipeline_metadata_.video_decoder_config.video_rotation() ==
-         VIDEO_ROTATION_0;
+  return pipeline_metadata_.video_decoder_config.video_transformation() ==
+         kNoTransformation;
 }
 
 void WebMediaPlayerImpl::ActivateViewportIntersectionMonitoring(bool activate) {
diff --git a/media/cast/sender/h264_vt_encoder_unittest.cc b/media/cast/sender/h264_vt_encoder_unittest.cc
index 6441d90..814cbc4 100644
--- a/media/cast/sender/h264_vt_encoder_unittest.cc
+++ b/media/cast/sender/h264_vt_encoder_unittest.cc
@@ -303,7 +303,7 @@
 TEST_F(H264VideoToolboxEncoderTest, DISABLED_CheckFramesAreDecodable) {
   VideoDecoderConfig config(
       kCodecH264, H264PROFILE_MAIN, frame_->format(), VideoColorSpace(),
-      VIDEO_ROTATION_0, frame_->coded_size(), frame_->visible_rect(),
+      kNoTransformation, frame_->coded_size(), frame_->visible_rect(),
       frame_->natural_size(), EmptyExtraData(), Unencrypted());
   scoped_refptr<EndToEndFrameChecker> checker(new EndToEndFrameChecker(config));
 
diff --git a/media/cdm/library_cdm/clear_key_cdm/cdm_video_decoder.cc b/media/cdm/library_cdm/clear_key_cdm/cdm_video_decoder.cc
index fcc8375..9504b1f 100644
--- a/media/cdm/library_cdm/clear_key_cdm/cdm_video_decoder.cc
+++ b/media/cdm/library_cdm/clear_key_cdm/cdm_video_decoder.cc
@@ -55,8 +55,7 @@
   VideoDecoderConfig media_config(
       ToMediaVideoCodec(config.codec), ToMediaVideoCodecProfile(config.profile),
       ToMediaVideoFormat(config.format), ToMediaColorSpace(config.color_space),
-      VideoRotation::VIDEO_ROTATION_0, coded_size, gfx::Rect(coded_size),
-      coded_size,
+      kNoTransformation, coded_size, gfx::Rect(coded_size), coded_size,
       std::vector<uint8_t>(config.extra_data,
                            config.extra_data + config.extra_data_size),
       Unencrypted());
diff --git a/media/ffmpeg/ffmpeg_common.cc b/media/ffmpeg/ffmpeg_common.cc
index 142e992..1d5d0851 100644
--- a/media/ffmpeg/ffmpeg_common.cc
+++ b/media/ffmpeg/ffmpeg_common.cc
@@ -621,8 +621,10 @@
     extra_data.assign(codec_context->extradata,
                       codec_context->extradata + codec_context->extradata_size);
   }
-  config->Initialize(codec, profile, format, color_space, video_rotation,
-                     coded_size, visible_rect, natural_size, extra_data,
+  // TODO(tmathmeyer) ffmpeg can't provide us with an actual video rotation yet.
+  config->Initialize(codec, profile, format, color_space,
+                     VideoTransformation(video_rotation), coded_size,
+                     visible_rect, natural_size, extra_data,
                      GetEncryptionScheme(stream));
 
   if (stream->nb_side_data) {
diff --git a/media/filters/ffmpeg_demuxer_unittest.cc b/media/filters/ffmpeg_demuxer_unittest.cc
index cada939..db87c5f 100644
--- a/media/filters/ffmpeg_demuxer_unittest.cc
+++ b/media/filters/ffmpeg_demuxer_unittest.cc
@@ -1288,7 +1288,7 @@
   ASSERT_TRUE(stream);
 
   const VideoDecoderConfig& video_config = stream->video_decoder_config();
-  ASSERT_EQ(VIDEO_ROTATION_0, video_config.video_rotation());
+  ASSERT_EQ(VIDEO_ROTATION_0, video_config.video_transformation().rotation);
 }
 
 TEST_F(FFmpegDemuxerTest, Rotate_Metadata_90) {
@@ -1299,7 +1299,7 @@
   ASSERT_TRUE(stream);
 
   const VideoDecoderConfig& video_config = stream->video_decoder_config();
-  ASSERT_EQ(VIDEO_ROTATION_90, video_config.video_rotation());
+  ASSERT_EQ(VIDEO_ROTATION_90, video_config.video_transformation().rotation);
 }
 
 TEST_F(FFmpegDemuxerTest, Rotate_Metadata_180) {
@@ -1310,7 +1310,7 @@
   ASSERT_TRUE(stream);
 
   const VideoDecoderConfig& video_config = stream->video_decoder_config();
-  ASSERT_EQ(VIDEO_ROTATION_180, video_config.video_rotation());
+  ASSERT_EQ(VIDEO_ROTATION_180, video_config.video_transformation().rotation);
 }
 
 TEST_F(FFmpegDemuxerTest, Rotate_Metadata_270) {
@@ -1321,7 +1321,7 @@
   ASSERT_TRUE(stream);
 
   const VideoDecoderConfig& video_config = stream->video_decoder_config();
-  ASSERT_EQ(VIDEO_ROTATION_270, video_config.video_rotation());
+  ASSERT_EQ(VIDEO_ROTATION_270, video_config.video_transformation().rotation);
 }
 
 TEST_F(FFmpegDemuxerTest, NaturalSizeWithoutPASP) {
diff --git a/media/filters/ffmpeg_video_decoder_unittest.cc b/media/filters/ffmpeg_video_decoder_unittest.cc
index dcc31bf..1a8fdfa 100644
--- a/media/filters/ffmpeg_video_decoder_unittest.cc
+++ b/media/filters/ffmpeg_video_decoder_unittest.cc
@@ -227,7 +227,7 @@
 TEST_F(FFmpegVideoDecoderTest, Initialize_OpenDecoderFails) {
   // Specify Theora w/o extra data so that avcodec_open2() fails.
   VideoDecoderConfig config(kCodecTheora, VIDEO_CODEC_PROFILE_UNKNOWN,
-                            kVideoFormat, VideoColorSpace(), VIDEO_ROTATION_0,
+                            kVideoFormat, VideoColorSpace(), kNoTransformation,
                             kCodedSize, kVisibleRect, kNaturalSize,
                             EmptyExtraData(), Unencrypted());
   InitializeWithConfigWithResult(config, false);
diff --git a/media/filters/source_buffer_state_unittest.cc b/media/filters/source_buffer_state_unittest.cc
index 2f3139ea..d0777f3 100644
--- a/media/filters/source_buffer_state_unittest.cc
+++ b/media/filters/source_buffer_state_unittest.cc
@@ -37,7 +37,7 @@
   gfx::Rect visible_rect(size);
   return VideoDecoderConfig(codec, VIDEO_CODEC_PROFILE_UNKNOWN,
                             PIXEL_FORMAT_I420, VideoColorSpace::REC709(),
-                            VIDEO_ROTATION_0, size, visible_rect, size,
+                            kNoTransformation, size, visible_rect, size,
                             EmptyExtraData(), Unencrypted());
 }
 
diff --git a/media/filters/vpx_video_decoder_fuzzertest.cc b/media/filters/vpx_video_decoder_fuzzertest.cc
index abe5145..d784d58 100644
--- a/media/filters/vpx_video_decoder_fuzzertest.cc
+++ b/media/filters/vpx_video_decoder_fuzzertest.cc
@@ -95,8 +95,10 @@
   auto coded_size = gfx::Size(1 + (rng() % 127), 1 + (rng() % 127));
   auto visible_rect = gfx::Rect(coded_size);
   auto natural_size = gfx::Size(1 + (rng() % 127), 1 + (rng() % 127));
+  uint8_t reflection = rng() % 4;
 
-  VideoDecoderConfig config(codec, profile, pixel_format, color_space, rotation,
+  VideoDecoderConfig config(codec, profile, pixel_format, color_space,
+                            VideoTransformation(rotation, reflection),
                             coded_size, visible_rect, natural_size,
                             EmptyExtraData(), Unencrypted());
 
diff --git a/media/formats/mp2t/es_adapter_video_unittest.cc b/media/formats/mp2t/es_adapter_video_unittest.cc
index 3c078a2..6ae3659 100644
--- a/media/formats/mp2t/es_adapter_video_unittest.cc
+++ b/media/formats/mp2t/es_adapter_video_unittest.cc
@@ -33,7 +33,7 @@
   gfx::Rect visible_rect(0, 0, 320, 240);
   gfx::Size natural_size(320, 240);
   return VideoDecoderConfig(kCodecH264, H264PROFILE_MAIN, PIXEL_FORMAT_I420,
-                            VideoColorSpace(), VIDEO_ROTATION_0, coded_size,
+                            VideoColorSpace(), kNoTransformation, coded_size,
                             visible_rect, natural_size, EmptyExtraData(),
                             Unencrypted());
 }
diff --git a/media/formats/mp2t/es_parser_h264.cc b/media/formats/mp2t/es_parser_h264.cc
index 41d85555..29e577b7 100644
--- a/media/formats/mp2t/es_parser_h264.cc
+++ b/media/formats/mp2t/es_parser_h264.cc
@@ -527,7 +527,7 @@
 
   VideoDecoderConfig video_decoder_config(
       kCodecH264, profile, PIXEL_FORMAT_I420, VideoColorSpace::REC709(),
-      VIDEO_ROTATION_0, coded_size.value(), visible_rect.value(), natural_size,
+      kNoTransformation, coded_size.value(), visible_rect.value(), natural_size,
       EmptyExtraData(), scheme);
 
   if (!video_decoder_config.IsValidConfig()) {
diff --git a/media/formats/mp4/mp4_stream_parser.cc b/media/formats/mp4/mp4_stream_parser.cc
index 623bc53..14848c7 100644
--- a/media/formats/mp4/mp4_stream_parser.cc
+++ b/media/formats/mp4/mp4_stream_parser.cc
@@ -245,12 +245,9 @@
   return ParseResult::kOk;
 }
 
-static inline double FixedToFloatingPoint(const int32_t& i) {
-  return static_cast<double>(i >> 16);
-}
-
-VideoRotation MP4StreamParser::CalculateRotation(const TrackHeader& track,
-                                                 const MovieHeader& movie) {
+VideoTransformation MP4StreamParser::CalculateRotation(
+    const TrackHeader& track,
+    const MovieHeader& movie) {
   static_assert(kDisplayMatrixDimension == 9, "Display matrix must be 3x3");
   // 3x3 matrix: [ a b c ]
   //             [ d e f ]
@@ -275,39 +272,9 @@
     }
   }
 
-  // Rotation by angle Θ is represented in the matrix as:
-  // [ cos(Θ), -sin(Θ), ...]
-  // [ sin(Θ),  cos(Θ), ...]
-  // [ ...,     ...,     1 ]
-  // But we only need cos(Θ) for the angle and sin(Θ) for the quadrant.
-  double angle = acos(FixedToFloatingPoint(rotation_matrix[0]))
-    * 180 / base::kPiDouble;
-
-  if (angle < 0)
-    angle += 360;
-
-  if (angle >= 360)
-    angle -= 360;
-
-  // 16 bits of fixed point decimal is enough to give 6 decimals of precision
-  // to cos(Θ). A delta of ±0.000001 causes acos(cos(Θ)) to differ by a minimum
-  // of 0.0002, which is why we only need to check that the angle is only
-  // accurate to within four decimal places. This is preferred to checking for
-  // a more precise accuracy, as the 'double' type is architecture dependant and
-  // ther may variance in floating point errors.
-  if (abs(angle - 0) < 1e-4)
-    return VIDEO_ROTATION_0;
-
-  if (abs(angle - 180) < 1e-4)
-    return VIDEO_ROTATION_180;
-
-  if (abs(angle - 90) < 1e-4) {
-    bool quadrant = asin(FixedToFloatingPoint(rotation_matrix[3])) < 0;
-    return quadrant ? VIDEO_ROTATION_90 : VIDEO_ROTATION_270;
-  }
-
-  // TODO(tmathmeyer): Record this event and the faulty matrix somewhere.
-  return VIDEO_ROTATION_0;
+  int32_t rotation_only[4] = {rotation_matrix[0], rotation_matrix[1],
+                              rotation_matrix[3], rotation_matrix[4]};
+  return VideoTransformation(rotation_only);
 }
 
 bool MP4StreamParser::ParseMoov(BoxReader* reader) {
diff --git a/media/formats/mp4/mp4_stream_parser.h b/media/formats/mp4/mp4_stream_parser.h
index 909bf55..86ef746 100644
--- a/media/formats/mp4/mp4_stream_parser.h
+++ b/media/formats/mp4/mp4_stream_parser.h
@@ -52,8 +52,8 @@
   bool Parse(const uint8_t* buf, int size) override;
 
   // Calculates the rotation value from the track header display matricies.
-  VideoRotation CalculateRotation(const TrackHeader& track,
-                                  const MovieHeader& movie);
+  VideoTransformation CalculateRotation(const TrackHeader& track,
+                                        const MovieHeader& movie);
 
  private:
   enum State {
diff --git a/media/formats/mp4/mp4_stream_parser_unittest.cc b/media/formats/mp4/mp4_stream_parser_unittest.cc
index afd3e55..7b9cf6b 100644
--- a/media/formats/mp4/mp4_stream_parser_unittest.cc
+++ b/media/formats/mp4/mp4_stream_parser_unittest.cc
@@ -738,7 +738,8 @@
 }
 
 // <cos(θ), sin(θ), θ expressed as a rotation Enum>
-using MatrixRotationTestCaseParam = std::tuple<double, double, VideoRotation>;
+using MatrixRotationTestCaseParam =
+    std::tuple<double, double, VideoTransformation>;
 
 class MP4StreamParserRotationMatrixEvaluatorTest
     : public ::testing::TestWithParam<MatrixRotationTestCaseParam> {
@@ -771,17 +772,23 @@
   track_header.display_matrix[1] = -(std::get<1>(data) * (1 << 16));
   track_header.display_matrix[3] = std::get<1>(data) * (1 << 16);
 
-  EXPECT_EQ(parser_->CalculateRotation(track_header, movie_header),
-            std::get<2>(data));
+  VideoTransformation expected = std::get<2>(data);
+  VideoTransformation actual =
+      parser_->CalculateRotation(track_header, movie_header);
+  EXPECT_EQ(actual.rotation, expected.rotation);
+  EXPECT_EQ(actual.mirrored, expected.mirrored);
 }
 
 MatrixRotationTestCaseParam rotation_test_cases[6] = {
-    {1, 0, VIDEO_ROTATION_0},     // cos(0)  = 1, sin(0)  = 0
-    {0, -1, VIDEO_ROTATION_90},   // cos(90) = 0, sin(90) =-1
-    {-1, 0, VIDEO_ROTATION_180},  // cos(180)=-1, sin(180)= 0
-    {0, 1, VIDEO_ROTATION_270},   // cos(270)= 0, sin(270)= 1
-    {1, 1, VIDEO_ROTATION_0},     // Error case
-    {5, 5, VIDEO_ROTATION_0},     // Error case
+    {1, 0, VideoTransformation(VIDEO_ROTATION_0)},  // cos(0)  = 1, sin(0)  = 0
+    {0, -1,
+     VideoTransformation(VIDEO_ROTATION_90)},  // cos(90) = 0, sin(90) =-1
+    {-1, 0,
+     VideoTransformation(VIDEO_ROTATION_180)},  // cos(180)=-1, sin(180)= 0
+    {0, 1,
+     VideoTransformation(VIDEO_ROTATION_270)},      // cos(270)= 0, sin(270)= 1
+    {1, 1, VideoTransformation(VIDEO_ROTATION_0)},  // Error case
+    {5, 5, VideoTransformation(VIDEO_ROTATION_0)},  // Error case
 };
 INSTANTIATE_TEST_SUITE_P(CheckMath,
                          MP4StreamParserRotationMatrixEvaluatorTest,
diff --git a/media/formats/webm/webm_video_client.cc b/media/formats/webm/webm_video_client.cc
index 231723bb..d2e5b8c8 100644
--- a/media/formats/webm/webm_video_client.cc
+++ b/media/formats/webm/webm_video_client.cc
@@ -128,7 +128,7 @@
     config->set_hdr_metadata(color_metadata.hdr_metadata);
   }
   config->Initialize(video_codec, profile, format, color_space,
-                     VIDEO_ROTATION_0, coded_size, visible_rect, natural_size,
+                     kNoTransformation, coded_size, visible_rect, natural_size,
                      codec_private, encryption_scheme);
   return config->IsValidConfig();
 }
diff --git a/media/formats/webm/webm_video_client_unittest.cc b/media/formats/webm/webm_video_client_unittest.cc
index dd9e33da..d9f2dd7 100644
--- a/media/formats/webm/webm_video_client_unittest.cc
+++ b/media/formats/webm/webm_video_client_unittest.cc
@@ -58,7 +58,7 @@
 
   VideoDecoderConfig expected_config(
       kCodecVP9, profile, PIXEL_FORMAT_I420, VideoColorSpace::REC709(),
-      VIDEO_ROTATION_0, kCodedSize, gfx::Rect(kCodedSize), kCodedSize,
+      kNoTransformation, kCodedSize, gfx::Rect(kCodedSize), kCodedSize,
       codec_private, Unencrypted());
 
   EXPECT_TRUE(config.Matches(expected_config))
diff --git a/media/gpu/android/android_video_surface_chooser.h b/media/gpu/android/android_video_surface_chooser.h
index c5254c6e..3b88a20 100644
--- a/media/gpu/android/android_video_surface_chooser.h
+++ b/media/gpu/android/android_video_surface_chooser.h
@@ -10,7 +10,7 @@
 #include "base/memory/weak_ptr.h"
 #include "base/optional.h"
 #include "media/base/android/android_overlay.h"
-#include "media/base/video_rotation.h"
+#include "media/base/video_transformation.h"
 #include "media/gpu/media_gpu_export.h"
 #include "ui/gfx/geometry/rect.h"
 
diff --git a/media/gpu/android/media_codec_video_decoder.cc b/media/gpu/android/media_codec_video_decoder.cc
index a4f2675..a9f588c5 100644
--- a/media/gpu/android/media_codec_video_decoder.cc
+++ b/media/gpu/android/media_codec_video_decoder.cc
@@ -279,7 +279,8 @@
   }
   decoder_config_ = config;
 
-  surface_chooser_helper_.SetVideoRotation(decoder_config_.video_rotation());
+  surface_chooser_helper_.SetVideoRotation(
+      decoder_config_.video_transformation().rotation);
 
   output_cb_ = output_cb;
   waiting_cb_ = waiting_cb;
diff --git a/media/gpu/android/surface_chooser_helper.h b/media/gpu/android/surface_chooser_helper.h
index 5d77f8ad..a019d03 100644
--- a/media/gpu/android/surface_chooser_helper.h
+++ b/media/gpu/android/surface_chooser_helper.h
@@ -9,7 +9,7 @@
 
 #include "base/macros.h"
 #include "base/time/time.h"
-#include "media/base/video_rotation.h"
+#include "media/base/video_transformation.h"
 #include "media/gpu/android/android_video_surface_chooser.h"
 #include "media/gpu/android/promotion_hint_aggregator.h"
 #include "media/gpu/media_gpu_export.h"
diff --git a/media/gpu/ipc/service/vda_video_decoder_unittest.cc b/media/gpu/ipc/service/vda_video_decoder_unittest.cc
index c7c79fe..4e34ed6 100644
--- a/media/gpu/ipc/service/vda_video_decoder_unittest.cc
+++ b/media/gpu/ipc/service/vda_video_decoder_unittest.cc
@@ -23,7 +23,7 @@
 #include "media/base/simple_sync_token_client.h"
 #include "media/base/video_codecs.h"
 #include "media/base/video_frame.h"
-#include "media/base/video_rotation.h"
+#include "media/base/video_transformation.h"
 #include "media/base/video_types.h"
 #include "media/gpu/ipc/service/picture_buffer_manager.h"
 #include "media/gpu/test/fake_command_buffer_helper.h"
@@ -142,7 +142,7 @@
     EXPECT_CALL(init_cb_, Run(true));
     InitializeWithConfig(VideoDecoderConfig(
         kCodecVP9, VP9PROFILE_PROFILE0, PIXEL_FORMAT_I420,
-        VideoColorSpace::REC709(), VIDEO_ROTATION_0, gfx::Size(1920, 1088),
+        VideoColorSpace::REC709(), kNoTransformation, gfx::Size(1920, 1088),
         gfx::Rect(1920, 1080), gfx::Size(1920, 1080), EmptyExtraData(),
         Unencrypted()));
     RunUntilIdle();
@@ -319,7 +319,7 @@
 TEST_P(VdaVideoDecoderTest, Initialize_UnsupportedSize) {
   InitializeWithConfig(
       VideoDecoderConfig(kCodecVP9, VP9PROFILE_PROFILE0, PIXEL_FORMAT_I420,
-                         VideoColorSpace::REC601(), VIDEO_ROTATION_0,
+                         VideoColorSpace::REC601(), kNoTransformation,
                          gfx::Size(320, 240), gfx::Rect(320, 240),
                          gfx::Size(320, 240), EmptyExtraData(), Unencrypted()));
   EXPECT_CALL(init_cb_, Run(false));
@@ -329,7 +329,7 @@
 TEST_P(VdaVideoDecoderTest, Initialize_UnsupportedCodec) {
   InitializeWithConfig(VideoDecoderConfig(
       kCodecH264, H264PROFILE_BASELINE, PIXEL_FORMAT_I420,
-      VideoColorSpace::REC709(), VIDEO_ROTATION_0, gfx::Size(1920, 1088),
+      VideoColorSpace::REC709(), kNoTransformation, gfx::Size(1920, 1088),
       gfx::Rect(1920, 1080), gfx::Size(1920, 1080), EmptyExtraData(),
       Unencrypted()));
   EXPECT_CALL(init_cb_, Run(false));
@@ -340,7 +340,7 @@
   EXPECT_CALL(*vda_, Initialize(_, vdavd_.get())).WillOnce(Return(false));
   InitializeWithConfig(VideoDecoderConfig(
       kCodecVP9, VP9PROFILE_PROFILE0, PIXEL_FORMAT_I420,
-      VideoColorSpace::REC709(), VIDEO_ROTATION_0, gfx::Size(1920, 1088),
+      VideoColorSpace::REC709(), kNoTransformation, gfx::Size(1920, 1088),
       gfx::Rect(1920, 1080), gfx::Size(1920, 1080), EmptyExtraData(),
       Unencrypted()));
   EXPECT_CALL(init_cb_, Run(false));
@@ -423,7 +423,7 @@
       .WillOnce(Return(GetParam()));
   InitializeWithConfig(VideoDecoderConfig(
       kCodecVP9, VP9PROFILE_PROFILE0, PIXEL_FORMAT_I420,
-      VideoColorSpace::REC709(), VIDEO_ROTATION_0, gfx::Size(640, 480),
+      VideoColorSpace::REC709(), kNoTransformation, gfx::Size(640, 480),
       gfx::Rect(640, 480), gfx::Size(1280, 480), EmptyExtraData(),
       Unencrypted()));
   EXPECT_CALL(init_cb_, Run(true));
diff --git a/media/gpu/test/video_player/video_decoder_client.cc b/media/gpu/test/video_player/video_decoder_client.cc
index bb58de63..ef02976 100644
--- a/media/gpu/test/video_player/video_decoder_client.cc
+++ b/media/gpu/test/video_player/video_decoder_client.cc
@@ -143,7 +143,7 @@
 
   VideoDecoderConfig config(
       video_->Codec(), video_->Profile(), PIXEL_FORMAT_I420, VideoColorSpace(),
-      VIDEO_ROTATION_0, video_->Resolution(), gfx::Rect(video_->Resolution()),
+      kNoTransformation, video_->Resolution(), gfx::Rect(video_->Resolution()),
       video_->Resolution(), std::vector<uint8_t>(0), EncryptionScheme());
 
   VideoDecoder::InitCB init_cb = BindToCurrentLoop(base::BindRepeating(
diff --git a/media/gpu/video_encode_accelerator_unittest.cc b/media/gpu/video_encode_accelerator_unittest.cc
index 221b179e..ab1cf895 100644
--- a/media/gpu/video_encode_accelerator_unittest.cc
+++ b/media/gpu/video_encode_accelerator_unittest.cc
@@ -1003,17 +1003,17 @@
   VideoDecoderConfig config;
   if (IsVP8(profile_)) {
     config.Initialize(kCodecVP8, VP8PROFILE_ANY, pixel_format_,
-                      VideoColorSpace(), VIDEO_ROTATION_0, coded_size,
+                      VideoColorSpace(), kNoTransformation, coded_size,
                       visible_size, natural_size, EmptyExtraData(),
                       Unencrypted());
   } else if (IsVP9(profile_)) {
     config.Initialize(kCodecVP9, VP9PROFILE_PROFILE0, pixel_format_,
-                      VideoColorSpace(), VIDEO_ROTATION_0, coded_size,
+                      VideoColorSpace(), kNoTransformation, coded_size,
                       visible_size, natural_size, EmptyExtraData(),
                       Unencrypted());
   } else if (IsH264(profile_)) {
     config.Initialize(kCodecH264, H264PROFILE_MAIN, pixel_format_,
-                      VideoColorSpace(), VIDEO_ROTATION_0, coded_size,
+                      VideoColorSpace(), kNoTransformation, coded_size,
                       visible_size, natural_size, EmptyExtraData(),
                       Unencrypted());
   } else {
diff --git a/media/mojo/interfaces/media_types.mojom b/media/mojo/interfaces/media_types.mojom
index 31b47d6a..cfae8698 100644
--- a/media/mojo/interfaces/media_types.mojom
+++ b/media/mojo/interfaces/media_types.mojom
@@ -58,10 +58,16 @@
 [Native]
 enum VideoPixelFormat;
 
-// See media/base/video_rotation.h for descriptions.
+// See media/base/video_transformation.h for descriptions.
 [Native]
 enum VideoRotation;
 
+// See media/base/video_transformation.h for descriptions.
+struct VideoTransformation {
+  VideoRotation rotation;
+  bool mirrored;
+};
+
 // See media/base/waiting.h for descriptions.
 [Native]
 enum WaitingReason;
@@ -156,7 +162,7 @@
   VideoCodec codec;
   VideoCodecProfile profile;
   VideoPixelFormat format;
-  VideoRotation video_rotation;
+  VideoTransformation transformation;
   gfx.mojom.Size coded_size;
   gfx.mojom.Rect visible_rect;
   gfx.mojom.Size natural_size;
diff --git a/media/mojo/interfaces/media_types.typemap b/media/mojo/interfaces/media_types.typemap
index 65ea50f..4488db03e 100644
--- a/media/mojo/interfaces/media_types.typemap
+++ b/media/mojo/interfaces/media_types.typemap
@@ -21,19 +21,27 @@
   "//media/base/sample_format.h",
   "//media/base/subsample_entry.h",
   "//media/base/video_codecs.h",
-  "//media/base/video_rotation.h",
+  "//media/base/video_transformation.h",
   "//media/base/video_types.h",
   "//media/base/waiting.h",
   "//media/base/watch_time_keys.h",
 ]
 
-traits_headers = [ "//media/base/ipc/media_param_traits_macros.h" ]
+traits_headers = [
+  "//media/base/ipc/media_param_traits_macros.h",
+  "//media/mojo/interfaces/video_transformation_mojom_traits.h",
+]
 
 public_deps = [
   "//media",
   "//media/base/ipc",
 ]
 
+sources = [
+  "//media/mojo/interfaces/video_transformation_mojom_traits.cc",
+  "//media/mojo/interfaces/video_transformation_mojom_traits.h",
+]
+
 type_mappings = [
   "media.mojom.AudioCodec=media::AudioCodec",
   "media.mojom.BufferingState=media::BufferingState",
@@ -52,6 +60,7 @@
   "media.mojom.VideoCodecProfile=media::VideoCodecProfile",
   "media.mojom.VideoPixelFormat=media::VideoPixelFormat",
   "media.mojom.VideoRotation=media::VideoRotation",
+  "media.mojom.VideoTransformation=media::VideoTransformation",
   "media.mojom.WaitingReason=media::WaitingReason",
   "media.mojom.WatchTimeKey=media::WatchTimeKey",
   "media.mojom.EncryptionPattern=media::EncryptionPattern",
diff --git a/media/mojo/interfaces/video_decoder_config_struct_traits.cc b/media/mojo/interfaces/video_decoder_config_struct_traits.cc
index ee076d7a..df3f80f2 100644
--- a/media/mojo/interfaces/video_decoder_config_struct_traits.cc
+++ b/media/mojo/interfaces/video_decoder_config_struct_traits.cc
@@ -23,8 +23,8 @@
   if (!input.ReadFormat(&format))
     return false;
 
-  media::VideoRotation rotation;
-  if (!input.ReadVideoRotation(&rotation))
+  media::VideoTransformation transformation;
+  if (!input.ReadTransformation(&transformation))
     return false;
 
   gfx::Size coded_size;
@@ -55,8 +55,9 @@
   if (!input.ReadHdrMetadata(&hdr_metadata))
     return false;
 
-  output->Initialize(codec, profile, format, color_space, rotation, coded_size,
-                     visible_rect, natural_size, extra_data, encryption_scheme);
+  output->Initialize(codec, profile, format, color_space, transformation,
+                     coded_size, visible_rect, natural_size, extra_data,
+                     encryption_scheme);
 
   if (hdr_metadata)
     output->set_hdr_metadata(hdr_metadata.value());
diff --git a/media/mojo/interfaces/video_decoder_config_struct_traits.h b/media/mojo/interfaces/video_decoder_config_struct_traits.h
index 6161bd8..4f2ab00f 100644
--- a/media/mojo/interfaces/video_decoder_config_struct_traits.h
+++ b/media/mojo/interfaces/video_decoder_config_struct_traits.h
@@ -11,6 +11,7 @@
 #include "media/mojo/interfaces/hdr_metadata_struct_traits.h"
 #include "media/mojo/interfaces/media_types.mojom.h"
 #include "media/mojo/interfaces/video_color_space_struct_traits.h"
+#include "media/mojo/interfaces/video_transformation_mojom_traits.h"
 #include "ui/gfx/geometry/mojo/geometry_struct_traits.h"
 
 namespace mojo {
@@ -59,9 +60,9 @@
     return input.color_space_info();
   }
 
-  static media::VideoRotation video_rotation(
+  static media::VideoTransformation transformation(
       const media::VideoDecoderConfig& input) {
-    return input.video_rotation();
+    return input.video_transformation();
   }
 
   static const base::Optional<media::HDRMetadata>& hdr_metadata(
diff --git a/media/mojo/interfaces/video_decoder_config_struct_traits_unittest.cc b/media/mojo/interfaces/video_decoder_config_struct_traits_unittest.cc
index d028eb5..6619e13 100644
--- a/media/mojo/interfaces/video_decoder_config_struct_traits_unittest.cc
+++ b/media/mojo/interfaces/video_decoder_config_struct_traits_unittest.cc
@@ -26,7 +26,7 @@
   const std::vector<uint8_t> kExtraDataVector(
       &kExtraData[0], &kExtraData[0] + base::size(kExtraData));
   VideoDecoderConfig input(kCodecVP8, VP8PROFILE_ANY, PIXEL_FORMAT_I420,
-                           VideoColorSpace(), VIDEO_ROTATION_0, kCodedSize,
+                           VideoColorSpace(), kNoTransformation, kCodedSize,
                            kVisibleRect, kNaturalSize, kExtraDataVector,
                            Unencrypted());
   std::vector<uint8_t> data =
@@ -40,7 +40,7 @@
 TEST(VideoDecoderConfigStructTraitsTest,
      ConvertVideoDecoderConfig_EmptyExtraData) {
   VideoDecoderConfig input(kCodecVP8, VP8PROFILE_ANY, PIXEL_FORMAT_I420,
-                           VideoColorSpace(), VIDEO_ROTATION_0, kCodedSize,
+                           VideoColorSpace(), kNoTransformation, kCodedSize,
                            kVisibleRect, kNaturalSize, EmptyExtraData(),
                            Unencrypted());
   std::vector<uint8_t> data =
@@ -53,7 +53,7 @@
 
 TEST(VideoDecoderConfigStructTraitsTest, ConvertVideoDecoderConfig_Encrypted) {
   VideoDecoderConfig input(kCodecVP8, VP8PROFILE_ANY, PIXEL_FORMAT_I420,
-                           VideoColorSpace(), VIDEO_ROTATION_0, kCodedSize,
+                           VideoColorSpace(), kNoTransformation, kCodedSize,
                            kVisibleRect, kNaturalSize, EmptyExtraData(),
                            AesCtrEncryptionScheme());
   std::vector<uint8_t> data =
@@ -72,7 +72,7 @@
                       VideoColorSpace::TransferID::SMPTEST2084,
                       VideoColorSpace::MatrixID::BT2020_CL,
                       gfx::ColorSpace::RangeID::LIMITED),
-      VIDEO_ROTATION_0, kCodedSize, kVisibleRect, kNaturalSize,
+      kNoTransformation, kCodedSize, kVisibleRect, kNaturalSize,
       EmptyExtraData(), Unencrypted());
   std::vector<uint8_t> data =
       media::mojom::VideoDecoderConfig::Serialize(&input);
@@ -85,7 +85,7 @@
 TEST(VideoDecoderConfigStructTraitsTest,
      ConvertVideoDecoderConfig_HDRMetadata) {
   VideoDecoderConfig input(kCodecVP8, VP8PROFILE_ANY, PIXEL_FORMAT_I420,
-                           VideoColorSpace(), VIDEO_ROTATION_0, kCodedSize,
+                           VideoColorSpace(), kNoTransformation, kCodedSize,
                            kVisibleRect, kNaturalSize, EmptyExtraData(),
                            Unencrypted());
   HDRMetadata hdr_metadata;
@@ -127,7 +127,7 @@
   // Next try an non-empty invalid config. Natural size must not be zero.
   const gfx::Size kInvalidNaturalSize(0, 0);
   input.Initialize(kCodecVP8, VP8PROFILE_ANY, PIXEL_FORMAT_I420,
-                   VideoColorSpace(), VIDEO_ROTATION_0, kCodedSize,
+                   VideoColorSpace(), kNoTransformation, kCodedSize,
                    kVisibleRect, kInvalidNaturalSize, EmptyExtraData(),
                    Unencrypted());
   EXPECT_FALSE(input.IsValidConfig());
diff --git a/media/mojo/interfaces/video_transformation_mojom_traits.cc b/media/mojo/interfaces/video_transformation_mojom_traits.cc
new file mode 100644
index 0000000..b8b51db
--- /dev/null
+++ b/media/mojo/interfaces/video_transformation_mojom_traits.cc
@@ -0,0 +1,22 @@
+// 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 "media/mojo/interfaces/video_transformation_mojom_traits.h"
+
+namespace mojo {
+
+// static
+bool StructTraits<media::mojom::VideoTransformationDataView,
+                  media::VideoTransformation>::
+    Read(media::mojom::VideoTransformationDataView input,
+         media::VideoTransformation* output) {
+  if (!input.ReadRotation(&output->rotation))
+    return false;
+
+  output->mirrored = input.mirrored();
+
+  return true;
+}
+
+}  // namespace mojo
diff --git a/media/mojo/interfaces/video_transformation_mojom_traits.h b/media/mojo/interfaces/video_transformation_mojom_traits.h
new file mode 100644
index 0000000..fe9c630
--- /dev/null
+++ b/media/mojo/interfaces/video_transformation_mojom_traits.h
@@ -0,0 +1,32 @@
+// 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 MEDIA_MOJO_INTERFACES_VIDEO_TRANSFORMATION_MOJOM_TRAITS_H_
+#define MEDIA_MOJO_INTERFACES_VIDEO_TRANSFORMATION_MOJOM_TRAITS_H_
+
+#include "media/base/ipc/media_param_traits.h"
+#include "media/base/video_transformation.h"
+#include "media/mojo/interfaces/media_types.mojom.h"
+
+namespace mojo {
+
+template <>
+struct StructTraits<media::mojom::VideoTransformationDataView,
+                    media::VideoTransformation> {
+  static media::VideoRotation rotation(
+      const media::VideoTransformation& input) {
+    return input.rotation;
+  }
+
+  static bool mirrored(const media::VideoTransformation& input) {
+    return input.mirrored;
+  }
+
+  static bool Read(media::mojom::VideoTransformationDataView input,
+                   media::VideoTransformation* output);
+};
+
+}  // namespace mojo
+
+#endif  // MEDIA_MOJO_INTERFACES_VIDEO_TRANSFORMATION_MOJOM_TRAITS_H_
diff --git a/media/remoting/fake_media_resource.cc b/media/remoting/fake_media_resource.cc
index 7fb2baff..1611d2e2 100644
--- a/media/remoting/fake_media_resource.cc
+++ b/media/remoting/fake_media_resource.cc
@@ -29,7 +29,7 @@
     gfx::Rect rect(0, 0, 640, 480);
     video_config_.Initialize(kCodecH264, H264PROFILE_BASELINE,
                              PIXEL_FORMAT_I420, VideoColorSpace::REC601(),
-                             VIDEO_ROTATION_0, size, rect, size,
+                             kNoTransformation, size, rect, size,
                              std::vector<uint8_t>(), Unencrypted());
   }
   ON_CALL(*this, Read(_))
diff --git a/media/remoting/proto_utils.cc b/media/remoting/proto_utils.cc
index ae23fe79..daac2b92 100644
--- a/media/remoting/proto_utils.cc
+++ b/media/remoting/proto_utils.cc
@@ -377,7 +377,7 @@
       ToMediaVideoCodec(video_message.codec()).value(),
       ToMediaVideoCodecProfile(video_message.profile()).value(),
       ToMediaVideoPixelFormat(video_message.format()).value(), color_space,
-      VIDEO_ROTATION_0,
+      kNoTransformation,
       gfx::Size(video_message.coded_size().width(),
                 video_message.coded_size().height()),
       gfx::Rect(video_message.visible_rect().x(),
diff --git a/media/remoting/stream_provider.cc b/media/remoting/stream_provider.cc
index df7482d1..12598b59 100644
--- a/media/remoting/stream_provider.cc
+++ b/media/remoting/stream_provider.cc
@@ -10,7 +10,7 @@
 #include "base/containers/circular_deque.h"
 #include "base/logging.h"
 #include "media/base/decoder_buffer.h"
-#include "media/base/video_rotation.h"
+#include "media/base/video_transformation.h"
 #include "media/remoting/proto_enum_utils.h"
 #include "media/remoting/proto_utils.h"
 
diff --git a/media/renderers/paint_canvas_video_renderer.cc b/media/renderers/paint_canvas_video_renderer.cc
index 4e7e91d6..d702375 100644
--- a/media/renderers/paint_canvas_video_renderer.cc
+++ b/media/renderers/paint_canvas_video_renderer.cc
@@ -578,7 +578,7 @@
     cc::PaintCanvas* canvas,
     const gfx::RectF& dest_rect,
     cc::PaintFlags& flags,
-    VideoRotation video_rotation,
+    VideoTransformation video_transformation,
     viz::ContextProvider* context_provider) {
   DCHECK(thread_checker_.CalledOnValidThread());
   if (flags.getAlpha() == 0) {
@@ -609,10 +609,11 @@
   video_flags.setBlendMode(flags.getBlendMode());
   video_flags.setFilterQuality(flags.getFilterQuality());
 
-  const bool need_rotation = video_rotation != VIDEO_ROTATION_0;
+  const bool need_rotation = video_transformation.rotation != VIDEO_ROTATION_0;
   const bool need_scaling =
       dest_rect.size() != gfx::SizeF(last_image_.width(), last_image_.height());
   const bool need_translation = !dest_rect.origin().IsOrigin();
+  // TODO(tmathmeyer): apply horizontal / vertical mirroring if needed.
   bool need_transform = need_rotation || need_scaling || need_translation;
   if (need_transform) {
     canvas->save();
@@ -620,7 +621,7 @@
         SkFloatToScalar(dest_rect.x() + (dest_rect.width() * 0.5f)),
         SkFloatToScalar(dest_rect.y() + (dest_rect.height() * 0.5f)));
     SkScalar angle = SkFloatToScalar(0.0f);
-    switch (video_rotation) {
+    switch (video_transformation.rotation) {
       case VIDEO_ROTATION_0:
         break;
       case VIDEO_ROTATION_90:
@@ -636,8 +637,8 @@
     canvas->rotate(angle);
 
     gfx::SizeF rotated_dest_size = dest_rect.size();
-    if (video_rotation == VIDEO_ROTATION_90 ||
-        video_rotation == VIDEO_ROTATION_270) {
+    if (video_transformation.rotation == VIDEO_ROTATION_90 ||
+        video_transformation.rotation == VIDEO_ROTATION_270) {
       rotated_dest_size =
           gfx::SizeF(rotated_dest_size.height(), rotated_dest_size.width());
     }
@@ -687,7 +688,7 @@
   flags.setFilterQuality(kLow_SkFilterQuality);
   Paint(video_frame, canvas,
         gfx::RectF(gfx::SizeF(video_frame->visible_rect().size())), flags,
-        media::VIDEO_ROTATION_0, context_provider);
+        media::kNoTransformation, context_provider);
 }
 
 namespace {
diff --git a/media/renderers/paint_canvas_video_renderer.h b/media/renderers/paint_canvas_video_renderer.h
index a222300..ca5d6fc 100644
--- a/media/renderers/paint_canvas_video_renderer.h
+++ b/media/renderers/paint_canvas_video_renderer.h
@@ -20,7 +20,7 @@
 #include "media/base/media_export.h"
 #include "media/base/timestamp_constants.h"
 #include "media/base/video_frame.h"
-#include "media/base/video_rotation.h"
+#include "media/base/video_transformation.h"
 
 namespace gfx {
 class RectF;
@@ -57,7 +57,7 @@
              cc::PaintCanvas* canvas,
              const gfx::RectF& dest_rect,
              cc::PaintFlags& flags,
-             VideoRotation video_rotation,
+             VideoTransformation video_transformation,
              viz::ContextProvider* context_provider);
 
   // Paints |video_frame| scaled to its visible size on |canvas|.
diff --git a/media/renderers/paint_canvas_video_renderer_unittest.cc b/media/renderers/paint_canvas_video_renderer_unittest.cc
index 0d23afa..7a4432b 100644
--- a/media/renderers/paint_canvas_video_renderer_unittest.cc
+++ b/media/renderers/paint_canvas_video_renderer_unittest.cc
@@ -86,7 +86,7 @@
                     const gfx::RectF& dest_rect,
                     Color color,
                     SkBlendMode mode,
-                    VideoRotation video_rotation);
+                    VideoTransformation video_transformation);
 
   void Copy(const scoped_refptr<VideoFrame>& video_frame,
             cc::PaintCanvas* canvas);
@@ -226,7 +226,7 @@
 void PaintCanvasVideoRendererTest::PaintWithoutFrame(cc::PaintCanvas* canvas) {
   cc::PaintFlags flags;
   flags.setFilterQuality(kLow_SkFilterQuality);
-  renderer_.Paint(nullptr, canvas, kNaturalRect, flags, VIDEO_ROTATION_0,
+  renderer_.Paint(nullptr, canvas, kNaturalRect, flags, kNoTransformation,
                   nullptr);
 }
 
@@ -235,7 +235,7 @@
     cc::PaintCanvas* canvas,
     Color color) {
   PaintRotated(video_frame, canvas, kNaturalRect, color, SkBlendMode::kSrcOver,
-               VIDEO_ROTATION_0);
+               kNoTransformation);
 }
 
 void PaintCanvasVideoRendererTest::PaintRotated(
@@ -244,7 +244,7 @@
     const gfx::RectF& dest_rect,
     Color color,
     SkBlendMode mode,
-    VideoRotation video_rotation) {
+    VideoTransformation video_transformation) {
   switch (color) {
     case kNone:
       break;
@@ -261,7 +261,7 @@
   cc::PaintFlags flags;
   flags.setBlendMode(mode);
   flags.setFilterQuality(kLow_SkFilterQuality);
-  renderer_.Paint(video_frame, canvas, dest_rect, flags, video_rotation,
+  renderer_.Paint(video_frame, canvas, dest_rect, flags, video_transformation,
                   nullptr);
 }
 
@@ -283,7 +283,7 @@
   PaintRotated(
       VideoFrame::CreateTransparentFrame(gfx::Size(kWidth, kHeight)).get(),
       target_canvas(), kNaturalRect, kNone, SkBlendMode::kSrcOver,
-      VIDEO_ROTATION_0);
+      kNoTransformation);
   EXPECT_EQ(static_cast<SkColor>(SK_ColorRED), bitmap()->getColor(0, 0));
 }
 
@@ -293,7 +293,7 @@
   PaintRotated(
       VideoFrame::CreateTransparentFrame(gfx::Size(kWidth, kHeight)).get(),
       target_canvas(), kNaturalRect, kNone, SkBlendMode::kSrc,
-      VIDEO_ROTATION_0);
+      kNoTransformation);
   EXPECT_EQ(static_cast<SkColor>(SK_ColorTRANSPARENT),
             bitmap()->getColor(0, 0));
 }
@@ -385,7 +385,7 @@
   SkBitmap bitmap = AllocBitmap(kWidth, kHeight);
   cc::SkiaPaintCanvas canvas(bitmap);
   PaintRotated(cropped_frame(), &canvas, kNaturalRect, kNone,
-               SkBlendMode::kSrcOver, VIDEO_ROTATION_90);
+               SkBlendMode::kSrcOver, VideoTransformation(VIDEO_ROTATION_90));
   // Check the corners.
   EXPECT_EQ(SK_ColorGREEN, bitmap.getColor(0, 0));
   EXPECT_EQ(SK_ColorBLACK, bitmap.getColor(kWidth - 1, 0));
@@ -397,7 +397,7 @@
   SkBitmap bitmap = AllocBitmap(kWidth, kHeight);
   cc::SkiaPaintCanvas canvas(bitmap);
   PaintRotated(cropped_frame(), &canvas, kNaturalRect, kNone,
-               SkBlendMode::kSrcOver, VIDEO_ROTATION_180);
+               SkBlendMode::kSrcOver, VideoTransformation(VIDEO_ROTATION_180));
   // Check the corners.
   EXPECT_EQ(SK_ColorBLUE, bitmap.getColor(0, 0));
   EXPECT_EQ(SK_ColorGREEN, bitmap.getColor(kWidth - 1, 0));
@@ -409,7 +409,7 @@
   SkBitmap bitmap = AllocBitmap(kWidth, kHeight);
   cc::SkiaPaintCanvas canvas(bitmap);
   PaintRotated(cropped_frame(), &canvas, kNaturalRect, kNone,
-               SkBlendMode::kSrcOver, VIDEO_ROTATION_270);
+               SkBlendMode::kSrcOver, VideoTransformation(VIDEO_ROTATION_270));
   // Check the corners.
   EXPECT_EQ(SK_ColorRED, bitmap.getColor(0, 0));
   EXPECT_EQ(SK_ColorBLUE, bitmap.getColor(kWidth - 1, 0));
@@ -424,7 +424,7 @@
 
   PaintRotated(cropped_frame(), &canvas,
                gfx::RectF(kWidth / 2, kHeight / 2, kWidth / 2, kHeight / 2),
-               kNone, SkBlendMode::kSrcOver, VIDEO_ROTATION_0);
+               kNone, SkBlendMode::kSrcOver, kNoTransformation);
   // Check the corners of quadrant 2 and 4.
   EXPECT_EQ(SK_ColorMAGENTA, bitmap.getColor(0, 0));
   EXPECT_EQ(SK_ColorMAGENTA, bitmap.getColor((kWidth / 2) - 1, 0));
@@ -444,7 +444,8 @@
 
   PaintRotated(cropped_frame(), &canvas,
                gfx::RectF(kWidth / 2, kHeight / 2, kWidth / 2, kHeight / 2),
-               kNone, SkBlendMode::kSrcOver, VIDEO_ROTATION_90);
+               kNone, SkBlendMode::kSrcOver,
+               VideoTransformation(VIDEO_ROTATION_90));
   // Check the corners of quadrant 2 and 4.
   EXPECT_EQ(SK_ColorMAGENTA, bitmap.getColor(0, 0));
   EXPECT_EQ(SK_ColorMAGENTA, bitmap.getColor((kWidth / 2) - 1, 0));
@@ -464,7 +465,8 @@
 
   PaintRotated(cropped_frame(), &canvas,
                gfx::RectF(kWidth / 2, kHeight / 2, kWidth / 2, kHeight / 2),
-               kNone, SkBlendMode::kSrcOver, VIDEO_ROTATION_180);
+               kNone, SkBlendMode::kSrcOver,
+               VideoTransformation(VIDEO_ROTATION_180));
   // Check the corners of quadrant 2 and 4.
   EXPECT_EQ(SK_ColorMAGENTA, bitmap.getColor(0, 0));
   EXPECT_EQ(SK_ColorMAGENTA, bitmap.getColor((kWidth / 2) - 1, 0));
@@ -484,7 +486,8 @@
 
   PaintRotated(cropped_frame(), &canvas,
                gfx::RectF(kWidth / 2, kHeight / 2, kWidth / 2, kHeight / 2),
-               kNone, SkBlendMode::kSrcOver, VIDEO_ROTATION_270);
+               kNone, SkBlendMode::kSrcOver,
+               VideoTransformation(VIDEO_ROTATION_270));
   // Check the corners of quadrant 2 and 4.
   EXPECT_EQ(SK_ColorMAGENTA, bitmap.getColor(0, 0));
   EXPECT_EQ(SK_ColorMAGENTA, bitmap.getColor((kWidth / 2) - 1, 0));
@@ -566,7 +569,7 @@
   flags.setFilterQuality(kNone_SkFilterQuality);
   renderer_.Paint(video_frame, &canvas,
                   gfx::RectF(bitmap.width(), bitmap.height()), flags,
-                  VIDEO_ROTATION_0, nullptr);
+                  kNoTransformation, nullptr);
   for (int j = 0; j < bitmap.height(); j++) {
     for (int i = 0; i < bitmap.width(); i++) {
       const int value = i + j * bitmap.width();
@@ -706,7 +709,7 @@
 
   cc::PaintFlags flags;
   flags.setFilterQuality(kLow_SkFilterQuality);
-  renderer_.Paint(video_frame, &canvas, kNaturalRect, flags, VIDEO_ROTATION_90,
+  renderer_.Paint(video_frame, &canvas, kNaturalRect, flags, kNoTransformation,
                   context_provider.get());
 }
 
@@ -731,7 +734,7 @@
 
   gfx::RectF visible_rect(visible_size.width(), visible_size.height());
   cc::PaintFlags flags;
-  renderer_.Paint(video_frame, &canvas, visible_rect, flags, VIDEO_ROTATION_0,
+  renderer_.Paint(video_frame, &canvas, visible_rect, flags, kNoTransformation,
                   nullptr);
 
   EXPECT_EQ(fWidth / 2, renderer_.LastImageDimensionsForTesting().width());
diff --git a/media/test/pipeline_integration_test.cc b/media/test/pipeline_integration_test.cc
index 1d9af55..ce17721f 100644
--- a/media/test/pipeline_integration_test.cc
+++ b/media/test/pipeline_integration_test.cc
@@ -2797,24 +2797,26 @@
 #if BUILDFLAG(USE_PROPRIETARY_CODECS)
 TEST_F(PipelineIntegrationTest, Rotated_Metadata_0) {
   ASSERT_EQ(PIPELINE_OK, Start("bear_rotate_0.mp4"));
-  ASSERT_EQ(VIDEO_ROTATION_0, metadata_.video_decoder_config.video_rotation());
+  ASSERT_EQ(VIDEO_ROTATION_0,
+            metadata_.video_decoder_config.video_transformation().rotation);
 }
 
 TEST_F(PipelineIntegrationTest, Rotated_Metadata_90) {
   ASSERT_EQ(PIPELINE_OK, Start("bear_rotate_90.mp4"));
-  ASSERT_EQ(VIDEO_ROTATION_90, metadata_.video_decoder_config.video_rotation());
+  ASSERT_EQ(VIDEO_ROTATION_90,
+            metadata_.video_decoder_config.video_transformation().rotation);
 }
 
 TEST_F(PipelineIntegrationTest, Rotated_Metadata_180) {
   ASSERT_EQ(PIPELINE_OK, Start("bear_rotate_180.mp4"));
   ASSERT_EQ(VIDEO_ROTATION_180,
-            metadata_.video_decoder_config.video_rotation());
+            metadata_.video_decoder_config.video_transformation().rotation);
 }
 
 TEST_F(PipelineIntegrationTest, Rotated_Metadata_270) {
   ASSERT_EQ(PIPELINE_OK, Start("bear_rotate_270.mp4"));
   ASSERT_EQ(VIDEO_ROTATION_270,
-            metadata_.video_decoder_config.video_rotation());
+            metadata_.video_decoder_config.video_transformation().rotation);
 }
 
 TEST_F(PipelineIntegrationTest, Spherical) {
diff --git a/mojo/public/cpp/bindings/README.md b/mojo/public/cpp/bindings/README.md
index 36bbe87..2d1ccc7 100644
--- a/mojo/public/cpp/bindings/README.md
+++ b/mojo/public/cpp/bindings/README.md
@@ -1797,6 +1797,6 @@
 
 ### Additional Documentation
 
-[Calling Mojo From Blink](https://www.chromium.org/developers/design-documents/mojo/calling-mojo-from-blink)
-:    A brief overview of what it looks like to use Mojom C++ bindings from
-     within Blink code.
+[Calling Mojo From Blink](/docs/mojo_ipc_conversion.md#Blink_Specific-Advice):
+A brief overview of what it looks like to use Mojom C++ bindings from
+within Blink code.
diff --git a/net/BUILD.gn b/net/BUILD.gn
index 87eecdd8..584c613 100644
--- a/net/BUILD.gn
+++ b/net/BUILD.gn
@@ -1464,6 +1464,10 @@
       "third_party/quiche/src/quic/core/http/quic_header_list.h",
       "third_party/quiche/src/quic/core/http/quic_headers_stream.cc",
       "third_party/quiche/src/quic/core/http/quic_headers_stream.h",
+      "third_party/quiche/src/quic/core/http/quic_receive_control_stream.cc",
+      "third_party/quiche/src/quic/core/http/quic_receive_control_stream.h",
+      "third_party/quiche/src/quic/core/http/quic_send_control_stream.cc",
+      "third_party/quiche/src/quic/core/http/quic_send_control_stream.h",
       "third_party/quiche/src/quic/core/http/quic_server_session_base.cc",
       "third_party/quiche/src/quic/core/http/quic_server_session_base.h",
       "third_party/quiche/src/quic/core/http/quic_spdy_client_session_base.cc",
@@ -1612,6 +1616,7 @@
       "third_party/quiche/src/quic/core/quic_versions.h",
       "third_party/quiche/src/quic/core/quic_write_blocked_list.cc",
       "third_party/quiche/src/quic/core/quic_write_blocked_list.h",
+      "third_party/quiche/src/quic/core/session_notifier_interface.h",
       "third_party/quiche/src/quic/core/tls_client_handshaker.cc",
       "third_party/quiche/src/quic/core/tls_client_handshaker.h",
       "third_party/quiche/src/quic/core/tls_handshaker.cc",
@@ -1665,7 +1670,10 @@
       "third_party/quiche/src/quic/platform/api/quic_stack_trace.h",
       "third_party/quiche/src/quic/platform/api/quic_str_cat.h",
       "third_party/quiche/src/quic/platform/api/quic_string_piece.h",
+      "third_party/quiche/src/quic/platform/api/quic_string_utils.h",
       "third_party/quiche/src/quic/platform/api/quic_text_utils.h",
+      "third_party/quiche/src/quic/platform/api/quic_thread.h",
+      "third_party/quiche/src/quic/platform/api/quic_uint128.h",
       "third_party/quiche/src/spdy/core/hpack/hpack_constants.cc",
       "third_party/quiche/src/spdy/core/hpack/hpack_constants.h",
       "third_party/quiche/src/spdy/core/hpack/hpack_decoder_adapter.cc",
@@ -3121,6 +3129,7 @@
       "third_party/quiche/src/quic/core/quic_packet_reader.cc",
       "third_party/quiche/src/quic/core/quic_packet_reader.h",
       "third_party/quiche/src/quic/platform/api/quic_default_proof_providers.h",
+      "third_party/quiche/src/quic/platform/api/quic_epoll.h",
       "third_party/quiche/src/quic/platform/api/quic_stream_buffer_allocator.h",
       "third_party/quiche/src/quic/platform/api/quic_system_event_loop.h",
       "third_party/quiche/src/quic/tools/quic_client.cc",
@@ -3464,6 +3473,7 @@
     "third_party/quiche/src/quic/tools/quic_simple_crypto_server_stream_helper.h",
     "third_party/quiche/src/quic/tools/quic_simple_dispatcher.cc",
     "third_party/quiche/src/quic/tools/quic_simple_dispatcher.h",
+    "third_party/quiche/src/quic/tools/quic_simple_server_backend.h",
     "third_party/quiche/src/quic/tools/quic_simple_server_session.cc",
     "third_party/quiche/src/quic/tools/quic_simple_server_session.h",
     "third_party/quiche/src/quic/tools/quic_simple_server_stream.cc",
@@ -3537,6 +3547,19 @@
       "//third_party/protobuf:protobuf_lite",
     ]
   }
+  executable("quic_crypto_message_printer") {
+    sources = [
+      "third_party/quiche/src/quic/core/crypto/crypto_message_printer_bin.cc",
+    ]
+    deps = [
+      ":net",
+      ":simple_quic_tools",
+      "//base",
+      "//build/win:default_exe_manifest",
+      "//third_party/boringssl",
+      "//third_party/protobuf:protobuf_lite",
+    ]
+  }
   executable("quic_reject_reason_decoder") {
     sources = [
       "third_party/quiche/src/quic/tools/quic_reject_reason_decoder_bin.cc",
@@ -5265,6 +5288,7 @@
     "third_party/quiche/src/quic/core/congestion_control/prr_sender_test.cc",
     "third_party/quiche/src/quic/core/congestion_control/rtt_stats_test.cc",
     "third_party/quiche/src/quic/core/congestion_control/send_algorithm_test.cc",
+    "third_party/quiche/src/quic/core/congestion_control/tcp_cubic_sender_bytes_test.cc",
     "third_party/quiche/src/quic/core/congestion_control/uber_loss_algorithm_test.cc",
     "third_party/quiche/src/quic/core/congestion_control/windowed_filter_test.cc",
     "third_party/quiche/src/quic/core/crypto/aes_128_gcm_12_decrypter_test.cc",
@@ -5302,6 +5326,8 @@
     "third_party/quiche/src/quic/core/http/quic_client_push_promise_index_test.cc",
     "third_party/quiche/src/quic/core/http/quic_header_list_test.cc",
     "third_party/quiche/src/quic/core/http/quic_headers_stream_test.cc",
+    "third_party/quiche/src/quic/core/http/quic_receive_control_stream_test.cc",
+    "third_party/quiche/src/quic/core/http/quic_send_control_stream_test.cc",
     "third_party/quiche/src/quic/core/http/quic_server_session_base_test.cc",
     "third_party/quiche/src/quic/core/http/quic_spdy_session_test.cc",
     "third_party/quiche/src/quic/core/http/quic_spdy_stream_body_buffer_test.cc",
@@ -5371,6 +5397,7 @@
     "third_party/quiche/src/quic/core/quic_write_blocked_list_test.cc",
     "third_party/quiche/src/quic/core/tls_handshaker_test.cc",
     "third_party/quiche/src/quic/core/uber_quic_stream_id_manager_test.cc",
+    "third_party/quiche/src/quic/core/uber_received_packet_manager_test.cc",
     "third_party/quiche/src/quic/platform/api/quic_containers_test.cc",
     "third_party/quiche/src/quic/platform/api/quic_endian_test.cc",
     "third_party/quiche/src/quic/platform/api/quic_hostname_utils_test.cc",
@@ -5380,6 +5407,7 @@
     "third_party/quiche/src/quic/platform/api/quic_mem_slice_test.cc",
     "third_party/quiche/src/quic/platform/api/quic_reference_counted_test.cc",
     "third_party/quiche/src/quic/platform/api/quic_str_cat_test.cc",
+    "third_party/quiche/src/quic/platform/api/quic_string_utils_test.cc",
     "third_party/quiche/src/quic/platform/api/quic_text_utils_test.cc",
     "third_party/quiche/src/quic/test_tools/crypto_test_utils_test.cc",
     "third_party/quiche/src/quic/test_tools/mock_quic_time_wait_list_manager.cc",
@@ -5388,6 +5416,7 @@
     "third_party/quiche/src/quic/test_tools/simple_session_notifier_test.cc",
     "third_party/quiche/src/quic/test_tools/simulator/quic_endpoint_test.cc",
     "third_party/quiche/src/quic/test_tools/simulator/simulator_test.cc",
+    "third_party/quiche/src/quic/tools/quic_simple_crypto_server_stream_helper_test.cc",
     "third_party/quiche/src/spdy/core/array_output_buffer.cc",
     "third_party/quiche/src/spdy/core/array_output_buffer.h",
     "third_party/quiche/src/spdy/core/array_output_buffer_test.cc",
@@ -5484,6 +5513,19 @@
       "third_party/quiche/src/quic/quartc/simulated_packet_transport.cc",
       "third_party/quiche/src/quic/quartc/simulated_packet_transport.h",
       "third_party/quiche/src/quic/quartc/simulated_packet_transport_test.cc",
+      "third_party/quiche/src/quic/quartc/test/bidi_test_runner.cc",
+      "third_party/quiche/src/quic/quartc/test/bidi_test_runner.h",
+      "third_party/quiche/src/quic/quartc/test/quartc_bidi_test.cc",
+      "third_party/quiche/src/quic/quartc/test/quartc_data_source.cc",
+      "third_party/quiche/src/quic/quartc/test/quartc_data_source.h",
+      "third_party/quiche/src/quic/quartc/test/quartc_data_source_test.cc",
+      "third_party/quiche/src/quic/quartc/test/quartc_peer.cc",
+      "third_party/quiche/src/quic/quartc/test/quartc_peer.h",
+      "third_party/quiche/src/quic/quartc/test/quartc_peer_test.cc",
+      "third_party/quiche/src/quic/quartc/test/random_delay_link.cc",
+      "third_party/quiche/src/quic/quartc/test/random_delay_link.h",
+      "third_party/quiche/src/quic/quartc/test/random_packet_filter.cc",
+      "third_party/quiche/src/quic/quartc/test/random_packet_filter.h",
     ]
   }
 
@@ -5611,7 +5653,6 @@
       "third_party/quiche/src/quic/tools/quic_url_test.cc",
       "tools/quic/quic_http_proxy_backend_stream_test.cc",
       "tools/quic/quic_http_proxy_backend_test.cc",
-      "tools/quic/quic_simple_server_session_helper_test.cc",
       "tools/quic/quic_simple_server_test.cc",
     ]
     deps += [
@@ -5959,6 +6000,7 @@
     "socket/fuzzed_socket.h",
     "socket/fuzzed_socket_factory.cc",
     "socket/fuzzed_socket_factory.h",
+    "third_party/quiche/src/quic/platform/api/quic_fuzzed_data_provider.h",
   ]
   public_deps = [
     "//base/test:test_support",
@@ -6564,6 +6606,20 @@
   ]
 }
 
+fuzzer_test("net_quic_framer_fuzzer") {
+  sources = [
+    "third_party/quiche/src/quic/test_tools/fuzzing/quic_framer_fuzzer.cc",
+  ]
+
+  deps = [
+    ":net_fuzzer_test_support",
+    ":quic_test_tools",
+    ":test_support",
+    "//net",
+    "//net/data/ssl/certificates:generate_fuzzer_cert_includes",
+  ]
+}
+
 fuzzer_test("net_uri_template_fuzzer") {
   sources = [
     "third_party/uri_template/uri_template_fuzzer.cc",
diff --git a/net/base/net_error_list.h b/net/base/net_error_list.h
index 6d3b6ef..9614b3d8 100644
--- a/net/base/net_error_list.h
+++ b/net/base/net_error_list.h
@@ -282,8 +282,7 @@
 // which exceeds size threshold).
 NET_ERROR(MSG_TOO_BIG, -142)
 
-// A SPDY session already exists, and should be used instead of this connection.
-NET_ERROR(SPDY_SESSION_ALREADY_EXISTS, -143)
+// Error -143 was removed (SPDY_SESSION_ALREADY_EXISTS)
 
 // Error -144 was removed (LIMIT_VIOLATION).
 
diff --git a/net/http/http_network_transaction_unittest.cc b/net/http/http_network_transaction_unittest.cc
index df46a606..b1c0dc5 100644
--- a/net/http/http_network_transaction_unittest.cc
+++ b/net/http/http_network_transaction_unittest.cc
@@ -35,6 +35,7 @@
 #include "base/test/simple_test_tick_clock.h"
 #include "base/test/test_file_util.h"
 #include "base/threading/thread_task_runner_handle.h"
+#include "build/build_config.h"
 #include "net/base/auth.h"
 #include "net/base/chunked_upload_data_stream.h"
 #include "net/base/completion_once_callback.h"
@@ -5906,9 +5907,9 @@
 }
 
 // Test the case where a proxied H2 session doesn't exist when an auth challenge
-// is observed, but does exist by the time auth credentials are provided.
-// Proxy-Connection: Close is used so that there's a second DNS lookup, which is
-// what causes the existing H2 session to be noticed and reused.
+// is observed, but does exist by the time auth credentials are provided. In
+// this case, auth and SSL are fully negotated on the second request, but then
+// the socket is discarded to use the shared session.
 TEST_F(HttpNetworkTransactionTest, ProxiedH2SessionAppearsDuringAuth) {
   ProxyConfig proxy_config;
   proxy_config.set_auto_detect(true);
@@ -5945,6 +5946,10 @@
                 "CONNECT www.example.org:443 HTTP/1.1\r\n"
                 "Host: www.example.org:443\r\n"
                 "Proxy-Connection: keep-alive\r\n\r\n"),
+      MockWrite(ASYNC, 2,
+                "CONNECT www.example.org:443 HTTP/1.1\r\n"
+                "Host: www.example.org:443\r\n"
+                "Proxy-Connection: keep-alive\r\n\r\n"),
   };
 
   MockRead auth_challenge_reads[] = {
@@ -5975,6 +5980,18 @@
       MockRead(SYNCHRONOUS, ERR_IO_PENDING, 8),
   };
 
+  MockWrite auth_response_writes_discarded_socket[] = {
+      MockWrite(ASYNC, 0,
+                "CONNECT www.example.org:443 HTTP/1.1\r\n"
+                "Host: www.example.org:443\r\n"
+                "Proxy-Connection: keep-alive\r\n"
+                "Proxy-Authorization: Basic Zm9vOmJhcg==\r\n\r\n"),
+  };
+
+  MockRead auth_response_reads_discarded_socket[] = {
+      MockRead(ASYNC, 1, "HTTP/1.1 200 OK\r\n\r\n"),
+  };
+
   SequencedSocketData auth_challenge1(auth_challenge_reads,
                                       auth_challenge_writes);
   session_deps_.socket_factory->AddSocketDataProvider(&auth_challenge1);
@@ -5986,10 +6003,20 @@
   SequencedSocketData spdy_data(spdy_reads, spdy_writes);
   session_deps_.socket_factory->AddSocketDataProvider(&spdy_data);
 
+  SequencedSocketData auth_response_discarded_socket(
+      auth_response_reads_discarded_socket,
+      auth_response_writes_discarded_socket);
+  session_deps_.socket_factory->AddSocketDataProvider(
+      &auth_response_discarded_socket);
+
   SSLSocketDataProvider ssl(ASYNC, OK);
   ssl.next_proto = kProtoHTTP2;
   session_deps_.socket_factory->AddSSLSocketDataProvider(&ssl);
 
+  SSLSocketDataProvider ssl2(ASYNC, OK);
+  ssl2.next_proto = kProtoHTTP2;
+  session_deps_.socket_factory->AddSSLSocketDataProvider(&ssl2);
+
   TestCompletionCallback callback;
   std::string response_data;
 
diff --git a/net/http/http_proxy_connect_job.cc b/net/http/http_proxy_connect_job.cc
index ed1b7a2..83f167d2 100644
--- a/net/http/http_proxy_connect_job.cc
+++ b/net/http/http_proxy_connect_job.cc
@@ -448,12 +448,6 @@
   if (result != OK) {
     UMA_HISTOGRAM_MEDIUM_TIMES("Net.HttpProxy.ConnectLatency.Insecure.Error",
                                base::TimeTicks::Now() - connect_start_time_);
-    // This is a special error code meaning to reuse an existing SPDY session
-    // rather than use a fresh socket. Overriding it with a proxy error message
-    // would cause the request to fail, instead of switching to using the SPDY
-    // session.
-    if (result == ERR_SPDY_SESSION_ALREADY_EXISTS)
-      return result;
     return ERR_PROXY_CONNECTION_FAILED;
   }
 
@@ -503,13 +497,6 @@
     // same way as server cert errors.
     return ERR_PROXY_CERTIFICATE_INVALID;
   }
-  // A SPDY session to the proxy completed prior to resolving the proxy
-  // hostname. Surface this error, and allow the delegate to retry.
-  // See crbug.com/334413.
-  if (result == ERR_SPDY_SESSION_ALREADY_EXISTS) {
-    DCHECK(!nested_connect_job_->socket());
-    return ERR_SPDY_SESSION_ALREADY_EXISTS;
-  }
   if (result < 0) {
     UMA_HISTOGRAM_MEDIUM_TIMES("Net.HttpProxy.ConnectLatency.Secure.Error",
                                base::TimeTicks::Now() - connect_start_time_);
diff --git a/net/http/http_stream_factory_job.cc b/net/http/http_stream_factory_job.cc
index 152f818..28af63b 100644
--- a/net/http/http_stream_factory_job.cc
+++ b/net/http/http_stream_factory_job.cc
@@ -43,6 +43,7 @@
 #include "net/quic/quic_http_stream.h"
 #include "net/socket/client_socket_handle.h"
 #include "net/socket/client_socket_pool_manager.h"
+#include "net/socket/connect_job.h"
 #include "net/socket/ssl_client_socket.h"
 #include "net/socket/stream_socket.h"
 #include "net/spdy/bidirectional_stream_spdy_impl.h"
@@ -486,23 +487,6 @@
   // |this| may be deleted after this call.
 }
 
-// static
-int HttpStreamFactory::Job::OnHostResolution(
-    SpdySessionPool* spdy_session_pool,
-    const SpdySessionKey& spdy_session_key,
-    bool enable_ip_based_pooling,
-    bool is_websocket,
-    const AddressList& addresses,
-    const NetLogWithSource& net_log) {
-  // It is OK to dereference spdy_session_pool, because the
-  // ClientSocketPoolManager will be destroyed in the same callback that
-  // destroys the SpdySessionPool.
-  return spdy_session_pool->FindAvailableSession(
-             spdy_session_key, enable_ip_based_pooling, is_websocket, net_log)
-             ? ERR_SPDY_SESSION_ALREADY_EXISTS
-             : OK;
-}
-
 void HttpStreamFactory::Job::OnIOComplete(int result) {
   TRACE_EVENT0(NetTracingCategory(), "HttpStreamFactory::Job::OnIOComplete");
   RunLoop(result);
@@ -693,8 +677,7 @@
 int HttpStreamFactory::Job::DoInitConnection() {
   net_log_.BeginEvent(NetLogEventType::HTTP_STREAM_JOB_INIT_CONNECTION);
   int result = DoInitConnectionImpl();
-  if (result != ERR_SPDY_SESSION_ALREADY_EXISTS &&
-      !expect_on_quic_host_resolution_) {
+  if (!expect_on_quic_host_resolution_) {
     delegate_->OnConnectionInitialized(this, result);
   }
   return result;
@@ -888,15 +871,6 @@
         request_info_.privacy_mode, net_log_, num_streams_);
   }
 
-  // If we can't use a HTTP/2 session, don't bother checking for one after
-  // the hostname is resolved.
-  OnHostResolutionCallback resolution_callback =
-      CanUseExistingSpdySession()
-          ? base::Bind(&Job::OnHostResolution, session_->spdy_session_pool(),
-                       spdy_session_key_, enable_ip_based_pooling_,
-                       try_websocket_over_http2_)
-          : OnHostResolutionCallback();
-
   ClientSocketPool::ProxyAuthCallback proxy_auth_callback =
       base::BindRepeating(&HttpStreamFactory::Job::OnNeedsProxyAuthCallback,
                           base::Unretained(this));
@@ -907,16 +881,15 @@
     return InitSocketHandleForWebSocketRequest(
         GetSocketGroup(), destination_, request_info_.load_flags, priority_,
         session_, proxy_info_, websocket_server_ssl_config, proxy_ssl_config_,
-        request_info_.privacy_mode, net_log_, connection_.get(),
-        resolution_callback, io_callback_, proxy_auth_callback);
+        request_info_.privacy_mode, net_log_, connection_.get(), io_callback_,
+        proxy_auth_callback);
   }
 
   return InitSocketHandleForHttpRequest(
       GetSocketGroup(), destination_, request_info_.load_flags, priority_,
       session_, proxy_info_, server_ssl_config_, proxy_ssl_config_,
       request_info_.privacy_mode, request_info_.socket_tag, net_log_,
-      connection_.get(), resolution_callback, io_callback_,
-      proxy_auth_callback);
+      connection_.get(), io_callback_, proxy_auth_callback);
 }
 
 void HttpStreamFactory::Job::OnQuicHostResolution(int result) {
@@ -945,23 +918,6 @@
     return OK;
   }
 
-  if (result == ERR_SPDY_SESSION_ALREADY_EXISTS) {
-    // We found a HTTP/2 connection after resolving the host. This is
-    // probably an IP pooled connection.
-    existing_spdy_session_ =
-        session_->spdy_session_pool()->FindAvailableSession(
-            spdy_session_key_, enable_ip_based_pooling_,
-            try_websocket_over_http2_, net_log_);
-    if (existing_spdy_session_) {
-      using_spdy_ = true;
-      next_state_ = STATE_CREATE_STREAM;
-    } else {
-      // It is possible that the HTTP/2 session no longer exists.
-      ReturnToStateInitConnection(true /* close connection */);
-    }
-    return OK;
-  }
-
   // |result| may be the result of any of the stacked pools. The following
   // logic is used when determining how to interpret an error.
   // If |result| < 0:
@@ -1225,6 +1181,12 @@
     base::WeakPtr<SpdySession> spdy_session) {
   DCHECK(spdy_session);
 
+  // No need for the connection any more, since |spdy_session| can be used
+  // instead, and there's no benefit from keeping the old ConnectJob in the
+  // socket pool.
+  if (connection_)
+    connection_->ResetAndCloseSocket();
+
   // Once a connection is initialized, or if there's any out-of-band callback,
   // like proxy auth challenge, the SpdySessionRequest is cancelled.
   DCHECK(next_state_ == STATE_INIT_CONNECTION ||
diff --git a/net/http/http_stream_factory_job.h b/net/http/http_stream_factory_job.h
index e7fb8e63..3946a9a 100644
--- a/net/http/http_stream_factory_job.h
+++ b/net/http/http_stream_factory_job.h
@@ -359,17 +359,6 @@
 
   void MaybeCopyConnectionAttemptsFromSocketOrHandle();
 
-  // Invoked by the transport socket pool after host resolution is complete
-  // to allow the connection to be aborted, if a matching SPDY session can
-  // be found.  Will return ERR_SPDY_SESSION_ALREADY_EXISTS if such a
-  // session is found, and OK otherwise.
-  static int OnHostResolution(SpdySessionPool* spdy_session_pool,
-                              const SpdySessionKey& spdy_session_key,
-                              bool enable_ip_based_pooling,
-                              bool is_websocket,
-                              const AddressList& addresses,
-                              const NetLogWithSource& net_log);
-
   // Returns true if the request should be throttled to allow for only one
   // connection attempt to be made to an H2 server at a time.
   bool ShouldThrottleConnectForSpdy() const;
diff --git a/net/http/http_stream_factory_unittest.cc b/net/http/http_stream_factory_unittest.cc
index 4ad52f4..cc67340 100644
--- a/net/http/http_stream_factory_unittest.cc
+++ b/net/http/http_stream_factory_unittest.cc
@@ -2003,7 +2003,7 @@
     scoped_refptr<ClientSocketPool::SocketParams> socket_params =
         base::MakeRefCounted<ClientSocketPool::SocketParams>(
             std::make_unique<SSLConfig>() /* ssl_config_for_origin */,
-            nullptr /* ssl_config_for_proxy */, OnHostResolutionCallback());
+            nullptr /* ssl_config_for_proxy */);
     ClientSocketPool::GroupId group_id(host_port_pair,
                                        ClientSocketPool::SocketType::kSsl,
                                        PrivacyMode::PRIVACY_MODE_DISABLED);
diff --git a/net/http/transport_security_state_static.json b/net/http/transport_security_state_static.json
index 5713762..88b51910 100644
--- a/net/http/transport_security_state_static.json
+++ b/net/http/transport_security_state_static.json
@@ -42454,7 +42454,6 @@
     { "name": "inscomers.net", "policy": "bulk-1-year", "mode": "force-https", "include_subdomains": true },
     { "name": "intae.it", "policy": "bulk-1-year", "mode": "force-https", "include_subdomains": true },
     { "name": "intpforum.com", "policy": "bulk-1-year", "mode": "force-https", "include_subdomains": true },
-    { "name": "inventoryexpress.xyz", "policy": "bulk-1-year", "mode": "force-https", "include_subdomains": true },
     { "name": "ip-tanz.com", "policy": "bulk-1-year", "mode": "force-https", "include_subdomains": true },
     { "name": "ipv6.jetzt", "policy": "bulk-1-year", "mode": "force-https", "include_subdomains": true },
     { "name": "isakssons.com", "policy": "bulk-1-year", "mode": "force-https", "include_subdomains": true },
@@ -59106,7 +59105,6 @@
     { "name": "downtownautospecialists.com", "policy": "bulk-1-year", "mode": "force-https", "include_subdomains": true },
     { "name": "dpecuador.com", "policy": "bulk-1-year", "mode": "force-https", "include_subdomains": true },
     { "name": "draliabadi.com", "policy": "bulk-1-year", "mode": "force-https", "include_subdomains": true },
-    { "name": "dreamstream.network", "policy": "bulk-1-year", "mode": "force-https", "include_subdomains": true },
     { "name": "dreamstream.nl", "policy": "bulk-1-year", "mode": "force-https", "include_subdomains": true },
     { "name": "dreamstream.tv", "policy": "bulk-1-year", "mode": "force-https", "include_subdomains": true },
     { "name": "dreamstream.video", "policy": "bulk-1-year", "mode": "force-https", "include_subdomains": true },
diff --git a/net/socket/client_socket_pool.cc b/net/socket/client_socket_pool.cc
index df6de12..5c9f818 100644
--- a/net/socket/client_socket_pool.cc
+++ b/net/socket/client_socket_pool.cc
@@ -11,11 +11,12 @@
 #include "net/http/http_proxy_connect_job.h"
 #include "net/log/net_log_event_type.h"
 #include "net/log/net_log_with_source.h"
+#include "net/socket/connect_job.h"
 #include "net/socket/socks_connect_job.h"
 #include "net/socket/ssl_connect_job.h"
 #include "net/socket/stream_socket.h"
-#include "net/socket/transport_connect_job.h"
-#include "net/socket/websocket_transport_connect_job.h"
+#include "net/spdy/spdy_session.h"
+#include "net/spdy/spdy_session_pool.h"
 
 namespace net {
 
@@ -24,23 +25,41 @@
 // The maximum duration, in seconds, to keep used idle persistent sockets alive.
 int64_t g_used_idle_socket_timeout_s = 300;  // 5 minutes
 
+// Invoked by the transport socket pool after host resolution is complete
+// to allow the connection to be aborted, if a matching SPDY session can
+// be found. Returns OnHostResolutionCallbackResult::kMayBeDeletedAsync if such
+// a session is found, as it will post a task that may delete the calling
+// ConnectJob. Also returns kMayBeDeletedAsync if there may already be such
+// a task posted.
+OnHostResolutionCallbackResult OnHostResolution(
+    SpdySessionPool* spdy_session_pool,
+    const SpdySessionKey& spdy_session_key,
+    bool is_for_websockets,
+    const HostPortPair& host_port_pair,
+    const AddressList& addresses) {
+  DCHECK(host_port_pair == spdy_session_key.host_port_pair());
+
+  // It is OK to dereference spdy_session_pool, because the
+  // ClientSocketPoolManager will be destroyed in the same callback that
+  // destroys the SpdySessionPool.
+  return spdy_session_pool->OnHostResolutionComplete(
+      spdy_session_key, is_for_websockets, addresses);
+}
+
 }  // namespace
 
 ClientSocketPool::SocketParams::SocketParams(
     std::unique_ptr<SSLConfig> ssl_config_for_origin,
-    std::unique_ptr<SSLConfig> ssl_config_for_proxy,
-    const OnHostResolutionCallback& resolution_callback)
+    std::unique_ptr<SSLConfig> ssl_config_for_proxy)
     : ssl_config_for_origin_(std::move(ssl_config_for_origin)),
-      ssl_config_for_proxy_(std::move(ssl_config_for_proxy)),
-      resolution_callback_(resolution_callback) {}
+      ssl_config_for_proxy_(std::move(ssl_config_for_proxy)) {}
 
 ClientSocketPool::SocketParams::~SocketParams() = default;
 
 scoped_refptr<ClientSocketPool::SocketParams>
 ClientSocketPool::SocketParams::CreateForHttpForTesting() {
   return base::MakeRefCounted<SocketParams>(nullptr /* ssl_config_for_origin */,
-                                            nullptr /* ssl_config_for_proxy */,
-                                            OnHostResolutionCallback());
+                                            nullptr /* ssl_config_for_proxy */);
 }
 
 ClientSocketPool::GroupId::GroupId()
@@ -128,12 +147,32 @@
     SocketTag socket_tag,
     ConnectJob::Delegate* delegate) {
   bool using_ssl = group_id.socket_type() == ClientSocketPool::SocketType::kSsl;
+
+  // If applicable, set up a callback to handle checking for H2 IP pooling
+  // opportunities.
+  OnHostResolutionCallback resolution_callback;
+  if (using_ssl && proxy_server.is_direct()) {
+    resolution_callback = base::BindRepeating(
+        &OnHostResolution, common_connect_job_params->spdy_session_pool,
+        SpdySessionKey(group_id.destination(), proxy_server,
+                       group_id.privacy_mode(),
+                       SpdySessionKey::IsProxySession::kFalse, socket_tag),
+        is_for_websockets);
+  } else if (proxy_server.is_https()) {
+    resolution_callback = base::BindRepeating(
+        &OnHostResolution, common_connect_job_params->spdy_session_pool,
+        SpdySessionKey(proxy_server.host_port_pair(), ProxyServer::Direct(),
+                       group_id.privacy_mode(),
+                       SpdySessionKey::IsProxySession::kTrue, socket_tag),
+        is_for_websockets);
+  }
+
   return ConnectJob::CreateConnectJob(
       using_ssl, group_id.destination(), proxy_server, proxy_annotation_tag,
       socket_params->ssl_config_for_origin(),
       socket_params->ssl_config_for_proxy(), is_for_websockets,
-      group_id.privacy_mode(), socket_params->resolution_callback(),
-      request_priority, socket_tag, common_connect_job_params, delegate);
+      group_id.privacy_mode(), resolution_callback, request_priority,
+      socket_tag, common_connect_job_params, delegate);
 }
 
 }  // namespace net
diff --git a/net/socket/client_socket_pool.h b/net/socket/client_socket_pool.h
index fdf9534..4db565e 100644
--- a/net/socket/client_socket_pool.h
+++ b/net/socket/client_socket_pool.h
@@ -153,18 +153,6 @@
     PrivacyMode privacy_mode_;
   };
 
-  // Callback to create a ConnectJob using the provided arguments. The lower
-  // level parameters used to construct the ConnectJob (like hostname, type of
-  // socket, proxy, etc) are all already bound to the callback.  If
-  // |websocket_endpoint_lock_manager| is non-null, a ConnectJob for use by
-  // WebSockets should be created.
-  using CreateConnectJobCallback =
-      base::RepeatingCallback<std::unique_ptr<ConnectJob>(
-          RequestPriority priority,
-          const SocketTag& socket_tag,
-          const CommonConnectJobParams* common_connect_job_params,
-          ConnectJob::Delegate* delegate)>;
-
   // Parameters that, in combination with GroupId, proxy, websocket information,
   // and global state, are sufficient to create a ConnectJob.
   //
@@ -179,8 +167,7 @@
     // For non-SSL requests / non-HTTPS proxies, the corresponding SSLConfig
     // argument may be nullptr.
     SocketParams(std::unique_ptr<SSLConfig> ssl_config_for_origin,
-                 std::unique_ptr<SSLConfig> ssl_config_for_proxy,
-                 const OnHostResolutionCallback& resolution_callback);
+                 std::unique_ptr<SSLConfig> ssl_config_for_proxy);
 
     // Creates a  SocketParams object with none of the fields populated. This
     // works for the HTTP case only.
@@ -194,17 +181,12 @@
       return ssl_config_for_proxy_.get();
     }
 
-    const OnHostResolutionCallback& resolution_callback() const {
-      return resolution_callback_;
-    }
-
    private:
     friend class base::RefCounted<SocketParams>;
     ~SocketParams();
 
     std::unique_ptr<SSLConfig> ssl_config_for_origin_;
     std::unique_ptr<SSLConfig> ssl_config_for_proxy_;
-    const OnHostResolutionCallback resolution_callback_;
 
     DISALLOW_COPY_AND_ASSIGN(SocketParams);
   };
diff --git a/net/socket/client_socket_pool_manager.cc b/net/socket/client_socket_pool_manager.cc
index aaae25b..35572c4d 100644
--- a/net/socket/client_socket_pool_manager.cc
+++ b/net/socket/client_socket_pool_manager.cc
@@ -90,15 +90,13 @@
     const ClientSocketPool::GroupId& group_id,
     const ProxyServer& proxy_server,
     const SSLConfig& ssl_config_for_origin,
-    const SSLConfig& ssl_config_for_proxy,
-    const OnHostResolutionCallback& resolution_callback) {
+    const SSLConfig& ssl_config_for_proxy) {
   bool using_ssl = group_id.socket_type() == ClientSocketPool::SocketType::kSsl;
   bool using_proxy_ssl = proxy_server.is_http_like() && !proxy_server.is_http();
   return base::MakeRefCounted<ClientSocketPool::SocketParams>(
       using_ssl ? std::make_unique<SSLConfig>(ssl_config_for_origin) : nullptr,
       using_proxy_ssl ? std::make_unique<SSLConfig>(ssl_config_for_proxy)
-                      : nullptr,
-      resolution_callback);
+                      : nullptr);
 }
 
 int InitSocketPoolHelper(
@@ -117,7 +115,6 @@
     int num_preconnect_streams,
     ClientSocketHandle* socket_handle,
     HttpNetworkSession::SocketPoolType socket_pool_type,
-    const OnHostResolutionCallback& resolution_callback,
     CompletionOnceCallback callback,
     const ClientSocketPool::ProxyAuthCallback& proxy_auth_callback) {
   bool using_ssl = group_type == ClientSocketPoolManager::SSL_GROUP;
@@ -133,8 +130,7 @@
       CreateGroupId(group_type, origin_host_port, proxy_info, privacy_mode);
   scoped_refptr<ClientSocketPool::SocketParams> socket_params =
       CreateSocketParams(connection_group, proxy_info.proxy_server(),
-                         ssl_config_for_origin, ssl_config_for_proxy,
-                         resolution_callback);
+                         ssl_config_for_origin, ssl_config_for_proxy);
 
   ClientSocketPool* pool =
       session->GetSocketPool(socket_pool_type, proxy_info.proxy_server());
@@ -247,7 +243,6 @@
     const SocketTag& socket_tag,
     const NetLogWithSource& net_log,
     ClientSocketHandle* socket_handle,
-    const OnHostResolutionCallback& resolution_callback,
     CompletionOnceCallback callback,
     const ClientSocketPool::ProxyAuthCallback& proxy_auth_callback) {
   DCHECK(socket_handle);
@@ -256,7 +251,7 @@
       proxy_info, ssl_config_for_origin, ssl_config_for_proxy,
       false /* is_for_websockets */, privacy_mode, socket_tag, net_log, 0,
       socket_handle, HttpNetworkSession::NORMAL_SOCKET_POOL,
-      resolution_callback, std::move(callback), proxy_auth_callback);
+      std::move(callback), proxy_auth_callback);
 }
 
 int InitSocketHandleForWebSocketRequest(
@@ -271,7 +266,6 @@
     PrivacyMode privacy_mode,
     const NetLogWithSource& net_log,
     ClientSocketHandle* socket_handle,
-    const OnHostResolutionCallback& resolution_callback,
     CompletionOnceCallback callback,
     const ClientSocketPool::ProxyAuthCallback& proxy_auth_callback) {
   DCHECK(socket_handle);
@@ -284,7 +278,7 @@
       proxy_info, ssl_config_for_origin, ssl_config_for_proxy,
       true /* is_for_websockets */, privacy_mode, SocketTag(), net_log, 0,
       socket_handle, HttpNetworkSession::WEBSOCKET_SOCKET_POOL,
-      resolution_callback, std::move(callback), proxy_auth_callback);
+      std::move(callback), proxy_auth_callback);
 }
 
 int PreconnectSocketsForHttpRequest(
@@ -307,8 +301,7 @@
       proxy_info, ssl_config_for_origin, ssl_config_for_proxy,
       false /* force_tunnel */, privacy_mode, SocketTag(), net_log,
       num_preconnect_streams, nullptr, HttpNetworkSession::NORMAL_SOCKET_POOL,
-      OnHostResolutionCallback(), CompletionOnceCallback(),
-      ClientSocketPool::ProxyAuthCallback());
+      CompletionOnceCallback(), ClientSocketPool::ProxyAuthCallback());
 }
 
 }  // namespace net
diff --git a/net/socket/client_socket_pool_manager.h b/net/socket/client_socket_pool_manager.h
index 1ec33cd..bfff5f1 100644
--- a/net/socket/client_socket_pool_manager.h
+++ b/net/socket/client_socket_pool_manager.h
@@ -26,9 +26,6 @@
 
 namespace net {
 
-typedef base::Callback<int(const AddressList&, const NetLogWithSource& net_log)>
-    OnHostResolutionCallback;
-
 class ClientSocketHandle;
 class HostPortPair;
 class NetLogWithSource;
@@ -112,7 +109,6 @@
     const SocketTag& socket_tag,
     const NetLogWithSource& net_log,
     ClientSocketHandle* socket_handle,
-    const OnHostResolutionCallback& resolution_callback,
     CompletionOnceCallback callback,
     const ClientSocketPool::ProxyAuthCallback& proxy_auth_callback);
 
@@ -137,7 +133,6 @@
     PrivacyMode privacy_mode,
     const NetLogWithSource& net_log,
     ClientSocketHandle* socket_handle,
-    const OnHostResolutionCallback& resolution_callback,
     CompletionOnceCallback callback,
     const ClientSocketPool::ProxyAuthCallback& proxy_auth_callback);
 
diff --git a/net/socket/connect_job.h b/net/socket/connect_job.h
index 70083a8ff..2ce8d90 100644
--- a/net/socket/connect_job.h
+++ b/net/socket/connect_job.h
@@ -97,9 +97,27 @@
   WebSocketEndpointLockManager* websocket_endpoint_lock_manager;
 };
 
+// When a host resolution completes, OnHostResolutionCallback() is invoked. If
+// it returns |kContinue|, the ConnectJob can continue immediately. If it
+// returns |kMayBeDeletedAsync|, the ConnectJob may be slated for asychronous
+// destruction, so should post a task before continuing, in case it will be
+// deleted. The purpose of kMayBeDeletedAsync is to avoid needlessly creating
+// and connecting a socket when it might not be needed.
+enum class OnHostResolutionCallbackResult {
+  kContinue,
+  kMayBeDeletedAsync,
+};
+
+// If non-null, invoked when host resolution completes. May not destroy the
+// ConnectJob synchronously, but may signal the ConnectJob may be destroyed
+// asynchronously. See OnHostResolutionCallbackResult above.
+//
+// |address_list| is the list of addresses the host being connected to was
+// resolved to, with the port fields populated to the port being connected to.
 using OnHostResolutionCallback =
-    base::RepeatingCallback<int(const AddressList&,
-                                const NetLogWithSource& net_log)>;
+    base::RepeatingCallback<OnHostResolutionCallbackResult(
+        const HostPortPair& host_port_pair,
+        const AddressList& address_list)>;
 
 // ConnectJob provides an abstract interface for "connecting" a socket.
 // The connection may involve host resolution, tcp connection, ssl connection,
diff --git a/net/socket/transport_client_socket_pool_unittest.cc b/net/socket/transport_client_socket_pool_unittest.cc
index 1265406..d8d59ca 100644
--- a/net/socket/transport_client_socket_pool_unittest.cc
+++ b/net/socket/transport_client_socket_pool_unittest.cc
@@ -1029,7 +1029,7 @@
   scoped_refptr<ClientSocketPool::SocketParams> socket_params =
       base::MakeRefCounted<ClientSocketPool::SocketParams>(
           GetSSLConfig() /* ssl_config_for_origin */,
-          nullptr /* ssl_config_for_proxy */, OnHostResolutionCallback());
+          nullptr /* ssl_config_for_proxy */);
 
   ClientSocketHandle handle;
   TestCompletionCallback callback;
@@ -1304,7 +1304,7 @@
     scoped_refptr<ClientSocketPool::SocketParams> socket_params =
         base::MakeRefCounted<ClientSocketPool::SocketParams>(
             nullptr /* ssl_config_for_origin */,
-            nullptr /* ssl_config_for_proxy */, OnHostResolutionCallback());
+            nullptr /* ssl_config_for_proxy */);
 
     SOCKS5MockData data(socket_io_mode);
     data.data_provider()->set_connect_data(MockConnect(socket_io_mode, OK));
@@ -1377,8 +1377,7 @@
   scoped_refptr<ClientSocketPool::SocketParams> socket_params =
       base::MakeRefCounted<ClientSocketPool::SocketParams>(
           GetSSLConfig() /* ssl_config_for_origin */,
-          GetSSLConfig() /* ssl_config_for_proxy */,
-          OnHostResolutionCallback());
+          GetSSLConfig() /* ssl_config_for_proxy */);
 
   ClientSocketPool::GroupId group_id(kEndpoint,
                                      ClientSocketPool::SocketType::kSsl,
@@ -1482,8 +1481,7 @@
   scoped_refptr<ClientSocketPool::SocketParams> socket_params =
       base::MakeRefCounted<ClientSocketPool::SocketParams>(
           GetSSLConfig() /* ssl_config_for_origin */,
-          GetSSLConfig() /* ssl_config_for_proxy */,
-          OnHostResolutionCallback());
+          GetSSLConfig() /* ssl_config_for_proxy */);
 
   ClientSocketPool::GroupId group_id(kEndpoint,
                                      ClientSocketPool::SocketType::kSsl,
@@ -1580,8 +1578,7 @@
       scoped_refptr<ClientSocketPool::SocketParams> socket_params =
           base::MakeRefCounted<ClientSocketPool::SocketParams>(
               GetSSLConfig() /* ssl_config_for_origin */,
-              GetSSLConfig() /* ssl_config_for_proxy */,
-              OnHostResolutionCallback());
+              GetSSLConfig() /* ssl_config_for_proxy */);
 
       int rv = handle.Init(
           ClientSocketPool::GroupId(kEndpoint,
@@ -1755,7 +1752,7 @@
   scoped_refptr<ClientSocketPool::SocketParams> socks_params =
       base::MakeRefCounted<ClientSocketPool::SocketParams>(
           nullptr /* ssl_config_for_origin */,
-          nullptr /* ssl_config_for_proxy */, OnHostResolutionCallback());
+          nullptr /* ssl_config_for_proxy */);
 
   // Test socket is tagged when created synchronously.
   SOCKS5MockData data_sync(SYNCHRONOUS);
@@ -1849,7 +1846,7 @@
   scoped_refptr<ClientSocketPool::SocketParams> socket_params =
       base::MakeRefCounted<ClientSocketPool::SocketParams>(
           std::make_unique<SSLConfig>() /* ssl_config_for_origin */,
-          nullptr /* ssl_config_for_proxy */, OnHostResolutionCallback());
+          nullptr /* ssl_config_for_proxy */);
 
   // Test socket is tagged before connected.
   uint64_t old_traffic = GetTaggedBytes(tag_val1);
@@ -1918,7 +1915,7 @@
   scoped_refptr<ClientSocketPool::SocketParams> socket_params =
       base::MakeRefCounted<ClientSocketPool::SocketParams>(
           GetSSLConfig() /* ssl_config_for_origin */,
-          nullptr /* ssl_config_for_proxy */, OnHostResolutionCallback());
+          nullptr /* ssl_config_for_proxy */);
 
   // Test connect jobs that are orphaned and then adopted, appropriately apply
   // new tag. Request socket with |tag1|.
@@ -1981,7 +1978,7 @@
   scoped_refptr<ClientSocketPool::SocketParams> socket_params =
       base::MakeRefCounted<ClientSocketPool::SocketParams>(
           GetSSLConfig() /* ssl_config_for_origin */,
-          nullptr /* ssl_config_for_proxy */, OnHostResolutionCallback());
+          nullptr /* ssl_config_for_proxy */);
 
   // Test that sockets paused by a full underlying socket pool are properly
   // connected and tagged when underlying pool is freed up.
@@ -2061,7 +2058,7 @@
   scoped_refptr<ClientSocketPool::SocketParams> socket_params =
       base::MakeRefCounted<ClientSocketPool::SocketParams>(
           nullptr /* ssl_config_for_origin */,
-          nullptr /* ssl_config_for_proxy */, OnHostResolutionCallback());
+          nullptr /* ssl_config_for_proxy */);
 
   // Verify requested socket is tagged properly.
   ClientSocketHandle handle;
@@ -2135,7 +2132,7 @@
   scoped_refptr<ClientSocketPool::SocketParams> socket_params =
       base::MakeRefCounted<ClientSocketPool::SocketParams>(
           GetSSLConfig() /* ssl_config_for_origin */,
-          nullptr /* ssl_config_for_proxy */, OnHostResolutionCallback());
+          nullptr /* ssl_config_for_proxy */);
 
   // Verify requested socket is tagged properly.
   ClientSocketHandle handle;
@@ -2248,7 +2245,7 @@
       scoped_refptr<ClientSocketPool::SocketParams> socket_params =
           base::MakeRefCounted<ClientSocketPool::SocketParams>(
               nullptr /* ssl_config_for_origin */,
-              nullptr /* ssl_config_for_proxy */, OnHostResolutionCallback());
+              nullptr /* ssl_config_for_proxy */);
       session_deps.socket_factory->AddSocketDataProvider(&provider_socket_1);
       ClientSocketHandle connection;
       TestCompletionCallback callback;
@@ -2293,7 +2290,7 @@
       scoped_refptr<ClientSocketPool::SocketParams> socket_params =
           base::MakeRefCounted<ClientSocketPool::SocketParams>(
               nullptr /* ssl_config_for_origin */,
-              nullptr /* ssl_config_for_proxy */, OnHostResolutionCallback());
+              nullptr /* ssl_config_for_proxy */);
       SequencedSocketData provider_socket_2(MockConnect(ASYNC, OK),
                                             base::span<MockRead>(),
                                             base::span<MockWrite>());
diff --git a/net/socket/transport_connect_job.cc b/net/socket/transport_connect_job.cc
index 30707ffa..047ac7a 100644
--- a/net/socket/transport_connect_job.cc
+++ b/net/socket/transport_connect_job.cc
@@ -12,6 +12,7 @@
 #include "base/logging.h"
 #include "base/metrics/histogram_macros.h"
 #include "base/strings/string_util.h"
+#include "base/threading/thread_task_runner_handle.h"
 #include "base/trace_event/trace_event.h"
 #include "base/values.h"
 #include "net/base/ip_endpoint.h"
@@ -100,7 +101,8 @@
                  NetLogEventType::TRANSPORT_CONNECT_JOB_CONNECT),
       params_(params),
       next_state_(STATE_NONE),
-      resolve_result_(OK) {
+      resolve_result_(OK),
+      weak_ptr_factory_(this) {
   // This is only set for WebSockets.
   DCHECK(!common_connect_job_params->websocket_endpoint_lock_manager);
 }
@@ -278,15 +280,22 @@
     return result;
   DCHECK(request_->GetAddressResults());
 
-  // Invoke callback, and abort if it fails.
+  next_state_ = STATE_TRANSPORT_CONNECT;
+
+  // Invoke callback.  If it indicates |this| may be slated for deletion, then
+  // only continue after a PostTask.
   if (!params_->host_resolution_callback().is_null()) {
-    result = params_->host_resolution_callback().Run(
-        request_->GetAddressResults().value(), net_log());
-    if (result != OK)
-      return result;
+    OnHostResolutionCallbackResult callback_result =
+        params_->host_resolution_callback().Run(
+            params_->destination(), request_->GetAddressResults().value());
+    if (callback_result == OnHostResolutionCallbackResult::kMayBeDeletedAsync) {
+      base::ThreadTaskRunnerHandle::Get()->PostTask(
+          FROM_HERE, base::BindOnce(&TransportConnectJob::OnIOComplete,
+                                    weak_ptr_factory_.GetWeakPtr(), OK));
+      return ERR_IO_PENDING;
+    }
   }
 
-  next_state_ = STATE_TRANSPORT_CONNECT;
   return result;
 }
 
diff --git a/net/socket/transport_connect_job.h b/net/socket/transport_connect_job.h
index 7d8b23f..0258e52 100644
--- a/net/socket/transport_connect_job.h
+++ b/net/socket/transport_connect_job.h
@@ -11,6 +11,7 @@
 #include "base/callback.h"
 #include "base/macros.h"
 #include "base/memory/ref_counted.h"
+#include "base/memory/weak_ptr.h"
 #include "base/time/time.h"
 #include "base/timer/timer.h"
 #include "net/base/host_port_pair.h"
@@ -169,6 +170,8 @@
   ConnectionAttempts connection_attempts_;
   ConnectionAttempts fallback_connection_attempts_;
 
+  base::WeakPtrFactory<TransportConnectJob> weak_ptr_factory_;
+
   DISALLOW_COPY_AND_ASSIGN(TransportConnectJob);
 };
 
diff --git a/net/socket/websocket_transport_connect_job.cc b/net/socket/websocket_transport_connect_job.cc
index 0a28e158..1b80ee69 100644
--- a/net/socket/websocket_transport_connect_job.cc
+++ b/net/socket/websocket_transport_connect_job.cc
@@ -7,6 +7,7 @@
 #include "base/bind.h"
 #include "base/location.h"
 #include "base/logging.h"
+#include "base/threading/thread_task_runner_handle.h"
 #include "base/time/time.h"
 #include "base/trace_event/trace_event.h"
 #include "base/values.h"
@@ -40,7 +41,8 @@
       next_state_(STATE_NONE),
       race_result_(TransportConnectJob::RACE_UNKNOWN),
       had_ipv4_(false),
-      had_ipv6_(false) {
+      had_ipv6_(false),
+      weak_ptr_factory_(this) {
   DCHECK(common_connect_job_params->websocket_endpoint_lock_manager);
 }
 
@@ -127,15 +129,22 @@
     return result;
   DCHECK(request_->GetAddressResults());
 
-  // Invoke callback, and abort if it fails.
+  next_state_ = STATE_TRANSPORT_CONNECT;
+
+  // Invoke callback.  If it indicates |this| may be slated for deletion, then
+  // only continue after a PostTask.
   if (!params_->host_resolution_callback().is_null()) {
-    result = params_->host_resolution_callback().Run(
-        request_->GetAddressResults().value(), net_log());
-    if (result != OK)
-      return result;
+    OnHostResolutionCallbackResult callback_result =
+        params_->host_resolution_callback().Run(
+            params_->destination(), request_->GetAddressResults().value());
+    if (callback_result == OnHostResolutionCallbackResult::kMayBeDeletedAsync) {
+      base::ThreadTaskRunnerHandle::Get()->PostTask(
+          FROM_HERE, base::BindOnce(&WebSocketTransportConnectJob::OnIOComplete,
+                                    weak_ptr_factory_.GetWeakPtr(), OK));
+      return ERR_IO_PENDING;
+    }
   }
 
-  next_state_ = STATE_TRANSPORT_CONNECT;
   return result;
 }
 
diff --git a/net/socket/websocket_transport_connect_job.h b/net/socket/websocket_transport_connect_job.h
index 1ebf709d..6ba51a0 100644
--- a/net/socket/websocket_transport_connect_job.h
+++ b/net/socket/websocket_transport_connect_job.h
@@ -11,6 +11,7 @@
 
 #include "base/macros.h"
 #include "base/memory/ref_counted.h"
+#include "base/memory/weak_ptr.h"
 #include "base/time/time.h"
 #include "base/timer/timer.h"
 #include "net/base/net_export.h"
@@ -105,6 +106,8 @@
   bool had_ipv4_;
   bool had_ipv6_;
 
+  base::WeakPtrFactory<WebSocketTransportConnectJob> weak_ptr_factory_;
+
   DISALLOW_COPY_AND_ASSIGN(WebSocketTransportConnectJob);
 };
 
diff --git a/net/spdy/spdy_network_transaction_unittest.cc b/net/spdy/spdy_network_transaction_unittest.cc
index aaefe03..b8adb41 100644
--- a/net/spdy/spdy_network_transaction_unittest.cc
+++ b/net/spdy/spdy_network_transaction_unittest.cc
@@ -17,6 +17,7 @@
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/test_file_util.h"
 #include "base/threading/thread_task_runner_handle.h"
+#include "build/build_config.h"
 #include "net/base/auth.h"
 #include "net/base/chunked_upload_data_stream.h"
 #include "net/base/completion_once_callback.h"
@@ -2547,6 +2548,8 @@
 
 TEST_F(SpdyNetworkTransactionTest, RedirectGetRequest) {
   SpdyURLRequestContext spdy_url_request_context;
+  // Use a different port to avoid trying to reuse the initial H2 session.
+  const char kRedirectUrl[] = "https://www.foo.com:8080/index.php";
 
   SSLSocketDataProvider ssl_provider0(ASYNC, OK);
   ssl_provider0.next_proto = kProtoHTTP2;
@@ -2564,8 +2567,7 @@
       spdy_util_.ConstructSpdyRstStream(1, spdy::ERROR_CODE_CANCEL));
   MockWrite writes0[] = {CreateMockWrite(req0, 0), CreateMockWrite(rst, 2)};
 
-  const char* const kExtraHeaders[] = {"location",
-                                       "https://www.foo.com/index.php"};
+  const char* const kExtraHeaders[] = {"location", kRedirectUrl};
   spdy::SpdySerializedFrame resp0(spdy_util_.ConstructSpdyReplyError(
       "301", kExtraHeaders, base::size(kExtraHeaders) / 2, 1));
   MockRead reads0[] = {CreateMockRead(resp0, 1), MockRead(ASYNC, 0, 3)};
@@ -2580,7 +2582,7 @@
 
   SpdyTestUtil spdy_util1;
   spdy::SpdyHeaderBlock headers1(
-      spdy_util1.ConstructGetHeaderBlock("https://www.foo.com/index.php"));
+      spdy_util1.ConstructGetHeaderBlock(kRedirectUrl));
   headers1["user-agent"] = "";
   headers1["accept-encoding"] = "gzip, deflate";
   spdy::SpdySerializedFrame req1(
@@ -3837,6 +3839,394 @@
   helper.VerifyDataConsumed();
 }
 
+TEST_F(SpdyNetworkTransactionTest, NoConnectionPoolingOverTunnel) {
+  // Use port 443 for two reasons:  This makes the endpoint is port 443 check in
+  // NormalSpdyTransactionHelper pass, and this means that the tunnel uses the
+  // same port as the servers, to further confuse things.
+  const char kPacString[] = "PROXY myproxy:443";
+
+  auto session_deps = std::make_unique<SpdySessionDependencies>(
+      ProxyResolutionService::CreateFixedFromPacResult(
+          kPacString, TRAFFIC_ANNOTATION_FOR_TESTS));
+  NormalSpdyTransactionHelper helper(request_, DEFAULT_PRIORITY, log_,
+                                     std::move(session_deps));
+
+  // Only one request uses the first connection.
+  spdy::SpdySerializedFrame req1(
+      spdy_util_.ConstructSpdyGet("https://www.example.org", 1, LOWEST));
+  MockWrite writes1[] = {
+      MockWrite(ASYNC, 0,
+                "CONNECT www.example.org:443 HTTP/1.1\r\n"
+                "Host: www.example.org:443\r\n"
+                "Proxy-Connection: keep-alive\r\n\r\n"),
+      CreateMockWrite(req1, 2),
+  };
+
+  spdy::SpdySerializedFrame resp1(
+      spdy_util_.ConstructSpdyGetReply(nullptr, 0, 1));
+  spdy::SpdySerializedFrame body1(spdy_util_.ConstructSpdyDataFrame(1, true));
+  MockRead reads1[] = {MockRead(ASYNC, 1, "HTTP/1.1 200 OK\r\n\r\n"),
+                       CreateMockRead(resp1, 3), CreateMockRead(body1, 4),
+                       MockRead(SYNCHRONOUS, ERR_IO_PENDING, 5)};
+
+  MockConnect connect1(ASYNC, OK);
+  SequencedSocketData data1(connect1, reads1, writes1);
+
+  // Run a transaction to completion to set up a SPDY session.
+  helper.RunToCompletion(&data1);
+  TransactionHelperResult out = helper.output();
+  EXPECT_THAT(out.rv, IsOk());
+  EXPECT_EQ("HTTP/1.1 200", out.status_line);
+  EXPECT_EQ("hello!", out.response_data);
+
+  // A new SPDY session should have been created.
+  SpdySessionKey key1(HostPortPair("www.example.org", 443),
+                      ProxyServer::FromPacString(kPacString),
+                      PRIVACY_MODE_DISABLED,
+                      SpdySessionKey::IsProxySession::kFalse, SocketTag());
+  base::WeakPtr<SpdySession> session1 =
+      helper.session()->spdy_session_pool()->FindAvailableSession(
+          key1, true /* enable_up_base_pooling */, false /* is_websocket */,
+          NetLogWithSource());
+  ASSERT_TRUE(session1);
+
+  // The second request uses a second connection.
+  SpdyTestUtil spdy_util2;
+  spdy::SpdySerializedFrame req2(
+      spdy_util2.ConstructSpdyGet("https://example.test", 1, LOWEST));
+  MockWrite writes2[] = {
+      MockWrite(ASYNC, 0,
+                "CONNECT example.test:443 HTTP/1.1\r\n"
+                "Host: example.test:443\r\n"
+                "Proxy-Connection: keep-alive\r\n\r\n"),
+      CreateMockWrite(req2, 2),
+  };
+
+  spdy::SpdySerializedFrame resp2(
+      spdy_util2.ConstructSpdyGetReply(nullptr, 0, 1));
+  spdy::SpdySerializedFrame body2(spdy_util2.ConstructSpdyDataFrame(1, true));
+  MockRead reads2[] = {MockRead(ASYNC, 1, "HTTP/1.1 200 OK\r\n\r\n"),
+                       CreateMockRead(resp2, 3), CreateMockRead(body2, 4),
+                       MockRead(SYNCHRONOUS, ERR_IO_PENDING, 5)};
+
+  MockConnect connect2(ASYNC, OK);
+  SequencedSocketData data2(connect2, reads2, writes2);
+  helper.AddData(&data2);
+
+  HttpRequestInfo request2;
+  request2.method = "GET";
+  request2.url = GURL("https://example.test/");
+  request2.load_flags = 0;
+  request2.traffic_annotation =
+      net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS);
+  auto trans2 = std::make_unique<HttpNetworkTransaction>(DEFAULT_PRIORITY,
+                                                         helper.session());
+
+  TestCompletionCallback callback;
+  EXPECT_THAT(trans2->Start(&request2, callback.callback(), NetLogWithSource()),
+              IsError(ERR_IO_PENDING));
+
+  // Wait for the second request to get headers.  It should create a new H2
+  // session to do so.
+  EXPECT_THAT(callback.WaitForResult(), IsOk());
+
+  const HttpResponseInfo* response = trans2->GetResponseInfo();
+  ASSERT_TRUE(response);
+  ASSERT_TRUE(response->headers);
+  EXPECT_EQ("HTTP/1.1 200", response->headers->GetStatusLine());
+  EXPECT_TRUE(response->was_fetched_via_spdy);
+  EXPECT_TRUE(response->was_alpn_negotiated);
+  std::string response_data;
+  ASSERT_THAT(ReadTransaction(trans2.get(), &response_data), IsOk());
+  EXPECT_EQ("hello!", response_data);
+
+  // Inspect the new session.
+  SpdySessionKey key2(HostPortPair("example.test", 443),
+                      ProxyServer::FromPacString(kPacString),
+                      PRIVACY_MODE_DISABLED,
+                      SpdySessionKey::IsProxySession::kFalse, SocketTag());
+  base::WeakPtr<SpdySession> session2 =
+      helper.session()->spdy_session_pool()->FindAvailableSession(
+          key2, true /* enable_up_base_pooling */, false /* is_websocket */,
+          NetLogWithSource());
+  ASSERT_TRUE(session2);
+  ASSERT_TRUE(session1);
+  EXPECT_NE(session1.get(), session2.get());
+}
+
+// Check that if a session is found after host resolution, but is closed before
+// the task to try to use it executes, the request will continue to create a new
+// socket and use it.
+TEST_F(SpdyNetworkTransactionTest, ConnectionPoolingSessionClosedBeforeUse) {
+  NormalSpdyTransactionHelper helper(request_, DEFAULT_PRIORITY, log_, nullptr);
+
+  // Only one request uses the first connection.
+  spdy::SpdySerializedFrame req1(
+      spdy_util_.ConstructSpdyGet("https://www.example.org", 1, LOWEST));
+  MockWrite writes1[] = {
+      CreateMockWrite(req1, 0),
+  };
+
+  spdy::SpdySerializedFrame resp1(
+      spdy_util_.ConstructSpdyGetReply(nullptr, 0, 1));
+  spdy::SpdySerializedFrame body1(spdy_util_.ConstructSpdyDataFrame(1, true));
+  MockRead reads1[] = {CreateMockRead(resp1, 1), CreateMockRead(body1, 2),
+                       MockRead(SYNCHRONOUS, ERR_IO_PENDING, 3)};
+
+  MockConnect connect1(ASYNC, OK);
+  SequencedSocketData data1(connect1, reads1, writes1);
+
+  // Run a transaction to completion to set up a SPDY session.
+  helper.RunToCompletion(&data1);
+  TransactionHelperResult out = helper.output();
+  EXPECT_THAT(out.rv, IsOk());
+  EXPECT_EQ("HTTP/1.1 200", out.status_line);
+  EXPECT_EQ("hello!", out.response_data);
+
+  // A new SPDY session should have been created.
+  SpdySessionKey key1(HostPortPair("www.example.org", 443),
+                      ProxyServer::Direct(), PRIVACY_MODE_DISABLED,
+                      SpdySessionKey::IsProxySession::kFalse, SocketTag());
+  EXPECT_TRUE(helper.session()->spdy_session_pool()->FindAvailableSession(
+      key1, true /* enable_up_base_pooling */, false /* is_websocket */,
+      NetLogWithSource()));
+
+  // The second request uses a second connection.
+  SpdyTestUtil spdy_util2;
+  spdy::SpdySerializedFrame req2(
+      spdy_util2.ConstructSpdyGet("https://example.test", 1, LOWEST));
+  MockWrite writes2[] = {
+      CreateMockWrite(req2, 0),
+  };
+
+  spdy::SpdySerializedFrame resp2(
+      spdy_util2.ConstructSpdyGetReply(nullptr, 0, 1));
+  spdy::SpdySerializedFrame body2(spdy_util2.ConstructSpdyDataFrame(1, true));
+  MockRead reads2[] = {CreateMockRead(resp2, 1), CreateMockRead(body2, 2),
+                       MockRead(SYNCHRONOUS, ERR_IO_PENDING, 3)};
+
+  MockConnect connect2(ASYNC, OK);
+  SequencedSocketData data2(connect2, reads2, writes2);
+  helper.AddData(&data2);
+
+  HttpRequestInfo request2;
+  request2.method = "GET";
+  request2.url = GURL("https://example.test/");
+  request2.load_flags = 0;
+  request2.traffic_annotation =
+      net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS);
+  auto trans2 = std::make_unique<HttpNetworkTransaction>(DEFAULT_PRIORITY,
+                                                         helper.session());
+
+  // Set on-demand mode and run the second request to the DNS lookup.
+  helper.session_deps()->host_resolver->set_ondemand_mode(true);
+  TestCompletionCallback callback;
+  EXPECT_THAT(trans2->Start(&request2, callback.callback(), NetLogWithSource()),
+              IsError(ERR_IO_PENDING));
+  base::RunLoop().RunUntilIdle();
+  ASSERT_TRUE(helper.session_deps()->host_resolver->has_pending_requests());
+
+  // Resolve the request now, which should create an alias for the SpdySession
+  // immediately, but the task to use the session for the second request should
+  // run asynchronously, so it hasn't run yet.
+  helper.session_deps()->host_resolver->ResolveOnlyRequestNow();
+  SpdySessionKey key2(HostPortPair("example.test", 443), ProxyServer::Direct(),
+                      PRIVACY_MODE_DISABLED,
+                      SpdySessionKey::IsProxySession::kFalse, SocketTag());
+  base::WeakPtr<SpdySession> session1 =
+      helper.session()->spdy_session_pool()->FindAvailableSession(
+          key2, true /* enable_up_base_pooling */, false /* is_websocket */,
+          NetLogWithSource());
+  ASSERT_TRUE(session1);
+  EXPECT_EQ(key1, session1->spdy_session_key());
+  // Remove the session before the second request can try to use it.
+  helper.session()->spdy_session_pool()->CloseAllSessions();
+
+  // Wait for the second request to get headers.  It should create a new H2
+  // session to do so.
+  EXPECT_THAT(callback.WaitForResult(), IsOk());
+
+  const HttpResponseInfo* response = trans2->GetResponseInfo();
+  ASSERT_TRUE(response);
+  ASSERT_TRUE(response->headers);
+  EXPECT_EQ("HTTP/1.1 200", response->headers->GetStatusLine());
+  EXPECT_TRUE(response->was_fetched_via_spdy);
+  EXPECT_TRUE(response->was_alpn_negotiated);
+  std::string response_data;
+  ASSERT_THAT(ReadTransaction(trans2.get(), &response_data), IsOk());
+  EXPECT_EQ("hello!", response_data);
+
+  // Inspect the new session.
+  base::WeakPtr<SpdySession> session2 =
+      helper.session()->spdy_session_pool()->FindAvailableSession(
+          key2, true /* enable_up_base_pooling */, false /* is_websocket */,
+          NetLogWithSource());
+  ASSERT_TRUE(session2);
+  EXPECT_EQ(key2, session2->spdy_session_key());
+  helper.VerifyDataConsumed();
+}
+
+#if defined(OS_ANDROID)
+
+// Test this if two HttpNetworkTransactions try to repurpose the same
+// SpdySession with two different SocketTags, only one request gets the session,
+// while the other makes a new SPDY session.
+TEST_F(SpdyNetworkTransactionTest, ConnectionPoolingMultipleSocketTags) {
+  const SocketTag kSocketTag1(SocketTag::UNSET_UID, 1);
+  const SocketTag kSocketTag2(SocketTag::UNSET_UID, 2);
+  const SocketTag kSocketTag3(SocketTag::UNSET_UID, 3);
+
+  NormalSpdyTransactionHelper helper(request_, DEFAULT_PRIORITY, log_, nullptr);
+
+  // The first and third requests use the first connection.
+  spdy::SpdySerializedFrame req1(
+      spdy_util_.ConstructSpdyGet("https://www.example.org", 1, LOWEST));
+  spdy_util_.UpdateWithStreamDestruction(1);
+  spdy::SpdySerializedFrame req3(
+      spdy_util_.ConstructSpdyGet("https://example.test/request3", 3, LOWEST));
+  MockWrite writes1[] = {
+      CreateMockWrite(req1, 0),
+      CreateMockWrite(req3, 3),
+  };
+
+  spdy::SpdySerializedFrame resp1(
+      spdy_util_.ConstructSpdyGetReply(nullptr, 0, 1));
+  spdy::SpdySerializedFrame body1(spdy_util_.ConstructSpdyDataFrame(1, true));
+  spdy::SpdySerializedFrame resp3(
+      spdy_util_.ConstructSpdyGetReply(nullptr, 0, 3));
+  spdy::SpdySerializedFrame body3(spdy_util_.ConstructSpdyDataFrame(3, true));
+  MockRead reads1[] = {CreateMockRead(resp1, 1), CreateMockRead(body1, 2),
+                       CreateMockRead(resp3, 4), CreateMockRead(body3, 5),
+                       MockRead(SYNCHRONOUS, ERR_IO_PENDING, 6)};
+
+  SequencedSocketData data1(MockConnect(ASYNC, OK), reads1, writes1);
+  helper.AddData(&data1);
+
+  // Due to the vagaries of how the socket pools work, in this particular case,
+  // the second ConnectJob will be cancelled, but only after it tries to start
+  // connecting. This does not happen in the general case of a bunch of requests
+  // using the same socket tag.
+  SequencedSocketData data2(MockConnect(SYNCHRONOUS, ERR_IO_PENDING),
+                            base::span<const MockRead>(),
+                            base::span<const MockWrite>());
+  helper.AddData(&data2);
+
+  // The second request uses a second connection.
+  SpdyTestUtil spdy_util2;
+  spdy::SpdySerializedFrame req2(
+      spdy_util2.ConstructSpdyGet("https://example.test/request2", 1, LOWEST));
+  MockWrite writes2[] = {
+      CreateMockWrite(req2, 0),
+  };
+
+  spdy::SpdySerializedFrame resp2(
+      spdy_util2.ConstructSpdyGetReply(nullptr, 0, 1));
+  spdy::SpdySerializedFrame body2(spdy_util2.ConstructSpdyDataFrame(1, true));
+  MockRead reads2[] = {CreateMockRead(resp2, 1), CreateMockRead(body2, 2),
+                       MockRead(SYNCHRONOUS, ERR_IO_PENDING, 3)};
+
+  SequencedSocketData data3(MockConnect(ASYNC, OK), reads2, writes2);
+  helper.AddData(&data3);
+
+  // Run a transaction to completion to set up a SPDY session. This can't use
+  // RunToCompletion(), since it can't call VerifyDataConsumed() yet.
+  helper.RunPreTestSetup();
+  helper.RunDefaultTest();
+  TransactionHelperResult out = helper.output();
+  EXPECT_THAT(out.rv, IsOk());
+  EXPECT_EQ("HTTP/1.1 200", out.status_line);
+  EXPECT_EQ("hello!", out.response_data);
+
+  // A new SPDY session should have been created.
+  SpdySessionKey key1(HostPortPair("www.example.org", 443),
+                      ProxyServer::Direct(), PRIVACY_MODE_DISABLED,
+                      SpdySessionKey::IsProxySession::kFalse, SocketTag());
+  EXPECT_TRUE(helper.session()->spdy_session_pool()->FindAvailableSession(
+      key1, true /* enable_up_base_pooling */, false /* is_websocket */,
+      NetLogWithSource()));
+
+  // Set on-demand mode for the next two requests.
+  helper.session_deps()->host_resolver->set_ondemand_mode(true);
+
+  HttpRequestInfo request2;
+  request2.socket_tag = kSocketTag2;
+  request2.method = "GET";
+  request2.url = GURL("https://example.test/request2");
+  request2.load_flags = 0;
+  request2.traffic_annotation =
+      net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS);
+  auto trans2 = std::make_unique<HttpNetworkTransaction>(DEFAULT_PRIORITY,
+                                                         helper.session());
+  TestCompletionCallback callback2;
+  EXPECT_THAT(
+      trans2->Start(&request2, callback2.callback(), NetLogWithSource()),
+      IsError(ERR_IO_PENDING));
+
+  HttpRequestInfo request3;
+  request3.socket_tag = kSocketTag3;
+  request3.method = "GET";
+  request3.url = GURL("https://example.test/request3");
+  request3.load_flags = 0;
+  request3.traffic_annotation =
+      net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS);
+  auto trans3 = std::make_unique<HttpNetworkTransaction>(DEFAULT_PRIORITY,
+                                                         helper.session());
+  TestCompletionCallback callback3;
+  EXPECT_THAT(
+      trans3->Start(&request3, callback3.callback(), NetLogWithSource()),
+      IsError(ERR_IO_PENDING));
+
+  // Run the message loop until both requests are waiting on the host resolver.
+  base::RunLoop().RunUntilIdle();
+  ASSERT_TRUE(helper.session_deps()->host_resolver->has_pending_requests());
+
+  // Complete the second requests's DNS lookup now, which should create an alias
+  // for the SpdySession immediately, but the task to use the session for the
+  // second request should run asynchronously, so it hasn't run yet.
+  helper.session_deps()->host_resolver->ResolveNow(2);
+  SpdySessionKey key2(HostPortPair("example.test", 443), ProxyServer::Direct(),
+                      PRIVACY_MODE_DISABLED,
+                      SpdySessionKey::IsProxySession::kFalse, kSocketTag2);
+
+  // Complete the third requests's DNS lookup now, which should hijack the
+  // SpdySession from the second request.
+  helper.session_deps()->host_resolver->ResolveNow(3);
+  SpdySessionKey key3(HostPortPair("example.test", 443), ProxyServer::Direct(),
+                      PRIVACY_MODE_DISABLED,
+                      SpdySessionKey::IsProxySession::kFalse, kSocketTag3);
+
+  // Wait for the second request to get headers.  It should create a new H2
+  // session to do so.
+  EXPECT_THAT(callback2.WaitForResult(), IsOk());
+
+  const HttpResponseInfo* response = trans2->GetResponseInfo();
+  ASSERT_TRUE(response);
+  ASSERT_TRUE(response->headers);
+  EXPECT_EQ("HTTP/1.1 200", response->headers->GetStatusLine());
+  EXPECT_TRUE(response->was_fetched_via_spdy);
+  EXPECT_TRUE(response->was_alpn_negotiated);
+  std::string response_data;
+  ASSERT_THAT(ReadTransaction(trans2.get(), &response_data), IsOk());
+  EXPECT_EQ("hello!", response_data);
+
+  // Wait for the third request to get headers.  It should have reused the first
+  // session.
+  EXPECT_THAT(callback3.WaitForResult(), IsOk());
+
+  response = trans3->GetResponseInfo();
+  ASSERT_TRUE(response);
+  ASSERT_TRUE(response->headers);
+  EXPECT_EQ("HTTP/1.1 200", response->headers->GetStatusLine());
+  EXPECT_TRUE(response->was_fetched_via_spdy);
+  EXPECT_TRUE(response->was_alpn_negotiated);
+  ASSERT_THAT(ReadTransaction(trans3.get(), &response_data), IsOk());
+  EXPECT_EQ("hello!", response_data);
+
+  helper.VerifyDataConsumed();
+}
+
+#endif  // defined(OS_ANDROID)
+
 // Regression test for https://crbug.com/727653.
 TEST_F(SpdyNetworkTransactionTest, RejectServerPushWithNoMethod) {
   base::HistogramTester histogram_tester;
@@ -8065,6 +8455,285 @@
                                       /* expected_count = */ 1);
 }
 
+// Same as above, but checks that a WebSocket connection avoids creating a new
+// socket if it detects an H2 session when host resolution completes, and
+// requests also use different hostnames.
+TEST_F(SpdyNetworkTransactionTest,
+       WebSocketOverHTTP2DetectsNewSessionWithAliasing) {
+  base::HistogramTester histogram_tester;
+  auto session_deps = std::make_unique<SpdySessionDependencies>();
+  session_deps->enable_websocket_over_http2 = true;
+  session_deps->host_resolver->set_ondemand_mode(true);
+  NormalSpdyTransactionHelper helper(request_, HIGHEST, log_,
+                                     std::move(session_deps));
+  helper.RunPreTestSetup();
+
+  spdy::SpdySerializedFrame req(
+      spdy_util_.ConstructSpdyGet(nullptr, 0, 1, HIGHEST));
+  spdy::SpdySerializedFrame settings_ack(spdy_util_.ConstructSpdySettingsAck());
+
+  spdy::SpdyHeaderBlock websocket_request_headers;
+  websocket_request_headers[spdy::kHttp2MethodHeader] = "CONNECT";
+  websocket_request_headers[spdy::kHttp2AuthorityHeader] = "example.test";
+  websocket_request_headers[spdy::kHttp2SchemeHeader] = "https";
+  websocket_request_headers[spdy::kHttp2PathHeader] = "/";
+  websocket_request_headers[spdy::kHttp2ProtocolHeader] = "websocket";
+  websocket_request_headers["origin"] = "http://example.test";
+  websocket_request_headers["sec-websocket-version"] = "13";
+  websocket_request_headers["sec-websocket-extensions"] =
+      "permessage-deflate; client_max_window_bits";
+  spdy::SpdySerializedFrame websocket_request(spdy_util_.ConstructSpdyHeaders(
+      3, std::move(websocket_request_headers), MEDIUM, false));
+
+  spdy::SpdySerializedFrame priority1(
+      spdy_util_.ConstructSpdyPriority(3, 0, MEDIUM, true));
+  spdy::SpdySerializedFrame priority2(
+      spdy_util_.ConstructSpdyPriority(1, 3, LOWEST, true));
+
+  MockWrite writes[] = {
+      CreateMockWrite(req, 0), CreateMockWrite(settings_ack, 2),
+      CreateMockWrite(websocket_request, 4), CreateMockWrite(priority1, 5),
+      CreateMockWrite(priority2, 6)};
+
+  spdy::SettingsMap settings;
+  settings[spdy::SETTINGS_ENABLE_CONNECT_PROTOCOL] = 1;
+  spdy::SpdySerializedFrame settings_frame(
+      spdy_util_.ConstructSpdySettings(settings));
+  spdy::SpdySerializedFrame resp1(
+      spdy_util_.ConstructSpdyGetReply(nullptr, 0, 1));
+  spdy::SpdySerializedFrame body1(spdy_util_.ConstructSpdyDataFrame(1, true));
+  spdy::SpdySerializedFrame websocket_response(
+      spdy_util_.ConstructSpdyGetReply(nullptr, 0, 3));
+  MockRead reads[] = {CreateMockRead(settings_frame, 1),
+                      CreateMockRead(resp1, 3), CreateMockRead(body1, 7),
+                      CreateMockRead(websocket_response, 8),
+                      MockRead(SYNCHRONOUS, ERR_IO_PENDING, 9)};
+
+  SequencedSocketData data(reads, writes);
+  helper.AddData(&data);
+
+  TestCompletionCallback callback1;
+  int rv = helper.trans()->Start(&request_, callback1.callback(), log_);
+  ASSERT_THAT(rv, IsError(ERR_IO_PENDING));
+
+  HttpRequestInfo request2;
+  request2.method = "GET";
+  request2.url = GURL("wss://example.test/");
+  request2.traffic_annotation =
+      net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS);
+  request2.extra_headers.SetHeader("Origin", "http://example.test");
+  request2.extra_headers.SetHeader("Sec-WebSocket-Version", "13");
+  // The following two headers must be removed by WebSocketHttp2HandshakeStream.
+  request2.extra_headers.SetHeader("Connection", "Upgrade");
+  request2.extra_headers.SetHeader("Upgrade", "websocket");
+
+  TestWebSocketHandshakeStreamCreateHelper websocket_stream_create_helper;
+
+  HttpNetworkTransaction trans2(MEDIUM, helper.session());
+  trans2.SetWebSocketHandshakeStreamCreateHelper(
+      &websocket_stream_create_helper);
+
+  TestCompletionCallback callback2;
+  rv = trans2.Start(&request2, callback2.callback(), log_);
+  ASSERT_THAT(rv, IsError(ERR_IO_PENDING));
+
+  // Make sure both requests are blocked on host resolution.
+  base::RunLoop().RunUntilIdle();
+
+  EXPECT_TRUE(helper.session_deps()->host_resolver->has_pending_requests());
+  // Complete the first DNS lookup, which should result in the first transaction
+  // creating an H2 session (And completing successfully).
+  helper.session_deps()->host_resolver->ResolveNow(1);
+  base::RunLoop().RunUntilIdle();
+
+  SpdySessionKey key1(HostPortPair::FromURL(request_.url),
+                      ProxyServer::Direct(), PRIVACY_MODE_DISABLED,
+                      SpdySessionKey::IsProxySession::kFalse, SocketTag());
+  base::WeakPtr<SpdySession> spdy_session1 =
+      helper.session()->spdy_session_pool()->FindAvailableSession(
+          key1, /* enable_ip_based_pooling = */ true,
+          /* is_websocket = */ false, log_);
+  ASSERT_TRUE(spdy_session1);
+  EXPECT_TRUE(spdy_session1->support_websocket());
+
+  // Second DNS lookup completes, which results in creating a WebSocket stream.
+  helper.session_deps()->host_resolver->ResolveNow(2);
+  ASSERT_TRUE(spdy_session1);
+
+  SpdySessionKey key2(HostPortPair::FromURL(request2.url),
+                      ProxyServer::Direct(), PRIVACY_MODE_DISABLED,
+                      SpdySessionKey::IsProxySession::kFalse, SocketTag());
+  base::WeakPtr<SpdySession> spdy_session2 =
+      helper.session()->spdy_session_pool()->FindAvailableSession(
+          key1, /* enable_ip_based_pooling = */ true,
+          /* is_websocket = */ true, log_);
+  ASSERT_TRUE(spdy_session2);
+  EXPECT_EQ(spdy_session1.get(), spdy_session2.get());
+
+  base::RunLoop().RunUntilIdle();
+
+  // First request has HIGHEST priority, WebSocket request has MEDIUM priority.
+  // Changing the priority of the first request to LOWEST changes their order,
+  // and therefore triggers sending PRIORITY frames.
+  helper.trans()->SetPriority(LOWEST);
+
+  rv = callback1.WaitForResult();
+  ASSERT_THAT(rv, IsOk());
+
+  const HttpResponseInfo* response = helper.trans()->GetResponseInfo();
+  ASSERT_TRUE(response->headers);
+  EXPECT_TRUE(response->was_fetched_via_spdy);
+  EXPECT_EQ("HTTP/1.1 200", response->headers->GetStatusLine());
+
+  std::string response_data;
+  rv = ReadTransaction(helper.trans(), &response_data);
+  EXPECT_THAT(rv, IsOk());
+  EXPECT_EQ("hello!", response_data);
+
+  rv = callback2.WaitForResult();
+  ASSERT_THAT(rv, IsOk());
+
+  helper.VerifyDataConsumed();
+}
+
+// Same as above, but the SpdySession is closed just before use, so the
+// WebSocket is sent over a new HTTP/1.x connection instead.
+TEST_F(SpdyNetworkTransactionTest,
+       WebSocketOverDetectsNewSessionWithAliasingButClosedBeforeUse) {
+  base::HistogramTester histogram_tester;
+  auto session_deps = std::make_unique<SpdySessionDependencies>();
+  session_deps->enable_websocket_over_http2 = true;
+  session_deps->host_resolver->set_ondemand_mode(true);
+  NormalSpdyTransactionHelper helper(request_, HIGHEST, log_,
+                                     std::move(session_deps));
+  helper.RunPreTestSetup();
+
+  spdy::SpdySerializedFrame req(
+      spdy_util_.ConstructSpdyGet(nullptr, 0, 1, HIGHEST));
+  spdy::SpdySerializedFrame settings_ack(spdy_util_.ConstructSpdySettingsAck());
+
+  MockWrite writes[] = {CreateMockWrite(req, 0),
+                        CreateMockWrite(settings_ack, 2)};
+
+  spdy::SettingsMap settings;
+  settings[spdy::SETTINGS_ENABLE_CONNECT_PROTOCOL] = 1;
+  spdy::SpdySerializedFrame settings_frame(
+      spdy_util_.ConstructSpdySettings(settings));
+  spdy::SpdySerializedFrame resp1(
+      spdy_util_.ConstructSpdyGetReply(nullptr, 0, 1));
+  spdy::SpdySerializedFrame body1(spdy_util_.ConstructSpdyDataFrame(1, true));
+  MockRead reads[] = {CreateMockRead(settings_frame, 1),
+                      CreateMockRead(resp1, 3), CreateMockRead(body1, 4),
+                      MockRead(SYNCHRONOUS, ERR_IO_PENDING, 5)};
+
+  SequencedSocketData data(reads, writes);
+  helper.AddData(&data);
+
+  MockWrite writes2[] = {
+      MockWrite("GET / HTTP/1.1\r\n"
+                "Host: example.test\r\n"
+                "Connection: Upgrade\r\n"
+                "Upgrade: websocket\r\n"
+                "Origin: http://example.test\r\n"
+                "Sec-WebSocket-Version: 13\r\n"
+                "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
+                "Sec-WebSocket-Extensions: permessage-deflate; "
+                "client_max_window_bits\r\n\r\n")};
+  MockRead reads2[] = {
+      MockRead("HTTP/1.1 101 Switching Protocols\r\n"
+               "Upgrade: websocket\r\n"
+               "Connection: Upgrade\r\n"
+               "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n")};
+  StaticSocketDataProvider data2(reads2, writes2);
+  auto ssl_provider2 = std::make_unique<SSLSocketDataProvider>(ASYNC, OK);
+  // Test that request has empty |alpn_protos|, that is, HTTP/2 is disabled.
+  ssl_provider2->next_protos_expected_in_ssl_config = NextProtoVector{};
+  // Force socket to use HTTP/1.1, the default protocol without ALPN.
+  ssl_provider2->next_proto = kProtoHTTP11;
+  ssl_provider2->ssl_info.cert =
+      ImportCertFromFile(GetTestCertsDirectory(), "spdy_pooling.pem");
+  helper.AddDataWithSSLSocketDataProvider(&data2, std::move(ssl_provider2));
+
+  TestCompletionCallback callback1;
+  int rv = helper.trans()->Start(&request_, callback1.callback(), log_);
+  ASSERT_THAT(rv, IsError(ERR_IO_PENDING));
+
+  HttpRequestInfo request2;
+  request2.method = "GET";
+  request2.url = GURL("wss://example.test/");
+  request2.traffic_annotation =
+      net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS);
+  request2.extra_headers.SetHeader("Connection", "Upgrade");
+  request2.extra_headers.SetHeader("Upgrade", "websocket");
+  request2.extra_headers.SetHeader("Origin", "http://example.test");
+  request2.extra_headers.SetHeader("Sec-WebSocket-Version", "13");
+
+  TestWebSocketHandshakeStreamCreateHelper websocket_stream_create_helper;
+
+  HttpNetworkTransaction trans2(MEDIUM, helper.session());
+  trans2.SetWebSocketHandshakeStreamCreateHelper(
+      &websocket_stream_create_helper);
+
+  TestCompletionCallback callback2;
+  rv = trans2.Start(&request2, callback2.callback(), log_);
+  ASSERT_THAT(rv, IsError(ERR_IO_PENDING));
+
+  // Make sure both requests are blocked on host resolution.
+  base::RunLoop().RunUntilIdle();
+
+  EXPECT_TRUE(helper.session_deps()->host_resolver->has_pending_requests());
+  // Complete the first DNS lookup, which should result in the first transaction
+  // creating an H2 session (And completing successfully).
+  helper.session_deps()->host_resolver->ResolveNow(1);
+
+  // Complete first request.
+  rv = callback1.WaitForResult();
+  ASSERT_THAT(rv, IsOk());
+  const HttpResponseInfo* response = helper.trans()->GetResponseInfo();
+  ASSERT_TRUE(response->headers);
+  EXPECT_TRUE(response->was_fetched_via_spdy);
+  EXPECT_EQ("HTTP/1.1 200", response->headers->GetStatusLine());
+  std::string response_data;
+  rv = ReadTransaction(helper.trans(), &response_data);
+  EXPECT_THAT(rv, IsOk());
+  EXPECT_EQ("hello!", response_data);
+
+  SpdySessionKey key1(HostPortPair::FromURL(request_.url),
+                      ProxyServer::Direct(), PRIVACY_MODE_DISABLED,
+                      SpdySessionKey::IsProxySession::kFalse, SocketTag());
+  base::WeakPtr<SpdySession> spdy_session1 =
+      helper.session()->spdy_session_pool()->FindAvailableSession(
+          key1, /* enable_ip_based_pooling = */ true,
+          /* is_websocket = */ false, log_);
+  ASSERT_TRUE(spdy_session1);
+  EXPECT_TRUE(spdy_session1->support_websocket());
+
+  // Second DNS lookup completes, which results in creating an alias for the
+  // SpdySession immediately, and a task is posted asynchronously to use the
+  // alias..
+  helper.session_deps()->host_resolver->ResolveNow(2);
+
+  SpdySessionKey key2(HostPortPair::FromURL(request2.url),
+                      ProxyServer::Direct(), PRIVACY_MODE_DISABLED,
+                      SpdySessionKey::IsProxySession::kFalse, SocketTag());
+  base::WeakPtr<SpdySession> spdy_session2 =
+      helper.session()->spdy_session_pool()->FindAvailableSession(
+          key1, /* enable_ip_based_pooling = */ true,
+          /* is_websocket = */ true, log_);
+  ASSERT_TRUE(spdy_session2);
+  EXPECT_EQ(spdy_session1.get(), spdy_session2.get());
+
+  // But the session is closed before it can be used.
+  helper.session()->spdy_session_pool()->CloseAllSessions();
+
+  // The second request establishes another connection (without even doing
+  // another DNS lookup) instead, and uses HTTP/1.x.
+  rv = callback2.WaitForResult();
+  ASSERT_THAT(rv, IsOk());
+
+  helper.VerifyDataConsumed();
+}
+
 TEST_F(SpdyNetworkTransactionTest, WebSocketNegotiatesHttp2) {
   HttpRequestInfo request;
   request.method = "GET";
diff --git a/net/spdy/spdy_session_pool.cc b/net/spdy/spdy_session_pool.cc
index e2447a2..8cc2dbd 100644
--- a/net/spdy/spdy_session_pool.cc
+++ b/net/spdy/spdy_session_pool.cc
@@ -190,13 +190,6 @@
           it->second->net_log().source().ToEventParametersCallback());
       return it->second;
     }
-
-    // Remove session from available sessions and from aliases, and remove
-    // key from the session's pooled alias set, so that a new session can be
-    // created with this |key|.
-    it->second->RemovePooledAlias(key);
-    UnmapKey(key);
-    RemoveAliases(key);
     return base::WeakPtr<SpdySession>();
   }
 
@@ -244,9 +237,8 @@
       if (is_websocket && !available_session->support_websocket())
         continue;
 
-      // If the session is a secure one, we need to verify that the
-      // server is authenticated to serve traffic for |host_port_proxy_pair|
-      // too.
+      // Need to verify that the server is authenticated to serve traffic for
+      // |host_port_proxy_pair| too.
       if (!available_session->VerifyDomainAuthentication(
               key.host_port_pair().host())) {
         UMA_HISTOGRAM_ENUMERATION("Net.SpdyIPPoolDomainMatch", 0, 2);
@@ -279,7 +271,8 @@
         UnmapKey(old_key);
         MapKeyToAvailableSession(new_key, available_session);
 
-        // Remap alias.
+        // Remap alias. From this point on |alias_it| is invalid, so no more
+        // iterations of the loop should be allowed.
         aliases_.insert(AliasMap::value_type(alias_it->first, new_key));
         aliases_.erase(alias_it);
 
@@ -316,6 +309,9 @@
         MapKeyToAvailableSession(key, available_session);
         available_session->AddPooledAlias(key);
       }
+      base::ThreadTaskRunnerHandle::Get()->PostTask(
+          FROM_HERE, base::BindOnce(&SpdySessionPool::UpdatePendingRequests,
+                                    weak_ptr_factory_.GetWeakPtr(), key));
       return available_session;
     }
   }
@@ -359,6 +355,146 @@
   return nullptr;
 }
 
+OnHostResolutionCallbackResult SpdySessionPool::OnHostResolutionComplete(
+    const SpdySessionKey& key,
+    bool is_websocket,
+    const AddressList& addresses) {
+  // If there are no pending requests for that alias, nothing to do.
+  if (spdy_session_request_map_.find(key) == spdy_session_request_map_.end())
+    return OnHostResolutionCallbackResult::kContinue;
+
+  // Check if there's already a matching session. If so, there may already
+  // be a pending task to inform consumers of the alias. In this case, do
+  // nothing, but inform the caller to wait for such a task to run.
+  auto existing_session_it = LookupAvailableSessionByKey(key);
+  if (existing_session_it != available_sessions_.end()) {
+    // If this is an alias, the host resolution is for a websocket
+    // connection, and the aliased session doesn't support websockets,
+    // continue looking for an aliased session that does.  Unlikely there
+    // is one, but can't hurt to check.
+    bool continue_searching_for_websockets =
+        is_websocket && !existing_session_it->second->support_websocket();
+
+    if (!continue_searching_for_websockets)
+      return OnHostResolutionCallbackResult::kMayBeDeletedAsync;
+  }
+
+  for (const auto& address : addresses) {
+    auto range = aliases_.equal_range(address);
+    for (auto alias_it = range.first; alias_it != range.second; ++alias_it) {
+      // We found a potential alias.
+      const SpdySessionKey& alias_key = alias_it->second;
+
+      auto available_session_it = LookupAvailableSessionByKey(alias_key);
+      // It shouldn't be in the aliases table if it doesn't exist!
+      DCHECK(available_session_it != available_sessions_.end());
+
+      // This session can be reused only if the proxy and privacy settings
+      // match.
+      if (!(alias_key.proxy_server() == key.proxy_server()) ||
+          !(alias_key.privacy_mode() == key.privacy_mode()) ||
+          !(alias_key.is_proxy_session() == key.is_proxy_session())) {
+        continue;
+      }
+
+      if (is_websocket && !available_session_it->second->support_websocket())
+        continue;
+
+      // Make copy of WeakPtr as call to UnmapKey() will delete original.
+      const base::WeakPtr<SpdySession> available_session =
+          available_session_it->second;
+
+      // Need to verify that the server is authenticated to serve traffic for
+      // |host_port_proxy_pair| too.
+      if (!available_session->VerifyDomainAuthentication(
+              key.host_port_pair().host())) {
+        UMA_HISTOGRAM_ENUMERATION("Net.SpdyIPPoolDomainMatch", 0, 2);
+        continue;
+      }
+
+      UMA_HISTOGRAM_ENUMERATION("Net.SpdyIPPoolDomainMatch", 1, 2);
+
+      bool adding_pooled_alias = true;
+
+      // If socket tags differ, see if session's socket tag can be changed.
+      if (alias_key.socket_tag() != key.socket_tag()) {
+        SpdySessionKey old_key = available_session->spdy_session_key();
+        SpdySessionKey new_key(old_key.host_port_pair(), old_key.proxy_server(),
+                               old_key.privacy_mode(),
+                               old_key.is_proxy_session(), key.socket_tag());
+
+        // If there is already a session with |new_key|, skip this one.
+        // It will be found in |aliases_| in a future iteration.
+        if (available_sessions_.find(new_key) != available_sessions_.end())
+          continue;
+
+        if (!available_session->ChangeSocketTag(key.socket_tag()))
+          continue;
+
+        DCHECK(available_session->spdy_session_key() == new_key);
+
+        // If this isn't a pooled alias, but the actual session that needs to
+        // have its socket tag change, there's no need to add an alias.
+        if (new_key == key)
+          adding_pooled_alias = false;
+
+        // Remap main session key.
+        UnmapKey(old_key);
+        MapKeyToAvailableSession(new_key, available_session);
+
+        // Remap alias. From this point on |alias_it| is invalid, so no more
+        // iterations of the loop should be allowed.
+        aliases_.insert(AliasMap::value_type(alias_it->first, new_key));
+        aliases_.erase(alias_it);
+
+        // Remap pooled session keys.
+        const auto& aliases = available_session->pooled_aliases();
+        for (auto it = aliases.begin(); it != aliases.end();) {
+          // Ignore aliases this loop is inserting.
+          if (it->socket_tag() == key.socket_tag()) {
+            ++it;
+            continue;
+          }
+          UnmapKey(*it);
+          SpdySessionKey new_pool_alias_key = SpdySessionKey(
+              it->host_port_pair(), it->proxy_server(), it->privacy_mode(),
+              it->is_proxy_session(), key.socket_tag());
+          MapKeyToAvailableSession(new_pool_alias_key, available_session);
+          auto old_it = it;
+          ++it;
+          available_session->RemovePooledAlias(*old_it);
+          available_session->AddPooledAlias(new_pool_alias_key);
+
+          // If this is desired key, no need to add an alias for the desired key
+          // at the end of this method.
+          if (new_pool_alias_key == key)
+            adding_pooled_alias = false;
+        }
+      }
+
+      if (adding_pooled_alias) {
+        // Add this session to the map so that we can find it next time.
+        MapKeyToAvailableSession(key, available_session);
+        available_session->AddPooledAlias(key);
+      }
+
+      // Post task to inform pending requests for session for |key| that a
+      // matching session is now available.
+      base::ThreadTaskRunnerHandle::Get()->PostTask(
+          FROM_HERE, base::BindOnce(&SpdySessionPool::UpdatePendingRequests,
+                                    weak_ptr_factory_.GetWeakPtr(), key));
+
+      // Inform the caller that the Callback may be deleted if the consumer is
+      // switched over to the newly aliased session. It's not guaranteed to be
+      // deleted, as the session may be closed, or taken by yet another pending
+      // request with a different SocketTag before the the request can try and
+      // use the session.
+      return OnHostResolutionCallbackResult::kMayBeDeletedAsync;
+    }
+  }
+  return OnHostResolutionCallbackResult::kContinue;
+}
+
 void SpdySessionPool::MakeSessionUnavailable(
     const base::WeakPtr<SpdySession>& available_session) {
   UnmapKey(available_session->spdy_session_key());
@@ -607,6 +743,20 @@
   UMA_HISTOGRAM_ENUMERATION("Net.SpdySessionGet", IMPORTED_FROM_SOCKET,
                             SPDY_SESSION_GET_MAX);
 
+  // If there's a pre-existing matching session, it has to be an alias. Remove
+  // the alias.
+  auto it = LookupAvailableSessionByKey(key);
+  if (it != available_sessions_.end()) {
+    DCHECK(key != it->second->spdy_session_key());
+
+    // Remove session from available sessions and from aliases, and remove
+    // key from the session's pooled alias set, so that a new session can be
+    // created with this |key|.
+    it->second->RemovePooledAlias(key);
+    UnmapKey(key);
+    RemoveAliases(key);
+  }
+
   return std::make_unique<SpdySession>(
       key, http_server_properties_, transport_security_state_,
       ssl_config_service_, quic_supported_versions_,
diff --git a/net/spdy/spdy_session_pool.h b/net/spdy/spdy_session_pool.h
index c7ec1e3..9d9476ec 100644
--- a/net/spdy/spdy_session_pool.h
+++ b/net/spdy/spdy_session_pool.h
@@ -28,6 +28,7 @@
 #include "net/cert/cert_database.h"
 #include "net/log/net_log_source.h"
 #include "net/proxy_resolution/proxy_config.h"
+#include "net/socket/connect_job.h"
 #include "net/spdy/http2_push_promise_index.h"
 #include "net/spdy/server_push_delegate.h"
 #include "net/spdy/spdy_session_key.h"
@@ -231,6 +232,14 @@
       std::unique_ptr<SpdySessionRequest>* spdy_session_request,
       bool* is_blocking_request_for_session);
 
+  // Invoked when a host resolution completes. Returns
+  // OnHostResolutionCallbackResult::kMayBeDeletedAsync if there's a SPDY
+  // session that's a suitable alias for |key|, setting up the alias if needed.
+  OnHostResolutionCallbackResult OnHostResolutionComplete(
+      const SpdySessionKey& key,
+      bool is_websocket,
+      const AddressList& addresses);
+
   // Remove all mappings and aliases for the given session, which must
   // still be available. Except for in tests, this must be called by
   // the given session itself.
diff --git a/net/spdy/spdy_test_util_common.cc b/net/spdy/spdy_test_util_common.cc
index 085e89d..a7fde303 100644
--- a/net/spdy/spdy_test_util_common.cc
+++ b/net/spdy/spdy_test_util_common.cc
@@ -495,7 +495,7 @@
   scoped_refptr<ClientSocketPool::SocketParams> socket_params =
       base::MakeRefCounted<ClientSocketPool::SocketParams>(
           std::make_unique<SSLConfig>() /* ssl_config_for_origin */,
-          nullptr /* ssl_config_for_proxy */, OnHostResolutionCallback());
+          nullptr /* ssl_config_for_proxy */);
   int rv = connection->Init(
       ClientSocketPool::GroupId(key.host_port_pair(),
                                 ClientSocketPool::SocketType::kSsl,
diff --git a/net/tools/quic/quic_simple_server_session_helper_test.cc b/net/tools/quic/quic_simple_server_session_helper_test.cc
deleted file mode 100644
index 581b9ae..0000000
--- a/net/tools/quic/quic_simple_server_session_helper_test.cc
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright 2013 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "net/third_party/quiche/src/quic/core/quic_connection_id.h"
-#include "net/third_party/quiche/src/quic/core/quic_utils.h"
-#include "net/third_party/quiche/src/quic/test_tools/mock_random.h"
-#include "net/third_party/quiche/src/quic/test_tools/quic_test_utils.h"
-#include "net/third_party/quiche/src/quic/tools/quic_simple_crypto_server_stream_helper.h"
-
-#include "testing/gtest/include/gtest/gtest.h"
-
-namespace net {
-
-TEST(QuicSimpleCryptoServerStreamHelperTest, GenerateConnectionIdForReject) {
-  quic::test::MockRandom random;
-  quic::QuicSimpleCryptoServerStreamHelper helper(&random);
-
-  EXPECT_EQ(quic::QuicUtils::CreateRandomConnectionId(&random),
-            // N.B., version number and ID are ignored in the helper.
-            helper.GenerateConnectionIdForReject(
-                quic::QUIC_VERSION_46, quic::test::TestConnectionId(42)));
-}
-
-}  // namespace net
diff --git a/net/websockets/websocket_basic_stream_adapters_test.cc b/net/websockets/websocket_basic_stream_adapters_test.cc
index 02403680..18a3cedd 100644
--- a/net/websockets/websocket_basic_stream_adapters_test.cc
+++ b/net/websockets/websocket_basic_stream_adapters_test.cc
@@ -22,7 +22,6 @@
 #include "net/http/http_network_session.h"
 #include "net/log/net_log_with_source.h"
 #include "net/socket/client_socket_handle.h"
-#include "net/socket/client_socket_pool_manager_impl.h"
 #include "net/socket/connect_job.h"
 #include "net/socket/socket_tag.h"
 #include "net/socket/socket_test_util.h"
@@ -56,41 +55,10 @@
  protected:
   WebSocketClientSocketHandleAdapterTest()
       : host_port_pair_("www.example.org", 443),
-        socket_pool_manager_(std::make_unique<ClientSocketPoolManagerImpl>(
-            CommonConnectJobParams(
-                &socket_factory_,
-                &host_resolver,
-                nullptr /* http_auth_cache */,
-                nullptr /* http_auth_handler_factory */,
-                nullptr /* spdy_session_pool */,
-                nullptr /* quic_supported_versions */,
-                nullptr /* quic_stream_factory */,
-                nullptr /* proxy_delegate */,
-                nullptr /* http_user_agent_settings */,
-                SSLClientSocketContext(),
-                SSLClientSocketContext(),
-                nullptr /* socket_performance_watcher_factory */,
-                nullptr /* network_quality_estimator */,
-                net_log_.net_log(),
-                nullptr /* websocket_endpoint_lock_manager */),
-            CommonConnectJobParams(
-                &socket_factory_,
-                &host_resolver,
-                nullptr /* http_auth_cache */,
-                nullptr /* http_auth_handler_factory */,
-                nullptr /* spdy_session_pool */,
-                nullptr /* quic_supported_versions */,
-                nullptr /* quic_stream_factory */,
-                nullptr /* proxy_delegate */,
-                nullptr /* http_user_agent_settings */,
-                SSLClientSocketContext(),
-                SSLClientSocketContext(),
-                nullptr /* socket_performance_watcher_factory */,
-                nullptr /* network_quality_estimator */,
-                net_log_.net_log(),
-                &websocket_endpoint_lock_manager_),
-            nullptr /* ssl_config_service */,
-            HttpNetworkSession::NORMAL_SOCKET_POOL)) {}
+        network_session_(
+            SpdySessionDependencies::SpdyCreateSession(&session_deps_)),
+        websocket_endpoint_lock_manager_(
+            network_session_->websocket_endpoint_lock_manager()) {}
 
   ~WebSocketClientSocketHandleAdapterTest() override = default;
 
@@ -98,7 +66,7 @@
     scoped_refptr<ClientSocketPool::SocketParams> socks_params =
         base::MakeRefCounted<ClientSocketPool::SocketParams>(
             std::make_unique<SSLConfig>() /* ssl_config_for_origin */,
-            nullptr /* ssl_config_for_proxy */, OnHostResolutionCallback());
+            nullptr /* ssl_config_for_proxy */);
     TestCompletionCallback callback;
     int rv = connection->Init(
         ClientSocketPool::GroupId(host_port_pair_,
@@ -107,17 +75,17 @@
         socks_params, TRAFFIC_ANNOTATION_FOR_TESTS /* proxy_annotation_tag */,
         MEDIUM, SocketTag(), ClientSocketPool::RespectLimits::ENABLED,
         callback.callback(), ClientSocketPool::ProxyAuthCallback(),
-        socket_pool_manager_->GetSocketPool(ProxyServer::Direct()), net_log_);
+        network_session_->GetSocketPool(HttpNetworkSession::NORMAL_SOCKET_POOL,
+                                        ProxyServer::Direct()),
+        NetLogWithSource());
     rv = callback.GetResult(rv);
     return rv == OK;
   }
 
   const HostPortPair host_port_pair_;
-  NetLogWithSource net_log_;
-  MockClientSocketFactory socket_factory_;
-  MockHostResolver host_resolver;
-  std::unique_ptr<ClientSocketPoolManagerImpl> socket_pool_manager_;
-  WebSocketEndpointLockManager websocket_endpoint_lock_manager_;
+  SpdySessionDependencies session_deps_;
+  std::unique_ptr<HttpNetworkSession> network_session_;
+  WebSocketEndpointLockManager* websocket_endpoint_lock_manager_;
 };
 
 TEST_F(WebSocketClientSocketHandleAdapterTest, Uninitialized) {
@@ -128,9 +96,9 @@
 
 TEST_F(WebSocketClientSocketHandleAdapterTest, IsInitialized) {
   StaticSocketDataProvider data;
-  socket_factory_.AddSocketDataProvider(&data);
+  session_deps_.socket_factory->AddSocketDataProvider(&data);
   SSLSocketDataProvider ssl_socket_data(ASYNC, OK);
-  socket_factory_.AddSSLSocketDataProvider(&ssl_socket_data);
+  session_deps_.socket_factory->AddSSLSocketDataProvider(&ssl_socket_data);
 
   auto connection = std::make_unique<ClientSocketHandle>();
   ClientSocketHandle* const connection_ptr = connection.get();
@@ -145,9 +113,9 @@
 
 TEST_F(WebSocketClientSocketHandleAdapterTest, Disconnect) {
   StaticSocketDataProvider data;
-  socket_factory_.AddSocketDataProvider(&data);
+  session_deps_.socket_factory->AddSocketDataProvider(&data);
   SSLSocketDataProvider ssl_socket_data(ASYNC, OK);
-  socket_factory_.AddSSLSocketDataProvider(&ssl_socket_data);
+  session_deps_.socket_factory->AddSSLSocketDataProvider(&ssl_socket_data);
 
   auto connection = std::make_unique<ClientSocketHandle>();
   EXPECT_TRUE(InitClientSocketHandle(connection.get()));
@@ -165,9 +133,9 @@
 TEST_F(WebSocketClientSocketHandleAdapterTest, Read) {
   MockRead reads[] = {MockRead(SYNCHRONOUS, "foo"), MockRead("bar")};
   StaticSocketDataProvider data(reads, base::span<MockWrite>());
-  socket_factory_.AddSocketDataProvider(&data);
+  session_deps_.socket_factory->AddSocketDataProvider(&data);
   SSLSocketDataProvider ssl_socket_data(ASYNC, OK);
-  socket_factory_.AddSSLSocketDataProvider(&ssl_socket_data);
+  session_deps_.socket_factory->AddSSLSocketDataProvider(&ssl_socket_data);
 
   auto connection = std::make_unique<ClientSocketHandle>();
   EXPECT_TRUE(InitClientSocketHandle(connection.get()));
@@ -196,9 +164,9 @@
 TEST_F(WebSocketClientSocketHandleAdapterTest, ReadIntoSmallBuffer) {
   MockRead reads[] = {MockRead(SYNCHRONOUS, "foo"), MockRead("bar")};
   StaticSocketDataProvider data(reads, base::span<MockWrite>());
-  socket_factory_.AddSocketDataProvider(&data);
+  session_deps_.socket_factory->AddSocketDataProvider(&data);
   SSLSocketDataProvider ssl_socket_data(ASYNC, OK);
-  socket_factory_.AddSSLSocketDataProvider(&ssl_socket_data);
+  session_deps_.socket_factory->AddSSLSocketDataProvider(&ssl_socket_data);
 
   auto connection = std::make_unique<ClientSocketHandle>();
   EXPECT_TRUE(InitClientSocketHandle(connection.get()));
@@ -235,9 +203,9 @@
 TEST_F(WebSocketClientSocketHandleAdapterTest, Write) {
   MockWrite writes[] = {MockWrite(SYNCHRONOUS, "foo"), MockWrite("bar")};
   StaticSocketDataProvider data(base::span<MockRead>(), writes);
-  socket_factory_.AddSocketDataProvider(&data);
+  session_deps_.socket_factory->AddSocketDataProvider(&data);
   SSLSocketDataProvider ssl_socket_data(ASYNC, OK);
-  socket_factory_.AddSSLSocketDataProvider(&ssl_socket_data);
+  session_deps_.socket_factory->AddSSLSocketDataProvider(&ssl_socket_data);
 
   auto connection = std::make_unique<ClientSocketHandle>();
   EXPECT_TRUE(InitClientSocketHandle(connection.get()));
@@ -269,9 +237,9 @@
   MockRead reads[] = {MockRead("foobar")};
   MockWrite writes[] = {MockWrite("baz")};
   StaticSocketDataProvider data(reads, writes);
-  socket_factory_.AddSocketDataProvider(&data);
+  session_deps_.socket_factory->AddSocketDataProvider(&data);
   SSLSocketDataProvider ssl_socket_data(ASYNC, OK);
-  socket_factory_.AddSSLSocketDataProvider(&ssl_socket_data);
+  session_deps_.socket_factory->AddSSLSocketDataProvider(&ssl_socket_data);
 
   auto connection = std::make_unique<ClientSocketHandle>();
   EXPECT_TRUE(InitClientSocketHandle(connection.get()));
@@ -345,18 +313,17 @@
   }
 
   base::WeakPtr<SpdySession> CreateSpdySession() {
-    return ::net::CreateSpdySession(session_.get(), key_, net_log_);
+    return ::net::CreateSpdySession(session_.get(), key_, NetLogWithSource());
   }
 
   base::WeakPtr<SpdyStream> CreateSpdyStream(
       base::WeakPtr<SpdySession> session) {
     return CreateStreamSynchronously(SPDY_BIDIRECTIONAL_STREAM, session, url_,
-                                     LOWEST, net_log_);
+                                     LOWEST, NetLogWithSource());
   }
 
   SpdyTestUtil spdy_util_;
   StrictMock<MockDelegate> mock_delegate_;
-  NetLogWithSource net_log_;
 
  private:
   const GURL url_;
@@ -375,7 +342,8 @@
 
   base::WeakPtr<SpdySession> session = CreateSpdySession();
   base::WeakPtr<SpdyStream> stream = CreateSpdyStream(session);
-  WebSocketSpdyStreamAdapter adapter(stream, &mock_delegate_, net_log_);
+  WebSocketSpdyStreamAdapter adapter(stream, &mock_delegate_,
+                                     NetLogWithSource());
   EXPECT_TRUE(adapter.is_initialized());
 
   base::RunLoop().RunUntilIdle();
@@ -408,7 +376,8 @@
 
   base::WeakPtr<SpdySession> session = CreateSpdySession();
   base::WeakPtr<SpdyStream> stream = CreateSpdyStream(session);
-  WebSocketSpdyStreamAdapter adapter(stream, &mock_delegate_, net_log_);
+  WebSocketSpdyStreamAdapter adapter(stream, &mock_delegate_,
+                                     NetLogWithSource());
   EXPECT_TRUE(adapter.is_initialized());
 
   int rv = stream->SendRequestHeaders(RequestHeaders(), MORE_DATA_TO_SEND);
@@ -449,7 +418,8 @@
 
   base::WeakPtr<SpdySession> session = CreateSpdySession();
   base::WeakPtr<SpdyStream> stream = CreateSpdyStream(session);
-  WebSocketSpdyStreamAdapter adapter(stream, &mock_delegate_, net_log_);
+  WebSocketSpdyStreamAdapter adapter(stream, &mock_delegate_,
+                                     NetLogWithSource());
   EXPECT_TRUE(adapter.is_initialized());
 
   int rv = stream->SendRequestHeaders(RequestHeaders(), MORE_DATA_TO_SEND);
@@ -491,7 +461,8 @@
 
   base::WeakPtr<SpdySession> session = CreateSpdySession();
   base::WeakPtr<SpdyStream> stream = CreateSpdyStream(session);
-  WebSocketSpdyStreamAdapter adapter(stream, &mock_delegate_, net_log_);
+  WebSocketSpdyStreamAdapter adapter(stream, &mock_delegate_,
+                                     NetLogWithSource());
   EXPECT_TRUE(adapter.is_initialized());
 
   int rv = stream->SendRequestHeaders(RequestHeaders(), MORE_DATA_TO_SEND);
@@ -522,7 +493,8 @@
 
   base::WeakPtr<SpdySession> session = CreateSpdySession();
   base::WeakPtr<SpdyStream> stream = CreateSpdyStream(session);
-  WebSocketSpdyStreamAdapter adapter(stream, &mock_delegate_, net_log_);
+  WebSocketSpdyStreamAdapter adapter(stream, &mock_delegate_,
+                                     NetLogWithSource());
   EXPECT_TRUE(adapter.is_initialized());
 
   EXPECT_TRUE(session);
@@ -550,7 +522,8 @@
 
   base::WeakPtr<SpdySession> session = CreateSpdySession();
   base::WeakPtr<SpdyStream> stream = CreateSpdyStream(session);
-  WebSocketSpdyStreamAdapter adapter(stream, &mock_delegate_, net_log_);
+  WebSocketSpdyStreamAdapter adapter(stream, &mock_delegate_,
+                                     NetLogWithSource());
   EXPECT_TRUE(adapter.is_initialized());
 
   int rv = stream->SendRequestHeaders(RequestHeaders(), MORE_DATA_TO_SEND);
@@ -585,7 +558,8 @@
 
   base::WeakPtr<SpdySession> session = CreateSpdySession();
   base::WeakPtr<SpdyStream> stream = CreateSpdyStream(session);
-  WebSocketSpdyStreamAdapter adapter(stream, &mock_delegate_, net_log_);
+  WebSocketSpdyStreamAdapter adapter(stream, &mock_delegate_,
+                                     NetLogWithSource());
   EXPECT_TRUE(adapter.is_initialized());
 
   int rv = stream->SendRequestHeaders(RequestHeaders(), MORE_DATA_TO_SEND);
@@ -615,7 +589,8 @@
 
   base::WeakPtr<SpdySession> session = CreateSpdySession();
   base::WeakPtr<SpdyStream> stream = CreateSpdyStream(session);
-  WebSocketSpdyStreamAdapter adapter(stream, &mock_delegate_, net_log_);
+  WebSocketSpdyStreamAdapter adapter(stream, &mock_delegate_,
+                                     NetLogWithSource());
   EXPECT_TRUE(adapter.is_initialized());
 
   // No Delegate methods shall be called after this.
@@ -660,7 +635,8 @@
 
   base::WeakPtr<SpdySession> session = CreateSpdySession();
   base::WeakPtr<SpdyStream> stream = CreateSpdyStream(session);
-  WebSocketSpdyStreamAdapter adapter(stream, &mock_delegate_, net_log_);
+  WebSocketSpdyStreamAdapter adapter(stream, &mock_delegate_,
+                                     NetLogWithSource());
   EXPECT_TRUE(adapter.is_initialized());
 
   int rv = stream->SendRequestHeaders(RequestHeaders(), MORE_DATA_TO_SEND);
@@ -728,7 +704,8 @@
 
   base::WeakPtr<SpdySession> session = CreateSpdySession();
   base::WeakPtr<SpdyStream> stream = CreateSpdyStream(session);
-  WebSocketSpdyStreamAdapter adapter(stream, &mock_delegate_, net_log_);
+  WebSocketSpdyStreamAdapter adapter(stream, &mock_delegate_,
+                                     NetLogWithSource());
   EXPECT_TRUE(adapter.is_initialized());
 
   int rv = stream->SendRequestHeaders(RequestHeaders(), MORE_DATA_TO_SEND);
@@ -784,7 +761,7 @@
 
   base::WeakPtr<SpdySession> session = CreateSpdySession();
   base::WeakPtr<SpdyStream> stream = CreateSpdyStream(session);
-  WebSocketSpdyStreamAdapter adapter(stream, nullptr, net_log_);
+  WebSocketSpdyStreamAdapter adapter(stream, nullptr, NetLogWithSource());
   EXPECT_TRUE(adapter.is_initialized());
 
   int rv = stream->SendRequestHeaders(RequestHeaders(), MORE_DATA_TO_SEND);
@@ -829,7 +806,7 @@
 
   base::WeakPtr<SpdySession> session = CreateSpdySession();
   base::WeakPtr<SpdyStream> stream = CreateSpdyStream(session);
-  WebSocketSpdyStreamAdapter adapter(stream, nullptr, net_log_);
+  WebSocketSpdyStreamAdapter adapter(stream, nullptr, NetLogWithSource());
   EXPECT_TRUE(adapter.is_initialized());
 
   int rv = stream->SendRequestHeaders(RequestHeaders(), MORE_DATA_TO_SEND);
@@ -903,7 +880,7 @@
   base::WeakPtr<SpdySession> session = CreateSpdySession();
   base::WeakPtr<SpdyStream> stream = CreateSpdyStream(session);
   auto adapter = std::make_unique<WebSocketSpdyStreamAdapter>(
-      stream, &mock_delegate_, net_log_);
+      stream, &mock_delegate_, NetLogWithSource());
   EXPECT_TRUE(adapter->is_initialized());
 
   int rv = stream->SendRequestHeaders(RequestHeaders(), MORE_DATA_TO_SEND);
@@ -953,7 +930,7 @@
   base::WeakPtr<SpdySession> session = CreateSpdySession();
   base::WeakPtr<SpdyStream> stream = CreateSpdyStream(session);
   auto adapter = std::make_unique<WebSocketSpdyStreamAdapter>(
-      stream, &mock_delegate_, net_log_);
+      stream, &mock_delegate_, NetLogWithSource());
   EXPECT_TRUE(adapter->is_initialized());
 
   int rv = stream->SendRequestHeaders(RequestHeaders(), MORE_DATA_TO_SEND);
diff --git a/services/network/public/cpp/network_connection_tracker.h b/services/network/public/cpp/network_connection_tracker.h
index be9fd29d..e338ac5 100644
--- a/services/network/public/cpp/network_connection_tracker.h
+++ b/services/network/public/cpp/network_connection_tracker.h
@@ -65,7 +65,8 @@
   // will contain the current connection type, and |callback| will not be
   // called; Otherwise, returns false and does not modify |type|, in which
   // case, |callback| will be called on the calling thread when connection type
-  // is ready. This method is thread safe. Please also refer to
+  // is ready. The connection type being available does not imply it is not
+  // CONNECTION_UNKNKOWN. This method is thread safe. Please also refer to
   // net::NetworkChangeNotifier::GetConnectionType() for documentation.
   virtual bool GetConnectionType(network::mojom::ConnectionType* type,
                                  ConnectionTypeCallback callback);
diff --git a/skia/BUILD.gn b/skia/BUILD.gn
index 789762f..a84a225 100644
--- a/skia/BUILD.gn
+++ b/skia/BUILD.gn
@@ -408,7 +408,7 @@
     ]
   }
 
-  if (is_linux || is_android || is_fuchsia) {
+  if (is_linux || is_android) {
     sources += [
       # Retain the files for the SkFontMgr_Android on linux to emulate android
       # fonts. See content/zygote/zygote_main_linux.cc
@@ -434,6 +434,9 @@
     ]
     deps += [
       "//third_party/fuchsia-sdk/sdk:fonts",
+      "//third_party/fuchsia-sdk/sdk:fonts",
+      "//third_party/fuchsia-sdk/sdk:io",
+      "//third_party/fuchsia-sdk/sdk:sys",
       "//third_party/fuchsia-sdk/sdk:zx",
       "//third_party/icu:icuuc",
     ]
@@ -749,7 +752,7 @@
 }
 
 # Font copies.
-if (is_android || is_fuchsia) {
+if (is_android) {
   copy("copy_android_fonts_config") {
     sources = [
       "ext/data/test_fonts/android_fallback_fonts.xml",
@@ -760,6 +763,16 @@
     ]
   }
 }
+if (is_fuchsia) {
+  copy("copy_fuchsia_fonts_manifest") {
+    sources = [
+      "ext/data/test_fonts/fuchsia_test_fonts_manifest.json",
+    ]
+    outputs = [
+      "$root_out_dir/test_fonts/{{source_file_part}}",
+    ]
+  }
+}
 if (is_mac) {
   bundle_data("test_fonts_bundle_data") {
     public_deps = [
@@ -789,10 +802,14 @@
     data_deps += [ "//third_party/test_fonts" ]
   }
 
-  if (is_android || is_fuchsia) {
+  if (is_android) {
     deps += [ ":copy_android_fonts_config" ]
     data_deps += [ ":copy_android_fonts_config" ]
   }
+  if (is_fuchsia) {
+    deps += [ ":copy_fuchsia_fonts_manifest" ]
+    data_deps += [ ":copy_fuchsia_fonts_manifest" ]
+  }
 }
 
 source_set("test_fonts") {
diff --git a/skia/ext/data/test_fonts/fuchsia_test_fonts_manifest.json b/skia/ext/data/test_fonts/fuchsia_test_fonts_manifest.json
new file mode 100644
index 0000000..bc6962af
--- /dev/null
+++ b/skia/ext/data/test_fonts/fuchsia_test_fonts_manifest.json
@@ -0,0 +1,205 @@
+{
+  "families": [
+    {
+      "family": "Arimo",
+      "aliases": [
+        "sans",
+        "sans serif",
+        "sans-serif",
+        "Arial",
+        "Helvetica"
+      ],
+      "fallback": true,
+      "fallback_group": "sans-serif",
+      "fonts": [
+        {
+          "asset": "Arimo-Regular.ttf"
+        },
+        {
+          "asset": "Arimo-Bold.ttf",
+          "weight": 700
+        },
+        {
+          "asset": "Arimo-Italic.ttf",
+          "slant": "italic"
+        },
+        {
+          "asset": "Arimo-BoldItalic.ttf",
+          "weight": 700,
+          "slant": "italic"
+        }
+      ]
+    },
+    {
+      "family": "Tinos",
+      "aliases": [
+        "serif",
+        "Times",
+        "Times New Roman",
+        "Monaco",
+        "SubpixelPositioning"
+      ],
+      "fallback": true,
+      "fallback_group": "serif",
+      "fonts": [
+        {
+          "asset": "Tinos-Regular.ttf"
+        },
+        {
+          "asset": "Tinos-Bold.ttf",
+          "weight": 700
+        },
+        {
+          "asset": "Tinos-Italic.ttf",
+          "slant": "italic"
+        },
+        {
+          "asset": "Tinos-BoldItalic.ttf",
+          "weight": 700,
+          "slant": "italic"
+        }
+      ]
+    },
+    {
+      "family": "Cousine",
+      "aliases": [
+        "mono",
+        "monospace",
+        "Courier",
+        "Courier New"
+      ],
+      "fallback": true,
+      "fallback_group": "monospace",
+      "fonts": [
+        {
+          "asset": "Cousine-Regular.ttf"
+        },
+        {
+          "asset": "Cousine-Bold.ttf",
+          "weight": 700
+        },
+        {
+          "asset": "Cousine-Italic.ttf",
+          "slant": "italic"
+        },
+        {
+          "asset": "Cousine-BoldItalic.ttf",
+          "weight": 700,
+          "slant": "italic"
+        }
+      ]
+    },
+    {
+      "family": "Gelasio",
+      "aliases": [
+        "Georgia"
+      ],
+      "fallback": false,
+      "fonts": [
+        {
+          "asset": "Gelasio-Regular.ttf"
+        },
+        {
+          "asset": "Gelasio-Bold.ttf",
+          "weight": 700
+        },
+        {
+          "asset": "Gelasio-Italic.ttf",
+          "slant": "italic"
+        },
+        {
+          "asset": "Gelasio-BoldItalic.ttf",
+          "weight": 700,
+          "slant": "italic"
+        }
+      ]
+    },
+    {
+      "family": "Ahem",
+      "aliases": [
+        "SubpixelPositioningAhem"
+      ],
+      "fallback": false,
+      "fallback_group": "sans-serif",
+      "fonts": [
+        {
+          "asset": "Ahem.ttf"
+        }
+      ]
+    },
+    {
+      "family": "DejaVu Sans",
+      "fallback": true,
+      "fonts": [
+        {
+          "asset": "DejaVuSans.ttf"
+        }
+      ]
+    },
+    {
+      "family": "Garuda",
+      "fallback": true,
+      "fonts": [
+        {
+          "asset": "Garuda.ttf"
+        }
+      ]
+    },
+    {
+      "family": "Lohit Devanagari",
+      "fallback": true,
+      "fonts": [
+        {
+          "asset": "Lohit-Devanagari.ttf"
+        }
+      ]
+    },
+    {
+      "family": "Lohit Gurmukhi",
+      "fallback": true,
+      "fonts": [
+        {
+          "asset": "Lohit-Gurmukhi.ttf"
+        }
+      ]
+    },
+    {
+      "family": "Lohit Tamil",
+      "fallback": true,
+      "fonts": [
+        {
+          "asset": "Lohit-Tamil.ttf"
+        }
+      ]
+    },
+    {
+      "family": "Mukti",
+      "fallback": true,
+      "fonts": [
+        {
+          "asset": "MuktiNarrow.ttf"
+        }
+      ]
+    },
+    {
+      "family": "Noto Sans Khmer",
+      "fallback": true,
+      "fonts": [
+        {
+          "asset": "NotoSansKhmer-Regular.ttf",
+          "language": "km"
+        }
+      ]
+    },
+    {
+      "family": "Noto Sans CJK JP",
+      "fallback": true,
+      "fonts": [
+        {
+          "asset": "NotoSansCJKjp-Regular.otf",
+          "language": "ja"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/skia/ext/test_fonts_fuchsia.cc b/skia/ext/test_fonts_fuchsia.cc
index 073ff91..d277605 100644
--- a/skia/ext/test_fonts_fuchsia.cc
+++ b/skia/ext/test_fonts_fuchsia.cc
@@ -4,25 +4,63 @@
 
 #include "skia/ext/test_fonts.h"
 
+#include <fuchsia/fonts/cpp/fidl.h>
+#include <fuchsia/io/cpp/fidl.h>
+#include <fuchsia/sys/cpp/fidl.h>
+#include <lib/fidl/cpp/interface_handle.h>
+
+#include "base/fuchsia/file_utils.h"
+#include "base/fuchsia/service_directory_client.h"
+#include "base/no_destructor.h"
+#include "base/path_service.h"
 #include "skia/ext/fontmgr_default.h"
 #include "third_party/skia/include/core/SkFontMgr.h"
-#include "third_party/skia/include/ports/SkFontMgr_android.h"
+#include "third_party/skia/include/ports/SkFontMgr_fuchsia.h"
 
 namespace skia {
 
 void ConfigureTestFont() {
-  // TODO(https://crbugs.com/927980): Use SkFontMgr_Fuchsia instead of
-  // SkFontMgr_Android. We just need to start a fonts::Provider instance with
-  // a custom manifest files and connect to it instead of the default
-  // fonts::Provider instance..
-  SkFontMgr_Android_CustomFonts custom;
-  custom.fSystemFontUse = SkFontMgr_Android_CustomFonts::kOnlyCustom;
-  custom.fBasePath = "/pkg/test_fonts/";
-  custom.fFontsXml = "/pkg/test_fonts/android_main_fonts.xml";
-  custom.fFallbackFontsXml = "/pkg/test_fonts/android_fallback_fonts.xml";
-  custom.fIsolated = false;
+  // ComponentController for the font provider service started below. It's a
+  // static field to keep the service running until the test process is
+  // destroyed.
+  static base::NoDestructor<
+      fidl::InterfaceHandle<fuchsia::sys::ComponentController>>
+      test_font_provider_controller;
+  DCHECK(!*test_font_provider_controller);
 
-  skia::OverrideDefaultSkFontMgr(SkFontMgr_New_Android(&custom));
+  // Start a fuchsia.fonts.Provider instance and configure it to load the test
+  // fonts, which must be bundled in the calling process' package.
+  fuchsia::sys::LaunchInfo launch_info;
+  launch_info.url = "fuchsia-pkg://fuchsia.com/fonts#meta/fonts.cmx";
+  launch_info.arguments.reset(
+      {"--no-default-fonts",
+       "--font-manifest=/test_fonts/fuchsia_test_fonts_manifest.json"});
+  launch_info.flat_namespace = fuchsia::sys::FlatNamespace::New();
+  launch_info.flat_namespace->paths.push_back("/test_fonts");
+
+  base::FilePath assets_path;
+  if (!base::PathService::Get(base::DIR_ASSETS, &assets_path))
+    LOG(FATAL) << "Can't get DIR_ASSETS";
+  launch_info.flat_namespace->directories.push_back(
+      base::fuchsia::OpenDirectory(assets_path.AppendASCII("test_fonts"))
+          .TakeChannel());
+
+  fidl::InterfaceHandle<fuchsia::io::Directory> font_provider_services_dir;
+  launch_info.directory_request =
+      font_provider_services_dir.NewRequest().TakeChannel();
+
+  fuchsia::sys::LauncherSyncPtr launcher =
+      base::fuchsia::ServiceDirectoryClient::ForCurrentProcess()
+          ->ConnectToServiceSync<fuchsia::sys::Launcher>();
+  launcher->CreateComponent(std::move(launch_info),
+                            test_font_provider_controller->NewRequest());
+
+  base::fuchsia::ServiceDirectoryClient font_provider_services_client(
+      std::move(font_provider_services_dir));
+
+  skia::OverrideDefaultSkFontMgr(SkFontMgr_New_Fuchsia(
+      font_provider_services_client
+          .ConnectToServiceSync<fuchsia::fonts::Provider>()));
 }
 
 }  // namespace skia
diff --git a/testing/buildbot/chromium.android.fyi.json b/testing/buildbot/chromium.android.fyi.json
index f8f5a020..0723d16b 100644
--- a/testing/buildbot/chromium.android.fyi.json
+++ b/testing/buildbot/chromium.android.fyi.json
@@ -1,680 +1,6 @@
 {
   "AAAAA1 AUTOGENERATED FILE DO NOT EDIT": {},
   "AAAAA2 See generate_buildbot_json.py to make changes": {},
-  "Android Cronet Builder (dbg)": {
-    "gtest_tests": [
-      {
-        "args": [
-          "--gs-results-bucket=chromium-result-details",
-          "--recover-devices"
-        ],
-        "merge": {
-          "args": [
-            "--bucket",
-            "chromium-result-details",
-            "--test-name",
-            "cronet_sample_test_apk"
-          ],
-          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
-              "location": "bin",
-              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
-            }
-          ],
-          "dimension_sets": [
-            {
-              "device_os": "KTU84P",
-              "device_type": "hammerhead",
-              "os": "Android"
-            }
-          ],
-          "output_links": [
-            {
-              "link": [
-                "https://luci-logdog.appspot.com/v/?s",
-                "=android%2Fswarming%2Flogcats%2F",
-                "${TASK_ID}%2F%2B%2Funified_logcats"
-              ],
-              "name": "shard #${SHARD_INDEX} logcats"
-            }
-          ]
-        },
-        "test": "cronet_sample_test_apk"
-      },
-      {
-        "args": [
-          "--gs-results-bucket=chromium-result-details",
-          "--recover-devices"
-        ],
-        "merge": {
-          "args": [
-            "--bucket",
-            "chromium-result-details",
-            "--test-name",
-            "cronet_smoketests_missing_native_library_instrumentation_apk"
-          ],
-          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
-              "location": "bin",
-              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
-            }
-          ],
-          "dimension_sets": [
-            {
-              "device_os": "KTU84P",
-              "device_type": "hammerhead",
-              "os": "Android"
-            }
-          ],
-          "output_links": [
-            {
-              "link": [
-                "https://luci-logdog.appspot.com/v/?s",
-                "=android%2Fswarming%2Flogcats%2F",
-                "${TASK_ID}%2F%2B%2Funified_logcats"
-              ],
-              "name": "shard #${SHARD_INDEX} logcats"
-            }
-          ]
-        },
-        "test": "cronet_smoketests_missing_native_library_instrumentation_apk"
-      },
-      {
-        "args": [
-          "--gs-results-bucket=chromium-result-details",
-          "--recover-devices"
-        ],
-        "merge": {
-          "args": [
-            "--bucket",
-            "chromium-result-details",
-            "--test-name",
-            "cronet_smoketests_platform_only_instrumentation_apk"
-          ],
-          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
-              "location": "bin",
-              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
-            }
-          ],
-          "dimension_sets": [
-            {
-              "device_os": "KTU84P",
-              "device_type": "hammerhead",
-              "os": "Android"
-            }
-          ],
-          "output_links": [
-            {
-              "link": [
-                "https://luci-logdog.appspot.com/v/?s",
-                "=android%2Fswarming%2Flogcats%2F",
-                "${TASK_ID}%2F%2B%2Funified_logcats"
-              ],
-              "name": "shard #${SHARD_INDEX} logcats"
-            }
-          ]
-        },
-        "test": "cronet_smoketests_platform_only_instrumentation_apk"
-      },
-      {
-        "args": [
-          "--gs-results-bucket=chromium-result-details",
-          "--recover-devices"
-        ],
-        "merge": {
-          "args": [
-            "--bucket",
-            "chromium-result-details",
-            "--test-name",
-            "cronet_test_instrumentation_apk"
-          ],
-          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
-              "location": "bin",
-              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
-            }
-          ],
-          "dimension_sets": [
-            {
-              "device_os": "KTU84P",
-              "device_type": "hammerhead",
-              "os": "Android"
-            }
-          ],
-          "output_links": [
-            {
-              "link": [
-                "https://luci-logdog.appspot.com/v/?s",
-                "=android%2Fswarming%2Flogcats%2F",
-                "${TASK_ID}%2F%2B%2Funified_logcats"
-              ],
-              "name": "shard #${SHARD_INDEX} logcats"
-            }
-          ]
-        },
-        "test": "cronet_test_instrumentation_apk"
-      },
-      {
-        "args": [
-          "--gs-results-bucket=chromium-result-details",
-          "--recover-devices"
-        ],
-        "merge": {
-          "args": [
-            "--bucket",
-            "chromium-result-details",
-            "--test-name",
-            "cronet_tests_android"
-          ],
-          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
-              "location": "bin",
-              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
-            }
-          ],
-          "dimension_sets": [
-            {
-              "device_os": "KTU84P",
-              "device_type": "hammerhead",
-              "os": "Android"
-            }
-          ],
-          "output_links": [
-            {
-              "link": [
-                "https://luci-logdog.appspot.com/v/?s",
-                "=android%2Fswarming%2Flogcats%2F",
-                "${TASK_ID}%2F%2B%2Funified_logcats"
-              ],
-              "name": "shard #${SHARD_INDEX} logcats"
-            }
-          ]
-        },
-        "test": "cronet_tests_android"
-      },
-      {
-        "args": [
-          "--gs-results-bucket=chromium-result-details",
-          "--recover-devices"
-        ],
-        "merge": {
-          "args": [
-            "--bucket",
-            "chromium-result-details",
-            "--test-name",
-            "cronet_unittests_android"
-          ],
-          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
-              "location": "bin",
-              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
-            }
-          ],
-          "dimension_sets": [
-            {
-              "device_os": "KTU84P",
-              "device_type": "hammerhead",
-              "os": "Android"
-            }
-          ],
-          "output_links": [
-            {
-              "link": [
-                "https://luci-logdog.appspot.com/v/?s",
-                "=android%2Fswarming%2Flogcats%2F",
-                "${TASK_ID}%2F%2B%2Funified_logcats"
-              ],
-              "name": "shard #${SHARD_INDEX} logcats"
-            }
-          ]
-        },
-        "test": "cronet_unittests_android"
-      },
-      {
-        "args": [
-          "--gs-results-bucket=chromium-result-details",
-          "--recover-devices"
-        ],
-        "merge": {
-          "args": [
-            "--bucket",
-            "chromium-result-details",
-            "--test-name",
-            "net_unittests"
-          ],
-          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
-              "location": "bin",
-              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
-            }
-          ],
-          "dimension_sets": [
-            {
-              "device_os": "KTU84P",
-              "device_type": "hammerhead",
-              "os": "Android"
-            }
-          ],
-          "output_links": [
-            {
-              "link": [
-                "https://luci-logdog.appspot.com/v/?s",
-                "=android%2Fswarming%2Flogcats%2F",
-                "${TASK_ID}%2F%2B%2Funified_logcats"
-              ],
-              "name": "shard #${SHARD_INDEX} logcats"
-            }
-          ],
-          "shards": 4
-        },
-        "test": "net_unittests"
-      }
-    ]
-  },
-  "Android Cronet Builder Asan": {
-    "gtest_tests": [
-      {
-        "swarming": {
-          "can_use_on_swarming_builders": false
-        },
-        "test": "cronet_sample_test_apk"
-      },
-      {
-        "swarming": {
-          "can_use_on_swarming_builders": false
-        },
-        "test": "cronet_smoketests_missing_native_library_instrumentation_apk"
-      },
-      {
-        "swarming": {
-          "can_use_on_swarming_builders": false
-        },
-        "test": "cronet_smoketests_platform_only_instrumentation_apk"
-      },
-      {
-        "swarming": {
-          "can_use_on_swarming_builders": false
-        },
-        "test": "cronet_test_instrumentation_apk"
-      },
-      {
-        "swarming": {
-          "can_use_on_swarming_builders": false
-        },
-        "test": "cronet_tests_android"
-      },
-      {
-        "swarming": {
-          "can_use_on_swarming_builders": false
-        },
-        "test": "cronet_unittests_android"
-      },
-      {
-        "swarming": {
-          "can_use_on_swarming_builders": false
-        },
-        "test": "net_unittests"
-      }
-    ]
-  },
-  "Android Cronet KitKat Builder": {
-    "gtest_tests": [
-      {
-        "args": [
-          "--gs-results-bucket=chromium-result-details",
-          "--recover-devices"
-        ],
-        "merge": {
-          "args": [
-            "--bucket",
-            "chromium-result-details",
-            "--test-name",
-            "cronet_sample_test_apk"
-          ],
-          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
-              "location": "bin",
-              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
-            }
-          ],
-          "dimension_sets": [
-            {
-              "device_os": "KTU84P",
-              "device_type": "hammerhead",
-              "os": "Android"
-            }
-          ],
-          "output_links": [
-            {
-              "link": [
-                "https://luci-logdog.appspot.com/v/?s",
-                "=android%2Fswarming%2Flogcats%2F",
-                "${TASK_ID}%2F%2B%2Funified_logcats"
-              ],
-              "name": "shard #${SHARD_INDEX} logcats"
-            }
-          ]
-        },
-        "test": "cronet_sample_test_apk"
-      },
-      {
-        "args": [
-          "--gs-results-bucket=chromium-result-details",
-          "--recover-devices"
-        ],
-        "merge": {
-          "args": [
-            "--bucket",
-            "chromium-result-details",
-            "--test-name",
-            "cronet_smoketests_missing_native_library_instrumentation_apk"
-          ],
-          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
-              "location": "bin",
-              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
-            }
-          ],
-          "dimension_sets": [
-            {
-              "device_os": "KTU84P",
-              "device_type": "hammerhead",
-              "os": "Android"
-            }
-          ],
-          "output_links": [
-            {
-              "link": [
-                "https://luci-logdog.appspot.com/v/?s",
-                "=android%2Fswarming%2Flogcats%2F",
-                "${TASK_ID}%2F%2B%2Funified_logcats"
-              ],
-              "name": "shard #${SHARD_INDEX} logcats"
-            }
-          ]
-        },
-        "test": "cronet_smoketests_missing_native_library_instrumentation_apk"
-      },
-      {
-        "args": [
-          "--gs-results-bucket=chromium-result-details",
-          "--recover-devices"
-        ],
-        "merge": {
-          "args": [
-            "--bucket",
-            "chromium-result-details",
-            "--test-name",
-            "cronet_smoketests_platform_only_instrumentation_apk"
-          ],
-          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
-              "location": "bin",
-              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
-            }
-          ],
-          "dimension_sets": [
-            {
-              "device_os": "KTU84P",
-              "device_type": "hammerhead",
-              "os": "Android"
-            }
-          ],
-          "output_links": [
-            {
-              "link": [
-                "https://luci-logdog.appspot.com/v/?s",
-                "=android%2Fswarming%2Flogcats%2F",
-                "${TASK_ID}%2F%2B%2Funified_logcats"
-              ],
-              "name": "shard #${SHARD_INDEX} logcats"
-            }
-          ]
-        },
-        "test": "cronet_smoketests_platform_only_instrumentation_apk"
-      },
-      {
-        "args": [
-          "--gs-results-bucket=chromium-result-details",
-          "--recover-devices"
-        ],
-        "merge": {
-          "args": [
-            "--bucket",
-            "chromium-result-details",
-            "--test-name",
-            "cronet_test_instrumentation_apk"
-          ],
-          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
-              "location": "bin",
-              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
-            }
-          ],
-          "dimension_sets": [
-            {
-              "device_os": "KTU84P",
-              "device_type": "hammerhead",
-              "os": "Android"
-            }
-          ],
-          "output_links": [
-            {
-              "link": [
-                "https://luci-logdog.appspot.com/v/?s",
-                "=android%2Fswarming%2Flogcats%2F",
-                "${TASK_ID}%2F%2B%2Funified_logcats"
-              ],
-              "name": "shard #${SHARD_INDEX} logcats"
-            }
-          ]
-        },
-        "test": "cronet_test_instrumentation_apk"
-      },
-      {
-        "args": [
-          "--gs-results-bucket=chromium-result-details",
-          "--recover-devices"
-        ],
-        "merge": {
-          "args": [
-            "--bucket",
-            "chromium-result-details",
-            "--test-name",
-            "cronet_tests_android"
-          ],
-          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
-              "location": "bin",
-              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
-            }
-          ],
-          "dimension_sets": [
-            {
-              "device_os": "KTU84P",
-              "device_type": "hammerhead",
-              "os": "Android"
-            }
-          ],
-          "output_links": [
-            {
-              "link": [
-                "https://luci-logdog.appspot.com/v/?s",
-                "=android%2Fswarming%2Flogcats%2F",
-                "${TASK_ID}%2F%2B%2Funified_logcats"
-              ],
-              "name": "shard #${SHARD_INDEX} logcats"
-            }
-          ]
-        },
-        "test": "cronet_tests_android"
-      },
-      {
-        "args": [
-          "--gs-results-bucket=chromium-result-details",
-          "--recover-devices"
-        ],
-        "merge": {
-          "args": [
-            "--bucket",
-            "chromium-result-details",
-            "--test-name",
-            "cronet_unittests_android"
-          ],
-          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
-              "location": "bin",
-              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
-            }
-          ],
-          "dimension_sets": [
-            {
-              "device_os": "KTU84P",
-              "device_type": "hammerhead",
-              "os": "Android"
-            }
-          ],
-          "output_links": [
-            {
-              "link": [
-                "https://luci-logdog.appspot.com/v/?s",
-                "=android%2Fswarming%2Flogcats%2F",
-                "${TASK_ID}%2F%2B%2Funified_logcats"
-              ],
-              "name": "shard #${SHARD_INDEX} logcats"
-            }
-          ]
-        },
-        "test": "cronet_unittests_android"
-      },
-      {
-        "args": [
-          "--gs-results-bucket=chromium-result-details",
-          "--recover-devices"
-        ],
-        "merge": {
-          "args": [
-            "--bucket",
-            "chromium-result-details",
-            "--test-name",
-            "net_unittests"
-          ],
-          "script": "//build/android/pylib/results/presentation/test_results_presentation.py"
-        },
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
-              "location": "bin",
-              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
-            }
-          ],
-          "dimension_sets": [
-            {
-              "device_os": "KTU84P",
-              "device_type": "hammerhead",
-              "os": "Android"
-            }
-          ],
-          "output_links": [
-            {
-              "link": [
-                "https://luci-logdog.appspot.com/v/?s",
-                "=android%2Fswarming%2Flogcats%2F",
-                "${TASK_ID}%2F%2B%2Funified_logcats"
-              ],
-              "name": "shard #${SHARD_INDEX} logcats"
-            }
-          ],
-          "shards": 4
-        },
-        "test": "net_unittests"
-      }
-    ],
-    "scripts": [
-      {
-        "args": [
-          "--platform",
-          "android-cronet",
-          "--perf-id",
-          "android_cronet_builder",
-          "cronet-arm/sizes"
-        ],
-        "name": "sizes",
-        "override_compile_targets": [
-          "cronet"
-        ],
-        "script": "cronet_sizes.py"
-      }
-    ]
-  },
   "Android WebView O NetworkService (dbg)": {
     "gtest_tests": [
       {
diff --git a/testing/buildbot/chromium.android.json b/testing/buildbot/chromium.android.json
index 263e6b2..c356e48 100644
--- a/testing/buildbot/chromium.android.json
+++ b/testing/buildbot/chromium.android.json
@@ -6,543 +6,6 @@
       "all"
     ]
   },
-  "Android Cronet Builder": {
-    "additional_compile_targets": [
-      "cronet_package"
-    ]
-  },
-  "Android Cronet Builder (dbg)": {
-    "gtest_tests": [
-      {
-        "args": [
-          "--gs-results-bucket=chromium-result-details",
-          "--recover-devices"
-        ],
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
-              "location": "bin",
-              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
-            }
-          ],
-          "dimension_sets": [
-            {
-              "device_os": "KTU84P",
-              "device_type": "hammerhead",
-              "os": "Android"
-            }
-          ],
-          "output_links": [
-            {
-              "link": [
-                "https://luci-logdog.appspot.com/v/?s",
-                "=android%2Fswarming%2Flogcats%2F",
-                "${TASK_ID}%2F%2B%2Funified_logcats"
-              ],
-              "name": "shard #${SHARD_INDEX} logcats"
-            }
-          ]
-        },
-        "test": "cronet_sample_test_apk"
-      },
-      {
-        "args": [
-          "--gs-results-bucket=chromium-result-details",
-          "--recover-devices"
-        ],
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
-              "location": "bin",
-              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
-            }
-          ],
-          "dimension_sets": [
-            {
-              "device_os": "KTU84P",
-              "device_type": "hammerhead",
-              "os": "Android"
-            }
-          ],
-          "output_links": [
-            {
-              "link": [
-                "https://luci-logdog.appspot.com/v/?s",
-                "=android%2Fswarming%2Flogcats%2F",
-                "${TASK_ID}%2F%2B%2Funified_logcats"
-              ],
-              "name": "shard #${SHARD_INDEX} logcats"
-            }
-          ]
-        },
-        "test": "cronet_smoketests_missing_native_library_instrumentation_apk"
-      },
-      {
-        "args": [
-          "--gs-results-bucket=chromium-result-details",
-          "--recover-devices"
-        ],
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
-              "location": "bin",
-              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
-            }
-          ],
-          "dimension_sets": [
-            {
-              "device_os": "KTU84P",
-              "device_type": "hammerhead",
-              "os": "Android"
-            }
-          ],
-          "output_links": [
-            {
-              "link": [
-                "https://luci-logdog.appspot.com/v/?s",
-                "=android%2Fswarming%2Flogcats%2F",
-                "${TASK_ID}%2F%2B%2Funified_logcats"
-              ],
-              "name": "shard #${SHARD_INDEX} logcats"
-            }
-          ]
-        },
-        "test": "cronet_smoketests_platform_only_instrumentation_apk"
-      },
-      {
-        "args": [
-          "--gs-results-bucket=chromium-result-details",
-          "--recover-devices"
-        ],
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
-              "location": "bin",
-              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
-            }
-          ],
-          "dimension_sets": [
-            {
-              "device_os": "KTU84P",
-              "device_type": "hammerhead",
-              "os": "Android"
-            }
-          ],
-          "output_links": [
-            {
-              "link": [
-                "https://luci-logdog.appspot.com/v/?s",
-                "=android%2Fswarming%2Flogcats%2F",
-                "${TASK_ID}%2F%2B%2Funified_logcats"
-              ],
-              "name": "shard #${SHARD_INDEX} logcats"
-            }
-          ]
-        },
-        "test": "cronet_test_instrumentation_apk"
-      },
-      {
-        "args": [
-          "--gs-results-bucket=chromium-result-details",
-          "--recover-devices"
-        ],
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
-              "location": "bin",
-              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
-            }
-          ],
-          "dimension_sets": [
-            {
-              "device_os": "KTU84P",
-              "device_type": "hammerhead",
-              "os": "Android"
-            }
-          ],
-          "output_links": [
-            {
-              "link": [
-                "https://luci-logdog.appspot.com/v/?s",
-                "=android%2Fswarming%2Flogcats%2F",
-                "${TASK_ID}%2F%2B%2Funified_logcats"
-              ],
-              "name": "shard #${SHARD_INDEX} logcats"
-            }
-          ]
-        },
-        "test": "cronet_tests_android"
-      },
-      {
-        "args": [
-          "--gs-results-bucket=chromium-result-details",
-          "--recover-devices"
-        ],
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
-              "location": "bin",
-              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
-            }
-          ],
-          "dimension_sets": [
-            {
-              "device_os": "KTU84P",
-              "device_type": "hammerhead",
-              "os": "Android"
-            }
-          ],
-          "output_links": [
-            {
-              "link": [
-                "https://luci-logdog.appspot.com/v/?s",
-                "=android%2Fswarming%2Flogcats%2F",
-                "${TASK_ID}%2F%2B%2Funified_logcats"
-              ],
-              "name": "shard #${SHARD_INDEX} logcats"
-            }
-          ]
-        },
-        "test": "cronet_unittests_android"
-      },
-      {
-        "args": [
-          "--gs-results-bucket=chromium-result-details",
-          "--recover-devices"
-        ],
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
-              "location": "bin",
-              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
-            }
-          ],
-          "dimension_sets": [
-            {
-              "device_os": "KTU84P",
-              "device_type": "hammerhead",
-              "os": "Android"
-            }
-          ],
-          "output_links": [
-            {
-              "link": [
-                "https://luci-logdog.appspot.com/v/?s",
-                "=android%2Fswarming%2Flogcats%2F",
-                "${TASK_ID}%2F%2B%2Funified_logcats"
-              ],
-              "name": "shard #${SHARD_INDEX} logcats"
-            }
-          ],
-          "shards": 4
-        },
-        "test": "net_unittests"
-      }
-    ]
-  },
-  "Android Cronet Builder Asan": {
-    "gtest_tests": [
-      {
-        "swarming": {
-          "can_use_on_swarming_builders": false
-        },
-        "test": "cronet_sample_test_apk"
-      },
-      {
-        "swarming": {
-          "can_use_on_swarming_builders": false
-        },
-        "test": "cronet_smoketests_missing_native_library_instrumentation_apk"
-      },
-      {
-        "swarming": {
-          "can_use_on_swarming_builders": false
-        },
-        "test": "cronet_smoketests_platform_only_instrumentation_apk"
-      },
-      {
-        "swarming": {
-          "can_use_on_swarming_builders": false
-        },
-        "test": "cronet_test_instrumentation_apk"
-      },
-      {
-        "swarming": {
-          "can_use_on_swarming_builders": false
-        },
-        "test": "cronet_tests_android"
-      },
-      {
-        "swarming": {
-          "can_use_on_swarming_builders": false
-        },
-        "test": "cronet_unittests_android"
-      },
-      {
-        "swarming": {
-          "can_use_on_swarming_builders": false
-        },
-        "test": "net_unittests"
-      }
-    ]
-  },
-  "Android Cronet KitKat Builder": {
-    "gtest_tests": [
-      {
-        "args": [
-          "--gs-results-bucket=chromium-result-details",
-          "--recover-devices"
-        ],
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
-              "location": "bin",
-              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
-            }
-          ],
-          "dimension_sets": [
-            {
-              "device_os": "KTU84P",
-              "device_type": "hammerhead",
-              "os": "Android"
-            }
-          ],
-          "output_links": [
-            {
-              "link": [
-                "https://luci-logdog.appspot.com/v/?s",
-                "=android%2Fswarming%2Flogcats%2F",
-                "${TASK_ID}%2F%2B%2Funified_logcats"
-              ],
-              "name": "shard #${SHARD_INDEX} logcats"
-            }
-          ]
-        },
-        "test": "cronet_sample_test_apk"
-      },
-      {
-        "args": [
-          "--gs-results-bucket=chromium-result-details",
-          "--recover-devices"
-        ],
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
-              "location": "bin",
-              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
-            }
-          ],
-          "dimension_sets": [
-            {
-              "device_os": "KTU84P",
-              "device_type": "hammerhead",
-              "os": "Android"
-            }
-          ],
-          "output_links": [
-            {
-              "link": [
-                "https://luci-logdog.appspot.com/v/?s",
-                "=android%2Fswarming%2Flogcats%2F",
-                "${TASK_ID}%2F%2B%2Funified_logcats"
-              ],
-              "name": "shard #${SHARD_INDEX} logcats"
-            }
-          ]
-        },
-        "test": "cronet_smoketests_missing_native_library_instrumentation_apk"
-      },
-      {
-        "args": [
-          "--gs-results-bucket=chromium-result-details",
-          "--recover-devices"
-        ],
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
-              "location": "bin",
-              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
-            }
-          ],
-          "dimension_sets": [
-            {
-              "device_os": "KTU84P",
-              "device_type": "hammerhead",
-              "os": "Android"
-            }
-          ],
-          "output_links": [
-            {
-              "link": [
-                "https://luci-logdog.appspot.com/v/?s",
-                "=android%2Fswarming%2Flogcats%2F",
-                "${TASK_ID}%2F%2B%2Funified_logcats"
-              ],
-              "name": "shard #${SHARD_INDEX} logcats"
-            }
-          ]
-        },
-        "test": "cronet_smoketests_platform_only_instrumentation_apk"
-      },
-      {
-        "args": [
-          "--gs-results-bucket=chromium-result-details",
-          "--recover-devices"
-        ],
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
-              "location": "bin",
-              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
-            }
-          ],
-          "dimension_sets": [
-            {
-              "device_os": "KTU84P",
-              "device_type": "hammerhead",
-              "os": "Android"
-            }
-          ],
-          "output_links": [
-            {
-              "link": [
-                "https://luci-logdog.appspot.com/v/?s",
-                "=android%2Fswarming%2Flogcats%2F",
-                "${TASK_ID}%2F%2B%2Funified_logcats"
-              ],
-              "name": "shard #${SHARD_INDEX} logcats"
-            }
-          ]
-        },
-        "test": "cronet_test_instrumentation_apk"
-      },
-      {
-        "args": [
-          "--gs-results-bucket=chromium-result-details",
-          "--recover-devices"
-        ],
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
-              "location": "bin",
-              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
-            }
-          ],
-          "dimension_sets": [
-            {
-              "device_os": "KTU84P",
-              "device_type": "hammerhead",
-              "os": "Android"
-            }
-          ],
-          "output_links": [
-            {
-              "link": [
-                "https://luci-logdog.appspot.com/v/?s",
-                "=android%2Fswarming%2Flogcats%2F",
-                "${TASK_ID}%2F%2B%2Funified_logcats"
-              ],
-              "name": "shard #${SHARD_INDEX} logcats"
-            }
-          ]
-        },
-        "test": "cronet_tests_android"
-      },
-      {
-        "args": [
-          "--gs-results-bucket=chromium-result-details",
-          "--recover-devices"
-        ],
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
-              "location": "bin",
-              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
-            }
-          ],
-          "dimension_sets": [
-            {
-              "device_os": "KTU84P",
-              "device_type": "hammerhead",
-              "os": "Android"
-            }
-          ],
-          "output_links": [
-            {
-              "link": [
-                "https://luci-logdog.appspot.com/v/?s",
-                "=android%2Fswarming%2Flogcats%2F",
-                "${TASK_ID}%2F%2B%2Funified_logcats"
-              ],
-              "name": "shard #${SHARD_INDEX} logcats"
-            }
-          ]
-        },
-        "test": "cronet_unittests_android"
-      },
-      {
-        "args": [
-          "--gs-results-bucket=chromium-result-details",
-          "--recover-devices"
-        ],
-        "swarming": {
-          "can_use_on_swarming_builders": true,
-          "cipd_packages": [
-            {
-              "cipd_package": "infra/tools/luci/logdog/butler/${platform}",
-              "location": "bin",
-              "revision": "git_revision:ff387eadf445b24c935f1cf7d6ddd279f8a6b04c"
-            }
-          ],
-          "dimension_sets": [
-            {
-              "device_os": "KTU84P",
-              "device_type": "hammerhead",
-              "os": "Android"
-            }
-          ],
-          "output_links": [
-            {
-              "link": [
-                "https://luci-logdog.appspot.com/v/?s",
-                "=android%2Fswarming%2Flogcats%2F",
-                "${TASK_ID}%2F%2B%2Funified_logcats"
-              ],
-              "name": "shard #${SHARD_INDEX} logcats"
-            }
-          ],
-          "shards": 4
-        },
-        "test": "net_unittests"
-      }
-    ]
-  },
   "Android WebView L (dbg)": {
     "gtest_tests": [
       {
diff --git a/testing/buildbot/filters/fuchsia.content_unittests.filter b/testing/buildbot/filters/fuchsia.content_unittests.filter
index d64c8f38..c93a056 100644
--- a/testing/buildbot/filters/fuchsia.content_unittests.filter
+++ b/testing/buildbot/filters/fuchsia.content_unittests.filter
@@ -61,6 +61,7 @@
 # Flaky, https://crbug.com/884250.
 -ReferrerSanitizerTest.SanitizesPolicyForNonEmptyReferrers
 
-# This test requires OSExchangeDataProviderFactory::CreateProvider to
+# These tests require OSExchangeDataProviderFactory::CreateProvider to
 # be implemented: https://crbug.com/750934.
 -WebContentsViewAuraTest.DragDropFiles
+-WebContentsViewAuraTest.DragDropFilesOriginateFromRenderer
diff --git a/testing/buildbot/test_suite_exceptions.pyl b/testing/buildbot/test_suite_exceptions.pyl
index d72385b..2ae803d8 100644
--- a/testing/buildbot/test_suite_exceptions.pyl
+++ b/testing/buildbot/test_suite_exceptions.pyl
@@ -1147,19 +1147,6 @@
   },
   'sizes': {
     'modifications': {
-      # chromium.android.fyi
-      'Android Cronet KitKat Builder': {
-        'args': [
-          '--platform',
-          'android-cronet',
-          '--perf-id',
-          'android_cronet_builder',
-          'cronet-arm/sizes',
-        ],
-        'override_compile_targets': [
-          'cronet',
-        ],
-      },
       'android-cronet-arm-dbg': {
         'args': [
           '--platform', 'android-cronet',
diff --git a/testing/buildbot/waterfalls.pyl b/testing/buildbot/waterfalls.pyl
index 1e2526b..5c6a151 100644
--- a/testing/buildbot/waterfalls.pyl
+++ b/testing/buildbot/waterfalls.pyl
@@ -156,59 +156,6 @@
           'all',
         ],
       },
-      'Android Cronet Builder': {
-        'additional_compile_targets': [
-          'cronet_package',
-        ],
-      },
-      'Android Cronet Builder (dbg)': {
-        'test_suites': {
-          'gtest_tests': 'cronet_gtests',
-        },
-        'swarming': {
-          'dimension_sets': [
-            {
-              'device_os': 'KTU84P',
-              'device_type': 'hammerhead',
-              'os': 'Android',
-            },
-          ],
-        },
-        'os_type': 'android',
-        'skip_merge_script': True,
-      },
-      'Android Cronet Builder Asan': {
-        'test_suites': {
-          'gtest_tests': 'cronet_gtests',
-        },
-        'use_swarming': False,
-        'swarming': {
-          'dimension_sets': [
-            {
-              'device_os': 'KTU84P',
-              'device_type': 'hammerhead',
-              'os': 'Android',
-            },
-          ],
-        },
-        'os_type': 'android',
-      },
-      'Android Cronet KitKat Builder': {
-        'test_suites': {
-          'gtest_tests': 'cronet_gtests',
-        },
-        'swarming': {
-          'dimension_sets': [
-            {
-              'device_os': 'KTU84P',
-              'device_type': 'hammerhead',
-              'os': 'Android',
-            },
-          ],
-        },
-        'os_type': 'android',
-        'skip_merge_script': True,
-      },
       'Android WebView L (dbg)': {
         'test_suites': {
           'gtest_tests': 'webview_bot_gtests',
@@ -660,44 +607,6 @@
   {
     'name': 'chromium.android.fyi',
     'machines': {
-      'Android Cronet Builder (dbg)': {
-        'test_suites': {
-          'gtest_tests': 'cronet_gtests',
-        },
-        'swarming': {
-          'dimension_sets': [
-            {
-              'device_os': 'KTU84P',
-              'device_type': 'hammerhead',
-              'os': 'Android',
-            },
-          ],
-        },
-        'os_type': 'android',
-      },
-      'Android Cronet Builder Asan': {
-        'test_suites': {
-          'gtest_tests': 'cronet_gtests',
-        },
-        'os_type': 'android',
-        'use_swarming': False,
-      },
-      'Android Cronet KitKat Builder': {
-        'test_suites': {
-          'gtest_tests': 'cronet_gtests',
-          'scripts': 'cronet_scripts',
-        },
-        'swarming': {
-          'dimension_sets': [
-            {
-              'device_os': 'KTU84P',
-              'device_type': 'hammerhead',
-              'os': 'Android',
-            },
-          ],
-        },
-        'os_type': 'android',
-      },
       'Android WebView O NetworkService (dbg)': {
         'test_suites': {
           'gtest_tests': 'webview_cts_tests_gtest',
diff --git a/third_party/blink/public/mojom/credentialmanager/credential_manager.mojom b/third_party/blink/public/mojom/credentialmanager/credential_manager.mojom
index 405998d..e22ed21 100644
--- a/third_party/blink/public/mojom/credentialmanager/credential_manager.mojom
+++ b/third_party/blink/public/mojom/credentialmanager/credential_manager.mojom
@@ -31,6 +31,7 @@
   NOT_IMPLEMENTED,
   NOT_FOCUSED,
   RESIDENT_CREDENTIALS_UNSUPPORTED,
+  PROTECTION_POLICY_INCONSISTENT,
   ANDROID_ALGORITHM_UNSUPPORTED,
   ANDROID_EMPTY_ALLOW_CREDENTIALS,
   ANDROID_NOT_SUPPORTED_ERROR,
diff --git a/third_party/blink/public/mojom/webauthn/authenticator.mojom b/third_party/blink/public/mojom/webauthn/authenticator.mojom
index 155ff22b..2aff731c 100644
--- a/third_party/blink/public/mojom/webauthn/authenticator.mojom
+++ b/third_party/blink/public/mojom/webauthn/authenticator.mojom
@@ -26,6 +26,7 @@
   ALGORITHM_UNSUPPORTED,
   EMPTY_ALLOW_CREDENTIALS,
   ANDROID_NOT_SUPPORTED_ERROR,
+  PROTECTION_POLICY_INCONSISTENT,
   UNKNOWN_ERROR,
 };
 
@@ -251,6 +252,14 @@
     CROSS_PLATFORM,
 };
 
+enum ProtectionPolicy {
+    // UNSPECIFIED means that no value was given at the Javascript level.
+    UNSPECIFIED,
+    NONE,
+    UV_OR_CRED_ID_REQUIRED,
+    UV_REQUIRED,
+};
+
 // See https://w3c.github.io/webauthn/#dictdef-authenticatorselectioncriteria.
 struct AuthenticatorSelectionCriteria {
   // Filter authenticators by attachment type.
@@ -313,6 +322,13 @@
   // the RP. See https://w3c.github.io/webauthn/#sctn-uvm-extension
   [EnableIf=is_android]
   bool user_verification_methods;
+
+  // The value of the `credentialProtectionPolicy` extension, or UNSPECIFIED if
+  // none was provided.
+  ProtectionPolicy protection_policy;
+  // The value of the `enforceCredentialProtectionPolicy`, or false if none was
+  // provided.
+  bool enforce_protection_policy;
 };
 
 enum PublicKeyCredentialType {
diff --git a/third_party/blink/public/platform/DEPS b/third_party/blink/public/platform/DEPS
index cafa3e7d..14ffffe4 100644
--- a/third_party/blink/public/platform/DEPS
+++ b/third_party/blink/public/platform/DEPS
@@ -23,7 +23,7 @@
     "+build/build_config.h",
     "+cc",
     "+components/viz/common",
-    "+media/base/video_rotation.h",
+    "+media/base/video_transformation.h",
     "+mojo/public",
     "+net/cert",
     "+net/http",
diff --git a/third_party/blink/renderer/core/accessibility/apply_dark_mode.cc b/third_party/blink/renderer/core/accessibility/apply_dark_mode.cc
index 8166679d..1be8571 100644
--- a/third_party/blink/renderer/core/accessibility/apply_dark_mode.cc
+++ b/third_party/blink/renderer/core/accessibility/apply_dark_mode.cc
@@ -53,6 +53,7 @@
   dark_mode_settings.grayscale = frame_settings.GetDarkModeGrayscale();
   dark_mode_settings.contrast = frame_settings.GetDarkModeContrast();
   dark_mode_settings.image_policy = frame_settings.GetDarkModeImagePolicy();
+  dark_mode_settings.image_style = frame_settings.GetDarkModeImageStyle();
   return dark_mode_settings;
 }
 
diff --git a/third_party/blink/renderer/core/execution_context/security_context.cc b/third_party/blink/renderer/core/execution_context/security_context.cc
index f1bf023a..afee13a1 100644
--- a/third_party/blink/renderer/core/execution_context/security_context.cc
+++ b/third_party/blink/renderer/core/execution_context/security_context.cc
@@ -37,14 +37,14 @@
 
 namespace {
 
-// Bucketize image compression into percentage in the following fashion:
-// if an image's compression ratio is 0.1, it will be represented as 1 percent
-// if an image's compression ratio is 5, it will be represented as 50 percents.
-int BucketizeCompressionRatio(double compression_ratio) {
-  int compression_ratio_percent = 10 * compression_ratio;
-  if (compression_ratio_percent < 0)
+// Bucketize image metrics into percentage in the following fashion:
+// if an image's metrics is 0.1, it will be represented as 1 percent
+// if an image's metrics is 5, it will be represented as 50 percents.
+int BucketizeImageMetrics(double ratio) {
+  int ratio_percent = 10 * ratio;
+  if (ratio_percent < 0)
     return 0;
-  return compression_ratio_percent > 100 ? 100 : compression_ratio_percent;
+  return ratio_percent > 100 ? 100 : ratio_percent;
 }
 
 inline const char* GetImagePolicyHistogramName(
@@ -56,6 +56,8 @@
       return "Blink.UseCounter.FeaturePolicy.LosslessImageCompression";
     case mojom::FeaturePolicyFeature::kUnoptimizedLosslessImagesStrict:
       return "Blink.UseCounter.FeaturePolicy.StrictLosslessImageCompression";
+    case mojom::FeaturePolicyFeature::kOversizedImages:
+      return "Blink.UseCounter.FeaturePolicy.ImageDownscalingRatio";
     default:
       NOTREACHED();
       break;
@@ -286,11 +288,11 @@
   // properly inherit the parent policy.
   DCHECK(feature_policy_);
 
-  // Log metrics for unoptimized-*-images policies.
-  if (feature == mojom::FeaturePolicyFeature::kUnoptimizedLossyImages ||
-      feature == mojom::FeaturePolicyFeature::kUnoptimizedLosslessImages ||
-      feature ==
-          mojom::FeaturePolicyFeature::kUnoptimizedLosslessImagesStrict) {
+  // Log metrics for unoptimized-*-images and oversized-images policies.
+  if ((feature >= mojom::FeaturePolicyFeature::kUnoptimizedLossyImages &&
+       feature <=
+           mojom::FeaturePolicyFeature::kUnoptimizedLosslessImagesStrict) ||
+      feature == mojom::FeaturePolicyFeature::kOversizedImages) {
     // Only log metrics if an image policy is specified.
     // If an image policy is specified, the policy value would be less than the
     // max value, otherwise by default the policy value is set to be the max
@@ -304,7 +306,7 @@
           static_cast<int>(
               mojom::FeaturePolicyFeature::kUnoptimizedLosslessImagesStrict) +
               1,
-          Add(BucketizeCompressionRatio(threshold_value.DoubleValue())),
+          Add(BucketizeImageMetrics(threshold_value.DoubleValue())),
           base::LinearHistogram::FactoryGet(
               GetImagePolicyHistogramName(feature), 0, 100, 101, 0x1));
     }
diff --git a/third_party/blink/renderer/core/exported/web_layer_test.cc b/third_party/blink/renderer/core/exported/web_layer_test.cc
index b8301003..47cf3fc 100644
--- a/third_party/blink/renderer/core/exported/web_layer_test.cc
+++ b/third_party/blink/renderer/core/exported/web_layer_test.cc
@@ -904,7 +904,7 @@
   auto effect_tree_index = outer_element_layer->effect_tree_index();
   auto* effect_node = GetPropertyTrees()->effect_tree.Node(effect_tree_index);
   EXPECT_EQ(effect_node->opacity, 0.5f);
-  EXPECT_FALSE(effect_node->has_render_surface);
+  EXPECT_FALSE(effect_node->HasRenderSurface());
 }
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/frame/settings.json5 b/third_party/blink/renderer/core/frame/settings.json5
index 2247bd690..7acc674 100644
--- a/third_party/blink/renderer/core/frame/settings.json5
+++ b/third_party/blink/renderer/core/frame/settings.json5
@@ -913,6 +913,12 @@
       invalidate: "Paint",
     },
     {
+      name: "darkModeImageStyle",
+      initial: "DarkModeImageStyle::kDefault",
+      type: "DarkModeImageStyle",
+      invalidate: "Paint",
+    },
+    {
       name: "navigatorPlatformOverride",
       type: "String",
     },
diff --git a/third_party/blink/renderer/core/html/forms/range_input_type.cc b/third_party/blink/renderer/core/html/forms/range_input_type.cc
index 916b61a2..e3d19561 100644
--- a/third_party/blink/renderer/core/html/forms/range_input_type.cc
+++ b/third_party/blink/renderer/core/html/forms/range_input_type.cc
@@ -332,7 +332,7 @@
 }
 
 inline SliderThumbElement* RangeInputType::GetSliderThumbElement() const {
-  return ToSliderThumbElementOrDie(
+  return To<SliderThumbElement>(
       GetElement().UserAgentShadowRoot()->getElementById(
           shadow_element_names::SliderThumb()));
 }
diff --git a/third_party/blink/renderer/core/html/forms/slider_thumb_element.cc b/third_party/blink/renderer/core/html/forms/slider_thumb_element.cc
index 051cdeb..01b7be2 100644
--- a/third_party/blink/renderer/core/html/forms/slider_thumb_element.cc
+++ b/third_party/blink/renderer/core/html/forms/slider_thumb_element.cc
@@ -392,7 +392,7 @@
   }
 
   TouchList* touches = event->targetTouches();
-  SliderThumbElement* thumb = ToSliderThumbElement(
+  auto* thumb = To<SliderThumbElement>(
       GetTreeScope().getElementById(shadow_element_names::SliderThumb()));
   if (!thumb || !touches)
     return;
diff --git a/third_party/blink/renderer/core/html/forms/slider_thumb_element.h b/third_party/blink/renderer/core/html/forms/slider_thumb_element.h
index 0423972..f7cdcb8 100644
--- a/third_party/blink/renderer/core/html/forms/slider_thumb_element.h
+++ b/third_party/blink/renderer/core/html/forms/slider_thumb_element.h
@@ -33,6 +33,7 @@
 #define THIRD_PARTY_BLINK_RENDERER_CORE_HTML_FORMS_SLIDER_THUMB_ELEMENT_H_
 
 #include "third_party/blink/renderer/core/html/html_div_element.h"
+#include "third_party/blink/renderer/platform/wtf/casting.h"
 #include "third_party/blink/renderer/platform/wtf/forward.h"
 
 namespace blink {
@@ -79,7 +80,15 @@
 }
 
 // FIXME: There are no ways to check if a node is a SliderThumbElement.
-DEFINE_ELEMENT_TYPE_CASTS(SliderThumbElement, IsHTMLElement());
+template <>
+inline bool IsElementOfType<const SliderThumbElement>(const Node& node) {
+  return node.IsHTMLElement();
+}
+
+template <>
+struct DowncastTraits<SliderThumbElement> {
+  static bool AllowFrom(const Node& node) { return node.IsHTMLElement(); }
+};
 
 class SliderContainerElement final : public HTMLDivElement {
  public:
diff --git a/third_party/blink/renderer/core/layout/custom/pending_layout_registry.cc b/third_party/blink/renderer/core/layout/custom/pending_layout_registry.cc
index 68559c90..fe52968 100644
--- a/third_party/blink/renderer/core/layout/custom/pending_layout_registry.cc
+++ b/third_party/blink/renderer/core/layout/custom/pending_layout_registry.cc
@@ -26,7 +26,7 @@
         const ComputedStyle& style = node->GetLayoutObject()->StyleRef();
         if (style.IsDisplayLayoutCustomBox() &&
             style.DisplayLayoutCustomName() == name)
-          node->LazyReattachIfAttached();
+          node->SetForceReattachLayoutTree();
       }
     }
   }
diff --git a/third_party/blink/renderer/core/layout/layout_block_flow_line.cc b/third_party/blink/renderer/core/layout/layout_block_flow_line.cc
index 4d5f46e..74631c1 100644
--- a/third_party/blink/renderer/core/layout/layout_block_flow_line.cc
+++ b/third_party/blink/renderer/core/layout/layout_block_flow_line.cc
@@ -21,6 +21,7 @@
  *
  */
 
+#include "base/containers/span.h"
 #include "build/build_config.h"
 #include "third_party/blink/renderer/core/accessibility/ax_object_cache.h"
 #include "third_party/blink/renderer/core/editing/editing_utilities.h"
@@ -56,7 +57,7 @@
     unsigned opportunities_in_run;
     if (text.Is8Bit()) {
       opportunities_in_run = Character::ExpansionOpportunityCount(
-          text.Characters8() + run.start_, run.stop_ - run.start_,
+          {text.Characters8() + run.start_, run.stop_ - run.start_},
           run.box_->Direction(), is_after_expansion, text_justify);
     } else if (run.line_layout_item_.IsCombineText()) {
       // Justfication applies to before and after the combined text as if
@@ -66,7 +67,7 @@
       is_after_expansion = true;
     } else {
       opportunities_in_run = Character::ExpansionOpportunityCount(
-          text.Characters16() + run.start_, run.stop_ - run.start_,
+          {text.Characters16() + run.start_, run.stop_ - run.start_},
           run.box_->Direction(), is_after_expansion, text_justify);
     }
     runs_with_expansions_.push_back(opportunities_in_run);
diff --git a/third_party/blink/renderer/core/layout/layout_box.cc b/third_party/blink/renderer/core/layout/layout_box.cc
index 0db5093..9fefcdc 100644
--- a/third_party/blink/renderer/core/layout/layout_box.cc
+++ b/third_party/blink/renderer/core/layout/layout_box.cc
@@ -3142,9 +3142,13 @@
     // Reset width so that any percent margins on inline children do not
     // use it when calculating min/max preferred width.
     // TODO(crbug.com/710026): Remove const_cast
+    LayoutUnit w = LogicalWidth();
     const_cast<LayoutBox*>(this)->SetLogicalWidth(LayoutUnit());
-    return std::max(MinPreferredLogicalWidth(),
-                    std::min(MaxPreferredLogicalWidth(), logical_width_result));
+    LayoutUnit result =
+        std::max(MinPreferredLogicalWidth(),
+                 std::min(MaxPreferredLogicalWidth(), logical_width_result));
+    const_cast<LayoutBox*>(this)->SetLogicalWidth(w);
+    return result;
   }
   return logical_width_result;
 }
diff --git a/third_party/blink/renderer/core/layout/layout_flexible_box.cc b/third_party/blink/renderer/core/layout/layout_flexible_box.cc
index 1b0a5ffb..c199ca1 100644
--- a/third_party/blink/renderer/core/layout/layout_flexible_box.cc
+++ b/third_party/blink/renderer/core/layout/layout_flexible_box.cc
@@ -533,11 +533,7 @@
   // and can just return the already-set logical width.
   if (!CrossAxisLengthIsDefinite(child, child.StyleRef().LogicalWidth())) {
     LogicalExtentComputedValues values;
-    // ComputeLogicalWidth has the side-effect of setting LogicalWidth to 0;
-    // so we store and re-set it.
-    LayoutUnit w = child.LogicalWidth();
     child.ComputeLogicalWidth(values);
-    const_cast<LayoutBox&>(child).SetLogicalWidth(w);
     return values.extent_;
   }
 
diff --git a/third_party/blink/renderer/core/layout/layout_slider.cc b/third_party/blink/renderer/core/layout/layout_slider.cc
index 4e349a8..db984bd95 100644
--- a/third_party/blink/renderer/core/layout/layout_slider.cc
+++ b/third_party/blink/renderer/core/layout/layout_slider.cc
@@ -58,7 +58,7 @@
 }
 
 inline SliderThumbElement* LayoutSlider::GetSliderThumbElement() const {
-  return ToSliderThumbElement(
+  return To<SliderThumbElement>(
       ToElement(GetNode())->UserAgentShadowRoot()->getElementById(
           shadow_element_names::SliderThumb()));
 }
diff --git a/third_party/blink/renderer/core/loader/resource/image_resource_content.cc b/third_party/blink/renderer/core/loader/resource/image_resource_content.cc
index 84263d0..1a9981b 100644
--- a/third_party/blink/renderer/core/loader/resource/image_resource_content.cc
+++ b/third_party/blink/renderer/core/loader/resource/image_resource_content.cc
@@ -523,7 +523,23 @@
   double compression_ratio_1k = (resource_length - 1024) / pixels;
   double compression_ratio_10k = (resource_length - 10240) / pixels;
 
-  int compression_format = GetCompressionFormat();
+  ImageDecoder::CompressionFormat compression_format = GetCompressionFormat();
+  const auto max_value =
+      PolicyValue::CreateMaxPolicyValue(mojom::PolicyValueType::kDecDouble);
+  // If an unoptimized-*-images policy is specified, the specified compression
+  // ratio will be less than the max value.
+  bool is_policy_specified =
+      !context.IsFeatureEnabled(
+          mojom::FeaturePolicyFeature::kUnoptimizedLossyImages, max_value) ||
+      !context.IsFeatureEnabled(
+          mojom::FeaturePolicyFeature::kUnoptimizedLosslessImagesStrict,
+          max_value) ||
+      !context.IsFeatureEnabled(
+          mojom::FeaturePolicyFeature::kUnoptimizedLosslessImages, max_value);
+  if (is_policy_specified) {
+    UMA_HISTOGRAM_ENUMERATION("Blink.UseCounter.FeaturePolicy.ImageFormats",
+                              compression_format);
+  }
   if (compression_format == ImageDecoder::kLossyFormat) {
     // Enforce the lossy image policy.
     return context.IsFeatureEnabled(
diff --git a/third_party/blink/renderer/core/paint/image_element_timing.cc b/third_party/blink/renderer/core/paint/image_element_timing.cc
index 8a04837..64ac720 100644
--- a/third_party/blink/renderer/core/paint/image_element_timing.cc
+++ b/third_party/blink/renderer/core/paint/image_element_timing.cc
@@ -62,7 +62,7 @@
 }
 
 void ImageElementTiming::NotifyImagePaintedInternal(
-    const Node* node,
+    Node* node,
     const LayoutObject& layout_object,
     const ImageResourceContent& cached_image,
     const PropertyTreeState& current_paint_chunk_properties) {
@@ -77,7 +77,7 @@
 
   FloatRect intersection_rect = ComputeIntersectionRect(
       frame, layout_object, current_paint_chunk_properties);
-  const Element* element = ToElement(node);
+  Element* element = ToElement(node);
   const AtomicString attr =
       element->FastGetAttribute(html_names::kElementtimingAttr);
   if (!ShouldReportElement(frame, attr, intersection_rect))
@@ -104,7 +104,8 @@
       performance->AddElementTiming(
           AtomicString(url.GetString()), intersection_rect, TimeTicks(),
           cached_image.LoadResponseEnd(), attr,
-          cached_image.IntrinsicSize(kDoNotRespectImageOrientation), id);
+          cached_image.IntrinsicSize(kDoNotRespectImageOrientation), id,
+          element);
     }
     return;
   }
@@ -116,10 +117,10 @@
   const String& image_name = url.ProtocolIsData()
                                  ? url.GetString().Left(kInlineImageMaxChars)
                                  : url.GetString();
-  element_timings_.emplace_back(
+  element_timings_.emplace_back(MakeGarbageCollected<ElementTimingInfo>(
       AtomicString(image_name), intersection_rect,
       cached_image.LoadResponseEnd(), attr,
-      cached_image.IntrinsicSize(kDoNotRespectImageOrientation), id);
+      cached_image.IntrinsicSize(kDoNotRespectImageOrientation), id, element));
   // Only queue a swap promise when |element_timings_| was empty. All of the
   // records in |element_timings_| will be processed when the promise succeeds
   // or fails, and at that time the vector is cleared.
@@ -132,7 +133,7 @@
 }
 
 void ImageElementTiming::NotifyBackgroundImagePainted(
-    const Node* node,
+    Node* node,
     const StyleImage* background_image,
     const PropertyTreeState& current_paint_chunk_properties) {
   DCHECK(node);
@@ -193,9 +194,10 @@
                       performance->ShouldBufferEntries())) {
     for (const auto& element_timing : element_timings_) {
       performance->AddElementTiming(
-          element_timing.name, element_timing.rect, timestamp,
-          element_timing.response_end, element_timing.identifier,
-          element_timing.intrinsic_size, element_timing.id);
+          element_timing->name, element_timing->rect, timestamp,
+          element_timing->response_end, element_timing->identifier,
+          element_timing->intrinsic_size, element_timing->id,
+          element_timing->element);
     }
   }
   element_timings_.clear();
@@ -212,6 +214,7 @@
 }
 
 void ImageElementTiming::Trace(blink::Visitor* visitor) {
+  visitor->Trace(element_timings_);
   Supplement<LocalDOMWindow>::Trace(visitor);
 }
 
diff --git a/third_party/blink/renderer/core/paint/image_element_timing.h b/third_party/blink/renderer/core/paint/image_element_timing.h
index ab2ddc6..0be100a 100644
--- a/third_party/blink/renderer/core/paint/image_element_timing.h
+++ b/third_party/blink/renderer/core/paint/image_element_timing.h
@@ -43,7 +43,7 @@
       const PropertyTreeState& current_paint_chunk_properties);
 
   void NotifyBackgroundImagePainted(
-      const Node*,
+      Node*,
       const StyleImage* background_image,
       const PropertyTreeState& current_paint_chunk_properties);
 
@@ -59,7 +59,7 @@
   friend class ImageElementTimingTest;
 
   void NotifyImagePaintedInternal(
-      const Node*,
+      Node*,
       const LayoutObject&,
       const ImageResourceContent& cached_image,
       const PropertyTreeState& current_paint_chunk_properties);
@@ -78,20 +78,27 @@
   void ReportImagePaintSwapTime(WebWidgetClient::SwapResult,
                                 base::TimeTicks timestamp);
 
-  // Struct containing information about image element timing.
-  struct ElementTimingInfo {
+  // Class containing information about image element timing.
+  class ElementTimingInfo
+      : public GarbageCollectedFinalized<ElementTimingInfo> {
+   public:
     ElementTimingInfo(const AtomicString& name,
                       const FloatRect& rect,
                       const TimeTicks& response_end,
                       const AtomicString& identifier,
                       const IntSize& intrinsic_size,
-                      const AtomicString& id)
+                      const AtomicString& id,
+                      Element* element)
         : name(name),
           rect(rect),
           response_end(response_end),
           identifier(identifier),
           intrinsic_size(intrinsic_size),
-          id(id) {}
+          id(id),
+          element(element) {}
+    ~ElementTimingInfo() = default;
+
+    void Trace(blink::Visitor* visitor) { visitor->Trace(element); }
 
     AtomicString name;
     FloatRect rect;
@@ -99,10 +106,15 @@
     AtomicString identifier;
     IntSize intrinsic_size;
     AtomicString id;
+    Member<Element> element;
+
+   private:
+    DISALLOW_COPY_AND_ASSIGN(ElementTimingInfo);
   };
+
   // Vector containing the element timing infos that will be reported during the
   // next swap promise callback.
-  WTF::Vector<ElementTimingInfo> element_timings_;
+  HeapVector<Member<ElementTimingInfo>> element_timings_;
   // Hashmap of LayoutObjects for which paint has already been notified.
   WTF::HashSet<const LayoutObject*> images_notified_;
   // Hashmap of pairs of elements, background images whose paint has been
diff --git a/third_party/blink/renderer/core/svg/svg_graphics_element.cc b/third_party/blink/renderer/core/svg/svg_graphics_element.cc
index 4cc5fa1..4ef3fc3 100644
--- a/third_party/blink/renderer/core/svg/svg_graphics_element.cc
+++ b/third_party/blink/renderer/core/svg/svg_graphics_element.cc
@@ -124,7 +124,7 @@
   // creation.
   if (SVGTests::IsKnownAttribute(attr_name)) {
     SVGElement::InvalidationGuard invalidation_guard(this);
-    LazyReattachIfAttached();
+    SetForceReattachLayoutTree();
     return;
   }
 
diff --git a/third_party/blink/renderer/core/timing/performance_element_timing.cc b/third_party/blink/renderer/core/timing/performance_element_timing.cc
index 9e976848..b1924de 100644
--- a/third_party/blink/renderer/core/timing/performance_element_timing.cc
+++ b/third_party/blink/renderer/core/timing/performance_element_timing.cc
@@ -18,14 +18,16 @@
     const AtomicString& identifier,
     int naturalWidth,
     int naturalHeight,
-    const AtomicString& id) {
+    const AtomicString& id,
+    Element* element) {
   // It is possible to 'paint' images which have naturalWidth or naturalHeight
   // equal to 0.
   DCHECK_GE(naturalWidth, 0);
   DCHECK_GE(naturalHeight, 0);
+  DCHECK(element);
   return MakeGarbageCollected<PerformanceElementTiming>(
       name, intersection_rect, start_time, response_end, identifier,
-      naturalWidth, naturalHeight, id);
+      naturalWidth, naturalHeight, id, element);
 }
 
 PerformanceElementTiming::PerformanceElementTiming(
@@ -36,8 +38,10 @@
     const AtomicString& identifier,
     int naturalWidth,
     int naturalHeight,
-    const AtomicString& id)
+    const AtomicString& id,
+    Element* element)
     : PerformanceEntry(name, start_time, start_time),
+      element_(element),
       intersection_rect_(DOMRectReadOnly::FromFloatRect(intersection_rect)),
       response_end_(response_end),
       identifier_(identifier),
@@ -55,12 +59,20 @@
   return PerformanceEntry::EntryType::kElement;
 }
 
+Element* PerformanceElementTiming::element() const {
+  if (!element_ || !element_->isConnected())
+    return nullptr;
+
+  return element_;
+}
+
 void PerformanceElementTiming::BuildJSONValue(V8ObjectBuilder& builder) const {
   PerformanceEntry::BuildJSONValue(builder);
   builder.Add("intersectionRect", intersection_rect_);
 }
 
 void PerformanceElementTiming::Trace(blink::Visitor* visitor) {
+  visitor->Trace(element_);
   visitor->Trace(intersection_rect_);
   PerformanceEntry::Trace(visitor);
 }
diff --git a/third_party/blink/renderer/core/timing/performance_element_timing.h b/third_party/blink/renderer/core/timing/performance_element_timing.h
index 6bb3f24..ac920ed 100644
--- a/third_party/blink/renderer/core/timing/performance_element_timing.h
+++ b/third_party/blink/renderer/core/timing/performance_element_timing.h
@@ -7,6 +7,7 @@
 
 #include "third_party/blink/renderer/core/core_export.h"
 #include "third_party/blink/renderer/core/dom/dom_high_res_time_stamp.h"
+#include "third_party/blink/renderer/core/dom/element.h"
 #include "third_party/blink/renderer/core/geometry/dom_rect_read_only.h"
 #include "third_party/blink/renderer/core/timing/performance_entry.h"
 
@@ -27,8 +28,8 @@
                                           const AtomicString& identifier,
                                           int naturalWidth,
                                           int naturalHeight,
-                                          const AtomicString& id);
-
+                                          const AtomicString& id,
+                                          Element*);
   PerformanceElementTiming(const AtomicString& name,
                            const FloatRect& intersection_rect,
                            DOMHighResTimeStamp start_time,
@@ -36,7 +37,9 @@
                            const AtomicString& identifier,
                            int naturalWidth,
                            int naturalHeight,
-                           const AtomicString& id);
+                           const AtomicString& id,
+                           Element*);
+
   ~PerformanceElementTiming() override;
 
   AtomicString entryType() const override;
@@ -54,11 +57,14 @@
 
   AtomicString id() const { return id_; }
 
+  Element* element() const;
+
   void Trace(blink::Visitor*) override;
 
  private:
   void BuildJSONValue(V8ObjectBuilder&) const override;
 
+  WeakMember<Element> element_;
   Member<DOMRectReadOnly> intersection_rect_;
   DOMHighResTimeStamp response_end_;
   AtomicString identifier_;
diff --git a/third_party/blink/renderer/core/timing/performance_element_timing.idl b/third_party/blink/renderer/core/timing/performance_element_timing.idl
index e91161fd..7a2c5a5 100644
--- a/third_party/blink/renderer/core/timing/performance_element_timing.idl
+++ b/third_party/blink/renderer/core/timing/performance_element_timing.idl
@@ -11,6 +11,7 @@
     readonly attribute unsigned long naturalWidth;
     readonly attribute unsigned long naturalHeight;
     readonly attribute DOMString id;
+    readonly attribute Element? element;
 
     // TODO(peria): toJSON is not in spec. https://crbug.com/736332
     [CallWith=ScriptState, ImplementedAs=toJSONForBinding] object toJSON();
diff --git a/third_party/blink/renderer/core/timing/window_performance.cc b/third_party/blink/renderer/core/timing/window_performance.cc
index 5161099..b81f61b9 100644
--- a/third_party/blink/renderer/core/timing/window_performance.cc
+++ b/third_party/blink/renderer/core/timing/window_performance.cc
@@ -400,12 +400,13 @@
                                          TimeTicks response_end,
                                          const AtomicString& identifier,
                                          const IntSize& intrinsic_size,
-                                         const AtomicString& id) {
+                                         const AtomicString& id,
+                                         Element* element) {
   DCHECK(RuntimeEnabledFeatures::ElementTimingEnabled(GetExecutionContext()));
   PerformanceElementTiming* entry = PerformanceElementTiming::Create(
       name, rect, MonotonicTimeToDOMHighResTimeStamp(start_time),
       MonotonicTimeToDOMHighResTimeStamp(response_end), identifier,
-      intrinsic_size.Width(), intrinsic_size.Height(), id);
+      intrinsic_size.Width(), intrinsic_size.Height(), id, element);
   if (HasObserverFor(PerformanceEntry::kElement)) {
     UseCounter::Count(GetFrame()->GetDocument(),
                       WebFeature::kElementTimingExplicitlyRequested);
diff --git a/third_party/blink/renderer/core/timing/window_performance.h b/third_party/blink/renderer/core/timing/window_performance.h
index 36b1bea3..2555946 100644
--- a/third_party/blink/renderer/core/timing/window_performance.h
+++ b/third_party/blink/renderer/core/timing/window_performance.h
@@ -82,7 +82,8 @@
                         TimeTicks response_end,
                         const AtomicString& identifier,
                         const IntSize& intrinsic_size,
-                        const AtomicString& id);
+                        const AtomicString& id,
+                        Element*);
 
   void AddLayoutJankFraction(double jank_fraction);
 
diff --git a/third_party/blink/renderer/core/workers/worker_animation_frame_provider.cc b/third_party/blink/renderer/core/workers/worker_animation_frame_provider.cc
index ff75cab..c905828 100644
--- a/third_party/blink/renderer/core/workers/worker_animation_frame_provider.cc
+++ b/third_party/blink/renderer/core/workers/worker_animation_frame_provider.cc
@@ -5,6 +5,7 @@
 #include "third_party/blink/renderer/core/workers/worker_animation_frame_provider.h"
 
 #include "third_party/blink/renderer/core/offscreencanvas/offscreen_canvas.h"
+#include "third_party/blink/renderer/core/timing/worker_global_scope_performance.h"
 #include "third_party/blink/renderer/platform/cross_thread_functional.h"
 #include "third_party/blink/renderer/platform/wtf/time.h"
 
@@ -43,7 +44,11 @@
           FROM_HERE,
           WTF::Bind(
               [](base::WeakPtr<WorkerAnimationFrameProvider> provider) {
-                double time = WTF::CurrentTimeTicksInMilliseconds();
+                ExecutionContext* context = provider->context_;
+                Performance* performance =
+                    WorkerGlobalScopePerformance::performance(
+                        *To<WorkerGlobalScope>(context));
+                double time = performance->now();
                 // We don't want to expose microseconds residues to users.
                 time = round(time * 60) / 60;
 
diff --git a/third_party/blink/renderer/modules/credentialmanager/authentication_extensions_client_inputs.idl b/third_party/blink/renderer/modules/credentialmanager/authentication_extensions_client_inputs.idl
index 801caf8e..6c05e22 100644
--- a/third_party/blink/renderer/modules/credentialmanager/authentication_extensions_client_inputs.idl
+++ b/third_party/blink/renderer/modules/credentialmanager/authentication_extensions_client_inputs.idl
@@ -13,4 +13,7 @@
   boolean hmacCreateSecret;
   // https://w3c.github.io/webauthn/#sctn-uvm-extension
   boolean uvm;
+  // https://drafts.fidoalliance.org/fido-2/latest/fido-client-to-authenticator-protocol-v2.0-wd-20190409.html#sctn-credProtect-extension
+  USVString credentialProtectionPolicy;
+  boolean enforceCredentialProtectionPolicy = false;
 };
diff --git a/third_party/blink/renderer/modules/credentialmanager/credential_manager_type_converters.cc b/third_party/blink/renderer/modules/credentialmanager/credential_manager_type_converters.cc
index 8101bac..bf719a5 100644
--- a/third_party/blink/renderer/modules/credentialmanager/credential_manager_type_converters.cc
+++ b/third_party/blink/renderer/modules/credentialmanager/credential_manager_type_converters.cc
@@ -144,6 +144,9 @@
     case blink::mojom::blink::AuthenticatorStatus::
         USER_VERIFICATION_UNSUPPORTED:
       return CredentialManagerError::ANDROID_USER_VERIFICATION_UNSUPPORTED;
+    case blink::mojom::blink::AuthenticatorStatus::
+        PROTECTION_POLICY_INCONSISTENT:
+      return CredentialManagerError::PROTECTION_POLICY_INCONSISTENT;
     case blink::mojom::blink::AuthenticatorStatus::SUCCESS:
       NOTREACHED();
       break;
@@ -437,6 +440,8 @@
     }
   }
 
+  mojo_options->protection_policy = blink::mojom::ProtectionPolicy::UNSPECIFIED;
+  mojo_options->enforce_protection_policy = false;
   if (options->hasExtensions()) {
     auto* extensions = options->extensions();
     if (extensions->hasCableRegistration()) {
@@ -454,6 +459,24 @@
       mojo_options->user_verification_methods = extensions->uvm();
     }
 #endif
+    if (extensions->hasCredentialProtectionPolicy()) {
+      const auto& policy = extensions->credentialProtectionPolicy();
+      if (policy == "userVerificationOptional") {
+        mojo_options->protection_policy = blink::mojom::ProtectionPolicy::NONE;
+      } else if (policy == "userVerificationOptionalWithCredentialIDList") {
+        mojo_options->protection_policy =
+            blink::mojom::ProtectionPolicy::UV_OR_CRED_ID_REQUIRED;
+      } else if (policy == "userVerificationRequired") {
+        mojo_options->protection_policy =
+            blink::mojom::ProtectionPolicy::UV_REQUIRED;
+      } else {
+        return nullptr;
+      }
+    }
+    if (extensions->hasEnforceCredentialProtectionPolicy() &&
+        extensions->enforceCredentialProtectionPolicy()) {
+      mojo_options->enforce_protection_policy = true;
+    }
   }
 
   return mojo_options;
diff --git a/third_party/blink/renderer/modules/credentialmanager/credentials_container.cc b/third_party/blink/renderer/modules/credentialmanager/credentials_container.cc
index 56d1922..fe16478 100644
--- a/third_party/blink/renderer/modules/credentialmanager/credentials_container.cc
+++ b/third_party/blink/renderer/modules/credentialmanager/credentials_container.cc
@@ -250,6 +250,11 @@
                                   "Resident credentials or empty "
                                   "'allowCredentials' lists are not supported "
                                   "at this time.");
+    case CredentialManagerError::PROTECTION_POLICY_INCONSISTENT:
+      return DOMException::Create(
+          DOMExceptionCode::kNotSupportedError,
+          "Requested protection policy is inconsistent or incongurent with "
+          "other requested parameters.");
     case CredentialManagerError::ANDROID_ALGORITHM_UNSUPPORTED:
       return DOMException::Create(DOMExceptionCode::kNotSupportedError,
                                   "None of the algorithms specified in "
diff --git a/third_party/blink/renderer/modules/xr/xr.cc b/third_party/blink/renderer/modules/xr/xr.cc
index e764e865..b6d309e 100644
--- a/third_party/blink/renderer/modules/xr/xr.cc
+++ b/third_party/blink/renderer/modules/xr/xr.cc
@@ -51,7 +51,7 @@
   session_options->immersive = (mode == XRSession::kModeImmersiveVR ||
                                 mode == XRSession::kModeImmersiveAR);
   session_options->environment_integration =
-      (mode == XRSession::kModeInlineAR || mode == XRSession::kModeImmersiveAR);
+      mode == XRSession::kModeImmersiveAR;
 
   return session_options;
 }
@@ -60,9 +60,6 @@
   if (mode_string == "inline") {
     return XRSession::kModeInline;
   }
-  if (mode_string == "legacy-inline-ar") {
-    return XRSession::kModeInlineAR;
-  }
   if (mode_string == "immersive-vr") {
     return XRSession::kModeImmersiveVR;
   }
@@ -250,28 +247,19 @@
                                            kActiveImmersiveSession));
   }
 
-  // All immersive and AR sessions require a user gesture.
+  // All immersive sessions require a user gesture.
   bool has_user_activation = LocalFrame::HasTransientUserActivation(frame);
-  if ((is_immersive || session_mode == XRSession::kModeInlineAR) &&
-      !has_user_activation) {
+  if (is_immersive && !has_user_activation) {
     return ScriptPromise::RejectWithDOMException(
         script_state, DOMException::Create(DOMExceptionCode::kSecurityError,
                                            kRequestRequiresUserActivation));
   }
 
-  if (session_mode == XRSession::kModeInlineAR) {
-    doc->AddConsoleMessage(ConsoleMessage::Create(
-        mojom::ConsoleMessageSource::kOther,
-        mojom::ConsoleMessageLevel::kWarning,
-        "Inline AR is deprecated and will be removed soon."));
-  }
-
   auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
   ScriptPromise promise = resolver->Promise();
 
   PendingSessionQuery* query =
       MakeGarbageCollected<PendingSessionQuery>(resolver, session_mode);
-  query->has_user_activation = has_user_activation;
 
   if (!device_) {
     pending_session_requests_.push_back(query);
@@ -304,7 +292,6 @@
 
   device::mojom::blink::XRSessionOptionsPtr session_options =
       convertModeToMojo(query->mode);
-  session_options->has_user_activation = query->has_user_activation;
 
   // TODO(http://crbug.com/826899) Once device activation is sorted out for
   // WebXR, either pass in the correct value for metrics to know whether
@@ -413,8 +400,7 @@
     return;
   }
 
-  bool environment_integration = query->mode == XRSession::kModeInlineAR ||
-                                 query->mode == XRSession::kModeImmersiveAR;
+  bool environment_integration = query->mode == XRSession::kModeImmersiveAR;
 
   // immersive sessions must supply display info.
   DCHECK(session_ptr->display_info);
@@ -422,6 +408,11 @@
   // as well.
   DCHECK(!environment_integration || session_ptr->display_info->capabilities
                                          ->canProvideEnvironmentIntegration);
+  DVLOG(2) << __func__
+           << ": environment_integration=" << environment_integration
+           << "canProvideEnvironmentIntegration="
+           << session_ptr->display_info->capabilities
+                  ->canProvideEnvironmentIntegration;
 
   // TODO(https://crbug.com/944936): The blend mode could be "additive".
   XRSession::EnvironmentBlendMode blend_mode = XRSession::kBlendModeOpaque;
@@ -435,6 +426,14 @@
   if (query->mode == XRSession::kModeImmersiveVR ||
       query->mode == XRSession::kModeImmersiveAR) {
     frameProvider()->BeginImmersiveSession(session, std::move(session_ptr));
+    if (environment_integration) {
+      frameProvider()->GetDataProvider()->GetEnvironmentIntegrationProvider(
+          mojo::MakeRequest(&environment_provider_,
+                            GetExecutionContext()->GetTaskRunner(
+                                TaskType::kMiscPlatformAPI)));
+      environment_provider_.set_connection_error_handler(WTF::Bind(
+          &XR::OnEnvironmentProviderDisconnect, WrapWeakPersistent(this)));
+    }
   } else {
     magic_window_provider_.Bind(std::move(session_ptr->data_provider));
     if (environment_integration) {
diff --git a/third_party/blink/renderer/modules/xr/xr_frame_provider.cc b/third_party/blink/renderer/modules/xr/xr_frame_provider.cc
index 5dd5542..6b2d929b 100644
--- a/third_party/blink/renderer/modules/xr/xr_frame_provider.cc
+++ b/third_party/blink/renderer/modules/xr/xr_frame_provider.cc
@@ -187,15 +187,6 @@
   ScheduleNonImmersiveFrame();
 }
 
-bool XRFrameProvider::HasARSession() {
-  for (unsigned i = 0; i < requesting_sessions_.size(); ++i) {
-    XRSession* session = requesting_sessions_.at(i).Get();
-    if (session->environmentIntegration())
-      return true;
-  }
-  return false;
-}
-
 void XRFrameProvider::ScheduleImmersiveFrame() {
   TRACE_EVENT0("gpu", __FUNCTION__);
   if (pending_immersive_vsync_)
@@ -208,13 +199,10 @@
                          WrapWeakPersistent(this)));
 }
 
-// TODO(lincolnfrog): add a ScheduleNonImmersiveARFrame, if we want camera RAF
-// alignment instead of doc RAF alignment.
 void XRFrameProvider::ScheduleNonImmersiveFrame() {
   TRACE_EVENT0("gpu", __FUNCTION__);
   DCHECK(!immersive_session_)
       << "Scheduling should be done via the exclusive session if present.";
-  DCHECK(xr_->xrMagicWindowProviderPtr() || !HasARSession());
 
   if (pending_non_immersive_vsync_)
     return;
@@ -247,15 +235,8 @@
     frame_pose_ = nullptr;
   }
 
-  // TODO(https://crbug.com/839253): Generalize the pass-through images
-  // code path so that it also works for immersive sessions on an AR device
-  // with pass-through technology.
-
-  // TODO(http://crbug.com/856257) Remove the special casing for AR and non-AR.
-  if (!HasARSession()) {
-    doc->RequestAnimationFrame(
-        MakeGarbageCollected<XRFrameProviderRequestCallback>(this));
-  }
+  doc->RequestAnimationFrame(
+      MakeGarbageCollected<XRFrameProviderRequestCallback>(this));
 }
 
 void XRFrameProvider::OnImmersiveFrameData(
@@ -354,21 +335,6 @@
   }
 
   frame_pose_ = std::move(frame_data->pose);
-
-  base::TimeTicks monotonic_time_now = TimeTicks() + frame_data->time_delta;
-  double high_res_now_ms =
-      doc->Loader()
-          ->GetTiming()
-          .MonotonicTimeToZeroBasedDocumentTime(monotonic_time_now)
-          .InMillisecondsF();
-
-  if (HasARSession()) {
-    frame->GetTaskRunner(blink::TaskType::kInternalMedia)
-        ->PostTask(FROM_HERE,
-                   WTF::Bind(&XRFrameProvider::ProcessScheduledFrame,
-                             WrapWeakPersistent(this), std::move(frame_data),
-                             high_res_now_ms));
-  }
 }
 
 void XRFrameProvider::ProcessScheduledFrame(
@@ -425,8 +391,7 @@
       immersive_session_->UpdateStageParameters(frame_data->stage_parameters);
     }
     immersive_session_->OnFrame(high_res_now_ms, std::move(pose_matrix),
-                                buffer_mailbox_holder_, base::nullopt,
-                                base::nullopt);
+                                buffer_mailbox_holder_);
   } else {
     // In the process of fulfilling the frame requests for each session they are
     // extremely likely to request another frame. Work off of a separate list
@@ -443,26 +408,13 @@
                                     frame_pose_->input_state.value());
       }
 
-      if (frame_data && frame_data->projection_matrix.has_value()) {
-        session->SetNonImmersiveProjectionMatrix(
-            frame_data->projection_matrix.value());
-      }
-
       if (frame_pose_ && frame_pose_->pose_reset) {
         session->OnPoseReset();
       }
 
       std::unique_ptr<TransformationMatrix> pose_matrix =
           getPoseMatrix(frame_pose_);
-      // TODO(https://crbug.com/837883): only render background for
-      // sessions that are using AR.
-      if (frame_data) {
-        session->OnFrame(high_res_now_ms, std::move(pose_matrix), base::nullopt,
-                         frame_data->buffer_holder, frame_data->buffer_size);
-      } else {
-        session->OnFrame(high_res_now_ms, std::move(pose_matrix), base::nullopt,
-                         base::nullopt, base::nullopt);
-      }
+      session->OnFrame(high_res_now_ms, std::move(pose_matrix), base::nullopt);
     }
 
     processing_sessions_.clear();
@@ -554,16 +506,24 @@
   float width = layer->framebufferWidth();
   float height = layer->framebufferHeight();
 
-  WebFloatRect left_coords(
-      static_cast<float>(left->x()) / width,
-      static_cast<float>(height - (left->y() + left->height())) / height,
-      static_cast<float>(left->width()) / width,
-      static_cast<float>(left->height()) / height);
-  WebFloatRect right_coords(
-      static_cast<float>(right->x()) / width,
-      static_cast<float>(height - (right->y() + right->height())) / height,
-      static_cast<float>(right->width()) / width,
-      static_cast<float>(right->height()) / height);
+  // We may only have one eye view, i.e. in smartphone immersive AR mode.
+  // Use all-zero bounds for unused views.
+  WebFloatRect left_coords =
+      left ? WebFloatRect(
+                 static_cast<float>(left->x()) / width,
+                 static_cast<float>(height - (left->y() + left->height())) /
+                     height,
+                 static_cast<float>(left->width()) / width,
+                 static_cast<float>(left->height()) / height)
+           : WebFloatRect();
+  WebFloatRect right_coords =
+      right ? WebFloatRect(
+                  static_cast<float>(right->x()) / width,
+                  static_cast<float>(height - (right->y() + right->height())) /
+                      height,
+                  static_cast<float>(right->width()) / width,
+                  static_cast<float>(right->height()) / height)
+            : WebFloatRect();
 
   presentation_provider_->UpdateLayerBounds(
       frame_id_, left_coords, right_coords, WebSize(width, height));
diff --git a/third_party/blink/renderer/modules/xr/xr_frame_provider.h b/third_party/blink/renderer/modules/xr/xr_frame_provider.h
index 50024bf..1710e5a 100644
--- a/third_party/blink/renderer/modules/xr/xr_frame_provider.h
+++ b/third_party/blink/renderer/modules/xr/xr_frame_provider.h
@@ -43,6 +43,10 @@
   void Dispose();
   void OnFocusChanged();
 
+  device::mojom::blink::XRFrameDataProvider* GetDataProvider() {
+    return immersive_data_provider_.get();
+  }
+
   virtual void Trace(blink::Visitor*);
 
  private:
@@ -56,8 +60,6 @@
   void ProcessScheduledFrame(device::mojom::blink::XRFrameDataPtr frame_data,
                              double high_res_now_ms);
 
-  bool HasARSession();
-
   const Member<XR> xr_;
   Member<XRSession> immersive_session_;
   Member<XRFrameTransport> frame_transport_;
diff --git a/third_party/blink/renderer/modules/xr/xr_session.cc b/third_party/blink/renderer/modules/xr/xr_session.cc
index 1bcbbc25..539b47e 100644
--- a/third_party/blink/renderer/modules/xr/xr_session.cc
+++ b/third_party/blink/renderer/modules/xr/xr_session.cc
@@ -115,8 +115,7 @@
     bool sensorless_session)
     : xr_(xr),
       mode_(mode),
-      environment_integration_(mode == kModeInlineAR ||
-                               mode == kModeImmersiveAR),
+      environment_integration_(mode == kModeImmersiveAR),
       client_binding_(this, std::move(client_request)),
       callback_collection_(
           MakeGarbageCollected<XRFrameRequestCallbackCollection>(
@@ -150,16 +149,6 @@
   return mode_ == kModeImmersiveVR || mode_ == kModeImmersiveAR;
 }
 
-void XRSession::SetNonImmersiveProjectionMatrix(
-    const WTF::Vector<float>& projection_matrix) {
-  DCHECK_EQ(projection_matrix.size(), 16lu);
-
-  non_immersive_projection_matrix_ = projection_matrix;
-  // It is about as expensive to check equality as to just
-  // update the views, so just update.
-  update_views_next_frame_ = true;
-}
-
 ExecutionContext* XRSession::GetExecutionContext() const {
   return xr_->GetExecutionContext();
 }
@@ -511,12 +500,16 @@
     return OutputCanvasSize();
   }
 
-  double width = (display_info_->leftEye->renderWidth +
-                  display_info_->rightEye->renderWidth);
-  double height = std::max(display_info_->leftEye->renderHeight,
-                           display_info_->rightEye->renderHeight);
-
   double scale = display_info_->webxr_default_framebuffer_scale;
+  double width = display_info_->leftEye->renderWidth;
+  double height = display_info_->leftEye->renderHeight;
+
+  if (display_info_->rightEye) {
+    width += display_info_->rightEye->renderWidth;
+    height = std::max(display_info_->leftEye->renderHeight,
+                      display_info_->rightEye->renderHeight);
+  }
+
   return DoubleSize(width * scale, height * scale);
 }
 
@@ -639,9 +632,7 @@
 void XRSession::OnFrame(
     double timestamp,
     std::unique_ptr<TransformationMatrix> base_pose_matrix,
-    const base::Optional<gpu::MailboxHolder>& output_mailbox_holder,
-    const base::Optional<gpu::MailboxHolder>& background_mailbox_holder,
-    const base::Optional<IntSize>& background_size) {
+    const base::Optional<gpu::MailboxHolder>& output_mailbox_holder) {
   TRACE_EVENT0("gpu", __FUNCTION__);
   DVLOG(2) << __FUNCTION__;
   // Don't process any outstanding frames once the session is ended.
@@ -678,15 +669,6 @@
 
     frame_base_layer->OnFrameStart(output_mailbox_holder);
 
-    // TODO(836349): revisit sending background image data to blink at all.
-    if (background_mailbox_holder) {
-      // If using a background image, the caller must provide its pixel size
-      // also. The source size can differ from the current drawing buffer size.
-      DCHECK(background_size);
-      frame_base_layer->HandleBackgroundImage(background_mailbox_holder.value(),
-                                              background_size.value());
-    }
-
     // Resolve the queued requestAnimationFrame callbacks. All XR rendering will
     // happen within these calls. resolving_frame_ will be true for the duration
     // of the callbacks.
@@ -746,12 +728,6 @@
     DVLOG(2) << __FUNCTION__ << ": got angle=" << output_angle;
   }
 
-  if (xr_->xrEnvironmentProviderPtr()) {
-    xr_->xrEnvironmentProviderPtr()->UpdateSessionGeometry(
-        IntSize(output_width_, output_height_),
-        display::Display::DegreesToRotation(output_angle));
-  }
-
   if (render_state_->baseLayer()) {
     render_state_->baseLayer()->OnResize();
   }
@@ -975,16 +951,21 @@
       // If we don't already have the views allocated, do so now.
       if (views_.IsEmpty()) {
         views_.push_back(MakeGarbageCollected<XRView>(this, XRView::kEyeLeft));
-        views_.push_back(MakeGarbageCollected<XRView>(this, XRView::kEyeRight));
+        if (display_info_->rightEye) {
+          views_.push_back(
+              MakeGarbageCollected<XRView>(this, XRView::kEyeRight));
+        }
       }
       // In immersive mode the projection and view matrices must be aligned with
       // the device's physical optics.
       UpdateViewFromEyeParameters(
           views_[XRView::kEyeLeft], display_info_->leftEye,
           render_state_->depthNear(), render_state_->depthFar());
-      UpdateViewFromEyeParameters(
-          views_[XRView::kEyeRight], display_info_->rightEye,
-          render_state_->depthNear(), render_state_->depthFar());
+      if (display_info_->rightEye) {
+        UpdateViewFromEyeParameters(
+            views_[XRView::kEyeRight], display_info_->rightEye,
+            render_state_->depthNear(), render_state_->depthFar());
+      }
     } else {
       if (views_.IsEmpty()) {
         views_.push_back(MakeGarbageCollected<XRView>(this, XRView::kEyeLeft));
@@ -997,31 +978,15 @@
                  static_cast<float>(output_height_);
       }
 
-      if (non_immersive_projection_matrix_.size() > 0) {
-        views_[XRView::kEyeLeft]->UpdateProjectionMatrixFromRawValues(
-            non_immersive_projection_matrix_, render_state_->depthNear(),
-            render_state_->depthFar());
-      } else {
-        // In non-immersive mode, if there is no explicit projection matrix
-        // provided, the projection matrix must be aligned with the
-        // output canvas dimensions.
-        views_[XRView::kEyeLeft]->UpdateProjectionMatrixFromAspect(
-            kMagicWindowVerticalFieldOfView, aspect, render_state_->depthNear(),
-            render_state_->depthFar());
-      }
+      // In non-immersive mode, if there is no explicit projection matrix
+      // provided, the projection matrix must be aligned with the
+      // output canvas dimensions.
+      views_[XRView::kEyeLeft]->UpdateProjectionMatrixFromAspect(
+          kMagicWindowVerticalFieldOfView, aspect, render_state_->depthNear(),
+          render_state_->depthFar());
     }
 
     views_dirty_ = false;
-  } else {
-    // TODO(https://crbug.com/836926): views_dirty_ is not working right for
-    // AR mode, we're not picking up the change on the right frame. Remove this
-    // fallback once that's sorted out.
-    DVLOG(2) << __FUNCTION__ << ": FIXME, fallback proj matrix update";
-    if (non_immersive_projection_matrix_.size() > 0) {
-      views_[XRView::kEyeLeft]->UpdateProjectionMatrixFromRawValues(
-          non_immersive_projection_matrix_, render_state_->depthNear(),
-          render_state_->depthFar());
-    }
   }
 
   return views_;
diff --git a/third_party/blink/renderer/modules/xr/xr_session.h b/third_party/blink/renderer/modules/xr/xr_session.h
index 7c1c559d..9567a8e 100644
--- a/third_party/blink/renderer/modules/xr/xr_session.h
+++ b/third_party/blink/renderer/modules/xr/xr_session.h
@@ -49,12 +49,7 @@
   USING_GARBAGE_COLLECTED_MIXIN(XRSession);
 
  public:
-  enum SessionMode {
-    kModeInline = 0,
-    kModeImmersiveVR,
-    kModeImmersiveAR,
-    kModeInlineAR
-  };
+  enum SessionMode { kModeInline = 0, kModeImmersiveVR, kModeImmersiveAR };
 
   enum EnvironmentBlendMode {
     kBlendModeOpaque = 0,
@@ -70,7 +65,6 @@
   ~XRSession() override = default;
 
   XR* xr() const { return xr_; }
-  bool environmentIntegration() const { return environment_integration_; }
   const String& environmentBlendMode() const { return blend_mode_string_; }
   XRRenderState* renderState() const { return render_state_; }
   XRWorldTrackingState* worldTrackingState() { return nullptr; }
@@ -134,9 +128,7 @@
   void OnFocusChanged();
   void OnFrame(double timestamp,
                std::unique_ptr<TransformationMatrix>,
-               const base::Optional<gpu::MailboxHolder>& output_mailbox_holder,
-               const base::Optional<gpu::MailboxHolder>& bg_mailbox_holder,
-               const base::Optional<IntSize>& background_size);
+               const base::Optional<gpu::MailboxHolder>& output_mailbox_holder);
   void OnInputStateChange(
       int16_t frame_id,
       const WTF::Vector<device::mojom::blink::XRInputSourceStatePtr>&);
@@ -164,6 +156,11 @@
     return true;
   }
 
+  // Immersive sessions currently use two views for VR, and only a single view
+  // for smartphone immersive AR mode. Convention is that we use the left eye
+  // if there's only a single view.
+  bool StereoscopicViews() { return display_info_ && display_info_->rightEye; }
+
   void UpdateEyeParameters(
       const device::mojom::blink::VREyeParametersPtr& left_eye,
       const device::mojom::blink::VREyeParametersPtr& right_eye);
@@ -175,7 +172,6 @@
   unsigned int DisplayInfoPtrId() const { return display_info_id_; }
   unsigned int StageParametersId() const { return stage_parameters_id_; }
 
-  void SetNonImmersiveProjectionMatrix(const WTF::Vector<float>&);
   void SetXRDisplayInfo(device::mojom::blink::VRDisplayInfoPtr display_info);
 
   void Trace(blink::Visitor*) override;
@@ -238,8 +234,6 @@
   Member<XRFrameRequestCallbackCollection> callback_collection_;
   std::unique_ptr<TransformationMatrix> base_pose_matrix_;
 
-  WTF::Vector<float> non_immersive_projection_matrix_;
-
   bool blurred_;
   bool ended_ = false;
   bool pending_frame_ = false;
diff --git a/third_party/blink/renderer/modules/xr/xr_session.idl b/third_party/blink/renderer/modules/xr/xr_session.idl
index f5e8c74..bec991ce2 100644
--- a/third_party/blink/renderer/modules/xr/xr_session.idl
+++ b/third_party/blink/renderer/modules/xr/xr_session.idl
@@ -5,7 +5,6 @@
 // https://immersive-web.github.io/webxr/#xrsession-interface
 enum XRSessionMode {
   "inline",
-  "legacy-inline-ar",
   "immersive-vr",
   "immersive-ar",
 };
diff --git a/third_party/blink/renderer/modules/xr/xr_view.cc b/third_party/blink/renderer/modules/xr/xr_view.cc
index 7290624..ea2be55 100644
--- a/third_party/blink/renderer/modules/xr/xr_view.cc
+++ b/third_party/blink/renderer/modules/xr/xr_view.cc
@@ -64,30 +64,6 @@
   return session_;
 }
 
-// TODO(http://crbug.com/836496): This method only supports
-// straight-ahead projection matrices. In order to support
-// multiple sessions embedded with projection matrices that act
-// like views into the shared camera space, this math needs to
-// be updated.
-void XRView::UpdateProjectionMatrixFromRawValues(
-    const WTF::Vector<float>& projection_matrix,
-    float near_depth,
-    float far_depth) {
-  DCHECK_EQ(projection_matrix.size(), 16lu);
-  float* out = projection_matrix_->Data();
-  for (int i = 0; i < 16; i++) {
-    out[i] = projection_matrix[i];
-  }
-
-  // Recalculate elements that depend on near/far depth. The input matrix used
-  // arbitrary values, need to adjust to what the client uses.
-  float inverse_near_far = 1.0f / (near_depth - far_depth);
-  out[10] = (near_depth + far_depth) * inverse_near_far;
-  out[14] = (2.0f * far_depth * near_depth) * inverse_near_far;
-
-  inv_projection_dirty_ = true;
-}
-
 void XRView::UpdateProjectionMatrixFromFoV(float up_rad,
                                            float down_rad,
                                            float left_rad,
diff --git a/third_party/blink/renderer/modules/xr/xr_view.h b/third_party/blink/renderer/modules/xr/xr_view.h
index 77e25afb..4f215d6d 100644
--- a/third_party/blink/renderer/modules/xr/xr_view.h
+++ b/third_party/blink/renderer/modules/xr/xr_view.h
@@ -39,11 +39,6 @@
   DOMFloat32Array* projectionMatrix() const { return projection_matrix_; }
   XRRigidTransform* transform() const;
 
-  void UpdateProjectionMatrixFromRawValues(
-      const WTF::Vector<float>& projection_matrix,
-      float near_depth,
-      float far_depth);
-
   void UpdateProjectionMatrixFromFoV(float up_rad,
                                      float down_rad,
                                      float left_rad,
diff --git a/third_party/blink/renderer/modules/xr/xr_webgl_layer.cc b/third_party/blink/renderer/modules/xr/xr_webgl_layer.cc
index 4cde7cd6..9f97a4f 100644
--- a/third_party/blink/renderer/modules/xr/xr_webgl_layer.cc
+++ b/third_party/blink/renderer/modules/xr/xr_webgl_layer.cc
@@ -214,13 +214,24 @@
   viewports_dirty_ = false;
 
   if (session()->immersive()) {
-    left_viewport_ = MakeGarbageCollected<XRViewport>(
-        0, 0, framebuffer_width * 0.5 * viewport_scale_,
-        framebuffer_height * viewport_scale_);
-    right_viewport_ = MakeGarbageCollected<XRViewport>(
-        framebuffer_width * 0.5 * viewport_scale_, 0,
-        framebuffer_width * 0.5 * viewport_scale_,
-        framebuffer_height * viewport_scale_);
+    if (session()->StereoscopicViews()) {
+      left_viewport_ = MakeGarbageCollected<XRViewport>(
+          0, 0, framebuffer_width * 0.5 * viewport_scale_,
+          framebuffer_height * viewport_scale_);
+      right_viewport_ = MakeGarbageCollected<XRViewport>(
+          framebuffer_width * 0.5 * viewport_scale_, 0,
+          framebuffer_width * 0.5 * viewport_scale_,
+          framebuffer_height * viewport_scale_);
+    } else {
+      // Phone immersive AR only uses one viewport, but the second viewport is
+      // needed for the UpdateLayerBounds mojo call which currently expects
+      // exactly two views. This should be revisited as part of a refactor to
+      // handle a more general list of viewports, cf. https://crbug.com/928433.
+      left_viewport_ = MakeGarbageCollected<XRViewport>(
+          0, 0, framebuffer_width * viewport_scale_,
+          framebuffer_height * viewport_scale_);
+      right_viewport_ = nullptr;
+    }
 
     session()->xr()->frameProvider()->UpdateWebGLLayerViewports(this);
 
@@ -274,13 +285,6 @@
   }
 }
 
-void XRWebGLLayer::OverwriteColorBufferFromMailboxTexture(
-    const gpu::MailboxHolder& mailbox_holder,
-    const IntSize& size) {
-  drawing_buffer_->OverwriteColorBufferFromMailboxTexture(mailbox_holder, size);
-  framebuffer_->SetContentsChanged(true);
-}
-
 void XRWebGLLayer::OnFrameStart(
     const base::Optional<gpu::MailboxHolder>& buffer_mailbox_holder) {
   // If the requested scale has changed since the last from, update it now.
@@ -337,12 +341,6 @@
   viewports_dirty_ = true;
 }
 
-void XRWebGLLayer::HandleBackgroundImage(
-    const gpu::MailboxHolder& mailbox_holder,
-    const IntSize& size) {
-  OverwriteColorBufferFromMailboxTexture(mailbox_holder, size);
-}
-
 scoped_refptr<StaticBitmapImage> XRWebGLLayer::TransferToStaticBitmapImage(
     std::unique_ptr<viz::SingleReleaseCallback>* out_release_callback) {
   return drawing_buffer_->TransferToStaticBitmapImage(out_release_callback);
diff --git a/third_party/blink/renderer/modules/xr/xr_webgl_layer.h b/third_party/blink/renderer/modules/xr/xr_webgl_layer.h
index b43b2ae..1219109 100644
--- a/third_party/blink/renderer/modules/xr/xr_webgl_layer.h
+++ b/third_party/blink/renderer/modules/xr/xr_webgl_layer.h
@@ -69,11 +69,6 @@
   void OnFrameStart(const base::Optional<gpu::MailboxHolder>&) override;
   void OnFrameEnd() override;
   void OnResize() override;
-  void HandleBackgroundImage(const gpu::MailboxHolder&,
-                             const IntSize&) override;
-
-  void OverwriteColorBufferFromMailboxTexture(const gpu::MailboxHolder&,
-                                              const IntSize& size);
 
   void UpdateWebXRMirror();
 
diff --git a/third_party/blink/renderer/platform/fonts/shaping/shape_cache.h b/third_party/blink/renderer/platform/fonts/shaping/shape_cache.h
index f59552c7..44e55bc 100644
--- a/third_party/blink/renderer/platform/fonts/shaping/shape_cache.h
+++ b/third_party/blink/renderer/platform/fonts/shaping/shape_cache.h
@@ -63,25 +63,23 @@
         : length_(kDeletedValueLength),
           direction_(static_cast<unsigned>(TextDirection::kLtr)) {}
 
-    SmallStringKey(const LChar* characters,
-                   uint16_t length,
-                   TextDirection direction)
-        : length_(length), direction_(static_cast<unsigned>(direction)) {
-      DCHECK(length <= kCapacity);
+    SmallStringKey(base::span<const LChar> characters, TextDirection direction)
+        : length_(static_cast<uint16_t>(characters.size())),
+          direction_(static_cast<unsigned>(direction)) {
+      DCHECK(characters.size() <= kCapacity);
       // Up-convert from LChar to UChar.
-      for (uint16_t i = 0; i < length; ++i) {
+      for (uint16_t i = 0; i < characters.size(); ++i) {
         characters_[i] = characters[i];
       }
 
       HashString();
     }
 
-    SmallStringKey(const UChar* characters,
-                   uint16_t length,
-                   TextDirection direction)
-        : length_(length), direction_(static_cast<unsigned>(direction)) {
-      DCHECK(length <= kCapacity);
-      memcpy(characters_, characters, length * sizeof(UChar));
+    SmallStringKey(base::span<const UChar> characters, TextDirection direction)
+        : length_(static_cast<uint16_t>(characters.size())),
+          direction_(static_cast<unsigned>(direction)) {
+      DCHECK(characters.size() <= kCapacity);
+      memcpy(characters_, characters.data(), characters.size_bytes());
       HashString();
     }
 
@@ -152,10 +150,9 @@
 
  private:
   ShapeCacheEntry* AddSlowCase(const TextRun& run, ShapeCacheEntry entry) {
-    unsigned length = run.length();
     bool is_new_entry;
     ShapeCacheEntry* value;
-    if (length == 1) {
+    if (run.length() == 1) {
       uint32_t key = run[0];
       // All current codepoints in UTF-32 are bewteen 0x0 and 0x10FFFF,
       // as such use bit 31 (zero-based) to indicate direction.
@@ -167,11 +164,9 @@
     } else {
       SmallStringKey small_string_key;
       if (run.Is8Bit()) {
-        small_string_key =
-            SmallStringKey(run.Characters8(), length, run.Direction());
+        small_string_key = SmallStringKey(run.Span8(), run.Direction());
       } else {
-        small_string_key =
-            SmallStringKey(run.Characters16(), length, run.Direction());
+        small_string_key = SmallStringKey(run.Span16(), run.Direction());
       }
 
       SmallStringMap::AddResult add_result =
diff --git a/third_party/blink/renderer/platform/fonts/shaping/shape_result_spacing.cc b/third_party/blink/renderer/platform/fonts/shaping/shape_result_spacing.cc
index 128481c..ad24510 100644
--- a/third_party/blink/renderer/platform/fonts/shaping/shape_result_spacing.cc
+++ b/third_party/blink/renderer/platform/fonts/shaping/shape_result_spacing.cc
@@ -84,12 +84,10 @@
   bool is_after_expansion = is_after_expansion_;
   if (text_.Is8Bit()) {
     expansion_opportunity_count_ = Character::ExpansionOpportunityCount(
-        text_.Characters8(), text_.length(), direction, is_after_expansion,
-        text_justify_);
+        text_.Span8(), direction, is_after_expansion, text_justify_);
   } else {
     expansion_opportunity_count_ = Character::ExpansionOpportunityCount(
-        text_.Characters16(), text_.length(), direction, is_after_expansion,
-        text_justify_);
+        text_.Span16(), direction, is_after_expansion, text_justify_);
   }
   if (is_after_expansion && !allows_trailing_expansion) {
     DCHECK_GT(expansion_opportunity_count_, 0u);
diff --git a/third_party/blink/renderer/platform/graphics/compositing/paint_artifact_compositor.cc b/third_party/blink/renderer/platform/graphics/compositing/paint_artifact_compositor.cc
index f82f270..c163574 100644
--- a/third_party/blink/renderer/platform/graphics/compositing/paint_artifact_compositor.cc
+++ b/third_party/blink/renderer/platform/graphics/compositing/paint_artifact_compositor.cc
@@ -1119,22 +1119,22 @@
   return false;
 }
 
-static bool IsRenderSurfaceCandidate(
+static cc::RenderSurfaceReason GetRenderSurfaceCandidateReason(
     const cc::EffectNode& effect,
     const Vector<const EffectPaintPropertyNode*>& blink_effects) {
-  if (effect.has_render_surface)
-    return false;
+  if (effect.HasRenderSurface())
+    return cc::RenderSurfaceReason::kNone;
   if (effect.blend_mode != SkBlendMode::kSrcOver)
-    return true;
+    return cc::RenderSurfaceReason::kBlendModeDstIn;
   if (effect.opacity != 1.f)
-    return true;
+    return cc::RenderSurfaceReason::kOpacity;
   if (static_cast<size_t>(effect.id) < blink_effects.size() &&
       blink_effects[effect.id] &&
       blink_effects[effect.id]->HasActiveOpacityAnimation())
-    return true;
+    return cc::RenderSurfaceReason::kOpacityAnimation;
   if (effect.is_fast_rounded_corner)
-    return true;
-  return false;
+    return cc::RenderSurfaceReason::kRoundedCorner;
+  return cc::RenderSurfaceReason::kNone;
 }
 
 // Every effect is supposed to have render surface enabled for grouping, but we
@@ -1163,16 +1163,18 @@
   for (auto id = effect_tree.size() - 1;
        id > cc::EffectTree::kSecondaryRootNodeId; id--) {
     auto* effect = effect_tree.Node(id);
-    if (effect_layer_counts[id] > 1 &&
-        IsRenderSurfaceCandidate(*effect, blink_effects)) {
-      // The render surface candidate needs a render surface because it
-      // controls more than 1 layer.
-      effect->has_render_surface = true;
+    if (effect_layer_counts[id] > 1) {
+      auto reason = GetRenderSurfaceCandidateReason(*effect, blink_effects);
+      if (reason != cc::RenderSurfaceReason::kNone) {
+        // The render surface candidate needs a render surface because it
+        // controls more than 1 layer.
+        effect->render_surface_reason = reason;
+      }
     }
 
     // We should not have visited the parent.
     DCHECK_NE(-1, effect_layer_counts[effect->parent_id]);
-    if (effect->has_render_surface) {
+    if (effect->HasRenderSurface()) {
       // A sub-render-surface counts as one controlled layer of the parent.
       effect_layer_counts[effect->parent_id]++;
     } else {
diff --git a/third_party/blink/renderer/platform/graphics/compositing/paint_artifact_compositor_test.cc b/third_party/blink/renderer/platform/graphics/compositing/paint_artifact_compositor_test.cc
index 565ffcf..6543a73 100644
--- a/third_party/blink/renderer/platform/graphics/compositing/paint_artifact_compositor_test.cc
+++ b/third_party/blink/renderer/platform/graphics/compositing/paint_artifact_compositor_test.cc
@@ -2033,7 +2033,7 @@
   EXPECT_EQ(gfx::Size(200, 200), masked_layer->bounds());
   const cc::EffectNode* masked_group =
       GetPropertyTrees().effect_tree.Node(masked_layer->effect_tree_index());
-  EXPECT_TRUE(masked_group->has_render_surface);
+  EXPECT_TRUE(masked_group->HasRenderSurface());
 
   const cc::Layer* masking_layer = ContentLayerAt(1);
   EXPECT_THAT(
@@ -2043,7 +2043,7 @@
   EXPECT_EQ(gfx::Size(100, 100), masking_layer->bounds());
   const cc::EffectNode* masking_group =
       GetPropertyTrees().effect_tree.Node(masking_layer->effect_tree_index());
-  EXPECT_FALSE(masking_group->has_render_surface);
+  EXPECT_FALSE(masking_group->HasRenderSurface());
   EXPECT_EQ(masked_group->id, masking_group->parent_id);
   ASSERT_EQ(1u, masking_group->filters.size());
   EXPECT_EQ(cc::FilterOperation::REFERENCE,
@@ -2081,7 +2081,7 @@
       GetPropertyTrees().effect_tree.Node(masking_layer->effect_tree_index());
 
   // There is a render surface because there are two children.
-  EXPECT_TRUE(masking_group->has_render_surface);
+  EXPECT_TRUE(masking_group->HasRenderSurface());
   ASSERT_EQ(1u, masking_group->filters.size());
   EXPECT_EQ(cc::FilterOperation::REFERENCE,
             masking_group->filters.at(0).type());
@@ -2112,7 +2112,7 @@
       GetPropertyTrees().effect_tree.Node(masking_layer->effect_tree_index());
 
   /// This requires a render surface.
-  EXPECT_TRUE(masking_group->has_render_surface);
+  EXPECT_TRUE(masking_group->HasRenderSurface());
 }
 
 TEST_P(PaintArtifactCompositorTest, UpdateProducesNewSequenceNumber) {
@@ -3483,7 +3483,7 @@
     const auto* effect = GetPropertyTrees().effect_tree.Node(effect_id); \
     EXPECT_EQ(expected_opacity, effect->opacity);                        \
     EXPECT_EQ(!!((expected_flags)&kHasRenderSurface),                    \
-              effect->has_render_surface);                               \
+              effect->HasRenderSurface());                               \
   } while (false)
 
 TEST_P(PaintArtifactCompositorTest, OpacityRenderSurfaces) {
@@ -3680,7 +3680,7 @@
 
   const auto* effect = GetPropertyTrees().effect_tree.Node(
       ContentLayerAt(1)->effect_tree_index());
-  EXPECT_TRUE(effect->has_render_surface);
+  EXPECT_TRUE(effect->HasRenderSurface());
 }
 
 TEST_P(PaintArtifactCompositorTest,
@@ -3701,7 +3701,7 @@
 
   const auto* effect = GetPropertyTrees().effect_tree.Node(
       ContentLayerAt(1)->effect_tree_index());
-  EXPECT_TRUE(effect->has_render_surface);
+  EXPECT_TRUE(effect->HasRenderSurface());
 }
 
 TEST_P(PaintArtifactCompositorTest, OpacityIndirectlyAffectingTwoLayers) {
diff --git a/third_party/blink/renderer/platform/graphics/compositing/property_tree_manager.cc b/third_party/blink/renderer/platform/graphics/compositing/property_tree_manager.cc
index 7d7b210..d6ee4d3 100644
--- a/third_party/blink/renderer/platform/graphics/compositing/property_tree_manager.cc
+++ b/third_party/blink/renderer/platform/graphics/compositing/property_tree_manager.cc
@@ -261,7 +261,7 @@
       CompositorElementIdFromUniqueObjectId(unique_id).GetInternalValue();
   effect_node.transform_id = kRealRootNodeId;
   effect_node.clip_id = kSecondaryRootNodeId;
-  effect_node.has_render_surface = true;
+  effect_node.render_surface_reason = cc::RenderSurfaceReason::kRoot;
   root_layer_->SetEffectTreeIndex(effect_node.id);
 
   SetCurrentEffectState(effect_node, CcEffectType::kEffect,
@@ -294,9 +294,8 @@
   return false;
 }
 
-static bool TransformsMayBe2dAxisMisaligned(
-    const TransformPaintPropertyNode& a,
-    const TransformPaintPropertyNode& b) {
+bool TransformsMayBe2dAxisMisaligned(const TransformPaintPropertyNode& a,
+                                     const TransformPaintPropertyNode& b) {
   if (&a == &b)
     return false;
   const auto& translation_2d_or_matrix =
@@ -306,8 +305,10 @@
     return true;
   // Assume any animation can cause 2d axis misalignment.
   const auto& lca = LowestCommonAncestor(a, b);
-  return TransformsToAncestorHaveActiveAnimation(a, lca) ||
-         TransformsToAncestorHaveActiveAnimation(b, lca);
+  if (TransformsToAncestorHaveActiveAnimation(a, lca) ||
+      TransformsToAncestorHaveActiveAnimation(b, lca))
+    return true;
+  return false;
 }
 
 void PropertyTreeManager::SetCurrentEffectState(
@@ -324,11 +325,11 @@
   DCHECK(!clip.IsParentAlias() || !clip.Parent());
   current_.clip = &clip;
 
-  if (cc_effect_node.has_render_surface) {
-    current_.may_be_2d_axis_misaligned_to_render_surface = false;
+  if (cc_effect_node.HasRenderSurface()) {
+    current_.may_be_2d_axis_misaligned_to_render_surface = 0;
   } else if (previous_transform &&
              !current_.may_be_2d_axis_misaligned_to_render_surface) {
-    current_.may_be_2d_axis_misaligned_to_render_surface =
+    current_.may_be_2d_axis_misaligned_to_render_surface |=
         TransformsMayBe2dAxisMisaligned(*previous_transform,
                                         current_.Transform());
   }
@@ -336,8 +337,10 @@
 
 // TODO(crbug.com/504464): Remove this when move render surface decision logic
 // into cc compositor thread.
-void PropertyTreeManager::SetCurrentEffectHasRenderSurface() {
-  GetEffectTree().Node(current_.effect_id)->has_render_surface = true;
+void PropertyTreeManager::SetCurrentEffectRenderSurfaceReason(
+    cc::RenderSurfaceReason reason) {
+  auto* effect = GetEffectTree().Node(current_.effect_id);
+  effect->render_surface_reason = reason;
 }
 
 int PropertyTreeManager::EnsureCompositorTransformNode(
@@ -420,12 +423,14 @@
   // context of an ancestor, cc needs a render surface for correct flattening.
   // TODO(crbug.com/504464): Move the logic into cc compositor thread.
   auto* current_cc_effect = GetEffectTree().Node(current_.effect_id);
-  if (current_cc_effect && !current_cc_effect->has_render_surface &&
+  if (current_cc_effect && !current_cc_effect->HasRenderSurface() &&
       current_cc_effect->transform_id == parent_id &&
       transform_node.FlattensInheritedTransform() && transform_node.Parent() &&
       transform_node.Parent()->RenderingContextId() &&
-      !transform_node.Parent()->FlattensInheritedTransform())
-    SetCurrentEffectHasRenderSurface();
+      !transform_node.Parent()->FlattensInheritedTransform()) {
+    SetCurrentEffectRenderSurfaceReason(
+        cc::RenderSurfaceReason::k3dTransformFlattening);
+  }
 
   auto result = transform_node_map_.Set(&transform_node, id);
   DCHECK(result.is_new_entry);
@@ -745,7 +750,7 @@
 
   // Cc requires that a rectangluar clip is 2d-axis-aligned with the render
   // surface to correctly apply the clip.
-  if (current_.may_be_2d_axis_misaligned_to_render_surface ||
+  if (current_.may_be_2d_axis_misaligned_to_render_surface |
       TransformsMayBe2dAxisMisaligned(clip.LocalTransformSpace(),
                                       current_.Transform()))
     return CcEffectType::kSyntheticFor2dAxisAlignment;
@@ -766,7 +771,7 @@
     // An effect node can't omit render surface if it has child with exotic
     // blending mode. See comments below for more detail.
     // TODO(crbug.com/504464): Remove premature optimization here.
-    SetCurrentEffectHasRenderSurface();
+    SetCurrentEffectRenderSurfaceReason(cc::RenderSurfaceReason::kBlendMode);
   } else {
     // Exit synthetic effects until there are no more synthesized clips below
     // our lowest common ancestor.
@@ -826,7 +831,6 @@
       // when the effect is closed. For now the default value INVALID_STABLE_ID
       // is used. See PropertyTreeManager::EmitClipMaskLayer().
     } else {
-      DCHECK_EQ(pending_clip.type, CcEffectType::kSyntheticFor2dAxisAlignment);
       synthetic_effect.stable_id =
           CompositorElementIdFromUniqueObjectId(NewUniqueObjectId())
               .GetInternalValue();
@@ -845,7 +849,16 @@
           gfx::RRectF(pending_clip.clip->ClipRect());
       synthetic_effect.is_fast_rounded_corner = true;
     } else {
-      synthetic_effect.has_render_surface = true;
+      if (pending_clip.type == CcEffectType::kSyntheticForNonTrivialClip) {
+        synthetic_effect.render_surface_reason =
+            pending_clip.clip->ClipRect().IsRounded()
+                ? cc::RenderSurfaceReason::kRoundedCorner
+                : cc::RenderSurfaceReason::kClipPath;
+      } else {
+        synthetic_effect.render_surface_reason =
+            cc::RenderSurfaceReason::kClipAxisAlignment;
+      }
+
       pending_synthetic_mask_layers_.insert(synthetic_effect.id);
     }
 
@@ -934,7 +947,7 @@
     // blending mode.
     // TODO(crbug.com/504464): Remove premature optimization here.
     if (effect.BlendMode() != SkBlendMode::kSrcOver)
-      SetCurrentEffectHasRenderSurface();
+      SetCurrentEffectRenderSurfaceReason(cc::RenderSurfaceReason::kBlendMode);
 
     blend_mode = effect.BlendMode();
     output_clip = current_.clip;
@@ -957,12 +970,20 @@
   // Also, kDstIn and kSrcOver blend modes have fast paths if only one layer
   // is under the blend mode. This value is adjusted in PaintArtifactCompositor
   // ::UpdateRenderSurfaceForEffects() to account for more than one layer.
-  if (!effect.Filter().IsEmpty() || effect.HasActiveFilterAnimation() ||
-      !effect.BackdropFilter().IsEmpty() ||
-      effect.HasActiveBackdropFilterAnimation() ||
-      (blend_mode != SkBlendMode::kSrcOver &&
-       blend_mode != SkBlendMode::kDstIn)) {
-    effect_node.has_render_surface = true;
+  if (!effect.Filter().IsEmpty()) {
+    effect_node.render_surface_reason = cc::RenderSurfaceReason::kFilter;
+  } else if (effect.HasActiveFilterAnimation()) {
+    effect_node.render_surface_reason =
+        cc::RenderSurfaceReason::kFilterAnimation;
+  } else if (!effect.BackdropFilter().IsEmpty()) {
+    effect_node.render_surface_reason =
+        cc::RenderSurfaceReason::kBackdropFilter;
+  } else if (effect.HasActiveBackdropFilterAnimation()) {
+    effect_node.render_surface_reason =
+        cc::RenderSurfaceReason::kBackdropFilterAnimation;
+  } else if (blend_mode != SkBlendMode::kSrcOver &&
+             blend_mode != SkBlendMode::kDstIn) {
+    effect_node.render_surface_reason = cc::RenderSurfaceReason::kBlendMode;
   }
 
   effect_node.opacity = effect.Opacity();
diff --git a/third_party/blink/renderer/platform/graphics/compositing/property_tree_manager.h b/third_party/blink/renderer/platform/graphics/compositing/property_tree_manager.h
index 2b6ab16..6eb8ada0 100644
--- a/third_party/blink/renderer/platform/graphics/compositing/property_tree_manager.h
+++ b/third_party/blink/renderer/platform/graphics/compositing/property_tree_manager.h
@@ -22,6 +22,7 @@
 class TransformTree;
 struct EffectNode;
 struct TransformNode;
+enum class RenderSurfaceReason : uint8_t;
 }
 
 namespace blink {
@@ -198,7 +199,7 @@
                              CcEffectType,
                              const EffectPaintPropertyNode&,
                              const ClipPaintPropertyNode&);
-  void SetCurrentEffectHasRenderSurface();
+  void SetCurrentEffectRenderSurfaceReason(cc::RenderSurfaceReason);
 
   cc::TransformTree& GetTransformTree();
   cc::ClipTree& GetClipTree();
diff --git a/third_party/blink/renderer/platform/graphics/dark_mode_filter.cc b/third_party/blink/renderer/platform/graphics/dark_mode_filter.cc
index 7229c9e..5d0233c2 100644
--- a/third_party/blink/renderer/platform/graphics/dark_mode_filter.cc
+++ b/third_party/blink/renderer/platform/graphics/dark_mode_filter.cc
@@ -1,12 +1,32 @@
 #include "third_party/blink/renderer/platform/graphics/dark_mode_filter.h"
 
+#include "base/optional.h"
+#include "third_party/blink/renderer/platform/graphics/dark_mode_settings.h"
 #include "third_party/skia/include/core/SkColorFilter.h"
 #include "third_party/skia/include/effects/SkHighContrastFilter.h"
 #include "third_party/skia/include/effects/SkTableColorFilter.h"
 
 namespace blink {
 
-DarkModeFilter::DarkModeFilter() : default_filter_(nullptr) {
+namespace {
+
+bool ShouldApplyToImage(const DarkModeSettings& settings,
+                        const FloatRect& src_rect,
+                        Image* image) {
+  switch (settings.image_policy) {
+    case DarkModeImagePolicy::kFilterSmart:
+      return image->ShouldApplyDarkModeFilter(src_rect);
+    case DarkModeImagePolicy::kFilterAll:
+      return true;
+    default:
+      return false;
+  }
+}
+
+}  // namespace
+
+DarkModeFilter::DarkModeFilter()
+    : default_filter_(nullptr), image_filter_(nullptr) {
   settings_.mode = DarkMode::kOff;
   settings_.image_policy = DarkModeImagePolicy::kFilterNone;
 }
@@ -18,6 +38,7 @@
   switch (settings_.mode) {
     case DarkMode::kOff:
       default_filter_.reset(nullptr);
+      image_filter_.reset(nullptr);
       return;
     case DarkMode::kSimpleInvertForTesting: {
       uint8_t identity[256], invert[256];
@@ -27,6 +48,7 @@
       }
       default_filter_ =
           SkTableColorFilter::MakeARGB(identity, invert, invert, invert);
+      image_filter_.reset(nullptr);
       return;
     }
     case DarkMode::kInvertBrightness:
@@ -41,32 +63,49 @@
   config.fGrayscale = settings_.grayscale;
   config.fContrast = settings_.contrast;
   default_filter_ = SkHighContrastFilter::Make(config);
-}
 
-sk_sp<SkColorFilter> DarkModeFilter::GetColorFilter() {
-  return default_filter_;
-}
-
-bool DarkModeFilter::ShouldApplyToImage(Image& image,
-                                        const FloatRect& src_rect) {
-  if (!GetColorFilter())
-    return false;
-
-  switch (settings_.image_policy) {
-    case DarkModeImagePolicy::kFilterSmart:
-      return image.ShouldApplyDarkModeFilter(src_rect);
-    case DarkModeImagePolicy::kFilterAll:
-      return true;
-    default:
-      return false;
+  if (settings_.image_style == DarkModeImageStyle::kGrayscale) {
+    config.fGrayscale = true;
+    image_filter_ = SkHighContrastFilter::Make(config);
+  } else {
+    image_filter_.reset(nullptr);
   }
 }
 
-Color DarkModeFilter::Apply(const Color& color) {
-  sk_sp<SkColorFilter> filter = GetColorFilter();
-  if (!filter)
+Color DarkModeFilter::ApplyIfNeeded(const Color& color) {
+  if (!default_filter_)
     return color;
-  return Color(filter->filterColor(color.Rgb()));
+  return Color(default_filter_->filterColor(color.Rgb()));
+}
+
+// TODO(gilmanmh): Investigate making |image| a const reference. This code
+// relies on Image::ShouldApplyDarkModeFilter(), which is not const. If it could
+// be made const, then |image| could also be const.
+void DarkModeFilter::ApplyToImageFlagsIfNeeded(const FloatRect& src_rect,
+                                               Image* image,
+                                               cc::PaintFlags* flags) {
+  sk_sp<SkColorFilter> filter = image_filter_;
+  if (!filter)
+    filter = default_filter_;
+
+  if (!filter || !ShouldApplyToImage(settings(), src_rect, image))
+    return;
+  flags->setColorFilter(std::move(filter));
+}
+
+base::Optional<cc::PaintFlags> DarkModeFilter::ApplyToFlagsIfNeeded(
+    const cc::PaintFlags& flags) {
+  if (!default_filter_)
+    return base::nullopt;
+
+  cc::PaintFlags dark_mode_flags = flags;
+  if (flags.HasShader()) {
+    dark_mode_flags.setColorFilter(default_filter_);
+  } else {
+    dark_mode_flags.setColor(default_filter_->filterColor(flags.getColor()));
+  }
+
+  return base::make_optional<cc::PaintFlags>(std::move(dark_mode_flags));
 }
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/platform/graphics/dark_mode_filter.h b/third_party/blink/renderer/platform/graphics/dark_mode_filter.h
index 6052af1..3f8a1fae 100644
--- a/third_party/blink/renderer/platform/graphics/dark_mode_filter.h
+++ b/third_party/blink/renderer/platform/graphics/dark_mode_filter.h
@@ -1,6 +1,7 @@
 #ifndef THIRD_PARTY_BLINK_RENDERER_PLATFORM_GRAPHICS_DARK_MODE_FILTER_H_
 #define THIRD_PARTY_BLINK_RENDERER_PLATFORM_GRAPHICS_DARK_MODE_FILTER_H_
 
+#include "cc/paint/paint_flags.h"
 #include "third_party/blink/renderer/platform/geometry/float_rect.h"
 #include "third_party/blink/renderer/platform/graphics/color.h"
 #include "third_party/blink/renderer/platform/graphics/dark_mode_settings.h"
@@ -18,15 +19,22 @@
   const DarkModeSettings& settings() const { return settings_; }
   void UpdateSettings(const DarkModeSettings& new_settings);
 
-  sk_sp<SkColorFilter> GetColorFilter();
+  Color ApplyIfNeeded(const Color& color);
 
-  bool ShouldApplyToImage(Image& image, const FloatRect& src_rect);
+  // |image| and |flags| must not be null.
+  void ApplyToImageFlagsIfNeeded(const FloatRect& src_rect,
+                                 Image* image,
+                                 cc::PaintFlags* flags);
 
-  Color Apply(const Color& color);
+  // |flags| must not be null.
+  base::Optional<cc::PaintFlags> ApplyToFlagsIfNeeded(
+      const cc::PaintFlags& flags);
 
  private:
   DarkModeSettings settings_;
+
   sk_sp<SkColorFilter> default_filter_;
+  sk_sp<SkColorFilter> image_filter_;
 };
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/platform/graphics/dark_mode_settings.h b/third_party/blink/renderer/platform/graphics/dark_mode_settings.h
index 42f67d8..21d1d9e2 100644
--- a/third_party/blink/renderer/platform/graphics/dark_mode_settings.h
+++ b/third_party/blink/renderer/platform/graphics/dark_mode_settings.h
@@ -25,6 +25,14 @@
   kFilterSmart,
 };
 
+// For images that should have a filter applied, which filter should be used?
+enum class DarkModeImageStyle {
+  // Invert images the same way as other elements
+  kDefault,
+  // Apply grayscale to images as well as inverting them
+  kGrayscale
+};
+
 enum class DarkModePagePolicy {
   // Apply dark-mode filter to all frames, regardless of content.
   kFilterAll,
@@ -37,6 +45,7 @@
   bool grayscale = false;
   float contrast = 0.0;  // Valid range from -1.0 to 1.0
   DarkModeImagePolicy image_policy = DarkModeImagePolicy::kFilterAll;
+  DarkModeImageStyle image_style = DarkModeImageStyle::kDefault;
 };
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/platform/graphics/gpu/xr_webgl_drawing_buffer.cc b/third_party/blink/renderer/platform/graphics/gpu/xr_webgl_drawing_buffer.cc
index d866b641..3c14d62 100644
--- a/third_party/blink/renderer/platform/graphics/gpu/xr_webgl_drawing_buffer.cc
+++ b/third_party/blink/renderer/platform/graphics/gpu/xr_webgl_drawing_buffer.cc
@@ -267,42 +267,6 @@
   return IntSize(width, height);
 }
 
-void XRWebGLDrawingBuffer::OverwriteColorBufferFromMailboxTexture(
-    const gpu::MailboxHolder& mailbox_holder,
-    const IntSize& size_in) {
-  TRACE_EVENT0("gpu", __FUNCTION__);
-  gpu::gles2::GLES2Interface* gl = drawing_buffer_->ContextGL();
-
-  gl->WaitSyncTokenCHROMIUM(mailbox_holder.sync_token.GetConstData());
-
-  GLuint source_texture =
-      gl->CreateAndConsumeTextureCHROMIUM(mailbox_holder.mailbox.name);
-
-  GLuint dest_texture = back_color_buffer_->texture_id;
-
-  // TODO(836496): clean this up and move some of the math to call site.
-  int dest_width = size_.Width();
-  int dest_height = size_.Height();
-  int source_width = size_in.Width();
-  int source_height = size_in.Height();
-
-  int copy_width = std::min(source_width, dest_width);
-  int copy_height = std::min(source_height, dest_height);
-
-  // If the source is too small, center the image.
-  int dest_x0 = source_width < dest_width ? (dest_width - source_width) / 2 : 0;
-  int dest_y0 =
-      source_height < dest_height ? (dest_height - source_height) / 2 : 0;
-  int src_x0 = source_width > dest_width ? (source_width - dest_width) / 2 : 0;
-  int src_y0 =
-      source_height > dest_height ? (source_height - dest_height) / 2 : 0;
-
-  gl->CopySubTextureCHROMIUM(
-      source_texture, 0, GL_TEXTURE_2D, dest_texture, 0, dest_x0, dest_y0,
-      src_x0, src_y0, copy_width, copy_height, false /* flipY */,
-      false /* premultiplyAlpha */, false /* unmultiplyAlpha */);
-}
-
 void XRWebGLDrawingBuffer::UseSharedBuffer(
     const gpu::MailboxHolder& buffer_mailbox_holder) {
   DVLOG(3) << __FUNCTION__;
diff --git a/third_party/blink/renderer/platform/graphics/gpu/xr_webgl_drawing_buffer.h b/third_party/blink/renderer/platform/graphics/gpu/xr_webgl_drawing_buffer.h
index 157ce76..c723ce9 100644
--- a/third_party/blink/renderer/platform/graphics/gpu/xr_webgl_drawing_buffer.h
+++ b/third_party/blink/renderer/platform/graphics/gpu/xr_webgl_drawing_buffer.h
@@ -43,9 +43,6 @@
 
   void Resize(const IntSize&);
 
-  void OverwriteColorBufferFromMailboxTexture(const gpu::MailboxHolder&,
-                                              const IntSize& size);
-
   scoped_refptr<StaticBitmapImage> TransferToStaticBitmapImage(
       std::unique_ptr<viz::SingleReleaseCallback>* out_release_callback);
 
diff --git a/third_party/blink/renderer/platform/graphics/graphics_context.cc b/third_party/blink/renderer/platform/graphics/graphics_context.cc
index 340fbf38..efe0bd83 100644
--- a/third_party/blink/renderer/platform/graphics/graphics_context.cc
+++ b/third_party/blink/renderer/platform/graphics/graphics_context.cc
@@ -59,25 +59,23 @@
 
 namespace blink {
 
+// Effectively allows modifying the provided |flags| without technically
+// violating its constness.
+//
+// TODO(gilmanmh): Investigate removing const from |flags| in the calling
+// methods so that this isn't necessary.
 class GraphicsContext::DarkModeFlags final {
   STACK_ALLOCATED();
 
  public:
   // This helper's lifetime should never exceed |flags|'.
   DarkModeFlags(GraphicsContext* gc, const PaintFlags& flags) {
-    sk_sp<SkColorFilter> filter = gc->dark_mode_filter_.GetColorFilter();
-    if (!filter) {
-      flags_ = &flags;
-    } else {
-      dark_mode_flags_ = flags;
-      if (flags.HasShader()) {
-        dark_mode_flags_->setColorFilter(filter);
-      } else {
-        dark_mode_flags_->setColor(filter->filterColor(flags.getColor()));
-      }
-
+    dark_mode_flags_ = gc->dark_mode_filter_.ApplyToFlagsIfNeeded(flags);
+    if (dark_mode_flags_) {
       flags_ = &dark_mode_flags_.value();
+      return;
     }
+    flags_ = &flags;
   }
 
   operator const PaintFlags&() const { return *flags_; }
@@ -387,15 +385,15 @@
 void GraphicsContext::DrawFocusRingPath(const SkPath& path,
                                         const Color& color,
                                         float width) {
-  DrawPlatformFocusRing(path, canvas_, dark_mode_filter_.Apply(color).Rgb(),
-                        width);
+  DrawPlatformFocusRing(path, canvas_,
+                        dark_mode_filter_.ApplyIfNeeded(color).Rgb(), width);
 }
 
 void GraphicsContext::DrawFocusRingRect(const SkRect& rect,
                                         const Color& color,
                                         float width) {
-  DrawPlatformFocusRing(rect, canvas_, dark_mode_filter_.Apply(color).Rgb(),
-                        width);
+  DrawPlatformFocusRing(rect, canvas_,
+                        dark_mode_filter_.ApplyIfNeeded(color).Rgb(), width);
 }
 
 void GraphicsContext::DrawFocusRing(const Path& focus_ring_path,
@@ -470,7 +468,7 @@
   if (ContextDisabled())
     return;
 
-  Color shadow_color = dark_mode_filter_.Apply(orig_shadow_color);
+  Color shadow_color = dark_mode_filter_.ApplyIfNeeded(orig_shadow_color);
 
   FloatRect hole_rect(rect.Rect());
   hole_rect.Inflate(-shadow_spread);
@@ -873,9 +871,8 @@
   image_flags.setBlendMode(op);
   image_flags.setColor(SK_ColorBLACK);
   image_flags.setFilterQuality(ComputeFilterQuality(image, dest, src));
-  if (dark_mode_filter_.ShouldApplyToImage(*image, src)) {
-    image_flags.setColorFilter(dark_mode_filter_.GetColorFilter());
-  }
+
+  dark_mode_filter_.ApplyToImageFlagsIfNeeded(src, image, &image_flags);
 
   image->Draw(canvas_, image_flags, dest, src, should_respect_image_orientation,
               Image::kClampImageToSourceRect, decode_mode);
@@ -910,9 +907,8 @@
   image_flags.setColor(SK_ColorBLACK);
   image_flags.setFilterQuality(
       ComputeFilterQuality(image, dest.Rect(), src_rect));
-  if (dark_mode_filter_.ShouldApplyToImage(*image, src_rect)) {
-    image_flags.setColorFilter(dark_mode_filter_.GetColorFilter());
-  }
+
+  dark_mode_filter_.ApplyToImageFlagsIfNeeded(src_rect, image, &image_flags);
 
   bool use_shader = (visible_src == src_rect) &&
                     (respect_orientation == kDoNotRespectImageOrientation);
@@ -1122,7 +1118,7 @@
       canvas_->drawDRRect(outer, inner, ImmutableState()->FillFlags());
     } else {
       PaintFlags flags(ImmutableState()->FillFlags());
-      flags.setColor(dark_mode_filter_.Apply(color).Rgb());
+      flags.setColor(dark_mode_filter_.ApplyIfNeeded(color).Rgb());
       canvas_->drawDRRect(outer, inner, flags);
     }
 
@@ -1135,7 +1131,7 @@
   stroke_r_rect.inset(stroke_width / 2, stroke_width / 2);
 
   PaintFlags stroke_flags(ImmutableState()->FillFlags());
-  stroke_flags.setColor(dark_mode_filter_.Apply(color).Rgb());
+  stroke_flags.setColor(dark_mode_filter_.ApplyIfNeeded(color).Rgb());
   stroke_flags.setStyle(PaintFlags::kStroke_Style);
   stroke_flags.setStrokeWidth(stroke_width);
 
@@ -1330,7 +1326,7 @@
     return;
 
   PaintFlags flags(ImmutableState()->FillFlags());
-  flags.setColor(dark_mode_filter_.Apply(color).Rgb());
+  flags.setColor(dark_mode_filter_.ApplyIfNeeded(color).Rgb());
   canvas_->drawDRRect(SkRRect::MakeRect(rect), rounded_hole_rect, flags);
 }
 
diff --git a/third_party/blink/renderer/platform/heap/heap_allocator.cc b/third_party/blink/renderer/platform/heap/heap_allocator.cc
index 411f19a..748fe6ec 100644
--- a/third_party/blink/renderer/platform/heap/heap_allocator.cc
+++ b/third_party/blink/renderer/platform/heap/heap_allocator.cc
@@ -106,6 +106,13 @@
     return false;
 
   HeapObjectHeader* header = HeapObjectHeader::FromPayload(address);
+
+  // Compaction may register slots for compaction in slots of vector backings.
+  // E.g., when vectors are embedded in each other. To avoid dereferincing a
+  // broken slot, bail out on already marked backings.
+  if (header->IsMarked())
+    return false;
+
   NormalPageArena* arena = static_cast<NormalPage*>(page)->ArenaForNormalPage();
   // We shrink the object only if the shrinking will make a non-small
   // prompt-free block.
diff --git a/third_party/blink/renderer/platform/text/character.cc b/third_party/blink/renderer/platform/text/character.cc
index f5877135..381135c 100644
--- a/third_party/blink/renderer/platform/text/character.cc
+++ b/third_party/blink/renderer/platform/text/character.cc
@@ -124,19 +124,19 @@
   RETURN_HAS_PROPERTY(character, kIsHangul);
 }
 
-unsigned Character::ExpansionOpportunityCount(const LChar* characters,
-                                              unsigned length,
-                                              TextDirection direction,
-                                              bool& is_after_expansion,
-                                              const TextJustify text_justify) {
-  unsigned count = 0;
+unsigned Character::ExpansionOpportunityCount(
+    base::span<const LChar> characters,
+    TextDirection direction,
+    bool& is_after_expansion,
+    const TextJustify text_justify) {
   if (text_justify == TextJustify::kDistribute) {
     is_after_expansion = true;
-    return length;
+    return characters.size();
   }
 
+  unsigned count = 0;
   if (direction == TextDirection::kLtr) {
-    for (unsigned i = 0; i < length; ++i) {
+    for (unsigned i = 0; i < characters.size(); ++i) {
       if (TreatAsSpace(characters[i])) {
         count++;
         is_after_expansion = true;
@@ -145,7 +145,7 @@
       }
     }
   } else {
-    for (unsigned i = length; i > 0; --i) {
+    for (unsigned i = characters.size(); i > 0; --i) {
       if (TreatAsSpace(characters[i - 1])) {
         count++;
         is_after_expansion = true;
@@ -158,21 +158,21 @@
   return count;
 }
 
-unsigned Character::ExpansionOpportunityCount(const UChar* characters,
-                                              unsigned length,
-                                              TextDirection direction,
-                                              bool& is_after_expansion,
-                                              const TextJustify text_justify) {
+unsigned Character::ExpansionOpportunityCount(
+    base::span<const UChar> characters,
+    TextDirection direction,
+    bool& is_after_expansion,
+    const TextJustify text_justify) {
   unsigned count = 0;
   if (direction == TextDirection::kLtr) {
-    for (unsigned i = 0; i < length; ++i) {
+    for (unsigned i = 0; i < characters.size(); ++i) {
       UChar32 character = characters[i];
       if (TreatAsSpace(character)) {
         count++;
         is_after_expansion = true;
         continue;
       }
-      if (U16_IS_LEAD(character) && i + 1 < length &&
+      if (U16_IS_LEAD(character) && i + 1 < characters.size() &&
           U16_IS_TRAIL(characters[i + 1])) {
         character = U16_GET_SUPPLEMENTARY(character, characters[i + 1]);
         i++;
@@ -188,7 +188,7 @@
       is_after_expansion = false;
     }
   } else {
-    for (unsigned i = length; i > 0; --i) {
+    for (unsigned i = characters.size(); i > 0; --i) {
       UChar32 character = characters[i - 1];
       if (TreatAsSpace(character)) {
         count++;
diff --git a/third_party/blink/renderer/platform/text/character.h b/third_party/blink/renderer/platform/text/character.h
index 5818db4..60ef74c 100644
--- a/third_party/blink/renderer/platform/text/character.h
+++ b/third_party/blink/renderer/platform/text/character.h
@@ -31,6 +31,7 @@
 #ifndef THIRD_PARTY_BLINK_RENDERER_PLATFORM_TEXT_CHARACTER_H_
 #define THIRD_PARTY_BLINK_RENDERER_PLATFORM_TEXT_CHARACTER_H_
 
+#include "base/containers/span.h"
 #include "third_party/blink/renderer/platform/platform_export.h"
 #include "third_party/blink/renderer/platform/text/character_property.h"
 #include "third_party/blink/renderer/platform/text/text_direction.h"
@@ -77,25 +78,22 @@
     return c < 0x1100 ? false : IsHangulSlow(c);
   }
 
-  static unsigned ExpansionOpportunityCount(const LChar*,
-                                            unsigned length,
+  static unsigned ExpansionOpportunityCount(base::span<const LChar>,
                                             TextDirection,
                                             bool& is_after_expansion,
                                             const TextJustify);
-  static unsigned ExpansionOpportunityCount(const UChar*,
-                                            unsigned length,
+  static unsigned ExpansionOpportunityCount(base::span<const UChar>,
                                             TextDirection,
                                             bool& is_after_expansion,
                                             const TextJustify);
   static unsigned ExpansionOpportunityCount(const TextRun& run,
                                             bool& is_after_expansion) {
     if (run.Is8Bit())
-      return ExpansionOpportunityCount(run.Characters8(), run.length(),
-                                       run.Direction(), is_after_expansion,
+      return ExpansionOpportunityCount(run.Span8(), run.Direction(),
+                                       is_after_expansion,
                                        run.GetTextJustify());
-    return ExpansionOpportunityCount(run.Characters16(), run.length(),
-                                     run.Direction(), is_after_expansion,
-                                     run.GetTextJustify());
+    return ExpansionOpportunityCount(run.Span16(), run.Direction(),
+                                     is_after_expansion, run.GetTextJustify());
   }
 
   static bool IsUprightInMixedVertical(UChar32 character);
diff --git a/third_party/blink/renderer/platform/text/text_run.h b/third_party/blink/renderer/platform/text/text_run.h
index c39f194..cb8d949 100644
--- a/third_party/blink/renderer/platform/text/text_run.h
+++ b/third_party/blink/renderer/platform/text/text_run.h
@@ -26,6 +26,7 @@
 
 #include <unicode/utf16.h>
 
+#include "base/containers/span.h"
 #include "base/optional.h"
 #include "third_party/blink/renderer/platform/heap/heap.h"
 #include "third_party/blink/renderer/platform/platform_export.h"
@@ -163,6 +164,16 @@
     return &data_.characters16[i];
   }
 
+  // Prefer Span8() and Span16() to Characters8() and Characters16().
+  base::span<const LChar> Span8() const {
+    DCHECK(Is8Bit());
+    return {data_.characters8, len_};
+  }
+  base::span<const UChar> Span16() const {
+    DCHECK(!Is8Bit());
+    return {data_.characters16, len_};
+  }
+
   const LChar* Characters8() const {
     DCHECK(Is8Bit());
     return data_.characters8;
diff --git a/third_party/blink/web_tests/TestExpectations b/third_party/blink/web_tests/TestExpectations
index 096e243..e4060b9 100644
--- a/third_party/blink/web_tests/TestExpectations
+++ b/third_party/blink/web_tests/TestExpectations
@@ -6332,3 +6332,5 @@
 
 # Sheriff 2019-05-01
 crbug.com/958347 [ Linux ] external/wpt/editing/run/removeformat.html [ Pass Crash ]
+crbug.com/941931 [ Linux Win ] virtual/outofblink-cors/http/tests/security/contentSecurityPolicy/1.1/plugintypes-affects-cross-site-child-disallowed.html [ Pass Failure ]
+crbug.com/958426 [ Mac10.13 ] fast/text/line-break-ascii.html [ Timeout Pass ]
diff --git a/third_party/blink/web_tests/VirtualTestSuites b/third_party/blink/web_tests/VirtualTestSuites
index 21d9b5f..26ef64b 100644
--- a/third_party/blink/web_tests/VirtualTestSuites
+++ b/third_party/blink/web_tests/VirtualTestSuites
@@ -449,6 +449,11 @@
   },
   {
     "prefix": "dark-mode",
+    "base": "paint/dark-mode/grayscale-images",
+    "args": ["--blink-settings=darkMode=3,darkModeImagePolicy=0,darkModeImageStyle=1"]
+  },
+  {
+    "prefix": "dark-mode",
     "base": "paint/dark-mode/image-filter-all",
     "args": ["--blink-settings=darkMode=3,darkModeImagePolicy=0"]
   },
diff --git a/third_party/blink/web_tests/WebDriverExpectations b/third_party/blink/web_tests/WebDriverExpectations
index 11785794..eab9a49 100644
--- a/third_party/blink/web_tests/WebDriverExpectations
+++ b/third_party/blink/web_tests/WebDriverExpectations
@@ -8,13 +8,11 @@
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/take_screenshot/user_prompts.py>>test_dismiss[capabilities0-confirm] [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/execute_script/promise.py>>test_promise_all_resolve [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/new_window/user_prompts.py>>test_default[prompt-None] [ Failure ]
-crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/element_click/scroll_into_view.py>>test_partially_visible_does_not_scroll[1] [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/permissions/set.py>>test_non_secure_context[denied] [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/permissions/set.py>>test_invalid_parameters[parameters2] [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/element_send_keys/interactability.py>>test_document_element_is_interactable [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/fullscreen_window/stress.py>>test_stress[4] [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/element_click/bubbling.py>>test_spin_event_loop [ Failure ]
-crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/element_click/scroll_into_view.py>>test_partially_visible_does_not_scroll[6] [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/take_screenshot/user_prompts.py>>test_ignore[capabilities0-confirm] [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/execute_script/promise.py>>test_promise_reject [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/permissions/set.py>>test_set_to_state[realmSetting2-denied] [ Failure ]
@@ -64,14 +62,12 @@
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/take_screenshot/user_prompts.py>>test_accept_and_notify[capabilities0-alert] [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/perform_actions/key_special_keys.py>>test_codepoint_keys_behave_correctly[\u1100\u1161\u11a8] [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/status/status.py>>test_status_with_session_running_on_endpoint_node [ Failure ]
-crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/element_click/scroll_into_view.py>>test_partially_visible_does_not_scroll[9] [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/element_send_keys/scroll_into_view.py>>test_option_select_container_outside_of_scrollable_viewport [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/take_screenshot/user_prompts.py>>test_default[prompt] [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/take_element_screenshot/user_prompts.py>>test_dismiss[capabilities0-alert] [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/permissions/set.py>>test_set_to_state[realmSetting0-denied] [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/fullscreen_window/user_prompts.py>>test_dismiss[capabilities0-alert-None] [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/maximize_window/maximize.py>>test_maximize_when_resized_to_max_size [ Failure ]
-crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/element_click/scroll_into_view.py>>test_partially_visible_does_not_scroll[3] [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/execute_script/json_serialize_windowproxy.py>>test_window_open [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/new_window/user_prompts.py>>test_dismiss_and_notify[capabilities0-alert-None] [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/new_window/new.py>>test_no_browsing_context [ Failure ]
@@ -85,11 +81,9 @@
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/execute_script/promise.py>>test_promise_resolve_timeout [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/new_window/user_prompts.py>>test_ignore[capabilities0-confirm] [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/take_screenshot/user_prompts.py>>test_dismiss_and_notify[capabilities0-prompt] [ Failure ]
-crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/element_click/scroll_into_view.py>>test_partially_visible_does_not_scroll[8] [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/perform_actions/key_events.py>>test_modifier_key_sends_correct_events[\ue03d-META] [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/take_element_screenshot/user_prompts.py>>test_dismiss_and_notify[capabilities0-prompt] [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/take_screenshot/user_prompts.py>>test_accept[capabilities0-prompt] [ Failure ]
-crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/element_click/scroll_into_view.py>>test_partially_visible_does_not_scroll[2] [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/new_window/new_window.py>>test_new_window_opens_about_blank [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/new_session/merge.py>>test_merge_browserName [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/new_session/default_values.py>>test_no_capabilites [ Failure ]
@@ -117,7 +111,6 @@
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/execute_script/promise.py>>test_await_promise_reject [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/take_element_screenshot/user_prompts.py>>test_default[confirm] [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/fullscreen_window/stress.py>>test_stress[3] [ Failure ]
-crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/element_click/scroll_into_view.py>>test_partially_visible_does_not_scroll[5] [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/new_window/new.py>>test_type_with_invalid_type[type_hint3] [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/new_window/user_prompts.py>>test_ignore[capabilities0-prompt] [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/new_window/user_prompts.py>>test_dismiss[capabilities0-confirm-False] [ Failure ]
@@ -134,7 +127,6 @@
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/perform_actions/pointer_contextmenu.py>>test_control_click[\ue009-ctrlKey] [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/new_window/new.py>>test_type_with_null_value [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/take_element_screenshot/user_prompts.py>>test_dismiss[capabilities0-confirm] [ Failure ]
-crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/element_click/scroll_into_view.py>>test_partially_visible_does_not_scroll[4] [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/perform_actions/pointer_contextmenu.py>>test_release_control_click [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/execute_script/promise.py>>test_promise_resolve_delayed [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/take_element_screenshot/user_prompts.py>>test_accept[capabilities0-alert] [ Failure ]
@@ -159,7 +151,6 @@
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/execute_script/json_serialize_windowproxy.py>>test_initial_window [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/element_clear/clear.py>>test_contenteditable [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/permissions/set.py>>test_invalid_parameters[parameters3] [ Failure ]
-crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/element_click/scroll_into_view.py>>test_partially_visible_does_not_scroll[7] [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/new_window/user_prompts.py>>test_accept_and_notify[capabilities0-confirm-True] [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/new_window/new_tab.py>>test_new_tab_opens_about_blank [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/new_session/default_values.py>>test_valid_but_unmatchable_key [ Failure ]
@@ -174,4 +165,4 @@
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/new_window/new_window.py>>test_new_window_sets_no_opener [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/new_window/user_prompts.py>>test_dismiss_and_notify[capabilities0-prompt-None] [ Failure ]
 crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/permissions/set.py>>test_set_to_state[realmSetting2-prompt] [ Failure ]
-crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/permissions/set.py>>test_set_to_state_cross_realm[realmSetting0-prompt] [ Failure ]
\ No newline at end of file
+crbug.com/626703 [ Linux ] external/wpt/webdriver/tests/permissions/set.py>>test_set_to_state_cross_realm[realmSetting0-prompt] [ Failure ]
diff --git a/third_party/blink/web_tests/external/WPT_BASE_MANIFEST_6.json b/third_party/blink/web_tests/external/WPT_BASE_MANIFEST_6.json
index 04d89a81..29f1c41 100644
--- a/third_party/blink/web_tests/external/WPT_BASE_MANIFEST_6.json
+++ b/third_party/blink/web_tests/external/WPT_BASE_MANIFEST_6.json
@@ -459570,7 +459570,7 @@
    "support"
   ],
   "interfaces/IndexedDB.idl": [
-   "21ff252fffe57f1c49649790b7be8d59083aef86",
+   "f2625ebe53ddde37b5625802b7a417220e7cafbd",
    "support"
   ],
   "interfaces/InputDeviceCapabilities.idl": [
@@ -460038,7 +460038,7 @@
    "support"
   ],
   "interfaces/wake-lock.idl": [
-   "c0d4bc9c239aae45d48924ef79c6fe354bbe7204",
+   "466d697cff81c37465c1f7ed73d40a93301832a2",
    "support"
   ],
   "interfaces/wasm-js-api.idl": [
@@ -498642,7 +498642,7 @@
    "support"
   ],
   "wake-lock/idlharness.https.any-expected.txt": [
-   "0f847f0ba3997d884cbdd8cb06fda6b3bbd1e47d",
+   "2dc5105bb38d7f0244841e1cfcc05ea022b382a7",
    "support"
   ],
   "wake-lock/idlharness.https.any.js": [
@@ -498650,7 +498650,7 @@
    "testharness"
   ],
   "wake-lock/idlharness.https.any.worker-expected.txt": [
-   "0d9d4a9e4c28a28e406dfb29ad2b3731b739ed18",
+   "86359c7da0dbe79366836d82d30c0dff279b87f8",
    "support"
   ],
   "wake-lock/wakelock-applicability-manual.https-expected.txt": [
diff --git a/third_party/blink/web_tests/external/wpt/element-timing/background-image-data-uri.html b/third_party/blink/web_tests/external/wpt/element-timing/background-image-data-uri.html
index 696f34ff..16d6dfcb 100644
--- a/third_party/blink/web_tests/external/wpt/element-timing/background-image-data-uri.html
+++ b/third_party/blink/web_tests/external/wpt/element-timing/background-image-data-uri.html
@@ -25,7 +25,7 @@
         // Only the first characters of the data URI are included in the entry.
         const uriPrefix = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKAQMAAAC3/F3+AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAA';
         checkElementWithoutResourceTiming(entry, uriPrefix, 'my_div', 'target',
-            beforeRender);
+            beforeRender, document.getElementById('target'));
         // The background image is a red square of length 10.
         checkRect(entry, [0, 100, 0, 50]);
         checkNaturalSize(entry, 10, 10);
diff --git a/third_party/blink/web_tests/external/wpt/element-timing/background-image-multiple-elements.html b/third_party/blink/web_tests/external/wpt/element-timing/background-image-multiple-elements.html
index 669f94d..22b4158 100644
--- a/third_party/blink/web_tests/external/wpt/element-timing/background-image-multiple-elements.html
+++ b/third_party/blink/web_tests/external/wpt/element-timing/background-image-multiple-elements.html
@@ -36,14 +36,16 @@
           numObservedElements++;
           if (entry.id == 'div1') {
             observedDiv1 = true;
-            checkElement(entry, pathname, 'et1', 'div1', beforeRender);
+            checkElement(entry, pathname, 'et1', 'div1', beforeRender,
+                document.getElementById('div1'));
             // Div is in the top left corner.
             checkRect(entry, [0, 100, 0, 100]);
             checkNaturalSize(entry, 100, 100);
           }
           else if (entry.id == 'div2') {
             observedDiv2 = true;
-            checkElement(entry, pathname, 'et2', 'div2', beforeRender);
+            checkElement(entry, pathname, 'et2', 'div2', beforeRender,
+                document.getElementById('div2'));
             // Div is below div1, on the left.
             checkRect(entry, [0, 200, 100, 200]);
             checkNaturalSize(entry, 100, 100);
diff --git a/third_party/blink/web_tests/external/wpt/element-timing/background-image-stretched.html b/third_party/blink/web_tests/external/wpt/element-timing/background-image-stretched.html
index 8f93b43..28c355398 100644
--- a/third_party/blink/web_tests/external/wpt/element-timing/background-image-stretched.html
+++ b/third_party/blink/web_tests/external/wpt/element-timing/background-image-stretched.html
@@ -25,7 +25,8 @@
         const index = window.location.href.lastIndexOf('/');
         const pathname = window.location.href.substring(0, index) +
             '/resources/square100.png';
-        checkElement(entry, pathname, 'my_div', 'target', beforeRender);
+        checkElement(entry, pathname, 'my_div', 'target', beforeRender,
+            document.getElementById('target'));
         // The background image extends to occupy to full size of the div.
         checkRect(entry, [0, 200, 0, 150]);
         // The natural size of the square remains unchanged.
diff --git a/third_party/blink/web_tests/external/wpt/element-timing/buffer-before-onload.html b/third_party/blink/web_tests/external/wpt/element-timing/buffer-before-onload.html
index 805777f2..03c7048 100644
--- a/third_party/blink/web_tests/external/wpt/element-timing/buffer-before-onload.html
+++ b/third_party/blink/web_tests/external/wpt/element-timing/buffer-before-onload.html
@@ -28,7 +28,7 @@
       const index = window.location.href.lastIndexOf('/');
       const pathname = window.location.href.substring(0, index) +
           '/resources/square20.jpg';
-      checkElement(entry, pathname, 'my_image', 'my_id', beforeRender);
+      checkElement(entry, pathname, 'my_image', 'my_id', beforeRender, img);
       checkNaturalSize(entry, 20, 20);
     });
   }, "Element Timing: image loads before onload.");
diff --git a/third_party/blink/web_tests/external/wpt/element-timing/cross-origin-element.sub.html b/third_party/blink/web_tests/external/wpt/element-timing/cross-origin-element.sub.html
index b1a5b7c..0af0ae96 100644
--- a/third_party/blink/web_tests/external/wpt/element-timing/cross-origin-element.sub.html
+++ b/third_party/blink/web_tests/external/wpt/element-timing/cross-origin-element.sub.html
@@ -12,13 +12,14 @@
 <script src="resources/element-timing-helpers.js"></script>
 <script>
   async_test((t) => {
+    let img;
     const pathname = 'http://{{domains[www]}}:{{ports[http][1]}}'
           + '/element-timing/resources/square100.png';
     const observer = new PerformanceObserver(
       t.step_func_done((entryList) => {
         assert_equals(entryList.getEntries().length, 1);
         const entry = entryList.getEntries()[0];
-        checkElement(entry, pathname, 'my_image', 'the_id', 0);
+        checkElement(entry, pathname, 'my_image', 'the_id', 0, img);
         assert_equals(entry.startTime, 0,
           'The startTime of a cross-origin image should be 0.');
         checkRect(entry, [0, 100, 0, 100]);
@@ -31,7 +32,7 @@
     // TODO(npm): change observer to use buffered flag.
     window.onload = t.step_func(() => {
       // Add a cross origin image resource.
-      const img = document.createElement('img');
+      img = document.createElement('img');
       img.src = pathname;
       img.setAttribute('elementtiming', 'my_image');
       img.setAttribute('id', 'the_id');
diff --git a/third_party/blink/web_tests/external/wpt/element-timing/disconnect-image.html b/third_party/blink/web_tests/external/wpt/element-timing/disconnect-image.html
new file mode 100644
index 0000000..4ee0516
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/element-timing/disconnect-image.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>Element Timing: element attribute returns null when element is disconnected</title>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/element-timing-helpers.js"></script>
+<script>
+  let beforeRender;
+  let img;
+  async_test(function (t) {
+    const observer = new PerformanceObserver(
+      t.step_func_done(function(entryList) {
+        assert_equals(entryList.getEntries().length, 1);
+        const entry = entryList.getEntries()[0];
+        const index = window.location.href.lastIndexOf('/');
+        const pathname = window.location.href.substring(0, index) +
+            '/resources/square100.png';
+        // This method will check that entry.element is |img|.
+        checkElement(entry, pathname, 'my_image', 'my_id', beforeRender, img);
+
+        img.parentNode.removeChild(img);
+        // After removing image, entry.element should return null.
+        assert_equals(entry.element, null);
+      })
+    );
+    observer.observe({entryTypes: ['element']});
+    // We add the image during onload to be sure that the observer is registered
+    // in time for it to observe the element timing.
+    window.onload = () => {
+      // Add image of width equal to 100 and height equal to 100.
+      img = document.createElement('img');
+      img.src = 'resources/square100.png';
+      img.setAttribute('elementtiming', 'my_image');
+      img.setAttribute('id', 'my_id');
+      document.body.appendChild(img);
+      beforeRender = performance.now();
+    };
+  }, 'Disconnected elements have null as their |element| attribute.');
+</script>
+
+</body>
diff --git a/third_party/blink/web_tests/external/wpt/element-timing/image-TAO-wildcard.sub.html b/third_party/blink/web_tests/external/wpt/element-timing/image-TAO-wildcard.sub.html
index 0e24af06..4ec8aa7d 100644
--- a/third_party/blink/web_tests/external/wpt/element-timing/image-TAO-wildcard.sub.html
+++ b/third_party/blink/web_tests/external/wpt/element-timing/image-TAO-wildcard.sub.html
@@ -13,13 +13,14 @@
 <script>
   async_test((t) => {
     let beforeRender;
+    let img;
     const img_src = 'http://{{domains[www]}}:{{ports[http][1]}}/element-timing/'
         + 'resources/TAOImage.py?tao=wildcard';
     const observer = new PerformanceObserver(
       t.step_func_done((entryList) => {
         assert_equals(entryList.getEntries().length, 1);
         const entry = entryList.getEntries()[0];
-        checkElement(entry, img_src, 'my_image', 'my_id', beforeRender);
+        checkElement(entry, img_src, 'my_image', 'my_id', beforeRender, img);
         // Assume viewport has size at least 20, so the element is fully visible.
         checkRect(entry, [0, 20, 0, 20]);
         checkNaturalSize(entry, 20, 20);
@@ -30,7 +31,7 @@
     // in time for it to observe the element timing.
     // TODO(npm): change observer to use buffered flag.
     window.onload = t.step_func(() => {
-      const img = document.createElement('img');
+      img = document.createElement('img');
       img.src = img_src;
       img.setAttribute('elementtiming', 'my_image');
       img.setAttribute('id', 'my_id');
diff --git a/third_party/blink/web_tests/external/wpt/element-timing/image-carousel.html b/third_party/blink/web_tests/external/wpt/element-timing/image-carousel.html
index 9f0ef79e..404eca3 100644
--- a/third_party/blink/web_tests/external/wpt/element-timing/image-carousel.html
+++ b/third_party/blink/web_tests/external/wpt/element-timing/image-carousel.html
@@ -37,13 +37,15 @@
     const observer = new PerformanceObserver(list => {
       list.getEntries().forEach(entry => {
         if (entry_count % 2 == 0) {
-          checkElement(entry, pathname0, 'image0', 'image0', beforeRenderTimes[entry_count]);
+          checkElement(entry, pathname0, 'image0', 'image0', beforeRenderTimes[entry_count],
+              document.getElementById('image0'));
           checkRect(entry, [0, 200, 0, 200]);
           checkNaturalSize(entry, 200, 200);
           entry_count_per_element[0]++;
         }
         else {
-          checkElement(entry, pathname1, 'image1', 'image1', beforeRenderTimes[entry_count]);
+          checkElement(entry, pathname1, 'image1', 'image1', beforeRenderTimes[entry_count],
+              document.getElementById('image1'));
           checkRect(entry, [0, 100, 0, 100]);
           checkNaturalSize(entry, 100, 100);
           entry_count_per_element[1]++;
diff --git a/third_party/blink/web_tests/external/wpt/element-timing/image-clipped-svg.html b/third_party/blink/web_tests/external/wpt/element-timing/image-clipped-svg.html
index 36cf1b1..3007bf7 100644
--- a/third_party/blink/web_tests/external/wpt/element-timing/image-clipped-svg.html
+++ b/third_party/blink/web_tests/external/wpt/element-timing/image-clipped-svg.html
@@ -14,7 +14,8 @@
       const index = window.location.href.lastIndexOf('/');
       const pathname = window.location.href.substring(0, index) +
           '/resources/circle.svg';
-      checkElement(entry, pathname, 'my_svg', 'SVG', beforeRender);
+      checkElement(entry, pathname, 'my_svg', 'SVG', beforeRender,
+          document.getElementById('SVG'));
       // Image size is 200x200 but SVG size is 100x100 so it is clipped.
       checkRect(entry, [0, 100, 0, 100]);
       checkNaturalSize(entry, 200, 200);
diff --git a/third_party/blink/web_tests/external/wpt/element-timing/image-data-uri.html b/third_party/blink/web_tests/external/wpt/element-timing/image-data-uri.html
index 22ff911..2b5d04e45 100644
--- a/third_party/blink/web_tests/external/wpt/element-timing/image-data-uri.html
+++ b/third_party/blink/web_tests/external/wpt/element-timing/image-data-uri.html
@@ -23,7 +23,7 @@
         // Only the first characters of the data URI are included in the entry.
         const uriPrefix = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKAQMAAAC3/F3+AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAA';
         checkElementWithoutResourceTiming(entry, uriPrefix, 'my_img', 'inline_wee',
-            beforeRender);
+            beforeRender, document.getElementById('inline_wee'));
         // The image is a red square of length 10.
         checkRect(entry, [0, 10, 0, 10]);
         checkNaturalSize(entry, 10, 10);
diff --git a/third_party/blink/web_tests/external/wpt/element-timing/image-not-fully-visible.html b/third_party/blink/web_tests/external/wpt/element-timing/image-not-fully-visible.html
index 279fa03..5716249 100644
--- a/third_party/blink/web_tests/external/wpt/element-timing/image-not-fully-visible.html
+++ b/third_party/blink/web_tests/external/wpt/element-timing/image-not-fully-visible.html
@@ -12,6 +12,7 @@
 <script src="resources/element-timing-helpers.js"></script>
 <script>
   let beforeRender;
+  let img;
   async_test(function (t) {
     const observer = new PerformanceObserver(
       t.step_func_done(function(entryList) {
@@ -20,7 +21,7 @@
         const index = window.location.href.lastIndexOf('/');
         const pathname = window.location.href.substring(0, index) +
             '/resources/square20.png';
-        checkElement(entry, pathname, 'not_fully_visible', '', beforeRender);
+        checkElement(entry, pathname, 'not_fully_visible', '', beforeRender, img);
         // Image will not be fully visible. It should start from the top left part
         // of the document, excluding the margin, and then overflow.
         checkRect(entry,
@@ -33,7 +34,7 @@
     // in time for it to observe the element timing.
     window.onload = () => {
       // Add an image setting width and height equal to viewport.
-      const img = document.createElement('img');
+      img = document.createElement('img');
       img.src = 'resources/square20.png';
       img.setAttribute('elementtiming', 'not_fully_visible');
       img.width = document.documentElement.clientWidth;
diff --git a/third_party/blink/web_tests/external/wpt/element-timing/image-rect-iframe.html b/third_party/blink/web_tests/external/wpt/element-timing/image-rect-iframe.html
index 94c872e..f051130 100644
--- a/third_party/blink/web_tests/external/wpt/element-timing/image-rect-iframe.html
+++ b/third_party/blink/web_tests/external/wpt/element-timing/image-rect-iframe.html
@@ -23,6 +23,7 @@
       assert_equals(e.data.naturalWidth, 100);
       assert_equals(e.data.naturalHeight, 100);
       assert_equals(e.data.id, 'iframe_img_id');
+      assert_equals(e.data.elementId, 'iframe_img_id');
       t.done();
     });
   }, 'Element Timing entry in iframe has coordinates relative to the iframe.');
diff --git a/third_party/blink/web_tests/external/wpt/element-timing/image-with-css-scale.html b/third_party/blink/web_tests/external/wpt/element-timing/image-with-css-scale.html
index 6d77429e..bdffdb26 100644
--- a/third_party/blink/web_tests/external/wpt/element-timing/image-with-css-scale.html
+++ b/third_party/blink/web_tests/external/wpt/element-timing/image-with-css-scale.html
@@ -28,7 +28,8 @@
         const index = window.location.href.lastIndexOf('/');
         const pathname = window.location.href.substring(0, index - 14) +
             'images/black-rectangle.png';
-        checkElement(entry, pathname, 'rectangle', 'rect_id', beforeRender);
+        checkElement(entry, pathname, 'rectangle', 'rect_id', beforeRender,
+            document.getElementById('rect_id'));
         checkRect(entry, [0, 200, 25, 125]);
         checkNaturalSize(entry, 100, 50);
       })
diff --git a/third_party/blink/web_tests/external/wpt/element-timing/image-with-rotation.html b/third_party/blink/web_tests/external/wpt/element-timing/image-with-rotation.html
index 70b635e..4433ecb 100644
--- a/third_party/blink/web_tests/external/wpt/element-timing/image-with-rotation.html
+++ b/third_party/blink/web_tests/external/wpt/element-timing/image-with-rotation.html
@@ -28,7 +28,8 @@
         const index = window.location.href.lastIndexOf('/');
         const pathname = window.location.href.substring(0, index - 14) +
             'images/black-rectangle.png';
-        checkElement(entry, pathname, 'rectangle', 'rect_id', beforeRender);
+        checkElement(entry, pathname, 'rectangle', 'rect_id', beforeRender,
+            document.getElementById('rect_id'));
         checkNaturalSize(entry, 100, 50);
         const rect = entry.intersectionRect;
         // The div rotates with respect to the origin, so part of it will be invisible.
diff --git a/third_party/blink/web_tests/external/wpt/element-timing/images-repeated-resource.html b/third_party/blink/web_tests/external/wpt/element-timing/images-repeated-resource.html
index dbcad24..fbb2d6a1 100644
--- a/third_party/blink/web_tests/external/wpt/element-timing/images-repeated-resource.html
+++ b/third_party/blink/web_tests/external/wpt/element-timing/images-repeated-resource.html
@@ -15,6 +15,8 @@
   let numEntries = 0;
   let responseEnd1;
   let responseEnd2;
+  let img;
+  let img2;
   const index = window.location.href.lastIndexOf('/');
   const pathname = window.location.href.substring(0, index) +
       '/resources/square100.png';
@@ -22,15 +24,18 @@
     const observer = new PerformanceObserver(
       t.step_func(function(entryList) {
         entryList.getEntries().forEach(entry => {
-          checkElement(entry, pathname, entry.identifier, 'image_id', beforeRender);
+          // Easier to check the |element| attribute here since element ID is the same for both images.
+          checkElement(entry, pathname, entry.identifier, 'image_id', beforeRender, null);
           checkNaturalSize(entry, 100, 100);
           if (entry.identifier === 'my_image') {
             ++numEntries;
             responseEnd1 = entry.responseEnd;
+            assert_equals(entry.element, img);
           }
           else if (entry.identifier === 'my_image2') {
             ++numEntries;
             responseEnd2 = entry.responseEnd;
+            assert_equals(entry.element, img2);
           }
         });
         if (numEntries == 2) {
@@ -44,13 +49,13 @@
     // in time for it to observe the element timing.
     window.onload = () => {
       // Add image of width and height equal to 100.
-      const img = document.createElement('img');
+      img = document.createElement('img');
       img.src = 'resources/square100.png';
       img.setAttribute('elementtiming', 'my_image');
       img.setAttribute('id', 'image_id');
       document.body.appendChild(img);
 
-      const img2 = document.createElement('img');
+      img2 = document.createElement('img');
       img2.src = 'resources/square100.png';
       img2.setAttribute('elementtiming', 'my_image2');
       img2.setAttribute('id', 'image_id');
diff --git a/third_party/blink/web_tests/external/wpt/element-timing/multiple-background-images.html b/third_party/blink/web_tests/external/wpt/element-timing/multiple-background-images.html
index ca349fe..f3fbe76 100644
--- a/third_party/blink/web_tests/external/wpt/element-timing/multiple-background-images.html
+++ b/third_party/blink/web_tests/external/wpt/element-timing/multiple-background-images.html
@@ -23,19 +23,20 @@
     let observedSquare = false;
     const index = window.location.href.lastIndexOf('/');
     const pathname = window.location.href.substring(0, index) + '/resources/';
+    let div = document.getElementById('target');
     const observer = new PerformanceObserver(
       t.step_func(entryList => {
         entryList.getEntries().forEach(entry => {
           numObservedElements++;
           if (entry.name.endsWith('square100.png')) {
             observedSquare = true;
-            checkElement(entry, pathname + 'square100.png', 'multi', 'target', beforeRender);
+            checkElement(entry, pathname + 'square100.png', 'multi', 'target', beforeRender, div);
             checkRect(entry, [0, 200, 0, 200]);
             checkNaturalSize(entry, 100, 100);
           }
           else if (entry.name.endsWith('circle.svg')) {
             observedCircle = true;
-            checkElement(entry, pathname + 'circle.svg', 'multi', 'target', beforeRender);
+            checkElement(entry, pathname + 'circle.svg', 'multi', 'target', beforeRender, div);
             checkRect(entry, [0, 200, 0, 200]);
             checkNaturalSize(entry, 200, 200);
           }
diff --git a/third_party/blink/web_tests/external/wpt/element-timing/observe-background-image.html b/third_party/blink/web_tests/external/wpt/element-timing/observe-background-image.html
index 0669b4c..680c5e4 100644
--- a/third_party/blink/web_tests/external/wpt/element-timing/observe-background-image.html
+++ b/third_party/blink/web_tests/external/wpt/element-timing/observe-background-image.html
@@ -25,7 +25,8 @@
         const index = window.location.href.lastIndexOf('/');
         const pathname = window.location.href.substring(0, index - 14) +
             'images/black-rectangle.png';
-        checkElement(entry, pathname, 'my_div', 'target', beforeRender);
+        checkElement(entry, pathname, 'my_div', 'target', beforeRender,
+            document.getElementById('target'));
         checkRect(entry, [0, 100, 0, 50]);
         checkNaturalSize(entry, 100, 50);
       })
diff --git a/third_party/blink/web_tests/external/wpt/element-timing/observe-elementtiming.html b/third_party/blink/web_tests/external/wpt/element-timing/observe-elementtiming.html
index 39fea05..73f9351 100644
--- a/third_party/blink/web_tests/external/wpt/element-timing/observe-elementtiming.html
+++ b/third_party/blink/web_tests/external/wpt/element-timing/observe-elementtiming.html
@@ -12,6 +12,7 @@
 <script src="resources/element-timing-helpers.js"></script>
 <script>
   let beforeRender;
+  let img;
   async_test(function (t) {
     const observer = new PerformanceObserver(
       t.step_func_done(function(entryList) {
@@ -20,7 +21,7 @@
         const index = window.location.href.lastIndexOf('/');
         const pathname = window.location.href.substring(0, index) +
             '/resources/square100.png';
-        checkElement(entry, pathname, 'my_image', 'my_id', beforeRender);
+        checkElement(entry, pathname, 'my_image', 'my_id', beforeRender, img);
         // Assume viewport has size at least 100, so the element is fully visible.
         checkRect(entry, [0, 100, 0, 100]);
         checkNaturalSize(entry, 100, 100);
@@ -31,7 +32,7 @@
     // in time for it to observe the element timing.
     window.onload = () => {
       // Add image of width equal to 100 and height equal to 100.
-      const img = document.createElement('img');
+      img = document.createElement('img');
       img.src = 'resources/square100.png';
       img.setAttribute('elementtiming', 'my_image');
       img.setAttribute('id', 'my_id');
diff --git a/third_party/blink/web_tests/external/wpt/element-timing/observe-large-image.html b/third_party/blink/web_tests/external/wpt/element-timing/observe-large-image.html
index a08274c..13fc71b 100644
--- a/third_party/blink/web_tests/external/wpt/element-timing/observe-large-image.html
+++ b/third_party/blink/web_tests/external/wpt/element-timing/observe-large-image.html
@@ -12,6 +12,7 @@
 <script src="resources/element-timing-helpers.js"></script>
 <script>
   let beforeRender;
+  let img;
   async_test(function (t) {
     const observer = new PerformanceObserver(
       t.step_func_done(function(entryList) {
@@ -20,7 +21,7 @@
         const index = window.location.href.lastIndexOf('/');
         const pathname = window.location.href.substring(0, index) +
             '/resources/square20.jpg';
-        checkElement(entry, pathname, '', 'large_one', beforeRender);
+        checkElement(entry, pathname, '', 'large_one', beforeRender, img);
         // Assume viewport hasn't changed, so the element occupies all of it.
         checkRect(entry,
           [0, document.documentElement.clientWidth, 0, document.documentElement.clientHeight]);
@@ -32,7 +33,7 @@
     // in time for it to observe the element timing.
     window.onload = () => {
       // Add an image setting width and height equal to viewport.
-      const img = document.createElement('img');
+      img = document.createElement('img');
       img.src = 'resources/square20.jpg';
       img.width = document.documentElement.clientWidth;
       img.height = document.documentElement.clientHeight;
diff --git a/third_party/blink/web_tests/external/wpt/element-timing/observe-multiple-images.html b/third_party/blink/web_tests/external/wpt/element-timing/observe-multiple-images.html
index 05c54ac0..b9e82ed 100644
--- a/third_party/blink/web_tests/external/wpt/element-timing/observe-multiple-images.html
+++ b/third_party/blink/web_tests/external/wpt/element-timing/observe-multiple-images.html
@@ -35,7 +35,8 @@
             const pathname1 = window.location.href.substring(0, index) +
                 '/resources/square100.png';
             // The images do not contain ID, so expect an empty ID.
-            checkElement(entry, pathname1, 'image1', 'img1', beforeRender);
+            checkElement(entry, pathname1, 'image1', 'img1', beforeRender,
+                document.getElementById('img1'));
             // This image is horizontally centered.
             // Using abs and comparing to 1 because the viewport sizes could be odd.
             // If a size is odd, then image cannot be in the pure center, but left
@@ -59,7 +60,8 @@
             image2Observed = 1;
             const pathname2 = window.location.href.substring(0, index) +
                 '/resources/square20.png';
-            checkElement(entry, pathname2, 'image2', 'img2', beforeRender);
+            checkElement(entry, pathname2, 'image2', 'img2', beforeRender,
+                document.getElementById('img2'));
             // This image should be below image 1, and should respect the margin.
             checkRect(entry, [50, 250, 250, 450], "of image2");
             checkNaturalSize(entry, 20, 20);
@@ -72,7 +74,8 @@
             image3Observed = 1;
             const pathname3 = window.location.href.substring(0, index) +
                 '/resources/circle.svg';
-            checkElement(entry, pathname3, 'image3', 'img3', beforeRender);
+            checkElement(entry, pathname3, 'image3', 'img3', beforeRender,
+                document.getElementById('img3'));
             // This image is just to the right of image2.
             checkRect(entry, [250, 450, 250, 450], "of image3");
             checkNaturalSize(entry, 200, 200);
diff --git a/third_party/blink/web_tests/external/wpt/element-timing/observe-shadow-image.html b/third_party/blink/web_tests/external/wpt/element-timing/observe-shadow-image.html
index 1fa6dd4..a4d21be 100644
--- a/third_party/blink/web_tests/external/wpt/element-timing/observe-shadow-image.html
+++ b/third_party/blink/web_tests/external/wpt/element-timing/observe-shadow-image.html
@@ -12,13 +12,14 @@
 <div id='target'></div>
 <script>
   let beforeRender;
+  let img;
   async_test(function (t) {
     const observer = new PerformanceObserver(
       t.step_func_done(function(entryList) {
         assert_equals(entryList.getEntries().length, 1);
         const entry = entryList.getEntries()[0];
         const pathname = window.location.origin + '/element-timing/resources/square100.png';
-        checkElement(entry, pathname, 'my_image', 'my_id', beforeRender);
+        checkElement(entry, pathname, 'my_image', 'my_id', beforeRender, img);
         // Assume viewport has size at least 100, so the element is fully visible.
         checkRect(entry, [0, 100, 0, 100]);
         checkNaturalSize(entry, 100, 100);
@@ -29,7 +30,7 @@
     // in time for it to observe the element timing.
     window.onload = () => {
       // Add image of width equal to 100 and height equal to 100.
-      const img = document.createElement('img');
+      img = document.createElement('img');
       img.src = 'resources/square100.png';
       img.setAttribute('elementtiming', 'my_image');
       img.setAttribute('id', 'my_id');
diff --git a/third_party/blink/web_tests/external/wpt/element-timing/observe-svg-image.html b/third_party/blink/web_tests/external/wpt/element-timing/observe-svg-image.html
index 45e800d..c3c178e 100644
--- a/third_party/blink/web_tests/external/wpt/element-timing/observe-svg-image.html
+++ b/third_party/blink/web_tests/external/wpt/element-timing/observe-svg-image.html
@@ -14,7 +14,8 @@
       const index = window.location.href.lastIndexOf('/');
       const pathname = window.location.href.substring(0, index) +
           '/resources/circle.svg';
-      checkElement(entry, pathname, 'my_svg', 'svg_id', beforeRender);
+      checkElement(entry, pathname, 'my_svg', 'svg_id', beforeRender,
+          document.getElementById('svg_id'));
       // Assume viewport has size at least 200, so the element is fully visible.
       checkRect(entry, [0, 200, 0, 200]);
       checkNaturalSize(entry, 200, 200);
diff --git a/third_party/blink/web_tests/external/wpt/element-timing/observe-video-poster.html b/third_party/blink/web_tests/external/wpt/element-timing/observe-video-poster.html
index d3a6993..500fcedc 100644
--- a/third_party/blink/web_tests/external/wpt/element-timing/observe-video-poster.html
+++ b/third_party/blink/web_tests/external/wpt/element-timing/observe-video-poster.html
@@ -14,7 +14,8 @@
       const index = window.location.href.lastIndexOf('/');
       const pathname = window.location.href.substring(0, index) +
           '/resources/circle.svg';
-      checkElement(entry, pathname, 'my_poster', 'the_poster', beforeRender);
+      checkElement(entry, pathname, 'my_poster', 'the_poster', beforeRender,
+          document.getElementById('the_poster'));
       // Assume viewport has size at least 200, so the element is fully visible.
       checkRect(entry, [0, 200, 0, 200]);
       checkNaturalSize(entry, 200, 200);
diff --git a/third_party/blink/web_tests/external/wpt/element-timing/progressively-loaded-image.html b/third_party/blink/web_tests/external/wpt/element-timing/progressively-loaded-image.html
index c0a7d4f..c534621c 100644
--- a/third_party/blink/web_tests/external/wpt/element-timing/progressively-loaded-image.html
+++ b/third_party/blink/web_tests/external/wpt/element-timing/progressively-loaded-image.html
@@ -8,6 +8,7 @@
 <body>
 <script>
   let beforeRender;
+  let img;
   // Number of characters to be read on the initial read, before sleeping.
   // Should be sufficient to do at least a first scan.
   let numInitial = 75;
@@ -24,13 +25,13 @@
             img_src;
         // Since the image is only fully loaded after the sleep, the render timestamp
         // must be greater than |beforeRender| + |sleep|.
-        checkElement(entry, pathname, 'my_image', '', beforeRender + sleep);
+        checkElement(entry, pathname, 'my_image', '', beforeRender + sleep, img);
         checkNaturalSize(entry, 20, 20);
       })
     );
     observer.observe({entryTypes: ['element']});
 
-    const img = document.createElement('img');
+    img = document.createElement('img');
     img.src = img_src;
     img.setAttribute('elementtiming', 'my_image');
     document.body.appendChild(img);
diff --git a/third_party/blink/web_tests/external/wpt/element-timing/rectangular-image.html b/third_party/blink/web_tests/external/wpt/element-timing/rectangular-image.html
index b0280845..a1af961 100644
--- a/third_party/blink/web_tests/external/wpt/element-timing/rectangular-image.html
+++ b/third_party/blink/web_tests/external/wpt/element-timing/rectangular-image.html
@@ -12,6 +12,7 @@
 <script src="resources/element-timing-helpers.js"></script>
 <script>
   let beforeRender;
+  let img;
   async_test(function (t) {
     const observer = new PerformanceObserver(
       t.step_func_done(function(entryList) {
@@ -21,7 +22,7 @@
         // Subtracting 14 to remove 'element-timing'.
         const pathname = window.location.href.substring(0, index - 14) +
             'images/black-rectangle.png';
-        checkElement(entry, pathname, 'my_image', 'rectangle', beforeRender);
+        checkElement(entry, pathname, 'my_image', 'rectangle', beforeRender, img);
         // Assume viewport has size at least 100, so the element is fully visible.
         checkRect(entry, [20, 120, 20, 70]);
         checkNaturalSize(entry, 100, 50);
@@ -32,7 +33,7 @@
     // in time for it to observe the element timing.
     window.onload = () => {
       // Add image of width equal to 100 and height equal to 50.
-      const img = document.createElement('img');
+      img = document.createElement('img');
       img.src = '/images/black-rectangle.png';
       img.id = 'rectangle';
       img.setAttribute('elementtiming', 'my_image');
diff --git a/third_party/blink/web_tests/external/wpt/element-timing/resources/element-timing-helpers.js b/third_party/blink/web_tests/external/wpt/element-timing/resources/element-timing-helpers.js
index b0ddf30..e378d617f 100644
--- a/third_party/blink/web_tests/external/wpt/element-timing/resources/element-timing-helpers.js
+++ b/third_party/blink/web_tests/external/wpt/element-timing/resources/element-timing-helpers.js
@@ -1,6 +1,6 @@
-// Checks that this is an ElementTiming entry with name |expectedName|. It also
-// does a very basic check on |startTime|: after |beforeRender| and before now().
-function checkElement(entry, expectedName, expectedIdentifier, expectedID, beforeRender) {
+// Common checks between checkElement() and checkElementWithoutResourceTiming().
+function checkElementInternal(entry, expectedName, expectedIdentifier, expectedID, beforeRender,
+    expectedElement) {
   assert_equals(entry.entryType, 'element');
   assert_equals(entry.name, expectedName);
   assert_equals(entry.identifier, expectedIdentifier);
@@ -8,20 +8,25 @@
   assert_equals(entry.id, expectedID);
   assert_greater_than_equal(entry.startTime, beforeRender);
   assert_greater_than_equal(performance.now(), entry.startTime);
+  if (expectedElement !== null)
+    assert_equals(entry.element, expectedElement);
+}
+
+// Checks that this is an ElementTiming entry with name |expectedName|. It also
+// does a very basic check on |startTime|: after |beforeRender| and before now().
+function checkElement(entry, expectedName, expectedIdentifier, expectedID, beforeRender,
+    expectedElement) {
+  checkElementInternal(entry, expectedName, expectedIdentifier, expectedID, beforeRender,
+      expectedElement);
   const rt_entries = performance.getEntriesByName(expectedName, 'resource');
   assert_equals(rt_entries.length, 1);
   assert_equals(rt_entries[0].responseEnd, entry.responseEnd);
 }
 
 function checkElementWithoutResourceTiming(entry, expectedName, expectedIdentifier,
-    expectedID, beforeRender) {
-  assert_equals(entry.entryType, 'element');
-  assert_equals(entry.name, expectedName);
-  assert_equals(entry.identifier, expectedIdentifier);
-  assert_equals(entry.duration, 0);
-  assert_equals(entry.id, expectedID);
-  assert_greater_than_equal(entry.startTime, beforeRender);
-  assert_greater_than_equal(performance.now(), entry.startTime);
+    expectedID, beforeRender, expectedElement) {
+  checkElementInternal(entry, expectedName, expectedIdentifier, expectedID, beforeRender,
+      expectedElement);
   // No associated resource from ResourceTiming, so the responseEnd should be 0.
   assert_equals(entry.responseEnd, 0);
 }
diff --git a/third_party/blink/web_tests/external/wpt/element-timing/resources/iframe-with-square-sends-entry.html b/third_party/blink/web_tests/external/wpt/element-timing/resources/iframe-with-square-sends-entry.html
index 25bd6793..b8af505 100644
--- a/third_party/blink/web_tests/external/wpt/element-timing/resources/iframe-with-square-sends-entry.html
+++ b/third_party/blink/web_tests/external/wpt/element-timing/resources/iframe-with-square-sends-entry.html
@@ -15,6 +15,8 @@
       'naturalWidth' : entryList.getEntries()[0].naturalWidth,
       'naturalHeight' : entryList.getEntries()[0].naturalHeight,
       'id': entryList.getEntries()[0].id,
+      // Elements cannot be cloned, so just send the element ID.
+      'elementId' : entryList.getEntries()[0].element.id,
     }, '*');
   });
   observer.observe({entryTypes: ['element']});
diff --git a/third_party/blink/web_tests/external/wpt/interfaces/IndexedDB.idl b/third_party/blink/web_tests/external/wpt/interfaces/IndexedDB.idl
index 21ff252f..f2625eb 100644
--- a/third_party/blink/web_tests/external/wpt/interfaces/IndexedDB.idl
+++ b/third_party/blink/web_tests/external/wpt/interfaces/IndexedDB.idl
@@ -171,7 +171,7 @@
   readonly attribute IDBCursorDirection direction;
   readonly attribute any key;
   readonly attribute any primaryKey;
-  readonly attribute IDBRequest request;
+  [SameObject] readonly attribute IDBRequest request;
 
   void advance([EnforceRange] unsigned long count);
   void continue(optional any key);
diff --git a/third_party/blink/web_tests/external/wpt/interfaces/wake-lock.idl b/third_party/blink/web_tests/external/wpt/interfaces/wake-lock.idl
index c0d4bc9..466d697c 100644
--- a/third_party/blink/web_tests/external/wpt/interfaces/wake-lock.idl
+++ b/third_party/blink/web_tests/external/wpt/interfaces/wake-lock.idl
@@ -15,11 +15,14 @@
   readonly attribute WakeLockType type;
   readonly attribute boolean active;
   attribute EventHandler onactivechange;
-  Promise<void> request();
-  void abort();
+  Promise<void> request(optional WakeLockRequestOptions options);
   static sequence<WakeLock> query(optional WakeLockQueryFilter filter);
 };
 
+dictionary WakeLockRequestOptions {
+  AbortSignal? signal = null;
+};
+
 dictionary WakeLockQueryFilter {
   WakeLockType? type;
   boolean? active;
diff --git a/third_party/blink/web_tests/external/wpt/wake-lock/idlharness.https.any-expected.txt b/third_party/blink/web_tests/external/wpt/wake-lock/idlharness.https.any-expected.txt
index 0f847f0..2dc5105 100644
--- a/third_party/blink/web_tests/external/wpt/wake-lock/idlharness.https.any-expected.txt
+++ b/third_party/blink/web_tests/external/wpt/wake-lock/idlharness.https.any-expected.txt
@@ -10,8 +10,7 @@
 PASS WakeLock interface: attribute type
 PASS WakeLock interface: attribute active
 PASS WakeLock interface: attribute onactivechange
-FAIL WakeLock interface: operation request() assert_own_property: interface prototype object missing non-static operation expected property "request" missing
-FAIL WakeLock interface: operation abort() assert_own_property: interface prototype object missing non-static operation expected property "abort" missing
+FAIL WakeLock interface: operation request(WakeLockRequestOptions) assert_own_property: interface prototype object missing non-static operation expected property "request" missing
 FAIL WakeLock interface: operation query(WakeLockQueryFilter) assert_own_property: interface object missing static operation expected property "query" missing
 FAIL WakeLock must be primary interface of new WakeLock("screen") assert_equals: Unexpected exception when evaluating object expected null but got object "TypeError: Illegal constructor"
 FAIL Stringification of new WakeLock("screen") assert_equals: Unexpected exception when evaluating object expected null but got object "TypeError: Illegal constructor"
@@ -20,8 +19,8 @@
 FAIL WakeLock interface: new WakeLock("screen") must inherit property "type" with the proper type assert_equals: Unexpected exception when evaluating object expected null but got object "TypeError: Illegal constructor"
 FAIL WakeLock interface: new WakeLock("screen") must inherit property "active" with the proper type assert_equals: Unexpected exception when evaluating object expected null but got object "TypeError: Illegal constructor"
 FAIL WakeLock interface: new WakeLock("screen") must inherit property "onactivechange" with the proper type assert_equals: Unexpected exception when evaluating object expected null but got object "TypeError: Illegal constructor"
-FAIL WakeLock interface: new WakeLock("screen") must inherit property "request()" with the proper type assert_equals: Unexpected exception when evaluating object expected null but got object "TypeError: Illegal constructor"
-FAIL WakeLock interface: new WakeLock("screen") must inherit property "abort()" with the proper type assert_equals: Unexpected exception when evaluating object expected null but got object "TypeError: Illegal constructor"
+FAIL WakeLock interface: new WakeLock("screen") must inherit property "request(WakeLockRequestOptions)" with the proper type assert_equals: Unexpected exception when evaluating object expected null but got object "TypeError: Illegal constructor"
+FAIL WakeLock interface: calling request(WakeLockRequestOptions) on new WakeLock("screen") with too few arguments must throw TypeError assert_equals: Unexpected exception when evaluating object expected null but got object "TypeError: Illegal constructor"
 FAIL WakeLock interface: new WakeLock("screen") must inherit property "query(WakeLockQueryFilter)" with the proper type assert_equals: Unexpected exception when evaluating object expected null but got object "TypeError: Illegal constructor"
 FAIL WakeLock interface: calling query(WakeLockQueryFilter) on new WakeLock("screen") with too few arguments must throw TypeError assert_equals: Unexpected exception when evaluating object expected null but got object "TypeError: Illegal constructor"
 Harness: the test ran to completion.
diff --git a/third_party/blink/web_tests/external/wpt/wake-lock/idlharness.https.any.worker-expected.txt b/third_party/blink/web_tests/external/wpt/wake-lock/idlharness.https.any.worker-expected.txt
index 0d9d4a9e..86359c7d 100644
--- a/third_party/blink/web_tests/external/wpt/wake-lock/idlharness.https.any.worker-expected.txt
+++ b/third_party/blink/web_tests/external/wpt/wake-lock/idlharness.https.any.worker-expected.txt
@@ -10,8 +10,7 @@
 FAIL WakeLock interface: attribute type assert_own_property: self does not have own property "WakeLock" expected property "WakeLock" missing
 FAIL WakeLock interface: attribute active assert_own_property: self does not have own property "WakeLock" expected property "WakeLock" missing
 FAIL WakeLock interface: attribute onactivechange assert_own_property: self does not have own property "WakeLock" expected property "WakeLock" missing
-FAIL WakeLock interface: operation request() assert_own_property: self does not have own property "WakeLock" expected property "WakeLock" missing
-FAIL WakeLock interface: operation abort() assert_own_property: self does not have own property "WakeLock" expected property "WakeLock" missing
+FAIL WakeLock interface: operation request(WakeLockRequestOptions) assert_own_property: self does not have own property "WakeLock" expected property "WakeLock" missing
 FAIL WakeLock interface: operation query(WakeLockQueryFilter) assert_own_property: self does not have own property "WakeLock" expected property "WakeLock" missing
 FAIL WakeLock must be primary interface of new WakeLock("screen") assert_equals: Unexpected exception when evaluating object expected null but got object "ReferenceError: WakeLock is not defined"
 FAIL Stringification of new WakeLock("screen") assert_equals: Unexpected exception when evaluating object expected null but got object "ReferenceError: WakeLock is not defined"
@@ -19,8 +18,8 @@
 FAIL WakeLock interface: new WakeLock("screen") must inherit property "type" with the proper type assert_equals: Unexpected exception when evaluating object expected null but got object "ReferenceError: WakeLock is not defined"
 FAIL WakeLock interface: new WakeLock("screen") must inherit property "active" with the proper type assert_equals: Unexpected exception when evaluating object expected null but got object "ReferenceError: WakeLock is not defined"
 FAIL WakeLock interface: new WakeLock("screen") must inherit property "onactivechange" with the proper type assert_equals: Unexpected exception when evaluating object expected null but got object "ReferenceError: WakeLock is not defined"
-FAIL WakeLock interface: new WakeLock("screen") must inherit property "request()" with the proper type assert_equals: Unexpected exception when evaluating object expected null but got object "ReferenceError: WakeLock is not defined"
-FAIL WakeLock interface: new WakeLock("screen") must inherit property "abort()" with the proper type assert_equals: Unexpected exception when evaluating object expected null but got object "ReferenceError: WakeLock is not defined"
+FAIL WakeLock interface: new WakeLock("screen") must inherit property "request(WakeLockRequestOptions)" with the proper type assert_equals: Unexpected exception when evaluating object expected null but got object "ReferenceError: WakeLock is not defined"
+FAIL WakeLock interface: calling request(WakeLockRequestOptions) on new WakeLock("screen") with too few arguments must throw TypeError assert_equals: Unexpected exception when evaluating object expected null but got object "ReferenceError: WakeLock is not defined"
 FAIL WakeLock interface: new WakeLock("screen") must inherit property "query(WakeLockQueryFilter)" with the proper type assert_equals: Unexpected exception when evaluating object expected null but got object "ReferenceError: WakeLock is not defined"
 FAIL WakeLock interface: calling query(WakeLockQueryFilter) on new WakeLock("screen") with too few arguments must throw TypeError assert_equals: Unexpected exception when evaluating object expected null but got object "ReferenceError: WakeLock is not defined"
 Harness: the test ran to completion.
diff --git a/third_party/blink/web_tests/fast/workers/worker-performance-time.html b/third_party/blink/web_tests/fast/workers/worker-performance-time.html
new file mode 100644
index 0000000..7bea03f
--- /dev/null
+++ b/third_party/blink/web_tests/fast/workers/worker-performance-time.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<title>Test the timing in the worker to be consistent between animation and performance.now().</title>
+<script src="../../resources/testharness.js"></script>
+<script src="../../resources/testharnessreport.js"></script>
+<script id="worker" type="text/worker">
+var countWorker = 10;
+function animate(time) {
+  var timeNow = performance.now();
+  countWorker--;
+  self.postMessage([time,timeNow, countWorker]);
+  if(countWorker > 0)
+    requestAnimationFrame(animate);
+};
+
+self.onmessage = function(msg){
+  requestAnimationFrame(animate);
+}
+</script>
+<script>
+async_test(function(t) {
+  var blob = new Blob([document.getElementById("worker").textContent]);
+  var worker = new Worker(URL.createObjectURL(blob));
+  worker.onmessage = t.step_func(function(pairTime) {
+    var time = pairTime.data[0];
+    var timeNow = pairTime.data[1];
+    var count = pairTime.data[2];
+    assert_approx_equals(time, timeNow, 1000, "Times must be close enough");
+    if(count == 0)
+      t.done();
+  });
+  worker.postMessage("");
+}, 'Test the timing in the worker to be consistent between animation and performance.now');
+</script>
\ No newline at end of file
diff --git a/third_party/blink/web_tests/paint/dark-mode/grayscale-images/desaturate-before-inversion-expected.png b/third_party/blink/web_tests/paint/dark-mode/grayscale-images/desaturate-before-inversion-expected.png
new file mode 100644
index 0000000..87a54af4
--- /dev/null
+++ b/third_party/blink/web_tests/paint/dark-mode/grayscale-images/desaturate-before-inversion-expected.png
Binary files differ
diff --git a/third_party/blink/web_tests/paint/dark-mode/grayscale-images/desaturate-before-inversion.html b/third_party/blink/web_tests/paint/dark-mode/grayscale-images/desaturate-before-inversion.html
new file mode 100644
index 0000000..09ebc168
--- /dev/null
+++ b/third_party/blink/web_tests/paint/dark-mode/grayscale-images/desaturate-before-inversion.html
@@ -0,0 +1,4 @@
+<!DOCTYPE html>
+<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 100 100">
+  <circle cx="50" cy="50" r="40" stroke="black" stroke-width="2" fill="navy" />
+</svg>
diff --git a/third_party/blink/web_tests/platform/mac/paint/dark-mode/grayscale-images/desaturate-before-inversion-expected.png b/third_party/blink/web_tests/platform/mac/paint/dark-mode/grayscale-images/desaturate-before-inversion-expected.png
new file mode 100644
index 0000000..36b619a
--- /dev/null
+++ b/third_party/blink/web_tests/platform/mac/paint/dark-mode/grayscale-images/desaturate-before-inversion-expected.png
Binary files differ
diff --git a/third_party/blink/web_tests/platform/mac/virtual/dark-mode/paint/dark-mode/grayscale-images/desaturate-before-inversion-expected.png b/third_party/blink/web_tests/platform/mac/virtual/dark-mode/paint/dark-mode/grayscale-images/desaturate-before-inversion-expected.png
new file mode 100644
index 0000000..9b9bd4a6
--- /dev/null
+++ b/third_party/blink/web_tests/platform/mac/virtual/dark-mode/paint/dark-mode/grayscale-images/desaturate-before-inversion-expected.png
Binary files differ
diff --git a/third_party/blink/web_tests/virtual/dark-mode/paint/dark-mode/grayscale-images/README.txt b/third_party/blink/web_tests/virtual/dark-mode/paint/dark-mode/grayscale-images/README.txt
new file mode 100644
index 0000000..895eda8
--- /dev/null
+++ b/third_party/blink/web_tests/virtual/dark-mode/paint/dark-mode/grayscale-images/README.txt
@@ -0,0 +1,3 @@
+# This suite runs the tests in LayoutTests/paint/dark-mode
+# with --blink-settings="darkMode=3,darkModeImagePolicy=0,darkModeImageStyle=1"
+# See the virtual_test_suites() method in tools/blinkpy/web_tests/port/base.py.
diff --git a/third_party/blink/web_tests/virtual/dark-mode/paint/dark-mode/grayscale-images/desaturate-before-inversion-expected.png b/third_party/blink/web_tests/virtual/dark-mode/paint/dark-mode/grayscale-images/desaturate-before-inversion-expected.png
new file mode 100644
index 0000000..cc7960a
--- /dev/null
+++ b/third_party/blink/web_tests/virtual/dark-mode/paint/dark-mode/grayscale-images/desaturate-before-inversion-expected.png
Binary files differ
diff --git a/third_party/blink/web_tests/webexposed/global-interface-listing-expected.txt b/third_party/blink/web_tests/webexposed/global-interface-listing-expected.txt
index ad3ea216..0609c9f 100644
--- a/third_party/blink/web_tests/webexposed/global-interface-listing-expected.txt
+++ b/third_party/blink/web_tests/webexposed/global-interface-listing-expected.txt
@@ -5354,6 +5354,7 @@
     setter onresourcetimingbufferfull
 interface PerformanceElementTiming : PerformanceEntry
     attribute @@toStringTag
+    getter element
     getter id
     getter identifier
     getter intersectionRect
diff --git a/third_party/blink/web_tests/xr/ar_hittest.html b/third_party/blink/web_tests/xr/ar_hittest.html
index d99409cf5..1ef6bef 100644
--- a/third_party/blink/web_tests/xr/ar_hittest.html
+++ b/third_party/blink/web_tests/xr/ar_hittest.html
@@ -13,10 +13,10 @@
 
 let testName = "Ensures hit-test returns expected mock results";
 
-let fakeDeviceInitParams = { supportsImmersive: false,
+let fakeDeviceInitParams = { supportsImmersive: true,
                              supportsEnvironmentIntegration: true };
 
-let requestSessionOptions = [ 'legacy-inline-ar' ];
+let requestSessionOptions = [ 'immersive-ar' ];
 
 let expectedHitMatrix = [1, 0, 0, 1,
                          0, 1, 0, 2,
@@ -24,7 +24,7 @@
                          0, 0, 0, 1];
 
 let testFunction = function(session, t, fakeDeviceController) {
-  assert_equals(session.mode, 'legacy-inline-ar');
+  assert_equals(session.mode, 'immersive-ar');
   assert_not_equals(session.environmentBlendMode, 'opaque');
   return session.requestReferenceSpace({ type: "stationary", subtype: "eye-level" }).then((referenceSpace) => {
     let ray = new XRRay({x : 0.0, y : 0.0, z : 0.0}, {x : 1.0, y : 0.0, z: 0.0});
diff --git a/third_party/blink/web_tests/xr/xrSession_environmentProviderDisconnect.html b/third_party/blink/web_tests/xr/xrSession_environmentProviderDisconnect.html
index bd72888..42ea0ee0 100644
--- a/third_party/blink/web_tests/xr/xrSession_environmentProviderDisconnect.html
+++ b/third_party/blink/web_tests/xr/xrSession_environmentProviderDisconnect.html
@@ -13,10 +13,10 @@
 
 let testName = "Outstanding promises get rejected if environmentProvider disconencts";
 
-let fakeDeviceInitParams = { supportsImmersive: false,
+let fakeDeviceInitParams = { supportsImmersive: true,
                              supportsEnvironmentIntegration: true };
 
-let requestSessionOptions = ['legacy-inline-ar'];
+let requestSessionOptions = ['immersive-ar'];
 let refSpace = undefined;
 
 let ray = new XRRay({x : 0.0, y : 0.0, z : 0.0}, {x : 1.0, y : 0.0, z: 0.0});
diff --git a/third_party/webxr_test_pages/README.chromium b/third_party/webxr_test_pages/README.chromium
index 2bf0d4ff..5bb9adc 100644
--- a/third_party/webxr_test_pages/README.chromium
+++ b/third_party/webxr_test_pages/README.chromium
@@ -25,8 +25,7 @@
 - Added missing license file and README.chromium for dat.gui
 
 - Removed the version shim, the samples are intended to work specifically
-  with the ToT Chrome version. The AR samples fall back to legacy-inline-ar
-  mode for now.
+  with the ToT Chrome version.
 
 Instructions:
 
diff --git a/third_party/webxr_test_pages/webxr-samples/proposals/phone-ar-hit-test.html b/third_party/webxr_test_pages/webxr-samples/proposals/phone-ar-hit-test.html
index 4d5dd47..a3df8810 100644
--- a/third_party/webxr_test_pages/webxr-samples/proposals/phone-ar-hit-test.html
+++ b/third_party/webxr_test_pages/webxr-samples/proposals/phone-ar-hit-test.html
@@ -107,35 +107,15 @@
           textEnterXRTitle: "START AR",
           textXRNotFoundTitle: "AR NOT FOUND",
           textExitXRTitle: "EXIT  AR",
-          supportedSessionTypes: ['immersive-ar', 'legacy-inline-ar']
+          supportedSessionTypes: ['immersive-ar']
         });
         document.querySelector('header').appendChild(xrButton.domElement);
       }
 
-      function makeCanvas() {
-          // Create a fullscreen canvas element for use with legacy AR mode.
-          let canvas = document.createElement('canvas');
-          canvas.style.width = '100%';
-          canvas.style.height = '100%';
-          canvas.style.left = 0;
-          canvas.style.top = 0;
-          canvas.style.right = 0;
-          canvas.style.bottom = 0;
-          canvas.style.margin = 0;
-          canvas.id = 'legacy-canvas';
-          return canvas;
-      }
-
       function onRequestSession() {
         navigator.xr.requestSession('immersive-ar').then((session) => {
               xrButton.setSession(session);
               onSessionStarted(session);
-        }).catch(() => {
-            navigator.xr.requestSession('legacy-inline-ar')
-              .then((session) => {
-                xrButton.setSession(session);
-                onSessionStarted(session);
-            });
         });
       }
 
@@ -153,13 +133,7 @@
           scene.setRenderer(renderer);
         }
 
-        let outputCanvas = makeCanvas();
-        document.body.appendChild(outputCanvas);
-
-        session.updateRenderState({
-            baseLayer: new XRWebGLLayer(session, gl),
-            outputContext: outputCanvas.getContext('xrpresent')
-        });
+        session.updateRenderState({ baseLayer: new XRWebGLLayer(session, gl) });
 
         session.requestReferenceSpace({ type: 'stationary', subtype: 'eye-level' }).then((refSpace) => {
           xrRefSpace = refSpace;
@@ -173,9 +147,6 @@
 
       function onSessionEnded(event) {
         xrButton.setSession(null);
-        if (event.session.renderState.outputContext) {
-          document.body.removeChild(event.session.renderState.outputContext.canvas);
-        }
       }
 
       // Adds a new object to the scene at the
diff --git a/third_party/webxr_test_pages/webxr-samples/proposals/phone-ar.html b/third_party/webxr_test_pages/webxr-samples/proposals/phone-ar.html
index 8c87585..ba1c455d1 100644
--- a/third_party/webxr_test_pages/webxr-samples/proposals/phone-ar.html
+++ b/third_party/webxr_test_pages/webxr-samples/proposals/phone-ar.html
@@ -88,25 +88,11 @@
           textEnterXRTitle: "START AR",
           textXRNotFoundTitle: "AR NOT FOUND",
           textExitXRTitle: "EXIT  AR",
-          supportedSessionTypes: ['immersive-ar', 'legacy-inline-ar']
+          supportedSessionTypes: ['immersive-ar']
         });
         document.querySelector('header').appendChild(xrButton.domElement);
       }
 
-      function makeCanvas() {
-          // Create a fullscreen canvas element for use with legacy AR mode.
-          let canvas = document.createElement('canvas');
-          canvas.style.width = '100%';
-          canvas.style.height = '100%';
-          canvas.style.left = 0;
-          canvas.style.top = 0;
-          canvas.style.right = 0;
-          canvas.style.bottom = 0;
-          canvas.style.margin = 0;
-          canvas.id = 'legacy-canvas';
-          return canvas;
-      }
-
       function onRequestSession() {
         // Requests an inline (non-immersive) session with environment integration
         // to get AR via video passthrough.
@@ -119,12 +105,6 @@
         navigator.xr.requestSession('immersive-ar').then((session) => {
             xrButton.setSession(session);
             onSessionStarted(session);
-        }).catch(() => {
-            navigator.xr.requestSession('legacy-inline-ar')
-              .then((session) => {
-                xrButton.setSession(session);
-                onSessionStarted(session);
-            });
         });
       }
 
@@ -141,11 +121,7 @@
           scene.setRenderer(renderer);
         }
 
-        let outputCanvas = makeCanvas();
-        document.body.appendChild(outputCanvas);
-
-        session.updateRenderState({ baseLayer: new XRWebGLLayer(session, gl),
-          outputContext: outputCanvas.getContext('xrpresent')});
+        session.updateRenderState({ baseLayer: new XRWebGLLayer(session, gl) });
 
         session.requestReferenceSpace({ type: 'stationary', subtype: 'eye-level' }).then((refSpace) => {
           xrRefSpace = refSpace;
@@ -159,9 +135,6 @@
 
       function onSessionEnded(event) {
         xrButton.setSession(null);
-        if (event.session.renderState.outputContext) {
-          document.body.removeChild(event.session.renderState.outputContext.canvas);
-        }
       }
 
       // Called every time a XRSession requests that a new frame be drawn.
diff --git a/tools/mb/mb_config.pyl b/tools/mb/mb_config.pyl
index e0e07a7..ec95829 100644
--- a/tools/mb/mb_config.pyl
+++ b/tools/mb/mb_config.pyl
@@ -49,17 +49,7 @@
 
     'chromium.android': {
       'Android ASAN (dbg)': 'android_clang_asan_debug_bot_minimal_symbols',
-      'Android Cronet ARM64 Builder': 'android_cronet_release_bot_minimal_symbols_arm64',
-      'Android Cronet ARM64 Builder (dbg)': 'android_cronet_debug_static_bot_arm64',
-      'Android Cronet Builder': 'android_cronet_release_bot_minimal_symbols_arm_no_neon',
-      'Android Cronet Builder (dbg)': 'android_cronet_debug_static_bot_arm_no_neon',
-      'Android Cronet Builder Asan': 'android_cronet_release_bot_minimal_symbols_arm_no_neon_clang_asan',
-      'Android Cronet KitKat Builder': 'android_cronet_release_bot_minimal_symbols_arm_no_neon',
-      'Android Cronet Lollipop Builder': 'android_cronet_release_bot_minimal_symbols_arm_no_neon',
-      'Android Cronet Marshmallow 64bit Builder': 'android_cronet_release_bot_minimal_symbols_arm64',
       'Android Cronet Marshmallow 64bit Perf': 'android_cronet_release_bot_minimal_symbols_arm64',
-      'Android Cronet x86 Builder': 'android_cronet_release_bot_minimal_symbols_x86',
-      'Android Cronet x86 Builder (dbg)': 'android_cronet_debug_static_bot_x86',
       'Android arm Builder (dbg)': 'android_debug_static_bot',
       'Android arm64 Builder (dbg)': 'android_debug_static_bot_arm64',
       'Android x64 Builder (dbg)': 'android_debug_static_bot_x64',
@@ -85,9 +75,6 @@
     },
 
     'chromium.android.fyi': {
-      'Android Cronet Builder (dbg)': 'android_cronet_debug_static_bot_arm_no_neon',
-      'Android Cronet Builder Asan': 'android_cronet_release_bot_minimal_symbols_arm_no_neon_clang_asan',
-      'Android Cronet KitKat Builder': 'android_cronet_release_bot_minimal_symbols_arm_no_neon',
       'Memory Infra Tester': 'android_release_thumb_bot',
       'NDK Next arm Builder':
         'android_ndk_next_release_bot_minimal_symbols',
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index e513c8f..ec44b9e 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -32950,6 +32950,7 @@
   <int value="-1160941363" label="AffiliationBasedMatching:disabled"/>
   <int value="-1160026273" label="enable-web-notification-custom-layouts"/>
   <int value="-1159563774" label="enable-accessibility-script-injection"/>
+  <int value="-1159369873" label="TabGroupsUiImprovementsAndroid:disabled"/>
   <int value="-1158993534" label="PrintScaling:enabled"/>
   <int value="-1156179600" label="OmniboxRichEntitySuggestions:enabled"/>
   <int value="-1155543191" label="CopylessPaste:disabled"/>
@@ -33307,6 +33308,7 @@
   <int value="-612480090" label="FasterLocationReload:enabled"/>
   <int value="-610411643" label="enable-printer-app-search"/>
   <int value="-606898702" label="MaterialDesignSettings:disabled"/>
+  <int value="-606696801" label="TabGroupsUiImprovementsAndroid:enabled"/>
   <int value="-606431158" label="DrawVerticallyEdgeToEdge:enabled"/>
   <int value="-604814313" label="enable-pinch"/>
   <int value="-604269405"
@@ -33932,6 +33934,8 @@
   <int value="393704200" label="account-consistency"/>
   <int value="398903399" label="GenericSensorExtraClasses:disabled"/>
   <int value="399039205" label="enable-webrtc-hw-vp9-encoding"/>
+  <int value="399398207"
+      label="OmniboxUIExperimentVerticalMarginLimitToNonTouchOnly:enabled"/>
   <int value="400272381" label="LazyFrameLoading:disabled"/>
   <int value="400322063" label="ash-disable-screen-orientation-lock"/>
   <int value="401983950" label="enable-spdy4"/>
@@ -34799,6 +34803,8 @@
   <int value="1750822869" label="CrostiniBackup:disabled"/>
   <int value="1752168018" label="enable-stale-while-revalidate"/>
   <int value="1755024316" label="HostWindowsInAppShimProcess:disabled"/>
+  <int value="1758262950"
+      label="OmniboxUIExperimentVerticalMarginLimitToNonTouchOnly:disabled"/>
   <int value="1760946944" label="MacViewsAutofillPopup:disabled"/>
   <int value="1762320532" label="AutofillKeyboardAccessory:enabled"/>
   <int value="1766676896" label="affiliation-based-matching:disabled"/>
diff --git a/tools/metrics/histograms/histograms.xml b/tools/metrics/histograms/histograms.xml
index c1e5c6f..f65dca6c 100644
--- a/tools/metrics/histograms/histograms.xml
+++ b/tools/metrics/histograms/histograms.xml
@@ -5502,6 +5502,15 @@
   </summary>
 </histogram>
 
+<histogram name="Ash.Display.PrimaryDisplayZoomAtStartup" units="%"
+    expires_after="M79">
+  <owner>jamescook@chromium.org</owner>
+  <owner>jessejames@chromium.org</owner>
+  <summary>
+    The display zoom setting for the primary display, recorded on startup.
+  </summary>
+</histogram>
+
 <histogram name="Ash.DisplayColorManager.HasColorCorrectionMatrix"
     enum="Boolean" expires_after="M77">
   <owner>mcasas@chromium.org</owner>
@@ -13036,6 +13045,19 @@
   </summary>
 </histogram>
 
+<histogram name="Blink.UseCounter.FeaturePolicy.ImageDownscalingRatio"
+    units="%" expires_after="M77">
+  <owner>loonybear@chromium.org</owner>
+  <owner>iclelland@chromium.org</owner>
+  <summary>
+    Logs downscaling ratio in percentage for images enforced by feature policy
+    oversized-images policy going into origin trials in M75. If an image's
+    downscaling ratio is 1, it will be represented as 10 percent, if an image's
+    downscaling ratio is 5, it will be represented as 50 percents. Recorded when
+    oversized-images policy is enforced and the image is about to be painted.
+  </summary>
+</histogram>
+
 <histogram name="Blink.UseCounter.FeaturePolicy.ImageFormats"
     enum="FeaturePolicyImageCompressionFormat" expires_after="M77">
   <owner>loonybear@chromium.org</owner>
@@ -48411,6 +48433,9 @@
 
 <histogram name="IOS.TabGridMediator.DidDetachNilWebStateList"
     enum="BooleanNil" expires_after="2019-05-01">
+  <obsolete>
+    Deprecated 2019-04.
+  </obsolete>
   <owner>edchin@chromium.org</owner>
   <owner>marq@chromium.org</owner>
   <summary>
@@ -48426,6 +48451,9 @@
 
 <histogram name="IOS.TabGridMediator.GetActiveTabIDNilWebStateList"
     enum="BooleanNil" expires_after="2019-05-01">
+  <obsolete>
+    Deprecated 2019-04.
+  </obsolete>
   <owner>edchin@chromium.org</owner>
   <owner>marq@chromium.org</owner>
   <summary>
@@ -49821,6 +49849,12 @@
   </summary>
 </histogram>
 
+<histogram name="Login.DefaultPageZoom" units="%" expires_after="M79">
+  <owner>jamescook@chromium.org</owner>
+  <owner>jessejames@chromium.org</owner>
+  <summary>The user's default page zoom setting, recorded on login.</summary>
+</histogram>
+
 <histogram name="Login.FailureReason" enum="LoginFailureReason">
   <owner>achuith@chromium.org</owner>
   <summary>Chrome OS login failure reason.</summary>
diff --git a/tools/perf/contrib/cluster_telemetry/generic_trace.py b/tools/perf/contrib/cluster_telemetry/generic_trace.py
index 653bb36..803c52a 100644
--- a/tools/perf/contrib/cluster_telemetry/generic_trace.py
+++ b/tools/perf/contrib/cluster_telemetry/generic_trace.py
@@ -70,7 +70,7 @@
 
 
 @benchmark.Info(emails=['wangxianzhu@chromium.org'],
-                documentation_url='https://bit.ly/2IMIMoI')
+                documentation_url='https://bit.ly/2DIOVy3')
 # For local verification.
 class GenericTraceTop25(_GenericTraceBenchmark):
   page_set = page_sets.StaticTop25PageSet
@@ -81,7 +81,7 @@
 
 
 @benchmark.Info(emails=['wangxianzhu@chromium.org'],
-                documentation_url='https://bit.ly/2IMIMoI')
+                documentation_url='https://bit.ly/2DIOVy3')
 class GenericTraceClusterTelemetry(_GenericTraceBenchmark):
   @classmethod
   def Name(cls):
diff --git a/tools/perf/expectations.config b/tools/perf/expectations.config
index 5c90f6ad..1cbe92ac 100644
--- a/tools/perf/expectations.config
+++ b/tools/perf/expectations.config
@@ -164,10 +164,6 @@
 crbug.com/865400 [ Pixel_2 Android_Webview ] loading.mobile/VoiceMemos_cold_3g [ Skip ]
 crbug.com/919191 [ Nexus5X_Webview ] loading.mobile/OLX_3g [ Skip ]
 
-# Benchmark: media.desktop
-crbug.com/957977 [ Win ] media.desktop/video.html?src=crowd1080.mp4 [ Skip ]
-crbug.com/957977 [ Win ] media.desktop/video.html?src=crowd1080.webm [ Skip ]
-
 # Benchmark: oilpan_gc_times.key_silk_cases
 crbug.com/446332 [ All ] oilpan_gc_times.key_silk_cases/slide_drawer [ Skip ]
 crbug.com/507865 [ All ] oilpan_gc_times.key_silk_cases/polymer_topeka [ Skip ]
@@ -324,6 +320,7 @@
 crbug.com/953371 [ Win ] v8.browsing_desktop/browse:social:twitter_infinite_scroll:2018 [ Skip ]
 crbug.com/954959 [ Linux ] v8.browsing_desktop/browse:media:pinterest:2018 [ Skip ]
 crbug.com/954959 [ Linux ] v8.browsing_desktop/browse:tools:maps [ Skip ]
+crbug.com/958422 [ Linux ] v8.browsing_desktop/browse:social:tumblr_infinite_scroll:2018 [ Skip ]
 
 # Benchmark v8.browsing_desktop-future
 crbug.com/788796 [ Linux ] v8.browsing_desktop-future/browse:media:imgur [ Skip ]
@@ -331,6 +328,7 @@
 crbug.com/773084 [ Mac ] v8.browsing_desktop-future/browse:tools:maps [ Skip ]
 crbug.com/906654 [ All ] v8.browsing_desktop-future/browse:search:google [ Skip ]
 crbug.com/953371 [ Win ] v8.browsing_desktop-future/browse:social:twitter_infinite_scroll:2018 [ Skip ]
+crbug.com/958422 [ Linux ] v8.browsing_desktop-future/browse:social:tumblr_infinite_scroll:2018 [ Skip ]
 
 # Benchmark: v8.browsing_mobile
 crbug.com/714650 [ Android ] v8.browsing_mobile/browse:news:globo [ Skip ]
diff --git a/ui/events/keycodes/dom_us_layout_data.h b/ui/events/keycodes/dom_us_layout_data.h
index b61b001..d5a1516 100644
--- a/ui/events/keycodes/dom_us_layout_data.h
+++ b/ui/events/keycodes/dom_us_layout_data.h
@@ -555,7 +555,7 @@
     // DomCode::LANG3                              0x070092 Lang3
     // DomCode::LANG4                              0x070093 Lang4
     // DomCode::LANG5                              0x070094 Lang5
-    // DomCode::ABORT                              0x07009B Abort
+    {DomCode::ABORT, VKEY_CANCEL},              // 0x07009B Abort
     // DomCode::PROPS                              0x0700A3 Props
     // DomCode::NUMPAD_PAREN_LEFT                  0x0700B6 NumpadParenLeft
     // DomCode::NUMPAD_PAREN_RIGHT                 0x0700B7 NumpadParenRight
diff --git a/ui/native_theme/native_theme.cc b/ui/native_theme/native_theme.cc
index 3dd69b5..b71efc47 100644
--- a/ui/native_theme/native_theme.cc
+++ b/ui/native_theme/native_theme.cc
@@ -6,8 +6,10 @@
 
 #include <cstring>
 
+#include "base/bind.h"
 #include "base/command_line.h"
 #include "ui/base/ui_base_switches.h"
+#include "ui/native_theme/dark_mode_observer.h"
 #include "ui/native_theme/native_theme_observer.h"
 
 namespace ui {
@@ -48,7 +50,10 @@
       is_dark_mode_(IsForcedDarkMode()),
       is_high_contrast_(IsForcedHighContrast()) {}
 
-NativeTheme::~NativeTheme() {}
+NativeTheme::~NativeTheme() {
+  if (dark_mode_parent_observer_)
+    dark_mode_parent_observer_->Stop();
+}
 
 bool NativeTheme::SystemDarkModeEnabled() const {
   return is_dark_mode_;
@@ -83,4 +88,16 @@
   return CaptionStyle::FromSystemSettings();
 }
 
+void NativeTheme::SetDarkModeParent(NativeTheme* dark_mode_parent) {
+  dark_mode_parent_observer_ = std::make_unique<DarkModeObserver>(
+      dark_mode_parent,
+      base::BindRepeating(&NativeTheme::OnParentDarkModeChanged,
+                          base::Unretained(this)));
+  dark_mode_parent_observer_->Start();
+}
+
+void NativeTheme::OnParentDarkModeChanged(bool is_dark_mode) {
+  set_dark_mode(is_dark_mode);
+  NotifyObservers();
+}
 }  // namespace ui
diff --git a/ui/native_theme/native_theme.h b/ui/native_theme/native_theme.h
index 7e3ee539..15f5391 100644
--- a/ui/native_theme/native_theme.h
+++ b/ui/native_theme/native_theme.h
@@ -23,7 +23,7 @@
 }
 
 namespace ui {
-
+class DarkModeObserver;
 class NativeThemeObserver;
 
 // This class supports drawing UI controls (like buttons, text fields, lists,
@@ -423,6 +423,10 @@
   // Returns the system's caption style.
   virtual CaptionStyle GetSystemCaptionStyle() const;
 
+  // Observes |dark_mode_parent| for dark mode changes and propagates them to
+  // self.
+  void SetDarkModeParent(NativeTheme* dark_mode_parent);
+
  protected:
   NativeTheme();
   virtual ~NativeTheme();
@@ -442,9 +446,13 @@
   unsigned int track_color_;
 
  private:
+  // DarkModeObserver callback.
+  void OnParentDarkModeChanged(bool is_dark_mode);
   // Observers to notify when the native theme changes.
   base::ObserverList<NativeThemeObserver>::Unchecked native_theme_observers_;
 
+  std::unique_ptr<DarkModeObserver> dark_mode_parent_observer_;
+
   bool is_dark_mode_ = false;
   bool is_high_contrast_ = false;
 
diff --git a/ui/native_theme/native_theme_win.cc b/ui/native_theme/native_theme_win.cc
index 24972aa..275f0c7d 100644
--- a/ui/native_theme/native_theme_win.cc
+++ b/ui/native_theme/native_theme_win.cc
@@ -266,6 +266,7 @@
             L"Themes\\Personalize",
             KEY_READ | KEY_NOTIFY) == ERROR_SUCCESS;
     if (key_open_succeeded) {
+      NativeTheme::GetInstanceForWeb()->SetDarkModeParent(this);
       UpdateDarkModeStatus();
       RegisterThemeRegkeyObserver();
     }
@@ -1921,7 +1922,6 @@
   hkcu_themes_regkey_.StartWatching(base::BindOnce(
       [](NativeThemeWin* native_theme) {
         native_theme->UpdateDarkModeStatus();
-        native_theme->NotifyObservers();
         // RegKey::StartWatching only provides one notification. Reregistration
         // is required to get future notifications.
         native_theme->RegisterThemeRegkeyObserver();
@@ -1938,6 +1938,7 @@
     fDarkModeEnabled = (apps_use_light_theme == 0);
   }
   set_dark_mode(fDarkModeEnabled);
+  NotifyObservers();
 }
 
 }  // namespace ui
diff --git a/ui/views/bubble/bubble_dialog_delegate_view.cc b/ui/views/bubble/bubble_dialog_delegate_view.cc
index bbcd9e7..48b8161 100644
--- a/ui/views/bubble/bubble_dialog_delegate_view.cc
+++ b/ui/views/bubble/bubble_dialog_delegate_view.cc
@@ -168,13 +168,8 @@
       provider->GetInsetsMetric(INSETS_DIALOG_SUBSECTION));
   frame->SetFootnoteView(CreateFootnoteView());
 
-  BubbleBorder::Arrow adjusted_arrow = arrow();
-  if (base::i18n::IsRTL()) {
-    adjusted_arrow = BubbleBorder::horizontal_mirror(adjusted_arrow);
-    arrow_ = adjusted_arrow;
-  }
   std::unique_ptr<BubbleBorder> border =
-      std::make_unique<BubbleBorder>(adjusted_arrow, GetShadow(), color());
+      std::make_unique<BubbleBorder>(arrow(), GetShadow(), color());
   if (CustomShadowsSupported() && ShouldHaveRoundCorners()) {
     border->SetCornerRadius(
         base::FeatureList::IsEnabled(features::kEnableMDRoundedCornersOnDialogs)
@@ -278,6 +273,8 @@
 }
 
 void BubbleDialogDelegateView::SetArrow(BubbleBorder::Arrow arrow) {
+  if (base::i18n::IsRTL())
+    arrow = BubbleBorder::horizontal_mirror(arrow);
   if (arrow_ == arrow)
     return;
   arrow_ = arrow;
@@ -321,12 +318,12 @@
     : close_on_deactivate_(true),
       anchor_view_tracker_(std::make_unique<ViewTracker>()),
       anchor_widget_(nullptr),
-      arrow_(arrow),
       shadow_(shadow),
       color_explicitly_set_(false),
       accept_events_(true),
       adjust_if_offscreen_(true),
       parent_window_(nullptr) {
+  SetArrow(arrow);
   LayoutProvider* provider = LayoutProvider::Get();
   // An individual bubble should override these margins if its layout differs
   // from the typical title/text/buttons.
diff --git a/ui/views/bubble/bubble_dialog_delegate_view.h b/ui/views/bubble/bubble_dialog_delegate_view.h
index 887bc541..a28d7a03 100644
--- a/ui/views/bubble/bubble_dialog_delegate_view.h
+++ b/ui/views/bubble/bubble_dialog_delegate_view.h
@@ -81,7 +81,7 @@
   // The anchor rect is used in the absence of an assigned anchor view.
   const gfx::Rect& anchor_rect() const { return anchor_rect_; }
 
-  BubbleBorder::Arrow arrow() const { return arrow_; }
+  // Set the desired arrow for the bubble. The arrow will be mirrored for RTL.
   void SetArrow(BubbleBorder::Arrow arrow);
 
   BubbleBorder::Shadow GetShadow() const;
@@ -140,6 +140,9 @@
       BubbleBorder::Arrow arrow,
       BubbleBorder::Shadow shadow = BubbleBorder::DIALOG_SHADOW);
 
+  // Returns the desired arrow post-RTL mirroring if needed.
+  BubbleBorder::Arrow arrow() const { return arrow_; }
+
   // Get bubble bounds from the anchor rect and client view's preferred size.
   virtual gfx::Rect GetBubbleBounds();
 
@@ -218,8 +221,8 @@
   // The anchor rect used in the absence of an anchor view.
   mutable gfx::Rect anchor_rect_;
 
-  // The arrow's location on the bubble.
-  BubbleBorder::Arrow arrow_;
+  // The arrow's default location on the bubble post-RTL mirroring if needed.
+  BubbleBorder::Arrow arrow_ = BubbleBorder::NONE;
 
   // Bubble border shadow to use.
   BubbleBorder::Shadow shadow_;
diff --git a/ui/webui/resources/cr_elements/cr_search_field/cr_search_field_behavior.js b/ui/webui/resources/cr_elements/cr_search_field/cr_search_field_behavior.js
index 88c79f23..48b9417 100644
--- a/ui/webui/resources/cr_elements/cr_search_field/cr_search_field_behavior.js
+++ b/ui/webui/resources/cr_elements/cr_search_field/cr_search_field_behavior.js
@@ -24,14 +24,11 @@
       reflectToAttribute: true,
       value: false,
     },
-
-    /** @private */
-    lastValue_: {
-      type: String,
-      value: '',
-    },
   },
 
+  /** @private {string} */
+  effectiveValue_: '',
+
   /** @private {number} */
   searchDelayTimer_: -1,
 
@@ -55,11 +52,16 @@
    *     firing for this change.
    */
   setValue: function(value, opt_noEvent) {
-    const searchInput = this.getSearchInput();
-    searchInput.value = value;
+    const updated = this.updateEffectiveValue_(value);
+    this.getSearchInput().value = this.effectiveValue_;
+    if (!updated) {
+      return;
+    }
 
     this.onSearchTermInput();
-    this.onValueChanged_(value, !!opt_noEvent);
+    if (!opt_noEvent) {
+      this.fire('search-changed', this.effectiveValue_);
+    }
   },
 
   /** @private */
@@ -106,18 +108,27 @@
    * @private
    */
   onValueChanged_: function(newValue, noEvent) {
-    // Trim leading whitespace and replace consecutive whitespace with single
-    // space. This will prevent empty string searches and searches for
-    // effectively the same query.
-    const effectiveValue = newValue.replace(/\s+/g, ' ').replace(/^\s/, '');
-    if (effectiveValue == this.lastValue_) {
-      return;
+    const updated = this.updateEffectiveValue_(newValue);
+    if (updated && !noEvent) {
+      this.fire('search-changed', this.effectiveValue_);
+    }
+  },
+
+  /**
+   * Trim leading whitespace and replace consecutive whitespace with single
+   * space. This will prevent empty string searches and searches for
+   * effectively the same query.
+   * @param {string} value
+   * @return {boolean}
+   * @private
+   */
+  updateEffectiveValue_: function(value) {
+    const effectiveValue = value.replace(/\s+/g, ' ').replace(/^\s/, '');
+    if (effectiveValue == this.effectiveValue_) {
+      return false;
     }
 
-    this.lastValue_ = effectiveValue;
-
-    if (!noEvent) {
-      this.fire('search-changed', effectiveValue);
-    }
+    this.effectiveValue_ = effectiveValue;
+    return true;
   },
 };