diff --git a/DEPS b/DEPS
index e2f7355..a7d7a58 100644
--- a/DEPS
+++ b/DEPS
@@ -318,7 +318,7 @@
   },
 
   'src/third_party/depot_tools':
-    Var('chromium_git') + '/chromium/tools/depot_tools.git' + '@' + '6c5a1c4c3c97b568c79ee44a7b1d7ffac17e30d3',
+    Var('chromium_git') + '/chromium/tools/depot_tools.git' + '@' + '17995a9f128290b66e16f2fdfba96b4369425f8c',
 
   # DevTools node modules. Used on Linux buildbots only.
   'src/third_party/devtools-node-modules': {
diff --git a/ash/login/ui/lock_debug_view.cc b/ash/login/ui/lock_debug_view.cc
index 7a84ef99..f6180d4 100644
--- a/ash/login/ui/lock_debug_view.cc
+++ b/ash/login/ui/lock_debug_view.cc
@@ -182,14 +182,11 @@
     debug_dispatcher_.SetLockScreenNoteState(lock_screen_note_state_);
   }
 
-  void ToggleLockScreenDevChannelInfo() {
-    show_dev_channel_info_ = !show_dev_channel_info_;
-    if (show_dev_channel_info_) {
-      debug_dispatcher_.SetDevChannelInfo(kDebugOsVersion, kDebugEnterpriseInfo,
-                                          kDebugBluetoothName);
-    } else {
-      debug_dispatcher_.SetDevChannelInfo("", "", "");
-    }
+  void AddLockScreenDevChannelInfo(const std::string& os_version,
+                                   const std::string& enterprise_info,
+                                   const std::string& bluetooth_name) {
+    debug_dispatcher_.SetDevChannelInfo(os_version, enterprise_info,
+                                        bluetooth_name);
   }
 
   // LoginDataDispatcher::Observer:
@@ -251,9 +248,6 @@
   // The current lock screen note action state.
   mojom::TrayActionState lock_screen_note_state_;
 
-  // If the dev channel info is being shown.
-  bool show_dev_channel_info_ = false;
-
   DISALLOW_COPY_AND_ASSIGN(DebugDataDispatcherTransformer);
 };
 
@@ -284,8 +278,8 @@
 
   toggle_blur_ = AddButton("Blur");
   toggle_note_action_ = AddButton("Toggle note action");
-  toggle_dev_channel_info_ = AddButton("Toggle dev channel info");
   toggle_caps_lock_ = AddButton("Toggle caps lock");
+  add_dev_channel_info_ = AddButton("Add dev channel info");
   add_user_ = AddButton("Add user");
   remove_user_ = AddButton("Remove user");
   toggle_auth_ = AddButton("Auth (allowed)");
@@ -316,12 +310,6 @@
     return;
   }
 
-  // Enable or disable dev channel info.
-  if (sender == toggle_dev_channel_info_) {
-    debug_data_dispatcher_->ToggleLockScreenDevChannelInfo();
-    return;
-  }
-
   // Enable or disable caps lock.
   if (sender == toggle_caps_lock_) {
     chromeos::input_method::ImeKeyboard* keyboard =
@@ -330,6 +318,25 @@
     return;
   }
 
