From e4a2f48892178962b7beeed8ff4aaadf634eb8aa Mon Sep 17 00:00:00 2001 From: Filip Filmar Date: Thu, 28 Jul 2022 15:33:11 -0500 Subject: [PATCH] [fuchsia] Support multi-line text editing (flutter/engine#34410) Multi-line text editing and actions other than DONE were never implemented in Flutter on Fuchsia. This change implements the feature, by plumbing the desired action through to Fuchsia proper, and back, as Fuchsia's text editing API expects. Tested: the new behavior was verified by Fuchsia-side integration tests. Issue: https://github.com/flutter/flutter/issues/106905 --- .../ci/licenses_golden/licenses_flutter | 3 + .../shell/platform/fuchsia/flutter/BUILD.gn | 3 + .../platform/fuchsia/flutter/platform_view.cc | 307 +----------- .../platform/fuchsia/flutter/platform_view.h | 49 +- .../platform/fuchsia/flutter/text_delegate.cc | 449 ++++++++++++++++++ .../platform/fuchsia/flutter/text_delegate.h | 154 ++++++ .../flutter/text_delegate_unittests.cc | 288 +++++++++++ 7 files changed, 919 insertions(+), 334 deletions(-) create mode 100644 engine/src/flutter/shell/platform/fuchsia/flutter/text_delegate.cc create mode 100644 engine/src/flutter/shell/platform/fuchsia/flutter/text_delegate.h create mode 100644 engine/src/flutter/shell/platform/fuchsia/flutter/text_delegate_unittests.cc diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter index 0bad4d89e83..26a869ffc1c 100644 --- a/engine/src/flutter/ci/licenses_golden/licenses_flutter +++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter @@ -2134,6 +2134,9 @@ FILE: ../../../flutter/shell/platform/fuchsia/flutter/surface.h FILE: ../../../flutter/shell/platform/fuchsia/flutter/surface_producer.h FILE: ../../../flutter/shell/platform/fuchsia/flutter/task_runner_adapter.cc FILE: ../../../flutter/shell/platform/fuchsia/flutter/task_runner_adapter.h +FILE: ../../../flutter/shell/platform/fuchsia/flutter/text_delegate.cc +FILE: ../../../flutter/shell/platform/fuchsia/flutter/text_delegate.h +FILE: ../../../flutter/shell/platform/fuchsia/flutter/text_delegate_unittests.cc FILE: ../../../flutter/shell/platform/fuchsia/flutter/unique_fdio_ns.h FILE: ../../../flutter/shell/platform/fuchsia/flutter/vsync_waiter.cc FILE: ../../../flutter/shell/platform/fuchsia/flutter/vsync_waiter.h diff --git a/engine/src/flutter/shell/platform/fuchsia/flutter/BUILD.gn b/engine/src/flutter/shell/platform/fuchsia/flutter/BUILD.gn index 865b34d224c..0680f429130 100644 --- a/engine/src/flutter/shell/platform/fuchsia/flutter/BUILD.gn +++ b/engine/src/flutter/shell/platform/fuchsia/flutter/BUILD.gn @@ -106,6 +106,8 @@ template("runner_sources") { "surface_producer.h", "task_runner_adapter.cc", "task_runner_adapter.h", + "text_delegate.cc", + "text_delegate.h", "unique_fdio_ns.h", "vsync_waiter.cc", "vsync_waiter.h", @@ -487,6 +489,7 @@ if (enable_unittests) { "tests/gfx_session_connection_unittests.cc", "tests/pointer_event_utility.cc", "tests/pointer_event_utility.h", + "text_delegate_unittests.cc", "vsync_waiter_unittest.cc", ] 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 4b7690baf8a..6ae402b4ef1 100644 --- a/engine/src/flutter/shell/platform/fuchsia/flutter/platform_view.cc +++ b/engine/src/flutter/shell/platform/fuchsia/flutter/platform_view.cc @@ -25,32 +25,16 @@ #include "logging.h" #include "runtime/dart/utils/inlines.h" +#include "text_delegate.h" #include "vsync_waiter.h" namespace flutter_runner { static constexpr char kFlutterPlatformChannel[] = "flutter/platform"; -static constexpr char kTextInputChannel[] = "flutter/textinput"; -static constexpr char kKeyEventChannel[] = "flutter/keyevent"; static constexpr char kAccessibilityChannel[] = "flutter/accessibility"; static constexpr char kFlutterPlatformViewsChannel[] = "flutter/platform_views"; static constexpr char kFuchsiaShaderWarmupChannel[] = "fuchsia/shader_warmup"; -// FL(77): Terminate engine if Fuchsia system FIDL connections have error. -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 << ", status: " << status; - }); -} -template -void SetInterfaceErrorHandler(fidl::Binding& binding, std::string name) { - binding.set_error_handler([name](zx_status_t status) { - FML_LOG(ERROR) << "Binding error on: " << name << ", status: " << status; - }); -} - PlatformView::PlatformView( bool is_flatland, flutter::PlatformView::Delegate& delegate, @@ -81,10 +65,6 @@ PlatformView::PlatformView( pointer_delegate_( std::make_shared(std::move(touch_source), std::move(mouse_source))), - ime_client_(this), - text_sync_service_(ime_service.Bind()), - keyboard_listener_binding_(this), - keyboard_(keyboard.Bind()), wireframe_enabled_callback_(std::move(wireframe_enabled_callback)), on_update_view_callback_(std::move(on_update_view_callback)), on_create_surface_callback_(std::move(on_create_surface_callback)), @@ -96,19 +76,21 @@ PlatformView::PlatformView( await_vsync_for_secondary_callback_callback_( await_vsync_for_secondary_callback_callback), weak_factory_(this) { - // Register all error handlers. - SetInterfaceErrorHandler(ime_, "Input Method Editor"); - SetInterfaceErrorHandler(ime_client_, "IME Client"); - SetInterfaceErrorHandler(text_sync_service_, "Text Sync Service"); - SetInterfaceErrorHandler(keyboard_listener_binding_, "Keyboard Listener"); - SetInterfaceErrorHandler(keyboard_, "Keyboard"); - fuchsia::ui::views::ViewRef view_ref_clone; fidl::Clone(view_ref, &view_ref_clone); - // Configure keyboard listener. - keyboard_->AddListener(std::move(view_ref), - keyboard_listener_binding_.NewBinding(), [] {}); + text_delegate_ = + std::make_unique( + std::move(view_ref), std::move(ime_service), std::move(keyboard), + [weak = weak_factory_.GetWeakPtr()]( + std::unique_ptr message) { + if (!weak) { + FML_LOG(WARNING) + << "PlatformView use-after-free attempted. Ignoring."; + } + weak->delegate_.OnPlatformViewDispatchPlatformMessage( + std::move(message)); + }); // Begin watching for focus changes. focus_delegate_->WatchLoop([weak = weak_factory_.GetWeakPtr()](bool focused) { @@ -119,10 +101,10 @@ PlatformView::PlatformView( // Ensure last_text_state_ is set to make sure Flutter actually wants // an IME. - if (focused && weak->last_text_state_) { - weak->ActivateIme(); + if (focused && weak->text_delegate_->HasTextState()) { + weak->text_delegate_->ActivateIme(); } else if (!focused) { - weak->DeactivateIme(); + weak->text_delegate_->DeactivateIme(); } }); @@ -171,8 +153,8 @@ void PlatformView::RegisterPlatformMessageHandlers() { std::bind(&PlatformView::HandleFlutterPlatformChannelPlatformMessage, this, std::placeholders::_1); platform_message_handlers_[kTextInputChannel] = - std::bind(&PlatformView::HandleFlutterTextInputChannelPlatformMessage, - this, std::placeholders::_1); + std::bind(&TextDelegate::HandleFlutterTextInputChannelPlatformMessage, + text_delegate_.get(), std::placeholders::_1); platform_message_handlers_[kAccessibilityChannel] = std::bind(&PlatformView::HandleAccessibilityChannelPlatformMessage, this, std::placeholders::_1); @@ -184,84 +166,6 @@ void PlatformView::RegisterPlatformMessageHandlers() { on_shader_warmup_, std::placeholders::_1); } -// |fuchsia::ui::input::InputMethodEditorClient| -void PlatformView::DidUpdateState( - fuchsia::ui::input::TextInputState state, - std::unique_ptr input_event) { - rapidjson::Document document; - auto& allocator = document.GetAllocator(); - rapidjson::Value encoded_state(rapidjson::kObjectType); - encoded_state.AddMember("text", state.text, allocator); - encoded_state.AddMember("selectionBase", state.selection.base, allocator); - encoded_state.AddMember("selectionExtent", state.selection.extent, allocator); - switch (state.selection.affinity) { - case fuchsia::ui::input::TextAffinity::UPSTREAM: - encoded_state.AddMember("selectionAffinity", - rapidjson::Value("TextAffinity.upstream"), - allocator); - break; - case fuchsia::ui::input::TextAffinity::DOWNSTREAM: - encoded_state.AddMember("selectionAffinity", - rapidjson::Value("TextAffinity.downstream"), - allocator); - break; - } - encoded_state.AddMember("selectionIsDirectional", true, allocator); - encoded_state.AddMember("composingBase", state.composing.start, allocator); - encoded_state.AddMember("composingExtent", state.composing.end, allocator); - - rapidjson::Value args(rapidjson::kArrayType); - args.PushBack(current_text_input_client_, allocator); - args.PushBack(encoded_state, allocator); - - document.SetObject(); - document.AddMember("method", - rapidjson::Value("TextInputClient.updateEditingState"), - allocator); - document.AddMember("args", args, allocator); - - rapidjson::StringBuffer buffer; - rapidjson::Writer writer(buffer); - document.Accept(writer); - - const uint8_t* data = reinterpret_cast(buffer.GetString()); - DispatchPlatformMessage(std::make_unique( - kTextInputChannel, // channel - fml::MallocMapping::Copy(data, buffer.GetSize()), // message - nullptr) // response - ); - last_text_state_ = - std::make_unique(state); -} - -// |fuchsia::ui::input::InputMethodEditorClient| -void PlatformView::OnAction(fuchsia::ui::input::InputMethodAction action) { - rapidjson::Document document; - auto& allocator = document.GetAllocator(); - - rapidjson::Value args(rapidjson::kArrayType); - args.PushBack(current_text_input_client_, allocator); - - // Done is currently the only text input action defined by Flutter. - args.PushBack("TextInputAction.done", allocator); - - document.SetObject(); - document.AddMember( - "method", rapidjson::Value("TextInputClient.performAction"), allocator); - document.AddMember("args", args, allocator); - - rapidjson::StringBuffer buffer; - rapidjson::Writer writer(buffer); - document.Accept(writer); - - const uint8_t* data = reinterpret_cast(buffer.GetString()); - DispatchPlatformMessage(std::make_unique( - kTextInputChannel, // channel - fml::MallocMapping::Copy(data, buffer.GetSize()), // message - nullptr) // response - ); -} - static flutter::PointerData::Change GetChangeFromPointerEventPhase( fuchsia::ui::input::PointerEventPhase phase) { switch (phase) { @@ -405,77 +309,6 @@ bool PlatformView::OnHandlePointerEvent( return true; } -// |fuchsia::ui:input3::KeyboardListener| -void PlatformView::OnKeyEvent( - fuchsia::ui::input3::KeyEvent key_event, - fuchsia::ui::input3::KeyboardListener::OnKeyEventCallback callback) { - const char* type = nullptr; - 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_LOG(ERROR) << "Unknown key event phase."; - callback(fuchsia::ui::input3::KeyEventStatus::NOT_HANDLED); - return; - } - keyboard_translator_.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_translator_.LastHIDUsage(), - allocator); - document.AddMember("codePoint", keyboard_translator_.LastCodePoint(), - allocator); - document.AddMember("modifiers", keyboard_translator_.Modifiers(), allocator); - rapidjson::StringBuffer buffer; - rapidjson::Writer writer(buffer); - document.Accept(writer); - - const uint8_t* data = reinterpret_cast(buffer.GetString()); - DispatchPlatformMessage(std::make_unique( - kKeyEventChannel, // channel - fml::MallocMapping::Copy(data, buffer.GetSize()), // data - nullptr) // response - ); - callback(fuchsia::ui::input3::KeyEventStatus::HANDLED); -} - -void PlatformView::ActivateIme() { - DEBUG_CHECK(last_text_state_ != nullptr, LOG_TAG, ""); - - text_sync_service_->GetInputMethodEditor( - fuchsia::ui::input::KeyboardType::TEXT, // keyboard type - fuchsia::ui::input::InputMethodAction::DONE, // input method action - *last_text_state_, // initial state - ime_client_.NewBinding(), // client - ime_.NewRequest() // editor - ); -} - -void PlatformView::DeactivateIme() { - if (ime_) { - text_sync_service_->HideKeyboard(); - ime_ = nullptr; - } - if (ime_client_.is_bound()) { - ime_client_.Unbind(); - } -} - // |flutter::PlatformView| std::unique_ptr PlatformView::CreateVSyncWaiter() { return std::make_unique( @@ -585,110 +418,6 @@ bool PlatformView::HandleFlutterPlatformChannelPlatformMessage( return false; } -// Channel handler for kTextInputChannel -bool PlatformView::HandleFlutterTextInputChannelPlatformMessage( - std::unique_ptr message) { - FML_DCHECK(message->channel() == kTextInputChannel); - const auto& data = message->data(); - rapidjson::Document document; - document.Parse(reinterpret_cast(data.GetMapping()), - data.GetSize()); - if (document.HasParseError() || !document.IsObject()) { - return false; - } - auto root = document.GetObject(); - auto method = root.FindMember("method"); - if (method == root.MemberEnd() || !method->value.IsString()) { - return false; - } - - if (method->value == "TextInput.show") { - if (ime_) { - text_sync_service_->ShowKeyboard(); - } - } else if (method->value == "TextInput.hide") { - if (ime_) { - text_sync_service_->HideKeyboard(); - } - } else if (method->value == "TextInput.setClient") { - current_text_input_client_ = 0; - DeactivateIme(); - auto args = root.FindMember("args"); - if (args == root.MemberEnd() || !args->value.IsArray() || - args->value.Size() != 2) - return false; - const auto& configuration = args->value[1]; - if (!configuration.IsObject()) { - return false; - } - // TODO(abarth): Read the keyboard type from the configuration. - current_text_input_client_ = args->value[0].GetInt(); - - auto initial_text_input_state = fuchsia::ui::input::TextInputState{}; - initial_text_input_state.text = ""; - last_text_state_ = std::make_unique( - initial_text_input_state); - ActivateIme(); - } else if (method->value == "TextInput.setEditingState") { - if (ime_) { - auto args_it = root.FindMember("args"); - if (args_it == root.MemberEnd() || !args_it->value.IsObject()) { - return false; - } - const auto& args = args_it->value; - fuchsia::ui::input::TextInputState state; - state.text = ""; - // TODO(abarth): Deserialize state. - auto text = args.FindMember("text"); - if (text != args.MemberEnd() && text->value.IsString()) - state.text = text->value.GetString(); - auto selection_base = args.FindMember("selectionBase"); - if (selection_base != args.MemberEnd() && selection_base->value.IsInt()) - state.selection.base = selection_base->value.GetInt(); - auto selection_extent = args.FindMember("selectionExtent"); - if (selection_extent != args.MemberEnd() && - selection_extent->value.IsInt()) - state.selection.extent = selection_extent->value.GetInt(); - auto selection_affinity = args.FindMember("selectionAffinity"); - if (selection_affinity != args.MemberEnd() && - selection_affinity->value.IsString() && - selection_affinity->value == "TextAffinity.upstream") - state.selection.affinity = fuchsia::ui::input::TextAffinity::UPSTREAM; - else - state.selection.affinity = fuchsia::ui::input::TextAffinity::DOWNSTREAM; - // We ignore selectionIsDirectional because that concept doesn't exist on - // Fuchsia. - auto composing_base = args.FindMember("composingBase"); - if (composing_base != args.MemberEnd() && composing_base->value.IsInt()) - state.composing.start = composing_base->value.GetInt(); - auto composing_extent = args.FindMember("composingExtent"); - if (composing_extent != args.MemberEnd() && - composing_extent->value.IsInt()) - state.composing.end = composing_extent->value.GetInt(); - ime_->SetState(std::move(state)); - } - } else if (method->value == "TextInput.clearClient") { - current_text_input_client_ = 0; - last_text_state_ = nullptr; - DeactivateIme(); - } else if (method->value == "TextInput.setCaretRect" || - method->value == "TextInput.setEditableSizeAndTransform" || - method->value == "TextInput.setMarkedTextRect" || - method->value == "TextInput.setStyle") { - // We don't have these methods implemented and they get - // sent a lot during text input, so we create an empty case for them - // here to avoid "Unknown flutter/textinput method TextInput.*" - // log spam. - // - // TODO(fxb/101619): We should implement these. - } else { - FML_LOG(ERROR) << "Unknown " << message->channel() << " method " - << method->value.GetString(); - } - // Complete with an empty response. - return false; -} - bool PlatformView::HandleFlutterPlatformViewsChannelPlatformMessage( std::unique_ptr message) { FML_DCHECK(message->channel() == kFlutterPlatformViewsChannel); 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 41bc46b8064..7c6aa98108e 100644 --- a/engine/src/flutter/shell/platform/fuchsia/flutter/platform_view.h +++ b/engine/src/flutter/shell/platform/fuchsia/flutter/platform_view.h @@ -34,6 +34,7 @@ #include "focus_delegate.h" #include "pointer_delegate.h" #include "pointer_injector_delegate.h" +#include "text_delegate.h" namespace flutter_runner { @@ -60,9 +61,7 @@ using OnShaderWarmup = std::function&, // in HandlePlatformMessage. This communication is bidirectional. Platform // messages are notably responsible for communication related to input and // external views / windowing. -class PlatformView : public flutter::PlatformView, - private fuchsia::ui::input3::KeyboardListener, - private fuchsia::ui::input::InputMethodEditorClient { +class PlatformView : public flutter::PlatformView { public: PlatformView( bool is_flatland, @@ -99,34 +98,10 @@ class PlatformView : public flutter::PlatformView, protected: 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, - std::unique_ptr event) override; - - // |fuchsia::ui::input::InputMethodEditorClient| - void OnAction(fuchsia::ui::input::InputMethodAction action) override; - bool OnHandlePointerEvent(const fuchsia::ui::input::PointerEvent& pointer); bool OnHandleFocusEvent(const fuchsia::ui::input::FocusEvent& focus); - // Gets a new input method editor from the input connection. Run when both - // Scenic has focus and Flutter has requested input with setClient. - void ActivateIme(); - - // Detaches the input method editor connection, ending the edit session and - // closing the onscreen keyboard. Call when input is no longer desired, either - // because Scenic says we lost focus or when Flutter no longer has a text - // field focused. - void DeactivateIme(); - // |flutter::PlatformView| std::unique_ptr CreateVSyncWaiter() override; @@ -152,10 +127,6 @@ class PlatformView : public flutter::PlatformView, bool HandleFlutterPlatformChannelPlatformMessage( std::unique_ptr message); - // Channel handler for kTextInputChannel - bool HandleFlutterTextInputChannelPlatformMessage( - std::unique_ptr message); - // Channel handler for kPlatformViewsChannel. bool HandleFlutterPlatformViewsChannelPlatformMessage( std::unique_ptr message); @@ -191,20 +162,8 @@ class PlatformView : public flutter::PlatformView, std::shared_ptr pointer_delegate_; std::unique_ptr pointer_injector_delegate_; - fidl::Binding ime_client_; - fuchsia::ui::input::InputMethodEditorPtr ime_; - fuchsia::ui::input::ImeServicePtr text_sync_service_; - int current_text_input_client_ = 0; - - fidl::Binding - keyboard_listener_binding_; - fuchsia::ui::input3::KeyboardPtr keyboard_; - Keyboard keyboard_translator_; - - // last_text_state_ is the last state of the text input as reported by the IME - // or initialized by Flutter. We set it to null if Flutter doesn't want any - // input, since then there is no text input state at all. - std::unique_ptr last_text_state_; + // Text delegate is responsible for handling keyboard input and text editing. + std::unique_ptr text_delegate_; std::set down_pointers_; std::map +#include +#include +#include + +#include "flutter/fml/logging.h" +#include "flutter/fml/mapping.h" +#include "flutter/lib/ui/window/platform_message.h" +#include "flutter/shell/platform/fuchsia/flutter/keyboard.h" +#include "flutter/shell/platform/fuchsia/runtime/dart/utils/inlines.h" +#include "third_party/rapidjson/include/rapidjson/document.h" +#include "third_party/rapidjson/include/rapidjson/stringbuffer.h" +#include "third_party/rapidjson/include/rapidjson/writer.h" + +#include "logging.h" + +namespace flutter_runner { + +static constexpr char kInputActionKey[] = "inputAction"; + +// See: https://api.flutter.dev/flutter/services/TextInputAction.html +// Only the actions relevant for Fuchsia are listed here. +static constexpr char kTextInputActionDone[] = "TextInputAction.done"; +static constexpr char kTextInputActionNewline[] = "TextInputAction.newline"; +static constexpr char kTextInputActionGo[] = "TextInputAction.go"; +static constexpr char kTextInputActionNext[] = "TextInputAction.next"; +static constexpr char kTextInputActionPrevious[] = "TextInputAction.previous"; +static constexpr char kTextInputActionNone[] = "TextInputAction.none"; +static constexpr char kTextInputActionSearch[] = "TextInputAction.search"; +static constexpr char kTextInputActionSend[] = "TextInputAction.send"; +static constexpr char kTextInputActionUnspecified[] = + "TextInputAction.unspecified"; + +// Converts Flutter TextInputAction to Fuchsia action enum. +static fuchsia::ui::input::InputMethodAction IntoInputMethodAction( + const std::string action_string) { + if (action_string == kTextInputActionNewline) { + return fuchsia::ui::input::InputMethodAction::NEWLINE; + } else if (action_string == kTextInputActionDone) { + return fuchsia::ui::input::InputMethodAction::DONE; + } else if (action_string == kTextInputActionGo) { + return fuchsia::ui::input::InputMethodAction::GO; + } else if (action_string == kTextInputActionNext) { + return fuchsia::ui::input::InputMethodAction::NEXT; + } else if (action_string == kTextInputActionPrevious) { + return fuchsia::ui::input::InputMethodAction::PREVIOUS; + } else if (action_string == kTextInputActionNone) { + return fuchsia::ui::input::InputMethodAction::NONE; + } else if (action_string == kTextInputActionSearch) { + return fuchsia::ui::input::InputMethodAction::SEARCH; + } else if (action_string == kTextInputActionSend) { + return fuchsia::ui::input::InputMethodAction::SEND; + } else if (action_string == kTextInputActionUnspecified) { + return fuchsia::ui::input::InputMethodAction::UNSPECIFIED; + } + // If this message comes along it means we should really add the missing 'if' + // above. + FML_VLOG(1) << "unexpected action_string: " << action_string; + // Substituting DONE for an unexpected action string will probably be OK. + return fuchsia::ui::input::InputMethodAction::DONE; +} + +// Converts the Fuchsia action enum into Flutter TextInputAction. +static const std::string IntoTextInputAction( + fuchsia::ui::input::InputMethodAction action) { + if (action == fuchsia::ui::input::InputMethodAction::NEWLINE) { + return kTextInputActionNewline; + } else if (action == fuchsia::ui::input::InputMethodAction::DONE) { + return kTextInputActionDone; + } else if (action == fuchsia::ui::input::InputMethodAction::GO) { + return kTextInputActionGo; + } else if (action == fuchsia::ui::input::InputMethodAction::NEXT) { + return kTextInputActionNext; + } else if (action == fuchsia::ui::input::InputMethodAction::PREVIOUS) { + return kTextInputActionPrevious; + } else if (action == fuchsia::ui::input::InputMethodAction::NONE) { + return kTextInputActionNone; + } else if (action == fuchsia::ui::input::InputMethodAction::SEARCH) { + return kTextInputActionSearch; + } else if (action == fuchsia::ui::input::InputMethodAction::SEND) { + return kTextInputActionSend; + } else if (action == fuchsia::ui::input::InputMethodAction::UNSPECIFIED) { + return kTextInputActionUnspecified; + } + // If this message comes along it means we should really add the missing 'if' + // above. + FML_VLOG(1) << "unexpected action: " << static_cast(action); + // Substituting "done" for an unexpected text input action will probably + // be OK. + return kTextInputActionDone; +} + +// TODO(fxbug.dev/8868): Terminate engine if Fuchsia system FIDL connections +// have error. +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 << ", status: " << status; + }); +} +template +void SetInterfaceErrorHandler(fidl::Binding& binding, std::string name) { + binding.set_error_handler([name](zx_status_t status) { + FML_LOG(ERROR) << "Binding error on: " << name << ", status: " << status; + }); +} + +TextDelegate::TextDelegate( + fuchsia::ui::views::ViewRef view_ref, + fuchsia::ui::input::ImeServiceHandle ime_service, + fuchsia::ui::input3::KeyboardHandle keyboard, + std::function)> + dispatch_callback) + : dispatch_callback_(dispatch_callback), + ime_client_(this), + text_sync_service_(ime_service.Bind()), + keyboard_listener_binding_(this), + keyboard_(keyboard.Bind()) { + // Register all error handlers. + SetInterfaceErrorHandler(ime_, "Input Method Editor"); + SetInterfaceErrorHandler(ime_client_, "IME Client"); + SetInterfaceErrorHandler(text_sync_service_, "Text Sync Service"); + SetInterfaceErrorHandler(keyboard_listener_binding_, "Keyboard Listener"); + SetInterfaceErrorHandler(keyboard_, "Keyboard"); + + // Configure keyboard listener. + keyboard_->AddListener(std::move(view_ref), + keyboard_listener_binding_.NewBinding(), [] {}); +} + +void TextDelegate::ActivateIme() { + ActivateIme(requested_text_action_.value_or( + fuchsia::ui::input::InputMethodAction::DONE)); +} + +void TextDelegate::ActivateIme(fuchsia::ui::input::InputMethodAction action) { + DEBUG_CHECK(last_text_state_.has_value(), LOG_TAG, ""); + + requested_text_action_ = action; + text_sync_service_->GetInputMethodEditor( + fuchsia::ui::input::KeyboardType::TEXT, // keyboard type + action, // input method action + last_text_state_.value(), // initial state + ime_client_.NewBinding(), // client + ime_.NewRequest() // editor + ); +} + +void TextDelegate::DeactivateIme() { + if (ime_) { + text_sync_service_->HideKeyboard(); + ime_ = nullptr; + } + if (ime_client_.is_bound()) { + ime_client_.Unbind(); + } +} + +// |fuchsia::ui::input::InputMethodEditorClient| +void TextDelegate::DidUpdateState( + fuchsia::ui::input::TextInputState state, + std::unique_ptr input_event) { + rapidjson::Document document; + auto& allocator = document.GetAllocator(); + rapidjson::Value encoded_state(rapidjson::kObjectType); + encoded_state.AddMember("text", state.text, allocator); + encoded_state.AddMember("selectionBase", state.selection.base, allocator); + encoded_state.AddMember("selectionExtent", state.selection.extent, allocator); + switch (state.selection.affinity) { + case fuchsia::ui::input::TextAffinity::UPSTREAM: + encoded_state.AddMember("selectionAffinity", + rapidjson::Value("TextAffinity.upstream"), + allocator); + break; + case fuchsia::ui::input::TextAffinity::DOWNSTREAM: + encoded_state.AddMember("selectionAffinity", + rapidjson::Value("TextAffinity.downstream"), + allocator); + break; + } + encoded_state.AddMember("selectionIsDirectional", true, allocator); + encoded_state.AddMember("composingBase", state.composing.start, allocator); + encoded_state.AddMember("composingExtent", state.composing.end, allocator); + + rapidjson::Value args(rapidjson::kArrayType); + args.PushBack(current_text_input_client_, allocator); + args.PushBack(encoded_state, allocator); + + document.SetObject(); + document.AddMember("method", + rapidjson::Value("TextInputClient.updateEditingState"), + allocator); + document.AddMember("args", args, allocator); + + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + document.Accept(writer); + + const uint8_t* data = reinterpret_cast(buffer.GetString()); + dispatch_callback_(std::make_unique( + kTextInputChannel, // channel + fml::MallocMapping::Copy(data, buffer.GetSize()), // message + nullptr) // response + ); + last_text_state_ = std::move(state); +} + +// |fuchsia::ui::input::InputMethodEditorClient| +void TextDelegate::OnAction(fuchsia::ui::input::InputMethodAction action) { + rapidjson::Document document; + auto& allocator = document.GetAllocator(); + + rapidjson::Value args(rapidjson::kArrayType); + args.PushBack(current_text_input_client_, allocator); + + const std::string action_string = IntoTextInputAction(action); + args.PushBack(rapidjson::Value{}.SetString(action_string.c_str(), + action_string.length()), + allocator); + + document.SetObject(); + document.AddMember( + "method", rapidjson::Value("TextInputClient.performAction"), allocator); + document.AddMember("args", args, allocator); + + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + document.Accept(writer); + + const uint8_t* data = reinterpret_cast(buffer.GetString()); + dispatch_callback_(std::make_unique( + kTextInputChannel, // channel + fml::MallocMapping::Copy(data, buffer.GetSize()), // message + nullptr) // response + ); +} + +// Channel handler for kTextInputChannel +bool TextDelegate::HandleFlutterTextInputChannelPlatformMessage( + std::unique_ptr message) { + FML_DCHECK(message->channel() == kTextInputChannel); + const auto& data = message->data(); + + rapidjson::Document document; + document.Parse(reinterpret_cast(data.GetMapping()), + data.GetSize()); + if (document.HasParseError() || !document.IsObject()) { + return false; + } + auto root = document.GetObject(); + auto method = root.FindMember("method"); + if (method == root.MemberEnd() || !method->value.IsString()) { + return false; + } + + if (method->value == "TextInput.show") { + if (ime_) { + text_sync_service_->ShowKeyboard(); + } + } else if (method->value == "TextInput.hide") { + if (ime_) { + text_sync_service_->HideKeyboard(); + } + } else if (method->value == "TextInput.setClient") { + // Sample "setClient" message: + // + // { + // "method": "TextInput.setClient", + // "args": [ + // 7, + // { + // "inputType": { + // "name": "TextInputType.multiline", + // "signed":null, + // "decimal":null + // }, + // "readOnly": false, + // "obscureText": false, + // "autocorrect":true, + // "smartDashesType":"1", + // "smartQuotesType":"1", + // "enableSuggestions":true, + // "enableInteractiveSelection":true, + // "actionLabel":null, + // "inputAction":"TextInputAction.newline", + // "textCapitalization":"TextCapitalization.none", + // "keyboardAppearance":"Brightness.dark", + // "enableIMEPersonalizedLearning":true, + // "enableDeltaModel":false + // } + // ] + // } + + current_text_input_client_ = 0; + DeactivateIme(); + auto args = root.FindMember("args"); + if (args == root.MemberEnd() || !args->value.IsArray() || + args->value.Size() != 2) + return false; + const auto& configuration = args->value[1]; + if (!configuration.IsObject()) { + return false; + } + // TODO(abarth): Read the keyboard type from the configuration. + current_text_input_client_ = args->value[0].GetInt(); + + auto initial_text_input_state = fuchsia::ui::input::TextInputState{}; + initial_text_input_state.text = ""; + last_text_state_ = std::move(initial_text_input_state); + + const auto configuration_object = configuration.GetObject(); + if (!configuration_object.HasMember(kInputActionKey)) { + return false; + } + const auto& action_object = configuration_object[kInputActionKey]; + if (!action_object.IsString()) { + return false; + } + const auto action_string = + std::string(action_object.GetString(), action_object.GetStringLength()); + ActivateIme(IntoInputMethodAction(std::move(action_string))); + } else if (method->value == "TextInput.setEditingState") { + if (ime_) { + auto args_it = root.FindMember("args"); + if (args_it == root.MemberEnd() || !args_it->value.IsObject()) { + return false; + } + const auto& args = args_it->value; + fuchsia::ui::input::TextInputState state; + state.text = ""; + // TODO(abarth): Deserialize state. + auto text = args.FindMember("text"); + if (text != args.MemberEnd() && text->value.IsString()) { + state.text = text->value.GetString(); + } + auto selection_base = args.FindMember("selectionBase"); + if (selection_base != args.MemberEnd() && selection_base->value.IsInt()) { + state.selection.base = selection_base->value.GetInt(); + } + auto selection_extent = args.FindMember("selectionExtent"); + if (selection_extent != args.MemberEnd() && + selection_extent->value.IsInt()) { + state.selection.extent = selection_extent->value.GetInt(); + } + auto selection_affinity = args.FindMember("selectionAffinity"); + if (selection_affinity != args.MemberEnd() && + selection_affinity->value.IsString() && + selection_affinity->value == "TextAffinity.upstream") { + state.selection.affinity = fuchsia::ui::input::TextAffinity::UPSTREAM; + } else { + state.selection.affinity = fuchsia::ui::input::TextAffinity::DOWNSTREAM; + } + // We ignore selectionIsDirectional because that concept doesn't exist on + // Fuchsia. + auto composing_base = args.FindMember("composingBase"); + if (composing_base != args.MemberEnd() && composing_base->value.IsInt()) { + state.composing.start = composing_base->value.GetInt(); + } + auto composing_extent = args.FindMember("composingExtent"); + if (composing_extent != args.MemberEnd() && + composing_extent->value.IsInt()) { + state.composing.end = composing_extent->value.GetInt(); + } + ime_->SetState(std::move(state)); + } + } else if (method->value == "TextInput.clearClient") { + current_text_input_client_ = 0; + last_text_state_ = std::nullopt; + requested_text_action_ = std::nullopt; + DeactivateIme(); + } else if (method->value == "TextInput.setCaretRect" || + method->value == "TextInput.setEditableSizeAndTransform" || + method->value == "TextInput.setMarkedTextRect" || + method->value == "TextInput.setStyle") { + // We don't have these methods implemented and they get + // sent a lot during text input, so we create an empty case for them + // here to avoid "Unknown flutter/textinput method TextInput.*" + // log spam. + // + // TODO(fxb/101619): We should implement these. + } else { + FML_LOG(ERROR) << "Unknown " << message->channel() << " method " + << method->value.GetString(); + } + // Complete with an empty response. + return false; +} + +// |fuchsia::ui:input3::KeyboardListener| +void TextDelegate::OnKeyEvent( + fuchsia::ui::input3::KeyEvent key_event, + fuchsia::ui::input3::KeyboardListener::OnKeyEventCallback callback) { + const char* type = nullptr; + 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: + // SYNC means the key was pressed while focus was not on this application. + // This should possibly behave like PRESSED in the future, though it + // doesn't hurt to ignore it today. + case fuchsia::ui::input3::KeyEventType::CANCEL: + // CANCEL means the key was released while focus was not on this + // application. + // This should possibly behave like RELEASED in the future to ensure that + // a key is not repeated forever if it is pressed while focus is lost. + default: + break; + } + if (type == nullptr) { + FML_VLOG(1) << "Unknown key event phase."; + callback(fuchsia::ui::input3::KeyEventStatus::NOT_HANDLED); + return; + } + keyboard_translator_.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_translator_.LastHIDUsage(), + allocator); + document.AddMember("codePoint", keyboard_translator_.LastCodePoint(), + allocator); + document.AddMember("modifiers", keyboard_translator_.Modifiers(), allocator); + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + document.Accept(writer); + + const uint8_t* data = reinterpret_cast(buffer.GetString()); + dispatch_callback_(std::make_unique( + kKeyEventChannel, // channel + fml::MallocMapping::Copy(data, buffer.GetSize()), // data + nullptr) // response + ); + callback(fuchsia::ui::input3::KeyEventStatus::HANDLED); +} +} // namespace flutter_runner diff --git a/engine/src/flutter/shell/platform/fuchsia/flutter/text_delegate.h b/engine/src/flutter/shell/platform/fuchsia/flutter/text_delegate.h new file mode 100644 index 00000000000..ce27c45b624 --- /dev/null +++ b/engine/src/flutter/shell/platform/fuchsia/flutter/text_delegate.h @@ -0,0 +1,154 @@ +// 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. + +/// Text editing functionality delegated from |PlatformView|. +/// See |TextDelegate| for details. + +#ifndef FLUTTER_SHELL_PLATFORM_FUCHSIA_TEXT_DELEGATE_H_ +#define FLUTTER_SHELL_PLATFORM_FUCHSIA_TEXT_DELEGATE_H_ + +#include + +#include +#include +#include +#include + +#include "flutter/lib/ui/window/platform_message.h" +#include "flutter/shell/common/platform_view.h" +#include "flutter/shell/platform/fuchsia/flutter/keyboard.h" + +#include "logging.h" + +namespace flutter_runner { + +/// The channel name used for text editing platofrm messages. +constexpr char kTextInputChannel[] = "flutter/textinput"; + +/// The channel name used for key event platform messages. +constexpr char kKeyEventChannel[] = "flutter/keyevent"; + +/// TextDelegate handles keyboard inpout and text editing. +/// +/// It mediates between Fuchsia's input and Flutter's platform messages. When it +/// is initialized, it contacts `fuchsia.ui.input.Keyboard` to register itself +/// as listener of key events. +/// +/// Whenever a text editing request comes from the +/// Flutter app, it will activate Fuchsia's input method editor, and will send +/// text edit actions coming from the Fuchsia platform over to the Flutter app, +/// by converting FIDL messages (`fuchsia.ui.input.InputMethodEditorClient` +/// calls) to appropriate text editing Flutter platform messages. +/// +/// For details refer to: +/// * Flutter side: +/// https://api.flutter.dev/javadoc/io/flutter/embedding/engine/systemchannels/TextInputChannel.html +/// * Fuchsia side: https://fuchsia.dev/reference/fidl/fuchsia.ui.input +class TextDelegate : public fuchsia::ui::input3::KeyboardListener, + public fuchsia::ui::input::InputMethodEditorClient { + public: + /// Creates a new TextDelegate. + /// + /// Args: + /// view_ref: the reference to the app's view. Required for registration + /// with Fuchsia. + /// ime_service: a handle to Fuchsia's input method service. + /// keyboard: the keyboard listener, gets notified of key presses and + /// releases. + /// dispatch_callback: a function used to send a Flutter platform message. + TextDelegate(fuchsia::ui::views::ViewRef view_ref, + fuchsia::ui::input::ImeServiceHandle ime_service, + fuchsia::ui::input3::KeyboardHandle keyboard, + std::function)> + dispatch_callback); + + /// |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| + /// Called by the embedder every time the edit state is updated. + void DidUpdateState( + fuchsia::ui::input::TextInputState state, + std::unique_ptr event) override; + + /// |fuchsia::ui::input::InputMethodEditorClient| + /// Called by the embedder when the action key is pressed, and the requested + /// action is supplied to Flutter. + void OnAction(fuchsia::ui::input::InputMethodAction action) override; + + /// Gets a new input method editor from the input connection. Run when both + /// Scenic has focus and Flutter has requested input with setClient. + void ActivateIme(); + + /// Detaches the input method editor connection, ending the edit session and + /// closing the onscreen keyboard. Call when input is no longer desired, + /// either because Scenic says we lost focus or when Flutter no longer has a + /// text field focused. + void DeactivateIme(); + + /// Channel handler for kTextInputChannel + bool HandleFlutterTextInputChannelPlatformMessage( + std::unique_ptr message); + + /// Returns true if there is a text state (i.e. if some text editing is in + /// progress). + bool HasTextState() { return last_text_state_.has_value(); } + + private: + // Activates the input method editor, assigning |action| to the "enter" key. + // This action will be reported by |OnAction| above when the "enter" key is + // pressed. Note that in the case of multi-line text editors, |OnAction| will + // never be called: instead, the text editor will insert a newline into the + // edited text. + void ActivateIme(fuchsia::ui::input::InputMethodAction action); + + // Converts Fuchsia platform key codes into Flutter key codes. + Keyboard keyboard_translator_; + + // A callback for sending a single Flutter platform message. + std::function)> + dispatch_callback_; + + // TextDelegate server-side binding. Methods called when the text edit state + // is updated. + fidl::Binding ime_client_; + + // An interface for interacting with a text input control. + fuchsia::ui::input::InputMethodEditorPtr ime_; + + // An interface for requesting the InputMethodEditor. + fuchsia::ui::input::ImeServicePtr text_sync_service_; + + // The locally-unique identifier of the text input currently in use. Flutter + // usually uses only one at a time. + int current_text_input_client_ = 0; + + // TextDelegate server side binding. Methods called when a key is pressed. + fidl::Binding + keyboard_listener_binding_; + + // The client-side stub for calling the Keyboard protocol. + fuchsia::ui::input3::KeyboardPtr keyboard_; + + // last_text_state_ is the last state of the text input as reported by the IME + // or initialized by Flutter. We set it to null if Flutter doesn't want any + // input, since then there is no text input state at all. + // If nullptr, then no editing is in progress. + std::optional last_text_state_; + + // The action that Flutter expects to happen when the user presses the "enter" + // key. For example, it could be `InputMethodAction::DONE` which would cause + // text editing to stop and the current text to be accepted. + // If set to std::nullopt, then no editing is in progress. + std::optional requested_text_action_; + + FML_DISALLOW_COPY_AND_ASSIGN(TextDelegate); +}; + +} // namespace flutter_runner + +#endif // FLUTTER_SHELL_PLATFORM_FUCHSIA_TEXT_DELEGATE_H_ diff --git a/engine/src/flutter/shell/platform/fuchsia/flutter/text_delegate_unittests.cc b/engine/src/flutter/shell/platform/fuchsia/flutter/text_delegate_unittests.cc new file mode 100644 index 00000000000..29835d0752d --- /dev/null +++ b/engine/src/flutter/shell/platform/fuchsia/flutter/text_delegate_unittests.cc @@ -0,0 +1,288 @@ +// 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 "text_delegate.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "tests/fakes/platform_message.h" + +#include "flutter/lib/ui/window/platform_message.h" + +#include + +namespace flutter_runner::testing { + +// Convert a |PlatformMessage| to string for ease of testing. +static std::string MessageToString(PlatformMessage& message) { + const char* data = reinterpret_cast(message.data().GetMapping()); + return std::string(data, message.data().GetSize()); +} + +// Fake |KeyboardService| implementation. Only responsibility is to remember +// what it was called with. +class FakeKeyboardService : public fuchsia::ui::input3::Keyboard { + public: + // |fuchsia.ui.input3/Keyboard.AddListener| + virtual void AddListener( + fuchsia::ui::views::ViewRef, + fidl::InterfaceHandle listener, + fuchsia::ui::input3::Keyboard::AddListenerCallback callback) { + listener_ = listener.Bind(); + callback(); + } + + fidl::InterfacePtr listener_; +}; + +// Fake ImeService implementation. Only responsibility is to remember what +// it was called with. +class FakeImeService : public fuchsia::ui::input::ImeService { + public: + virtual void GetInputMethodEditor( + fuchsia::ui::input::KeyboardType keyboard_type, + fuchsia::ui::input::InputMethodAction action, + fuchsia::ui::input::TextInputState input_state, + fidl::InterfaceHandle client, + fidl::InterfaceRequest ime) { + keyboard_type_ = std::move(keyboard_type); + action_ = std::move(action); + input_state_ = std::move(input_state); + client_ = client.Bind(); + ime_ = std::move(ime); + } + + virtual void ShowKeyboard() { keyboard_shown_ = true; } + + virtual void HideKeyboard() { keyboard_shown_ = false; } + + bool IsKeyboardShown() { return keyboard_shown_; } + + bool keyboard_shown_ = false; + + fuchsia::ui::input::KeyboardType keyboard_type_; + fuchsia::ui::input::InputMethodAction action_; + fuchsia::ui::input::TextInputState input_state_; + fidl::InterfacePtr client_; + fidl::InterfaceRequest ime_; +}; + +class TextDelegateTest : public ::testing::Test { + protected: + TextDelegateTest() + : loop_(&kAsyncLoopConfigAttachToCurrentThread), + keyboard_service_binding_(&keyboard_service_), + ime_service_binding_(&ime_service_) { + fidl::InterfaceHandle keyboard_handle; + auto keyboard_request = keyboard_handle.NewRequest(); + keyboard_service_binding_.Bind(keyboard_request.TakeChannel()); + + fidl::InterfaceHandle ime_service_handle; + ime_service_binding_.Bind(ime_service_handle.NewRequest().TakeChannel()); + + auto fake_view_ref_pair = scenic::ViewRefPair::New(); + + text_delegate_ = std::make_unique( + std::move(fake_view_ref_pair.view_ref), std::move(ime_service_handle), + std::move(keyboard_handle), + // Should this be accessed through a weak pointer? + [this](std::unique_ptr message) { + last_message_ = std::move(message); + }); + + // TextDelegate has some async initialization that needs to happen. + RunLoopUntilIdle(); + } + + // Runs the event loop until all scheduled events are spent. + void RunLoopUntilIdle() { loop_.RunUntilIdle(); } + + void TearDown() override { + loop_.Quit(); + ASSERT_EQ(loop_.ResetQuit(), 0); + } + + async::Loop loop_; + + FakeKeyboardService keyboard_service_; + fidl::Binding keyboard_service_binding_; + + FakeImeService ime_service_; + fidl::Binding ime_service_binding_; + + // Unit under test. + std::unique_ptr text_delegate_; + + std::unique_ptr last_message_; +}; + +// Goes through several steps of a text edit protocol. These are hard to test +// in isolation because the text edit protocol depends on the correct method +// invocation sequence. The text editor is initialized with the editing +// parameters, and we verify that the correct input action is parsed out. We +// then exercise showing and hiding the keyboard, as well as a text state +// update. +TEST_F(TextDelegateTest, ActivateIme) { + auto fake_platform_message_response = FakePlatformMessageResponse::Create(); + { + // Initialize the editor. Without this initialization, the protocol code + // will crash. + const auto set_client_msg = R"( + { + "method": "TextInput.setClient", + "args": [ + 7, + { + "inputType": { + "name": "TextInputType.multiline", + "signed":null, + "decimal":null + }, + "readOnly": false, + "obscureText": false, + "autocorrect":true, + "smartDashesType":"1", + "smartQuotesType":"1", + "enableSuggestions":true, + "enableInteractiveSelection":true, + "actionLabel":null, + "inputAction":"TextInputAction.newline", + "textCapitalization":"TextCapitalization.none", + "keyboardAppearance":"Brightness.dark", + "enableIMEPersonalizedLearning":true, + "enableDeltaModel":false + } + ] + } + )"; + auto message = fake_platform_message_response->WithMessage( + kTextInputChannel, set_client_msg); + text_delegate_->HandleFlutterTextInputChannelPlatformMessage( + std::move(message)); + RunLoopUntilIdle(); + EXPECT_EQ(ime_service_.action_, + fuchsia::ui::input::InputMethodAction::NEWLINE); + EXPECT_FALSE(ime_service_.IsKeyboardShown()); + } + + { + // Verify that showing keyboard results in the correct platform effect. + const auto set_client_msg = R"( + { + "method": "TextInput.show" + } + )"; + auto message = fake_platform_message_response->WithMessage( + kTextInputChannel, set_client_msg); + text_delegate_->HandleFlutterTextInputChannelPlatformMessage( + std::move(message)); + RunLoopUntilIdle(); + EXPECT_TRUE(ime_service_.IsKeyboardShown()); + } + + { + // Verify that hiding keyboard results in the correct platform effect. + const auto set_client_msg = R"( + { + "method": "TextInput.hide" + } + )"; + auto message = fake_platform_message_response->WithMessage( + kTextInputChannel, set_client_msg); + text_delegate_->HandleFlutterTextInputChannelPlatformMessage( + std::move(message)); + RunLoopUntilIdle(); + EXPECT_FALSE(ime_service_.IsKeyboardShown()); + } + + { + // Update the editing state from the Fuchsia platform side. + fuchsia::ui::input::TextInputState state = { + .revision = 42, + .text = "Foo", + .selection = fuchsia::ui::input::TextSelection{}, + .composing = fuchsia::ui::input::TextRange{}, + }; + auto input_event = std::make_unique(); + ime_service_.client_->DidUpdateState(std::move(state), + std::move(input_event)); + RunLoopUntilIdle(); + EXPECT_EQ( + R"({"method":"TextInputClient.updateEditingState","args":[7,{"text":"Foo","selectionBase":0,"selectionExtent":0,"selectionAffinity":"TextAffinity.upstream","selectionIsDirectional":true,"composingBase":-1,"composingExtent":-1}]})", + MessageToString(*last_message_)); + } + + { + // Notify Flutter that the action key has been pressed. + ime_service_.client_->OnAction(fuchsia::ui::input::InputMethodAction::DONE); + RunLoopUntilIdle(); + EXPECT_EQ( + R"({"method":"TextInputClient.performAction","args":[7,"TextInputAction.done"]})", + MessageToString(*last_message_)); + } +} + +// Hands a few typical |KeyEvent|s to the text delegate. Regular key events are +// handled, "odd" key events are rejected (not handled). "Handling" a key event +// means converting it to an appropriate |PlatformMessage| and forwarding it. +TEST_F(TextDelegateTest, OnAction) { + { + // A sensible key event is converted into a platform message. + fuchsia::ui::input3::KeyEvent key_event; + *key_event.mutable_type() = fuchsia::ui::input3::KeyEventType::PRESSED; + *key_event.mutable_key() = fuchsia::input::Key::A; + key_event.mutable_key_meaning()->set_codepoint('a'); + + fuchsia::ui::input3::KeyEventStatus status; + keyboard_service_.listener_->OnKeyEvent( + std::move(key_event), [&status](fuchsia::ui::input3::KeyEventStatus s) { + status = std::move(s); + }); + RunLoopUntilIdle(); + EXPECT_EQ(fuchsia::ui::input3::KeyEventStatus::HANDLED, status); + EXPECT_EQ( + R"({"type":"keydown","keymap":"fuchsia","hidUsage":458756,"codePoint":97,"modifiers":0})", + MessageToString(*last_message_)); + } + + { + // SYNC event is not handled. + // This is currently expected, though we may need to change that behavior. + fuchsia::ui::input3::KeyEvent key_event; + *key_event.mutable_type() = fuchsia::ui::input3::KeyEventType::SYNC; + + fuchsia::ui::input3::KeyEventStatus status; + keyboard_service_.listener_->OnKeyEvent( + std::move(key_event), [&status](fuchsia::ui::input3::KeyEventStatus s) { + status = std::move(s); + }); + RunLoopUntilIdle(); + EXPECT_EQ(fuchsia::ui::input3::KeyEventStatus::NOT_HANDLED, status); + } + + { + // CANCEL event is not handled. + // This is currently expected, though we may need to change that behavior. + fuchsia::ui::input3::KeyEvent key_event; + *key_event.mutable_type() = fuchsia::ui::input3::KeyEventType::CANCEL; + + fuchsia::ui::input3::KeyEventStatus status; + keyboard_service_.listener_->OnKeyEvent( + std::move(key_event), [&status](fuchsia::ui::input3::KeyEventStatus s) { + status = std::move(s); + }); + RunLoopUntilIdle(); + EXPECT_EQ(fuchsia::ui::input3::KeyEventStatus::NOT_HANDLED, status); + } +} + +} // namespace flutter_runner::testing