[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",