[root_presenter] Send initial MediaButtons state

Add logic to store the last seen report from
a MediaButtons device in InputDeviceImpl.
This report is retrieved whenever a new
listener registers for media events so they
can see what the state is.

SCN-1294 #done

TEST: fx run-test e2e_input_tests

Change-Id: Ia1b81cb5b768af80fe30b4aa6752c5c0c2d217f4
diff --git a/garnet/bin/ui/root_presenter/presentation.cc b/garnet/bin/ui/root_presenter/presentation.cc
index 0a0a7fb..978790d 100644
--- a/garnet/bin/ui/root_presenter/presentation.cc
+++ b/garnet/bin/ui/root_presenter/presentation.cc
@@ -41,6 +41,22 @@
 constexpr float kAmbient = 0.3f;
 constexpr float kNonAmbient = 1.f - kAmbient;
 
+void SendMediaButtonReportToListener(
+    const fuchsia::ui::input::InputReport& report,
+    fuchsia::ui::policy::MediaButtonsListener* listener) {
+  fuchsia::ui::input::MediaButtonsEvent event;
+  int8_t volume_gain = 0;
+  if (report.media_buttons->volume_up) {
+    volume_gain++;
+  }
+  if (report.media_buttons->volume_down) {
+    volume_gain--;
+  }
+  event.set_volume(volume_gain);
+  event.set_mic_mute(report.media_buttons->mic_mute);
+  listener->OnMediaButtonsEvent(std::move(event));
+}
+
 }  // namespace
 
 Presentation::Presentation(
@@ -429,6 +445,7 @@
     state = std::make_unique<ui_input::DeviceState>(
         input_device->id(), input_device->descriptor(), std::move(callback));
   } else if (input_device->descriptor()->media_buttons) {
+    media_buttons_ids_.push_back(input_device->id());
     ui_input::OnMediaButtonsEventCallback callback =
         [this](fuchsia::ui::input::InputReport report) {
           OnMediaButtonsEvent(std::move(report));
@@ -452,6 +469,12 @@
 
 void Presentation::OnDeviceRemoved(uint32_t device_id) {
   FXL_VLOG(1) << "OnDeviceRemoved: device_id=" << device_id;
+  for (size_t i = 0; i < media_buttons_ids_.size(); i++) {
+    if (media_buttons_ids_[i] == device_id) {
+      media_buttons_ids_.erase(media_buttons_ids_.begin() + i);
+      break;
+    }
+  }
   if (device_states_by_id_.count(device_id) != 0) {
     device_states_by_id_[device_id].second->OnUnregistered();
     auto it = cursors_.find(device_id);
@@ -555,6 +578,8 @@
   FXL_LOG(INFO) << "Presentation mode, now listening.";
 }
 
+// TODO(SCN-1405) Eventually pull this out from Presentation into something
+// else.
 void Presentation::RegisterMediaButtonsListener(
     fidl::InterfaceHandle<fuchsia::ui::policy::MediaButtonsListener>
         listener_handle) {
@@ -573,6 +598,17 @@
         media_buttons_listeners_.end());
   });
 
+  // Send the last seen report to the listener so they have the information
+  // about the media button's state.
+  for (uint32_t media_buttons_id : media_buttons_ids_) {
+    const ui_input::InputDeviceImpl* device_impl =
+        std::get<0>(device_states_by_id_[media_buttons_id]);
+    const fuchsia::ui::input::InputReport* report = device_impl->LastReport();
+    if (report != nullptr) {
+      SendMediaButtonReportToListener(*report, listener.get());
+    }
+  }
+
   media_buttons_listeners_.push_back(std::move(listener));
 }
 
@@ -729,21 +765,13 @@
   }
 }
 
+// TODO(SCN-1405) Eventually pull this out from Presentation into something
+// else.
 void Presentation::OnMediaButtonsEvent(fuchsia::ui::input::InputReport report) {
   FXL_CHECK(report.media_buttons);
 
   for (auto& listener : media_buttons_listeners_) {
-    fuchsia::ui::input::MediaButtonsEvent event;
-    int8_t volume_gain = 0;
-    if (report.media_buttons->volume_up) {
-      volume_gain++;
-    }
-    if (report.media_buttons->volume_down) {
-      volume_gain--;
-    }
-    event.set_volume(volume_gain);
-    event.set_mic_mute(report.media_buttons->mic_mute);
-    listener->OnMediaButtonsEvent(std::move(event));
+    SendMediaButtonReportToListener(report, listener.get());
   }
 }
 
