blob: a2e3c55c310cf04c46ea65ef1cc0080d1dfb3716 [file] [log] [blame]
// 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