[scenic] Add IME support to BaseView.

Testing: E2E test to ensure keyboard events are piped through IME.

Change-Id: I0d71601c73768326409676e81007ca3e06b416a1
diff --git a/garnet/public/lib/ui/base_view/cpp/base_view.cc b/garnet/public/lib/ui/base_view/cpp/base_view.cc
index ad341e0..e375211 100644
--- a/garnet/public/lib/ui/base_view/cpp/base_view.cc
+++ b/garnet/public/lib/ui/base_view/cpp/base_view.cc
@@ -8,6 +8,7 @@
 #include <lib/ui/scenic/cpp/commands.h>
 #include <lib/ui/scenic/cpp/view_token_pair.h>
 #include <trace/event.h>
+#include <zircon/status.h>
 
 #include "src/lib/fxl/logging.h"
 
@@ -21,13 +22,28 @@
                         std::move(context.session_and_listener_request.second)),
       session_(std::move(context.session_and_listener_request.first)),
       view_(&session_, std::move(context.view_token), debug_name),
-      root_node_(&session_) {
+      root_node_(&session_),
+      ime_client_(this),
+      enable_ime_(context.enable_ime) {
   session_.SetDebugName(debug_name);
 
   // Listen for metrics events on our top node.
   root_node_.SetEventMask(fuchsia::ui::gfx::kMetricsEventMask);
   view_.AddChild(root_node_);
 
+  if (enable_ime_) {
+    startup_context_->ConnectToEnvironmentService(ime_manager_.NewRequest());
+
+    ime_.set_error_handler([](zx_status_t status) {
+      FXL_LOG(ERROR) << "Interface error on: Input Method Editor "
+                     << zx_status_get_string(status);
+    });
+    ime_manager_.set_error_handler([](zx_status_t status) {
+      FXL_LOG(ERROR) << "Interface error on: Text Sync Service "
+                     << zx_status_get_string(status);
+    });
+  }
+
   // We must immediately invalidate the scene, otherwise we wouldn't ever hook
   // the View up to the ViewHolder.  An alternative would be to require
   // subclasses to call an Init() method to set up the initial connection.
@@ -101,6 +117,11 @@
         }
         break;
       case ::fuchsia::ui::scenic::Event::Tag::kInput: {
+        if (event.input().Which() ==
+                fuchsia::ui::input::InputEvent::Tag::kFocus &&
+            enable_ime_) {
+          OnHandleFocusEvent(event.input().focus());
+        }
         OnInputEvent(std::move(event.input()));
         break;
       }
@@ -154,4 +175,50 @@
       });
 }
 
+// |fuchsia::ui::input::InputMethodEditorClient|
+void BaseView::DidUpdateState(
+    fuchsia::ui::input::TextInputState state,
+    std::unique_ptr<fuchsia::ui::input::InputEvent> input_event) {
+  if (input_event) {
+    const fuchsia::ui::input::InputEvent& input = *input_event;
+    fuchsia::ui::input::InputEvent input_event_copy;
+    fidl::Clone(input, &input_event_copy);
+    OnInputEvent(std::move(input_event_copy));
+  }
+}
+
+// |fuchsia::ui::input::InputMethodEditorClient|
+void BaseView::OnAction(fuchsia::ui::input::InputMethodAction action) {}
+
+bool BaseView::OnHandleFocusEvent(const fuchsia::ui::input::FocusEvent& focus) {
+  if (focus.focused) {
+    ActivateIme();
+    return true;
+  } else if (!focus.focused) {
+    DeactivateIme();
+    return true;
+  }
+  return false;
+}
+
+void BaseView::ActivateIme() {
+  ime_manager_->GetInputMethodEditor(
+      fuchsia::ui::input::KeyboardType::TEXT,       // keyboard type
+      fuchsia::ui::input::InputMethodAction::DONE,  // input method action
+      fuchsia::ui::input::TextInputState{},         // initial state
+      ime_client_.NewBinding(),                     // client
+      ime_.NewRequest()                             // editor
+  );
+}
+
+void BaseView::DeactivateIme() {
+  if (ime_) {
+    ime_manager_->HideKeyboard();
+    ime_ = nullptr;
+  }
+  if (ime_client_.is_bound()) {
+    ime_client_.Unbind();
+  }
+}
+
 }  // namespace scenic
