blob: c044f65a7118281bd23ef88c3272142f5d2dc6f9 [file] [log] [blame]
// Copyright 2016 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 "components/arc/ime/arc_ime_service.h"
#include <memory>
#include <set>
#include <utility>
#include "base/strings/string16.h"
#include "base/strings/utf_string_conversions.h"
#include "components/arc/arc_bridge_service.h"
#include "components/arc/common/ime.mojom.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/aura/test/test_window_delegate.h"
#include "ui/aura/test/test_windows.h"
#include "ui/aura/window.h"
#include "ui/base/ime/composition_text.h"
#include "ui/base/ime/dummy_input_method.h"
#include "ui/base/ime/text_input_flags.h"
#include "ui/events/event.h"
#include "ui/events/keycodes/dom/dom_code.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "ui/keyboard/keyboard_controller.h"
namespace arc {
namespace {
class FakeArcImeBridge : public ArcImeBridge {
public:
FakeArcImeBridge()
: count_send_insert_text_(0), last_keyboard_availability_(false) {}
void SendSetCompositionText(const ui::CompositionText& composition) override {
}
void SendConfirmCompositionText() override {
}
void SendInsertText(const base::string16& text) override {
count_send_insert_text_++;
}
void SendExtendSelectionAndDelete(size_t before, size_t after) override {
}
void SendOnKeyboardAppearanceChanging(const gfx::Rect& new_bounds,
bool is_available) override {
last_keyboard_bounds_ = new_bounds;
last_keyboard_availability_ = is_available;
}
int count_send_insert_text() const { return count_send_insert_text_; }
const gfx::Rect& last_keyboard_bounds() const {
return last_keyboard_bounds_;
}
bool last_keyboard_availability() const {
return last_keyboard_availability_;
}
private:
int count_send_insert_text_;
gfx::Rect last_keyboard_bounds_;
bool last_keyboard_availability_;
};
class FakeInputMethod : public ui::DummyInputMethod {
public:
FakeInputMethod()
: client_(nullptr),
count_show_ime_if_needed_(0),
count_cancel_composition_(0),
count_set_focused_text_input_client_(0),
count_on_text_input_type_changed_(0) {}
void SetFocusedTextInputClient(ui::TextInputClient* client) override {
count_set_focused_text_input_client_++;
client_ = client;
}
ui::TextInputClient* GetTextInputClient() const override {
return client_;
}
void ShowVirtualKeyboardIfEnabled() override { count_show_ime_if_needed_++; }
void CancelComposition(const ui::TextInputClient* client) override {
if (client == client_)
count_cancel_composition_++;
}
void DetachTextInputClient(ui::TextInputClient* client) override {
if (client_ == client)
client_ = nullptr;
}
void OnTextInputTypeChanged(const ui::TextInputClient* client) override {
count_on_text_input_type_changed_++;
}
int count_show_ime_if_needed() const {
return count_show_ime_if_needed_;
}
int count_cancel_composition() const {
return count_cancel_composition_;
}
int count_set_focused_text_input_client() const {
return count_set_focused_text_input_client_;
}
int count_on_text_input_type_changed() const {
return count_on_text_input_type_changed_;
}
private:
ui::TextInputClient* client_;
int count_show_ime_if_needed_;
int count_cancel_composition_;
int count_set_focused_text_input_client_;
int count_on_text_input_type_changed_;
};
// Helper class for testing the window focus tracking feature of ArcImeService,
// not depending on the full setup of Exo and Ash.
class FakeArcWindowDelegate : public ArcImeService::ArcWindowDelegate {
public:
explicit FakeArcWindowDelegate(ui::InputMethod* input_method)
: next_id_(0), test_input_method_(input_method) {}
bool IsInArcAppWindow(const aura::Window* window) const override {
if (!window)
return false;
return arc_window_id_.count(window->id());
}
void RegisterFocusObserver() override {}
void UnregisterFocusObserver() override {}
ui::InputMethod* GetInputMethodForWindow(
aura::Window* window) const override {
return window ? test_input_method_ : nullptr;
}
std::unique_ptr<aura::Window> CreateFakeArcWindow() {
const int id = next_id_++;
arc_window_id_.insert(id);
return base::WrapUnique(aura::test::CreateTestWindowWithDelegate(
&dummy_delegate_, id, gfx::Rect(), nullptr));
}
std::unique_ptr<aura::Window> CreateFakeNonArcWindow() {
const int id = next_id_++;
return base::WrapUnique(aura::test::CreateTestWindowWithDelegate(
&dummy_delegate_, id, gfx::Rect(), nullptr));
}
private:
aura::test::TestWindowDelegate dummy_delegate_;
int next_id_;
std::set<int> arc_window_id_;
ui::InputMethod* test_input_method_;
};
} // namespace
class ArcImeServiceTest : public testing::Test {
public:
ArcImeServiceTest() = default;
protected:
std::unique_ptr<ArcBridgeService> arc_bridge_service_;
std::unique_ptr<FakeInputMethod> fake_input_method_;
std::unique_ptr<ArcImeService> instance_;
FakeArcImeBridge* fake_arc_ime_bridge_; // Owned by |instance_|
FakeArcWindowDelegate* fake_window_delegate_; // Owned by |instance_|
std::unique_ptr<aura::Window> arc_win_;
// Needed by ArcImeService.
keyboard::KeyboardController keyboard_controller_;
private:
void SetUp() override {
arc_bridge_service_ = std::make_unique<ArcBridgeService>();
instance_ =
std::make_unique<ArcImeService>(nullptr, arc_bridge_service_.get());
fake_arc_ime_bridge_ = new FakeArcImeBridge();
instance_->SetImeBridgeForTesting(base::WrapUnique(fake_arc_ime_bridge_));
fake_input_method_ = std::make_unique<FakeInputMethod>();
fake_window_delegate_ = new FakeArcWindowDelegate(fake_input_method_.get());
instance_->SetArcWindowDelegateForTesting(
base::WrapUnique(fake_window_delegate_));
arc_win_ = fake_window_delegate_->CreateFakeArcWindow();
}
void TearDown() override {
ArcImeService::SetOverrideDefaultDeviceScaleFactorForTesting(base::nullopt);
arc_win_.reset();
fake_window_delegate_ = nullptr;
fake_arc_ime_bridge_ = nullptr;
instance_.reset();
arc_bridge_service_.reset();
}
};
TEST_F(ArcImeServiceTest, HasCompositionText) {
instance_->OnWindowFocused(arc_win_.get(), nullptr);
ui::CompositionText composition;
composition.text = base::UTF8ToUTF16("nonempty text");
EXPECT_FALSE(instance_->HasCompositionText());
instance_->SetCompositionText(composition);
EXPECT_TRUE(instance_->HasCompositionText());
instance_->ClearCompositionText();
EXPECT_FALSE(instance_->HasCompositionText());
instance_->SetCompositionText(composition);
EXPECT_TRUE(instance_->HasCompositionText());
instance_->ConfirmCompositionText();
EXPECT_FALSE(instance_->HasCompositionText());
instance_->SetCompositionText(composition);
EXPECT_TRUE(instance_->HasCompositionText());
instance_->InsertText(base::UTF8ToUTF16("another text"));
EXPECT_FALSE(instance_->HasCompositionText());
instance_->SetCompositionText(composition);
EXPECT_TRUE(instance_->HasCompositionText());
instance_->SetCompositionText(ui::CompositionText());
EXPECT_FALSE(instance_->HasCompositionText());
}
TEST_F(ArcImeServiceTest, ShowVirtualKeyboardIfEnabled) {
instance_->OnWindowFocused(arc_win_.get(), nullptr);
instance_->OnTextInputTypeChanged(ui::TEXT_INPUT_TYPE_NONE, false,
mojom::TEXT_INPUT_FLAG_NONE);
ASSERT_EQ(0, fake_input_method_->count_show_ime_if_needed());
// Text input type change does not imply the show ime request.
instance_->OnTextInputTypeChanged(ui::TEXT_INPUT_TYPE_TEXT, true,
mojom::TEXT_INPUT_FLAG_NONE);
EXPECT_EQ(0, fake_input_method_->count_show_ime_if_needed());
instance_->ShowVirtualKeyboardIfEnabled();
EXPECT_EQ(1, fake_input_method_->count_show_ime_if_needed());
}
TEST_F(ArcImeServiceTest, CancelComposition) {
instance_->OnWindowFocused(arc_win_.get(), nullptr);
// The bridge should forward the cancel event to the input method.
instance_->OnCancelComposition();
EXPECT_EQ(1, fake_input_method_->count_cancel_composition());
}
TEST_F(ArcImeServiceTest, InsertChar) {
instance_->OnWindowFocused(arc_win_.get(), nullptr);
// When text input type is NONE, the event is not forwarded.
instance_->OnTextInputTypeChanged(ui::TEXT_INPUT_TYPE_NONE, false,
mojom::TEXT_INPUT_FLAG_NONE);
instance_->InsertChar(ui::KeyEvent('a', ui::VKEY_A, ui::DomCode::NONE, 0));
EXPECT_EQ(0, fake_arc_ime_bridge_->count_send_insert_text());
// When the bridge is accepting text inputs, forward the event.
instance_->OnTextInputTypeChanged(ui::TEXT_INPUT_TYPE_TEXT, true,
mojom::TEXT_INPUT_FLAG_NONE);
instance_->InsertChar(ui::KeyEvent('a', ui::VKEY_A, ui::DomCode::NONE, 0));
EXPECT_EQ(1, fake_arc_ime_bridge_->count_send_insert_text());
}
TEST_F(ArcImeServiceTest, WindowFocusTracking) {
std::unique_ptr<aura::Window> arc_win2 =
fake_window_delegate_->CreateFakeArcWindow();
std::unique_ptr<aura::Window> nonarc_win =
fake_window_delegate_->CreateFakeNonArcWindow();
// ARC window is focused. ArcImeService is set as the text input client.
instance_->OnWindowFocused(arc_win_.get(), nullptr);
EXPECT_EQ(instance_.get(), fake_input_method_->GetTextInputClient());
EXPECT_EQ(1, fake_input_method_->count_set_focused_text_input_client());
// Focus is moving between ARC windows. No state change should happen.
instance_->OnWindowFocused(arc_win2.get(), arc_win_.get());
EXPECT_EQ(instance_.get(), fake_input_method_->GetTextInputClient());
EXPECT_EQ(1, fake_input_method_->count_set_focused_text_input_client());
// Focus moved to a non-ARC window. ArcImeService is detached.
instance_->OnWindowFocused(nonarc_win.get(), arc_win2.get());
EXPECT_EQ(nullptr, fake_input_method_->GetTextInputClient());
EXPECT_EQ(1, fake_input_method_->count_set_focused_text_input_client());
// Focus came back to an ARC window. ArcImeService is re-attached.
instance_->OnWindowFocused(arc_win_.get(), nonarc_win.get());
EXPECT_EQ(instance_.get(), fake_input_method_->GetTextInputClient());
EXPECT_EQ(2, fake_input_method_->count_set_focused_text_input_client());
// Focus is moving out.
instance_->OnWindowFocused(nullptr, arc_win_.get());
EXPECT_EQ(nullptr, fake_input_method_->GetTextInputClient());
EXPECT_EQ(2, fake_input_method_->count_set_focused_text_input_client());
}
TEST_F(ArcImeServiceTest, RootWindowChange) {
std::unique_ptr<aura::Window> dummy_root =
fake_window_delegate_->CreateFakeNonArcWindow();
instance_->OnWindowFocused(arc_win_.get(), nullptr);
EXPECT_EQ(instance_.get(), fake_input_method_->GetTextInputClient());
// Moving to another root window with that shares the same input method.
// ArcImeService should keep attached to the IME.
instance_->OnWindowRemovingFromRootWindow(arc_win_.get(), dummy_root.get());
EXPECT_EQ(instance_.get(), fake_input_method_->GetTextInputClient());
// Removed from a root window. It should be detached.
instance_->OnWindowRemovingFromRootWindow(arc_win_.get(), nullptr);
EXPECT_NE(instance_.get(), fake_input_method_->GetTextInputClient());
// Unfocusing afterwards should not cause any trouble like crashing.
instance_->OnWindowFocused(nullptr, arc_win_.get());
}
TEST_F(ArcImeServiceTest, GetTextFromRange) {
instance_->OnWindowFocused(arc_win_.get(), nullptr);
const base::string16 text = base::ASCIIToUTF16("abcdefghijklmn");
// Assume the cursor is between 'c' and 'd'.
const uint32_t cursor_pos = 3;
const gfx::Range text_range(cursor_pos - 1, cursor_pos + 1);
const base::string16 text_in_range = text.substr(cursor_pos - 1, 2);
const gfx::Range selection_range(cursor_pos, cursor_pos);
instance_->OnCursorRectChangedWithSurroundingText(
gfx::Rect(0, 0, 1, 1), text_range, text_in_range, selection_range,
true /* is_screen_coordinates */);
gfx::Range temp;
instance_->GetTextRange(&temp);
EXPECT_EQ(text_range, temp);
base::string16 temp_str;
instance_->GetTextFromRange(text_range, &temp_str);
EXPECT_EQ(text_in_range, temp_str);
instance_->GetEditableSelectionRange(&temp);
EXPECT_EQ(selection_range, temp);
}
TEST_F(ArcImeServiceTest, OnKeyboardAppearanceChanged) {
instance_->OnWindowFocused(arc_win_.get(), nullptr);
EXPECT_EQ(gfx::Rect(), fake_arc_ime_bridge_->last_keyboard_bounds());
EXPECT_FALSE(fake_arc_ime_bridge_->last_keyboard_availability());
const gfx::Rect keyboard_bounds(0, 480, 1200, 320);
keyboard::KeyboardStateDescriptor desc{true, keyboard_bounds, keyboard_bounds,
keyboard_bounds};
instance_->OnKeyboardAppearanceChanged(desc);
EXPECT_EQ(keyboard_bounds, fake_arc_ime_bridge_->last_keyboard_bounds());
EXPECT_TRUE(fake_arc_ime_bridge_->last_keyboard_availability());
// Change the default scale factor of the internal display.
const double new_scale_factor = 10.0;
const gfx::Rect new_keyboard_bounds(
0 * new_scale_factor, 480 * new_scale_factor, 1200 * new_scale_factor,
320 * new_scale_factor);
instance_->SetOverrideDefaultDeviceScaleFactorForTesting(new_scale_factor);
// Keyboard bounds passed to Android should be changed.
instance_->OnKeyboardAppearanceChanged(desc);
EXPECT_EQ(new_keyboard_bounds, fake_arc_ime_bridge_->last_keyboard_bounds());
EXPECT_TRUE(fake_arc_ime_bridge_->last_keyboard_availability());
}
TEST_F(ArcImeServiceTest, GetCaretBounds) {
EXPECT_EQ(gfx::Rect(), instance_->GetCaretBounds());
const gfx::Rect window_rect(123, 321, 100, 100);
arc_win_->SetBounds(window_rect);
instance_->OnWindowFocused(arc_win_.get(), nullptr);
const gfx::Rect cursor_rect(10, 12, 2, 8);
instance_->OnCursorRectChanged(cursor_rect, true); // screen coordinates
EXPECT_EQ(cursor_rect, instance_->GetCaretBounds());
instance_->OnCursorRectChanged(cursor_rect, false); // window coordinates
EXPECT_EQ(cursor_rect + window_rect.OffsetFromOrigin(),
instance_->GetCaretBounds());
const double new_scale_factor = 10.0;
const gfx::Rect new_cursor_rect(10 * new_scale_factor, 12 * new_scale_factor,
2 * new_scale_factor, 8 * new_scale_factor);
instance_->SetOverrideDefaultDeviceScaleFactorForTesting(new_scale_factor);
instance_->OnCursorRectChanged(new_cursor_rect, true); // screen coordinates
EXPECT_EQ(cursor_rect, instance_->GetCaretBounds());
instance_->OnCursorRectChanged(new_cursor_rect, false); // window coordinates
EXPECT_EQ(cursor_rect + window_rect.OffsetFromOrigin(),
instance_->GetCaretBounds());
}
TEST_F(ArcImeServiceTest, ShouldDoLearning) {
instance_->OnWindowFocused(arc_win_.get(), nullptr);
ASSERT_NE(ui::TEXT_INPUT_TYPE_TEXT, instance_->GetTextInputType());
instance_->OnTextInputTypeChanged(ui::TEXT_INPUT_TYPE_TEXT, true,
mojom::TEXT_INPUT_FLAG_NONE);
EXPECT_TRUE(instance_->ShouldDoLearning());
EXPECT_EQ(1, fake_input_method_->count_on_text_input_type_changed());
instance_->OnTextInputTypeChanged(ui::TEXT_INPUT_TYPE_TEXT, false,
mojom::TEXT_INPUT_FLAG_NONE);
EXPECT_FALSE(instance_->ShouldDoLearning());
EXPECT_EQ(2, fake_input_method_->count_on_text_input_type_changed());
instance_->OnTextInputTypeChanged(ui::TEXT_INPUT_TYPE_URL, false,
mojom::TEXT_INPUT_FLAG_NONE);
EXPECT_FALSE(instance_->ShouldDoLearning());
EXPECT_EQ(3, fake_input_method_->count_on_text_input_type_changed());
}
} // namespace arc