| // 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. |
| |
| #define RAPIDJSON_HAS_STDSTRING 1 |
| |
| #include "src/embedder/text_delegate.h" |
| |
| #include <fuchsia/ui/input/cpp/fidl.h> |
| #include <fuchsia/ui/input3/cpp/fidl.h> |
| #include <fuchsia/ui/views/cpp/fidl.h> |
| #include <lib/fidl/cpp/binding.h> |
| #include <lib/syslog/global.h> |
| #include <zircon/status.h> |
| |
| #include "include/rapidjson/document.h" |
| #include "include/rapidjson/rapidjson.h" |
| #include "include/rapidjson/stringbuffer.h" |
| #include "include/rapidjson/writer.h" |
| #include "logging.h" |
| #include "src/embedder/engine/embedder.h" |
| #include "src/embedder/fuchsia_logger.h" |
| #include "src/embedder/keyboard.h" |
| #include "src/embedder/logging.h" |
| #include "src/embedder/platform_message_channels.h" |
| |
| namespace embedder { |
| |
| 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"; |
| |
| [[maybe_unused]] |
| // 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. |
| FX_LOGF(INFO, kLogTag, "unexpected action_string: %s", action_string.c_str()); |
| // Substituting DONE for an unexpected action string will probably be OK. |
| return fuchsia::ui::input::InputMethodAction::DONE; |
| } |
| |
| [[maybe_unused]] |
| // 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. |
| FX_LOGF(INFO, kLogTag, "unexpected action: %d", static_cast<uint32_t>(action)); |
| |
| // Substituting "done" for an unexpected text input action will probably |
| // be OK. |
| return kTextInputActionDone; |
| } |
| |
| // TODO(fxbug.dev/8868): Terminate embedder if Fuchsia system FIDL connections |
| // have error. |
| template <class T> |
| void SetInterfaceErrorHandler(fidl::InterfacePtr<T>& interface, std::string name) { |
| interface.set_error_handler([name](zx_status_t status) { |
| FX_LOGF(ERROR, kLogTag, "Interface error on: %s, status: %s", name.c_str(), |
| zx_status_get_string(status)); |
| }); |
| } |
| |
| template <class T> |
| void SetInterfaceErrorHandler(fidl::Binding<T>& binding, std::string name) { |
| binding.set_error_handler([name](zx_status_t status) { |
| FX_LOGF(ERROR, kLogTag, "Binding error on: %s, status: %s", name.c_str(), |
| zx_status_get_string(status)); |
| }); |
| } |
| |
| TextDelegate::TextDelegate( |
| fuchsia::ui::views::ViewRef view_ref, fuchsia::ui::input::ImeServiceHandle ime_service, |
| fuchsia::ui::input3::KeyboardHandle keyboard, |
| std::function<void(const FlutterKeyEvent*)> key_event_dispatch_callback, |
| std::function<void(const FlutterPlatformMessage*)> platform_dispatch_callback, |
| FlutterPlatformMessageResponseHandle* response_handle) |
| : key_event_dispatch_callback_(key_event_dispatch_callback), |
| platform_dispatch_callback_(platform_dispatch_callback), |
| response_handle_(response_handle), |
| 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) { |
| FX_DCHECK(last_text_state_.has_value()); |
| |
| 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(); |
| } |
| } |
| |
| void TextDelegate::DidUpdateState(fuchsia::ui::input::TextInputState state, |
| std::unique_ptr<fuchsia::ui::input::InputEvent> 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<rapidjson::StringBuffer> writer(buffer); |
| document.Accept(writer); |
| |
| const uint8_t* data = reinterpret_cast<const uint8_t*>(buffer.GetString()); |
| FlutterPlatformMessage flutter_platform_message = {.struct_size = sizeof(FlutterPlatformMessage), |
| .channel = kTextInputChannel, |
| .message = data, |
| .message_size = buffer.GetSize(), |
| .response_handle = response_handle_}; |
| platform_dispatch_callback_(&flutter_platform_message); |
| last_text_state_ = std::move(state); |
| } |
| |
| 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<rapidjson::StringBuffer> writer(buffer); |
| document.Accept(writer); |
| |
| const uint8_t* data = reinterpret_cast<const uint8_t*>(buffer.GetString()); |
| FlutterPlatformMessage flutter_platform_message = {.struct_size = sizeof(FlutterPlatformMessage), |
| .channel = kTextInputChannel, |
| .message = data, |
| .message_size = buffer.GetSize(), |
| .response_handle = response_handle_}; |
| platform_dispatch_callback_(&flutter_platform_message); |
| } |
| |
| // Channel handler for kTextInputChannel |
| bool TextDelegate::HandleFlutterTextInputChannelPlatformMessage( |
| const FlutterPlatformMessage* message) { |
| FX_DCHECK(!strcmp(message->channel, kTextInputChannel)); |
| |
| const auto& data = message->message; |
| |
| rapidjson::Document document; |
| document.Parse(reinterpret_cast<const char*>(data), message->message_size); |
| if (document.HasParseError()) { |
| return false; |
| } |
| |
| if (!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 { |
| FX_LOGF(ERROR, kLogTag, "Unknown %s method %s", message->channel, 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 on_key_event_callback) { |
| const char* type = nullptr; |
| FlutterKeyEventType flutter_key_type; |
| switch (key_event.type()) { |
| case fuchsia::ui::input3::KeyEventType::PRESSED: |
| type = "keydown"; |
| flutter_key_type = FlutterKeyEventType::kFlutterKeyEventTypeDown; |
| break; |
| case fuchsia::ui::input3::KeyEventType::RELEASED: |
| type = "keyup"; |
| flutter_key_type = FlutterKeyEventType::kFlutterKeyEventTypeUp; |
| 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) { |
| FX_LOGF(INFO, kLogTag, "Unknown key event phase."); |
| |
| // Notify the key event wasn't handled by this keyboard listener. |
| on_key_event_callback(fuchsia::ui::input3::KeyEventStatus::NOT_HANDLED); |
| return; |
| } |
| |
| keyboard_translator_.ConsumeEvent(std::move(key_event)); |
| |
| // Encourage the Engine to vsync for a 60Hz display. |
| const uint64_t now_nanos = FlutterEngineGetCurrentTime(); |
| const uint64_t now_micro = now_nanos * 1e3; |
| |
| // The character representation for key-up strokes should be null as the |
| // true character is already sent to the engine on the key-down stroke. |
| static char keyUp[1] = {'\0'}; |
| |
| // Key message to be passed via flutter's flutter/keydata channel. |
| FlutterKeyEvent flutter_key_data_event = { |
| .struct_size = sizeof(FlutterKeyEvent), |
| .timestamp = static_cast<double>(now_micro), |
| .type = flutter_key_type, |
| .physical = static_cast<uint64_t>(keyboard_translator_.LastHIDUsage()), |
| .logical = static_cast<uint64_t>(keyboard_translator_.LastLogicalKey()), |
| .character = strcmp(type, "keydown") == 0 |
| ? keyboard_translator_.LastCodePointAsChar() |
| : keyUp, // Only assign the character on keydown strokes and |
| .synthesized = false}; |
| |
| // Send key data event to engine. |
| key_event_dispatch_callback_(&flutter_key_data_event); |
| |
| // Key message to be passed via flutter's flutter/keyevent channel. To be deprecated in the |
| // future in favor of flutter/keydata channel. |
| 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<rapidjson::StringBuffer> writer(buffer); |
| document.Accept(writer); |
| |
| const uint8_t* data = reinterpret_cast<const uint8_t*>(buffer.GetString()); |
| FlutterPlatformMessage flutter_raw_event = {.struct_size = sizeof(FlutterPlatformMessage), |
| .channel = kKeyEventChannel, |
| .message = data, |
| .message_size = buffer.GetSize(), |
| .response_handle = response_handle_}; |
| |
| // Send raw key event to engine. |
| platform_dispatch_callback_(&flutter_raw_event); |
| |
| on_key_event_callback(fuchsia::ui::input3::KeyEventStatus::HANDLED); |
| } |
| } // namespace embedder |