diff --git a/garnet/bin/ui/root_presenter/presentation.h b/garnet/bin/ui/root_presenter/presentation.h
index 2b8fff7..ee050f8 100644
--- a/garnet/bin/ui/root_presenter/presentation.h
+++ b/garnet/bin/ui/root_presenter/presentation.h
@@ -248,6 +248,9 @@
   fuchsia::ui::policy::PresentationMode presentation_mode_;
   std::unique_ptr<presentation_mode::Detector> presentation_mode_detector_;
 
+  // TODO(SCN-1405) Pull these out of a presentation since this should probably
+  // be global state.
+  std::vector<uint32_t> media_buttons_ids_;
   // A registry of listeners for media button events.
   std::vector<fuchsia::ui::policy::MediaButtonsListenerPtr>
       media_buttons_listeners_;
diff --git a/garnet/public/lib/ui/input/input_device_impl.cc b/garnet/public/lib/ui/input/input_device_impl.cc
index 895b47c..ddd219a 100644
--- a/garnet/public/lib/ui/input/input_device_impl.cc
+++ b/garnet/public/lib/ui/input/input_device_impl.cc
@@ -25,6 +25,12 @@
   TRACE_DURATION("input", "input_report_listener", "id", report.trace_id);
   TRACE_FLOW_END("input", "hid_read_to_listener", report.trace_id);
   TRACE_FLOW_BEGIN("input", "report_to_presenter", report.trace_id);
+  if (descriptor_.media_buttons) {
+    if (!last_report_) {
+      last_report_ = fuchsia::ui::input::InputReport::New();
+    }
+    fidl::Clone(report, last_report_.get());
+  }
   listener_->OnReport(this, std::move(report));
 }
 
diff --git a/garnet/public/lib/ui/input/input_device_impl.h b/garnet/public/lib/ui/input/input_device_impl.h
index d600300..10db089d 100644
--- a/garnet/public/lib/ui/input/input_device_impl.h
+++ b/garnet/public/lib/ui/input/input_device_impl.h
@@ -29,12 +29,20 @@
   uint32_t id() { return id_; }
   fuchsia::ui::input::DeviceDescriptor* descriptor() { return &descriptor_; }
 
+  // Returns the last seen InputReport or nullptr if no reports have been
+  // seen. At the moment we only ever save InputReports from a MediaButton,
+  // so all other device types will always return nullptr.
+  const fuchsia::ui::input::InputReport* LastReport() const {
+    return last_report_.get();
+  }
+
  private:
   // |InputDevice|
   void DispatchReport(fuchsia::ui::input::InputReport report) override;
 
   uint32_t id_;
   fuchsia::ui::input::DeviceDescriptor descriptor_;
+  fuchsia::ui::input::InputReportPtr last_report_ = nullptr;
   fidl::Binding<fuchsia::ui::input::InputDevice> input_device_binding_;
   Listener* listener_;
 };
diff --git a/garnet/tests/e2e_input_tests/BUILD.gn b/garnet/tests/e2e_input_tests/BUILD.gn
index 1f62f7b..9e6b8aa 100644
--- a/garnet/tests/e2e_input_tests/BUILD.gn
+++ b/garnet/tests/e2e_input_tests/BUILD.gn
@@ -36,6 +36,7 @@
   testonly = true
 
   sources = [
+    "mediabuttons_listener_test.cc",
     "minimal_input_test.cc",
   ]
 