diff --git a/garnet/public/lib/ui/base_view/cpp/base_view.h b/garnet/public/lib/ui/base_view/cpp/base_view.h
index 4b0578c..eb12057 100644
--- a/garnet/public/lib/ui/base_view/cpp/base_view.h
+++ b/garnet/public/lib/ui/base_view/cpp/base_view.h
@@ -23,6 +23,7 @@
   fidl::InterfaceRequest<fuchsia::sys::ServiceProvider> incoming_services;
   fidl::InterfaceHandle<fuchsia::sys::ServiceProvider> outgoing_services;
   component::StartupContext* startup_context;
+  bool enable_ime = false;
 };
 
 // Abstract base implementation of a view for simple applications.
@@ -31,7 +32,8 @@
 //
 // It is not necessary to use this class to implement all Views.
 // This class is merely intended to make the simple apps easier to write.
-class BaseView : private fuchsia::ui::scenic::SessionListener {
+class BaseView : private fuchsia::ui::scenic::SessionListener,
+                 private fuchsia::ui::input::InputMethodEditorClient {
  public:
   // Subclasses are typically created by ViewProviderService::CreateView(),
   // which provides the necessary args to pass down to this base class.
@@ -161,8 +163,27 @@
   // Subclasses should not override this.
   void OnScenicEvent(::std::vector<fuchsia::ui::scenic::Event> events) override;
 
+  // |fuchsia::ui::input::InputMethodEditorClient|
+  void DidUpdateState(
+      fuchsia::ui::input::TextInputState state,
+      std::unique_ptr<fuchsia::ui::input::InputEvent> event) override;
+
+  // |fuchsia::ui::input::InputMethodEditorClient|
+  void OnAction(fuchsia::ui::input::InputMethodAction action) override;
+
   void PresentScene(zx_time_t presentation_time);
 
+  // Handles focus event when IME is enabled. This event is used to activate
+  // or deactivate the IME client.
+  bool OnHandleFocusEvent(const fuchsia::ui::input::FocusEvent& focus);
+
+  // Gets a new input method editor from the IME manager.
+  void ActivateIme();
+
+  // Detaches the input method editor connection, ending the edit session and
+  // closing the onscreen keyboard.
+  void DeactivateIme();
+
   component::StartupContext* const startup_context_;
   fuchsia::sys::ServiceProviderPtr incoming_services_;
   component::ServiceNamespace outgoing_services_;
@@ -172,6 +193,10 @@
   scenic::View view_;
   scenic::EntityNode root_node_;
 
+  fidl::Binding<fuchsia::ui::input::InputMethodEditorClient> ime_client_;
+  fuchsia::ui::input::InputMethodEditorPtr ime_;
+  fuchsia::ui::input::ImeServicePtr ime_manager_;
+
   fuchsia::ui::gfx::vec3 logical_size_;
   fuchsia::ui::gfx::vec3 physical_size_;
   fuchsia::ui::gfx::ViewProperties view_properties_;
@@ -181,6 +206,7 @@
   size_t session_present_count_ = 0;
   bool invalidate_pending_ = false;
   bool present_pending_ = false;
+  bool enable_ime_ = false;
 };
 
 }  // namespace scenic
