vk: Create KeyboardUIModel and put state logic in there.

We wish to refactor the keyboard code using MVC in order to reduce the
size and responsibility of KeyboardController.

The plan is to have MVC for both the keyboard (configuration, enable
flags) and the keyboard UI (window management / animations etc.), akin
to how //ash/assistant has both |assistant_controller| and
|assistant_ui_controller|.

This patch creates a KeyboardUIModel, meant to contain information
related to the keyboard window & UI. We'll start by putting the UI state
code in there.

Future patches will move more code into the model and clean things up.

Bug: 964191
Change-Id: Ia2b026874ee72e48a21140cd3db4a0d86b62c7b5
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1616826
Reviewed-by: Scott Violet <sky@chromium.org>
Commit-Queue: Darren Shen <shend@chromium.org>
Cr-Commit-Position: refs/heads/master@{#661496}
diff --git a/ash/keyboard/ui/BUILD.gn b/ash/keyboard/ui/BUILD.gn
index ed41d81..9be75ac 100644
--- a/ash/keyboard/ui/BUILD.gn
+++ b/ash/keyboard/ui/BUILD.gn
@@ -35,6 +35,8 @@
     "keyboard_ui.h",
     "keyboard_ui_factory.cc",
     "keyboard_ui_factory.h",
+    "keyboard_ui_model.cc",
+    "keyboard_ui_model.h",
     "keyboard_ukm_recorder.cc",
     "keyboard_ukm_recorder.h",
     "keyboard_util.cc",
diff --git a/ash/keyboard/ui/keyboard_controller.cc b/ash/keyboard/ui/keyboard_controller.cc
index 56855d8..c747292 100644
--- a/ash/keyboard/ui/keyboard_controller.cc
+++ b/ash/keyboard/ui/keyboard_controller.cc
@@ -71,41 +71,6 @@
 constexpr base::TimeDelta kTransientBlurThreshold =
     base::TimeDelta::FromMilliseconds(3500);
 
-// State transition diagram (document linked from crbug.com/719905)
-bool IsAllowedStateTransition(KeyboardControllerState from,
-                              KeyboardControllerState to) {
-  static const std::set<
-      std::pair<KeyboardControllerState, KeyboardControllerState>>
-      kAllowedStateTransition = {
-          // The initial ShowKeyboard scenario
-          // INITIAL -> LOADING_EXTENSION -> HIDDEN -> SHOWN.
-          {KeyboardControllerState::kInitial,
-           KeyboardControllerState::kLoadingExtension},
-          {KeyboardControllerState::kLoadingExtension,
-           KeyboardControllerState::kHidden},
-          {KeyboardControllerState::kHidden, KeyboardControllerState::kShown},
-
-          // Hide scenario
-          // SHOWN -> WILL_HIDE -> HIDDEN.
-          {KeyboardControllerState::kShown, KeyboardControllerState::kWillHide},
-          {KeyboardControllerState::kWillHide,
-           KeyboardControllerState::kHidden},
-
-          // Focus transition scenario
-          // SHOWN -> WILL_HIDE -> SHOWN.
-          {KeyboardControllerState::kWillHide, KeyboardControllerState::kShown},
-
-          // HideKeyboard can be called at anytime for example on shutdown.
-          {KeyboardControllerState::kShown, KeyboardControllerState::kHidden},
-
-          // Return to INITIAL when keyboard is disabled.
-          {KeyboardControllerState::kLoadingExtension,
-           KeyboardControllerState::kInitial},
-          {KeyboardControllerState::kHidden, KeyboardControllerState::kInitial},
-      };
-  return kAllowedStateTransition.count(std::make_pair(from, to)) == 1;
-}
-
 void SetTouchEventLogging(bool enable) {
   ui::InputController* controller =
       ui::OzonePlatform::GetInstance()->GetInputController();
@@ -113,23 +78,6 @@
     controller->SetTouchEventLoggingEnabled(enable);
 }
 
-std::string StateToStr(KeyboardControllerState state) {
-  switch (state) {
-    case KeyboardControllerState::kUnknown:
-      return "UNKNOWN";
-    case KeyboardControllerState::kInitial:
-      return "INITIAL";
-    case KeyboardControllerState::kLoadingExtension:
-      return "LOADING_EXTENSION";
-    case KeyboardControllerState::kShown:
-      return "SHOWN";
-    case KeyboardControllerState::kWillHide:
-      return "WILL_HIDE";
-    case KeyboardControllerState::kHidden:
-      return "HIDDEN";
-  }
-}
-
 // An enumeration of different keyboard control events that should be logged.
 // These values are persisted to logs. Entries should not be renumbered and
 // numeric values should never be reused.
@@ -271,7 +219,7 @@
 
   show_on_keyboard_window_load_ = false;
   keyboard_locked_ = false;
-  DCHECK_EQ(state_, KeyboardControllerState::kInitial);
+  DCHECK_EQ(model_.state(), KeyboardControllerState::kInitial);
   ui_->SetController(this);
   SetContainerBehaviorInternal(mojom::ContainerType::kFullWidth);
   visual_bounds_in_root_ = gfx::Rect();
@@ -303,7 +251,7 @@
 
   // Return to the INITIAL state to ensure that transitions entering a state
   // is equal to transitions leaving the state.
-  if (state_ != KeyboardControllerState::kInitial)
+  if (model_.state() != KeyboardControllerState::kInitial)
     ChangeState(KeyboardControllerState::kInitial);
 
   // TODO(https://crbug.com/731537): Move KeyboardController members into a
@@ -412,7 +360,7 @@
 
 void KeyboardController::NotifyKeyboardWindowLoaded() {
   const bool should_show = show_on_keyboard_window_load_;
-  if (state_ == KeyboardControllerState::kLoadingExtension)
+  if (model_.state() == KeyboardControllerState::kLoadingExtension)
     ChangeState(KeyboardControllerState::kHidden);
   if (should_show) {
     // The window height is set to 0 initially or before switch to an IME in a
@@ -581,7 +529,7 @@
 void KeyboardController::HideKeyboard(HideReason reason) {
   TRACE_EVENT0("vk", "HideKeyboard");
 
-  switch (state_) {
+  switch (model_.state()) {
     case KeyboardControllerState::kUnknown:
     case KeyboardControllerState::kInitial:
     case KeyboardControllerState::kHidden:
@@ -671,7 +619,7 @@
 }
 
 void KeyboardController::HideKeyboardImplicitlyBySystem() {
-  if (state_ != KeyboardControllerState::kShown || keyboard_locked_)
+  if (model_.state() != KeyboardControllerState::kShown || keyboard_locked_)
     return;
 
   ChangeState(KeyboardControllerState::kWillHide);
@@ -686,7 +634,7 @@
 
 // private
 void KeyboardController::HideAnimationFinished() {
-  if (state_ == KeyboardControllerState::kHidden) {
+  if (model_.state() == KeyboardControllerState::kHidden) {
     if (queued_container_type_) {
       SetContainerBehaviorInternal(queued_container_type_->container_type());
       // The position of the container window will be adjusted shortly in
@@ -753,7 +701,7 @@
 }
 
 void KeyboardController::LoadKeyboardWindowInBackground() {
-  DCHECK_EQ(state_, KeyboardControllerState::kInitial);
+  DCHECK_EQ(model_.state(), KeyboardControllerState::kInitial);
 
   TRACE_EVENT0("vk", "LoadKeyboardWindowInBackground");
 
@@ -847,7 +795,7 @@
       client && client->GetTextInputFlags() != ui::TEXT_INPUT_FLAG_NONE;
 
   if (should_hide) {
-    switch (state_) {
+    switch (model_.state()) {
       case KeyboardControllerState::kLoadingExtension:
         show_on_keyboard_window_load_ = false;
         return;
@@ -858,7 +806,7 @@
         return;
     }
   } else {
-    switch (state_) {
+    switch (model_.state()) {
       case KeyboardControllerState::kWillHide:
         // Abort a pending keyboard hide.
         ChangeState(KeyboardControllerState::kShown);
@@ -898,9 +846,9 @@
 
 void KeyboardController::PopulateKeyboardContent(
     aura::Window* target_container) {
-  DCHECK_NE(state_, KeyboardControllerState::kInitial);
+  DCHECK_NE(model_.state(), KeyboardControllerState::kInitial);
 
-  DVLOG(1) << "PopulateKeyboardContent: " << StateToStr(state_);
+  DVLOG(1) << "PopulateKeyboardContent: " << StateToStr(model_.state());
   TRACE_EVENT0("vk", "PopulateKeyboardContent");
 
   MoveToParentContainer(target_container);
@@ -909,7 +857,7 @@
   DCHECK(keyboard_window);
   DCHECK_EQ(parent_container_, keyboard_window->parent());
 
-  switch (state_) {
+  switch (model_.state()) {
     case KeyboardControllerState::kShown:
       return;
     case KeyboardControllerState::kLoadingExtension:
@@ -923,7 +871,7 @@
 
   SetTouchEventLogging(false /* enable */);
 
-  switch (state_) {
+  switch (model_.state()) {
     case KeyboardControllerState::kWillHide:
       ChangeState(KeyboardControllerState::kShown);
       return;
@@ -931,7 +879,7 @@
       break;
   }
 
-  DCHECK_EQ(state_, KeyboardControllerState::kHidden);
+  DCHECK_EQ(model_.state(), KeyboardControllerState::kHidden);
 
   // If the container is not animating, makes sure the position and opacity
   // are at begin states for animation.
@@ -967,7 +915,7 @@
 
 bool KeyboardController::WillHideKeyboard() const {
   bool res = weak_factory_will_hide_.HasWeakPtrs();
-  DCHECK_EQ(res, state_ == KeyboardControllerState::kWillHide);
+  DCHECK_EQ(res, model_.state() == KeyboardControllerState::kWillHide);
   return res;
 }
 
@@ -976,33 +924,8 @@
     observer.OnKeyboardConfigChanged();
 }
 
-void KeyboardController::CheckStateTransition(KeyboardControllerState prev,
-                                              KeyboardControllerState next) {
-  std::stringstream error_message;
-  const bool valid_transition = IsAllowedStateTransition(prev, next);
-  if (!valid_transition)
-    error_message << "Unexpected transition";
-
-  // Emit UMA
-  const int transition_record =
-      (valid_transition ? 1 : -1) *
-      (static_cast<int>(prev) * 1000 + static_cast<int>(next));
-  base::UmaHistogramSparse("VirtualKeyboard.ControllerStateTransition",
-                           transition_record);
-  UMA_HISTOGRAM_BOOLEAN("VirtualKeyboard.ControllerStateTransitionIsValid",
-                        transition_record > 0);
-
-  DCHECK(error_message.str().empty())
-      << "State: " << StateToStr(prev) << " -> " << StateToStr(next) << " "
-      << error_message.str();
-}
-
 void KeyboardController::ChangeState(KeyboardControllerState state) {
-  CheckStateTransition(state_, state);
-  if (state_ == state)
-    return;
-
-  state_ = state;
+  model_.ChangeState(state);
 
   if (state != KeyboardControllerState::kWillHide)
     weak_factory_will_hide_.InvalidateWeakPtrs();
@@ -1010,7 +933,7 @@
     show_on_keyboard_window_load_ = false;
 
   weak_factory_report_lingering_state_.InvalidateWeakPtrs();
-  switch (state_) {
+  switch (model_.state()) {
     case KeyboardControllerState::kLoadingExtension:
     case KeyboardControllerState::kWillHide:
       base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
@@ -1026,9 +949,10 @@
 }
 
 void KeyboardController::ReportLingeringState() {
-  LOG(ERROR) << "KeyboardController lingering in " << StateToStr(state_);
+  LOG(ERROR) << "KeyboardController lingering in "
+             << StateToStr(model_.state());
   UMA_HISTOGRAM_ENUMERATION("VirtualKeyboard.LingeringIntermediateState",
-                            state_);
+                            model_.state());
 }
 
 gfx::Rect KeyboardController::GetWorkspaceOccludedBoundsInScreen() const {
@@ -1098,7 +1022,7 @@
     return;
   }
 
-  if (state_ == KeyboardControllerState::kShown) {
+  if (model_.state() == KeyboardControllerState::kShown) {
     // Keyboard is already shown. Hiding the keyboard at first then switching
     // container type.
     queued_container_type_ = std::make_unique<QueuedContainerType>(
@@ -1130,7 +1054,7 @@
 }
 
 bool KeyboardController::IsKeyboardVisible() {
-  if (state_ == KeyboardControllerState::kShown) {
+  if (model_.state() == KeyboardControllerState::kShown) {
     DCHECK(IsEnabled());
     return true;
   }
diff --git a/ash/keyboard/ui/keyboard_controller.h b/ash/keyboard/ui/keyboard_controller.h
index 4a8b8ee6..4d99e54 100644
--- a/ash/keyboard/ui/keyboard_controller.h
+++ b/ash/keyboard/ui/keyboard_controller.h
@@ -14,6 +14,7 @@
 #include "ash/keyboard/ui/keyboard_event_handler.h"
 #include "ash/keyboard/ui/keyboard_export.h"
 #include "ash/keyboard/ui/keyboard_layout_delegate.h"
+#include "ash/keyboard/ui/keyboard_ui_model.h"
 #include "ash/keyboard/ui/keyboard_ukm_recorder.h"
 #include "ash/keyboard/ui/notification_manager.h"
 #include "ash/keyboard/ui/queued_container_type.h"
@@ -47,28 +48,6 @@
 class KeyboardUI;
 class KeyboardUIFactory;
 
-// Represents the current state of the keyboard managed by the controller.
-// These values are persisted to logs. Entries should not be renumbered and
-// numeric values should never be reused.
-enum class KeyboardControllerState {
-  kUnknown = 0,
-  // Keyboard has never been shown.
-  kInitial = 1,
-  // Waiting for an extension to be loaded. Will move to HIDDEN if this is
-  // loading pre-emptively, otherwise will move to SHOWN.
-  kLoadingExtension = 2,
-  // kShowing = 3,  // no longer used
-  // Keyboard is shown.
-  kShown = 4,
-  // Keyboard is still shown, but will move to HIDING in a short period, or if
-  // an input element gets focused again, will move to SHOWN.
-  kWillHide = 5,
-  // kHiding = 6,  // no longer used
-  // Keyboard is hidden, but has shown at least once.
-  kHidden = 7,
-  kMaxValue = kHidden
-};
-
 // Provides control of the virtual keyboard, including enabling/disabling the
 // keyboard and controlling its visibility.
 class KEYBOARD_EXPORT KeyboardController : public ui::InputMethodObserver,
@@ -253,7 +232,7 @@
       std::unique_ptr<ContainerBehavior> container_behavior) {
     container_behavior_ = std::move(container_behavior);
   }
-  KeyboardControllerState GetStateForTest() const { return state_; }
+  KeyboardControllerState GetStateForTest() const { return model_.state(); }
   ui::InputMethod* GetInputMethodForTest();
   void EnsureCaretInWorkAreaForTest(const gfx::Rect& occluded_bounds_in_root);
 
@@ -436,18 +415,19 @@
   bool show_on_keyboard_window_load_ = false;
 
   // If true, the keyboard is always visible even if no window has input focus.
+  // TODO(https://crbug.com/964191): Move this to the UI model.
   bool keyboard_locked_ = false;
   KeyboardEventHandler event_handler_;
 
   base::ObserverList<KeyboardControllerObserver>::Unchecked observer_list_;
 
+  KeyboardUIModel model_;
+
   // The bounds for the visible portion of the keyboard, relative to the root
   // window. If the keyboard window is visible, this should be the same size as
   // the keyboard window. If not, this should be empty.
   gfx::Rect visual_bounds_in_root_;
 
-  KeyboardControllerState state_ = KeyboardControllerState::kInitial;
-
   // Keyboard configuration associated with the controller.
   mojom::KeyboardConfig keyboard_config_;
 
diff --git a/ash/keyboard/ui/keyboard_ui_model.cc b/ash/keyboard/ui/keyboard_ui_model.cc
new file mode 100644
index 0000000..b65d002
--- /dev/null
+++ b/ash/keyboard/ui/keyboard_ui_model.cc
@@ -0,0 +1,99 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ash/keyboard/ui/keyboard_ui_model.h"
+
+#include "base/metrics/histogram_functions.h"
+#include "base/metrics/histogram_macros.h"
+#include "base/no_destructor.h"
+
+namespace keyboard {
+
+namespace {
+
+// Returns whether a given state transition is valid.
+// See the design document linked in https://crbug.com/71990.
+bool IsAllowedStateTransition(KeyboardControllerState from,
+                              KeyboardControllerState to) {
+  static const base::NoDestructor<
+      std::set<std::pair<KeyboardControllerState, KeyboardControllerState>>>
+      kAllowedStateTransition({
+          // The initial ShowKeyboard scenario
+          // INITIAL -> LOADING_EXTENSION -> HIDDEN -> SHOWN.
+          {KeyboardControllerState::kInitial,
+           KeyboardControllerState::kLoadingExtension},
+          {KeyboardControllerState::kLoadingExtension,
+           KeyboardControllerState::kHidden},
+          {KeyboardControllerState::kHidden, KeyboardControllerState::kShown},
+
+          // Hide scenario
+          // SHOWN -> WILL_HIDE -> HIDDEN.
+          {KeyboardControllerState::kShown, KeyboardControllerState::kWillHide},
+          {KeyboardControllerState::kWillHide,
+           KeyboardControllerState::kHidden},
+
+          // Focus transition scenario
+          // SHOWN -> WILL_HIDE -> SHOWN.
+          {KeyboardControllerState::kWillHide, KeyboardControllerState::kShown},
+
+          // HideKeyboard can be called at anytime for example on shutdown.
+          {KeyboardControllerState::kShown, KeyboardControllerState::kHidden},
+
+          // Return to INITIAL when keyboard is disabled.
+          {KeyboardControllerState::kLoadingExtension,
+           KeyboardControllerState::kInitial},
+          {KeyboardControllerState::kHidden, KeyboardControllerState::kInitial},
+      });
+  return kAllowedStateTransition->count(std::make_pair(from, to)) == 1;
+}
+
+// Records a state transition for metrics.
+void RecordStateTransition(KeyboardControllerState prev,
+                           KeyboardControllerState next) {
+  const bool valid_transition = IsAllowedStateTransition(prev, next);
+
+  // Emit UMA
+  const int transition_record =
+      (valid_transition ? 1 : -1) *
+      (static_cast<int>(prev) * 1000 + static_cast<int>(next));
+  base::UmaHistogramSparse("VirtualKeyboard.ControllerStateTransition",
+                           transition_record);
+  UMA_HISTOGRAM_BOOLEAN("VirtualKeyboard.ControllerStateTransitionIsValid",
+                        valid_transition);
+
+  DCHECK(valid_transition) << "State: " << StateToStr(prev) << " -> "
+                           << StateToStr(next) << " Unexpected transition";
+}
+
+}  // namespace
+
+std::string StateToStr(KeyboardControllerState state) {
+  switch (state) {
+    case KeyboardControllerState::kUnknown:
+      return "UNKNOWN";
+    case KeyboardControllerState::kInitial:
+      return "INITIAL";
+    case KeyboardControllerState::kLoadingExtension:
+      return "LOADING_EXTENSION";
+    case KeyboardControllerState::kShown:
+      return "SHOWN";
+    case KeyboardControllerState::kWillHide:
+      return "WILL_HIDE";
+    case KeyboardControllerState::kHidden:
+      return "HIDDEN";
+  }
+}
+
+KeyboardUIModel::KeyboardUIModel() = default;
+
+void KeyboardUIModel::ChangeState(KeyboardControllerState new_state) {
+  RecordStateTransition(state_, new_state);
+
+  if (new_state == state_)
+    return;
+
+  state_ = new_state;
+}
+
+}  // namespace keyboard
diff --git a/ash/keyboard/ui/keyboard_ui_model.h b/ash/keyboard/ui/keyboard_ui_model.h
new file mode 100644
index 0000000..94a593f3
--- /dev/null
+++ b/ash/keyboard/ui/keyboard_ui_model.h
@@ -0,0 +1,62 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef ASH_KEYBOARD_UI_KEYBOARD_UI_MODEL_H_
+#define ASH_KEYBOARD_UI_KEYBOARD_UI_MODEL_H_
+
+#include <string>
+
+#include "ash/keyboard/ui/keyboard_export.h"
+#include "ash/public/interfaces/keyboard_config.mojom.h"
+#include "base/macros.h"
+
+namespace keyboard {
+
+// TODO(https://crbug.com/964191): Change this to be part of the model.
+// Represents the current state of the keyboard managed by the controller.
+// These values are persisted to logs. Entries should not be renumbered and
+// numeric values should never be reused.
+enum class KeyboardControllerState {
+  kUnknown = 0,
+  // Keyboard has never been shown.
+  kInitial = 1,
+  // Waiting for an extension to be loaded. Will move to HIDDEN if this is
+  // loading pre-emptively, otherwise will move to SHOWN.
+  kLoadingExtension = 2,
+  // kShowing = 3,  // no longer used
+  // Keyboard is shown.
+  kShown = 4,
+  // Keyboard is still shown, but will move to HIDING in a short period, or if
+  // an input element gets focused again, will move to SHOWN.
+  kWillHide = 5,
+  // kHiding = 6,  // no longer used
+  // Keyboard is hidden, but has shown at least once.
+  kHidden = 7,
+  kMaxValue = kHidden
+};
+
+// Convert a state into a string.
+std::string StateToStr(KeyboardControllerState state);
+
+// Model for the virtual keyboard UI.
+class KEYBOARD_EXPORT KeyboardUIModel {
+ public:
+  KeyboardUIModel();
+
+  // Get the current state of the keyboard UI.
+  KeyboardControllerState state() const { return state_; }
+
+  // Changes the current state to another. Only accepts valid state transitions.
+  void ChangeState(KeyboardControllerState new_state);
+
+ private:
+  // Current state of the keyboard UI.
+  KeyboardControllerState state_ = KeyboardControllerState::kInitial;
+
+  DISALLOW_COPY_AND_ASSIGN(KeyboardUIModel);
+};
+
+}  // namespace keyboard
+
+#endif  // ASH_KEYBOARD_UI_KEYBOARD_UI_MODEL_H_