+  // Iteratively adds more info to the dev channel labels to test 7 permutations
+  // and then disables the button.
+  if (sender == add_dev_channel_info_) {
+    DCHECK(num_dev_channel_info_clicks_ < 7u);
+    ++num_dev_channel_info_clicks_;
+    if (num_dev_channel_info_clicks_ == 7u)
+      add_dev_channel_info_->SetEnabled(false);
+
+    std::string os_version =
+        num_dev_channel_info_clicks_ / 4 ? kDebugOsVersion : "";
+    std::string enterprise_info =
+        (num_dev_channel_info_clicks_ % 4) / 2 ? kDebugEnterpriseInfo : "";
+    std::string bluetooth_name =
+        num_dev_channel_info_clicks_ % 2 ? kDebugBluetoothName : "";
+    debug_data_dispatcher_->AddLockScreenDevChannelInfo(
+        os_version, enterprise_info, bluetooth_name);
+    return;
+  }
+
   // Add or remove a user.
   if (sender == add_user_ || sender == remove_user_) {
     if (sender == add_user_)
diff --git a/ash/login/ui/lock_debug_view.h b/ash/login/ui/lock_debug_view.h
index 16fa56e..3e1bb40 100644
--- a/ash/login/ui/lock_debug_view.h
+++ b/ash/login/ui/lock_debug_view.h
@@ -59,8 +59,8 @@
   views::View* debug_row_ = nullptr;
   views::MdTextButton* toggle_blur_ = nullptr;
   views::MdTextButton* toggle_note_action_ = nullptr;
-  views::MdTextButton* toggle_dev_channel_info_ = nullptr;
   views::MdTextButton* toggle_caps_lock_ = nullptr;
+  views::MdTextButton* add_dev_channel_info_ = nullptr;
   views::MdTextButton* add_user_ = nullptr;
   views::MdTextButton* remove_user_ = nullptr;
   views::MdTextButton* toggle_auth_ = nullptr;
@@ -68,6 +68,7 @@
   // Debug dispatcher and cached data for the UI.
   std::unique_ptr<DebugDataDispatcherTransformer> const debug_data_dispatcher_;
   size_t num_users_ = 1u;
+  size_t num_dev_channel_info_clicks_ = 0u;
   LoginScreenController::ForceFailAuth force_fail_auth_ =
       LoginScreenController::ForceFailAuth::kOff;
 
diff --git a/ash/public/cpp/ash_pref_names.cc b/ash/public/cpp/ash_pref_names.cc
index 1be3dfd..f5e57e5 100644
--- a/ash/public/cpp/ash_pref_names.cc
+++ b/ash/public/cpp/ash_pref_names.cc
@@ -133,6 +133,9 @@
 // a confirmation dialog.
 const char kLogoutDialogDurationMs[] = "logout_dialog_duration_ms";
 
+// A dictionary pref that maps usernames to wallpaper info.
+const char kUserWallpaperInfo[] = "user_wallpaper_info";
+
 // A dictionary pref that maps wallpaper file paths to their prominent colors.
 const char kWallpaperColors[] = "ash.wallpaper.prominent_colors";
 
diff --git a/ash/public/cpp/ash_pref_names.h b/ash/public/cpp/ash_pref_names.h
index 5a800ccd..72106378 100644
--- a/ash/public/cpp/ash_pref_names.h
+++ b/ash/public/cpp/ash_pref_names.h
@@ -50,6 +50,7 @@
 ASH_PUBLIC_EXPORT extern const char kShowLogoutButtonInTray[];
 ASH_PUBLIC_EXPORT extern const char kLogoutDialogDurationMs[];
 
+ASH_PUBLIC_EXPORT extern const char kUserWallpaperInfo[];
 ASH_PUBLIC_EXPORT extern const char kWallpaperColors[];
 
 ASH_PUBLIC_EXPORT extern const char kUserBluetoothAdapterEnabled[];
diff --git a/ash/shell.cc b/ash/shell.cc
index b4560b2..3a74570 100644
--- a/ash/shell.cc
+++ b/ash/shell.cc
@@ -1103,11 +1103,6 @@
   if (config != Config::MASH)
     virtual_keyboard_controller_.reset(new VirtualKeyboardController);
 
-  // Initialize the wallpaper after the RootWindowController has been created,
-  // otherwise the widget will not paint when restoring after a browser crash.
-  // Also, initialize after display initialization to ensure correct sizing.
-  wallpaper_delegate_->InitializeWallpaper();
-
   if (cursor_manager_) {
     if (initially_hide_cursor_)
       cursor_manager_->HideCursor();
diff --git a/ash/system/screen_layout_observer.cc b/ash/system/screen_layout_observer.cc
index 076fe9ac..30ddeca 100644
--- a/ash/system/screen_layout_observer.cc
+++ b/ash/system/screen_layout_observer.cc
@@ -69,7 +69,9 @@
 }
 
 // Callback to handle a user selecting the notification view.
-void OpenSettingsFromNotification() {
+void OpenSettingsFromNotification(base::Optional<int> button_index) {
+  DCHECK(!button_index);
+
   Shell::Get()->metrics()->RecordUserMetricsAction(
       UMA_STATUS_AREA_DISPLAY_NOTIFICATION_SELECTED);
   // Settings may be blocked, e.g. at the lock screen.
@@ -358,7 +360,7 @@
               message_center::NotifierId::SYSTEM_COMPONENT,
               system_notifier::kNotifierDisplay),
           message_center::RichNotificationData(),
-          new message_center::HandleNotificationClickedDelegate(
+          new message_center::HandleNotificationClickDelegate(
               base::Bind(&OpenSettingsFromNotification)),
           kNotificationScreenIcon,
           message_center::SystemNotificationWarningLevel::NORMAL);
diff --git a/ash/wallpaper/wallpaper_controller.cc b/ash/wallpaper/wallpaper_controller.cc
index d45cd2c4..3783dd1 100644
--- a/ash/wallpaper/wallpaper_controller.cc
+++ b/ash/wallpaper/wallpaper_controller.cc
@@ -25,6 +25,7 @@
 #include "base/command_line.h"
 #include "base/logging.h"
 #include "base/sequenced_task_runner.h"
+#include "base/strings/string_number_conversions.h"
 #include "base/task_scheduler/post_task.h"
 #include "base/values.h"
 #include "components/prefs/pref_registry_simple.h"
@@ -43,11 +44,18 @@
 using color_utils::LumaRange;
 using color_utils::SaturationRange;
 using wallpaper::ColorProfileType;
+using wallpaper::WallpaperInfo;
 
 namespace ash {
 
 namespace {
 
+// Names of nodes with wallpaper info in |kUserWallpaperInfo| dictionary.
+const char kNewWallpaperDateNodeName[] = "date";
+const char kNewWallpaperLayoutNodeName[] = "layout";
+const char kNewWallpaperLocationNodeName[] = "file";
+const char kNewWallpaperTypeNodeName[] = "type";
+
 // How long to wait reloading the wallpaper after the display size has changed.
 constexpr int kWallpaperReloadDelayMs = 100;
 
@@ -57,10 +65,9 @@
 // Caches color calculation results in local state pref service.
 void CacheProminentColors(const std::vector<SkColor>& colors,
                           const std::string& current_location) {
-  // Local state can be null during startup.
-  if (!Shell::Get()->GetLocalStatePrefService()) {
+  // Local state can be null in tests.
+  if (!Shell::Get()->GetLocalStatePrefService())
     return;
-  }
   DictionaryPrefUpdate wallpaper_colors_update(
       Shell::Get()->GetLocalStatePrefService(), prefs::kWallpaperColors);
   auto wallpaper_colors = std::make_unique<base::ListValue>();
@@ -76,7 +83,7 @@
     const std::string& current_location) {
   base::Optional<std::vector<SkColor>> cached_colors_out;
   const base::ListValue* prominent_colors = nullptr;
-  // Local state can be null during startup.
+  // Local state can be null in tests.
   if (!Shell::Get()->GetLocalStatePrefService() ||
       !Shell::Get()
            ->GetLocalStatePrefService()
@@ -185,7 +192,8 @@
 // static
 void WallpaperController::RegisterLocalStatePrefs(
     PrefRegistrySimple* registry) {
-  registry->RegisterForeignPref(prefs::kWallpaperColors);
+  registry->RegisterDictionaryPref(prefs::kUserWallpaperInfo);
+  registry->RegisterDictionaryPref(prefs::kWallpaperColors);
 }
 
 void WallpaperController::BindRequest(
@@ -226,9 +234,8 @@
   return wallpaper::WALLPAPER_LAYOUT_CENTER_CROPPED;
 }
 
-void WallpaperController::SetWallpaperImage(
-    const gfx::ImageSkia& image,
-    const wallpaper::WallpaperInfo& info) {
+void WallpaperController::SetWallpaperImage(const gfx::ImageSkia& image,
+                                            const WallpaperInfo& info) {
   wallpaper::WallpaperLayout layout = info.layout;
   VLOG(1) << "SetWallpaper: image_id="
           << wallpaper::WallpaperResizer::GetImageId(image)
@@ -254,6 +261,7 @@
     observer.OnWallpaperDataChanged();
   wallpaper_mode_ = WALLPAPER_IMAGE;
   InstallDesktopControllerForAllWindows();
+  wallpaper_count_for_testing_++;
 }
 
 void WallpaperController::CreateEmptyWallpaper() {
@@ -297,7 +305,7 @@
 
 void WallpaperController::OnLocalStatePrefServiceInitialized(
     PrefService* pref_service) {
-  CalculateWallpaperColors();
+  Shell::Get()->wallpaper_delegate()->InitializeWallpaper();
 }
 
 void WallpaperController::OnSessionStateChanged(
@@ -387,6 +395,134 @@
              switches::kAshDisableLoginDimAndBlur);
 }
 
+void WallpaperController::SetUserWallpaperInfo(const AccountId& account_id,
+                                               const WallpaperInfo& info,
+                                               bool is_persistent) {
+  current_user_wallpaper_info_ = info;
+  if (!is_persistent)
+    return;
+
+  PrefService* local_state = Shell::Get()->GetLocalStatePrefService();
+  // Local state can be null in tests.
+  if (!local_state)
+    return;
+  WallpaperInfo old_info;
+  if (GetUserWallpaperInfo(account_id, &old_info, is_persistent)) {
+    // Remove the color cache of the previous wallpaper if it exists.
+    DictionaryPrefUpdate wallpaper_colors_update(local_state,
+                                                 prefs::kWallpaperColors);
+    wallpaper_colors_update->RemoveWithoutPathExpansion(old_info.location,
+                                                        nullptr);
+  }
+  DictionaryPrefUpdate wallpaper_update(local_state, prefs::kUserWallpaperInfo);
+
+  auto wallpaper_info_dict = std::make_unique<base::DictionaryValue>();
+  wallpaper_info_dict->SetString(
+      kNewWallpaperDateNodeName,
+      base::Int64ToString(info.date.ToInternalValue()));
+  wallpaper_info_dict->SetString(kNewWallpaperLocationNodeName, info.location);
+  wallpaper_info_dict->SetInteger(kNewWallpaperLayoutNodeName, info.layout);
+  wallpaper_info_dict->SetInteger(kNewWallpaperTypeNodeName, info.type);
+  wallpaper_update->SetWithoutPathExpansion(account_id.GetUserEmail(),
+                                            std::move(wallpaper_info_dict));
+}
+
+bool WallpaperController::GetUserWallpaperInfo(const AccountId& account_id,
+                                               WallpaperInfo* info,
+                                               bool is_persistent) {
+  if (!is_persistent) {
+    // Default to the values cached in memory.
+    *info = current_user_wallpaper_info_;
+
+    // Ephemeral users do not save anything to local state. But we have got
+    // wallpaper info from memory. Returns true.
+    return true;
+  }
+
+  PrefService* local_state = Shell::Get()->GetLocalStatePrefService();
+  // Local state can be null in tests.
+  if (!local_state)
+    return false;
+  const base::DictionaryValue* info_dict;
+  if (!local_state->GetDictionary(prefs::kUserWallpaperInfo)
+           ->GetDictionaryWithoutPathExpansion(account_id.GetUserEmail(),
+                                               &info_dict)) {
+    return false;
+  }
+
+  // Use temporary variables to keep |info| untouched in the error case.
+  std::string location;
+  if (!info_dict->GetString(kNewWallpaperLocationNodeName, &location))
+    return false;
+  int layout;
+  if (!info_dict->GetInteger(kNewWallpaperLayoutNodeName, &layout))
+    return false;
+  int type;
+  if (!info_dict->GetInteger(kNewWallpaperTypeNodeName, &type))
+    return false;
+  std::string date_string;
+  if (!info_dict->GetString(kNewWallpaperDateNodeName, &date_string))
+    return false;
+  int64_t date_val;
+  if (!base::StringToInt64(date_string, &date_val))
+    return false;
+
+  info->location = location;
+  info->layout = static_cast<wallpaper::WallpaperLayout>(layout);
+  info->type = static_cast<wallpaper::WallpaperType>(type);
+  info->date = base::Time::FromInternalValue(date_val);
+  return true;
+}
+
+void WallpaperController::RemoveUserWallpaperInfo(const AccountId& account_id,
+                                                  bool is_persistent) {
+  if (wallpaper_cache_map_.find(account_id) != wallpaper_cache_map_.end())
+    wallpaper_cache_map_.erase(account_id);
+
+  PrefService* local_state = Shell::Get()->GetLocalStatePrefService();
+  // Local state can be null in tests.
+  if (!local_state)
+    return;
+  WallpaperInfo info;
+  GetUserWallpaperInfo(account_id, &info, is_persistent);
+  DictionaryPrefUpdate prefs_wallpapers_info_update(local_state,
+                                                    prefs::kUserWallpaperInfo);
+  prefs_wallpapers_info_update->RemoveWithoutPathExpansion(
+      account_id.GetUserEmail(), nullptr);
+  // Remove the color cache of the previous wallpaper if it exists.
+  DictionaryPrefUpdate wallpaper_colors_update(local_state,
+                                               prefs::kWallpaperColors);
+  wallpaper_colors_update->RemoveWithoutPathExpansion(info.location, nullptr);
+}
+
+bool WallpaperController::GetWallpaperFromCache(const AccountId& account_id,
+                                                gfx::ImageSkia* image) {
+  CustomWallpaperMap::const_iterator it = wallpaper_cache_map_.find(account_id);
+  if (it != wallpaper_cache_map_.end() && !it->second.second.isNull()) {
+    *image = it->second.second;
+    return true;
+  }
+  return false;
+}
+
+bool WallpaperController::GetPathFromCache(const AccountId& account_id,
+                                           base::FilePath* path) {
+  CustomWallpaperMap::const_iterator it = wallpaper_cache_map_.find(account_id);
+  if (it != wallpaper_cache_map_.end()) {
+    *path = (*it).second.first;
+    return true;
+  }
+  return false;
+}
+
+wallpaper::WallpaperInfo* WallpaperController::GetCurrentUserWallpaperInfo() {
+  return &current_user_wallpaper_info_;
+}
+
+CustomWallpaperMap* WallpaperController::GetWallpaperCacheMap() {
+  return &wallpaper_cache_map_;
+}
+
 void WallpaperController::SetClient(
     mojom::WallpaperControllerClientPtr client) {
   wallpaper_controller_client_ = std::move(client);
@@ -409,7 +545,28 @@
     const std::string& url,
     wallpaper::WallpaperLayout layout,
     bool show_wallpaper) {
-  NOTIMPLEMENTED();
+  DCHECK(Shell::Get()->session_controller()->IsActiveUserSessionStarted());
+
+  // There is no visible wallpaper in kiosk mode.
+  base::Optional<user_manager::UserType> user_type =
+      Shell::Get()->session_controller()->GetUserType();
+  if (!user_type || *user_type == user_manager::USER_TYPE_KIOSK_APP)
+    return;
+
+  WallpaperInfo info = {url, layout, wallpaper::ONLINE,
+                        base::Time::Now().LocalMidnight()};
+  SetUserWallpaperInfo(user_info->account_id, info, !user_info->is_ephemeral);
+
+  if (show_wallpaper) {
+    // TODO(crbug.com/776464): This should ideally go through PendingWallpaper.
+    SetWallpaper(image, info);
+  }
+
+  // Leave the file path empty, because in most cases the file path is not used
+  // when fetching cache, but in case it needs to be checked, we should avoid
+  // confusing the URL with a real file path.
+  wallpaper_cache_map_[user_info->account_id] = CustomWallpaperElement(
+      base::FilePath(), gfx::ImageSkia::CreateFrom1xBitmap(image));
 }
 
 void WallpaperController::SetDefaultWallpaper(
@@ -440,7 +597,7 @@
 }
 
 void WallpaperController::SetWallpaper(const SkBitmap& wallpaper,
-                                       const wallpaper::WallpaperInfo& info) {
+                                       const WallpaperInfo& info) {
   if (wallpaper.isNull())
     return;
   SetWallpaperImage(gfx::ImageSkia::CreateFrom1xBitmap(wallpaper), info);
@@ -467,6 +624,8 @@
 void WallpaperController::OnColorCalculationComplete() {
   const std::vector<SkColor> colors = color_calculator_->prominent_colors();
   color_calculator_.reset();
+  // TODO(crbug.com/787134): The prominent colors of wallpapers with empty
+  // location should be cached as well.
   if (!current_location_.empty())
     CacheProminentColors(colors, current_location_);
   SetProminentColors(colors);
diff --git a/ash/wallpaper/wallpaper_controller.h b/ash/wallpaper/wallpaper_controller.h
index 80ecc31..e225fb5 100644
--- a/ash/wallpaper/wallpaper_controller.h
+++ b/ash/wallpaper/wallpaper_controller.h
@@ -12,6 +12,7 @@
 #include "ash/public/interfaces/wallpaper.mojom.h"
 #include "ash/session/session_observer.h"
 #include "ash/shell_observer.h"
+#include "base/files/file_path.h"
 #include "base/memory/ref_counted.h"
 #include "base/observer_list.h"
 #include "base/timer/timer.h"
@@ -21,6 +22,7 @@
 #include "mojo/public/cpp/bindings/binding_set.h"
 #include "mojo/public/cpp/bindings/interface_ptr_set.h"
 #include "ui/compositor/compositor_lock.h"
+#include "ui/gfx/image/image_skia.h"
 
 class PrefRegistrySimple;
 
@@ -41,6 +43,12 @@
 
 class WallpaperControllerObserver;
 
+// The |CustomWallpaperElement| contains |first| the path of the image which
+// is currently being loaded and or in progress of being loaded and |second|
+// the image itself.
+using CustomWallpaperElement = std::pair<base::FilePath, gfx::ImageSkia>;
+using CustomWallpaperMap = std::map<AccountId, CustomWallpaperElement>;
+
 // Controls the desktop background wallpaper:
 //   - Sets a wallpaper image and layout;
 //   - Handles display change (add/remove display, configuration change etc);
@@ -137,6 +145,39 @@
   // Returns whether the current wallpaper is blurred.
   bool IsWallpaperBlurred() const { return is_wallpaper_blurred_; }
 
+  // TODO(crbug.com/776464): Make this private. WallpaperInfo should be an
+  // internal concept. In addition, change |is_persistent| to |is_ephemeral| to
+  // be consistent with |mojom::WallpaperUserInfo|.
+  // Sets wallpaper info for |account_id| and saves it to local state if
+  // |is_persistent| is true.
+  void SetUserWallpaperInfo(const AccountId& account_id,
+                            const wallpaper::WallpaperInfo& info,
+                            bool is_persistent);
+
+  // Gets wallpaper info of |account_id| from local state, or memory if
+  // |is_persistent| is false. Returns false if wallpaper info is not found.
+  bool GetUserWallpaperInfo(const AccountId& account_id,
+                            wallpaper::WallpaperInfo* info,
+                            bool is_persistent);
+
+  // Removes |account_id|'s wallpaper info and color cache if it exists.
+  void RemoveUserWallpaperInfo(const AccountId& account_id, bool is_persistent);
+
+  // Gets encoded wallpaper from cache. Returns true if success.
+  bool GetWallpaperFromCache(const AccountId& account_id,
+                             gfx::ImageSkia* image);
+
+  // Gets path of encoded wallpaper from cache. Returns true if success.
+  bool GetPathFromCache(const AccountId& account_id, base::FilePath* path);
+
+  // TODO(crbug.com/776464): Remove this after WallpaperManager is removed.
+  // Returns the pointer of |current_user_wallpaper_info_|.
+  wallpaper::WallpaperInfo* GetCurrentUserWallpaperInfo();
+
+  // TODO(crbug.com/776464): Remove this after WallpaperManager is removed.
+  // Returns the pointer of |wallpaper_cache_map_|.
+  CustomWallpaperMap* GetWallpaperCacheMap();
+
   // mojom::WallpaperController overrides:
   void SetClient(mojom::WallpaperControllerClientPtr client) override;
   void SetCustomWallpaper(mojom::WallpaperUserInfoPtr user_info,
@@ -254,6 +295,12 @@
   // Caches the color profiles that need to do wallpaper color extracting.
   const std::vector<color_utils::ColorProfile> color_profiles_;
 
+  // Cached logged-in user wallpaper info.
+  wallpaper::WallpaperInfo current_user_wallpaper_info_;
+
+  // Cached wallpapers of users.
+  CustomWallpaperMap wallpaper_cache_map_;
+
   // Location (see WallpaperInfo::location) used by the current wallpaper.
   // Used as a key for storing |prominent_colors_| in the
   // wallpaper::kWallpaperColors pref. An empty string disables color caching.
@@ -273,6 +320,9 @@
 
   std::unique_ptr<ui::CompositorLock> compositor_lock_;
 
+  // Tracks how many wallpapers have been set, for testing purpose.
+  int wallpaper_count_for_testing_ = 0;
+
   DISALLOW_COPY_AND_ASSIGN(WallpaperController);
 };
 
diff --git a/ash/wallpaper/wallpaper_controller_unittest.cc b/ash/wallpaper/wallpaper_controller_unittest.cc
index f8745778..ee12d9a 100644
--- a/ash/wallpaper/wallpaper_controller_unittest.cc
+++ b/ash/wallpaper/wallpaper_controller_unittest.cc
@@ -32,6 +32,7 @@
 #include "ui/gfx/canvas.h"
 #include "ui/views/widget/widget.h"
 
+using wallpaper::WallpaperLayout;
 using wallpaper::WALLPAPER_LAYOUT_CENTER;
 using wallpaper::WALLPAPER_LAYOUT_CENTER_CROPPED;
 using wallpaper::WALLPAPER_LAYOUT_STRETCH;
@@ -240,15 +241,36 @@
 
   // Helper function to create a |WallpaperInfo| struct with dummy values
   // given the desired layout.
-  wallpaper::WallpaperInfo CreateWallpaperInfo(
-      wallpaper::WallpaperLayout layout) {
+  wallpaper::WallpaperInfo CreateWallpaperInfo(WallpaperLayout layout) {
     return wallpaper::WallpaperInfo("", layout, wallpaper::DEFAULT,
                                     base::Time::Now().LocalMidnight());
   }
 
+  // Helper function to create a new |mojom::WallpaperUserInfoPtr| instance with
+  // default values. In addition, remove the previous wallpaper info (if any)
+  // and clear the wallpaper count. May be called multiple times for the
+  // same |account_id|.
+  mojom::WallpaperUserInfoPtr InitializeUser(const AccountId& account_id) {
+    mojom::WallpaperUserInfoPtr wallpaper_user_info =
+        mojom::WallpaperUserInfo::New();
+    wallpaper_user_info->account_id = account_id;
+    wallpaper_user_info->type = user_manager::USER_TYPE_REGULAR;
+    wallpaper_user_info->is_ephemeral = false;
+    wallpaper_user_info->has_gaia_account = true;
+
+    controller_->RemoveUserWallpaperInfo(account_id,
+                                         !wallpaper_user_info->is_ephemeral);
+    controller_->current_user_wallpaper_info_ = wallpaper::WallpaperInfo();
+    controller_->wallpaper_count_for_testing_ = 0;
+
+    return wallpaper_user_info;
+  }
+
   // Wrapper for private ShouldCalculateColors()
   bool ShouldCalculateColors() { return controller_->ShouldCalculateColors(); }
 
+  int GetWallpaperCount() { return controller_->wallpaper_count_for_testing_; }
+
   WallpaperController* controller_;  // Not owned.
 
   TestWallpaperDelegate* wallpaper_delegate_;
@@ -592,4 +614,99 @@
   EXPECT_EQ(2, observer.wallpaper_colors_changed_count());
 }
 
+TEST_F(WallpaperControllerTest, SetOnlineWallpaper) {
+  gfx::ImageSkia image = CreateImage(640, 480, kCustomWallpaperColor);
+  const std::string url = "dummy_url";
+  const std::string user_email = "user@test.com";
+  const AccountId account_id = AccountId::FromUserEmail(user_email);
+  WallpaperLayout layout = WALLPAPER_LAYOUT_CENTER;
+
+  SimulateUserLogin(user_email);
+
+  // Verify the wallpaper is set successfully and wallpaper info is updated.
+  mojom::WallpaperUserInfoPtr wallpaper_user_info = InitializeUser(account_id);
+  controller_->SetOnlineWallpaper(std::move(wallpaper_user_info),
+                                  *image.bitmap(), url, layout,
+                                  true /* show_wallpaper */);
+  RunAllTasksUntilIdle();
+  EXPECT_EQ(1, GetWallpaperCount());
+  wallpaper::WallpaperInfo wallpaper_info;
+  EXPECT_TRUE(controller_->GetUserWallpaperInfo(account_id, &wallpaper_info,
+                                                true /* is_persistent */));
+  wallpaper::WallpaperInfo expected_wallpaper_info(
+      url, layout, wallpaper::ONLINE, base::Time::Now().LocalMidnight());
+  EXPECT_EQ(wallpaper_info, expected_wallpaper_info);
+
+  // Verify that the wallpaper is not set when |show_wallpaper| is false, but
+  // wallpaper info is updated properly.
+  wallpaper_user_info = InitializeUser(account_id);
+  controller_->SetOnlineWallpaper(std::move(wallpaper_user_info),
+                                  *image.bitmap(), url, layout,
+                                  false /* show_wallpaper */);
+  RunAllTasksUntilIdle();
+  EXPECT_EQ(0, GetWallpaperCount());
+  EXPECT_TRUE(controller_->GetUserWallpaperInfo(account_id, &wallpaper_info,
+                                                true /* is_persistent */));
+  EXPECT_EQ(wallpaper_info, expected_wallpaper_info);
+}
+
+TEST_F(WallpaperControllerTest, IgnoreWallpaperRequestInKioskMode) {
+  gfx::ImageSkia image = CreateImage(640, 480, kCustomWallpaperColor);
+  const std::string kiosk_app = "kiosk";
+  const AccountId account_id = AccountId::FromUserEmail("user@test.com");
+
+  // Simulate kiosk login.
+  TestSessionControllerClient* session = GetSessionControllerClient();
+  session->AddUserSession(kiosk_app, user_manager::USER_TYPE_KIOSK_APP);
+  session->SwitchActiveUser(AccountId::FromUserEmail(kiosk_app));
+  session->SetSessionState(SessionState::ACTIVE);
+
+  // Verify that the wallpaper request is ignored in kiosk mode, and
+  // |account_id|'s wallpaper info is not updated.
+  controller_->SetOnlineWallpaper(InitializeUser(account_id), *image.bitmap(),
+                                  "dummy_url", WALLPAPER_LAYOUT_CENTER,
+                                  true /* show_wallpaper */);
+  RunAllTasksUntilIdle();
+  EXPECT_EQ(0, GetWallpaperCount());
+  wallpaper::WallpaperInfo wallpaper_info;
+  EXPECT_FALSE(controller_->GetUserWallpaperInfo(account_id, &wallpaper_info,
+                                                 true /* is_persistent */));
+}
+
+TEST_F(WallpaperControllerTest, VerifyWallpaperCache) {
+  gfx::ImageSkia image = CreateImage(640, 480, kCustomWallpaperColor);
+  const std::string user1 = "user1@test.com";
+
+  SimulateUserLogin(user1);
+
+  // |user1| doesn't have wallpaper cache in the beginning.
+  gfx::ImageSkia cached_wallpaper;
+  EXPECT_FALSE(controller_->GetWallpaperFromCache(
+      AccountId::FromUserEmail(user1), &cached_wallpaper));
+  base::FilePath path;
+  EXPECT_FALSE(
+      controller_->GetPathFromCache(AccountId::FromUserEmail(user1), &path));
+
+  // Verify |SetOnlineWallpaper| updates wallpaper cache for |user1|.
+  mojom::WallpaperUserInfoPtr wallpaper_user_info =
+      InitializeUser(AccountId::FromUserEmail(user1));
+  controller_->SetOnlineWallpaper(
+      std::move(wallpaper_user_info), *image.bitmap(), "dummy_file_location",
+      WALLPAPER_LAYOUT_CENTER, true /* show_wallpaper */);
+  RunAllTasksUntilIdle();
+  EXPECT_TRUE(controller_->GetWallpaperFromCache(
+      AccountId::FromUserEmail(user1), &cached_wallpaper));
+  EXPECT_TRUE(
+      controller_->GetPathFromCache(AccountId::FromUserEmail(user1), &path));
+
+  // After |user2| is logged in, |user1|'s wallpaper cache should still be kept
+  // (crbug.com/339576). Note the active user is still |user1|.
+  TestSessionControllerClient* session = GetSessionControllerClient();
+  session->AddUserSession("user2@test.com");
+  EXPECT_TRUE(controller_->GetWallpaperFromCache(
+      AccountId::FromUserEmail(user1), &cached_wallpaper));
+  EXPECT_TRUE(
+      controller_->GetPathFromCache(AccountId::FromUserEmail(user1), &path));
+}
+
 }  // namespace ash
diff --git a/chrome/browser/android/vr_shell/BUILD.gn b/chrome/browser/android/vr_shell/BUILD.gn
index 4e51490..00febf3 100644
--- a/chrome/browser/android/vr_shell/BUILD.gn
+++ b/chrome/browser/android/vr_shell/BUILD.gn
@@ -20,6 +20,8 @@
     "autocomplete_controller.cc",
     "autocomplete_controller.h",
     "gl_browser_interface.h",
+    "gvr_keyboard_delegate.cc",
+    "gvr_keyboard_delegate.h",
     "gvr_keyboard_shim.cc",
     "gvr_util.cc",
     "gvr_util.h",
diff --git a/chrome/browser/android/vr_shell/gvr_keyboard_delegate.cc b/chrome/browser/android/vr_shell/gvr_keyboard_delegate.cc
new file mode 100644
index 0000000..a4467b3
--- /dev/null
+++ b/chrome/browser/android/vr_shell/gvr_keyboard_delegate.cc
@@ -0,0 +1,183 @@
+// 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 "chrome/browser/android/vr_shell/gvr_keyboard_delegate.h"
+
+#include "base/bind.h"
+#include "base/memory/ptr_util.h"
+#include "base/strings/utf_string_conversions.h"
+#include "chrome/browser/android/vr_shell/gvr_util.h"
+#include "chrome/browser/vr/model/camera_model.h"
+#include "chrome/browser/vr/model/text_input_info.h"
+#include "third_party/gvr-android-sdk/src/libraries/headers/vr/gvr/capi/include/gvr.h"
+#include "third_party/gvr-android-sdk/src/libraries/headers/vr/gvr/capi/include/gvr_types.h"
+#include "ui/gfx/geometry/point3_f.h"
+#include "ui/gfx/geometry/vector3d_f.h"
+
+namespace vr_shell {
+
+namespace {
+
+void OnKeyboardEvent(void* closure, GvrKeyboardDelegate::EventType event) {
+  auto* callback =
+      reinterpret_cast<GvrKeyboardDelegate::OnEventCallback*>(closure);
+  if (callback)
+    callback->Run(event);
+}
+
+}  // namespace
+
+std::unique_ptr<GvrKeyboardDelegate> GvrKeyboardDelegate::Create() {
+  auto delegate = base::WrapUnique(new GvrKeyboardDelegate());
+  void* callback = reinterpret_cast<void*>(&delegate->keyboard_event_callback_);
+  auto* gvr_keyboard = gvr_keyboard_create(callback, OnKeyboardEvent);
+  if (!gvr_keyboard)
+    return nullptr;
+  delegate->Init(gvr_keyboard);
+  return delegate;
+}
+
+GvrKeyboardDelegate::GvrKeyboardDelegate() {
+  keyboard_event_callback_ = base::BindRepeating(
+      &GvrKeyboardDelegate::OnGvrKeyboardEvent, base::Unretained(this));
+}
+
+void GvrKeyboardDelegate::Init(gvr_keyboard_context* keyboard_context) {
+  DCHECK(keyboard_context);
+  gvr_keyboard_ = keyboard_context;
+
+  gvr_mat4f matrix;
+  gvr_keyboard_get_recommended_world_from_keyboard_matrix(2.0f, &matrix);
+  gvr_keyboard_set_world_from_keyboard_matrix(gvr_keyboard_, &matrix);
+}
+
+GvrKeyboardDelegate::~GvrKeyboardDelegate() {
+  if (gvr_keyboard_)
+    gvr_keyboard_destroy(&gvr_keyboard_);
+}
+
+void GvrKeyboardDelegate::SetController(VrController* controller) {
+  controller_ = controller;
+}
+
+void GvrKeyboardDelegate::SetUiInterface(vr::KeyboardUiInterface* ui) {
+  ui_ = ui;
+}
+
+void GvrKeyboardDelegate::OnBeginFrame() {
+  gvr::ClockTimePoint target_time = gvr::GvrApi::GetTimePointNow();
+  gvr_keyboard_set_frame_time(gvr_keyboard_, &target_time);
+  gvr_keyboard_advance_frame(gvr_keyboard_);
+
+  if (controller_) {
+    bool pressed = controller_->ButtonUpHappened(
+        gvr::ControllerButton::GVR_CONTROLLER_BUTTON_CLICK);
+    gvr_keyboard_update_button_state(
+        gvr_keyboard_, gvr::ControllerButton::GVR_CONTROLLER_BUTTON_CLICK,
+        pressed);
+  }
+}
+
+void GvrKeyboardDelegate::ShowKeyboard() {
+  gvr_keyboard_show(gvr_keyboard_);
+}
+
+void GvrKeyboardDelegate::HideKeyboard() {
+  gvr_keyboard_hide(gvr_keyboard_);
+}
+
+void GvrKeyboardDelegate::SetTransform(const gfx::Transform& transform) {
+  gvr_mat4f matrix;
+  TransformToGvrMat(transform, &matrix);
+  gvr_keyboard_set_world_from_keyboard_matrix(gvr_keyboard_, &matrix);
+}
+
+bool GvrKeyboardDelegate::HitTest(const gfx::Point3F& ray_origin,
+                                  const gfx::Point3F& ray_target,
+                                  gfx::Point3F* hit_position) {
+  gvr_vec3f start;
+  start.x = ray_origin.x();
+  start.y = ray_origin.y();
+  start.z = ray_origin.z();
+  gvr_vec3f end;
+  end.x = ray_target.x();
+  end.y = ray_target.y();
+  end.z = ray_target.z();
+  gvr_vec3f hit_point;
+  bool hits = gvr_keyboard_update_controller_ray(gvr_keyboard_, &start, &end,
+                                                 &hit_point);
+  if (hits)
+    hit_position->SetPoint(hit_point.x, hit_point.y, hit_point.z);
+  return hits;
+}
+
+void GvrKeyboardDelegate::Draw(const vr::CameraModel& model) {
+  int eye = model.eye_type;
+  gvr::Mat4f view_matrix;
+  TransformToGvrMat(model.view_matrix, &view_matrix);
+  gvr_keyboard_set_eye_from_world_matrix(gvr_keyboard_, eye, &view_matrix);
+
+  gvr::Mat4f proj_matrix;
+  TransformToGvrMat(model.proj_matrix, &proj_matrix);
+  gvr_keyboard_set_projection_matrix(gvr_keyboard_, eye, &proj_matrix);
+
+  gfx::Rect viewport_rect = model.viewport;
+  const gvr::Recti viewport = {viewport_rect.x(), viewport_rect.right(),
+                               viewport_rect.y(), viewport_rect.bottom()};
+  gvr_keyboard_set_viewport(gvr_keyboard_, eye, &viewport);
+  gvr_keyboard_render(gvr_keyboard_, eye);
+}
+
+void GvrKeyboardDelegate::UpdateInput(const vr::TextInputInfo& info) {
+  gvr_keyboard_set_text(gvr_keyboard_, base::UTF16ToUTF8(info.text).c_str());
+  gvr_keyboard_set_selection_indices(gvr_keyboard_, info.selection_start,
+                                     info.selection_end);
+}
+
+void GvrKeyboardDelegate::OnGvrKeyboardEvent(EventType event) {
+  DCHECK(ui_ != nullptr);
+  switch (event) {
+    case GVR_KEYBOARD_ERROR_UNKNOWN:
+      LOG(ERROR) << "Unknown GVR keyboard error.";
+      break;
+    case GVR_KEYBOARD_ERROR_SERVICE_NOT_CONNECTED:
+      LOG(ERROR) << "GVR keyboard service not connected.";
+      break;
+    case GVR_KEYBOARD_ERROR_NO_LOCALES_FOUND:
+      LOG(ERROR) << "No GVR keyboard locales found.";
+      break;
+    case GVR_KEYBOARD_ERROR_SDK_LOAD_FAILED:
+      LOG(ERROR) << "GVR keyboard sdk load failed.";
+      break;
+    case GVR_KEYBOARD_SHOWN:
+      break;
+    case GVR_KEYBOARD_HIDDEN:
+      ui_->OnKeyboardHidden();
+      break;
+    case GVR_KEYBOARD_TEXT_UPDATED:
+      ui_->OnInputEdited(GetTextInfo());
+      break;
+    case GVR_KEYBOARD_TEXT_COMMITTED:
+      ui_->OnInputCommitted(GetTextInfo());
+      break;
+  }
+}
+
+vr::TextInputInfo GvrKeyboardDelegate::GetTextInfo() {
+  vr::TextInputInfo info;
+  // Get text. Note that we wrap the text in a unique ptr since we're
+  // responsible for freeing the memory allocated by gvr_keyboard_get_text.
+  std::unique_ptr<char, decltype(std::free)*> scoped_text{
+      gvr_keyboard_get_text(gvr_keyboard_), std::free};
+  std::string text(scoped_text.get());
+  info.text = base::UTF8ToUTF16(text);
+  // Get selection indices.
+  size_t start, end;
+  gvr_keyboard_get_selection_indices(gvr_keyboard_, &start, &end);
+  info.selection_start = start;
+  info.selection_end = end;
+  return info;
+}
+
+}  // namespace vr_shell
diff --git a/chrome/browser/android/vr_shell/gvr_keyboard_delegate.h b/chrome/browser/android/vr_shell/gvr_keyboard_delegate.h
new file mode 100644
index 0000000..7a3ffeff
--- /dev/null
+++ b/chrome/browser/android/vr_shell/gvr_keyboard_delegate.h
@@ -0,0 +1,63 @@
+// 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.
+
+#ifndef CHROME_BROWSER_ANDROID_VR_SHELL_GVR_KEYBOARD_DELEGATE_H_
+#define CHROME_BROWSER_ANDROID_VR_SHELL_GVR_KEYBOARD_DELEGATE_H_
+
+#include "base/callback.h"
+#include "base/macros.h"
+#include "chrome/browser/android/vr_shell/vr_controller.h"
+#include "chrome/browser/vr/keyboard_delegate.h"
+#include "chrome/browser/vr/keyboard_ui_interface.h"
+#include "third_party/gvr-android-keyboard/src/libraries/headers/vr/gvr/capi/include/gvr_keyboard.h"
+
+namespace vr {
+struct TextInputInfo;
+}
+
+namespace vr_shell {
+
+class GvrKeyboardDelegate : public vr::KeyboardDelegate {
+ public:
+  // Constructs a GvrKeyboardDelegate by dynamically loading the GVR keyboard
+  // api. A null pointer is returned upon failure.
+  static std::unique_ptr<GvrKeyboardDelegate> Create();
+  ~GvrKeyboardDelegate() override;
+
+  void SetController(VrController* controller);
+  void SetUiInterface(vr::KeyboardUiInterface* ui);
+
+  typedef int32_t EventType;
+  typedef base::RepeatingCallback<void(EventType)> OnEventCallback;
+
+  // vr::KeyboardDelegate implementation.
+  void OnBeginFrame() override;
+  void ShowKeyboard() override;
+  void HideKeyboard() override;
+  void SetTransform(const gfx::Transform& transform) override;
+  bool HitTest(const gfx::Point3F& ray_origin,
+               const gfx::Point3F& ray_target,
+               gfx::Point3F* hit_position) override;
+  void Draw(const vr::CameraModel& model) override;
+
+  // Called to update GVR keyboard with the given text input info.
+  void UpdateInput(const vr::TextInputInfo& info);
+
+ private:
+  GvrKeyboardDelegate();
+  void Init(gvr_keyboard_context* keyboard_context);
+  void OnGvrKeyboardEvent(EventType);
+  vr::TextInputInfo GetTextInfo();
+
+  VrController* controller_;
+  vr::KeyboardUiInterface* ui_;
+  gvr_keyboard_context* gvr_keyboard_ = nullptr;
+  OnEventCallback keyboard_event_callback_;
+
+  DISALLOW_COPY_AND_ASSIGN(GvrKeyboardDelegate);
+};
+
+}  // namespace vr_shell
+
+#endif  // CHROME_BROWSER_ANDROID_VR_SHELL_GVR_KEYBOARD_DELEGATE_H_
diff --git a/chrome/browser/android/vr_shell/gvr_util.cc b/chrome/browser/android/vr_shell/gvr_util.cc
index e78198e..d90d2b0 100644
--- a/chrome/browser/android/vr_shell/gvr_util.cc
+++ b/chrome/browser/android/vr_shell/gvr_util.cc
@@ -112,4 +112,20 @@
       gvr::Rectf{left_degrees, right_degrees, bottom_degrees, top_degrees};
 }
 
+void TransformToGvrMat(const gfx::Transform& in, gvr::Mat4f* out) {
+  for (int i = 0; i < 4; ++i) {
+    for (int j = 0; j < 4; ++j) {
+      out->m[i][j] = in.matrix().get(i, j);
+    }
+  }
+}
+
+void GvrMatToTransform(const gvr::Mat4f& in, gfx::Transform* out) {
+  for (int i = 0; i < 4; ++i) {
+    for (int j = 0; j < 4; ++j) {
+      out->matrix().set(i, j, in.m[i][j]);
+    }
+  }
+}
+
 }  // namespace vr_shell
diff --git a/chrome/browser/android/vr_shell/gvr_util.h b/chrome/browser/android/vr_shell/gvr_util.h
index ef28479..535c6e0 100644
--- a/chrome/browser/android/vr_shell/gvr_util.h
+++ b/chrome/browser/android/vr_shell/gvr_util.h
@@ -39,6 +39,12 @@
                    float z_near,
                    gvr::Rectf* out_fov);
 
+// Transforms the given gfx::Transform to gvr::Mat4f.
+void TransformToGvrMat(const gfx::Transform& in, gvr::Mat4f* out);
+
+// Transforms the given vr::Mat4f to gfx::Transform.
+void GvrMatToTransform(const gvr::Mat4f& in, gfx::Transform* out);
+
 }  // namespace vr_shell
 
 #endif  // CHROME_BROWSER_ANDROID_VR_SHELL_GVR_UTIL_H_
diff --git a/chrome/browser/android/vr_shell/vr_gl_thread.cc b/chrome/browser/android/vr_shell/vr_gl_thread.cc
index 13eed5e..6bf7d72c 100644
--- a/chrome/browser/android/vr_shell/vr_gl_thread.cc
+++ b/chrome/browser/android/vr_shell/vr_gl_thread.cc
@@ -14,6 +14,7 @@
 #include "chrome/browser/vr/model/omnibox_suggestions.h"
 #include "chrome/browser/vr/model/toolbar_state.h"
 #include "chrome/browser/vr/ui.h"
+#include "chrome/common/chrome_features.h"
 #include "third_party/skia/include/core/SkBitmap.h"
 
 namespace vr_shell {
@@ -42,10 +43,31 @@
 }
 
 void VrGLThread::Init() {
-  auto ui = base::MakeUnique<vr::Ui>(this, this, ui_initial_state_);
+  bool keyboard_enabled =
+      base::FeatureList::IsEnabled(features::kVrBrowserKeyboard);
+  if (keyboard_enabled) {
+    keyboard_delegate_ = GvrKeyboardDelegate::Create();
+    text_input_delegate_ = base::MakeUnique<vr::TextInputDelegate>();
+  }
+  auto* keyboard_delegate =
+      !keyboard_delegate_ ? nullptr : keyboard_delegate_.get();
+  auto ui =
+      base::MakeUnique<vr::Ui>(this, this, keyboard_delegate,
+                               text_input_delegate_.get(), ui_initial_state_);
+  if (keyboard_enabled) {
+    text_input_delegate_->SetRequestFocusCallback(
+        base::BindRepeating(&vr::Ui::RequestFocus, base::Unretained(ui.get())));
+    if (keyboard_delegate) {
+      keyboard_delegate_->SetUiInterface(ui.get());
+      text_input_delegate_->SetUpdateInputCallback(
+          base::BindRepeating(&GvrKeyboardDelegate::UpdateInput,
+                              base::Unretained(keyboard_delegate_.get())));
+    }
+  }
+
   vr_shell_gl_ = base::MakeUnique<VrShellGl>(
       this, std::move(ui), gvr_api_, reprojected_rendering_, daydream_support_,
-      ui_initial_state_.in_web_vr);
+      ui_initial_state_.in_web_vr, keyboard_delegate_.get());
 
   browser_ui_ = vr_shell_gl_->GetBrowserUiWeakPtr();
 
diff --git a/chrome/browser/android/vr_shell/vr_gl_thread.h b/chrome/browser/android/vr_shell/vr_gl_thread.h
index 6becaed..908280a 100644
--- a/chrome/browser/android/vr_shell/vr_gl_thread.h
+++ b/chrome/browser/android/vr_shell/vr_gl_thread.h
@@ -12,8 +12,10 @@
 #include "base/memory/weak_ptr.h"
 #include "base/single_thread_task_runner.h"
 #include "chrome/browser/android/vr_shell/gl_browser_interface.h"
+#include "chrome/browser/android/vr_shell/gvr_keyboard_delegate.h"
 #include "chrome/browser/vr/browser_ui_interface.h"
 #include "chrome/browser/vr/content_input_delegate.h"
+#include "chrome/browser/vr/text_input_delegate.h"
 #include "chrome/browser/vr/ui.h"
 #include "chrome/browser/vr/ui_browser_interface.h"
 #include "third_party/gvr-android-sdk/src/libraries/headers/vr/gvr/capi/include/gvr_types.h"
@@ -101,6 +103,8 @@
 
   // Created on GL thread.
   std::unique_ptr<VrShellGl> vr_shell_gl_;
+  std::unique_ptr<GvrKeyboardDelegate> keyboard_delegate_;
+  std::unique_ptr<vr::TextInputDelegate> text_input_delegate_;
 
   base::WeakPtr<VrShell> weak_vr_shell_;
   base::WeakPtr<vr::BrowserUiInterface> browser_ui_;
diff --git a/chrome/browser/android/vr_shell/vr_shell_gl.cc b/chrome/browser/android/vr_shell/vr_shell_gl.cc
index 86d5647..a7f6aac 100644
--- a/chrome/browser/android/vr_shell/vr_shell_gl.cc
+++ b/chrome/browser/android/vr_shell/vr_shell_gl.cc
@@ -103,46 +103,6 @@
 
 static constexpr float kRedrawSceneAngleDeltaDegrees = 1.0;
 
-static gvr_keyboard_context* keyboard_context;
-
-// TODO(ymalik,crbug.com/780318): This callback is temporary until we have an
-// editable input field.
-void OnKeyboardEvent(void*, int32_t event) {
-  switch (event) {
-    case GVR_KEYBOARD_ERROR_UNKNOWN:
-      LOG(ERROR) << "Unknown GVR keyboard error.";
-      break;
-    case GVR_KEYBOARD_ERROR_SERVICE_NOT_CONNECTED:
-      LOG(ERROR) << "GVR keyboard service not connected.";
-      break;
-    case GVR_KEYBOARD_ERROR_NO_LOCALES_FOUND:
-      LOG(ERROR) << "No GVR keyboard locales found.";
-      break;
-    case GVR_KEYBOARD_ERROR_SDK_LOAD_FAILED:
-      LOG(ERROR) << "GVR keyboard sdk load failed.";
-      break;
-    case GVR_KEYBOARD_SHOWN:
-      DVLOG(1) << "GVR keyboard shown.";
-      break;
-    case GVR_KEYBOARD_HIDDEN:
-      DVLOG(1) << "GVR keyboard hidden.";
-      break;
-    case GVR_KEYBOARD_TEXT_UPDATED: {
-      char* text = gvr_keyboard_get_text(keyboard_context);
-      DVLOG(1) << "GVR keyboard text updated: " << text;
-      free(reinterpret_cast<void*>(text));
-    } break;
-    case GVR_KEYBOARD_TEXT_COMMITTED: {
-      char* text = gvr_keyboard_get_text(keyboard_context);
-      DVLOG(1) << "GVR keyboard text updated: " << text;
-      free(reinterpret_cast<void*>(text));
-      gvr_keyboard_set_text(keyboard_context, "");
-    } break;
-    default:
-      NOTREACHED();
-  }
-}
-
 gfx::Transform PerspectiveMatrixFromView(const gvr::Rectf& fov,
                                          float z_near,
                                          float z_far) {
@@ -175,22 +135,6 @@
   return result;
 }
 
-void TransformToGvrMat(const gfx::Transform& in, gvr::Mat4f* out) {
-  for (int i = 0; i < 4; ++i) {
-    for (int j = 0; j < 4; ++j) {
-      out->m[i][j] = in.matrix().get(i, j);
-    }
-  }
-}
-
-void GvrMatToTransform(const gvr::Mat4f& in, gfx::Transform* out) {
-  for (int i = 0; i < 4; ++i) {
-    for (int j = 0; j < 4; ++j) {
-      out->matrix().set(i, j, in.m[i][j]);
-    }
-  }
-}
-
 gvr::Rectf UVFromGfxRect(gfx::RectF rect) {
   return {rect.x(), rect.x() + rect.width(), 1.0f - rect.bottom(),
           1.0f - rect.y()};
@@ -219,7 +163,8 @@
                      gvr_context* gvr_api,
                      bool reprojected_rendering,
                      bool daydream_support,
-                     bool start_in_web_vr_mode)
+                     bool start_in_web_vr_mode,
+                     GvrKeyboardDelegate* keyboard_delegate)
     : ui_(std::move(ui)),
       web_vr_mode_(start_in_web_vr_mode),
       surfaceless_rendering_(reprojected_rendering),
@@ -227,6 +172,7 @@
       task_runner_(base::ThreadTaskRunnerHandle::Get()),
       binding_(this),
       browser_(browser_interface),
+      keyboard_delegate_(keyboard_delegate),
       fps_meter_(new vr::FPSMeter()),
       webvr_js_time_(new vr::SlidingTimeDeltaAverage(kWebVRSlidingAverageSize)),
       webvr_render_time_(
@@ -237,9 +183,6 @@
 
 VrShellGl::~VrShellGl() {
   ClosePresentationBindings();
-  if (keyboard_enabled_) {
-    gvr_keyboard_destroy(&gvr_keyboard_);
-  }
 }
 
 void VrShellGl::Initialize() {
@@ -501,6 +444,8 @@
   gvr_api_ = gvr::GvrApi::WrapNonOwned(gvr_api);
   controller_.reset(new VrController(gvr_api));
   ui_->OnPlatformControllerInitialized(controller_.get());
+  if (keyboard_delegate_)
+    keyboard_delegate_->SetController(controller_.get());
 
   VrMetricsUtil::LogVrViewerType(gvr_api_->GetViewerType());
 
@@ -513,7 +458,6 @@
 
 void VrShellGl::InitializeRenderer() {
   gvr_api_->InitializeGl();
-  CreateKeyboard();
   gfx::Transform head_pose;
   device::GvrDelegate::GetGvrPoseWithNeckModel(gvr_api_.get(), &head_pose);
   webvr_head_pose_.assign(kPoseRingBufferSize, head_pose);
@@ -695,18 +639,6 @@
 
   if (controller_->ButtonUpHappened(
           gvr::ControllerButton::GVR_CONTROLLER_BUTTON_APP)) {
-    // TODO(ymalik,crbug.com/780318): We temporarily show and hide the keyboard
-    // when the app button is pressed. This behavior is behind a runtime enabled
-    // feature and should go away as soon as we have editable input fields.
-    show_keyboard_ = keyboard_enabled_ && !show_keyboard_;
-    if (keyboard_enabled_) {
-      if (show_keyboard_) {
-        gvr_keyboard_show(gvr_keyboard_);
-      } else {
-        gvr_keyboard_hide(gvr_keyboard_);
-      }
-    }
-
     // A gesture is a movement of the controller while holding the App button.
     // If the angle of the movement is within a threshold, the action is
     // considered a regular click
@@ -955,10 +887,6 @@
   // while the splash screen is up ShouldDrawWebVr() will return false.
   if (!ShouldDrawWebVr()) {
     ui_->ui_renderer()->Draw(render_info_primary_);
-
-    // Draw keyboard. TODO(ymalik,crbug.com/780135): Keyboard should be a UI
-    // element and this special rendering logic should move out of here.
-    DrawKeyboard();
   }
 
   content_frame_available_ = false;
@@ -1039,77 +967,6 @@
   }
 }
 
-void VrShellGl::CreateKeyboard() {
-  if (gvr_keyboard_)
-    return;
-
-  keyboard_enabled_ =
-      base::FeatureList::IsEnabled(features::kVrBrowserKeyboard);
-  if (!keyboard_enabled_)
-    return;
-
-  gvr_keyboard_ = gvr_keyboard_create(nullptr, OnKeyboardEvent);
-  if (!gvr_keyboard_) {
-    keyboard_enabled_ = false;
-    return;
-  }
-  keyboard_context = gvr_keyboard_;
-
-  gvr_mat4f matrix;
-  gvr_keyboard_get_recommended_world_from_keyboard_matrix(2.0f, &matrix);
-  gvr_keyboard_set_world_from_keyboard_matrix(gvr_keyboard_, &matrix);
-}
-
-void VrShellGl::DrawKeyboard() {
-  if (!keyboard_enabled_)
-    return;
-
-  // Note that according to the keyboard API, these functions must be called
-  // every frame after the keyboard is created to process events, regardless of
-  // keyboard visibility.
-  gvr::ClockTimePoint target_time = gvr::GvrApi::GetTimePointNow();
-  gvr_keyboard_set_frame_time(gvr_keyboard_, &target_time);
-  gvr_keyboard_advance_frame(gvr_keyboard_);
-
-  if (!show_keyboard_)
-    return;
-
-  bool pressed = controller_->ButtonUpHappened(
-      gvr::ControllerButton::GVR_CONTROLLER_BUTTON_CLICK);
-  gvr_keyboard_update_button_state(
-      gvr_keyboard_, gvr::ControllerButton::GVR_CONTROLLER_BUTTON_CLICK,
-      pressed);
-
-  gvr_vec3f start;
-  start.x = controller_model_.laser_origin.x();
-  start.y = controller_model_.laser_origin.y();
-  start.z = controller_model_.laser_origin.z();
-  gvr_vec3f end;
-  end.x = start.x + controller_model_.laser_direction.x();
-  end.y = start.y + controller_model_.laser_direction.y();
-  end.z = start.z + controller_model_.laser_direction.z();
-  gvr_vec3f hit_point;
-  gvr_keyboard_update_controller_ray(gvr_keyboard_, &start, &end, &hit_point);
-  for (auto eye : {GVR_LEFT_EYE, GVR_RIGHT_EYE}) {
-    vr::CameraModel& eye_info = (eye == GVR_LEFT_EYE)
-                                    ? render_info_primary_.left_eye_model
-                                    : render_info_primary_.right_eye_model;
-    gvr::Mat4f view_matrix;
-    TransformToGvrMat(eye_info.view_matrix, &view_matrix);
-    gvr_keyboard_set_eye_from_world_matrix(gvr_keyboard_, eye, &view_matrix);
-
-    gvr::Mat4f proj_matrix;
-    TransformToGvrMat(eye_info.proj_matrix, &proj_matrix);
-    gvr_keyboard_set_projection_matrix(gvr_keyboard_, eye, &proj_matrix);
-
-    gfx::Rect viewport_rect = eye_info.viewport;
-    const gvr::Recti viewport = {viewport_rect.x(), viewport_rect.right(),
-                                 viewport_rect.y(), viewport_rect.bottom()};
-    gvr_keyboard_set_viewport(gvr_keyboard_, eye, &viewport);
-    gvr_keyboard_render(gvr_keyboard_, eye);
-  }
-}
-
 void VrShellGl::DrawFrameSubmitWhenReady(
     int16_t frame_index,
     const gfx::Transform& head_pose,
diff --git a/chrome/browser/android/vr_shell/vr_shell_gl.h b/chrome/browser/android/vr_shell/vr_shell_gl.h
index 8896d79..66e66e4 100644
--- a/chrome/browser/android/vr_shell/vr_shell_gl.h
+++ b/chrome/browser/android/vr_shell/vr_shell_gl.h
@@ -16,6 +16,7 @@
 #include "base/memory/weak_ptr.h"
 #include "base/single_thread_task_runner.h"
 #include "chrome/browser/android/vr_shell/android_vsync_helper.h"
+#include "chrome/browser/android/vr_shell/gvr_keyboard_delegate.h"
 #include "chrome/browser/android/vr_shell/vr_controller.h"
 #include "chrome/browser/vr/content_input_delegate.h"
 #include "chrome/browser/vr/controller_mesh.h"
@@ -24,7 +25,6 @@
 #include "chrome/browser/vr/ui_renderer.h"
 #include "device/vr/vr_service.mojom.h"
 #include "mojo/public/cpp/bindings/binding.h"
-#include "third_party/gvr-android-keyboard/src/libraries/headers/vr/gvr/capi/include/gvr_keyboard.h"
 #include "third_party/gvr-android-sdk/src/libraries/headers/vr/gvr/capi/include/gvr.h"
 #include "third_party/gvr-android-sdk/src/libraries/headers/vr/gvr/capi/include/gvr_types.h"
 #include "ui/gfx/geometry/quaternion.h"
@@ -81,7 +81,8 @@
             gvr_context* gvr_api,
             bool reprojected_rendering,
             bool daydream_support,
-            bool start_in_web_vr_mode);
+            bool start_in_web_vr_mode,
+            GvrKeyboardDelegate* keyboard_delegate);
   ~VrShellGl() override;
 
   void Initialize();
@@ -90,8 +91,6 @@
   void OnTriggerEvent();
   void OnPause();
   void OnResume();
-  void DrawKeyboard();
-  void CreateKeyboard();
 
   base::WeakPtr<vr::BrowserUiInterface> GetBrowserUiWeakPtr();
 
@@ -232,7 +231,6 @@
   bool is_exiting_ = false;
 
   std::unique_ptr<VrController> controller_;
-  gvr_keyboard_context* gvr_keyboard_ = nullptr;
 
   scoped_refptr<base::SingleThreadTaskRunner> task_runner_;
 
@@ -243,6 +241,7 @@
   device::mojom::VRSubmitFrameClientPtr submit_client_;
 
   GlBrowserInterface* browser_;
+  GvrKeyboardDelegate* keyboard_delegate_;
 
   uint8_t frame_index_ = 0;
   // Larger than frame_index_ so it can be initialized out-of-band.
@@ -275,8 +274,6 @@
   bool content_frame_available_ = false;
   gfx::Transform last_used_head_pose_;
 
-  bool keyboard_enabled_ = false;
-  bool show_keyboard_ = false;
   vr::ControllerModel controller_model_;
 
   base::WeakPtrFactory<VrShellGl> weak_ptr_factory_;
diff --git a/chrome/browser/chromeos/arc/wallpaper/arc_wallpaper_service_unittest.cc b/chrome/browser/chromeos/arc/wallpaper/arc_wallpaper_service_unittest.cc
index ce93bc5..f765447 100644
--- a/chrome/browser/chromeos/arc/wallpaper/arc_wallpaper_service_unittest.cc
+++ b/chrome/browser/chromeos/arc/wallpaper/arc_wallpaper_service_unittest.cc
@@ -74,18 +74,9 @@
     // Prefs
     TestingBrowserProcess::GetGlobal()->SetLocalState(&pref_service_);
     pref_service_.registry()->RegisterDictionaryPref(
-        ash::prefs::kWallpaperColors);
+        ash::prefs::kUserWallpaperInfo);
     pref_service_.registry()->RegisterDictionaryPref(
-        chromeos::kUsersWallpaperInfo);
-
-    // Ash prefs
-    auto pref_service = std::make_unique<TestingPrefServiceSimple>();
-    ash::Shell::RegisterLocalStatePrefs(pref_service->registry());
-    pref_service->registry()->SetDefaultForeignPrefValue(
-        ash::prefs::kWallpaperColors, std::make_unique<base::DictionaryValue>(),
-        0);
-    ash::ShellTestApi().OnLocalStatePrefServiceInitialized(
-        std::move(pref_service));
+        ash::prefs::kWallpaperColors);
 
     // User
     user_manager_->AddUser(user_manager::StubAccountId());
diff --git a/chrome/browser/chromeos/extensions/wallpaper_private_api.cc b/chrome/browser/chromeos/extensions/wallpaper_private_api.cc
index 0ee3b574..d052a65 100644
--- a/chrome/browser/chromeos/extensions/wallpaper_private_api.cc
+++ b/chrome/browser/chromeos/extensions/wallpaper_private_api.cc
@@ -31,6 +31,7 @@
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/sync/profile_sync_service_factory.h"
 #include "chrome/browser/ui/ash/ash_util.h"
+#include "chrome/browser/ui/ash/wallpaper_controller_client.h"
 #include "chrome/common/chrome_paths.h"
 #include "chrome/common/pref_names.h"
 #include "chrome/grit/generated_resources.h"
@@ -333,16 +334,14 @@
   // Set unsafe_wallpaper_decoder_ to null since the decoding already finished.
   unsafe_wallpaper_decoder_ = NULL;
 
-  chromeos::WallpaperManager* wallpaper_manager =
-      chromeos::WallpaperManager::Get();
   wallpaper::WallpaperLayout layout = wallpaper_api_util::GetLayoutEnum(
       wallpaper_base::ToString(params->layout));
 
   bool update_wallpaper =
       account_id_ ==
       user_manager::UserManager::Get()->GetActiveUser()->GetAccountId();
-  wallpaper_manager->SetOnlineWallpaper(account_id_, image, params->url, layout,
-                                        update_wallpaper);
+  WallpaperControllerClient::Get()->SetOnlineWallpaper(
+      account_id_, image, params->url, layout, update_wallpaper);
   SetResult(base::MakeUnique<base::Value>(true));
   Profile* profile = Profile::FromBrowserContext(browser_context());
   // This API is only available to the component wallpaper picker. We do not
@@ -432,17 +431,14 @@
 
 void WallpaperPrivateSetWallpaperFunction::SetDecodedWallpaper(
     std::unique_ptr<gfx::ImageSkia> image) {
-  chromeos::WallpaperManager* wallpaper_manager =
-      chromeos::WallpaperManager::Get();
-
   wallpaper::WallpaperLayout layout = wallpaper_api_util::GetLayoutEnum(
       wallpaper_base::ToString(params->layout));
 
   bool update_wallpaper =
       account_id_ ==
       user_manager::UserManager::Get()->GetActiveUser()->GetAccountId();
-  wallpaper_manager->SetOnlineWallpaper(account_id_, *image.get(), params->url,
-                                        layout, update_wallpaper);
+  WallpaperControllerClient::Get()->SetOnlineWallpaper(
+      account_id_, *image.get(), params->url, layout, update_wallpaper);
 
   Profile* profile = Profile::FromBrowserContext(browser_context());
   // This API is only available to the component wallpaper picker. We do not
diff --git a/chrome/browser/chromeos/login/users/wallpaper/wallpaper_manager.cc b/chrome/browser/chromeos/login/users/wallpaper/wallpaper_manager.cc
index 34b0add..fa60c08 100644
--- a/chrome/browser/chromeos/login/users/wallpaper/wallpaper_manager.cc
+++ b/chrome/browser/chromeos/login/users/wallpaper/wallpaper_manager.cc
@@ -12,7 +12,6 @@
 #include "ash/public/cpp/window_properties.h"
 #include "ash/public/interfaces/constants.mojom.h"
 #include "ash/shell.h"
-#include "ash/wallpaper/wallpaper_controller.h"
 #include "ash/wallpaper/wallpaper_window_state_manager.h"
 #include "base/bind.h"
 #include "base/bind_helpers.h"
@@ -51,9 +50,7 @@
 #include "chrome/common/pref_names.h"
 #include "chromeos/chromeos_switches.h"
 #include "chromeos/cryptohome/system_salt_getter.h"
-#include "components/prefs/pref_registry_simple.h"
 #include "components/prefs/pref_service.h"
-#include "components/prefs/scoped_user_pref_update.h"
 #include "components/signin/core/account_id/account_id.h"
 #include "components/user_manager/known_user.h"
 #include "components/user_manager/user.h"
@@ -89,12 +86,6 @@
 
 const int kCacheWallpaperDelayMs = 500;
 
-// Names of nodes with wallpaper info in |kUserWallpapersInfo| dictionary.
-const char kNewWallpaperDateNodeName[] = "date";
-const char kNewWallpaperLayoutNodeName[] = "layout";
-const char kNewWallpaperLocationNodeName[] = "file";
-const char kNewWallpaperTypeNodeName[] = "type";
-
 // Known user keys.
 const char kWallpaperFilesId[] = "wallpaper-files-id";
 
@@ -353,8 +344,6 @@
 const int kWallpaperThumbnailWidth = 108;
 const int kWallpaperThumbnailHeight = 68;
 
-const char kUsersWallpaperInfo[] = "user_wallpaper_info";
-
 // This is "wallpaper either scheduled to load, or loading right now".
 //
 // While enqueued, it defines moment in the future, when it will be loaded.
@@ -603,8 +592,8 @@
                                                   const base::FilePath& path,
                                                   const gfx::ImageSkia& image) {
   DCHECK(!image.isNull());
-  wallpaper_manager_->wallpaper_cache_[account_id] =
-      CustomWallpaperElement(path, image);
+  (*wallpaper_manager_->GetWallpaperCacheMap())[account_id] =
+      ash::CustomWallpaperElement(path, image);
 }
 
 void WallpaperManager::TestApi::ClearDisposableWallpaperCache() {
@@ -661,11 +650,6 @@
 }
 
 // static
-void WallpaperManager::RegisterPrefs(PrefRegistrySimple* registry) {
-  registry->RegisterDictionaryPref(kUsersWallpaperInfo);
-}
-
-// static
 bool WallpaperManager::ResizeImage(const gfx::ImageSkia& image,
                                    wallpaper::WallpaperLayout layout,
                                    int preferred_width,
@@ -812,34 +796,8 @@
   if (show_wallpaper)
     GetPendingWallpaper()->SetWallpaperFromImage(account_id, image, info);
 
-  wallpaper_cache_[account_id] = CustomWallpaperElement(wallpaper_path, image);
-}
-
-void WallpaperManager::SetOnlineWallpaper(const AccountId& account_id,
-                                          const gfx::ImageSkia& image,
-                                          const std::string& url,
-                                          wallpaper::WallpaperLayout layout,
-                                          bool show_wallpaper) {
-  DCHECK(user_manager::UserManager::Get()->IsUserLoggedIn());
-
-  // There is no visible wallpaper in kiosk mode.
-  if (user_manager::UserManager::Get()->IsLoggedInAsKioskApp())
-    return;
-
-  WallpaperInfo info = {url, layout, wallpaper::ONLINE,
-                        base::Time::Now().LocalMidnight()};
-  bool is_persistent = !user_manager::UserManager::Get()
-                            ->IsCurrentUserNonCryptohomeDataEphemeral();
-  SetUserWallpaperInfo(account_id, info, is_persistent);
-
-  if (show_wallpaper)
-    GetPendingWallpaper()->SetWallpaperFromImage(account_id, image, info);
-
-  // Leave the file path empty, because in most cases the file path is not used
-  // when fetching cache, but in case it needs to be checked, we should avoid
-  // confusing the URL with a real file path.
-  wallpaper_cache_[account_id] =
-      CustomWallpaperElement(base::FilePath(), image);
+  (*GetWallpaperCacheMap())[account_id] =
+      ash::CustomWallpaperElement(wallpaper_path, image);
 }
 
 void WallpaperManager::SetDefaultWallpaper(const AccountId& account_id,
@@ -930,7 +888,7 @@
   }
 
   gfx::ImageSkia user_wallpaper;
-  current_user_wallpaper_info_ = info;
+  *GetCachedWallpaperInfo() = info;
   if (GetWallpaperFromCache(account_id, &user_wallpaper)) {
     GetPendingWallpaper()->SetWallpaperFromImage(account_id, user_wallpaper,
                                                  info);
@@ -959,16 +917,19 @@
         wallpaper_path = GetDeviceWallpaperFilePath();
       }
 
-      CustomWallpaperMap::iterator it = wallpaper_cache_.find(account_id);
+      ash::CustomWallpaperMap* wallpaper_cache_map = GetWallpaperCacheMap();
+      ash::CustomWallpaperMap::iterator it =
+          wallpaper_cache_map->find(account_id);
       // Do not try to load the wallpaper if the path is the same. Since loading
       // could still be in progress, we ignore the existence of the image.
-      if (it != wallpaper_cache_.end() && it->second.first == wallpaper_path)
+      if (it != wallpaper_cache_map->end() &&
+          it->second.first == wallpaper_path)
         return;
 
       // Set the new path and reset the existing image - the image will be
       // added once it becomes available.
-      wallpaper_cache_[account_id] =
-          CustomWallpaperElement(wallpaper_path, gfx::ImageSkia());
+      (*wallpaper_cache_map)[account_id] =
+          ash::CustomWallpaperElement(wallpaper_path, gfx::ImageSkia());
       loaded_wallpapers_for_test_++;
 
       GetPendingWallpaper()->SetWallpaperFromPath(account_id, info,
@@ -990,22 +951,16 @@
 }
 
 void WallpaperManager::RemoveUserWallpaper(const AccountId& account_id) {
-  if (wallpaper_cache_.find(account_id) != wallpaper_cache_.end())
-    wallpaper_cache_.erase(account_id);
-
-  PrefService* prefs = g_browser_process->local_state();
-  // PrefService could be NULL in tests.
-  if (!prefs)
-    return;
-  WallpaperInfo info;
-  GetUserWallpaperInfo(account_id, &info);
-  DictionaryPrefUpdate prefs_wallpapers_info_update(prefs, kUsersWallpaperInfo);
-  prefs_wallpapers_info_update->RemoveWithoutPathExpansion(
-      account_id.GetUserEmail(), NULL);
-  // Remove the color cache of the previous wallpaper if it exists.
-  DictionaryPrefUpdate wallpaper_colors_update(prefs,
-                                               ash::prefs::kWallpaperColors);
-  wallpaper_colors_update->RemoveWithoutPathExpansion(info.location, nullptr);
+  if (ash::Shell::HasInstance() && !ash_util::IsRunningInMash()) {
+    // Some unit tests come here without a Shell instance.
+    // TODO(crbug.com/776464): This is intended not to work under mash. Make it
+    // work again after WallpaperManager is removed.
+    bool is_persistent =
+        !user_manager::UserManager::Get()->IsUserNonCryptohomeDataEphemeral(
+            account_id);
+    ash::Shell::Get()->wallpaper_controller()->RemoveUserWallpaperInfo(
+        account_id, is_persistent);
+  }
   DeleteUserWallpapers(account_id);
 }
 
@@ -1013,30 +968,14 @@
                                             const WallpaperInfo& info,
                                             bool is_persistent) {
   DCHECK_CURRENTLY_ON(BrowserThread::UI);
-  current_user_wallpaper_info_ = info;
-  if (!is_persistent)
+  if (!ash::Shell::HasInstance() || ash_util::IsRunningInMash()) {
+    // Some unit tests come here without a Shell instance.
+    // TODO(crbug.com/776464): This is intended not to work under mash. Make it
+    // work again after WallpaperManager is removed.
     return;
-
-  PrefService* local_state = g_browser_process->local_state();
-  // Remove the color cache of the previous wallpaper if it exists.
-  WallpaperInfo old_info;
-  if (GetUserWallpaperInfo(account_id, &old_info)) {
-    DictionaryPrefUpdate wallpaper_colors_update(local_state,
-                                                 ash::prefs::kWallpaperColors);
-    wallpaper_colors_update->RemoveWithoutPathExpansion(old_info.location,
-                                                        nullptr);
   }
-  DictionaryPrefUpdate wallpaper_update(local_state, kUsersWallpaperInfo);
-
-  auto wallpaper_info_dict = base::MakeUnique<base::DictionaryValue>();
-  wallpaper_info_dict->SetString(
-      kNewWallpaperDateNodeName,
-      base::Int64ToString(info.date.ToInternalValue()));
-  wallpaper_info_dict->SetString(kNewWallpaperLocationNodeName, info.location);
-  wallpaper_info_dict->SetInteger(kNewWallpaperLayoutNodeName, info.layout);
-  wallpaper_info_dict->SetInteger(kNewWallpaperTypeNodeName, info.type);
-  wallpaper_update->SetWithoutPathExpansion(account_id.GetUserEmail(),
-                                            std::move(wallpaper_info_dict));
+  ash::Shell::Get()->wallpaper_controller()->SetUserWallpaperInfo(
+      account_id, info, is_persistent);
 }
 
 void WallpaperManager::InitializeWallpaper() {
@@ -1096,7 +1035,7 @@
   for (auto& observer : observers_)
     observer.OnUpdateWallpaperForTesting();
   if (clear_cache)
-    wallpaper_cache_.clear();
+    GetWallpaperCacheMap()->clear();
   if (!device_wallpaper_set)
     ShowUserWallpaper(last_selected_user_);
 }
@@ -1124,12 +1063,11 @@
   DCHECK(thread_checker_.CalledOnValidThread());
 
   if (user_manager::UserManager::Get()->IsLoggedInAsStub()) {
-    info->location = current_user_wallpaper_info_.location = "";
-    info->layout = current_user_wallpaper_info_.layout =
-        wallpaper::WALLPAPER_LAYOUT_CENTER_CROPPED;
-    info->type = current_user_wallpaper_info_.type = wallpaper::DEFAULT;
-    info->date = current_user_wallpaper_info_.date =
-        base::Time::Now().LocalMidnight();
+    info->location = "";
+    info->layout = wallpaper::WALLPAPER_LAYOUT_CENTER_CROPPED;
+    info->type = wallpaper::DEFAULT;
+    info->date = base::Time::Now().LocalMidnight();
+    *GetCachedWallpaperInfo() = *info;
     return true;
   }
 
@@ -1162,7 +1100,7 @@
     UMA_HISTOGRAM_ENUMERATION("Ash.Wallpaper.Type", info.type,
                               wallpaper::WALLPAPER_TYPE_COUNT);
     RecordWallpaperAppType();
-    if (info == current_user_wallpaper_info_)
+    if (info == *GetCachedWallpaperInfo())
       return;
   }
   ShowUserWallpaper(
@@ -1506,23 +1444,27 @@
 bool WallpaperManager::GetWallpaperFromCache(const AccountId& account_id,
                                              gfx::ImageSkia* image) {
   DCHECK(thread_checker_.CalledOnValidThread());
-  CustomWallpaperMap::const_iterator it = wallpaper_cache_.find(account_id);
-  if (it != wallpaper_cache_.end() && !(*it).second.second.isNull()) {
-    *image = (*it).second.second;
-    return true;
+  if (!ash::Shell::HasInstance() || ash_util::IsRunningInMash()) {
+    // Some unit tests come here without a Shell instance.
+    // TODO(crbug.com/776464): This is intended not to work under mash. Make it
+    // work again after WallpaperManager is removed.
+    return false;
   }
-  return false;
+  return ash::Shell::Get()->wallpaper_controller()->GetWallpaperFromCache(
+      account_id, image);
 }
 
 bool WallpaperManager::GetPathFromCache(const AccountId& account_id,
                                         base::FilePath* path) {
   DCHECK(thread_checker_.CalledOnValidThread());
-  CustomWallpaperMap::const_iterator it = wallpaper_cache_.find(account_id);
-  if (it != wallpaper_cache_.end()) {
-    *path = (*it).second.first;
-    return true;
+  if (!ash::Shell::HasInstance() || ash_util::IsRunningInMash()) {
+    // Some unit tests come here without a Shell instance.
+    // TODO(crbug.com/776464): This is intended not to work under mash. Make it
+    // work again after WallpaperManager is removed.
+    return false;
   }
-  return false;
+  return ash::Shell::Get()->wallpaper_controller()->GetPathFromCache(account_id,
+                                                                     path);
 }
 
 int WallpaperManager::loaded_wallpapers_for_test() const {
@@ -1547,8 +1489,9 @@
 
 void WallpaperManager::CacheUserWallpaper(const AccountId& account_id) {
   DCHECK(thread_checker_.CalledOnValidThread());
-  CustomWallpaperMap::iterator it = wallpaper_cache_.find(account_id);
-  if (it != wallpaper_cache_.end() && !it->second.second.isNull())
+  ash::CustomWallpaperMap* wallpaper_cache_map = GetWallpaperCacheMap();
+  ash::CustomWallpaperMap::iterator it = wallpaper_cache_map->find(account_id);
+  if (it != wallpaper_cache_map->end() && !it->second.second.isNull())
     return;
   WallpaperInfo info;
   if (GetUserWallpaperInfo(account_id, &info)) {
@@ -1567,8 +1510,8 @@
         wallpaper_path = GetCustomWallpaperDir(sub_dir).Append(info.location);
       }
       // Set the path to the cache.
-      wallpaper_cache_[account_id] =
-          CustomWallpaperElement(wallpaper_path, gfx::ImageSkia());
+      (*wallpaper_cache_map)[account_id] =
+          ash::CustomWallpaperElement(wallpaper_path, gfx::ImageSkia());
       task_runner_->PostTask(
           FROM_HERE,
           base::Bind(&WallpaperManager::GetCustomWallpaperInternal, account_id,
@@ -1595,15 +1538,16 @@
     logged_in_user_account_ids.insert((*it)->GetAccountId());
   }
 
-  CustomWallpaperMap logged_in_users_cache;
-  for (CustomWallpaperMap::iterator it = wallpaper_cache_.begin();
-       it != wallpaper_cache_.end(); ++it) {
+  ash::CustomWallpaperMap logged_in_users_cache;
+  ash::CustomWallpaperMap* wallpaper_cache_map = GetWallpaperCacheMap();
+  for (ash::CustomWallpaperMap::iterator it = wallpaper_cache_map->begin();
+       it != wallpaper_cache_map->end(); ++it) {
     if (logged_in_user_account_ids.find(it->first) !=
         logged_in_user_account_ids.end()) {
       logged_in_users_cache.insert(*it);
     }
   }
-  wallpaper_cache_ = logged_in_users_cache;
+  *wallpaper_cache_map = logged_in_users_cache;
 }
 
 void WallpaperManager::DeleteUserWallpapers(const AccountId& account_id) {
@@ -1719,9 +1663,11 @@
 
     // If the wallpaper exists and it contains already the correct image we can
     // return immediately.
-    CustomWallpaperMap::iterator it = wallpaper_cache_.find(account_id);
-    if (it != wallpaper_cache_.end() && it->second.first == wallpaper_path &&
-        !it->second.second.isNull())
+    ash::CustomWallpaperMap* wallpaper_cache_map = GetWallpaperCacheMap();
+    ash::CustomWallpaperMap::iterator it =
+        wallpaper_cache_map->find(account_id);
+    if (it != wallpaper_cache_map->end() &&
+        it->second.first == wallpaper_path && !it->second.second.isNull())
       return;
 
     loaded_wallpapers_for_test_++;
@@ -1781,47 +1727,17 @@
 bool WallpaperManager::GetUserWallpaperInfo(const AccountId& account_id,
                                             WallpaperInfo* info) const {
   DCHECK_CURRENTLY_ON(BrowserThread::UI);
-
-  if (user_manager::UserManager::Get()->IsUserNonCryptohomeDataEphemeral(
-          account_id)) {
-    // Default to the values cached in memory.
-    *info = current_user_wallpaper_info_;
-
-    // Ephemeral users do not save anything to local state. But we have got
-    // wallpaper info from memory. Returns true.
-    return true;
-  }
-
-  const base::DictionaryValue* info_dict;
-  if (!g_browser_process->local_state()
-           ->GetDictionary(kUsersWallpaperInfo)
-           ->GetDictionaryWithoutPathExpansion(account_id.GetUserEmail(),
-                                               &info_dict)) {
+  if (!ash::Shell::HasInstance() || ash_util::IsRunningInMash()) {
+    // Some unit tests come here without a Shell instance.
+    // TODO(crbug.com/776464): This is intended not to work under mash. Make it
+    // work again after WallpaperManager is removed.
     return false;
   }
-
-  // Use temporary variables to keep |info| untouched in the error case.
-  std::string location;
-  if (!info_dict->GetString(kNewWallpaperLocationNodeName, &location))
-    return false;
-  int layout;
-  if (!info_dict->GetInteger(kNewWallpaperLayoutNodeName, &layout))
-    return false;
-  int type;
-  if (!info_dict->GetInteger(kNewWallpaperTypeNodeName, &type))
-    return false;
-  std::string date_string;
-  if (!info_dict->GetString(kNewWallpaperDateNodeName, &date_string))
-    return false;
-  int64_t date_val;
-  if (!base::StringToInt64(date_string, &date_val))
-    return false;
-
-  info->location = location;
-  info->layout = static_cast<wallpaper::WallpaperLayout>(layout);
-  info->type = static_cast<wallpaper::WallpaperType>(type);
-  info->date = base::Time::FromInternalValue(date_val);
-  return true;
+  bool is_persistent =
+      !user_manager::UserManager::Get()->IsUserNonCryptohomeDataEphemeral(
+          account_id);
+  return ash::Shell::Get()->wallpaper_controller()->GetUserWallpaperInfo(
+      account_id, info, is_persistent);
 }
 
 bool WallpaperManager::ShouldSetDeviceWallpaper(const AccountId& account_id,
@@ -1883,7 +1799,7 @@
   }
 
   // Update the image, but keep the path which was set earlier.
-  wallpaper_cache_[account_id].second = user_image->image();
+  (*GetWallpaperCacheMap())[account_id].second = user_image->image();
 
   if (update_wallpaper)
     SetWallpaper(user_image->image(), info);
@@ -1896,7 +1812,7 @@
   // There is no visible wallpaper in kiosk mode.
   if (user_manager::UserManager::Get()->IsLoggedInAsKioskApp())
     return;
-  wallpaper_cache_.erase(account_id);
+  GetWallpaperCacheMap()->erase(account_id);
 
   WallpaperResolution resolution = GetAppropriateResolution();
   const bool use_small = (resolution == WALLPAPER_RESOLUTION_SMALL);
@@ -1957,8 +1873,8 @@
   if (update_wallpaper) {
     // We are now about to change the wallpaper, so update the path and remove
     // the existing image.
-    wallpaper_cache_[account_id] =
-        CustomWallpaperElement(wallpaper_path, gfx::ImageSkia());
+    (*GetWallpaperCacheMap())[account_id] =
+        ash::CustomWallpaperElement(wallpaper_path, gfx::ImageSkia());
   }
   user_image_loader::StartWithFilePath(
       task_runner_, wallpaper_path, ImageDecoder::ROBUST_JPEG_CODEC,
@@ -2400,4 +2316,18 @@
                         MovableOnDestroyCallbackHolder());
 }
 
+wallpaper::WallpaperInfo* WallpaperManager::GetCachedWallpaperInfo() {
+  if (!ash::Shell::HasInstance() || ash_util::IsRunningInMash())
+    return &dummy_current_user_wallpaper_info_;
+  return ash::Shell::Get()
+      ->wallpaper_controller()
+      ->GetCurrentUserWallpaperInfo();
+}
+
+ash::CustomWallpaperMap* WallpaperManager::GetWallpaperCacheMap() {
+  if (!ash::Shell::HasInstance() || ash_util::IsRunningInMash())
+    return &dummy_wallpaper_cache_map_;
+  return ash::Shell::Get()->wallpaper_controller()->GetWallpaperCacheMap();
+}
+
 }  // namespace chromeos
diff --git a/chrome/browser/chromeos/login/users/wallpaper/wallpaper_manager.h b/chrome/browser/chromeos/login/users/wallpaper/wallpaper_manager.h
index 6bbb4a7..ee4b384 100644
--- a/chrome/browser/chromeos/login/users/wallpaper/wallpaper_manager.h
+++ b/chrome/browser/chromeos/login/users/wallpaper/wallpaper_manager.h
@@ -12,6 +12,7 @@
 #include <string>
 #include <vector>
 
+#include "ash/wallpaper/wallpaper_controller.h"
 #include "base/containers/circular_deque.h"
 #include "base/files/file_path.h"
 #include "base/macros.h"
@@ -37,8 +38,6 @@
 #include "ui/wm/public/activation_change_observer.h"
 #include "ui/wm/public/activation_client.h"
 
-class PrefRegistrySimple;
-
 namespace base {
 class CommandLine;
 class SequencedTaskRunner;
@@ -91,9 +90,6 @@
 extern const int kWallpaperThumbnailWidth;
 extern const int kWallpaperThumbnailHeight;
 
-// A dictionary pref that maps usernames to wallpaper info.
-extern const char kUsersWallpaperInfo[];
-
 class WallpaperManager : public content::NotificationObserver,
                          public user_manager::UserManager::Observer,
                          public wm::ActivationChangeObserver,
@@ -212,9 +208,6 @@
   // Returns custom wallpaper directory by appending corresponding |sub_dir|.
   static base::FilePath GetCustomWallpaperDir(const char* sub_dir);
 
-  // Registers wallpaper manager preferences.
-  static void RegisterPrefs(PrefRegistrySimple* registry);
-
   // Resizes |image| to a resolution which is nearest to |preferred_width| and
   // |preferred_height| while respecting the |layout| choice. |output_skia| is
   // optional (may be NULL). Returns true on success.
@@ -261,21 +254,6 @@
                           const gfx::ImageSkia& image,
                           bool show_wallpaper);
 
-  // Sets wallpaper from the wallpaper picker selection, i.e., the wallpaper
-  // type is ONLINE.
-  // |account_id|: The user's account id.
-  // |image|: The wallpaper image.
-  // |url|: The url corresponding to this wallpaper. Used as a placeholder for
-  //        the location in WallpaperInfo.
-  // |layout|: The layout of the wallpaper, used for wallpaper resizing.
-  // |show_wallpaper|: If false, don't show the new wallpaper now but only
-  //                   update cache.
-  void SetOnlineWallpaper(const AccountId& account_id,
-                          const gfx::ImageSkia& image,
-                          const std::string& url,
-                          wallpaper::WallpaperLayout layout,
-                          bool show_wallpaper);
-
   // Sets |account_id|'s wallpaper to be the default wallpaper. Note: different
   // user types may have different default wallpapers. If |show_wallpaper| is
   // false, don't show the default wallpaper now.
@@ -307,10 +285,7 @@
   // Removes all of |account_id|'s saved wallpapers and related info.
   void RemoveUserWallpaper(const AccountId& account_id);
 
-  // TODO(crbug.com/776464): Make this private. WallpaperInfo should be an
-  // internal concept.
-  // Sets wallpaper info for |account_id| and saves it to local state if
-  // |is_persistent| is true.
+  // A wrapper of |WallpaperController::SetUserWallpaperInfo|.
   void SetUserWallpaperInfo(const AccountId& account_id,
                             const wallpaper::WallpaperInfo& info,
                             bool is_persistent);
@@ -402,12 +377,6 @@
 
   WallpaperManager();
 
-  // The |CustomWallpaperElement| contains |first| the path of the image which
-  // is currently being loaded and or in progress of being loaded and |second|
-  // the image itself.
-  typedef std::pair<base::FilePath, gfx::ImageSkia> CustomWallpaperElement;
-  typedef std::map<AccountId, CustomWallpaperElement> CustomWallpaperMap;
-
   // Saves original custom wallpaper to |path| (absolute path) on filesystem
   // and starts resizing operation of the custom wallpaper if necessary.
   static void SaveCustomWallpaper(
@@ -452,11 +421,11 @@
   // set the device wallpaper as the login screen wallpaper.
   bool SetDeviceWallpaperIfApplicable(const AccountId& account_id);
 
-  // Gets encoded wallpaper from cache. Returns true if success.
+  // A wrapper of |WallpaperController::GetWallpaperFromCache|.
   bool GetWallpaperFromCache(const AccountId& account_id,
                              gfx::ImageSkia* image);
 
-  // Gets path of encoded wallpaper from cache. Returns true if success.
+  // A wrapper of |WallpaperController::GetPathFromCache|.
   bool GetPathFromCache(const AccountId& account_id, base::FilePath* path);
 
   // The number of wallpapers have loaded. For test only.
@@ -510,9 +479,7 @@
   // wallpaper_files_id is ready.
   void MoveLoggedInUserCustomWallpaper();
 
-  // Gets wallpaper information of |account_id| from Local State or memory.
-  // Returns
-  // false if wallpaper information is not found.
+  // A wrapper of |WallpaperController::GetUserWallpaperInfo|.
   bool GetUserWallpaperInfo(const AccountId& account_id,
                             wallpaper::WallpaperInfo* info) const;
 
@@ -671,6 +638,13 @@
       const base::FilePath& customized_default_wallpaper_file_large,
       std::unique_ptr<gfx::ImageSkia> large_wallpaper_image);
 
+  // Returns the cached logged-in user wallpaper info, or a dummy value under
+  // mash.
+  wallpaper::WallpaperInfo* GetCachedWallpaperInfo();
+
+  // Returns the wallpaper cache map, or a dummy value under mash.
+  ash::CustomWallpaperMap* GetWallpaperCacheMap();
+
   std::unique_ptr<CrosSettings::ObserverSubscription>
       show_user_name_on_signin_subscription_;
 
@@ -687,14 +661,14 @@
   // Wallpaper sequenced task runner.
   scoped_refptr<base::SequencedTaskRunner> task_runner_;
 
-  // Logged-in user wallpaper information.
-  wallpaper::WallpaperInfo current_user_wallpaper_info_;
-
   // If non-NULL, used in place of the real command line.
   base::CommandLine* command_line_for_testing_ = nullptr;
 
-  // Caches wallpapers of users. Accessed only on UI thread.
-  CustomWallpaperMap wallpaper_cache_;
+  // A placeholder for |current_user_wallpaper_info_| under mash.
+  wallpaper::WallpaperInfo dummy_current_user_wallpaper_info_;
+
+  // A placeholder for |wallpaper_cache_map_| under mash.
+  ash::CustomWallpaperMap dummy_wallpaper_cache_map_;
 
   // The last selected user on user pod row.
   AccountId last_selected_user_ = EmptyAccountId();
diff --git a/chrome/browser/chromeos/login/users/wallpaper/wallpaper_manager_browsertest.cc b/chrome/browser/chromeos/login/users/wallpaper/wallpaper_manager_browsertest.cc
index 9ff347f..9eb8c2ea 100644
--- a/chrome/browser/chromeos/login/users/wallpaper/wallpaper_manager_browsertest.cc
+++ b/chrome/browser/chromeos/login/users/wallpaper/wallpaper_manager_browsertest.cc
@@ -541,11 +541,9 @@
 
 // Tests for crbug.com/339576. Wallpaper cache should be updated in
 // multi-profile mode when user:
-// 1. chooses an online wallpaper from wallpaper
-//    picker (calls SetOnlineWallpaper);
-// 2. chooses a custom wallpaper from wallpaper
-//    picker (calls SetCustomWallpaper);
-// 3. reverts to a default wallpaper.
+// 1. chooses a custom wallpaper from wallpaper picker
+//    (calls SetCustomWallpaper);
+// 2. reverts to a default wallpaper.
 // Also, when user login at multi-profile mode, previous logged in users'
 // wallpaper cache should not be deleted.
 IN_PROC_BROWSER_TEST_F(WallpaperManagerBrowserTestCacheUpdate,
@@ -578,18 +576,6 @@
   EXPECT_TRUE(test_api->GetPathFromCache(test_account_id1_, &path));
   EXPECT_EQ(original_path, path);
 
-  gfx::ImageSkia red_wallpaper = CreateTestImage(SK_ColorRED);
-  wallpaper_manager->SetOnlineWallpaper(test_account_id1_, red_wallpaper,
-                                        "dummy" /* dummy url */,
-                                        WALLPAPER_LAYOUT_CENTER, true);
-  wallpaper_manager_test_utils::WaitAsyncWallpaperLoadFinished();
-  // SetOnlineWallpaper should update wallpaper cache when multi-profile is
-  // turned on.
-  EXPECT_TRUE(
-      test_api->GetWallpaperFromCache(test_account_id1_, &cached_wallpaper));
-  EXPECT_TRUE(test_api->GetPathFromCache(test_account_id1_, &path));
-  EXPECT_TRUE(cached_wallpaper.BackedBySameObjectAs(red_wallpaper));
-
   gfx::ImageSkia green_wallpaper = CreateTestImage(SK_ColorGREEN);
   wallpaper_manager->SetCustomWallpaper(
       test_account_id1_, test_account1_wallpaper_files_id_,
diff --git a/chrome/browser/chromeos/net/network_state_notifier.cc b/chrome/browser/chromeos/net/network_state_notifier.cc
index a57384a..1b75893 100644
--- a/chrome/browser/chromeos/net/network_state_notifier.cc
+++ b/chrome/browser/chromeos/net/network_state_notifier.cc
@@ -94,7 +94,7 @@
         message_center::NotifierId(message_center::NotifierId::SYSTEM_COMPONENT,
                                    ash::system_notifier::kNotifierNetworkError),
         message_center::RichNotificationData(),
-        new message_center::HandleNotificationClickedDelegate(callback),
+        new message_center::HandleNotificationClickDelegate(callback),
         GetErrorNotificationVectorIcon(network_type),
         message_center::SystemNotificationWarningLevel::CRITICAL_WARNING);
   } else {
diff --git a/chrome/browser/chromeos/preferences.cc b/chrome/browser/chromeos/preferences.cc
index d21804a..2e09ec2 100644
--- a/chrome/browser/chromeos/preferences.cc
+++ b/chrome/browser/chromeos/preferences.cc
@@ -140,9 +140,6 @@
   registry->RegisterIntegerPref(
       prefs::kSystemTimezoneAutomaticDetectionPolicy,
       enterprise_management::SystemTimezoneProto::USERS_DECIDE);
-
-  registry->RegisterDictionaryPref(ash::prefs::kWallpaperColors,
-                                   PrefRegistry::PUBLIC);
   registry->RegisterStringPref(prefs::kMinimumAllowedChromeVersion, "");
 }
 
diff --git a/chrome/browser/chromeos/status/data_promo_notification.cc b/chrome/browser/chromeos/status/data_promo_notification.cc
index c103170..8588d188 100644
--- a/chrome/browser/chromeos/status/data_promo_notification.cc
+++ b/chrome/browser/chromeos/status/data_promo_notification.cc
@@ -277,7 +277,7 @@
               message_center::NotifierId::SYSTEM_COMPONENT,
               ash::system_notifier::kNotifierNetwork),
           message_center::RichNotificationData(),
-          new message_center::HandleNotificationClickedDelegate(base::Bind(
+          new message_center::HandleNotificationClickDelegate(base::Bind(
               &NotificationClicked, default_network->guid(), info_url)),
           kNotificationMobileDataIcon,
           message_center::SystemNotificationWarningLevel::NORMAL);
@@ -324,7 +324,7 @@
               message_center::NotifierId::SYSTEM_COMPONENT,
               ash::system_notifier::kNotifierNetwork),
           message_center::RichNotificationData(),
-          new message_center::HandleNotificationClickedDelegate(
+          new message_center::HandleNotificationClickDelegate(
               base::Bind(&NotificationClicked, "", kDataSaverExtensionUrl)),
           kNotificationMobileDataIcon,
           message_center::SystemNotificationWarningLevel::NORMAL);
diff --git a/chrome/browser/chromeos/ui/low_disk_notification.cc b/chrome/browser/chromeos/ui/low_disk_notification.cc
index 4f2f36e..dd40e2a 100644
--- a/chrome/browser/chromeos/ui/low_disk_notification.cc
+++ b/chrome/browser/chromeos/ui/low_disk_notification.cc
@@ -113,15 +113,18 @@
       ash::system_notifier::kNotifierDisk);
 
   auto on_click = base::Bind(
-      [](Profile* profile, int button_index) {
-        chrome::ShowSettingsSubPageForProfile(profile, kStoragePage);
+      [](Profile* profile, base::Optional<int> button_index) {
+        if (button_index) {
+          DCHECK_EQ(0, *button_index);
+          chrome::ShowSettingsSubPageForProfile(profile, kStoragePage);
+        }
       },
       profile);
   std::unique_ptr<message_center::Notification> notification =
       ash::system_notifier::CreateSystemNotification(
           message_center::NOTIFICATION_TYPE_SIMPLE, kLowDiskId, title, message,
           icon, base::string16(), GURL(), notifier_id, optional_fields,
-          new message_center::HandleNotificationButtonClickDelegate(on_click),
+          new message_center::HandleNotificationClickDelegate(on_click),
           kNotificationStorageFullIcon, warning_level);
 
   return notification;
diff --git a/chrome/browser/extensions/extension_storage_monitor.cc b/chrome/browser/extensions/extension_storage_monitor.cc
index cb45c93..04b30718 100644
--- a/chrome/browser/extensions/extension_storage_monitor.cc
+++ b/chrome/browser/extensions/extension_storage_monitor.cc
@@ -442,7 +442,7 @@
       message_center::NotifierId(message_center::NotifierId::SYSTEM_COMPONENT,
                                  kSystemNotifierId),
       notification_data,
-      new message_center::HandleNotificationButtonClickDelegate(
+      new message_center::HandleNotificationClickDelegate(
           base::Bind(&ExtensionStorageMonitor::OnNotificationButtonClick,
                      weak_ptr_factory_.GetWeakPtr(), extension_id))));
   notification->SetSystemPriority();
@@ -453,8 +453,12 @@
 }
 
 void ExtensionStorageMonitor::OnNotificationButtonClick(
-    const std::string& extension_id, int button_index) {
-  switch (button_index) {
+    const std::string& extension_id,
+    base::Optional<int> button_index) {
+  if (!button_index)
+    return;
+
+  switch (*button_index) {
     case BUTTON_DISABLE_NOTIFICATION: {
       DisableStorageMonitoring(extension_id);
       break;
diff --git a/chrome/browser/extensions/extension_storage_monitor.h b/chrome/browser/extensions/extension_storage_monitor.h
index 2e9cafb..664b9f7 100644
--- a/chrome/browser/extensions/extension_storage_monitor.h
+++ b/chrome/browser/extensions/extension_storage_monitor.h
@@ -12,6 +12,7 @@
 #include "base/macros.h"
 #include "base/memory/ref_counted.h"
 #include "base/memory/weak_ptr.h"
+#include "base/optional.h"
 #include "base/scoped_observer.h"
 #include "chrome/browser/extensions/extension_uninstall_dialog.h"
 #include "components/keyed_service/core/keyed_service.h"
@@ -86,7 +87,7 @@
                      int64_t current_usage,
                      const gfx::Image& image);
   void OnNotificationButtonClick(const std::string& extension_id,
-                                 int button_index);
+                                 base::Optional<int> button_index);
 
   void DisableStorageMonitoring(const std::string& extension_id);
   void StartMonitoringStorage(const Extension* extension);
diff --git a/chrome/browser/notifications/native_notification_display_service.cc b/chrome/browser/notifications/native_notification_display_service.cc
index 3f1e48b..f235c09 100644
--- a/chrome/browser/notifications/native_notification_display_service.cc
+++ b/chrome/browser/notifications/native_notification_display_service.cc
@@ -54,8 +54,7 @@
 void NativeNotificationDisplayService::OnNotificationPlatformBridgeReady(
     bool success) {
   UMA_HISTOGRAM_BOOLEAN("Notifications.UsingNativeNotificationCenter", success);
-  if (success)
-    notification_bridge_ready_ = true;
+  notification_bridge_ready_ = success;
 
   // TODO(estade): this shouldn't be necessary in the succesful case, but some
   // notification bridges can't handle TRANSIENT notifications and still have to
diff --git a/chrome/browser/prefs/browser_prefs.cc b/chrome/browser/prefs/browser_prefs.cc
index 535debe..e0cee2a 100644
--- a/chrome/browser/prefs/browser_prefs.cc
+++ b/chrome/browser/prefs/browser_prefs.cc
@@ -434,7 +434,6 @@
   chromeos::TimeZoneResolver::RegisterPrefs(registry);
   chromeos::UserImageManager::RegisterPrefs(registry);
   chromeos::UserSessionManager::RegisterPrefs(registry);
-  chromeos::WallpaperManager::RegisterPrefs(registry);
   chromeos::echo_offer::RegisterPrefs(registry);
   extensions::ExtensionAssetsManagerChromeOS::RegisterPrefs(registry);
   extensions::lock_screen_data::LockScreenItemStorage::RegisterLocalState(
diff --git a/chrome/browser/resource_coordinator/page_signal_receiver.h b/chrome/browser/resource_coordinator/page_signal_receiver.h
index 11510ce8..8d61c11 100644
--- a/chrome/browser/resource_coordinator/page_signal_receiver.h
+++ b/chrome/browser/resource_coordinator/page_signal_receiver.h
@@ -21,6 +21,9 @@
 class PageSignalObserver {
  public:
   virtual ~PageSignalObserver() = default;
+  // PageSignalReceiver will deliver signals with a |web_contents| even it's not
+  // managed by the client. Thus the clients are responsible for checking the
+  // passed |web_contents| by themselves.
   virtual void OnPageAlmostIdle(content::WebContents* web_contents) {}
   virtual void OnExpectedTaskQueueingDurationSet(
       content::WebContents* web_contents,
diff --git a/chrome/browser/resource_coordinator/tab_manager_resource_coordinator_signal_observer.cc b/chrome/browser/resource_coordinator/tab_manager_resource_coordinator_signal_observer.cc
index 2058084..a2d9927 100644
--- a/chrome/browser/resource_coordinator/tab_manager_resource_coordinator_signal_observer.cc
+++ b/chrome/browser/resource_coordinator/tab_manager_resource_coordinator_signal_observer.cc
@@ -27,6 +27,8 @@
     content::WebContents* web_contents) {
   auto* web_contents_data =
       TabManager::WebContentsData::FromWebContents(web_contents);
+  if (!web_contents_data)
+    return;
   web_contents_data->NotifyTabIsLoaded();
 }
 
diff --git a/chrome/browser/resource_coordinator/tab_manager_unittest.cc b/chrome/browser/resource_coordinator/tab_manager_unittest.cc
index ef93a0e..a9576b2 100644
--- a/chrome/browser/resource_coordinator/tab_manager_unittest.cc
+++ b/chrome/browser/resource_coordinator/tab_manager_unittest.cc
@@ -26,6 +26,7 @@
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/resource_coordinator/background_tab_navigation_throttle.h"
 #include "chrome/browser/resource_coordinator/tab_manager_features.h"
+#include "chrome/browser/resource_coordinator/tab_manager_resource_coordinator_signal_observer.h"
 #include "chrome/browser/resource_coordinator/tab_manager_stats_collector.h"
 #include "chrome/browser/resource_coordinator/tab_manager_web_contents_data.h"
 #include "chrome/browser/resource_coordinator/tab_stats.h"
@@ -1260,6 +1261,8 @@
   EXPECT_TRUE(base::FeatureList::IsEnabled(features::kPageAlmostIdle));
 
   TabManager* tab_manager = g_browser_process->GetTabManager();
+  tab_manager->resource_coordinator_signal_observer_.reset(
+      new TabManager::ResourceCoordinatorSignalObserver());
   tab_manager->ResetMemoryPressureListenerForTest();
 
   EXPECT_EQ(TabManager::BackgroundTabLoadingMode::kStaggered,
@@ -1291,7 +1294,8 @@
   // Simulate tab 1 has finished loading by receiving idle signal from resource
   // coordinator. Since the page idle signal feature is enabled, this should
   // start next loading.
-  tab_manager->GetWebContentsData(contents1_.get())->NotifyTabIsLoaded();
+  tab_manager->resource_coordinator_signal_observer_->OnPageAlmostIdle(
+      contents1_.get());
 
   // Tab 2 should start loading right away.
   EXPECT_TRUE(tab_manager->IsTabLoadingForTest(contents2_.get()));
@@ -1303,11 +1307,21 @@
 
   // Simulate tab 2 has finished loading by receiving idle signal from resource
   // coordinator.
-  tab_manager->GetWebContentsData(contents2_.get())->NotifyTabIsLoaded();
+  tab_manager->resource_coordinator_signal_observer_->OnPageAlmostIdle(
+      contents2_.get());
 
   // Tab 3 should start loading now in staggered loading mode.
   EXPECT_TRUE(tab_manager->IsTabLoadingForTest(contents3_.get()));
   EXPECT_FALSE(tab_manager->IsNavigationDelayedForTest(nav_handle3_.get()));
+
+  // |ignored_web_contents| is not managed by TabManager, thus will be ignored
+  // and shouldn't cause any crash or side effect.
+  WebContents* ignored_web_contents =
+      WebContentsTester::CreateTestWebContents(browser_context(), nullptr);
+  tab_manager->resource_coordinator_signal_observer_->OnPageAlmostIdle(
+      ignored_web_contents);
+  EXPECT_TRUE(tab_manager->IsTabLoadingForTest(contents3_.get()));
+  EXPECT_FALSE(tab_manager->IsNavigationDelayedForTest(nav_handle3_.get()));
 }
 
 }  // namespace resource_coordinator
diff --git a/chrome/browser/signin/signin_error_notifier_ash.cc b/chrome/browser/signin/signin_error_notifier_ash.cc
index 18747d3..6a5547c9 100644
--- a/chrome/browser/signin/signin_error_notifier_ash.cc
+++ b/chrome/browser/signin/signin_error_notifier_ash.cc
@@ -10,11 +10,11 @@
 #include "base/strings/utf_string_conversions.h"
 #include "build/build_config.h"
 #include "chrome/app/vector_icons/vector_icons.h"
-#include "chrome/browser/browser_process.h"
 #include "chrome/browser/chromeos/login/user_flow.h"
 #include "chrome/browser/chromeos/login/users/chrome_user_manager.h"
 #include "chrome/browser/lifetime/application_lifetime.h"
-#include "chrome/browser/notifications/notification_ui_manager.h"
+#include "chrome/browser/notifications/notification_common.h"
+#include "chrome/browser/notifications/notification_display_service.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/ash/multi_user/multi_user_util.h"
 #include "chrome/browser/ui/browser_tabstrip.h"
@@ -37,34 +37,7 @@
 
 const char kProfileSigninNotificationId[] = "chrome://settings/signin/";
 
-// A notification delegate for the sign-out button.
-// TODO(estade): should this use a generic notification delegate?
-class SigninNotificationDelegate : public message_center::NotificationDelegate {
- public:
-  SigninNotificationDelegate();
-
-  // NotificationDelegate:
-  void Click() override;
-  void ButtonClick(int button_index) override;
-
- protected:
-  ~SigninNotificationDelegate() override;
-
- private:
-  // Unique id of the notification.
-  const std::string id_;
-
-  DISALLOW_COPY_AND_ASSIGN(SigninNotificationDelegate);
-};
-
-SigninNotificationDelegate::SigninNotificationDelegate() = default;
-SigninNotificationDelegate::~SigninNotificationDelegate() = default;
-
-void SigninNotificationDelegate::Click() {
-  chrome::AttemptUserExit();
-}
-
-void SigninNotificationDelegate::ButtonClick(int button_index) {
+void HandleNotificationClick(base::Optional<int> button_index) {
   chrome::AttemptUserExit();
 }
 
@@ -93,16 +66,9 @@
 }
 
 void SigninErrorNotifier::OnErrorChanged() {
-  NotificationUIManager* notification_ui_manager =
-      g_browser_process->notification_ui_manager();
-
-  // notification_ui_manager() may return NULL when shutting down.
-  if (!notification_ui_manager)
-    return;
-
   if (!error_controller_->HasError()) {
-    g_browser_process->notification_ui_manager()->CancelById(
-        notification_id_, NotificationUIManager::GetProfileID(profile_));
+    NotificationDisplayService::GetForProfile(profile_)->Close(
+        NotificationHandler::Type::TRANSIENT, notification_id_);
     return;
   }
 
@@ -122,7 +88,6 @@
   data.buttons.push_back(message_center::ButtonInfo(
       l10n_util::GetStringUTF16(IDS_SYNC_RELOGIN_LINK_LABEL)));
 
-
   message_center::NotifierId notifier_id(
       message_center::NotifierId::SYSTEM_COMPONENT,
       kProfileSigninNotificationId);
@@ -141,7 +106,8 @@
                 IDR_NOTIFICATION_ALERT),
       l10n_util::GetStringUTF16(IDS_SIGNIN_ERROR_DISPLAY_SOURCE),
       GURL(notification_id_), notifier_id, data,
-      new SigninNotificationDelegate());
+      new message_center::HandleNotificationClickDelegate(
+          base::Bind(&HandleNotificationClick)));
   if (message_center::IsNewStyleNotificationEnabled()) {
     notification.set_accent_color(
         message_center::kSystemNotificationColorWarning);
@@ -153,7 +119,8 @@
   notification.SetSystemPriority();
 
   // Update or add the notification.
-  notification_ui_manager->Add(notification, profile_);
+  NotificationDisplayService::GetForProfile(profile_)->Display(
+      NotificationHandler::Type::TRANSIENT, notification);
 }
 
 base::string16 SigninErrorNotifier::GetMessageBody() const {
diff --git a/chrome/browser/sync/sync_error_notifier_ash.cc b/chrome/browser/sync/sync_error_notifier_ash.cc
index 20db1d380..38ed1e4c 100644
--- a/chrome/browser/sync/sync_error_notifier_ash.cc
+++ b/chrome/browser/sync/sync_error_notifier_ash.cc
@@ -10,7 +10,8 @@
 #include "chrome/browser/browser_process.h"
 #include "chrome/browser/chromeos/login/user_flow.h"
 #include "chrome/browser/chromeos/login/users/chrome_user_manager.h"
-#include "chrome/browser/notifications/notification_ui_manager.h"
+#include "chrome/browser/notifications/notification_common.h"
+#include "chrome/browser/notifications/notification_display_service.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/ash/multi_user/multi_user_util.h"
 #include "chrome/browser/ui/chrome_pages.h"
@@ -101,20 +102,14 @@
 }
 
 void SyncErrorNotifier::OnErrorChanged() {
-  NotificationUIManager* notification_ui_manager =
-      g_browser_process->notification_ui_manager();
-
-  // notification_ui_manager() may return null when shutting down.
-  if (!notification_ui_manager)
-    return;
-
   if (error_controller_->HasError() == notification_displayed_)
     return;
 
+  auto* display_service = NotificationDisplayService::GetForProfile(profile_);
   if (!error_controller_->HasError()) {
     notification_displayed_ = false;
-    g_browser_process->notification_ui_manager()->CancelById(
-        notification_id_, NotificationUIManager::GetProfileID(profile_));
+    display_service->Close(NotificationHandler::Type::TRANSIENT,
+                           notification_id_);
     return;
   }
 
@@ -132,9 +127,6 @@
   // Error state just got triggered. There shouldn't be previous notification.
   // Let's display one.
   DCHECK(!notification_displayed_ && error_controller_->HasError());
-  DCHECK(notification_ui_manager->FindById(
-             notification_id_, NotificationUIManager::GetProfileID(profile_)) ==
-         nullptr);
 
   message_center::NotifierId notifier_id(
       message_center::NotifierId::SYSTEM_COMPONENT, kProfileSyncNotificationId);
@@ -165,6 +157,6 @@
     notification.set_vector_small_image(kNotificationWarningIcon);
   }
 
-  notification_ui_manager->Add(notification, profile_);
+  display_service->Display(NotificationHandler::Type::TRANSIENT, notification);
   notification_displayed_ = true;
 }
diff --git a/chrome/browser/ui/ash/chrome_screenshot_grabber.cc b/chrome/browser/ui/ash/chrome_screenshot_grabber.cc
index 7b26f8e9..74757217 100644
--- a/chrome/browser/ui/ash/chrome_screenshot_grabber.cc
+++ b/chrome/browser/ui/ash/chrome_screenshot_grabber.cc
@@ -29,7 +29,7 @@
 #include "chrome/browser/chromeos/file_manager/open_util.h"
 #include "chrome/browser/chromeos/note_taking_helper.h"
 #include "chrome/browser/download/download_prefs.h"
-#include "chrome/browser/notifications/notification_ui_manager.h"
+#include "chrome/browser/notifications/notification_display_service.h"
 #include "chrome/browser/platform_util.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/profiles/profile_manager.h"
@@ -560,8 +560,8 @@
   const std::string notification_id(kNotificationId);
   // We cancel a previous screenshot notification, if any, to ensure we get
   // a fresh notification pop-up.
-  g_browser_process->notification_ui_manager()->CancelById(
-      notification_id, NotificationUIManager::GetProfileID(GetProfile()));
+  NotificationDisplayService::GetForProfile(GetProfile())
+      ->Close(NotificationHandler::Type::TRANSIENT, notification_id);
 
   const bool success =
       (result == ui::ScreenshotGrabberObserver::SCREENSHOT_SUCCESS);
@@ -618,7 +618,8 @@
     notification.set_vector_small_image(kNotificationImageIcon);
   }
 
-  g_browser_process->notification_ui_manager()->Add(notification, GetProfile());
+  NotificationDisplayService::GetForProfile(GetProfile())
+      ->Display(NotificationHandler::Type::TRANSIENT, notification);
 }
 
 Profile* ChromeScreenshotGrabber::GetProfile() {
diff --git a/chrome/browser/ui/extensions/extension_installed_notification.cc b/chrome/browser/ui/extensions/extension_installed_notification.cc
index a889531..a4f33faa 100644
--- a/chrome/browser/ui/extensions/extension_installed_notification.cc
+++ b/chrome/browser/ui/extensions/extension_installed_notification.cc
@@ -8,7 +8,8 @@
 #include "chrome/app/vector_icons/vector_icons.h"
 #include "chrome/browser/browser_process.h"
 #include "chrome/browser/extensions/extension_util.h"
-#include "chrome/browser/notifications/notification_ui_manager.h"
+#include "chrome/browser/notifications/notification_common.h"
+#include "chrome/browser/notifications/notification_display_service.h"
 #include "chrome/browser/ui/extensions/app_launch_params.h"
 #include "chrome/browser/ui/extensions/application_launch.h"
 #include "chrome/grit/generated_resources.h"
@@ -34,7 +35,6 @@
 void ExtensionInstalledNotification::Show(
     const extensions::Extension* extension, Profile* profile) {
   DCHECK_CURRENTLY_ON(BrowserThread::UI);
-  DCHECK(g_browser_process->notification_ui_manager());
 
   // It's lifetime is managed by the parent class NotificationDelegate.
   new ExtensionInstalledNotification(extension, profile);
@@ -43,8 +43,6 @@
 ExtensionInstalledNotification::ExtensionInstalledNotification(
     const extensions::Extension* extension, Profile* profile)
     : extension_id_(extension->id()), profile_(profile) {
-
-  message_center::RichNotificationData optional_field;
   message_center::Notification notification(
       message_center::NOTIFICATION_TYPE_SIMPLE, extension_id_,
       base::UTF8ToUTF16(extension->name()),
@@ -55,7 +53,7 @@
       GURL(extension_urls::kChromeWebstoreBaseURL) /* origin_url */,
       message_center::NotifierId(message_center::NotifierId::SYSTEM_COMPONENT,
                                  kNotifierId),
-      optional_field, this);
+      {}, this);
   if (message_center::IsNewStyleNotificationEnabled()) {
     notification.set_icon(gfx::Image());
     notification.set_accent_color(
@@ -65,7 +63,8 @@
                               message_center::kSystemNotificationColorNormal)));
     notification.set_vector_small_image(kNotificationInstalledIcon);
   }
-  g_browser_process->notification_ui_manager()->Add(notification, profile_);
+  NotificationDisplayService::GetForProfile(profile_)->Display(
+      NotificationHandler::Type::TRANSIENT, notification);
 }
 
 ExtensionInstalledNotification::~ExtensionInstalledNotification() {}
diff --git a/chrome/browser/vr/BUILD.gn b/chrome/browser/vr/BUILD.gn
index afadab0..f1971bf 100644
--- a/chrome/browser/vr/BUILD.gn
+++ b/chrome/browser/vr/BUILD.gn
@@ -57,6 +57,8 @@
     "elements/grid.h",
     "elements/invisible_hit_target.cc",
     "elements/invisible_hit_target.h",
+    "elements/keyboard.cc",
+    "elements/keyboard.h",
     "elements/laser.cc",
     "elements/laser.h",
     "elements/linear_layout.cc",
@@ -122,10 +124,13 @@
     "gltf_asset.h",
     "gltf_parser.cc",
     "gltf_parser.h",
+    "keyboard_delegate.h",
+    "keyboard_ui_interface.h",
     "macros.h",
     "metrics_helper.cc",
     "metrics_helper.h",
     "mode.h",
+    "model/camera_model.h",
     "model/color_scheme.cc",
     "model/color_scheme.h",
     "model/controller_model.h",
@@ -138,6 +143,8 @@
     "model/permissions_model.h",
     "model/reticle_model.h",
     "model/speech_recognition_model.h",
+    "model/text_input_info.cc",
+    "model/text_input_info.h",
     "model/toolbar_state.cc",
     "model/toolbar_state.h",
     "model/web_vr_timeout_state.h",
@@ -169,6 +176,8 @@
     "speech_recognizer.h",
     "target_property.cc",
     "target_property.h",
+    "text_input_delegate.cc",
+    "text_input_delegate.h",
     "toolbar_helper.cc",
     "toolbar_helper.h",
     "transition.cc",
@@ -260,6 +269,7 @@
     "test/run_all_unittests.cc",
     "test/ui_test.cc",
     "test/ui_test.h",
+    "text_input_unittest.cc",
     "ui_input_manager_unittest.cc",
     "ui_scene_unittest.cc",
     "ui_unittest.cc",
diff --git a/chrome/browser/vr/elements/keyboard.cc b/chrome/browser/vr/elements/keyboard.cc
new file mode 100644
index 0000000..01b6126
--- /dev/null
+++ b/chrome/browser/vr/elements/keyboard.cc
@@ -0,0 +1,99 @@
+// 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 "chrome/browser/vr/elements/keyboard.h"
+
+#include "chrome/browser/vr/controller_mesh.h"
+#include "chrome/browser/vr/model/controller_model.h"
+#include "chrome/browser/vr/ui_element_renderer.h"
+
+namespace vr {
+
+Keyboard::Keyboard() {
+  set_name(kKeyboard);
+  SetVisibleImmediately(false);
+}
+
+Keyboard::~Keyboard() = default;
+
+void Keyboard::SetKeyboardDelegate(KeyboardDelegate* keyboard_delegate) {
+  delegate_ = keyboard_delegate;
+  UpdateDelegateVisibility();
+}
+
+void Keyboard::HitTest(const HitTestRequest& request,
+                       HitTestResult* result) const {
+  if (!delegate_)
+    return;
+
+  result->type = HitTestResult::Type::kNone;
+  gfx::Point3F hit_point;
+  if (!delegate_->HitTest(request.ray_origin, request.ray_target, &hit_point))
+    return;
+
+  float distance_to_plane = (hit_point - request.ray_origin).Length();
+  if (distance_to_plane < 0 ||
+      distance_to_plane > request.max_distance_to_plane) {
+    return;
+  }
+
+  result->type = HitTestResult::Type::kHits;
+  result->distance_to_plane = distance_to_plane;
+  result->hit_point = hit_point;
+  // The local hit point is unused since the keyboard delegate does the
+  // local  hittesting for us.
+  result->local_hit_point = gfx::PointF(0, 0);
+}
+
+void Keyboard::NotifyClientFloatAnimated(float value,
+                                         int target_property_id,
+                                         cc::Animation* animation) {
+  DCHECK(target_property_id == OPACITY);
+  UiElement::NotifyClientFloatAnimated(value, target_property_id, animation);
+  UpdateDelegateVisibility();
+}
+
+void Keyboard::NotifyClientTransformOperationsAnimated(
+    const cc::TransformOperations& operations,
+    int target_property_id,
+    cc::Animation* animation) {
+  UiElement::NotifyClientTransformOperationsAnimated(
+      operations, target_property_id, animation);
+  if (!delegate_)
+    return;
+
+  delegate_->SetTransform(LocalTransform());
+}
+
+bool Keyboard::OnBeginFrame(const base::TimeTicks& time,
+                            const gfx::Vector3dF& head_direction) {
+  if (!delegate_)
+    return false;
+
+  delegate_->OnBeginFrame();
+  // We return false here because any visible changes to the keyboard, such as
+  // hover effects and showing/hiding of the keyboard will be drawn by the
+  // controller's dirtyness, so it's safe to assume not visual changes here.
+  return false;
+}
+
+void Keyboard::Render(UiElementRenderer* renderer,
+                      const CameraModel& camera_model) const {
+  if (!delegate_)
+    return;
+
+  delegate_->Draw(camera_model);
+}
+
+void Keyboard::UpdateDelegateVisibility() {
+  if (!delegate_)
+    return;
+
+  if (opacity() > 0)
+    delegate_->ShowKeyboard();
+  else
+    delegate_->HideKeyboard();
+}
+
+}  // namespace vr
diff --git a/chrome/browser/vr/elements/keyboard.h b/chrome/browser/vr/elements/keyboard.h
new file mode 100644
index 0000000..a052730
--- /dev/null
+++ b/chrome/browser/vr/elements/keyboard.h
@@ -0,0 +1,47 @@
+// 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.
+
+#ifndef CHROME_BROWSER_VR_ELEMENTS_KEYBOARD_H_
+#define CHROME_BROWSER_VR_ELEMENTS_KEYBOARD_H_
+
+#include "base/macros.h"
+#include "chrome/browser/vr/elements/ui_element.h"
+#include "chrome/browser/vr/keyboard_delegate.h"
+
+namespace vr {
+
+// Represents the virtual keyboard. This element is a proxy to the
+// platform-specific keyboard implementation.
+class Keyboard : public UiElement {
+ public:
+  Keyboard();
+  ~Keyboard() override;
+
+  void SetKeyboardDelegate(KeyboardDelegate* keyboard_delegate);
+  void HitTest(const HitTestRequest& request,
+               HitTestResult* result) const final;
+  void NotifyClientFloatAnimated(float value,
+                                 int target_property_id,
+                                 cc::Animation* animation) override;
+  void NotifyClientTransformOperationsAnimated(
+      const cc::TransformOperations& operations,
+      int target_property_id,
+      cc::Animation* animation) override;
+
+ private:
+  bool OnBeginFrame(const base::TimeTicks& time,
+                    const gfx::Vector3dF& head_direction) override;
+  void Render(UiElementRenderer* renderer,
+              const CameraModel& camera_model) const final;
+
+  void UpdateDelegateVisibility();
+
+  KeyboardDelegate* delegate_ = nullptr;
+
+  DISALLOW_COPY_AND_ASSIGN(Keyboard);
+};
+
+}  // namespace vr
+
+#endif  // CHROME_BROWSER_VR_ELEMENTS_KEYBOARD_H_
diff --git a/chrome/browser/vr/elements/text_input.cc b/chrome/browser/vr/elements/text_input.cc
index 07d35b5c..cbd93505 100644
--- a/chrome/browser/vr/elements/text_input.cc
+++ b/chrome/browser/vr/elements/text_input.cc
@@ -83,39 +83,72 @@
 
 TextInput::TextInput(int maximum_width_pixels,
                      float font_height_meters,
-                     float text_width_meters)
+                     float text_width_meters,
+                     OnFocusChangedCallback focus_changed_callback,
+                     OnInputEditedCallback input_edit_callback)
     : TexturedElement(maximum_width_pixels),
       texture_(base::MakeUnique<TextInputTexture>(font_height_meters,
-                                                  text_width_meters)) {
+                                                  text_width_meters)),
+      focus_changed_callback_(focus_changed_callback),
+      input_edit_callback_(input_edit_callback) {
   SetSize(text_width_meters, font_height_meters);
 }
