diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter index 1e7141f4def..0a9309d8c38 100755 --- a/engine/src/flutter/ci/licenses_golden/licenses_flutter +++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter @@ -1230,6 +1230,9 @@ FILE: ../../../flutter/shell/platform/fuchsia/flutter/isolate_configurator.h FILE: ../../../flutter/shell/platform/fuchsia/flutter/kernel/extract_far.dart FILE: ../../../flutter/shell/platform/fuchsia/flutter/kernel/framework_shim.dart FILE: ../../../flutter/shell/platform/fuchsia/flutter/kernel/libraries.json +FILE: ../../../flutter/shell/platform/fuchsia/flutter/keyboard.cc +FILE: ../../../flutter/shell/platform/fuchsia/flutter/keyboard.h +FILE: ../../../flutter/shell/platform/fuchsia/flutter/keyboard_unittest.cc FILE: ../../../flutter/shell/platform/fuchsia/flutter/logging.h FILE: ../../../flutter/shell/platform/fuchsia/flutter/loop.cc FILE: ../../../flutter/shell/platform/fuchsia/flutter/loop.h diff --git a/engine/src/flutter/shell/platform/fuchsia/flutter/BUILD.gn b/engine/src/flutter/shell/platform/fuchsia/flutter/BUILD.gn index c03534e7c9d..dc6d225b146 100644 --- a/engine/src/flutter/shell/platform/fuchsia/flutter/BUILD.gn +++ b/engine/src/flutter/shell/platform/fuchsia/flutter/BUILD.gn @@ -63,6 +63,8 @@ template("runner_sources") { "fuchsia_intl.h", "isolate_configurator.cc", "isolate_configurator.h", + "keyboard.cc", + "keyboard.h", "logging.h", "loop.cc", "loop.h", @@ -446,6 +448,7 @@ executable("flutter_runner_unittests") { "component_unittest.cc", "flutter_runner_fakes.h", "fuchsia_intl_unittest.cc", + "keyboard_unittest.cc", "platform_view_unittest.cc", "runner_unittest.cc", "tests/engine_unittests.cc", diff --git a/engine/src/flutter/shell/platform/fuchsia/flutter/engine.cc b/engine/src/flutter/shell/platform/fuchsia/flutter/engine.cc index 1a3f7ebf8c5..c867be19564 100644 --- a/engine/src/flutter/shell/platform/fuchsia/flutter/engine.cc +++ b/engine/src/flutter/shell/platform/fuchsia/flutter/engine.cc @@ -101,10 +101,16 @@ Engine::Engine(Delegate& delegate, scenic->CreateSession2(session.NewRequest(), session_listener.Bind(), focuser.NewRequest()); - // Make clones of the `ViewRef` before sending it down to Scenic. - fuchsia::ui::views::ViewRef platform_view_ref, isolate_view_ref; + // Make clones of the `ViewRef` before sending it down to Scenic, since the + // refs are not copyable, and multiple consumers need view refs. + fuchsia::ui::views::ViewRef platform_view_ref; view_ref_pair.view_ref.Clone(&platform_view_ref); + fuchsia::ui::views::ViewRef isolate_view_ref; view_ref_pair.view_ref.Clone(&isolate_view_ref); + // Input3 keyboard listener registration requires a ViewRef as an event + // filter. So we clone it here, as ViewRefs can not be reused, only cloned. + fuchsia::ui::views::ViewRef keyboard_view_ref; + view_ref_pair.view_ref.Clone(&keyboard_view_ref); // Session is terminated on the raster thread, but we must terminate ourselves // on the platform thread. @@ -203,6 +209,26 @@ Engine::Engine(Delegate& delegate, auto run_configuration = flutter::RunConfiguration::InferFromSettings( settings, task_runners.GetIOTaskRunner()); + // Connect to fuchsia.ui.input3.Keyboard to hand out a listener. + using fuchsia::ui::input3::Keyboard; + using fuchsia::ui::input3::KeyboardListener; + + // Keyboard client-side stub. + keyboard_svc_ = svc->Connect(); + ZX_ASSERT(keyboard_svc_.is_bound()); + // KeyboardListener handle pair is not initialized until NewRequest() is + // called. + fidl::InterfaceHandle keyboard_listener; + + // Server side of KeyboardListener. Initializes the keyboard_listener + // handle. + fidl::InterfaceRequest keyboard_listener_request = + keyboard_listener.NewRequest(); + ZX_ASSERT(keyboard_listener_request.is_valid()); + + keyboard_svc_->AddListener(std::move(keyboard_view_ref), + keyboard_listener.Bind(), [] {}); + // Setup the callback that will instantiate the platform view. flutter::Shell::CreateCallback on_create_platform_view = fml::MakeCopyable( @@ -222,7 +248,9 @@ Engine::Engine(Delegate& delegate, on_create_surface_callback = std::move(on_create_surface_callback), external_view_embedder = GetExternalViewEmbedder(), vsync_offset = product_config.get_vsync_offset(), - vsync_handle = vsync_event_.get()](flutter::Shell& shell) mutable { + vsync_handle = vsync_event_.get(), + keyboard_listener_request = std::move(keyboard_listener_request)]( + flutter::Shell& shell) mutable { return std::make_unique( shell, // delegate debug_label, // debug label @@ -232,6 +260,9 @@ Engine::Engine(Delegate& delegate, std::move(parent_environment_service_provider), // services std::move(session_listener_request), // session listener std::move(focuser), + // Server-side part of the fuchsia.ui.input3.KeyboardListener + // connection. + std::move(keyboard_listener_request), std::move(on_session_listener_error_callback), std::move(on_enable_wireframe_callback), std::move(on_create_view_callback), diff --git a/engine/src/flutter/shell/platform/fuchsia/flutter/engine.h b/engine/src/flutter/shell/platform/fuchsia/flutter/engine.h index 9fd5c60782e..fd77f3ff452 100644 --- a/engine/src/flutter/shell/platform/fuchsia/flutter/engine.h +++ b/engine/src/flutter/shell/platform/fuchsia/flutter/engine.h @@ -114,6 +114,8 @@ class Engine final { friend class testing::EngineTest; + fidl::InterfacePtr keyboard_svc_; + FML_DISALLOW_COPY_AND_ASSIGN(Engine); }; diff --git a/engine/src/flutter/shell/platform/fuchsia/flutter/keyboard.cc b/engine/src/flutter/shell/platform/fuchsia/flutter/keyboard.cc new file mode 100644 index 00000000000..5353535003e --- /dev/null +++ b/engine/src/flutter/shell/platform/fuchsia/flutter/keyboard.cc @@ -0,0 +1,318 @@ +// Copyright 2013 The Flutter 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 "flutter/shell/platform/fuchsia/flutter/keyboard.h" + +#include +#include +#include + +#include + +namespace flutter_runner { + +using fuchsia::input::Key; +using fuchsia::ui::input::kModifierCapsLock; +using fuchsia::ui::input::kModifierLeftAlt; +using fuchsia::ui::input::kModifierLeftControl; +using fuchsia::ui::input::kModifierLeftShift; +using fuchsia::ui::input::kModifierNone; +using fuchsia::ui::input::kModifierRightAlt; +using fuchsia::ui::input::kModifierRightControl; +using fuchsia::ui::input::kModifierRightShift; +using fuchsia::ui::input3::KeyEvent; +using fuchsia::ui::input3::KeyEventType; + +namespace { + +// A simple keymap from a QWERTY keyboard to code points. A value 0 means no +// code point has been assigned for the respective keypress. Column 0 is the +// code point without a level modifier active, and Column 1 is the code point +// with a level modifier (e.g. Shift key) active. +static const uint32_t QWERTY_TO_CODE_POINTS[][2] = { + // 0x00 + {}, + {}, + {}, + {}, + // 0x04, + {'a', 'A'}, + {'b', 'B'}, + {'c', 'C'}, + {'d', 'D'}, + // 0x08 + {'e', 'E'}, + {'f', 'F'}, + {'g', 'G'}, + {'h', 'H'}, + // 0x0c + {'i', 'I'}, + {'j', 'J'}, + {'k', 'K'}, + {'l', 'L'}, + // 0x10 + {'m', 'M'}, + {'n', 'N'}, + {'o', 'O'}, + {'p', 'P'}, + // 0x14 + {'q', 'Q'}, + {'r', 'R'}, + {'s', 'S'}, + {'t', 'T'}, + // 0x18 + {'u', 'U'}, + {'v', 'V'}, + {'w', 'W'}, + {'x', 'X'}, + // 0x1c + {'y', 'Y'}, + {'z', 'Z'}, + {'1', '!'}, + {'2', '@'}, + // 0x20 + {'3', '#'}, + {'4', '$'}, + {'5', '%'}, + {'6', '^'}, + // 0x24 + {'7', '&'}, + {'8', '*'}, + {'9', '('}, + {'0', ')'}, + // 0x28 + {}, + {}, + {}, + {}, + // 0x2c + {' ', ' '}, + {'-', '_'}, + {'=', '+'}, + {'[', '{'}, + // 0x30 + {']', '}'}, + {'\\', '|'}, + {}, + {';', ':'}, + // 0x34 + {'\'', '"'}, + {'`', '~'}, + {',', '<'}, + {'.', '>'}, + // 0x38 + {'/', '?'}, + {}, + {}, + {}, + // 0x3c + {}, + {}, + {}, + {}, + // 0x40 + {}, + {}, + {}, + {}, + // 0x44 + {}, + {}, + {}, + {}, + // 0x48 + {}, + {}, + {}, + {}, + // 0x4c + {}, + {}, + {}, + {}, + // 0x50 + {}, + {}, + {}, + {}, + // 0x54 + {'/', 0}, + {'*', 0}, + {'-', 0}, + {'+', 0}, + // 0x58 + {}, + {'1', 0}, + {'2', 0}, + {'3', 0}, + // 0x5c + {'4', 0}, + {'5', 0}, + {'6', 0}, + {'7', 0}, + // 0x60 + {'8', 0}, + {'9', 0}, + {'0', 0}, + {'.', 0}, +}; + +} // namespace + +Keyboard::Keyboard() + : any_events_received_(false), + stateful_caps_lock_(false), + left_shift_(false), + right_shift_(false), + left_alt_(false), + right_alt_(false), + left_ctrl_(false), + right_ctrl_(false), + last_event_() {} + +bool Keyboard::ConsumeEvent(KeyEvent event) { + if (!event.has_type()) { + return false; + } + if (!event.has_key()) { + return false; + } + // Check if the time sequence of the events is correct. + + last_event_ = std::move(event); + any_events_received_ = true; + const Key& key = last_event_.key(); + const KeyEventType& event_type = last_event_.type(); + switch (event_type) { + // For modifier keys, a SYNC is the same as a press. + case KeyEventType::SYNC: + switch (key) { + case Key::CAPS_LOCK: + stateful_caps_lock_ = true; + break; + case Key::LEFT_ALT: + left_alt_ = true; + break; + case Key::LEFT_CTRL: + left_ctrl_ = true; + break; + case Key::LEFT_SHIFT: + left_shift_ = true; + break; + case Key::RIGHT_ALT: + right_alt_ = true; + break; + case Key::RIGHT_CTRL: + right_ctrl_ = true; + break; + case Key::RIGHT_SHIFT: + right_shift_ = true; + break; + default: + // no-op + break; + } + break; + case KeyEventType::PRESSED: + switch (key) { + case Key::CAPS_LOCK: + stateful_caps_lock_ = !stateful_caps_lock_; + break; + case Key::LEFT_ALT: + left_alt_ = true; + break; + case Key::LEFT_CTRL: + left_ctrl_ = true; + break; + case Key::LEFT_SHIFT: + left_shift_ = true; + break; + case Key::RIGHT_ALT: + right_alt_ = true; + break; + case Key::RIGHT_CTRL: + right_ctrl_ = true; + break; + case Key::RIGHT_SHIFT: + right_shift_ = true; + break; + default: + // No-op + break; + } + break; + case KeyEventType::RELEASED: + switch (key) { + case Key::CAPS_LOCK: + // No-op. + break; + case Key::LEFT_ALT: + left_alt_ = false; + break; + case Key::LEFT_CTRL: + left_ctrl_ = false; + break; + case Key::LEFT_SHIFT: + left_shift_ = false; + break; + case Key::RIGHT_ALT: + right_alt_ = false; + break; + case Key::RIGHT_CTRL: + right_ctrl_ = false; + break; + case Key::RIGHT_SHIFT: + right_shift_ = false; + break; + default: + // No-op + break; + } + break; + case KeyEventType::CANCEL: + // No-op? + break; + default: + // No-op + break; + } + return false; +} + +bool Keyboard::IsShift() { + return left_shift_ | right_shift_ | stateful_caps_lock_; +} + +bool Keyboard::IsKeys() { + return (static_cast(last_event_.key()) & 0xFFFF0000) == 0x00070000; +} + +uint32_t Keyboard::Modifiers() { + return kModifierNone + (kModifierLeftShift * left_shift_) + + (kModifierLeftAlt * left_alt_) + (kModifierLeftControl * left_ctrl_) + + (kModifierRightShift * right_shift_) + + (kModifierRightAlt * right_alt_) + + (kModifierRightControl * right_ctrl_) + + (kModifierCapsLock * stateful_caps_lock_); +} + +uint32_t Keyboard::LastCodePoint() { + static const int qwerty_map_size = + sizeof(QWERTY_TO_CODE_POINTS) / sizeof(QWERTY_TO_CODE_POINTS[0]); + if (!IsKeys()) { + return 0; + } + const auto usage = LastHIDUsage(); + if (usage < qwerty_map_size) { + return QWERTY_TO_CODE_POINTS[usage][IsShift() & 1]; + } + // Any other keys don't have a code point. + return 0; +} + +uint32_t Keyboard::LastHIDUsage() { + return static_cast(last_event_.key()) & 0xFFFF; +} + +} // namespace flutter_runner diff --git a/engine/src/flutter/shell/platform/fuchsia/flutter/keyboard.h b/engine/src/flutter/shell/platform/fuchsia/flutter/keyboard.h new file mode 100644 index 00000000000..86135488f4f --- /dev/null +++ b/engine/src/flutter/shell/platform/fuchsia/flutter/keyboard.h @@ -0,0 +1,63 @@ +// Copyright 2013 The Flutter 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 FLUTTER_SHELL_PLATFORM_FUCHSIA_KEYBOARD_H_ +#define FLUTTER_SHELL_PLATFORM_FUCHSIA_KEYBOARD_H_ + +#include + +namespace flutter_runner { + +// Keyboard handles the keyboard signals from fuchsia.ui.input3. Specifically, +// input3 has no notion of a code point, and does not track stateful versions +// of the modifier keys. +class Keyboard final { + public: + explicit Keyboard(); + + // Consumes the given keyboard event. Keyboard will adjust the modifier + // state based on the info given in the event. Returns true if the event has + // been integrated into the internal state successfully, or false otherwise. + bool ConsumeEvent(fuchsia::ui::input3::KeyEvent event); + + // Gets the currently active modifier keys. + uint32_t Modifiers(); + + // Gets the last encountered code point. The reported code point depends on + // the state of the modifier keys. + uint32_t LastCodePoint(); + + // Gets the last encountered HID usage. + uint32_t LastHIDUsage(); + + private: + // Return true if any level shift is active. + bool IsShift(); + + // Returns true if the last key event was about a key that may have a code + // point associated. + bool IsKeys(); + + // Set to false until any event is received. + bool any_events_received_ : 1; + + // The flags below show the state of the keyboard modifiers after the last + // event has been processed. Stateful keys remain in the same state after + // a release and require an additional press to toggle. + bool stateful_caps_lock_ : 1; + bool left_shift_ : 1; + bool right_shift_ : 1; + bool left_alt_ : 1; + bool right_alt_ : 1; + bool left_ctrl_ : 1; + bool right_ctrl_ : 1; + + // The last received key event. If any_events_received_ is not set, this is + // not valid. + fuchsia::ui::input3::KeyEvent last_event_; +}; + +} // namespace flutter_runner + +#endif // FLUTTER_SHELL_PLATFORM_FUCHSIA_KEYBOARD_H_ diff --git a/engine/src/flutter/shell/platform/fuchsia/flutter/keyboard_unittest.cc b/engine/src/flutter/shell/platform/fuchsia/flutter/keyboard_unittest.cc new file mode 100644 index 00000000000..bb00d319e29 --- /dev/null +++ b/engine/src/flutter/shell/platform/fuchsia/flutter/keyboard_unittest.cc @@ -0,0 +1,172 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Testing the stateful Fuchsia Input3 keyboard interactions. This test case +// is not intended to be exhaustive: it is intended to capture the tests that +// demonstrate how we think Input3 interaction should work, and possibly +// regression tests if we catch some behavior that needs to be guarded long +// term. Pragmatically, this should be enough to ensure no specific bug +// happens twice. + +#include "flutter/shell/platform/fuchsia/flutter/keyboard.h" + +#include +#include +#include + +#include +#include +#include + +namespace flutter_runner { +namespace { + +using fuchsia::input::Key; +using fuchsia::ui::input::kModifierCapsLock; +using fuchsia::ui::input::kModifierLeftAlt; +using fuchsia::ui::input::kModifierLeftControl; +using fuchsia::ui::input::kModifierLeftShift; +using fuchsia::ui::input::kModifierNone; +using fuchsia::ui::input::kModifierRightAlt; +using fuchsia::ui::input::kModifierRightControl; +using fuchsia::ui::input::kModifierRightShift; +using fuchsia::ui::input3::KeyEvent; +using fuchsia::ui::input3::KeyEventType; + +class KeyboardTest : public testing::Test { + protected: + static void SetUpTestCase() { testing::Test::SetUpTestCase(); } + + // Creates a new key event for testing. + KeyEvent NewKeyEvent(KeyEventType event_type, Key key) { + KeyEvent event; + // Assume events are delivered with correct timing. + event.set_timestamp(++timestamp_); + event.set_type(event_type); + event.set_key(key); + return event; + } + + // Makes the keyboard consume all the provided `events`. The end state of + // the keyboard is as if all of the specified events happened between the + // start state of the keyboard and its end state. + void ConsumeEvents(Keyboard* keyboard, const std::vector& events) { + for (const auto& event : events) { + KeyEvent e; + event.Clone(&e); + keyboard->ConsumeEvent(std::move(e)); + } + } + + // Converts a pressed key to usage value. + uint32_t ToUsage(Key key) { return static_cast(key) & 0xFFFF; } + + private: + zx_time_t timestamp_ = 0; +}; + +// This test checks that if a caps lock has been pressed when we didn't have +// focus, the effect of caps lock remains. Only this first test case is +// commented to explain how the test case works. +TEST_F(KeyboardTest, CapsLockSync) { + // Place the key events since the beginning of time into `keys`. + std::vector keys; + keys.emplace_back(NewKeyEvent(KeyEventType::SYNC, Key::CAPS_LOCK)); + + // Replay them on the keyboard. + Keyboard keyboard; + ConsumeEvents(&keyboard, keys); + + // Verify the state of the keyboard's public API: + // - check that the key sync had no code point (it was a caps lock press). + // - check that the registered usage was that of caps lock. + // - check that the net effect is that the caps lock modifier is locked + // active. + EXPECT_EQ(0u, keyboard.LastCodePoint()); + EXPECT_EQ(ToUsage(Key::CAPS_LOCK), keyboard.LastHIDUsage()); + EXPECT_EQ(kModifierCapsLock, keyboard.Modifiers()); +} + +TEST_F(KeyboardTest, CapsLockPress) { + std::vector keys; + keys.emplace_back(NewKeyEvent(KeyEventType::PRESSED, Key::CAPS_LOCK)); + + Keyboard keyboard; + ConsumeEvents(&keyboard, keys); + + EXPECT_EQ(0u, keyboard.LastCodePoint()); + EXPECT_EQ(ToUsage(Key::CAPS_LOCK), keyboard.LastHIDUsage()); + EXPECT_EQ(kModifierCapsLock, keyboard.Modifiers()); +} + +TEST_F(KeyboardTest, CapsLockPressRelease) { + std::vector keys; + keys.emplace_back(NewKeyEvent(KeyEventType::PRESSED, Key::CAPS_LOCK)); + keys.emplace_back(NewKeyEvent(KeyEventType::RELEASED, Key::CAPS_LOCK)); + + Keyboard keyboard; + ConsumeEvents(&keyboard, keys); + + EXPECT_EQ(0u, keyboard.LastCodePoint()); + EXPECT_EQ(ToUsage(Key::CAPS_LOCK), keyboard.LastHIDUsage()); + EXPECT_EQ(kModifierCapsLock, keyboard.Modifiers()); +} + +TEST_F(KeyboardTest, ShiftA) { + std::vector keys; + keys.emplace_back(NewKeyEvent(KeyEventType::PRESSED, Key::LEFT_SHIFT)); + keys.emplace_back(NewKeyEvent(KeyEventType::PRESSED, Key::A)); + + Keyboard keyboard; + ConsumeEvents(&keyboard, keys); + + EXPECT_EQ(static_cast('A'), keyboard.LastCodePoint()); + EXPECT_EQ(ToUsage(Key::A), keyboard.LastHIDUsage()); + EXPECT_EQ(kModifierLeftShift, keyboard.Modifiers()); +} + +TEST_F(KeyboardTest, ShiftAWithRelease) { + std::vector keys; + keys.emplace_back(NewKeyEvent(KeyEventType::PRESSED, Key::LEFT_SHIFT)); + keys.emplace_back(NewKeyEvent(KeyEventType::PRESSED, Key::A)); + keys.emplace_back(NewKeyEvent(KeyEventType::RELEASED, Key::A)); + + Keyboard keyboard; + ConsumeEvents(&keyboard, keys); + + EXPECT_EQ(static_cast('A'), keyboard.LastCodePoint()); + EXPECT_EQ(ToUsage(Key::A), keyboard.LastHIDUsage()); + EXPECT_EQ(kModifierLeftShift, keyboard.Modifiers()); +} + +TEST_F(KeyboardTest, ShiftAWithReleaseShift) { + std::vector keys; + keys.emplace_back(NewKeyEvent(KeyEventType::PRESSED, Key::LEFT_SHIFT)); + keys.emplace_back(NewKeyEvent(KeyEventType::PRESSED, Key::A)); + keys.emplace_back(NewKeyEvent(KeyEventType::RELEASED, Key::LEFT_SHIFT)); + keys.emplace_back(NewKeyEvent(KeyEventType::RELEASED, Key::A)); + + Keyboard keyboard; + ConsumeEvents(&keyboard, keys); + + EXPECT_EQ(static_cast('a'), keyboard.LastCodePoint()); + EXPECT_EQ(ToUsage(Key::A), keyboard.LastHIDUsage()); + EXPECT_EQ(kModifierNone, keyboard.Modifiers()); +} + +TEST_F(KeyboardTest, LowcaseA) { + std::vector keys; + keys.emplace_back(NewKeyEvent(KeyEventType::PRESSED, Key::A)); + keys.emplace_back(NewKeyEvent(KeyEventType::RELEASED, Key::A)); + + Keyboard keyboard; + ConsumeEvents(&keyboard, keys); + + EXPECT_EQ(static_cast('a'), keyboard.LastCodePoint()); + EXPECT_EQ(ToUsage(Key::A), keyboard.LastHIDUsage()); + EXPECT_EQ(kModifierNone, keyboard.Modifiers()); +} + +} // namespace +} // namespace flutter_runner diff --git a/engine/src/flutter/shell/platform/fuchsia/flutter/platform_view.cc b/engine/src/flutter/shell/platform/fuchsia/flutter/platform_view.cc index 0d379b39575..d18c8c1ff5b 100644 --- a/engine/src/flutter/shell/platform/fuchsia/flutter/platform_view.cc +++ b/engine/src/flutter/shell/platform/fuchsia/flutter/platform_view.cc @@ -40,13 +40,13 @@ template void SetInterfaceErrorHandler(fidl::InterfacePtr& interface, std::string name) { interface.set_error_handler([name](zx_status_t status) { - FML_LOG(ERROR) << "Interface error on: " << name; + FML_LOG(ERROR) << "Interface error on: " << name << "status: " << status; }); } template void SetInterfaceErrorHandler(fidl::Binding& binding, std::string name) { binding.set_error_handler([name](zx_status_t status) { - FML_LOG(ERROR) << "Interface error on: " << name; + FML_LOG(ERROR) << "Interface error on: " << name << ", status: " << status; }); } @@ -61,6 +61,8 @@ PlatformView::PlatformView( fidl::InterfaceRequest session_listener_request, fidl::InterfaceHandle focuser, + fidl::InterfaceRequest + keyboard_listener_request, fit::closure session_listener_error_callback, OnEnableWireframe wireframe_enabled_callback, OnCreateView on_create_view_callback, @@ -85,13 +87,16 @@ PlatformView::PlatformView( external_view_embedder_(external_view_embedder), ime_client_(this), vsync_offset_(std::move(vsync_offset)), - vsync_event_handle_(vsync_event_handle) { + vsync_event_handle_(vsync_event_handle), + keyboard_listener_binding_(this, std::move(keyboard_listener_request)) { // Register all error handlers. SetInterfaceErrorHandler(session_listener_binding_, "SessionListener"); SetInterfaceErrorHandler(ime_, "Input Method Editor"); SetInterfaceErrorHandler(text_sync_service_, "Text Sync Service"); SetInterfaceErrorHandler(parent_environment_service_provider_, "Parent Environment Service Provider"); + SetInterfaceErrorHandler(keyboard_listener_binding_, + "KeyboardListener Service"); // Access the IME service. parent_environment_service_provider_ = parent_environment_service_provider_handle.Bind(); @@ -174,12 +179,6 @@ void PlatformView::DidUpdateState( ); last_text_state_ = std::make_unique(state); - - // Handle keyboard input events for HID keys only. - // TODO(SCN-1189): Are we done here? - if (input_event && input_event->keyboard().hid_usage != 0) { - OnHandleKeyboardEvent(input_event->keyboard()); - } } // |fuchsia::ui::input::InputMethodEditorClient| @@ -308,7 +307,9 @@ void PlatformView::OnScenicEvent( break; } case fuchsia::ui::input::InputEvent::Tag::kKeyboard: { - OnHandleKeyboardEvent(event.input().keyboard()); + // All devices should receive key events via input3.KeyboardListener + // instead. + FML_LOG(WARNING) << "Keyboard event from Scenic: ignored"; break; } case fuchsia::ui::input::InputEvent::Tag::Invalid: { @@ -501,31 +502,40 @@ bool PlatformView::OnHandlePointerEvent( return true; } -bool PlatformView::OnHandleKeyboardEvent( - const fuchsia::ui::input::KeyboardEvent& keyboard) { +// |fuchsia::ui:input3::KeyboardListener| +void PlatformView::OnKeyEvent( + fuchsia::ui::input3::KeyEvent key_event, + fuchsia::ui::input3::KeyboardListener::OnKeyEventCallback callback) { const char* type = nullptr; - if (keyboard.phase == fuchsia::ui::input::KeyboardEventPhase::PRESSED) { - type = "keydown"; - } else if (keyboard.phase == fuchsia::ui::input::KeyboardEventPhase::REPEAT) { - type = "keydown"; // TODO change this to keyrepeat - } else if (keyboard.phase == - fuchsia::ui::input::KeyboardEventPhase::RELEASED) { - type = "keyup"; + switch (key_event.type()) { + case fuchsia::ui::input3::KeyEventType::PRESSED: + type = "keydown"; + break; + case fuchsia::ui::input3::KeyEventType::RELEASED: + type = "keyup"; + break; + case fuchsia::ui::input3::KeyEventType::SYNC: + // What, if anything, should happen here? + case fuchsia::ui::input3::KeyEventType::CANCEL: + // What, if anything, should happen here? + default: + break; } - if (type == nullptr) { FML_DLOG(ERROR) << "Unknown key event phase."; - return false; + callback(fuchsia::ui::input3::KeyEventStatus::NOT_HANDLED); + return; } + keyboard_.ConsumeEvent(std::move(key_event)); rapidjson::Document document; auto& allocator = document.GetAllocator(); document.SetObject(); document.AddMember("type", rapidjson::Value(type, strlen(type)), allocator); document.AddMember("keymap", rapidjson::Value("fuchsia"), allocator); - document.AddMember("hidUsage", keyboard.hid_usage, allocator); - document.AddMember("codePoint", keyboard.code_point, allocator); - document.AddMember("modifiers", keyboard.modifiers, allocator); + document.AddMember("hidUsage", keyboard_.LastHIDUsage(), allocator); + document.AddMember("codePoint", keyboard_.LastCodePoint(), allocator); + document.AddMember("modifiers", keyboard_.Modifiers(), allocator); rapidjson::StringBuffer buffer; rapidjson::Writer writer(buffer); document.Accept(writer); @@ -536,8 +546,7 @@ bool PlatformView::OnHandleKeyboardEvent( std::vector(data, data + buffer.GetSize()), // data nullptr) // response ); - - return true; + callback(fuchsia::ui::input3::KeyEventStatus::HANDLED); } bool PlatformView::OnHandleFocusEvent( diff --git a/engine/src/flutter/shell/platform/fuchsia/flutter/platform_view.h b/engine/src/flutter/shell/platform/fuchsia/flutter/platform_view.h index d847d29540c..974051b0abf 100644 --- a/engine/src/flutter/shell/platform/fuchsia/flutter/platform_view.h +++ b/engine/src/flutter/shell/platform/fuchsia/flutter/platform_view.h @@ -6,6 +6,7 @@ #define FLUTTER_SHELL_PLATFORM_FUCHSIA_PLATFORM_VIEW_H_ #include +#include #include #include #include @@ -19,6 +20,7 @@ #include "flutter/fml/time/time_delta.h" #include "flutter/shell/common/platform_view.h" #include "flutter/shell/platform/fuchsia/flutter/fuchsia_external_view_embedder.h" +#include "flutter/shell/platform/fuchsia/flutter/keyboard.h" #include "accessibility_bridge.h" @@ -45,6 +47,7 @@ using OnCreateSurface = fit::function()>; class PlatformView final : public flutter::PlatformView, public AccessibilityBridge::Delegate, private fuchsia::ui::scenic::SessionListener, + private fuchsia::ui::input3::KeyboardListener, private fuchsia::ui::input::InputMethodEditorClient { public: PlatformView(flutter::PlatformView::Delegate& delegate, @@ -57,6 +60,8 @@ class PlatformView final : public flutter::PlatformView, fidl::InterfaceRequest session_listener_request, fidl::InterfaceHandle focuser, + fidl::InterfaceRequest + keyboard_listener, fit::closure on_session_listener_error_callback, OnEnableWireframe wireframe_enabled_callback, OnCreateView on_create_view_callback, @@ -87,6 +92,12 @@ class PlatformView final : public flutter::PlatformView, private: void RegisterPlatformMessageHandlers(); + // |fuchsia.ui.input3.KeyboardListener| + // Called by the embedder every time there is a key event to process. + void OnKeyEvent(fuchsia::ui::input3::KeyEvent key_event, + fuchsia::ui::input3::KeyboardListener::OnKeyEventCallback + callback) override; + // |fuchsia::ui::input::InputMethodEditorClient| void DidUpdateState( fuchsia::ui::input::TextInputState state, @@ -105,8 +116,6 @@ class PlatformView final : public flutter::PlatformView, bool OnHandlePointerEvent(const fuchsia::ui::input::PointerEvent& pointer); - bool OnHandleKeyboardEvent(const fuchsia::ui::input::KeyboardEvent& keyboard); - bool OnHandleFocusEvent(const fuchsia::ui::input::FocusEvent& focus); // Gets a new input method editor from the input connection. Run when both @@ -204,6 +213,13 @@ class PlatformView final : public flutter::PlatformView, fml::TimeDelta vsync_offset_; zx_handle_t vsync_event_handle_ = 0; + // The registered binding for serving the keyboard listener server endpoint. + fidl::Binding + keyboard_listener_binding_; + + // The keyboard translation for fuchsia.ui.input3.KeyEvent. + Keyboard keyboard_; + FML_DISALLOW_COPY_AND_ASSIGN(PlatformView); }; diff --git a/engine/src/flutter/shell/platform/fuchsia/flutter/platform_view_unittest.cc b/engine/src/flutter/shell/platform/fuchsia/flutter/platform_view_unittest.cc index 7fabcbd11d1..628eefc7d9b 100644 --- a/engine/src/flutter/shell/platform/fuchsia/flutter/platform_view_unittest.cc +++ b/engine/src/flutter/shell/platform/fuchsia/flutter/platform_view_unittest.cc @@ -162,6 +162,129 @@ class MockResponse : public flutter::PlatformMessageResponse { MOCK_METHOD0(CompleteEmpty, void()); }; +// Used to construct partial instances of PlatformView for testing. The +// PlatformView constructor has many parameters, not all of which need to +// be filled out for each test. The builder allows you to initialize only +// those that matter to your specific test. Not all builder methods are +// provided: if you find some that are missing, feel free to add them. +class PlatformViewBuilder { + public: + PlatformViewBuilder(flutter::PlatformView::Delegate& delegate, + flutter::TaskRunners task_runners, + std::shared_ptr runner_services) + : delegate_(delegate), + debug_label_("test_platform_view"), + view_ref_(fuchsia::ui::views::ViewRef()), + task_runners_(task_runners), + runner_services_(runner_services) {} + + // Add builder methods as required. + + PlatformViewBuilder& SetServiceProvider( + fidl::InterfaceHandle service_provider) { + parent_environment_service_provider_ = std::move(service_provider); + return *this; + } + + PlatformViewBuilder& SetFocuser( + fidl::InterfaceHandle focuser) { + focuser_ = std::move(focuser); + return *this; + } + + PlatformViewBuilder& SetDestroyViewCallback(OnDestroyView callback) { + on_destroy_view_callback_ = std::move(callback); + return *this; + } + + PlatformViewBuilder& SetUpdateViewCallback(OnUpdateView callback) { + on_update_view_callback_ = std::move(callback); + return *this; + } + + PlatformViewBuilder& SetEnableWireframeCallback(OnEnableWireframe callback) { + wireframe_enabled_callback_ = std::move(callback); + return *this; + } + + PlatformViewBuilder& SetCreateViewCallback(OnCreateView callback) { + on_create_view_callback_ = std::move(callback); + return *this; + } + + PlatformViewBuilder& SetSessionListenerRequest( + fidl::InterfaceRequest request) { + session_listener_request_ = std::move(request); + return *this; + } + + PlatformViewBuilder& SetCreateSurfaceCallback(OnCreateSurface callback) { + on_create_surface_callback_ = std::move(callback); + return *this; + } + + PlatformViewBuilder& SetViewEmbedder( + std::shared_ptr embedder) { + view_embedder_ = embedder; + return *this; + } + + PlatformViewBuilder& SetKeyboardListener( + fidl::InterfaceRequest listener) { + keyboard_listener_ = std::move(listener); + return *this; + } + + // Once Build is called, the instance is no longer usable. + PlatformView Build() { + EXPECT_EQ(false, built_) + << "Build() was already called, this buider is good for one use only."; + built_ = true; + return PlatformView(delegate_, debug_label_, std::move(view_ref_), + task_runners_, runner_services_, + std::move(parent_environment_service_provider_), + std::move(session_listener_request_), + std::move(focuser_), std::move(keyboard_listener_), + std::move(on_session_listener_error_callback_), + std::move(wireframe_enabled_callback_), + std::move(on_create_view_callback_), + std::move(on_update_view_callback_), + std::move(on_destroy_view_callback_), + std::move(on_create_surface_callback_), view_embedder_, + std::move(vsync_offset_), vsync_event_handle_); + } + + private: + PlatformViewBuilder() = delete; + + bool built_{false}; + + // Required elements. Make sure to initialize them. + flutter::PlatformView::Delegate& delegate_; + std::string debug_label_; + fuchsia::ui::views::ViewRef view_ref_; + flutter::TaskRunners task_runners_; + std::shared_ptr runner_services_{nullptr}; + fidl::InterfaceHandle + parent_environment_service_provider_{nullptr}; + + // Optional elements. + fidl::InterfaceRequest + session_listener_request_{nullptr}; + fidl::InterfaceHandle focuser_{nullptr}; + fidl::InterfaceRequest + keyboard_listener_{nullptr}; + fit::closure on_session_listener_error_callback_{nullptr}; + OnEnableWireframe wireframe_enabled_callback_{nullptr}; + OnCreateView on_create_view_callback_{nullptr}; + OnUpdateView on_update_view_callback_{nullptr}; + OnDestroyView on_destroy_view_callback_{nullptr}; + OnCreateSurface on_create_surface_callback_{nullptr}; + std::shared_ptr view_embedder_{nullptr}; + fml::TimeDelta vsync_offset_{fml::TimeDelta::Zero()}; + zx_handle_t vsync_event_handle_{ZX_HANDLE_INVALID}; +}; + } // namespace class PlatformViewTests : public ::testing::Test { @@ -175,9 +298,25 @@ class PlatformViewTests : public ::testing::Test { loop_.ResetQuit(); } + fuchsia::ui::input3::KeyEvent MakeEvent( + fuchsia::ui::input3::KeyEventType event_type, + std::optional modifiers, + fuchsia::input::Key key) { + fuchsia::ui::input3::KeyEvent event; + event.set_timestamp(++event_timestamp_); + event.set_type(event_type); + if (modifiers.has_value()) { + event.set_modifiers(modifiers.value()); + } + event.set_key(key); + return event; + } + private: async::Loop loop_; + uint64_t event_timestamp_{42}; + FML_DISALLOW_COPY_AND_ASSIGN(PlatformViewTests); }; @@ -206,25 +345,12 @@ TEST_F(PlatformViewTests, CreateSurfaceTest) { "PlatformViewTest", view_embedder, gr_context.get()); }; - auto platform_view = flutter_runner::PlatformView( - delegate, // delegate - "test_platform_view", // label - fuchsia::ui::views::ViewRef(), // view_ref - std::move(task_runners), // task_runners - services_provider.service_directory(), // runner_services - nullptr, // parent_environment_service_provider_handle - nullptr, // session_listener_request - nullptr, // focuser, - nullptr, // on_session_listener_error_callback - nullptr, // on_enable_wireframe_callback, - nullptr, // on_create_view_callback, - nullptr, // on_update_view_callback, - nullptr, // on_destroy_view_callback, - CreateSurfaceCallback, // on_create_surface_callback, - view_embedder, // external_view_embedder, - fml::TimeDelta::Zero(), // vsync_offset - ZX_HANDLE_INVALID // vsync_event_handle - ); + flutter_runner::PlatformView platform_view = + PlatformViewBuilder(delegate, std::move(task_runners), + services_provider.service_directory()) + .SetCreateSurfaceCallback(CreateSurfaceCallback) + .SetViewEmbedder(view_embedder) + .Build(); platform_view.NotifyCreated(); RunLoopUntilIdle(); @@ -251,25 +377,11 @@ TEST_F(PlatformViewTests, SetViewportMetrics) { sys::testing::ServiceDirectoryProvider services_provider(dispatcher()); flutter::TaskRunners task_runners("test_runners", nullptr, nullptr, nullptr, nullptr); - flutter_runner::PlatformView platform_view( - delegate, // delegate - "test_platform_view", // label - fuchsia::ui::views::ViewRef(), // view_ref - std::move(task_runners), // task_runners - services_provider.service_directory(), // runner_services - nullptr, // parent_environment_service_provider_handle - session_listener.NewRequest(), // session_listener_request - nullptr, // focuser, - nullptr, // on_session_listener_error_callback - nullptr, // on_enable_wireframe_callback, - nullptr, // on_create_view_callback, - nullptr, // on_update_view_callback, - nullptr, // on_destroy_view_callback, - nullptr, // on_create_surface_callback, - nullptr, // external_view_embedder, - fml::TimeDelta::Zero(), // vsync_offset - ZX_HANDLE_INVALID // vsync_event_handle - ); + flutter_runner::PlatformView platform_view = + PlatformViewBuilder(delegate, std::move(task_runners), + services_provider.service_directory()) + .SetSessionListenerRequest(session_listener.NewRequest()) + .Build(); RunLoopUntilIdle(); EXPECT_EQ(delegate.metrics(), flutter::ViewportMetrics()); @@ -384,25 +496,13 @@ TEST_F(PlatformViewTests, ChangesAccessibilitySettings) { EXPECT_FALSE(delegate.semantics_enabled()); EXPECT_EQ(delegate.semantics_features(), 0); - auto platform_view = flutter_runner::PlatformView( - delegate, // delegate - "test_platform_view", // label - fuchsia::ui::views::ViewRef(), // view_ref - std::move(task_runners), // task_runners - services_provider.service_directory(), // runner_services - nullptr, // parent_environment_service_provider_handle - nullptr, // session_listener_request - nullptr, // focuser, - nullptr, // on_session_listener_error_callback - nullptr, // on_enable_wireframe_callback, - nullptr, // on_create_view_callback, - nullptr, // on_update_view_callback, - nullptr, // on_destroy_view_callback, - nullptr, // on_create_surface_callback, - nullptr, // external_view_embedder, - fml::TimeDelta::Zero(), // vsync_offset - ZX_HANDLE_INVALID // vsync_event_handle - ); + flutter_runner::PlatformView platform_view = + PlatformViewBuilder( + delegate, // delegate + std::move(task_runners), // task_runners + services_provider.service_directory() // runner_services + ) + .Build(); RunLoopUntilIdle(); @@ -435,25 +535,11 @@ TEST_F(PlatformViewTests, EnableWireframeTest) { wireframe_enabled = should_enable; }; - auto platform_view = flutter_runner::PlatformView( - delegate, // delegate - "test_platform_view", // label - fuchsia::ui::views::ViewRef(), // view_ref - std::move(task_runners), // task_runners - services_provider.service_directory(), // runner_services - nullptr, // parent_environment_service_provider_handle - nullptr, // session_listener_request - nullptr, // focuser, - nullptr, // on_session_listener_error_callback - EnableWireframeCallback, // on_enable_wireframe_callback, - nullptr, // on_create_view_callback, - nullptr, // on_update_view_callback, - nullptr, // on_destroy_view_callback, - nullptr, // on_create_surface_callback, - nullptr, // external_view_embedder, - fml::TimeDelta::Zero(), // vsync_offset - ZX_HANDLE_INVALID // vsync_event_handle - ); + flutter_runner::PlatformView platform_view = + PlatformViewBuilder(delegate, std::move(task_runners), + services_provider.service_directory()) + .SetEnableWireframeCallback(EnableWireframeCallback) + .Build(); // Cast platform_view to its base view so we can have access to the public // "HandlePlatformMessage" function. @@ -497,25 +583,11 @@ TEST_F(PlatformViewTests, CreateViewTest) { int64_t view_id, bool hit_testable, bool focusable) { create_view_called = true; }; - auto platform_view = flutter_runner::PlatformView( - delegate, // delegate - "test_platform_view", // label - fuchsia::ui::views::ViewRef(), // view_ref - std::move(task_runners), // task_runners - services_provider.service_directory(), // runner_services - nullptr, // parent_environment_service_provider_handle - nullptr, // session_listener_request - nullptr, // focuser, - nullptr, // on_session_listener_error_callback - nullptr, // on_enable_wireframe_callback, - CreateViewCallback, // on_create_view_callback, - nullptr, // on_update_view_callback, - nullptr, // on_destroy_view_callback, - nullptr, // on_create_surface_callback, - nullptr, // external_view_embedder, - fml::TimeDelta::Zero(), // vsync_offset - ZX_HANDLE_INVALID // vsync_event_handle - ); + flutter_runner::PlatformView platform_view = + PlatformViewBuilder(delegate, std::move(task_runners), + services_provider.service_directory()) + .SetCreateViewCallback(CreateViewCallback) + .Build(); // Cast platform_view to its base view so we can have access to the public // "HandlePlatformMessage" function. @@ -561,25 +633,11 @@ TEST_F(PlatformViewTests, UpdateViewTest) { int64_t view_id, bool hit_testable, bool focusable) { update_view_called = true; }; - auto platform_view = flutter_runner::PlatformView( - delegate, // delegate - "test_platform_view", // label - fuchsia::ui::views::ViewRef(), // view_ref - std::move(task_runners), // task_runners - services_provider.service_directory(), // runner_services - nullptr, // parent_environment_service_provider_handle - nullptr, // session_listener_request - nullptr, // focuser, - nullptr, // on_session_listener_error_callback - nullptr, // on_enable_wireframe_callback, - nullptr, // on_create_view_callback, - UpdateViewCallback, // on_update_view_callback, - nullptr, // on_destroy_view_callback, - nullptr, // on_create_surface_callback, - nullptr, // external_view_embedder, - fml::TimeDelta::Zero(), // vsync_offset - ZX_HANDLE_INVALID // vsync_event_handle - ); + flutter_runner::PlatformView platform_view = + PlatformViewBuilder(delegate, std::move(task_runners), + services_provider.service_directory()) + .SetUpdateViewCallback(UpdateViewCallback) + .Build(); // Cast platform_view to its base view so we can have access to the public // "HandlePlatformMessage" function. @@ -625,25 +683,11 @@ TEST_F(PlatformViewTests, DestroyViewTest) { destroy_view_called = true; }; - auto platform_view = flutter_runner::PlatformView( - delegate, // delegate - "test_platform_view", // label - fuchsia::ui::views::ViewRef(), // view_ref - std::move(task_runners), // task_runners - services_provider.service_directory(), // runner_services - nullptr, // parent_environment_service_provider_handle - nullptr, // session_listener_request - nullptr, // focuser, - nullptr, // on_session_listener_error_callback - nullptr, // on_enable_wireframe_callback, - nullptr, // on_create_view_callback, - nullptr, // on_update_view_callback, - DestroyViewCallback, // on_destroy_view_callback, - nullptr, // on_create_surface_callback, - nullptr, // external_view_embedder, - fml::TimeDelta::Zero(), // vsync_offset - ZX_HANDLE_INVALID // vsync_event_handle - ); + flutter_runner::PlatformView platform_view = + PlatformViewBuilder(delegate, std::move(task_runners), + services_provider.service_directory()) + .SetDestroyViewCallback(DestroyViewCallback) + .Build(); // Cast platform_view to its base view so we can have access to the public // "HandlePlatformMessage" function. @@ -686,25 +730,12 @@ TEST_F(PlatformViewTests, ViewEventsTest) { flutter_runner::CreateFMLTaskRunner(async_get_default_dispatcher()), nullptr); - auto platform_view = flutter_runner::PlatformView( - delegate, // delegate - "test_platform_view", // label - fuchsia::ui::views::ViewRef(), // view_ref - std::move(task_runners), // task_runners - services_provider.service_directory(), // runner_services - nullptr, // parent_environment_service_provider_handle - session_listener.NewRequest(), // session_listener_request - nullptr, // focuser, - nullptr, // on_session_listener_error_callback - nullptr, // on_enable_wireframe_callback, - nullptr, // on_create_view_callback, - nullptr, // on_update_view_callback, - nullptr, // on_destroy_view_callback, - nullptr, // on_create_surface_callback, - nullptr, // external_view_embedder, - fml::TimeDelta::Zero(), // vsync_offset - ZX_HANDLE_INVALID // vsync_event_handle - ); + flutter_runner::PlatformView platform_view = + PlatformViewBuilder(delegate, std::move(task_runners), + services_provider.service_directory()) + .SetSessionListenerRequest(session_listener.NewRequest()) + .Build(); + RunLoopUntilIdle(); // ViewConnected event. @@ -769,25 +800,11 @@ TEST_F(PlatformViewTests, RequestFocusTest) { fidl::BindingSet focuser_bindings; auto focuser_handle = focuser_bindings.AddBinding(&mock_focuser); - auto platform_view = flutter_runner::PlatformView( - delegate, // delegate - "test_platform_view", // label - fuchsia::ui::views::ViewRef(), // view_ref - std::move(task_runners), // task_runners - services_provider.service_directory(), // runner_services - nullptr, // parent_environment_service_provider_handle - nullptr, // session_listener_request - std::move(focuser_handle), // focuser, - nullptr, // on_session_listener_error_callback - nullptr, // on_enable_wireframe_callback, - nullptr, // on_create_view_callback, - nullptr, // on_update_view_callback, - nullptr, // on_destroy_view_callback, - nullptr, // on_create_surface_callback, - nullptr, // external_view_embedder, - fml::TimeDelta::Zero(), // vsync_offset - ZX_HANDLE_INVALID // vsync_event_handle - ); + flutter_runner::PlatformView platform_view = + PlatformViewBuilder(delegate, std::move(task_runners), + services_provider.service_directory()) + .SetFocuser(std::move(focuser_handle)) + .Build(); // Cast platform_view to its base view so we can have access to the public // "HandlePlatformMessage" function. @@ -846,25 +863,11 @@ TEST_F(PlatformViewTests, RequestFocusFailTest) { fidl::BindingSet focuser_bindings; auto focuser_handle = focuser_bindings.AddBinding(&mock_focuser); - auto platform_view = flutter_runner::PlatformView( - delegate, // delegate - "test_platform_view", // label - fuchsia::ui::views::ViewRef(), // view_ref - std::move(task_runners), // task_runners - services_provider.service_directory(), // runner_services - nullptr, // parent_environment_service_provider_handle - nullptr, // session_listener_request - std::move(focuser_handle), // focuser, - nullptr, // on_session_listener_error_callback - nullptr, // on_enable_wireframe_callback, - nullptr, // on_create_view_callback, - nullptr, // on_update_view_callback, - nullptr, // on_destroy_view_callback, - nullptr, // on_create_surface_callback, - nullptr, // external_view_embedder, - fml::TimeDelta::Zero(), // vsync_offset - ZX_HANDLE_INVALID // vsync_event_handle - ); + flutter_runner::PlatformView platform_view = + PlatformViewBuilder(delegate, std::move(task_runners), + services_provider.service_directory()) + .SetFocuser(std::move(focuser_handle)) + .Build(); // Cast platform_view to its base view so we can have access to the public // "HandlePlatformMessage" function. @@ -916,4 +919,88 @@ TEST_F(PlatformViewTests, RequestFocusFailTest) { EXPECT_EQ(out.str(), result); } +struct EventFlow { + fuchsia::ui::input3::KeyEvent event; + fuchsia::ui::input3::KeyEventStatus expected_key_event_status; + std::string expected_platform_message; +}; + +// Makes sure that OnKeyEvent is dispatched as a platform message. +TEST_F(PlatformViewTests, OnKeyEvent) { + sys::testing::ServiceDirectoryProvider services_provider(dispatcher()); + MockPlatformViewDelegate delegate; + flutter::TaskRunners task_runners = + flutter::TaskRunners("test_runners", nullptr, nullptr, nullptr, nullptr); + + fidl::InterfacePtr keyboard_listener; + + flutter_runner::PlatformView platform_view = + PlatformViewBuilder(delegate, std::move(task_runners), + services_provider.service_directory()) + .SetKeyboardListener(keyboard_listener.NewRequest(dispatcher())) + .Build(); + + using fuchsia::input::Key; + using fuchsia::ui::input3::KeyEvent; + using fuchsia::ui::input3::KeyEventStatus; + using fuchsia::ui::input3::KeyEventType; + using fuchsia::ui::input3::Modifiers; + + std::vector events; + // Press A. Get 'a'. + events.emplace_back(EventFlow{ + MakeEvent(KeyEventType::PRESSED, std::nullopt, Key::A), + KeyEventStatus::HANDLED, + R"({"type":"keydown","keymap":"fuchsia","hidUsage":4,"codePoint":97,"modifiers":0})", + }); + // Release A. Get 'a' release. + events.emplace_back(EventFlow{ + MakeEvent(KeyEventType::RELEASED, std::nullopt, Key::A), + KeyEventStatus::HANDLED, + R"({"type":"keyup","keymap":"fuchsia","hidUsage":4,"codePoint":97,"modifiers":0})", + }); + // Press CAPS_LOCK. Modifier now active. + events.emplace_back(EventFlow{ + MakeEvent(KeyEventType::PRESSED, Modifiers::CAPS_LOCK, Key::CAPS_LOCK), + KeyEventStatus::HANDLED, + R"({"type":"keydown","keymap":"fuchsia","hidUsage":57,"codePoint":0,"modifiers":1})", + }); + // Pres A. Get 'A'. + events.emplace_back(EventFlow{ + MakeEvent(KeyEventType::PRESSED, std::nullopt, Key::A), + KeyEventStatus::HANDLED, + R"({"type":"keydown","keymap":"fuchsia","hidUsage":4,"codePoint":65,"modifiers":1})", + }); + // Release CAPS_LOCK. + events.emplace_back(EventFlow{ + MakeEvent(KeyEventType::RELEASED, Modifiers::CAPS_LOCK, Key::CAPS_LOCK), + KeyEventStatus::HANDLED, + R"({"type":"keyup","keymap":"fuchsia","hidUsage":57,"codePoint":0,"modifiers":1})", + }); + // Press A again. This time get 'A'. + // CAPS_LOCK is latched active even if it was just released. + events.emplace_back(EventFlow{ + MakeEvent(KeyEventType::PRESSED, std::nullopt, Key::A), + KeyEventStatus::HANDLED, + R"({"type":"keydown","keymap":"fuchsia","hidUsage":4,"codePoint":65,"modifiers":1})", + }); + + for (const auto& event : events) { + KeyEvent e; + event.event.Clone(&e); + fuchsia::ui::input3::KeyEventStatus key_event_status; + keyboard_listener->OnKeyEvent( + std::move(e), + [&key_event_status](fuchsia::ui::input3::KeyEventStatus status) { + key_event_status = status; + }); + RunLoopUntilIdle(); + const std::vector data = delegate.message()->data(); + const std::string message = std::string(data.begin(), data.end()); + + EXPECT_EQ(event.expected_platform_message, message); + EXPECT_EQ(key_event_status, event.expected_key_event_status); + } +} + } // namespace flutter_runner::testing diff --git a/engine/src/flutter/testing/fuchsia/meta/fuchsia_test.cmx b/engine/src/flutter/testing/fuchsia/meta/fuchsia_test.cmx index 894e70198e1..89af09f157d 100644 --- a/engine/src/flutter/testing/fuchsia/meta/fuchsia_test.cmx +++ b/engine/src/flutter/testing/fuchsia/meta/fuchsia_test.cmx @@ -12,15 +12,15 @@ "services": [ "fuchsia.accessibility.semantics.SemanticsManager", "fuchsia.deprecatedtimezone.Timezone", + "fuchsia.fonts.Provider", "fuchsia.intl.PropertyProvider", "fuchsia.logger.LogSink", "fuchsia.process.Launcher", "fuchsia.settings.Intl", "fuchsia.sysmem.Allocator", "fuchsia.tracing.provider.Registry", - "fuchsia.vulkan.loader.Loader", - "fuchsia.intl.PropertyProvider", - "fuchsia.fonts.Provider" + "fuchsia.ui.input3.Keyboard", + "fuchsia.vulkan.loader.Loader" ] } }