chromium/chrome/browser/ash/crosapi/input_method_test_interface_ash.cc

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ash/crosapi/input_method_test_interface_ash.h"

#include <optional>
#include <string_view>
#include <utility>

#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_offset_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "chromeos/crosapi/cpp/input_method_test_interface_constants.h"
#include "ui/base/ime/ash/extension_ime_util.h"
#include "ui/base/ime/ash/ime_bridge.h"
#include "ui/base/ime/ash/input_method_ash.h"
#include "ui/base/ime/ash/input_method_manager.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/keycodes/dom/dom_code.h"

namespace crosapi {
namespace {

ash::InputMethodAsh* GetTextInputTarget() {
  const ash::IMEBridge* bridge = ash::IMEBridge::Get();
  if (!bridge)
    return nullptr;

  ash::TextInputTarget* handler = bridge->GetInputContextHandler();
  if (!handler)
    return nullptr;

  // Guaranteed to be an ash::InputMethodAsh*.
  return static_cast<ash::InputMethodAsh*>(handler->GetInputMethod());
}

scoped_refptr<ash::input_method::InputMethodManager::State>
GetInputMethodManagerState() {
  return ash::input_method::InputMethodManager::Get()->GetActiveIMEState();
}

bool HasCapability(const std::string_view capability) {
  return false;
}

std::string GenerateUniqueExtensionId() {
  static int counter = 0;

  // Use a static counter to generate unique extension IDs.
  // The extension ID must be 32 characters long, so pad it out.
  std::string extension_id = base::NumberToString(counter++);
  extension_id.append(32 - extension_id.size(), '_');
  return extension_id;
}

}  // namespace

FakeTextInputMethod::FakeTextInputMethod() = default;

FakeTextInputMethod::~FakeTextInputMethod() = default;

void FakeTextInputMethod::Focus(const InputContext& input_context) {
  for (auto& observer : observers_) {
    observer.OnFocus();
  }
}

ui::VirtualKeyboardController*
FakeTextInputMethod::GetVirtualKeyboardController() const {
  return nullptr;
}

bool FakeTextInputMethod::IsReadyForTesting() {
  return true;
}

void FakeTextInputMethod::ProcessKeyEvent(const ui::KeyEvent& key_event,
                                          KeyEventDoneCallback callback) {
  ++current_key_event_id_;
  pending_key_event_callbacks_.emplace(current_key_event_id_,
                                       std::move(callback));
}

void FakeTextInputMethod::SetSurroundingText(const std::u16string& text,
                                             const gfx::Range selection_range,
                                             uint32_t offset_pos) {
  // TODO(b/238838841): Handle `offset_pos`.
  // Don't send surrounding text changed event if the surrounding text hasn't
  // changed.
  if (previous_surrounding_text_ == text &&
      previous_selection_range_ == selection_range) {
    return;
  }

  previous_surrounding_text_ = text;
  previous_selection_range_ = selection_range;

  for (auto& observer : observers_) {
    observer.OnSurroundingTextChanged(text, selection_range);
  }
}

void FakeTextInputMethod::AddObserver(Observer* observer) {
  observers_.AddObserver(observer);
}

void FakeTextInputMethod::RemoveObserver(Observer* observer) {
  observers_.RemoveObserver(observer);
}

uint64_t FakeTextInputMethod::GetCurrentKeyEventId() const {
  return current_key_event_id_;
}

void FakeTextInputMethod::KeyEventHandled(uint64_t key_event_id, bool handled) {
  if (const auto it = pending_key_event_callbacks_.find(key_event_id);
      it != pending_key_event_callbacks_.end()) {
    std::move(it->second)
        .Run(handled ? ui::ime::KeyEventHandledState::kHandledByIME
                     : ui::ime::KeyEventHandledState::kNotHandled);
    pending_key_event_callbacks_.erase(it);
  }
}

InputMethodTestInterfaceAsh::InputMethodTestInterfaceAsh()
    : text_input_target_(GetTextInputTarget()) {
  DCHECK(text_input_target_);
  InstallAndSwitchToInputMethod(mojom::InputMethod::New(/*xkb_layout=*/"us"),
                                base::DoNothing());
  text_input_method_observation_.Observe(&fake_text_input_method_);
}

InputMethodTestInterfaceAsh::~InputMethodTestInterfaceAsh() = default;

void InputMethodTestInterfaceAsh::WaitForFocus(WaitForFocusCallback callback) {
  // If `GetTextInputClient` is not null, then it's already focused.
  if (text_input_target_->GetTextInputClient()) {
    std::move(callback).Run();
    return;
  }

  // `callback` is assumed to outlive this class.
  focus_callbacks_.AddUnsafe(std::move(callback));
}

void InputMethodTestInterfaceAsh::CommitText(const std::string& text,
                                             CommitTextCallback callback) {
  text_input_target_->CommitText(
      base::UTF8ToUTF16(text),
      ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText);
  std::move(callback).Run();
}

void InputMethodTestInterfaceAsh::SetComposition(
    const std::string& text,
    uint32_t index,
    SetCompositionCallback callback) {
  ui::CompositionText composition;
  composition.text = base::UTF8ToUTF16(text);

  text_input_target_->UpdateCompositionText(composition, index,
                                            /*visible=*/true);
  std::move(callback).Run();
}

void InputMethodTestInterfaceAsh::SendKeyEvent(mojom::KeyEventPtr event,
                                               SendKeyEventCallback callback) {
  ui::KeyEvent key_press(event->type == mojom::KeyEventType::kKeyPress
                             ? ui::EventType::kKeyPressed
                             : ui::EventType::kKeyReleased,
                         static_cast<ui::KeyboardCode>(event->key_code),
                         static_cast<ui::DomCode>(event->dom_code),
                         event->flags, static_cast<ui::DomKey>(event->dom_key),
                         ui::EventTimeForNow());
  text_input_target_->SendKeyEvent(&key_press);
  std::move(callback).Run(fake_text_input_method_.GetCurrentKeyEventId());
}

void InputMethodTestInterfaceAsh::KeyEventHandled(
    uint64_t key_event_id,
    bool handled,
    KeyEventHandledCallback callback) {
  fake_text_input_method_.KeyEventHandled(key_event_id, handled);
  std::move(callback).Run();
}

void InputMethodTestInterfaceAsh::WaitForNextSurroundingTextChange(
    WaitForNextSurroundingTextChangeCallback callback) {
  // If there are no queued surrounding text changes, then save the callback to
  // be called by the next surrounding text change. Otherwise, pop the first
  // pending surrounding text and pass it to the callback.
  if (surrounding_text_changes_.empty()) {
    DCHECK(surrounding_text_change_callback_.is_null());
    // `callback` is assumed to outlive this class.
    surrounding_text_change_callback_ = std::move(callback);
    return;
  }
  auto surrounding_text = std::move(surrounding_text_changes_.front());
  surrounding_text_changes_.pop();
  std::move(callback).Run(surrounding_text.text,
                          surrounding_text.selection_range);
}

void InputMethodTestInterfaceAsh::HasCapabilities(
    const std::vector<std::string>& capabilities,
    HasCapabilitiesCallback callback) {
  for (const std::string& capability : capabilities) {
    if (!HasCapability(capability)) {
      std::move(callback).Run(false);
      return;
    }
  }
  std::move(callback).Run(true);
}

void InputMethodTestInterfaceAsh::ConfirmComposition(
    ConfirmCompositionCallback callback) {
  text_input_target_->ConfirmComposition(/*reset_engine=*/false);
  std::move(callback).Run();
}

void InputMethodTestInterfaceAsh::DeleteSurroundingText(
    uint32_t length_before_selection,
    uint32_t length_after_selection,
    DeleteSurroundingTextCallback callback) {
  text_input_target_->DeleteSurroundingText(length_before_selection,
                                            length_after_selection);
  std::move(callback).Run();
}

void InputMethodTestInterfaceAsh::InstallAndSwitchToInputMethod(
    mojom::InputMethodPtr input_method,
    InstallAndSwitchToInputMethodCallback callback) {
  // For testing, only allow one input method to be installed. Replace the
  // previously installed input method with the new one.
  installed_input_method_ = std::make_unique<ScopedInputMethodInstall>(
      *input_method, &fake_text_input_method_);
  GetInputMethodManagerState()->ChangeInputMethod(
      installed_input_method_->GetInputMethodId(), /*show_message=*/false);
  std::move(callback).Run();
}

void InputMethodTestInterfaceAsh::OnFocus() {
  focus_callbacks_.Notify();
}

void InputMethodTestInterfaceAsh::OnSurroundingTextChanged(
    const std::u16string& text,
    const gfx::Range& selection_range) {
  std::vector<size_t> offsets = {selection_range.start(),
                                 selection_range.end()};
  const std::string text_utf8 =
      base::UTF16ToUTF8AndAdjustOffsets(text, &offsets);
  const gfx::Range selection_range_utf8(offsets[0], offsets[1]);

  // If there is no pending WaitForNextSurroundingTextChange callback, queue the
  // surrounding text change to be returned by the next
  // WaitForNextSurroundingTextChange call. Otherwise, resolve the pending
  // callback with the current surrounding text change.
  if (surrounding_text_change_callback_.is_null()) {
    surrounding_text_changes_.push({text_utf8, selection_range_utf8});
    return;
  }

  std::move(surrounding_text_change_callback_)
      .Run(text_utf8, selection_range_utf8);
}

InputMethodTestInterfaceAsh::ScopedInputMethodInstall::ScopedInputMethodInstall(
    const mojom::InputMethod& input_method,
    ash::TextInputMethod* text_input_method)
    : extension_id_(GenerateUniqueExtensionId()) {
  const std::string input_method_id = GetInputMethodId();

  scoped_refptr<ash::input_method::InputMethodManager::State> ime_state =
      GetInputMethodManagerState();
  ime_state->SetEnabledExtensionImes(std::vector<std::string>{input_method_id});
  ime_state->AddInputMethodExtension(
      extension_id_,
      {ash::input_method::InputMethodDescriptor(
          input_method_id, "", /*indicator=*/"T", input_method.xkb_layout, {},
          /*is_login_keyboard=*/true, {}, {},
          /*handwriting_language=*/std::nullopt)},
      text_input_method);
}

InputMethodTestInterfaceAsh::ScopedInputMethodInstall::
    ~ScopedInputMethodInstall() {
  GetInputMethodManagerState()->RemoveInputMethodExtension(extension_id());
}

const std::string&
InputMethodTestInterfaceAsh::ScopedInputMethodInstall::extension_id() const {
  return extension_id_;
}

std::string
InputMethodTestInterfaceAsh::ScopedInputMethodInstall::GetInputMethodId()
    const {
  return ash::extension_ime_util::GetInputMethodID(extension_id_,
                                                   /*engine_id=*/"test");
}

}  // namespace crosapi