diff --git a/garnet/tests/e2e_input_tests/mediabuttons_listener_test.cc b/garnet/tests/e2e_input_tests/mediabuttons_listener_test.cc
new file mode 100644
index 0000000..a74fda0
--- /dev/null
+++ b/garnet/tests/e2e_input_tests/mediabuttons_listener_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 <cstdio>
+
+#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/command_line.h"
+#include "src/lib/fxl/log_settings_command_line.h"
+#include "src/lib/fxl/logging.h"
+
+namespace {
+
+using fuchsia::ui::input::InputEvent;
+using fuchsia::ui::input::MediaButtonsEvent;
+using Phase = fuchsia::ui::input::PointerEventPhase;
+
+// 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);
+
+// This implements the MediaButtonsListener class. Its purpose is to attach
+// to the presentation and test that MediaButton Events are actually sent
+// out to the Listeners.
+class ButtonsListenerImpl : public fuchsia::ui::policy::MediaButtonsListener {
+ public:
+  ButtonsListenerImpl(
+      fidl::InterfaceRequest<fuchsia::ui::policy::MediaButtonsListener>
+          listener_request)
+      : listener_binding_(this, std::move(listener_request)) {}
+  ~ButtonsListenerImpl() = default;
+
+  int EventsSeen() { return events_seen_; }
+
+  void SetOnTerminateCallback(
+      fit::function<void(const std::vector<MediaButtonsEvent>&)> on_terminate,
+      int num_events_to_terminate) {
+    on_terminate_ = std::move(on_terminate);
+    num_events_to_terminate_ = num_events_to_terminate;
+  }
+
+ private:
+  // |MediaButtonsListener|
+  void OnMediaButtonsEvent(
+      fuchsia::ui::input::MediaButtonsEvent event) override {
+    // Store inputs for checking later.
+    observed_.push_back(std::move(event));
+
+    events_seen_++;
+    if (events_seen_ >= num_events_to_terminate_) {
+      on_terminate_(observed_);
+    }
+  }
+
+  int events_seen_ = 0;
+  int num_events_to_terminate_ = 0;
+  fidl::Binding<fuchsia::ui::policy::MediaButtonsListener> listener_binding_;
+  fit::function<void(const std::vector<MediaButtonsEvent>&)> on_terminate_;
+  std::vector<MediaButtonsEvent> observed_;
+};
+
+// A very small Scenic client. Puts up a fuchsia-colored rectangle.
+class MinimalClientView : public scenic::BaseView {
+ public:
+  MinimalClientView(scenic::ViewContext context, async_dispatcher_t* dispatcher)
+      : scenic::BaseView(std::move(context), "MinimalClientView"),
+        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);
+        });
+  }
+
+ private:
+  // |scenic::SessionListener|
+  void OnScenicError(std::string error) override { FXL_LOG(FATAL) << error; }
+
+  async_dispatcher_t* dispatcher_ = nullptr;
+};
+
+class MediaButtonsListenerTest : public gtest::RealLoopFixture {
+ protected:
+  // Mildly complex ctor, but we don't throw and we don't call virtual methods.
+  MediaButtonsListenerTest() {
+    // 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 = {
+        .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<MinimalClientView>(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);
+    });
+
+    fidl::InterfaceHandle<fuchsia::ui::policy::Presentation> presentation;
+    root_presenter_->PresentView(std::move(view_holder_token),
+                                 presentation.NewRequest());
+
+    // Connect to the MediaButtons listener.
+    fidl::InterfacePtr<fuchsia::ui::policy::Presentation> presentation_ptr;
+    fidl::InterfaceHandle<fuchsia::ui::policy::MediaButtonsListener>
+        listener_handle;
+    button_listener_impl_ =
+        std::make_unique<ButtonsListenerImpl>(listener_handle.NewRequest());
+
+    presentation_ptr = presentation.Bind();
+    presentation_ptr->RegisterMediaButtonsListener(std::move(listener_handle));
+
+    // 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);
+  }
+
+  ~MediaButtonsListenerTest() 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(const std::vector<MediaButtonsEvent>&)> on_terminate,
+      int num_events_to_terminate) {
+    button_listener_impl_->SetOnTerminateCallback(std::move(on_terminate),
+                                                  num_events_to_terminate);
+  }
+
+  std::unique_ptr<ButtonsListenerImpl> button_listener_impl_;
+  fuchsia::ui::policy::PresenterPtr root_presenter_;
+  fuchsia::ui::scenic::ScenicPtr scenic_;
+
+  std::unique_ptr<MinimalClientView> view_;
+  uint32_t display_width_ = 0;
+  uint32_t display_height_ = 0;
+
+  fit::function<void()> inject_input_;
+  bool test_was_run_ = false;
+};
+
+TEST_F(MediaButtonsListenerTest, MediaButtons) {
+  // Set up inputs. Fires when display and content are available.
+  SetInjectInputCallback([this] {
+    InjectInput({"media_button", "1", "1", "1", "1", nullptr});
+  });
+
+  // Set up expectations. Terminate when we see 1 message.
+  SetOnTerminateCallback(
+      [this](const std::vector<MediaButtonsEvent>& observed) {
+        EXPECT_EQ(observed.size(), 1U);
+        QuitLoop();
+        // Today, we can't quietly break the View/ViewHolder connection.
+      },
+      1);
+
+  RunLoop();  // Go!
+}
+
+}  // namespace
+
+// NOTE: We link in FXL's gtest_main to enable proper logging.
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 cd5b523..df915aa 100644
--- a/garnet/tests/e2e_input_tests/meta/minimal_input_test.cmx
+++ b/garnet/tests/e2e_input_tests/meta/minimal_input_test.cmx
@@ -19,6 +19,7 @@
             "shell-commands"
         ],
         "services": [
+            "fuchsia.tracelink.Registry",
             "fuchsia.process.Launcher",
             "fuchsia.process.Resolver",
             "fuchsia.sys.Environment",