+
 TextInput::~TextInput() {}
 
-void TextInput::SetText(const base::string16& text) {
-  if (text_ == text)
+void TextInput::SetTextInputDelegate(TextInputDelegate* text_input_delegate) {
+  delegate_ = text_input_delegate;
+}
+
+bool TextInput::IsEditable() {
+  return true;
+}
+
+void TextInput::OnButtonUp(const gfx::PointF& position) {
+  if (!delegate_)
     return;
-  text_ = text;
-  texture_->SetText(text);
-  if (text_changed_callback_)
-    text_changed_callback_.Run(text);
+
+  delegate_->RequestFocus(id());
 }
 
-void TextInput::SetCursorPosition(int position) {
-  texture_->SetCursorPosition(position);
+void TextInput::OnFocusChanged(bool focused) {
+  focused_ = focused;
+  texture_->SetCursorVisible(focused);
+  // Update the keyboard with the current text.
+  if (delegate_ && focused)
+    delegate_->UpdateInput(text_info_);
+
+  focus_changed_callback_.Run(focused);
 }
 
-void TextInput::SetTextChangedCallback(const TextInputCallback& callback) {
-  text_changed_callback_ = callback;
+void TextInput::OnInputEdited(const TextInputInfo& info) {
+  input_edit_callback_.Run(info);
 }
 
+void TextInput::OnInputCommitted(const TextInputInfo& info) {}
+
 void TextInput::SetColor(SkColor color) {
   texture_->SetColor(color);
 }
 
+void TextInput::UpdateInput(const TextInputInfo& info) {
+  if (text_info_ == info)
+    return;
+
+  text_info_ = info;
+  texture_->SetText(info.text);
+  texture_->SetCursorPosition(info.selection_end);
+
+  if (delegate_ && focused_)
+    delegate_->UpdateInput(info);
+}
+
 bool TextInput::OnBeginFrame(const base::TimeTicks& time,
                              const gfx::Vector3dF& look_at) {
   base::TimeDelta delta = time - base::TimeTicks();
-  texture_->SetCursorVisible(delta.InMilliseconds() / 500 % 2);
+  if (focused_)
+    texture_->SetCursorVisible(delta.InMilliseconds() / 500 % 2);
+
   return false;
 }
 
diff --git a/chrome/browser/vr/elements/text_input.h b/chrome/browser/vr/elements/text_input.h
index c3416d4..f998643 100644
--- a/chrome/browser/vr/elements/text_input.h
+++ b/chrome/browser/vr/elements/text_input.h
@@ -10,6 +10,8 @@
 #include "base/callback.h"
 #include "chrome/browser/vr/elements/textured_element.h"
 #include "chrome/browser/vr/elements/ui_texture.h"
+#include "chrome/browser/vr/model/text_input_info.h"
+#include "chrome/browser/vr/text_input_delegate.h"
 #include "third_party/skia/include/core/SkColor.h"
 
 namespace vr {
@@ -21,17 +23,27 @@
 // the keyboard and omnibox.
 class TextInput : public TexturedElement {
  public:
+  // Called when this element recieves focus.
+  typedef base::RepeatingCallback<void(bool)> OnFocusChangedCallback;
+  // Called when the user enters text while this element is focused.
+  typedef base::RepeatingCallback<void(const TextInputInfo&)>
+      OnInputEditedCallback;
   TextInput(int maximum_width_pixels,
             float font_height_meters,
-            float text_width_meters);
+            float text_width_meters,
+            OnFocusChangedCallback focus_changed_callback,
+            OnInputEditedCallback input_edit_callback);
   ~TextInput() override;
 
-  void SetText(const base::string16& text);
-  void SetCursorPosition(int position);
-  void SetColor(SkColor color);
+  bool IsEditable() override;
+  void OnButtonUp(const gfx::PointF& position) override;
+  void OnFocusChanged(bool focused) override;
+  void OnInputEdited(const TextInputInfo& info) override;
+  void OnInputCommitted(const TextInputInfo& info) override;
 
-  typedef base::Callback<void(const base::string16& text)> TextInputCallback;
-  void SetTextChangedCallback(const TextInputCallback& callback);
+  void SetTextInputDelegate(TextInputDelegate* text_input_delegate);
+  void SetColor(SkColor color);
+  void UpdateInput(const TextInputInfo& info);
 
   bool OnBeginFrame(const base::TimeTicks& time,
                     const gfx::Vector3dF& look_at) override;
@@ -40,8 +52,11 @@
   UiTexture* GetTexture() const override;
 
   std::unique_ptr<TextInputTexture> texture_;
-  TextInputCallback text_changed_callback_;
-  base::string16 text_;
+  OnFocusChangedCallback focus_changed_callback_;
+  OnInputEditedCallback input_edit_callback_;
+  TextInputDelegate* delegate_ = nullptr;
+  TextInputInfo text_info_;
+  bool focused_ = false;
 
   DISALLOW_COPY_AND_ASSIGN(TextInput);
 };
diff --git a/chrome/browser/vr/elements/ui_element.cc b/chrome/browser/vr/elements/ui_element.cc
index a9585e0..39b56dd 100644
--- a/chrome/browser/vr/elements/ui_element.cc
+++ b/chrome/browser/vr/elements/ui_element.cc
@@ -11,6 +11,7 @@
 #include "base/stl_util.h"
 #include "base/strings/stringprintf.h"
 #include "base/time/time.h"
+#include "chrome/browser/vr/model/camera_model.h"
 #include "third_party/WebKit/public/platform/WebGestureEvent.h"
 #include "third_party/skia/include/core/SkRRect.h"
 #include "third_party/skia/include/core/SkRect.h"
@@ -148,6 +149,18 @@
 void UiElement::OnScrollEnd(std::unique_ptr<blink::WebGestureEvent> gesture,
                             const gfx::PointF& position) {}
 
+void UiElement::OnFocusChanged(bool focused) {
+  NOTREACHED();
+}
+
+void UiElement::OnInputEdited(const TextInputInfo& info) {
+  NOTREACHED();
+}
+
+void UiElement::OnInputCommitted(const TextInputInfo& info) {
+  NOTREACHED();
+}
+
 bool UiElement::PrepareToDraw() {
   return false;
 }
@@ -177,6 +190,10 @@
   return IsVisible() && hit_testable_;
 }
 
+bool UiElement::IsEditable() {
+  return false;
+}
+
 void UiElement::SetSize(float width, float height) {
   animation_player_.TransitionSizeTo(last_frame_time_, BOUNDS, size_,
                                      gfx::SizeF(width, height));
diff --git a/chrome/browser/vr/elements/ui_element.h b/chrome/browser/vr/elements/ui_element.h
index f51d9b7..34a2325 100644
--- a/chrome/browser/vr/elements/ui_element.h
+++ b/chrome/browser/vr/elements/ui_element.h
@@ -43,6 +43,7 @@
 class SkiaSurfaceProvider;
 class UiElementRenderer;
 struct CameraModel;
+struct TextInputInfo;
 
 enum LayoutAlignment {
   NONE = 0,
@@ -139,6 +140,10 @@
   // Indicates whether the element should be tested for cursor input.
   bool IsHitTestable() const;
 
+  // Indicates whether the element is an editable element. Editable elements
+  // such as the input field should override this.
+  virtual bool IsEditable();
+
   virtual void Render(UiElementRenderer* renderer,
                       const CameraModel& model) const;
 
@@ -172,7 +177,8 @@
 
   // Performs a hit test for the ray supplied in the request and populates the
   // result. The ray is in the world coordinate space.
-  void HitTest(const HitTestRequest& request, HitTestResult* result) const;
+  virtual void HitTest(const HitTestRequest& request,
+                       HitTestResult* result) const;
 
   int id() const { return id_; }
 
@@ -205,6 +211,11 @@
     event_handlers_ = event_handlers;
   }
 
+  // Editable elements should override these functions.
+  virtual void OnFocusChanged(bool focused);
+  virtual void OnInputEdited(const TextInputInfo& info);
+  virtual void OnInputCommitted(const TextInputInfo& info);
+
   gfx::SizeF size() const;
   void SetSize(float width, float hight);
   virtual void OnSetSize(gfx::SizeF size);
diff --git a/chrome/browser/vr/elements/ui_element_name.cc b/chrome/browser/vr/elements/ui_element_name.cc
index c93fad07..73fd82b31 100644
--- a/chrome/browser/vr/elements/ui_element_name.cc
+++ b/chrome/browser/vr/elements/ui_element_name.cc
@@ -27,6 +27,7 @@
     "kLaser",
     "kController",
     "kReticle",
+    "kKeyboard",
     "kBackplane",
     "kCeiling",
     "kFloor",
diff --git a/chrome/browser/vr/elements/ui_element_name.h b/chrome/browser/vr/elements/ui_element_name.h
index 658a647..debe1898 100644
--- a/chrome/browser/vr/elements/ui_element_name.h
+++ b/chrome/browser/vr/elements/ui_element_name.h
@@ -26,6 +26,7 @@
   kLaser,
   kController,
   kReticle,
+  kKeyboard,
   kBackplane,
   kCeiling,
   kFloor,
diff --git a/chrome/browser/vr/elements/ui_texture.cc b/chrome/browser/vr/elements/ui_texture.cc
index d5aa73a..a426b79 100644
--- a/chrome/browser/vr/elements/ui_texture.cc
+++ b/chrome/browser/vr/elements/ui_texture.cc
@@ -51,7 +51,7 @@
   Draw(canvas, texture_size);
 }
 
-bool UiTexture::HitTest(const gfx::PointF& point) const {
+bool UiTexture::LocalHitTest(const gfx::PointF& point) const {
   return false;
 }
 
diff --git a/chrome/browser/vr/elements/ui_texture.h b/chrome/browser/vr/elements/ui_texture.h
index d16c3887b..bf5c55c 100644
--- a/chrome/browser/vr/elements/ui_texture.h
+++ b/chrome/browser/vr/elements/ui_texture.h
@@ -36,7 +36,7 @@
   void DrawAndLayout(SkCanvas* canvas, const gfx::Size& texture_size);
   virtual gfx::Size GetPreferredTextureSize(int maximum_width) const = 0;
   virtual gfx::SizeF GetDrawnSize() const = 0;
-  virtual bool HitTest(const gfx::PointF& point) const;
+  virtual bool LocalHitTest(const gfx::PointF& point) const;
 
   bool dirty() const { return dirty_; }
 
diff --git a/chrome/browser/vr/keyboard_delegate.h b/chrome/browser/vr/keyboard_delegate.h
new file mode 100644
index 0000000..b2ccf9f0
--- /dev/null
+++ b/chrome/browser/vr/keyboard_delegate.h
@@ -0,0 +1,35 @@
+// 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.
+
+#ifndef CHROME_BROWSER_VR_KEYBOARD_DELEGATE_H_
+#define CHROME_BROWSER_VR_KEYBOARD_DELEGATE_H_
+
+#include "base/memory/weak_ptr.h"
+
+namespace gfx {
+class Point3F;
+class Transform;
+}  // namespace gfx
+
+namespace vr {
+
+struct CameraModel;
+
+class KeyboardDelegate {
+ public:
+  virtual ~KeyboardDelegate() {}
+
+  virtual void ShowKeyboard() = 0;
+  virtual void HideKeyboard() = 0;
+  virtual void SetTransform(const gfx::Transform&) = 0;
+  virtual bool HitTest(const gfx::Point3F& ray_origin,
+                       const gfx::Point3F& ray_target,
+                       gfx::Point3F* hit_position) = 0;
+  virtual void OnBeginFrame() {}
+  virtual void Draw(const CameraModel&) = 0;
+};
+
+}  // namespace vr
+
+#endif  // CHROME_BROWSER_VR_KEYBOARD_DELEGATE_H_
diff --git a/chrome/browser/vr/keyboard_ui_interface.h b/chrome/browser/vr/keyboard_ui_interface.h
new file mode 100644
index 0000000..d16c4b23
--- /dev/null
+++ b/chrome/browser/vr/keyboard_ui_interface.h
@@ -0,0 +1,26 @@
+// 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.
+
+#ifndef CHROME_BROWSER_VR_KEYBOARD_UI_INTERFACE_H_
+#define CHROME_BROWSER_VR_KEYBOARD_UI_INTERFACE_H_
+
+#include "chrome/browser/vr/model/text_input_info.h"
+
+namespace vr {
+
+// The keyboard communicates state changes to the VR UI via this interface. Note
+// that we have this interface to restrict the UI API to keyboard-specific
+// callback functions because the keyboard delegate doesn't need access to all
+// of the UI.
+class KeyboardUiInterface {
+ public:
+  virtual ~KeyboardUiInterface() {}
+  virtual void OnInputEdited(const TextInputInfo& info) = 0;
+  virtual void OnInputCommitted(const TextInputInfo& info) = 0;
+  virtual void OnKeyboardHidden() = 0;
+};
+
+}  // namespace vr
+
+#endif  // CHROME_BROWSER_VR_KEYBOARD_UI_INTERFACE_H_
diff --git a/chrome/browser/vr/model/model.h b/chrome/browser/vr/model/model.h
index 4ac415e..c0fc8e43 100644
--- a/chrome/browser/vr/model/model.h
+++ b/chrome/browser/vr/model/model.h
@@ -12,6 +12,7 @@
 #include "chrome/browser/vr/model/permissions_model.h"
 #include "chrome/browser/vr/model/reticle_model.h"
 #include "chrome/browser/vr/model/speech_recognition_model.h"
+#include "chrome/browser/vr/model/text_input_info.h"
 #include "chrome/browser/vr/model/toolbar_state.h"
 #include "chrome/browser/vr/model/web_vr_timeout_state.h"
 #include "chrome/browser/vr/ui_element_renderer.h"
@@ -60,6 +61,10 @@
   }
   WebVrTimeoutState web_vr_timeout_state = kWebVrNoTimeoutPending;
 
+  // Focused text state.
+  bool editing_input = false;
+  TextInputInfo omnibox_text_field_info;
+
   // Controller state.
   ControllerModel controller;
   ReticleModel reticle;
diff --git a/chrome/browser/vr/model/text_input_info.cc b/chrome/browser/vr/model/text_input_info.cc
new file mode 100644
index 0000000..263721d
--- /dev/null
+++ b/chrome/browser/vr/model/text_input_info.cc
@@ -0,0 +1,22 @@
+// 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 "chrome/browser/vr/model/text_input_info.h"
+
+namespace vr {
+
+TextInputInfo::TextInputInfo() : selection_start(0), selection_end(0) {}
+TextInputInfo::TextInputInfo(base::string16 t)
+    : text(t), selection_start(t.length()), selection_end(t.length()) {}
+
+bool TextInputInfo::operator==(const TextInputInfo& other) const {
+  return text == other.text && selection_start == other.selection_start &&
+         selection_end == other.selection_end;
+}
+
+bool TextInputInfo::operator!=(const TextInputInfo& other) const {
+  return !(*this == other);
+}
+
+}  // namespace vr
diff --git a/chrome/browser/vr/model/text_input_info.h b/chrome/browser/vr/model/text_input_info.h
new file mode 100644
index 0000000..3a6f836
--- /dev/null
+++ b/chrome/browser/vr/model/text_input_info.h
@@ -0,0 +1,35 @@
+// 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.
+
+#ifndef CHROME_BROWSER_VR_MODEL_TEXT_INPUT_INFO_H_
+#define CHROME_BROWSER_VR_MODEL_TEXT_INPUT_INFO_H_
+
+#include "base/strings/string16.h"
+
+namespace vr {
+
+// Represents the state of an editable text field.
+struct TextInputInfo {
+ public:
+  TextInputInfo();
+  explicit TextInputInfo(base::string16 t);
+
+  bool operator==(const TextInputInfo& other) const;
+  bool operator!=(const TextInputInfo& other) const;
+
+  // The value of the input field.
+  base::string16 text;
+
+  // The cursor position of the current selection start, or the caret position
+  // if nothing is selected.
+  int selection_start;
+
+  // The cursor position of the current selection end, or the caret position
+  // if nothing is selected.
+  int selection_end;
+};
+
+}  // namespace vr
+
+#endif  // CHROME_BROWSER_VR_MODEL_TEXT_INPUT_INFO_H_
diff --git a/chrome/browser/vr/test/ui_pixel_test.cc b/chrome/browser/vr/test/ui_pixel_test.cc
index 261e103..3dc9f59 100644
--- a/chrome/browser/vr/test/ui_pixel_test.cc
+++ b/chrome/browser/vr/test/ui_pixel_test.cc
@@ -56,7 +56,8 @@
 
 void UiPixelTest::MakeUi(const UiInitialState& ui_initial_state,
                          const ToolbarState& toolbar_state) {
-  ui_ = base::MakeUnique<Ui>(browser_.get(), nullptr, ui_initial_state);
+  ui_ = base::MakeUnique<Ui>(browser_.get(), nullptr, nullptr, nullptr,
+                             ui_initial_state);
   ui_->OnGlInitialized(content_texture_,
                        vr::UiElementRenderer::kTextureLocationLocal, true);
   ui_->GetBrowserUiWeakPtr()->SetToolbarState(toolbar_state);
diff --git a/chrome/browser/vr/test/ui_test.cc b/chrome/browser/vr/test/ui_test.cc
index db680d5..4fbeac6c 100644
--- a/chrome/browser/vr/test/ui_test.cc
+++ b/chrome/browser/vr/test/ui_test.cc
@@ -248,9 +248,9 @@
   ui_initial_state.in_cct = in_cct;
   ui_initial_state.in_web_vr = in_web_vr;
   ui_initial_state.web_vr_autopresentation_expected = web_vr_autopresented;
-  ui_ =
-      base::MakeUnique<Ui>(std::move(browser_.get()),
-                           std::move(content_input_delegate), ui_initial_state);
+  ui_ = base::MakeUnique<Ui>(std::move(browser_.get()),
+                             std::move(content_input_delegate), nullptr,
+                             nullptr, ui_initial_state);
   scene_ = ui_->scene();
   model_ = ui_->model_for_test();
 
diff --git a/chrome/browser/vr/testapp/vr_test_context.cc b/chrome/browser/vr/testapp/vr_test_context.cc
index f3726a2..d70b00c2 100644
--- a/chrome/browser/vr/testapp/vr_test_context.cc
+++ b/chrome/browser/vr/testapp/vr_test_context.cc
@@ -64,7 +64,7 @@
 
   base::i18n::InitializeICU();
 
-  ui_ = base::MakeUnique<Ui>(this, nullptr, UiInitialState());
+  ui_ = base::MakeUnique<Ui>(this, nullptr, nullptr, nullptr, UiInitialState());
   model_ = ui_->model_for_test();
 
   ToolbarState state(GURL("https://dangerous.com/dir/file.html"),
diff --git a/chrome/browser/vr/text_input_delegate.cc b/chrome/browser/vr/text_input_delegate.cc
new file mode 100644
index 0000000..b293d2e
--- /dev/null
+++ b/chrome/browser/vr/text_input_delegate.cc
@@ -0,0 +1,34 @@
+// 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 "chrome/browser/vr/text_input_delegate.h"
+#include "chrome/browser/vr/model/text_input_info.h"
+
+namespace vr {
+
+TextInputDelegate::TextInputDelegate() {}
+
+TextInputDelegate::~TextInputDelegate() = default;
+
+void TextInputDelegate::SetRequestFocusCallback(
+    const RequestFocusCallback& callback) {
+  request_focus_callback_ = callback;
+}
+
+void TextInputDelegate::SetUpdateInputCallback(
+    const UpdateInputCallback& callback) {
+  update_input_callback_ = callback;
+}
+
+void TextInputDelegate::RequestFocus(int element_id) {
+  if (!request_focus_callback_.is_null())
+    request_focus_callback_.Run(element_id);
+}
+
+void TextInputDelegate::UpdateInput(const vr::TextInputInfo& info) {
+  if (!update_input_callback_.is_null())
+    update_input_callback_.Run(info);
+}
+
+}  // namespace vr
diff --git a/chrome/browser/vr/text_input_delegate.h b/chrome/browser/vr/text_input_delegate.h
new file mode 100644
index 0000000..001f386c6
--- /dev/null
+++ b/chrome/browser/vr/text_input_delegate.h
@@ -0,0 +1,42 @@
+// 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.
+
+#ifndef CHROME_BROWSER_VR_TEXT_INPUT_DELEGATE_H_
+#define CHROME_BROWSER_VR_TEXT_INPUT_DELEGATE_H_
+
+#include "base/callback.h"
+#include "base/macros.h"
+
+namespace vr {
+
+struct TextInputInfo;
+
+class TextInputDelegate {
+ public:
+  TextInputDelegate();
+  virtual ~TextInputDelegate();
+
+  // RequestFocusCallback gets called when an element request's focus.
+  typedef base::RepeatingCallback<void(int)> RequestFocusCallback;
+  // UpdateInputCallback gets called when the text input info changes for the
+  // element being edited.
+  typedef base::RepeatingCallback<void(const TextInputInfo&)>
+      UpdateInputCallback;
+
+  void SetRequestFocusCallback(const RequestFocusCallback& callback);
+  void SetUpdateInputCallback(const UpdateInputCallback& callback);
+
+  virtual void RequestFocus(int element_id);
+  virtual void UpdateInput(const TextInputInfo& info);
+
+ private:
+  RequestFocusCallback request_focus_callback_;
+  UpdateInputCallback update_input_callback_;
+
+  DISALLOW_COPY_AND_ASSIGN(TextInputDelegate);
+};
+
+}  // namespace vr
+
+#endif  // CHROME_BROWSER_VR_TEXT_INPUT_DELEGATE_H_
diff --git a/chrome/browser/vr/text_input_unittest.cc b/chrome/browser/vr/text_input_unittest.cc
new file mode 100644
index 0000000..094ad9b3
--- /dev/null
+++ b/chrome/browser/vr/text_input_unittest.cc
@@ -0,0 +1,148 @@
+// 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 "chrome/browser/vr/ui_input_manager.h"
+
+#include <memory>
+
+#include "base/memory/ptr_util.h"
+#include "base/strings/utf_string_conversions.h"
+#include "build/build_config.h"
+#include "chrome/browser/vr/databinding/binding.h"
+#include "chrome/browser/vr/elements/keyboard.h"
+#include "chrome/browser/vr/elements/ui_element.h"
+#include "chrome/browser/vr/keyboard_delegate.h"
+#include "chrome/browser/vr/model/camera_model.h"
+#include "chrome/browser/vr/model/model.h"
+#include "chrome/browser/vr/test/constants.h"
+#include "chrome/browser/vr/test/ui_test.h"
+#include "chrome/browser/vr/text_input_delegate.h"
+#include "chrome/browser/vr/ui_scene.h"
+#include "chrome/browser/vr/ui_scene_constants.h"
+#include "chrome/browser/vr/ui_scene_creator.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "third_party/WebKit/public/platform/WebGestureEvent.h"
+
+using ::testing::_;
+using ::testing::InSequence;
+using ::testing::StrictMock;
+
+namespace vr {
+
+namespace {
+
+#if defined(OS_LINUX)
+// The purpose here is to catch forgetting to update operator==. Ideally this
+// should go in TextInputInfo, but the size of base::string16 varies accross
+// platforms (i.e 24 bytes on linux and 20 bytes on android), so we can't add it
+// there in a generic way.
+static constexpr size_t kTextInputInfoSize = 32;
+static_assert(kTextInputInfoSize == sizeof(TextInputInfo),
+              "If new fields are added to TextInputInfo, we must explicitly "
+              "bump this size and update operator== below");
+#endif
+
+}  // namespace
+
+class MockTextInputDelegate : public TextInputDelegate {
+ public:
+  MockTextInputDelegate() = default;
+  ~MockTextInputDelegate() override = default;
+
+  MOCK_METHOD1(UpdateInput, void(const TextInputInfo& info));
+  MOCK_METHOD1(RequestFocus, void(int));
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(MockTextInputDelegate);
+};
+
+class MockKeyboardDelegate : public KeyboardDelegate {
+ public:
+  MockKeyboardDelegate() = default;
+  ~MockKeyboardDelegate() override = default;
+
+  MOCK_METHOD0(ShowKeyboard, void());
+  MOCK_METHOD0(HideKeyboard, void());
+  MOCK_METHOD1(SetTransform, void(const gfx::Transform&));
+  MOCK_METHOD3(HitTest,
+               bool(const gfx::Point3F&, const gfx::Point3F&, gfx::Point3F*));
+  MOCK_METHOD0(OnBeginFrame, void());
+  MOCK_METHOD1(Draw, void(const CameraModel&));
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(MockKeyboardDelegate);
+};
+
+class TextInputTest : public UiTest {
+ public:
+  void SetUp() override {
+    UiTest::SetUp();
+    CreateScene(kNotInCct, kNotInWebVr);
+
+    // Make test text input.
+    text_input_delegate_ =
+        base::MakeUnique<StrictMock<MockTextInputDelegate>>();
+    text_input_info_ = base::MakeUnique<TextInputInfo>();
+    auto text_input = UiSceneCreator::CreateTextInput(
+        512, 1, 1, model_, text_input_info_.get(), text_input_delegate_.get());
+    text_input_ = text_input.get();
+    scene_->AddUiElement(k2dBrowsingForeground, std::move(text_input));
+    EXPECT_TRUE(OnBeginFrame());
+  }
+
+ protected:
+  TextInput* text_input_;
+  std::unique_ptr<StrictMock<MockTextInputDelegate>> text_input_delegate_;
+  std::unique_ptr<TextInputInfo> text_input_info_;
+  testing::Sequence in_sequence_;
+};
+
+TEST_F(TextInputTest, InputFieldFocus) {
+  // Set mock delegates.
+  auto* kb = static_cast<Keyboard*>(scene_->GetUiElementByName(kKeyboard));
+  auto kb_delegate = base::MakeUnique<StrictMock<MockKeyboardDelegate>>();
+  EXPECT_CALL(*kb_delegate, HideKeyboard()).InSequence(in_sequence_);
+  kb->SetKeyboardDelegate(kb_delegate.get());
+
+  // Clicking on the text field should request focus.
+  EXPECT_CALL(*text_input_delegate_, RequestFocus(_)).InSequence(in_sequence_);
+  text_input_->OnButtonUp(gfx::PointF());
+
+  // Focusing on an input field should show the keyboard and tell the delegate
+  // the field's content.
+  EXPECT_CALL(*text_input_delegate_, UpdateInput(_)).InSequence(in_sequence_);
+  text_input_->OnFocusChanged(true);
+  EXPECT_CALL(*kb_delegate, ShowKeyboard()).InSequence(in_sequence_);
+  EXPECT_CALL(*kb_delegate, OnBeginFrame()).InSequence(in_sequence_);
+  EXPECT_CALL(*kb_delegate, SetTransform(_)).InSequence(in_sequence_);
+  EXPECT_TRUE(OnBeginFrame());
+
+  // Focusing out of an input field should hide the keyboard.
+  text_input_->OnFocusChanged(false);
+  EXPECT_CALL(*kb_delegate, HideKeyboard()).InSequence(in_sequence_);
+  EXPECT_CALL(*kb_delegate, OnBeginFrame()).InSequence(in_sequence_);
+  EXPECT_CALL(*kb_delegate, SetTransform(_)).InSequence(in_sequence_);
+  EXPECT_TRUE(OnBeginFrame());
+}
+
+TEST_F(TextInputTest, InputFieldEdit) {
+  // UpdateInput should not be called if the text input doesn't have focus.
+  EXPECT_CALL(*text_input_delegate_, UpdateInput(_))
+      .Times(0)
+      .InSequence(in_sequence_);
+  text_input_->OnFocusChanged(false);
+
+  // Focus on input field.
+  EXPECT_CALL(*text_input_delegate_, UpdateInput(_)).InSequence(in_sequence_);
+  text_input_->OnFocusChanged(true);
+
+  // Edits from the keyboard update the underlying text input  model.
+  EXPECT_CALL(*text_input_delegate_, UpdateInput(_)).InSequence(in_sequence_);
+  TextInputInfo info(base::ASCIIToUTF16("asdfg"));
+  text_input_->OnInputEdited(info);
+  EXPECT_TRUE(OnBeginFrame());
+  EXPECT_EQ(info, *text_input_info_);
+}
+
+}  // namespace vr
diff --git a/chrome/browser/vr/ui.cc b/chrome/browser/vr/ui.cc
index 9688e79..7a3c296 100644
--- a/chrome/browser/vr/ui.cc
+++ b/chrome/browser/vr/ui.cc
@@ -12,7 +12,9 @@
 #include "base/threading/thread_task_runner_handle.h"
 #include "chrome/browser/vr/content_input_delegate.h"
 #include "chrome/browser/vr/cpu_surface_provider.h"
+#include "chrome/browser/vr/elements/text_input.h"
 #include "chrome/browser/vr/ganesh_surface_provider.h"
+#include "chrome/browser/vr/keyboard_delegate.h"
 #include "chrome/browser/vr/model/model.h"
 #include "chrome/browser/vr/model/omnibox_suggestions.h"
 #include "chrome/browser/vr/speech_recognizer.h"
@@ -28,13 +30,19 @@
 
 Ui::Ui(UiBrowserInterface* browser,
        ContentInputForwarder* content_input_forwarder,
+       vr::KeyboardDelegate* keyboard_delegate,
+       vr::TextInputDelegate* text_input_delegate,
        const UiInitialState& ui_initial_state)
     : Ui(browser,
          base::MakeUnique<ContentInputDelegate>(content_input_forwarder),
+         keyboard_delegate,
+         text_input_delegate,
          ui_initial_state) {}
 
 Ui::Ui(UiBrowserInterface* browser,
        std::unique_ptr<ContentInputDelegate> content_input_delegate,
+       vr::KeyboardDelegate* keyboard_delegate,
+       vr::TextInputDelegate* text_input_delegate,
        const UiInitialState& ui_initial_state)
     : browser_(browser),
       scene_(base::MakeUnique<UiScene>()),
@@ -44,7 +52,7 @@
       weak_ptr_factory_(this) {
   InitializeModel(ui_initial_state);
   UiSceneCreator(browser, scene_.get(), content_input_delegate_.get(),
-                 model_.get())
+                 keyboard_delegate, text_input_delegate, model_.get())
       .CreateScene();
 }
 
@@ -185,6 +193,22 @@
   model_->content_location = content_location;
 }
 
+void Ui::RequestFocus(int element_id) {
+  input_manager_->RequestFocus(element_id);
+}
+
+void Ui::OnInputEdited(const TextInputInfo& info) {
+  input_manager_->OnInputEdited(info);
+}
+
+void Ui::OnInputCommitted(const TextInputInfo& info) {
+  input_manager_->OnInputCommitted(info);
+}
+
+void Ui::OnKeyboardHidden() {
+  input_manager_->OnKeyboardHidden();
+}
+
 void Ui::OnAppButtonClicked() {
   // App button clicks should be a no-op when auto-presenting WebVR.
   if (model_->web_vr_started_for_autopresentation) {
diff --git a/chrome/browser/vr/ui.h b/chrome/browser/vr/ui.h
index a1b8a7c..d0f2ed4 100644
--- a/chrome/browser/vr/ui.h
+++ b/chrome/browser/vr/ui.h
@@ -10,6 +10,7 @@
 #include "base/macros.h"
 #include "base/memory/weak_ptr.h"
 #include "chrome/browser/vr/browser_ui_interface.h"
+#include "chrome/browser/vr/keyboard_ui_interface.h"
 #include "chrome/browser/vr/platform_controller.h"
 #include "chrome/browser/vr/ui_element_renderer.h"
 
@@ -17,7 +18,9 @@
 class BrowserUiInterface;
 class ContentInputDelegate;
 class ContentInputForwarder;
+class KeyboardDelegate;
 class SkiaSurfaceProvider;
+class TextInputDelegate;
 class UiBrowserInterface;
 class UiInputManager;
 class UiRenderer;
@@ -38,14 +41,18 @@
 
 // This class manages all GLThread owned objects and GL rendering for VrShell.
 // It is not threadsafe and must only be used on the GL thread.
-class Ui : public BrowserUiInterface {
+class Ui : public BrowserUiInterface, public KeyboardUiInterface {
  public:
   Ui(UiBrowserInterface* browser,
      ContentInputForwarder* content_input_forwarder,
+     vr::KeyboardDelegate* keyboard_delegate,
+     vr::TextInputDelegate* text_input_delegate,
      const UiInitialState& ui_initial_state);
 
   Ui(UiBrowserInterface* browser,
      std::unique_ptr<ContentInputDelegate> content_input_delegate,
+     vr::KeyboardDelegate* keyboard_delegate,
+     vr::TextInputDelegate* text_input_delegate,
      const UiInitialState& ui_initial_state);
 
   ~Ui() override;
@@ -106,6 +113,12 @@
 
   void Dump();
 
+  // Keyboard input related.
+  void RequestFocus(int element_id);
+  void OnInputEdited(const TextInputInfo& info) override;
+  void OnInputCommitted(const TextInputInfo& info) override;
+  void OnKeyboardHidden() override;
+
  private:
   void InitializeModel(const UiInitialState& ui_initial_state);
   UiBrowserInterface* browser_;
diff --git a/chrome/browser/vr/ui_input_manager.cc b/chrome/browser/vr/ui_input_manager.cc
index 7fee3b3..7a883b4 100644
--- a/chrome/browser/vr/ui_input_manager.cc
+++ b/chrome/browser/vr/ui_input_manager.cc
@@ -11,6 +11,7 @@
 #include "chrome/browser/vr/elements/ui_element.h"
 #include "chrome/browser/vr/model/controller_model.h"
 #include "chrome/browser/vr/model/reticle_model.h"
+#include "chrome/browser/vr/model/text_input_info.h"
 #include "chrome/browser/vr/ui_renderer.h"
 #include "chrome/browser/vr/ui_scene.h"
 // TODO(tiborg): Remove include once we use a generic type to pass scroll/fling
@@ -322,6 +323,12 @@
   if (target) {
     target->OnButtonDown(target_point);
     input_locked_element_id_ = target->id();
+    // Clicking outside of the focused element causes it to lose focus.
+    // TODO(ymalik): We will lose focus if we hit an element inside the focused
+    // element, which is incorrect behavior.
+    if (target->id() != focused_element_id_ && target->name() != kKeyboard) {
+      UnfocusFocusedElement();
+    }
   } else {
     input_locked_element_id_ = 0;
   }
@@ -421,4 +428,47 @@
   }
 }
 
+void UiInputManager::UnfocusFocusedElement() {
+  if (!focused_element_id_)
+    return;
+
+  UiElement* focused = scene_->GetUiElementById(focused_element_id_);
+  if (focused && focused->IsEditable()) {
+    focused->OnFocusChanged(false);
+  }
+  focused_element_id_ = -1;
+}
+
+void UiInputManager::RequestFocus(int element_id) {
+  if (element_id == focused_element_id_)
+    return;
+
+  UnfocusFocusedElement();
+
+  UiElement* focused = scene_->GetUiElementById(element_id);
+  if (!focused || !focused->IsEditable())
+    return;
+
+  focused_element_id_ = element_id;
+  focused->OnFocusChanged(true);
+}
+
+void UiInputManager::OnInputEdited(const TextInputInfo& info) {
+  UiElement* focused = scene_->GetUiElementById(focused_element_id_);
+  if (!focused || !focused->IsEditable())
+    return;
+  focused->OnInputEdited(info);
+}
+
+void UiInputManager::OnInputCommitted(const TextInputInfo& info) {
+  UiElement* focused = scene_->GetUiElementById(focused_element_id_);
+  if (!focused || !focused->IsEditable())
+    return;
+  focused->OnInputCommitted(info);
+}
+
+void UiInputManager::OnKeyboardHidden() {
+  UnfocusFocusedElement();
+}
+
 }  // namespace vr
diff --git a/chrome/browser/vr/ui_input_manager.h b/chrome/browser/vr/ui_input_manager.h
index 0ed4aee..8fb3578 100644
--- a/chrome/browser/vr/ui_input_manager.h
+++ b/chrome/browser/vr/ui_input_manager.h
@@ -24,6 +24,7 @@
 class UiElement;
 struct ControllerModel;
 struct ReticleModel;
+struct TextInputInfo;
 
 using GestureList = std::vector<std::unique_ptr<blink::WebGestureEvent>>;
 
@@ -55,6 +56,12 @@
                    ReticleModel* reticle_model,
                    GestureList* gesture_list);
 
+  // Text input related.
+  void RequestFocus(int element_id);
+  void OnInputEdited(const TextInputInfo& info);
+  void OnInputCommitted(const TextInputInfo& info);
+  void OnKeyboardHidden();
+
   bool controller_quiescent() const { return controller_quiescent_; }
 
   void set_hit_test_strategy(HitTestStrategy strategy) {
@@ -86,6 +93,8 @@
   void UpdateQuiescenceState(base::TimeTicks current_time,
                              const ControllerModel& controller_model);
 
+  void UnfocusFocusedElement();
+
   UiScene* scene_;
   int hover_target_id_ = 0;
   // TODO(mthiesse): We shouldn't have a fling target. Elements should fling
@@ -95,8 +104,10 @@
   int input_locked_element_id_ = 0;
   bool in_click_ = false;
   bool in_scroll_ = false;
+  int focused_element_id_ = 0;
 
   HitTestStrategy hit_test_strategy_ = HitTestStrategy::PROJECT_TO_WORLD_ORIGIN;
+
   ButtonState previous_button_state_ = ButtonState::UP;
 
   base::TimeTicks last_significant_controller_update_time_;
diff --git a/chrome/browser/vr/ui_input_manager_unittest.cc b/chrome/browser/vr/ui_input_manager_unittest.cc
index f73b45f..8c2bf22 100644
--- a/chrome/browser/vr/ui_input_manager_unittest.cc
+++ b/chrome/browser/vr/ui_input_manager_unittest.cc
@@ -7,6 +7,7 @@
 #include <memory>
 
 #include "base/memory/ptr_util.h"
+#include "base/strings/utf_string_conversions.h"
 #include "base/time/time.h"
 #include "cc/test/geometry_test_utils.h"
 #include "chrome/browser/vr/content_input_delegate.h"
@@ -51,6 +52,24 @@
   DISALLOW_COPY_AND_ASSIGN(MockRect);
 };
 
+class MockTextInput : public TextInput {
+ public:
+  MockTextInput()
+      : TextInput(512,
+                  1,
+                  1,
+                  base::RepeatingCallback<void(bool)>(),
+                  base::RepeatingCallback<void(const TextInputInfo&)>()) {}
+  ~MockTextInput() override = default;
+
+  MOCK_METHOD1(OnFocusChanged, void(bool));
+  MOCK_METHOD1(OnInputEdited, void(const TextInputInfo&));
+  MOCK_METHOD1(OnInputCommitted, void(const TextInputInfo&));
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(MockTextInput);
+};
+
 class UiInputManagerTest : public testing::Test {
  public:
   void SetUp() override {
@@ -68,6 +87,16 @@
     return p_element;
   }
 
+  StrictMock<MockTextInput>* CreateMockInputElement(float z_position) {
+    auto element = base::MakeUnique<StrictMock<MockTextInput>>();
+    StrictMock<MockTextInput>* p_element = element.get();
+    element->SetTranslate(0, 0, z_position);
+    element->SetVisible(true);
+    scene_->AddUiElement(kRoot, std::move(element));
+    scene_->OnBeginFrame(base::TimeTicks(), kForwardVector);
+    return p_element;
+  }
+
   void HandleInput(const gfx::Vector3dF& laser_direction,
                    UiInputManager::ButtonState button_state) {
     HandleInput({0, 0, 0}, laser_direction, button_state);
@@ -104,6 +133,30 @@
   UiInputManager* input_manager_;
 };
 
+TEST_F(UiInputManagerTest, FocusedElement) {
+  StrictMock<MockTextInput>* p_element1 = CreateMockInputElement(-5.f);
+  StrictMock<MockTextInput>* p_element2 = CreateMockInputElement(-5.f);
+  TextInputInfo edit(base::ASCIIToUTF16("asdfg"));
+
+  // Focus request triggers OnFocusChanged.
+  testing::Sequence s;
+  EXPECT_CALL(*p_element1, OnFocusChanged(true)).InSequence(s);
+  input_manager_->RequestFocus(p_element1->id());
+
+  // Edit goes to focused element.
+  EXPECT_CALL(*p_element1, OnInputEdited(edit)).InSequence(s);
+  input_manager_->OnInputEdited(edit);
+
+  // Commit goes to focused element.
+  EXPECT_CALL(*p_element1, OnInputCommitted(edit)).InSequence(s);
+  input_manager_->OnInputCommitted(edit);
+
+  // Focus on a different element triggers OnFocusChanged.
+  EXPECT_CALL(*p_element1, OnFocusChanged(false)).InSequence(s);
+  EXPECT_CALL(*p_element2, OnFocusChanged(true)).InSequence(s);
+  input_manager_->RequestFocus(p_element2->id());
+}
+
 TEST_F(UiInputManagerTest, ReticleRenderTarget) {
   auto element = base::MakeUnique<Rect>();
   UiElement* p_element = element.get();
diff --git a/chrome/browser/vr/ui_renderer.cc b/chrome/browser/vr/ui_renderer.cc
index 57aeacd..507797c 100644
--- a/chrome/browser/vr/ui_renderer.cc
+++ b/chrome/browser/vr/ui_renderer.cc
@@ -23,7 +23,7 @@
 void UiRenderer::Draw(const RenderInfo& render_info) {
   Draw2dBrowsing(render_info);
   DrawSplashScreen(render_info);
-  DrawController(render_info);
+  DrawInputModalElements(render_info);
 }
 
 void UiRenderer::Draw2dBrowsing(const RenderInfo& render_info) {
@@ -66,13 +66,16 @@
   // another buffer that is size optimized.
 }
 
-void UiRenderer::DrawController(const RenderInfo& render_info) {
+void UiRenderer::DrawInputModalElements(const RenderInfo& render_info) {
+  const auto& keyboard_elements = scene_->GetVisibleKeyboardElements();
   const auto& controller_elements = scene_->GetVisibleControllerElements();
-  if (controller_elements.empty())
-    return;
-
-  glEnable(GL_CULL_FACE);
-  DrawUiView(render_info, controller_elements);
+  if (!keyboard_elements.empty()) {
+    DrawUiView(render_info, keyboard_elements);
+  }
+  if (!controller_elements.empty()) {
+    glEnable(GL_CULL_FACE);
+    DrawUiView(render_info, controller_elements);
+  }
 }
 
 void UiRenderer::DrawWebVrOverlayForeground(const RenderInfo& render_info) {
diff --git a/chrome/browser/vr/ui_renderer.h b/chrome/browser/vr/ui_renderer.h
index 70f4350..b33ba6ca 100644
--- a/chrome/browser/vr/ui_renderer.h
+++ b/chrome/browser/vr/ui_renderer.h
@@ -44,7 +44,7 @@
  private:
   void Draw2dBrowsing(const RenderInfo& render_info);
   void DrawSplashScreen(const RenderInfo& render_info);
-  void DrawController(const RenderInfo& render_info);
+  void DrawInputModalElements(const RenderInfo& render_info);
 
   void DrawUiView(const RenderInfo& render_info,
                   const std::vector<const UiElement*>& elements);
diff --git a/chrome/browser/vr/ui_scene.cc b/chrome/browser/vr/ui_scene.cc
index ffb789b..378464e 100644
--- a/chrome/browser/vr/ui_scene.cc
+++ b/chrome/browser/vr/ui_scene.cc
@@ -15,6 +15,7 @@
 #include "base/values.h"
 #include "chrome/browser/vr/databinding/binding_base.h"
 #include "chrome/browser/vr/elements/draw_phase.h"
+#include "chrome/browser/vr/elements/keyboard.h"
 #include "chrome/browser/vr/elements/reticle.h"
 #include "chrome/browser/vr/elements/ui_element.h"
 #include "ui/gfx/transform.h"
@@ -80,11 +81,8 @@
   CHECK_GE(element->id(), 0);
   CHECK_EQ(GetUiElementById(element->id()), nullptr);
   CHECK_GE(element->draw_phase(), 0);
-  if (gl_initialized_) {
-    for (auto& child : *element) {
-      child.Initialize(provider_);
-    }
-  }
+  if (gl_initialized_)
+    element->Initialize(provider_);
   GetUiElementByName(parent)->AddChild(std::move(element));
   is_dirty_ = true;
 }
@@ -267,6 +265,14 @@
       });
 }
 
+UiScene::Elements UiScene::GetVisibleKeyboardElements() const {
+  return GetVisibleElements(GetUiElementByName(kKeyboard),
+                            GetUiElementByName(kReticle),
+                            [](UiElement* element) {
+                              return element->draw_phase() == kPhaseForeground;
+                            });
+}
+
 UiScene::Elements UiScene::GetPotentiallyVisibleElements() const {
   UiScene::Elements elements;
   for (auto& element : *root_element_) {
@@ -289,9 +295,8 @@
 void UiScene::OnGlInitialized(SkiaSurfaceProvider* provider) {
   gl_initialized_ = true;
   provider_ = provider;
-
   for (auto& element : *root_element_)
-    element.Initialize(provider);
+    element.Initialize(provider_);
 }
 
 }  // namespace vr
diff --git a/chrome/browser/vr/ui_scene.h b/chrome/browser/vr/ui_scene.h
index bc7dffe..33a0773 100644
--- a/chrome/browser/vr/ui_scene.h
+++ b/chrome/browser/vr/ui_scene.h
@@ -13,6 +13,7 @@
 #include "chrome/browser/vr/elements/ui_element.h"
 #include "chrome/browser/vr/elements/ui_element_iterator.h"
 #include "chrome/browser/vr/elements/ui_element_name.h"
+#include "chrome/browser/vr/keyboard_delegate.h"
 #include "third_party/skia/include/core/SkColor.h"
 
 namespace base {
@@ -58,6 +59,7 @@
   Elements GetVisibleSplashScreenElements() const;
   Elements GetVisibleWebVrOverlayForegroundElements() const;
   Elements GetVisibleControllerElements() const;
+  Elements GetVisibleKeyboardElements() const;
   Elements GetPotentiallyVisibleElements() const;
 
   float background_distance() const { return background_distance_; }
@@ -74,6 +76,8 @@
   void OnGlInitialized(SkiaSurfaceProvider* provider);
 
  private:
+  void InitializeElement(UiElement* element);
+
   std::unique_ptr<UiElement> root_element_;
 
   float background_distance_ = 10.0f;
diff --git a/chrome/browser/vr/ui_scene_constants.h b/chrome/browser/vr/ui_scene_constants.h
index 66140bba..10c0e99 100644
--- a/chrome/browser/vr/ui_scene_constants.h
+++ b/chrome/browser/vr/ui_scene_constants.h
@@ -212,6 +212,9 @@
 
 static constexpr float kModalPromptFadeOpacity = 0.5f;
 
+static constexpr float kKeyboardDistance = 1.0f;
+static constexpr float kKeyboardVerticalOffset = -0.45f * kKeyboardDistance;
+
 }  // namespace vr
 
 #endif  // CHROME_BROWSER_VR_UI_SCENE_CONSTANTS_H_
diff --git a/chrome/browser/vr/ui_scene_creator.cc b/chrome/browser/vr/ui_scene_creator.cc
index ac6988b..620209d7 100644
--- a/chrome/browser/vr/ui_scene_creator.cc
+++ b/chrome/browser/vr/ui_scene_creator.cc
@@ -19,6 +19,7 @@
 #include "chrome/browser/vr/elements/full_screen_rect.h"
 #include "chrome/browser/vr/elements/grid.h"
 #include "chrome/browser/vr/elements/invisible_hit_target.h"
+#include "chrome/browser/vr/elements/keyboard.h"
 #include "chrome/browser/vr/elements/laser.h"
 #include "chrome/browser/vr/elements/linear_layout.h"
 #include "chrome/browser/vr/elements/rect.h"
@@ -38,6 +39,7 @@
 #include "chrome/browser/vr/elements/vector_icon.h"
 #include "chrome/browser/vr/elements/viewport_aware_root.h"
 #include "chrome/browser/vr/elements/webvr_url_toast.h"
+#include "chrome/browser/vr/keyboard_delegate.h"
 #include "chrome/browser/vr/model/model.h"
 #include "chrome/browser/vr/speech_recognizer.h"
 #include "chrome/browser/vr/target_property.h"
@@ -204,10 +206,14 @@
 UiSceneCreator::UiSceneCreator(UiBrowserInterface* browser,
                                UiScene* scene,
                                ContentInputDelegate* content_input_delegate,
+                               KeyboardDelegate* keyboard_delegate,
+                               TextInputDelegate* text_input_delegate,
                                Model* model)
     : browser_(browser),
       scene_(scene),
       content_input_delegate_(content_input_delegate),
+      keyboard_delegate_(keyboard_delegate),
+      text_input_delegate_(text_input_delegate),
       model_(model) {}
 
 UiSceneCreator::~UiSceneCreator() {}
@@ -232,6 +238,7 @@
   CreateUnderDevelopmentNotice();
   CreateVoiceSearchUiGroup();
   CreateController();
+  CreateKeyboard();
 }
 
 void UiSceneCreator::Create2dBrowsingSubtreeRoots() {
@@ -971,6 +978,47 @@
   scene_->AddUiElement(kControllerGroup, std::move(reticle));
 }
 
+std::unique_ptr<TextInput> UiSceneCreator::CreateTextInput(
+    int maximum_width_pixels,
+    float font_height_meters,
+    float text_width_meters,
+    Model* model,
+    TextInputInfo* text_input_model,
+    TextInputDelegate* text_input_delegate) {
+  auto text_input = base::MakeUnique<TextInput>(
+      maximum_width_pixels, font_height_meters, text_width_meters,
+      base::BindRepeating(
+          [](Model* model, bool focused) { model->editing_input = focused; },
+          base::Unretained(model)),
+      base::BindRepeating(
+          [](TextInputInfo* model, const TextInputInfo& text_input_info) {
+            *model = text_input_info;
+          },
+          base::Unretained(text_input_model)));
+  text_input->set_draw_phase(kPhaseForeground);
+  text_input->SetTextInputDelegate(text_input_delegate);
+  text_input->set_hit_testable(true);
+  text_input->AddBinding(base::MakeUnique<Binding<TextInputInfo>>(
+      base::BindRepeating([](TextInputInfo* info) { return *info; },
+                          base::Unretained(text_input_model)),
+      base::BindRepeating(
+          [](TextInput* e, const TextInputInfo& value) {
+            e->UpdateInput(value);
+          },
+          base::Unretained(text_input.get()))));
+  return text_input;
+}
+
+void UiSceneCreator::CreateKeyboard() {
+  auto keyboard = base::MakeUnique<Keyboard>();
+  keyboard->SetKeyboardDelegate(keyboard_delegate_);
+  keyboard->set_draw_phase(kPhaseForeground);
+  keyboard->SetTranslate(0.0, kKeyboardVerticalOffset, -kKeyboardDistance);
+  keyboard->AddBinding(VR_BIND_FUNC(bool, Model, model_, editing_input,
+                                    UiElement, keyboard.get(), SetVisible));
+  scene_->AddUiElement(kRoot, std::move(keyboard));
+}
+
 void UiSceneCreator::CreateUrlBar() {
   auto url_bar = base::MakeUnique<UrlBar>(
       512,
@@ -1067,16 +1115,16 @@
 
   float width = kOmniboxWidthDMM - 2 * kOmniboxTextMarginDMM;
   auto omnibox_text_field =
-      base::MakeUnique<TextInput>(1024, kOmniboxTextHeightDMM, width);
+      CreateTextInput(1024, kOmniboxTextHeightDMM, width, model_,
+                      &model_->omnibox_text_field_info, text_input_delegate_);
+  omnibox_text_field->AddBinding(
+      VR_BIND(TextInputInfo, Model, model_, omnibox_text_field_info,
+              UiBrowserInterface, browser_, StartAutocomplete(value.text)));
   omnibox_text_field->SetSize(width, 0);
   omnibox_text_field->set_name(kOmniboxTextField);
-  omnibox_text_field->set_draw_phase(kPhaseForeground);
   omnibox_text_field->set_x_anchoring(LEFT);
   omnibox_text_field->set_x_centering(LEFT);
   omnibox_text_field->SetTranslate(kOmniboxTextMarginDMM, 0, 0);
-
-  omnibox_text_field->SetTextChangedCallback(base::Bind(
-      &UiBrowserInterface::StartAutocomplete, base::Unretained(browser_)));
   scene_->AddUiElement(kOmniboxContainer, std::move(omnibox_text_field));
 
   auto close_button = Create<Button>(
diff --git a/chrome/browser/vr/ui_scene_creator.h b/chrome/browser/vr/ui_scene_creator.h
index 3d7ce7d..e1762c5 100644
--- a/chrome/browser/vr/ui_scene_creator.h
+++ b/chrome/browser/vr/ui_scene_creator.h
@@ -6,7 +6,9 @@
 #define CHROME_BROWSER_VR_UI_SCENE_CREATOR_H_
 
 #include "base/macros.h"
+#include "chrome/browser/vr/elements/text_input.h"
 #include "chrome/browser/vr/elements/ui_element_name.h"
+#include "chrome/browser/vr/keyboard_delegate.h"
 
 namespace vr {
 
@@ -21,11 +23,21 @@
   UiSceneCreator(UiBrowserInterface* browser,
                  UiScene* scene,
                  ContentInputDelegate* content_input_delegate,
+                 KeyboardDelegate* keyboard_delegate,
+                 TextInputDelegate* text_input_delegate,
                  Model* model);
   ~UiSceneCreator();
 
   void CreateScene();
 
+  static std::unique_ptr<TextInput> CreateTextInput(
+      int maximum_width_pixels,
+      float font_height_meters,
+      float text_width_meters,
+      Model* model,
+      TextInputInfo* text_input_model,
+      TextInputDelegate* text_input_delegate);
+
  private:
   void Create2dBrowsingSubtreeRoots();
   void CreateWebVrRoot();
@@ -46,10 +58,13 @@
   void CreateToasts();
   void CreateVoiceSearchUiGroup();
   void CreateController();
+  void CreateKeyboard();
 
   UiBrowserInterface* browser_;
   UiScene* scene_;
   ContentInputDelegate* content_input_delegate_;
+  KeyboardDelegate* keyboard_delegate_;
+  TextInputDelegate* text_input_delegate_;
   Model* model_;
 
   DISALLOW_COPY_AND_ASSIGN(UiSceneCreator);
diff --git a/chrome/browser/vr/ui_unittest.cc b/chrome/browser/vr/ui_unittest.cc
index 5f294aba..3b5ed0c 100644
--- a/chrome/browser/vr/ui_unittest.cc
+++ b/chrome/browser/vr/ui_unittest.cc
@@ -54,6 +54,7 @@
     kFloor,
     kCeiling,
     kBackplane,
+    kKeyboard,
     kContentQuad,
     kAudioCaptureIndicator,
     kVideoCaptureIndicator,
@@ -140,6 +141,7 @@
     "kController",
     "kLaser",
     "kReticle",
+    "kKeyboard",
     "kExitWarning",
     "kWebVrUrlToast",
     "kExclusiveScreenToastViewportAware",
diff --git a/components/wallpaper/wallpaper_info.h b/components/wallpaper/wallpaper_info.h
index d323882..054faf7 100644
--- a/components/wallpaper/wallpaper_info.h
+++ b/components/wallpaper/wallpaper_info.h
@@ -63,7 +63,7 @@
 
   ~WallpaperInfo() {}
 
-  bool operator==(const WallpaperInfo& other) {
+  bool operator==(const WallpaperInfo& other) const {
     return (location == other.location) && (layout == other.layout) &&
            (type == other.type);
   }
diff --git a/content/browser/frame_host/render_frame_host_impl.cc b/content/browser/frame_host/render_frame_host_impl.cc
index b472d9d..b7509cd 100644
--- a/content/browser/frame_host/render_frame_host_impl.cc
+++ b/content/browser/frame_host/render_frame_host_impl.cc
@@ -916,8 +916,6 @@
                         OnDidAddContentSecurityPolicies)
     IPC_MESSAGE_HANDLER(FrameHostMsg_EnforceInsecureRequestPolicy,
                         OnEnforceInsecureRequestPolicy)
-    IPC_MESSAGE_HANDLER(FrameHostMsg_UpdateToUniqueOrigin,
-                        OnUpdateToUniqueOrigin)
     IPC_MESSAGE_HANDLER(FrameHostMsg_DidChangeFramePolicy,
                         OnDidChangeFramePolicy)
     IPC_MESSAGE_HANDLER(FrameHostMsg_DidChangeFrameOwnerProperties,
@@ -2272,17 +2270,6 @@
   frame_tree_node()->SetInsecureRequestPolicy(policy);
 }
 
-void RenderFrameHostImpl::OnUpdateToUniqueOrigin(
-    bool is_potentially_trustworthy_unique_origin) {
-  TRACE_EVENT1("navigation", "RenderFrameHostImpl::OnUpdateToUniqueOrigin",
-               "frame_tree_node", frame_tree_node_->frame_tree_node_id());
-
-  url::Origin origin;
-  DCHECK(origin.unique());
-  frame_tree_node()->SetCurrentOrigin(origin,
-                                      is_potentially_trustworthy_unique_origin);
-}
-
 FrameTreeNode* RenderFrameHostImpl::FindAndVerifyChild(
     int32_t child_frame_routing_id,
     bad_message::BadMessageReason reason) {
diff --git a/content/browser/frame_host/render_frame_host_impl.h b/content/browser/frame_host/render_frame_host_impl.h
index a2db91d..2f1613d 100644
--- a/content/browser/frame_host/render_frame_host_impl.h
+++ b/content/browser/frame_host/render_frame_host_impl.h
@@ -807,7 +807,6 @@
       const std::vector<ContentSecurityPolicy>& policies);
 
   void OnEnforceInsecureRequestPolicy(blink::WebInsecureRequestPolicy policy);
-  void OnUpdateToUniqueOrigin(bool is_potentially_trustworthy_unique_origin);
   void OnDidChangeFramePolicy(int32_t frame_routing_id,
                               const blink::FramePolicy& frame_policy);
   void OnDidChangeFrameOwnerProperties(int32_t frame_routing_id,
diff --git a/content/common/frame_messages.h b/content/common/frame_messages.h
index e939706..ebc3a5b 100644
--- a/content/common/frame_messages.h
+++ b/content/common/frame_messages.h
@@ -1189,13 +1189,6 @@
 IPC_MESSAGE_ROUTED1(FrameHostMsg_EnforceInsecureRequestPolicy,
                     blink::WebInsecureRequestPolicy)
 
-// Sent when the frame is set to a unique origin. TODO(estark): this IPC
-// only exists to support dynamic sandboxing via a CSP delivered in a
-// <meta> tag. This is not supposed to be allowed per the CSP spec and
-// should be ripped out. https://crbug.com/594645
-IPC_MESSAGE_ROUTED1(FrameHostMsg_UpdateToUniqueOrigin,
-                    bool /* is potentially trustworthy unique origin */)
-
 // Sent when the renderer changed the progress of a load.
 IPC_MESSAGE_ROUTED1(FrameHostMsg_DidChangeLoadProgress,
                     double /* load_progress */)
diff --git a/content/renderer/render_frame_impl.cc b/content/renderer/render_frame_impl.cc
index 20785b44..bd485e59 100644
--- a/content/renderer/render_frame_impl.cc
+++ b/content/renderer/render_frame_impl.cc
@@ -3578,12 +3578,6 @@
   Send(new FrameHostMsg_EnforceInsecureRequestPolicy(routing_id_, policy));
 }
 
-void RenderFrameImpl::DidUpdateToUniqueOrigin(
-    bool is_potentially_trustworthy_unique_origin) {
-  Send(new FrameHostMsg_UpdateToUniqueOrigin(
-      routing_id_, is_potentially_trustworthy_unique_origin));
-}
-
 void RenderFrameImpl::DidChangeFramePolicy(
     blink::WebFrame* child_frame,
     blink::WebSandboxFlags flags,
diff --git a/content/renderer/render_frame_impl.h b/content/renderer/render_frame_impl.h
index a06fdcd..01a93e2 100644
--- a/content/renderer/render_frame_impl.h
+++ b/content/renderer/render_frame_impl.h
@@ -571,8 +571,6 @@
   void DidChangeName(const blink::WebString& name) override;
   void DidEnforceInsecureRequestPolicy(
       blink::WebInsecureRequestPolicy policy) override;
-  void DidUpdateToUniqueOrigin(
-      bool is_potentially_trustworthy_unique_origin) override;
   void DidChangeFramePolicy(
       blink::WebFrame* child_frame,
       blink::WebSandboxFlags flags,
diff --git a/content/renderer/render_frame_proxy.cc b/content/renderer/render_frame_proxy.cc
index 44c2ee0..6d74f5d7 100644
--- a/content/renderer/render_frame_proxy.cc
+++ b/content/renderer/render_frame_proxy.cc
@@ -268,12 +268,32 @@
 
 void RenderFrameProxy::SetReplicatedState(const FrameReplicationState& state) {
   DCHECK(web_frame_);
-  web_frame_->SetReplicatedOrigin(state.origin);
+
+  web_frame_->SetReplicatedOrigin(
+      state.origin, state.has_potentially_trustworthy_unique_origin);
+
+#if DCHECK_IS_ON()
+  blink::WebSecurityOrigin security_origin_before_sandbox_flags =
+      web_frame_->GetSecurityOrigin();
+#endif
+
   web_frame_->SetReplicatedSandboxFlags(state.active_sandbox_flags);
+
+#if DCHECK_IS_ON()
+  // If |state.has_potentially_trustworthy_unique_origin| is set,
+  // - |state.origin| should be unique (this is checked in
+  //   blink::SecurityOrigin::SetUniqueOriginIsPotentiallyTrustworthy() in
+  //   SetReplicatedOrigin()), and thus
+  // - The security origin is not updated by SetReplicatedSandboxFlags() and
+  //   thus we don't have to apply |has_potentially_trustworthy_unique_origin|
+  //   flag after SetReplicatedSandboxFlags().
+  if (state.has_potentially_trustworthy_unique_origin)
+    DCHECK(security_origin_before_sandbox_flags ==
+           web_frame_->GetSecurityOrigin());
+#endif
+
   web_frame_->SetReplicatedName(blink::WebString::FromUTF8(state.name));
   web_frame_->SetReplicatedInsecureRequestPolicy(state.insecure_request_policy);
-  web_frame_->SetReplicatedPotentiallyTrustworthyUniqueOrigin(
-      state.has_potentially_trustworthy_unique_origin);
   web_frame_->SetReplicatedFeaturePolicyHeader(state.feature_policy_header);
   if (state.has_received_user_gesture)
     web_frame_->SetHasReceivedUserGesture();
@@ -464,9 +484,8 @@
 void RenderFrameProxy::OnDidUpdateOrigin(
     const url::Origin& origin,
     bool is_potentially_trustworthy_unique_origin) {
-  web_frame_->SetReplicatedOrigin(origin);
-  web_frame_->SetReplicatedPotentiallyTrustworthyUniqueOrigin(
-      is_potentially_trustworthy_unique_origin);
+  web_frame_->SetReplicatedOrigin(origin,
+                                  is_potentially_trustworthy_unique_origin);
 }
 
 void RenderFrameProxy::OnSetPageFocus(bool is_focused) {
diff --git a/gpu/config/gpu_driver_bug_list.json b/gpu/config/gpu_driver_bug_list.json
index 873db32..179ee65 100644
--- a/gpu/config/gpu_driver_bug_list.json
+++ b/gpu/config/gpu_driver_bug_list.json
@@ -1897,6 +1897,10 @@
       "os": {
         "type": "win"
       },
+      "driver_version": {
+        "op": "<",
+        "value": "21.21.13.7576"
+      },
       "vendor_id": "0x10de",
       "features": [
         "disable_dxgi_zero_copy_video"
diff --git a/net/http/http_stream_factory_impl_job_controller.cc b/net/http/http_stream_factory_impl_job_controller.cc
index 94e72c27..262b34ed 100644
--- a/net/http/http_stream_factory_impl_job_controller.cc
+++ b/net/http/http_stream_factory_impl_job_controller.cc
@@ -567,7 +567,7 @@
 }
 
 void HttpStreamFactoryImpl::JobController::ResumeMainJob() {
-  if (main_job_is_resumed_)
+  if (main_job_is_resumed_ || !main_job_)
     return;
 
   main_job_is_resumed_ = true;
diff --git a/third_party/WebKit/Source/core/dom/Document.cpp b/third_party/WebKit/Source/core/dom/Document.cpp
index 7f7457c..3d35a5a1 100644
--- a/third_party/WebKit/Source/core/dom/Document.cpp
+++ b/third_party/WebKit/Source/core/dom/Document.cpp
@@ -6252,8 +6252,6 @@
       GetSecurityOrigin()->IsUnique()) {
     GetSecurityOrigin()->SetUniqueOriginIsPotentiallyTrustworthy(
         stand_in_origin->IsPotentiallyTrustworthy());
-    if (GetFrame())
-      GetFrame()->Client()->DidUpdateToUniqueOrigin();
   }
 }
 
diff --git a/third_party/WebKit/Source/core/exported/LocalFrameClientImpl.cpp b/third_party/WebKit/Source/core/exported/LocalFrameClientImpl.cpp
index d58d53d..3f685c8 100644
--- a/third_party/WebKit/Source/core/exported/LocalFrameClientImpl.cpp
+++ b/third_party/WebKit/Source/core/exported/LocalFrameClientImpl.cpp
@@ -927,14 +927,6 @@
   web_frame_->Client()->DidEnforceInsecureRequestPolicy(policy);
 }
 
-void LocalFrameClientImpl::DidUpdateToUniqueOrigin() {
-  if (!web_frame_->Client())
-    return;
-  DCHECK(web_frame_->GetSecurityOrigin().IsUnique());
-  web_frame_->Client()->DidUpdateToUniqueOrigin(
-      web_frame_->GetSecurityOrigin().IsPotentiallyTrustworthy());
-}
-
 void LocalFrameClientImpl::DidChangeFramePolicy(
     Frame* child_frame,
     SandboxFlags flags,
diff --git a/third_party/WebKit/Source/core/exported/LocalFrameClientImpl.h b/third_party/WebKit/Source/core/exported/LocalFrameClientImpl.h
index 052c61a..4642775 100644
--- a/third_party/WebKit/Source/core/exported/LocalFrameClientImpl.h
+++ b/third_party/WebKit/Source/core/exported/LocalFrameClientImpl.h
@@ -185,7 +185,6 @@
   void FrameFocused() const override;
   void DidChangeName(const String&) override;
   void DidEnforceInsecureRequestPolicy(WebInsecureRequestPolicy) override;
-  void DidUpdateToUniqueOrigin() override;
   void DidChangeFramePolicy(Frame* child_frame,
                             SandboxFlags,
                             const ParsedFeaturePolicy&) override;
diff --git a/third_party/WebKit/Source/core/exported/WebFrameTest.cpp b/third_party/WebKit/Source/core/exported/WebFrameTest.cpp
index deb8cf2..35b83f8 100644
--- a/third_party/WebKit/Source/core/exported/WebFrameTest.cpp
+++ b/third_party/WebKit/Source/core/exported/WebFrameTest.cpp
@@ -9268,7 +9268,8 @@
   WebRemoteFrame* remote_frame = FrameTestHelpers::CreateRemote();
   WebFrame* target_frame = MainFrame()->FirstChild();
   target_frame->Swap(remote_frame);
-  remote_frame->SetReplicatedOrigin(SecurityOrigin::CreateUnique());
+  remote_frame->SetReplicatedOrigin(
+      WebSecurityOrigin(SecurityOrigin::CreateUnique()), false);
 
   // Invoking setTimeout should throw a security error.
   {
@@ -9349,7 +9350,8 @@
   // accessing a named property doesn't crash.
   WebRemoteFrame* remote_frame = FrameTestHelpers::CreateRemote();
   LastChild(MainFrame())->Swap(remote_frame);
-  remote_frame->SetReplicatedOrigin(SecurityOrigin::CreateUnique());
+  remote_frame->SetReplicatedOrigin(
+      WebSecurityOrigin(SecurityOrigin::CreateUnique()), false);
   v8::Local<v8::Value> remote_window_property =
       MainFrame()->ExecuteScriptAndReturnValue(
           WebScriptSource("window[2].foo"));
@@ -9376,7 +9378,8 @@
 
   WebRemoteFrame* remote_parent_frame = FrameTestHelpers::CreateRemote();
   MainFrame()->Swap(remote_parent_frame);
-  remote_parent_frame->SetReplicatedOrigin(SecurityOrigin::CreateUnique());
+  remote_parent_frame->SetReplicatedOrigin(
+      WebSecurityOrigin(SecurityOrigin::CreateUnique()), false);
 
   WebLocalFrame* child_frame =
       FrameTestHelpers::CreateLocalChild(*remote_parent_frame);
@@ -9403,7 +9406,8 @@
 
   WebRemoteFrame* remote_parent_frame = FrameTestHelpers::CreateRemote();
   MainFrame()->Swap(remote_parent_frame);
-  remote_parent_frame->SetReplicatedOrigin(SecurityOrigin::CreateUnique());
+  remote_parent_frame->SetReplicatedOrigin(
+      WebSecurityOrigin(SecurityOrigin::CreateUnique()), false);
 
   WebLocalFrame* child_frame =
       FrameTestHelpers::CreateLocalChild(*remote_parent_frame);
@@ -9514,7 +9518,7 @@
   ASSERT_EQ(MainFrame()->FirstChild(), remote_frame);
 
   remote_frame->SetReplicatedOrigin(
-      WebSecurityOrigin::CreateFromString("http://127.0.0.1"));
+      WebSecurityOrigin::CreateFromString("http://127.0.0.1"), false);
   MainFrame()->ExecuteScript(
       WebScriptSource("document.getElementsByTagName('iframe')[0]."
                       "contentWindow.location = 'data:text/html,hi'"));
@@ -9530,7 +9534,8 @@
   RemoteNavigationClient remote_client;
   WebRemoteFrame* remote_frame = FrameTestHelpers::CreateRemote(&remote_client);
   MainFrame()->FirstChild()->Swap(remote_frame);
-  remote_frame->SetReplicatedOrigin(SecurityOrigin::CreateUnique());
+  remote_frame->SetReplicatedOrigin(
+      WebSecurityOrigin(SecurityOrigin::CreateUnique()), false);
 
   ASSERT_TRUE(MainFrame()->FirstChild()->IsWebRemoteFrame());
   LocalDOMWindow* main_window =
diff --git a/third_party/WebKit/Source/core/exported/WebRemoteFrameImpl.cpp b/third_party/WebKit/Source/core/exported/WebRemoteFrameImpl.cpp
index 495226a..263d498d8 100644
--- a/third_party/WebKit/Source/core/exported/WebRemoteFrameImpl.cpp
+++ b/third_party/WebKit/Source/core/exported/WebRemoteFrameImpl.cpp
@@ -213,9 +213,14 @@
   return client->GetWebFrame();
 }
 
-void WebRemoteFrameImpl::SetReplicatedOrigin(const WebSecurityOrigin& origin) {
+void WebRemoteFrameImpl::SetReplicatedOrigin(
+    const WebSecurityOrigin& origin,
+    bool is_potentially_trustworthy_unique_origin) {
   DCHECK(GetFrame());
-  GetFrame()->GetSecurityContext()->SetReplicatedOrigin(origin);
+  scoped_refptr<SecurityOrigin> security_origin = origin.Get()->IsolatedCopy();
+  security_origin->SetUniqueOriginIsPotentiallyTrustworthy(
+      is_potentially_trustworthy_unique_origin);
+  GetFrame()->GetSecurityContext()->SetReplicatedOrigin(security_origin);
 
   // If the origin of a remote frame changed, the accessibility object for the
   // owner element now points to a different child.
@@ -283,20 +288,6 @@
   GetFrame()->GetSecurityContext()->SetInsecureRequestPolicy(policy);
 }
 
-void WebRemoteFrameImpl::SetReplicatedPotentiallyTrustworthyUniqueOrigin(
-    bool is_unique_origin_potentially_trustworthy) {
-  DCHECK(GetFrame());
-  // If |isUniqueOriginPotentiallyTrustworthy| is true, then the origin must be
-  // unique.
-  DCHECK(!is_unique_origin_potentially_trustworthy ||
-         GetFrame()->GetSecurityContext()->GetSecurityOrigin()->IsUnique());
-  GetFrame()
-      ->GetSecurityContext()
-      ->GetSecurityOrigin()
-      ->SetUniqueOriginIsPotentiallyTrustworthy(
-          is_unique_origin_potentially_trustworthy);
-}
-
 void WebRemoteFrameImpl::DispatchLoadEventOnFrameOwner() {
   DCHECK(GetFrame()->Owner()->IsLocal());
   GetFrame()->Owner()->DispatchLoad();
diff --git a/third_party/WebKit/Source/core/exported/WebRemoteFrameImpl.h b/third_party/WebKit/Source/core/exported/WebRemoteFrameImpl.h
index 6439c08..a24e3db 100644
--- a/third_party/WebKit/Source/core/exported/WebRemoteFrameImpl.h
+++ b/third_party/WebKit/Source/core/exported/WebRemoteFrameImpl.h
@@ -57,7 +57,9 @@
                                     WebRemoteFrameClient*,
                                     WebFrame* opener) override;
   void SetWebLayer(WebLayer*) override;
-  void SetReplicatedOrigin(const WebSecurityOrigin&) override;
+  void SetReplicatedOrigin(
+      const WebSecurityOrigin&,
+      bool is_potentially_trustworthy_unique_origin) override;
   void SetReplicatedSandboxFlags(WebSandboxFlags) override;
   void SetReplicatedName(const WebString&) override;
   void SetReplicatedFeaturePolicyHeader(
@@ -68,7 +70,6 @@
       WebContentSecurityPolicySource) override;
   void ResetReplicatedContentSecurityPolicy() override;
   void SetReplicatedInsecureRequestPolicy(WebInsecureRequestPolicy) override;
-  void SetReplicatedPotentiallyTrustworthyUniqueOrigin(bool) override;
   void DispatchLoadEventOnFrameOwner() override;
   void DidStartLoading() override;
   void DidStopLoading() override;
diff --git a/third_party/WebKit/Source/core/frame/LocalFrameClient.h b/third_party/WebKit/Source/core/frame/LocalFrameClient.h
index 4c0595c..b4df206 100644
--- a/third_party/WebKit/Source/core/frame/LocalFrameClient.h
+++ b/third_party/WebKit/Source/core/frame/LocalFrameClient.h
@@ -275,8 +275,6 @@
 
   virtual void DidEnforceInsecureRequestPolicy(WebInsecureRequestPolicy) {}
 
-  virtual void DidUpdateToUniqueOrigin() {}
-
   virtual void DidChangeFramePolicy(Frame* child_frame,
                                     SandboxFlags,
                                     const ParsedFeaturePolicy&) {}
diff --git a/third_party/WebKit/Source/core/loader/DocumentThreadableLoader.cpp b/third_party/WebKit/Source/core/loader/DocumentThreadableLoader.cpp
index 2fe43cf7..aa9626d 100644
--- a/third_party/WebKit/Source/core/loader/DocumentThreadableLoader.cpp
+++ b/third_party/WebKit/Source/core/loader/DocumentThreadableLoader.cpp
@@ -53,6 +53,7 @@
 #include "platform/loader/fetch/FetchUtils.h"
 #include "platform/loader/fetch/Resource.h"
 #include "platform/loader/fetch/ResourceFetcher.h"
+#include "platform/loader/fetch/ResourceLoader.h"
 #include "platform/loader/fetch/ResourceLoaderOptions.h"
 #include "platform/loader/fetch/ResourceRequest.h"
 #include "platform/weborigin/SchemeRegistry.h"
@@ -536,7 +537,7 @@
 
 DocumentThreadableLoader::~DocumentThreadableLoader() {
   CHECK(!client_);
-  DCHECK(!resource_);
+  DCHECK(!GetResource());
 }
 
 void DocumentThreadableLoader::OverrideTimeout(
@@ -585,14 +586,16 @@
 }
 
 void DocumentThreadableLoader::SetDefersLoading(bool value) {
-  if (GetResource())
-    GetResource()->SetDefersLoading(value);
+  if (GetResource() && GetResource()->Loader())
+    GetResource()->Loader()->SetDefersLoading(value);
 }
 
 void DocumentThreadableLoader::Clear() {
   client_ = nullptr;
   timeout_timer_.Stop();
   request_started_seconds_ = 0.0;
+  if (GetResource())
+    checker_.WillRemoveClient();
   ClearResource();
 }
 
@@ -734,6 +737,8 @@
 
   // FIXME: consider combining this with CORS redirect handling performed by
   // CrossOriginAccessControl::handleRedirect().
+  if (GetResource())
+    checker_.WillRemoveClient();
   ClearResource();
 
   // If
@@ -1101,6 +1106,8 @@
 }
 
 void DocumentThreadableLoader::LoadFallbackRequestForServiceWorker() {
+  if (GetResource())
+    checker_.WillRemoveClient();
   ClearResource();
   ResourceRequest fallback_request(fallback_request_for_service_worker_);
   fallback_request_for_service_worker_ = ResourceRequest();
@@ -1116,6 +1123,8 @@
   actual_request_ = ResourceRequest();
   actual_options_ = ResourceLoaderOptions();
 
+  if (GetResource())
+    checker_.WillRemoveClient();
   ClearResource();
 
   PrepareCrossOriginRequest(actual_request);
@@ -1200,6 +1209,8 @@
   } else {
     SetResource(RawResource::Fetch(new_params, fetcher));
   }
+  if (GetResource())
+    checker_.WillAddClient();
 
   if (!GetResource()) {
     probe::documentThreadableLoaderFailedToStartLoadingForClient(
@@ -1365,7 +1376,6 @@
 }
 
 void DocumentThreadableLoader::Trace(blink::Visitor* visitor) {
-  visitor->Trace(resource_);
   visitor->Trace(loading_context_);
   ThreadableLoader::Trace(visitor);
   RawResourceClient::Trace(visitor);
diff --git a/third_party/WebKit/Source/core/loader/DocumentThreadableLoader.h b/third_party/WebKit/Source/core/loader/DocumentThreadableLoader.h
index 89b55c0..73cadb9 100644
--- a/third_party/WebKit/Source/core/loader/DocumentThreadableLoader.h
+++ b/third_party/WebKit/Source/core/loader/DocumentThreadableLoader.h
@@ -57,8 +57,9 @@
 
 // TODO(horo): We are using this class not only in documents, but also in
 // workers. We should change the name to ThreadableLoaderImpl.
-class CORE_EXPORT DocumentThreadableLoader final : public ThreadableLoader,
-                                                   private RawResourceClient {
+class CORE_EXPORT DocumentThreadableLoader final
+    : public ThreadableLoader,
+      private ResourceOwner<RawResource> {
   USING_GARBAGE_COLLECTED_MIXIN(DocumentThreadableLoader);
 
  public:
@@ -203,32 +204,6 @@
   void LoadRequest(ResourceRequest&, ResourceLoaderOptions);
   bool IsAllowedRedirect(network::mojom::FetchRequestMode, const KURL&) const;
 
-  // TODO(hiroshige): After crbug.com/633696 is fixed,
-  // - Remove RawResourceClientStateChecker logic,
-  // - Make DocumentThreadableLoader to be a ResourceOwner and remove this
-  //   re-implementation of ResourceOwner, and
-  // - Consider re-applying RawResourceClientStateChecker in a more
-  //   general fashion (crbug.com/640291).
-  RawResource* GetResource() const { return resource_.Get(); }
-  void ClearResource() { SetResource(nullptr); }
-  void SetResource(RawResource* new_resource) {
-    if (new_resource == resource_)
-      return;
-
-    if (RawResource* old_resource = resource_.Release()) {
-      checker_.WillRemoveClient();
-      old_resource->RemoveClient(this);
-    }
-
-    if (new_resource) {
-      resource_ = new_resource;
-      checker_.WillAddClient();
-      resource_->AddClient(this);
-    }
-  }
-  Member<RawResource> resource_;
-  // End of ResourceOwner re-implementation, see above.
-
   SecurityOrigin* GetSecurityOrigin() const;
 
   // Returns null if the loader is not associated with Document.
diff --git a/third_party/WebKit/Source/core/page/scrolling/RootScrollerTest.cpp b/third_party/WebKit/Source/core/page/scrolling/RootScrollerTest.cpp
index b594ed9..929b3c9 100644
--- a/third_party/WebKit/Source/core/page/scrolling/RootScrollerTest.cpp
+++ b/third_party/WebKit/Source/core/page/scrolling/RootScrollerTest.cpp
@@ -785,7 +785,8 @@
   {
     WebRemoteFrameImpl* remote_main_frame = FrameTestHelpers::CreateRemote();
     helper_.LocalMainFrame()->Swap(remote_main_frame);
-    remote_main_frame->SetReplicatedOrigin(SecurityOrigin::CreateUnique());
+    remote_main_frame->SetReplicatedOrigin(
+        WebSecurityOrigin(SecurityOrigin::CreateUnique()), false);
     local_frame = FrameTestHelpers::CreateLocalChild(*remote_main_frame);
 
     FrameTestHelpers::LoadFrame(local_frame,
diff --git a/third_party/WebKit/Source/platform/exported/WebSecurityOrigin.cpp b/third_party/WebKit/Source/platform/exported/WebSecurityOrigin.cpp
index 1bf54ff4..3f08bb7 100644
--- a/third_party/WebKit/Source/platform/exported/WebSecurityOrigin.cpp
+++ b/third_party/WebKit/Source/platform/exported/WebSecurityOrigin.cpp
@@ -149,4 +149,10 @@
   return Get()->ToUrlOrigin();
 }
 
+#if DCHECK_IS_ON()
+bool WebSecurityOrigin::operator==(const WebSecurityOrigin& other) const {
+  return Get() == other.Get();
+}
+#endif
+
 }  // namespace blink
diff --git a/third_party/WebKit/Source/platform/loader/fetch/RawResource.cpp b/third_party/WebKit/Source/platform/loader/fetch/RawResource.cpp
index 7b634398..f06f0904 100644
--- a/third_party/WebKit/Source/platform/loader/fetch/RawResource.cpp
+++ b/third_party/WebKit/Source/platform/loader/fetch/RawResource.cpp
@@ -31,7 +31,6 @@
 #include "platform/loader/fetch/MemoryCache.h"
 #include "platform/loader/fetch/ResourceClientWalker.h"
 #include "platform/loader/fetch/ResourceFetcher.h"
-#include "platform/loader/fetch/ResourceLoader.h"
 #include "platform/network/http_names.h"
 #include "platform/scheduler/child/web_scheduler.h"
 #include "public/platform/Platform.h"
@@ -296,11 +295,6 @@
   Resource::NotifyFinished();
 }
 
-void RawResource::SetDefersLoading(bool defers) {
-  if (Loader())
-    Loader()->SetDefersLoading(defers);
-}
-
 static bool ShouldIgnoreHeaderForCacheReuse(AtomicString header_name) {
   // FIXME: This list of headers that don't affect cache policy almost certainly
   // isn't complete.
diff --git a/third_party/WebKit/Source/platform/loader/fetch/RawResource.h b/third_party/WebKit/Source/platform/loader/fetch/RawResource.h
index 8d59b043..b81d484 100644
--- a/third_party/WebKit/Source/platform/loader/fetch/RawResource.h
+++ b/third_party/WebKit/Source/platform/loader/fetch/RawResource.h
@@ -66,12 +66,6 @@
     return CreateForTest(KURL(url), type);
   }
 
-  // FIXME: AssociatedURLLoader shouldn't be a DocumentThreadableLoader and
-  // therefore shouldn't use RawResource. However, it is, and it needs to be
-  // able to defer loading. This can be fixed by splitting CORS preflighting out
-  // of DocumentThreadableLoader.
-  void SetDefersLoading(bool);
-
   // Resource implementation
   bool CanReuse(const FetchParameters&) const override;
   bool WillFollowRedirect(const ResourceRequest&,
diff --git a/third_party/WebKit/public/platform/WebSecurityOrigin.h b/third_party/WebKit/public/platform/WebSecurityOrigin.h
index 1b44129..d7a3086 100644
--- a/third_party/WebKit/public/platform/WebSecurityOrigin.h
+++ b/third_party/WebKit/public/platform/WebSecurityOrigin.h
@@ -119,6 +119,10 @@
   BLINK_PLATFORM_EXPORT WebSecurityOrigin(const url::Origin&);
   BLINK_PLATFORM_EXPORT operator url::Origin() const;
 
+#if DCHECK_IS_ON()
+  BLINK_PLATFORM_EXPORT bool operator==(const WebSecurityOrigin&) const;
+#endif
+
  private:
   // Present only to facilitate conversion from 'url::Origin'; this constructor
   // shouldn't be used anywhere else.
diff --git a/third_party/WebKit/public/web/WebFrameClient.h b/third_party/WebKit/public/web/WebFrameClient.h
index 979921b..411ec23 100644
--- a/third_party/WebKit/public/web/WebFrameClient.h
+++ b/third_party/WebKit/public/web/WebFrameClient.h
@@ -259,15 +259,6 @@
   // This frame has set an insecure request policy.
   virtual void DidEnforceInsecureRequestPolicy(WebInsecureRequestPolicy) {}
 
-  // This frame has been updated to a unique origin, which should be
-  // considered potentially trustworthy if
-  // |isPotentiallyTrustworthyUniqueOrigin| is true. TODO(estark):
-  // this method only exists to support dynamic sandboxing via a CSP
-  // delivered in a <meta> tag. This is not supposed to be allowed per
-  // the CSP spec and should be ripped out. https://crbug.com/594645
-  virtual void DidUpdateToUniqueOrigin(
-      bool is_potentially_trustworthy_unique_origin) {}
-
   // The sandbox flags or container policy have changed for a child frame of
   // this frame.
   virtual void DidChangeFramePolicy(
diff --git a/third_party/WebKit/public/web/WebRemoteFrame.h b/third_party/WebKit/public/web/WebRemoteFrame.h
index 5a2beb69..e73a801 100644
--- a/third_party/WebKit/public/web/WebRemoteFrame.h
+++ b/third_party/WebKit/public/web/WebRemoteFrame.h
@@ -62,7 +62,9 @@
   virtual void SetWebLayer(WebLayer*) = 0;
 
   // Set security origin replicated from another process.
-  virtual void SetReplicatedOrigin(const WebSecurityOrigin&) = 0;
+  virtual void SetReplicatedOrigin(
+      const WebSecurityOrigin&,
+      bool is_potentially_trustworthy_unique_origin) = 0;
 
   // Set sandbox flags replicated from another process.
   virtual void SetReplicatedSandboxFlags(WebSandboxFlags) = 0;
@@ -86,10 +88,6 @@
   // process.
   virtual void SetReplicatedInsecureRequestPolicy(WebInsecureRequestPolicy) = 0;
 
-  // Set the frame to a unique origin that is potentially trustworthy,
-  // replicated from another process.
-  virtual void SetReplicatedPotentiallyTrustworthyUniqueOrigin(bool) = 0;
-
   virtual void DispatchLoadEventOnFrameOwner() = 0;
 
   virtual void DidStartLoading() = 0;
diff --git a/ui/message_center/notification.cc b/ui/message_center/notification.cc
index 4ef3cb6..af6bd704 100644
--- a/ui/message_center/notification.cc
+++ b/ui/message_center/notification.cc
@@ -251,7 +251,7 @@
       base::string16() /* display_source */, GURL(),
       NotifierId(NotifierId::SYSTEM_COMPONENT, system_component_id),
       RichNotificationData(),
-      new HandleNotificationClickedDelegate(click_callback), gfx::kNoneIcon,
+      new HandleNotificationClickDelegate(click_callback), gfx::kNoneIcon,
       SystemNotificationWarningLevel::CRITICAL_WARNING);
   notification->set_clickable(true);
   notification->SetSystemPriority();
diff --git a/ui/message_center/notification_delegate.cc b/ui/message_center/notification_delegate.cc
index 95b3fe3e..ba57dc743 100644
--- a/ui/message_center/notification_delegate.cc
+++ b/ui/message_center/notification_delegate.cc
@@ -4,6 +4,7 @@
 
 #include "ui/message_center/notification_delegate.h"
 
+#include "base/bind.h"
 #include "base/logging.h"
 
 namespace message_center {
@@ -27,33 +28,36 @@
 
 void NotificationDelegate::DisableNotification() {}
 
-// HandleNotificationClickedDelegate:
+// HandleNotificationClickDelegate:
 
-HandleNotificationClickedDelegate::HandleNotificationClickedDelegate(
-    const base::Closure& closure)
-    : closure_(closure) {
+HandleNotificationClickDelegate::HandleNotificationClickDelegate(
+    const base::Closure& callback) {
+  if (!callback.is_null()) {
+    // Create a callback that consumes and ignores the button index parameter,
+    // and just runs the provided closure.
+    callback_ = base::Bind(
+        [](const base::Closure& closure, base::Optional<int> button_index) {
+          DCHECK(!button_index);
+          closure.Run();
+        },
+        callback);
+  }
 }
 
-HandleNotificationClickedDelegate::~HandleNotificationClickedDelegate() {}
+HandleNotificationClickDelegate::HandleNotificationClickDelegate(
+    const ButtonClickCallback& callback)
+    : callback_(callback) {}
 
-void HandleNotificationClickedDelegate::Click() {
-  if (!closure_.is_null())
-    closure_.Run();
+HandleNotificationClickDelegate::~HandleNotificationClickDelegate() {}
+
+void HandleNotificationClickDelegate::Click() {
+  if (!callback_.is_null())
+    callback_.Run(base::nullopt);
 }
 
-// HandleNotificationButtonClickDelegate:
-
-HandleNotificationButtonClickDelegate::HandleNotificationButtonClickDelegate(
-    const ButtonClickCallback& button_callback)
-    : button_callback_(button_callback) {
-}
-
-HandleNotificationButtonClickDelegate::
-    ~HandleNotificationButtonClickDelegate() {}
-
-void HandleNotificationButtonClickDelegate::ButtonClick(int button_index) {
-  if (!button_callback_.is_null())
-    button_callback_.Run(button_index);
+void HandleNotificationClickDelegate::ButtonClick(int button_index) {
+  if (!callback_.is_null())
+    callback_.Run(button_index);
 }
 
 }  // namespace message_center
diff --git a/ui/message_center/notification_delegate.h b/ui/message_center/notification_delegate.h
index 97108481..79f8ce4 100644
--- a/ui/message_center/notification_delegate.h
+++ b/ui/message_center/notification_delegate.h
@@ -11,6 +11,7 @@
 #include "base/callback.h"
 #include "base/macros.h"
 #include "base/memory/ref_counted.h"
+#include "base/optional.h"
 #include "base/strings/string16.h"
 #include "ui/message_center/message_center_export.h"
 
@@ -55,45 +56,33 @@
   friend class base::RefCountedThreadSafe<NotificationDelegate>;
 };
 
-// A simple notification delegate which invokes the passed closure when clicked.
-class MESSAGE_CENTER_EXPORT HandleNotificationClickedDelegate
+// A simple notification delegate which invokes the passed closure when the body
+// or a button is clicked.
+class MESSAGE_CENTER_EXPORT HandleNotificationClickDelegate
     : public NotificationDelegate {
  public:
-  explicit HandleNotificationClickedDelegate(const base::Closure& closure);
+  // The parameter is the index of the button that was clicked, or nullopt if
+  // the body was clicked.
+  using ButtonClickCallback = base::Callback<void(base::Optional<int>)>;
+
+  // Creates a delegate that handles clicks on a button or on the body.
+  explicit HandleNotificationClickDelegate(const ButtonClickCallback& callback);
+
+  // Creates a delegate that only handles clicks on the body of the
+  // notification.
+  explicit HandleNotificationClickDelegate(const base::Closure& closure);
 
   // message_center::NotificationDelegate overrides:
   void Click() override;
-
- protected:
-  ~HandleNotificationClickedDelegate() override;
-
- private:
-  std::string id_;
-  base::Closure closure_;
-
-  DISALLOW_COPY_AND_ASSIGN(HandleNotificationClickedDelegate);
-};
-
-// A notification delegate which invokes a callback when a notification button
-// has been clicked.
-class MESSAGE_CENTER_EXPORT HandleNotificationButtonClickDelegate
-    : public NotificationDelegate {
- public:
-  typedef base::Callback<void(int)> ButtonClickCallback;
-
-  explicit HandleNotificationButtonClickDelegate(
-      const ButtonClickCallback& button_callback);
-
-  // message_center::NotificationDelegate overrides:
   void ButtonClick(int button_index) override;
 
  protected:
-  ~HandleNotificationButtonClickDelegate() override;
+  ~HandleNotificationClickDelegate() override;
 
  private:
-  ButtonClickCallback button_callback_;
+  ButtonClickCallback callback_;
 
-  DISALLOW_COPY_AND_ASSIGN(HandleNotificationButtonClickDelegate);
+  DISALLOW_COPY_AND_ASSIGN(HandleNotificationClickDelegate);
 };
 
 }  //  namespace message_center
diff --git a/ui/message_center/notification_delegate_unittest.cc b/ui/message_center/notification_delegate_unittest.cc
index d168cfe..c45ee0b 100644
--- a/ui/message_center/notification_delegate_unittest.cc
+++ b/ui/message_center/notification_delegate_unittest.cc
@@ -14,56 +14,53 @@
 
 class NotificationDelegateTest : public testing::Test {
  public:
-  NotificationDelegateTest();
-  ~NotificationDelegateTest() override;
+  NotificationDelegateTest() = default;
+  ~NotificationDelegateTest() override = default;
 
-  void ClickCallback();
+  void BodyClickCallback() { ++callback_count_; }
 
-  int GetClickedCallbackAndReset();
+  void ButtonClickCallback(base::Optional<int> button_index) {
+    ++callback_count_;
+    last_button_index_ = button_index;
+  }
+
+ protected:
+  int callback_count_ = 0;
+  base::Optional<int> last_button_index_;
 
  private:
-  int callback_count_;
-
   DISALLOW_COPY_AND_ASSIGN(NotificationDelegateTest);
 };
 
-NotificationDelegateTest::NotificationDelegateTest() : callback_count_(0) {}
-
-NotificationDelegateTest::~NotificationDelegateTest() {}
-
-void NotificationDelegateTest::ClickCallback() {
-  ++callback_count_;
-}
-
-int NotificationDelegateTest::GetClickedCallbackAndReset() {
-  int result = callback_count_;
-  callback_count_ = 0;
-  return result;
-}
-
-TEST_F(NotificationDelegateTest, ClickedDelegate) {
-  scoped_refptr<HandleNotificationClickedDelegate> delegate(
-      new HandleNotificationClickedDelegate(
-          base::Bind(&NotificationDelegateTest::ClickCallback,
-                     base::Unretained(this))));
+TEST_F(NotificationDelegateTest, ClickDelegate) {
+  auto delegate = base::MakeRefCounted<HandleNotificationClickDelegate>(
+      base::Bind(&NotificationDelegateTest::BodyClickCallback,
+                 base::Unretained(this)));
 
   delegate->Click();
-  EXPECT_EQ(1, GetClickedCallbackAndReset());
-
-  // ButtonClick doesn't call the callback.
-  delegate->ButtonClick(0);
-  EXPECT_EQ(0, GetClickedCallbackAndReset());
+  EXPECT_EQ(1, callback_count_);
 }
 
-TEST_F(NotificationDelegateTest, NullClickedDelegate) {
-  scoped_refptr<HandleNotificationClickedDelegate> delegate(
-      new HandleNotificationClickedDelegate(base::Closure()));
+TEST_F(NotificationDelegateTest, NullClickDelegate) {
+  auto delegate =
+      base::MakeRefCounted<HandleNotificationClickDelegate>(base::Closure());
 
   delegate->Click();
-  EXPECT_EQ(0, GetClickedCallbackAndReset());
+  EXPECT_EQ(0, callback_count_);
+}
 
-  delegate->ButtonClick(0);
-  EXPECT_EQ(0, GetClickedCallbackAndReset());
+TEST_F(NotificationDelegateTest, ButtonClickDelegate) {
+  auto delegate = base::MakeRefCounted<HandleNotificationClickDelegate>(
+      base::Bind(&NotificationDelegateTest::ButtonClickCallback,
+                 base::Unretained(this)));
+
+  delegate->Click();
+  EXPECT_EQ(1, callback_count_);
+  EXPECT_EQ(base::nullopt, last_button_index_);
+
+  delegate->ButtonClick(3);
+  EXPECT_EQ(2, callback_count_);
+  EXPECT_EQ(3, *last_button_index_);
 }
 
 }  // namespace message_center