diff --git a/garnet/tests/e2e_input_tests/BUILD.gn b/garnet/tests/e2e_input_tests/BUILD.gn
index 32bbc5d..f40fc73 100644
--- a/garnet/tests/e2e_input_tests/BUILD.gn
+++ b/garnet/tests/e2e_input_tests/BUILD.gn
@@ -7,12 +7,23 @@
 
 test_package("e2e_input_tests") {
   deps = [
+    ":base_view_ime_test",
     ":mediabuttons_listener_test",
     ":minimal_input_test",
   ]
 
   tests = [
     {
+      name = "base_view_ime_test"
+      environments = [
+        {
+          dimensions = {
+            device_type = "Intel NUC Kit NUC7i5DNHE"
+          }
+        },
+      ]
+    },
+    {
       name = "minimal_input_test"
       environments = [
         {
@@ -61,6 +72,31 @@
 }
 
 # Each e2e test must run in its own executable.
+test("base_view_ime_test") {
+  sources = [
+    "base_view_ime_test.cc",
+  ]
+  output_name = "base_view_ime_test"
+  deps = [
+    "//garnet/public/lib/component/cpp",
+    "//garnet/public/lib/gtest",
+    "//garnet/public/lib/ui/base_view/cpp",
+    "//garnet/public/lib/ui/input/cpp",
+    "//sdk/fidl/fuchsia.ui.input",
+    "//sdk/fidl/fuchsia.ui.policy",
+    "//sdk/fidl/fuchsia.ui.scenic",
+    "//sdk/lib/ui/scenic/cpp",
+    "//src/lib/fxl",
+    "//src/lib/fxl/test:gtest_main",
+    "//third_party/googletest:gtest",
+    "//zircon/public/lib/async-loop-cpp",
+    "//zircon/public/lib/fdio",
+    "//zircon/public/lib/fit",
+    "//zircon/public/lib/zx",
+  ]
+}
+
+# Each e2e test must run in its own executable.
 test("mediabuttons_listener_test") {
   sources = [
     "mediabuttons_listener_test.cc",
diff --git a/garnet/tests/e2e_input_tests/base_view_ime_test.cc b/garnet/tests/e2e_input_tests/base_view_ime_test.cc
new file mode 100644
index 0000000..1656d79
--- /dev/null
+++ b/garnet/tests/e2e_input_tests/base_view_ime_test.cc
@@ -0,0 +1,278 @@
+// Copyright 2019 The Fuchsia 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 <fuchsia/ui/input/cpp/fidl.h>
+#include <fuchsia/ui/policy/cpp/fidl.h>
+#include <fuchsia/ui/scenic/cpp/fidl.h>
+#include <gtest/gtest.h>
+#include <lib/async-loop/cpp/loop.h>
+#include <lib/component/cpp/startup_context.h>
+#include <lib/fdio/spawn.h>
+#include <lib/fit/function.h>
+#include <lib/gtest/real_loop_fixture.h>
+#include <lib/ui/base_view/cpp/base_view.h>
+#include <lib/ui/input/cpp/formatting.h>
+#include <lib/ui/scenic/cpp/session.h>
+#include <lib/ui/scenic/cpp/view_token_pair.h>
+#include <zircon/status.h>
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "src/lib/fxl/logging.h"
+
+// NOTE WELL. Run each of these e2e tests in its own executable.  They each
+// consume and maintain process-global context, so it's better to keep them
+// separate.  Plus, separation means they start up components in a known good
+// state, instead of reusing component state possibly dirtied by other tests.
+
+namespace {
+
+using fuchsia::ui::input::InputEvent;
+using PointerPhase = fuchsia::ui::input::PointerEventPhase;
+using KeyboardPhase = fuchsia::ui::input::KeyboardEventPhase;
+
+// Shared context for all tests in this process.
+// Set it up once, never delete it.
+component::StartupContext* g_context = nullptr;
+
+// Max timeout in failure cases.
+// Set this as low as you can that still works across all test platforms.
+constexpr zx::duration kTimeout = zx::min(5);
+
+// A very small Scenic client. Puts up a fuchsia-colored rectangle, and stores
+// input events for examination.
+class ImeClientView : public scenic::BaseView {
+ public:
+  ImeClientView(scenic::ViewContext context, async_dispatcher_t* dispatcher)
+      : scenic::BaseView(std::move(context), "ImeClientView"),
+        dispatcher_(dispatcher) {
+    FXL_CHECK(dispatcher_);
+  }
+
+  void CreateScene(uint32_t width_in_px, uint32_t height_in_px) {
+    float width = static_cast<float>(width_in_px);
+    float height = static_cast<float>(height_in_px);
+
+    scenic::ShapeNode background(session());
+
+    scenic::Material material(session());
+    material.SetColor(255, 0, 255, 255);  // Fuchsia
+    background.SetMaterial(material);
+
+    scenic::Rectangle rectangle(session(), width, height);
+    background.SetShape(rectangle);
+    background.SetTranslation(width / 2, height / 2, -10.f);
+
+    root_node().AddChild(background);
+  }
+
+  void Update(uint64_t present_time) {
+    session()->Present(
+        present_time, [this](fuchsia::images::PresentationInfo info) {
+          Update(info.presentation_time + info.presentation_interval);
+        });
+  }
+
+  // |scenic::BaseView|
+  void OnInputEvent(InputEvent event) override {
+    if (on_input_) {
+      on_input_(std::move(event));
+    }
+  }
+
+  void SetOnInputCallback(fit::function<void(InputEvent)> on_input) {
+    on_input_ = std::move(on_input);
+  }
+
+ private:
+  // |scenic::SessionListener|
+  void OnScenicError(std::string error) override { FXL_LOG(FATAL) << error; }
+
+  async_dispatcher_t* dispatcher_ = nullptr;
+  fit::function<void(InputEvent)> on_input_;
+};
+
+class ImeInputTest : public gtest::RealLoopFixture {
+ protected:
+  // Mildly complex ctor, but we don't throw and we don't call virtual methods.
+  ImeInputTest() {
+    // This fixture constructor may run multiple times, but we want the context
+    // to be set up just once per process.
+    if (g_context == nullptr) {
+      g_context = component::StartupContext::CreateFromStartupInfo().release();
+    }
+
+    auto [view_token, view_holder_token] = scenic::ViewTokenPair::New();
+
+    // Connect to Scenic, create a View.
+    scenic_ =
+        g_context->ConnectToEnvironmentService<fuchsia::ui::scenic::Scenic>();
+    scenic_.set_error_handler([](zx_status_t status) {
+      FXL_LOG(FATAL) << "Lost connection to Scenic: "
+                     << zx_status_get_string(status);
+    });
+    scenic::ViewContext view_context = {
+        .enable_ime = true,
+        .session_and_listener_request =
+            scenic::CreateScenicSessionPtrAndListenerRequest(scenic_.get()),
+        .view_token = std::move(view_token),
+        .incoming_services = nullptr,
+        .outgoing_services = nullptr,
+        .startup_context = g_context,
+    };
+    view_ = std::make_unique<ImeClientView>(std::move(view_context),
+                                                dispatcher());
+
+    // Connect to RootPresenter, create a ViewHolder.
+    root_presenter_ =
+        g_context
+            ->ConnectToEnvironmentService<fuchsia::ui::policy::Presenter>();
+    root_presenter_.set_error_handler([](zx_status_t status) {
+      FXL_LOG(FATAL) << "Lost connection to RootPresenter: "
+                     << zx_status_get_string(status);
+    });
+    root_presenter_->PresentView(std::move(view_holder_token), nullptr);
+
+    // When display is available, create content and drive input to touchscreen.
+    scenic_->GetDisplayInfo([this](fuchsia::ui::gfx::DisplayInfo display_info) {
+      display_width_ = display_info.width_in_px;
+      display_height_ = display_info.height_in_px;
+
+      FXL_CHECK(display_width_ > 0 && display_height_ > 0)
+          << "Display size unsuitable for this test: (" << display_width_
+          << ", " << display_height_ << ").";
+
+      view_->CreateScene(display_width_, display_height_);
+      view_->Update(zx_clock_get_monotonic());
+
+      inject_input_();  // Display up, content ready. Send in input.
+
+      test_was_run_ = true;  // Actually did work for this test.
+    });
+
+    // Post a "just in case" quit task, if the test hangs.
+    async::PostDelayedTask(
+        dispatcher(),
+        [] {
+          FXL_LOG(FATAL)
+              << "\n\n>> Test did not complete in time, terminating. <<\n\n";
+        },
+        kTimeout);
+  }
+
+  ~ImeInputTest() override {
+    FXL_CHECK(test_was_run_) << "Oops, didn't actually do anything.";
+  }
+
+  void InjectInput(std::vector<const char*> args) {
+    // Start with process name, end with nullptr.
+    args.insert(args.begin(), "input");
+    args.push_back(nullptr);
+
+    // Start the /bin/input process.
+    zx_handle_t proc;
+    zx_status_t status = fdio_spawn(ZX_HANDLE_INVALID, FDIO_SPAWN_CLONE_ALL,
+                                    "/bin/input", args.data(), &proc);
+    FXL_CHECK(status == ZX_OK)
+        << "fdio_spawn: " << zx_status_get_string(status);
+
+    // Wait for termination.
+    status = zx_object_wait_one(proc, ZX_PROCESS_TERMINATED,
+                                (zx::clock::get_monotonic() + kTimeout).get(),
+                                nullptr);
+    FXL_CHECK(status == ZX_OK)
+        << "zx_object_wait_one: " << zx_status_get_string(status);
+
+    // Check termination status.
+    zx_info_process_t info;
+    status = zx_object_get_info(proc, ZX_INFO_PROCESS, &info, sizeof(info),
+                                nullptr, nullptr);
+    FXL_CHECK(status == ZX_OK)
+        << "zx_object_get_info: " << zx_status_get_string(status);
+    FXL_CHECK(info.return_code == 0)
+        << "info.return_code: " << info.return_code;
+  }
+
+  void SetInjectInputCallback(fit::function<void()> inject_input) {
+    inject_input_ = std::move(inject_input);
+  }
+
+  void SetOnTerminateCallback(fit::function<void()> on_terminate) {
+    on_terminate_ = std::move(on_terminate);
+  }
+
+  fuchsia::ui::policy::PresenterPtr root_presenter_;
+  fuchsia::ui::scenic::ScenicPtr scenic_;
+
+  std::unique_ptr<ImeClientView> view_;
+  uint32_t display_width_ = 0;
+  uint32_t display_height_ = 0;
+
+  std::vector<InputEvent> observed_;
+  fit::function<void()> inject_input_;
+  fit::function<void()> on_terminate_;
+  bool test_was_run_ = false;
+};
+
+TEST_F(ImeInputTest, Keyboard) {
+  // Handle input. Fires for every input event received.
+  view_->SetOnInputCallback([this](InputEvent event) {
+    // Store inputs for checking later.
+    observed_.push_back(std::move(event));
+
+    // Inject text events after tap gesture is done and view has focus.
+    if (event.is_pointer() && event.pointer().phase == PointerPhase::REMOVE) {
+      async::PostTask(dispatcher(), [this] {
+        // Send the Esc key(hid usage code: 41)
+        InjectInput({"keyevent", "41", nullptr});
+      });
+    }
+
+    // Simple termination condition: when key up event is received.
+    if (event.is_keyboard() &&
+        event.keyboard().phase == KeyboardPhase::RELEASED) {
+      async::PostTask(dispatcher(), [this] {
+        FXL_CHECK(on_terminate_) << "on_terminate_ was not set!";
+        on_terminate_();
+      });
+    }
+  });
+
+  // Inject tap. Fires when display and content are available.
+  SetInjectInputCallback([this] {
+    InjectInput({"tap",  // Tap at the center of the display
+                 std::to_string(display_width_ / 2).c_str(),
+                 std::to_string(display_height_ / 2).c_str(), nullptr});
+  });
+
+  // Set up expectations. Fires when we see the "quit" message.
+  SetOnTerminateCallback([this]() {
+    if (FXL_VLOG_IS_ON(2)) {
+      for (const auto& event : observed_) {
+        FXL_LOG(INFO) << "Input event observed: " << event;
+      }
+    }
+
+    EXPECT_EQ(observed_.size(), 7u);
+
+    EXPECT_EQ(observed_[0].pointer().phase, PointerPhase::ADD);
+    EXPECT_TRUE(observed_[1].focus().focused);
+    EXPECT_EQ(observed_[2].pointer().phase, PointerPhase::DOWN);
+    EXPECT_EQ(observed_[3].pointer().phase, PointerPhase::UP);
+    EXPECT_EQ(observed_[4].pointer().phase, PointerPhase::REMOVE);
+    EXPECT_EQ(observed_[5].keyboard().phase, KeyboardPhase::PRESSED);
+    EXPECT_EQ(observed_[6].keyboard().phase, KeyboardPhase::RELEASED);
+
+    QuitLoop();
+    // Today, we can't quietly break the View/ViewHolder connection.
+  });
+
+  RunLoop();  // Go!
+}
+
+}  // namespace
+
+// NOTE: We link in FXL's gtest_main to enable proper logging.
diff --git a/garnet/tests/e2e_input_tests/meta/base_view_ime_test.cmx b/garnet/tests/e2e_input_tests/meta/base_view_ime_test.cmx
new file mode 100644
index 0000000..b09d864
--- /dev/null
+++ b/garnet/tests/e2e_input_tests/meta/base_view_ime_test.cmx
@@ -0,0 +1,34 @@
+{
+    "facets": {
+        "fuchsia.test": {
+            "injected-services": {
+                "fuchsia.sysmem.Allocator": "fuchsia-pkg://fuchsia.com/sysmem_connector#meta/sysmem_connector.cmx",
+                "fuchsia.ui.input.ImeService": "fuchsia-pkg://fuchsia.com/ime_service#meta/ime_service.cmx",
+                "fuchsia.ui.input.InputDeviceRegistry": "fuchsia-pkg://fuchsia.com/root_presenter#meta/root_presenter.cmx",
+                "fuchsia.ui.policy.Presenter": "fuchsia-pkg://fuchsia.com/root_presenter#meta/root_presenter.cmx",
+                "fuchsia.ui.scenic.Scenic": "fuchsia-pkg://fuchsia.com/scenic#meta/scenic.cmx",
+                "fuchsia.vulkan.loader.Loader": "fuchsia-pkg://fuchsia.com/vulkan_loader#meta/vulkan_loader.cmx"
+            }
+        }
+    },
+    "program": {
+        "binary": "test/base_view_ime_test"
+    },
+    "sandbox": {
+        "features": [
+            "shell-commands"
+        ],
+        "services": [
+            "fuchsia.tracelink.Registry",
+            "fuchsia.process.Launcher",
+            "fuchsia.process.Resolver",
+            "fuchsia.sys.Environment",
+            "fuchsia.sys.Launcher",
+            "fuchsia.sys.Loader",
+            "fuchsia.ui.input.ImeService",
+            "fuchsia.ui.input.InputDeviceRegistry",
+            "fuchsia.ui.policy.Presenter",
+            "fuchsia.ui.scenic.Scenic"
+        ]
+    }
+}
diff --git a/garnet/tests/e2e_input_tests/meta/minimal_input_test.cmx b/garnet/tests/e2e_input_tests/meta/minimal_input_test.cmx
index df915aa..176793b 100644
--- a/garnet/tests/e2e_input_tests/meta/minimal_input_test.cmx
+++ b/garnet/tests/e2e_input_tests/meta/minimal_input_test.cmx
@@ -25,6 +25,7 @@
             "fuchsia.sys.Environment",
             "fuchsia.sys.Launcher",
             "fuchsia.sys.Loader",
+            "fuchsia.ui.input.ImeService",
             "fuchsia.ui.input.InputDeviceRegistry",
             "fuchsia.ui.policy.Presenter",
             "fuchsia.ui.scenic.Scenic"
diff --git a/products/workstation.gni b/products/workstation.gni
index 9c6ac16..0eef5c1 100644
--- a/products/workstation.gni
+++ b/products/workstation.gni
@@ -10,11 +10,10 @@
   "//src/modular/bundles:framework",
   "//topaz/packages/config:ermine",
   "//topaz/packages/prod:ermine",
+  "//topaz/packages/prod:term",
 ]
 
-cache_package_labels += [
-  "//topaz/packages/prod:simple_browser",
-]
+cache_package_labels += [ "//topaz/packages/prod:simple_browser" ]
 
 universe_package_labels += [
   "//garnet/packages/prod:media_audio",