blob: 33c09c506175d7f8494d1a1d61e872bd29e045a7 [file] [log] [blame]
// Copyright 2019 The Chromium 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 "chrome/browser/chromeos/input_method/native_input_method_engine.h"
#include "base/feature_list.h"
#include "base/i18n/i18n_constants.h"
#include "base/i18n/icu_string_conversions.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "chromeos/constants/chromeos_features.h"
#include "ui/base/ime/chromeos/input_method_manager.h"
#include "ui/base/ime/ime_bridge.h"
namespace chromeos {
namespace {
// Returns the current input context. This may change during the session, even
// if the IME engine does not change.
ui::IMEInputContextHandlerInterface* GetInputContext() {
return ui::IMEBridge::Get()->GetInputContextHandler();
}
bool ShouldEngineUseMojo(const std::string& engine_id) {
return base::FeatureList::IsEnabled(
chromeos::features::kNativeRuleBasedTyping) &&
base::StartsWith(engine_id, "vkd_", base::CompareCase::SENSITIVE);
}
std::string NormalizeString(const std::string& str) {
std::string normalized_str;
base::ConvertToUtf8AndNormalize(str, base::kCodepageUTF8, &normalized_str);
return normalized_str;
}
enum class ImeServiceEvent {
kUnknown = 0,
kInitSuccess = 1,
kInitFailed = 2,
kActivateImeSuccess = 3,
kActivateImeFailed = 4,
kServiceDisconnected = 5,
kMaxValue = kServiceDisconnected
};
void LogEvent(ImeServiceEvent event) {
UMA_HISTOGRAM_ENUMERATION("InputMethod.Mojo.Extension.Event", event);
}
void LogLatency(const char* name, const base::TimeDelta& latency) {
base::UmaHistogramCustomCounts(name, latency.InMilliseconds(), 0, 1000, 50);
}
} // namespace
NativeInputMethodEngine::NativeInputMethodEngine() = default;
NativeInputMethodEngine::~NativeInputMethodEngine() = default;
void NativeInputMethodEngine::Initialize(
std::unique_ptr<InputMethodEngineBase::Observer> observer,
const char* extension_id,
Profile* profile) {
std::unique_ptr<AssistiveSuggester> assistive_suggester =
std::make_unique<AssistiveSuggester>(this, profile);
// Wrap the given observer in our observer that will decide whether to call
// Mojo directly or forward to the extension.
auto native_observer =
std::make_unique<chromeos::NativeInputMethodEngine::ImeObserver>(
std::move(observer), std::move(assistive_suggester));
InputMethodEngine::Initialize(std::move(native_observer), extension_id,
profile);
}
void NativeInputMethodEngine::FlushForTesting() {
GetNativeObserver()->FlushForTesting();
}
bool NativeInputMethodEngine::IsConnectedForTesting() const {
return GetNativeObserver()->IsConnectedForTesting();
}
NativeInputMethodEngine::ImeObserver*
NativeInputMethodEngine::GetNativeObserver() const {
return static_cast<ImeObserver*>(observer_.get());
}
NativeInputMethodEngine::ImeObserver::ImeObserver(
std::unique_ptr<InputMethodEngineBase::Observer> base_observer,
std::unique_ptr<AssistiveSuggester> assistive_suggester)
: base_observer_(std::move(base_observer)),
receiver_from_engine_(this),
assistive_suggester_(std::move(assistive_suggester)) {
}
NativeInputMethodEngine::ImeObserver::~ImeObserver() = default;
void NativeInputMethodEngine::ImeObserver::OnActivate(
const std::string& engine_id) {
if (ShouldEngineUseMojo(engine_id)) {
if (!remote_manager_.is_bound()) {
auto* ime_manager = input_method::InputMethodManager::Get();
const auto start = base::Time::Now();
ime_manager->ConnectInputEngineManager(
remote_manager_.BindNewPipeAndPassReceiver());
LogLatency("InputMethod.Mojo.Extension.ServiceInitLatency",
base::Time::Now() - start);
remote_manager_.set_disconnect_handler(base::BindOnce(
&ImeObserver::OnError, base::Unretained(this), base::Time::Now()));
LogEvent(ImeServiceEvent::kInitSuccess);
}
// For legacy reasons, |engine_id| starts with "vkd_" in the input method
// manifest, but the InputEngineManager expects the prefix "m17n:".
// TODO(https://crbug.com/1012490): Migrate to m17n prefix and remove this.
const auto new_engine_id = "m17n:" + engine_id.substr(4);
// Deactivate any existing engine.
remote_to_engine_.reset();
receiver_from_engine_.reset();
remote_manager_->ConnectToImeEngine(
new_engine_id, remote_to_engine_.BindNewPipeAndPassReceiver(),
receiver_from_engine_.BindNewPipeAndPassRemote(), {},
base::BindOnce(&ImeObserver::OnConnected, base::Unretained(this),
base::Time::Now()));
} else {
// Release the IME service.
// TODO(b/147709499): A better way to cleanup all.
remote_manager_.reset();
}
base_observer_->OnActivate(engine_id);
}
void NativeInputMethodEngine::ImeObserver::OnFocus(
const IMEEngineHandlerInterface::InputContext& context) {
if (IsAssistiveFeatureEnabled())
assistive_suggester_->OnFocus(context.id);
base_observer_->OnFocus(context);
}
void NativeInputMethodEngine::ImeObserver::OnBlur(int context_id) {
if (IsAssistiveFeatureEnabled())
assistive_suggester_->OnBlur();
base_observer_->OnBlur(context_id);
}
void NativeInputMethodEngine::ImeObserver::OnKeyEvent(
const std::string& engine_id,
const InputMethodEngineBase::KeyboardEvent& event,
ui::IMEEngineHandlerInterface::KeyEventDoneCallback callback) {
if (IsAssistiveFeatureEnabled()) {
if (assistive_suggester_->OnKeyEvent(event)) {
std::move(callback).Run(true);
return;
}
}
if (ShouldEngineUseMojo(engine_id) && remote_to_engine_.is_bound()) {
remote_to_engine_->ProcessKeypressForRulebased(
ime::mojom::KeypressInfoForRulebased::New(
event.type, event.code, event.shift_key, event.altgr_key,
event.caps_lock, event.ctrl_key, event.alt_key),
base::BindOnce(&ImeObserver::OnKeyEventResponse, base::Unretained(this),
base::Time::Now(), std::move(callback)));
} else {
base_observer_->OnKeyEvent(engine_id, event, std::move(callback));
}
}
void NativeInputMethodEngine::ImeObserver::OnReset(
const std::string& engine_id) {
if (ShouldEngineUseMojo(engine_id) && remote_to_engine_.is_bound()) {
remote_to_engine_->ResetForRulebased();
}
base_observer_->OnReset(engine_id);
}
void NativeInputMethodEngine::ImeObserver::OnDeactivated(
const std::string& engine_id) {
if (ShouldEngineUseMojo(engine_id)) {
remote_to_engine_.reset();
}
base_observer_->OnDeactivated(engine_id);
}
void NativeInputMethodEngine::ImeObserver::OnCompositionBoundsChanged(
const std::vector<gfx::Rect>& bounds) {
base_observer_->OnCompositionBoundsChanged(bounds);
}
void NativeInputMethodEngine::ImeObserver::OnSurroundingTextChanged(
const std::string& engine_id,
const base::string16& text,
int cursor_pos,
int anchor_pos,
int offset_pos) {
assistive_suggester_->RecordAssistiveCoverageMetrics(text, cursor_pos,
anchor_pos);
if (IsAssistiveFeatureEnabled()) {
// If |assistive_suggester_| changes the surrounding text, no longer need
// to call the following function, as the information is out-dated.
if (assistive_suggester_->OnSurroundingTextChanged(text, cursor_pos,
anchor_pos)) {
return;
}
}
base_observer_->OnSurroundingTextChanged(engine_id, text, cursor_pos,
anchor_pos, offset_pos);
}
void NativeInputMethodEngine::ImeObserver::OnInputContextUpdate(
const IMEEngineHandlerInterface::InputContext& context) {
base_observer_->OnInputContextUpdate(context);
}
void NativeInputMethodEngine::ImeObserver::OnCandidateClicked(
const std::string& component_id,
int candidate_id,
InputMethodEngineBase::MouseButtonEvent button) {
base_observer_->OnCandidateClicked(component_id, candidate_id, button);
}
void NativeInputMethodEngine::ImeObserver::OnMenuItemActivated(
const std::string& component_id,
const std::string& menu_id) {
base_observer_->OnMenuItemActivated(component_id, menu_id);
}
void NativeInputMethodEngine::ImeObserver::OnScreenProjectionChanged(
bool is_projected) {
base_observer_->OnScreenProjectionChanged(is_projected);
}
void NativeInputMethodEngine::ImeObserver::FlushForTesting() {
remote_manager_.FlushForTesting();
if (remote_to_engine_.is_bound())
receiver_from_engine_.FlushForTesting();
if (remote_to_engine_.is_bound())
remote_to_engine_.FlushForTesting();
}
void NativeInputMethodEngine::ImeObserver::OnConnected(base::Time start,
bool bound) {
LogLatency("InputMethod.Mojo.Extension.ActivateIMELatency",
base::Time::Now() - start);
LogEvent(bound ? ImeServiceEvent::kActivateImeSuccess
: ImeServiceEvent::kActivateImeSuccess);
connected_to_engine_ = bound;
}
void NativeInputMethodEngine::ImeObserver::OnError(base::Time start) {
LOG(ERROR) << "IME Service connection error";
// If the Mojo pipe disconnection happens in 1000 ms after the service
// is initialized, we consider it as a failure. Normally it's caused
// by the Mojo service itself or misconfigured on Chrome OS.
if (base::Time::Now() - start < base::TimeDelta::FromMilliseconds(1000)) {
LogEvent(ImeServiceEvent::kInitFailed);
} else {
LogEvent(ImeServiceEvent::kServiceDisconnected);
}
}
void NativeInputMethodEngine::ImeObserver::OnKeyEventResponse(
base::Time start,
ui::IMEEngineHandlerInterface::KeyEventDoneCallback callback,
ime::mojom::KeypressResponseForRulebasedPtr response) {
LogLatency("InputMethod.Mojo.Extension.Rulebased.ProcessLatency",
base::Time::Now() - start);
for (const auto& op : response->operations) {
switch (op->method) {
case ime::mojom::OperationMethodForRulebased::COMMIT_TEXT:
GetInputContext()->CommitText(NormalizeString(op->arguments));
break;
case ime::mojom::OperationMethodForRulebased::SET_COMPOSITION:
ui::CompositionText composition;
composition.text = base::UTF8ToUTF16(NormalizeString(op->arguments));
GetInputContext()->UpdateCompositionText(
composition, composition.text.length(), /*visible=*/true);
break;
}
}
std::move(callback).Run(response->result);
}
} // namespace chromeos