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