1 // Copyright 2018 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "chrome/browser/chromeos/arc/input_method_manager/input_connection_impl.h"
6
7 #include <tuple>
8
9 #include "base/bind.h"
10 #include "base/strings/utf_string_conversions.h"
11 #include "base/threading/thread_task_runner_handle.h"
12 #include "ui/base/ime/chromeos/ime_bridge.h"
13 #include "ui/base/ime/chromeos/ime_keymap.h"
14 #include "ui/events/keycodes/dom/keycode_converter.h"
15 #include "ui/events/keycodes/keyboard_code_conversion.h"
16 #include "ui/events/keycodes/keyboard_codes.h"
17
18 namespace arc {
19
20 namespace {
21
22 // Timeout threshold after the IME operation is sent to TextInputClient.
23 // If no text input state observer methods in below ArcProxyInputMethodObserver
24 // is called during this time period, the current text input state is sent to
25 // Android.
26 // TODO(yhanada): Implement a way to observe an IME operation completion and
27 // send the current text input state right after the IME operation completion.
28 constexpr base::TimeDelta kStateUpdateTimeout = base::TimeDelta::FromSeconds(1);
29
30 // Characters which should be sent as a KeyEvent and attributes of generated
31 // KeyEvent.
32 constexpr std::tuple<char, ui::KeyboardCode, const char*>
33 kControlCharToKeyEvent[] = {{'\n', ui::VKEY_RETURN, "Enter"}};
34
IsControlChar(const base::string16 & text)35 bool IsControlChar(const base::string16& text) {
36 const std::string str = base::UTF16ToUTF8(text);
37 if (str.length() != 1)
38 return false;
39 for (const auto& t : kControlCharToKeyEvent) {
40 if (str[0] == std::get<0>(t))
41 return true;
42 }
43 return false;
44 }
45
GetTextInputClient()46 ui::TextInputClient* GetTextInputClient() {
47 ui::IMEBridge* bridge = ui::IMEBridge::Get();
48 DCHECK(bridge);
49 ui::IMEInputContextHandlerInterface* handler =
50 bridge->GetInputContextHandler();
51 if (!handler)
52 return nullptr;
53 ui::TextInputClient* client = handler->GetInputMethod()->GetTextInputClient();
54 DCHECK(client);
55 return client;
56 }
57
58 } // namespace
59
InputConnectionImpl(chromeos::InputMethodEngine * ime_engine,ArcInputMethodManagerBridge * imm_bridge,int input_context_id)60 InputConnectionImpl::InputConnectionImpl(
61 chromeos::InputMethodEngine* ime_engine,
62 ArcInputMethodManagerBridge* imm_bridge,
63 int input_context_id)
64 : ime_engine_(ime_engine),
65 imm_bridge_(imm_bridge),
66 input_context_id_(input_context_id),
67 state_update_timer_() {}
68
69 InputConnectionImpl::~InputConnectionImpl() = default;
70
Bind(mojo::PendingRemote<mojom::InputConnection> * remote)71 void InputConnectionImpl::Bind(
72 mojo::PendingRemote<mojom::InputConnection>* remote) {
73 receiver_.Bind(remote->InitWithNewPipeAndPassReceiver());
74 }
75
UpdateTextInputState(bool is_input_state_update_requested)76 void InputConnectionImpl::UpdateTextInputState(
77 bool is_input_state_update_requested) {
78 if (state_update_timer_.IsRunning()) {
79 // There is a pending request.
80 is_input_state_update_requested = true;
81 }
82 state_update_timer_.Stop();
83 imm_bridge_->SendUpdateTextInputState(
84 GetTextInputState(is_input_state_update_requested));
85 }
86
GetTextInputState(bool is_input_state_update_requested) const87 mojom::TextInputStatePtr InputConnectionImpl::GetTextInputState(
88 bool is_input_state_update_requested) const {
89 ui::TextInputClient* client = GetTextInputClient();
90 gfx::Range text_range = gfx::Range();
91 gfx::Range selection_range = gfx::Range();
92 base::Optional<gfx::Range> composition_text_range = gfx::Range();
93 base::string16 text;
94
95 if (!client) {
96 return mojom::TextInputStatePtr(base::in_place, 0, text, text_range,
97 selection_range, ui::TEXT_INPUT_TYPE_NONE,
98 false, 0, is_input_state_update_requested,
99 composition_text_range);
100 }
101
102 client->GetTextRange(&text_range);
103 client->GetEditableSelectionRange(&selection_range);
104 if (!client->GetCompositionTextRange(&composition_text_range.value()))
105 composition_text_range.reset();
106 client->GetTextFromRange(text_range, &text);
107
108 return mojom::TextInputStatePtr(
109 base::in_place, selection_range.start(), text, text_range,
110 selection_range, client->GetTextInputType(), client->ShouldDoLearning(),
111 client->GetTextInputFlags(), is_input_state_update_requested,
112 composition_text_range);
113 }
114
CommitText(const base::string16 & text,int new_cursor_pos)115 void InputConnectionImpl::CommitText(const base::string16& text,
116 int new_cursor_pos) {
117 StartStateUpdateTimer();
118
119 std::string error;
120 // Clear the current composing text at first.
121 if (!ime_engine_->ClearComposition(input_context_id_, &error))
122 LOG(ERROR) << "ClearComposition failed: error=\"" << error << "\"";
123
124 if (IsControlChar(text)) {
125 SendControlKeyEvent(text);
126 return;
127 }
128
129 if (!ime_engine_->CommitText(input_context_id_,
130 base::UTF16ToUTF8(text).c_str(), &error))
131 LOG(ERROR) << "CommitText failed: error=\"" << error << "\"";
132 }
133
DeleteSurroundingText(int before,int after)134 void InputConnectionImpl::DeleteSurroundingText(int before, int after) {
135 StartStateUpdateTimer();
136
137 if (before == 0 && after == 0) {
138 // This should be no-op.
139 // Return the current state immediately.
140 UpdateTextInputState(true);
141 return;
142 }
143
144 std::string error;
145 // DeleteSurroundingText takes a start position relative to the current cursor
146 // position and a length of the text is going to be deleted.
147 // |before| is a number of characters is going to be deleted before the cursor
148 // and |after| is a number of characters is going to be deleted after the
149 // cursor.
150 if (!ime_engine_->DeleteSurroundingText(input_context_id_, -before,
151 before + after, &error)) {
152 LOG(ERROR) << "DeleteSurroundingText failed: before = " << before
153 << ", after = " << after << ", error = \"" << error << "\"";
154 }
155 }
156
FinishComposingText()157 void InputConnectionImpl::FinishComposingText() {
158 StartStateUpdateTimer();
159
160 ui::TextInputClient* client = GetTextInputClient();
161 if (!client)
162 return;
163 gfx::Range selection_range, composition_range;
164 client->GetEditableSelectionRange(&selection_range);
165 client->GetCompositionTextRange(&composition_range);
166
167 if (composition_range.is_empty()) {
168 // There is no ongoing composing. Do nothing.
169 UpdateTextInputState(true);
170 return;
171 }
172
173 base::string16 composing_text;
174 client->GetTextFromRange(composition_range, &composing_text);
175
176 std::string error;
177 if (!ime_engine_->CommitText(input_context_id_,
178 base::UTF16ToUTF8(composing_text).c_str(),
179 &error)) {
180 LOG(ERROR) << "FinishComposingText: CommitText() failed, error=\"" << error
181 << "\"";
182 }
183
184 if (selection_range.start() == selection_range.end() &&
185 selection_range.start() == composition_range.end()) {
186 // The call of CommitText won't update the state.
187 // Return the current state immediately.
188 UpdateTextInputState(true);
189 }
190 }
191
SetComposingText(const base::string16 & text,int new_cursor_pos,const base::Optional<gfx::Range> & new_selection_range)192 void InputConnectionImpl::SetComposingText(
193 const base::string16& text,
194 int new_cursor_pos,
195 const base::Optional<gfx::Range>& new_selection_range) {
196 // It's relative to the last character of the composing text,
197 // so 0 means the cursor should be just before the last character of the text.
198 new_cursor_pos += text.length() - 1;
199
200 StartStateUpdateTimer();
201
202 const int selection_start = new_selection_range
203 ? new_selection_range.value().start()
204 : new_cursor_pos;
205 const int selection_end =
206 new_selection_range ? new_selection_range.value().end() : new_cursor_pos;
207
208 ui::TextInputClient* client = GetTextInputClient();
209 if (!client)
210 return;
211
212 gfx::Range selection_range;
213 client->GetEditableSelectionRange(&selection_range);
214 if (text.empty() &&
215 selection_range.start() == static_cast<uint32_t>(selection_start) &&
216 selection_range.end() == static_cast<uint32_t>(selection_end)) {
217 // This SetComposingText call is no-op.
218 // Return the current state immediately.
219 UpdateTextInputState(true);
220 }
221
222 std::string error;
223 if (!ime_engine_->SetComposition(
224 input_context_id_, base::UTF16ToUTF8(text).c_str(), selection_start,
225 selection_end, new_cursor_pos,
226 std::vector<chromeos::InputMethodEngineBase::SegmentInfo>(),
227 &error)) {
228 LOG(ERROR) << "SetComposingText failed: pos=" << new_cursor_pos
229 << ", error=\"" << error << "\"";
230 return;
231 }
232 }
233
RequestTextInputState(mojom::InputConnection::RequestTextInputStateCallback callback)234 void InputConnectionImpl::RequestTextInputState(
235 mojom::InputConnection::RequestTextInputStateCallback callback) {
236 std::move(callback).Run(GetTextInputState(false));
237 }
238
SetSelection(const gfx::Range & new_selection_range)239 void InputConnectionImpl::SetSelection(const gfx::Range& new_selection_range) {
240 ui::TextInputClient* client = GetTextInputClient();
241 if (!client)
242 return;
243
244 gfx::Range selection_range;
245 client->GetEditableSelectionRange(&selection_range);
246 if (new_selection_range == selection_range) {
247 // This SetSelection call is no-op.
248 // Return the current state immediately.
249 UpdateTextInputState(true);
250 }
251
252 StartStateUpdateTimer();
253 client->SetEditableSelectionRange(new_selection_range);
254 }
255
SendKeyEvent(std::unique_ptr<ui::KeyEvent> key_event)256 void InputConnectionImpl::SendKeyEvent(
257 std::unique_ptr<ui::KeyEvent> key_event) {
258 DCHECK(key_event);
259 chromeos::InputMethodEngine::KeyboardEvent event;
260 if (key_event->type() == ui::ET_KEY_PRESSED)
261 event.type = "keydown";
262 else
263 event.type = "keyup";
264
265 event.key = key_event->GetCodeString();
266 event.code = ui::KeyboardCodeToDomKeycode(key_event->key_code());
267 event.key_code = key_event->key_code();
268 event.alt_key = key_event->IsAltDown();
269 event.ctrl_key = key_event->IsControlDown();
270 event.shift_key = key_event->IsShiftDown();
271 event.caps_lock = key_event->IsCapsLockOn();
272
273 std::string error;
274 if (!ime_engine_->SendKeyEvents(input_context_id_, {event}, &error)) {
275 LOG(ERROR) << error;
276 }
277 }
278
SetCompositionRange(const gfx::Range & new_composition_range)279 void InputConnectionImpl::SetCompositionRange(
280 const gfx::Range& new_composition_range) {
281 ui::TextInputClient* client = GetTextInputClient();
282 if (!client)
283 return;
284
285 gfx::Range selection_range = gfx::Range();
286 if (!client->GetEditableSelectionRange(&selection_range)) {
287 LOG(ERROR)
288 << "SetCompositionRange failed: Editable text field is not focused";
289 return;
290 }
291
292 StartStateUpdateTimer();
293
294 const int before = selection_range.start() - new_composition_range.start();
295 const int after = new_composition_range.end() - selection_range.end();
296 chromeos::InputMethodEngineBase::SegmentInfo segment_info;
297 segment_info.start = 0;
298 segment_info.end = new_composition_range.length();
299 segment_info.style = chromeos::InputMethodEngineBase::SEGMENT_STYLE_UNDERLINE;
300
301 std::string error;
302 if (!ime_engine_->chromeos::InputMethodEngineBase::SetCompositionRange(
303 input_context_id_, before, after, {segment_info}, &error)) {
304 LOG(ERROR) << "SetCompositionRange failed: range="
305 << new_composition_range.ToString() << ", error=\"" << error
306 << "\"";
307 }
308 }
309
StartStateUpdateTimer()310 void InputConnectionImpl::StartStateUpdateTimer() {
311 // It's safe to use Unretained() here because the timer is automatically
312 // canceled when it go out of scope.
313 state_update_timer_.Start(
314 FROM_HERE, kStateUpdateTimeout,
315 base::BindOnce(&InputConnectionImpl::UpdateTextInputState,
316 base::Unretained(this),
317 true /* is_input_state_update_requested */));
318 }
319
SendControlKeyEvent(const base::string16 & text)320 void InputConnectionImpl::SendControlKeyEvent(const base::string16& text) {
321 DCHECK(IsControlChar(text));
322
323 const std::string str = base::UTF16ToUTF8(text);
324 DCHECK_EQ(1u, str.length());
325
326 for (const auto& t : kControlCharToKeyEvent) {
327 if (std::get<0>(t) == str[0]) {
328 chromeos::InputMethodEngine::KeyboardEvent press;
329 press.type = "keydown";
330 press.key_code = std::get<1>(t);
331 press.key = press.code = std::get<2>(t);
332 chromeos::InputMethodEngine::KeyboardEvent release(press);
333 release.type = "keyup";
334 std::string error;
335 if (!ime_engine_->SendKeyEvents(input_context_id_, {press, release},
336 &error)) {
337 LOG(ERROR) << error;
338 }
339 break;
340 }
341 }
342 return;
343 }
344
345 } // namespace arc
346