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