OLD | NEW |
---|---|
(Empty) | |
1 // Copyright 2016 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 package org.chromium.content.browser.input; | |
6 | |
7 import android.os.Bundle; | |
8 import android.view.KeyCharacterMap; | |
9 import android.view.KeyEvent; | |
10 import android.view.inputmethod.CompletionInfo; | |
11 import android.view.inputmethod.CorrectionInfo; | |
12 import android.view.inputmethod.EditorInfo; | |
13 import android.view.inputmethod.ExtractedText; | |
14 import android.view.inputmethod.ExtractedTextRequest; | |
15 import android.view.inputmethod.InputConnection; | |
16 | |
17 import org.chromium.base.Log; | |
18 import org.chromium.base.ThreadUtils; | |
19 | |
20 import java.util.concurrent.BlockingQueue; | |
21 import java.util.concurrent.LinkedBlockingQueue; | |
22 | |
23 /** | |
24 * An implementation of {@link InputConnection} to communicate with external inp ut method | |
25 * apps. Note that it is running on IME thread (except for constructor and calls from ImeAdapter) | |
26 * such that it does not block UI thread and returns text values immediately aft er any change | |
27 * to them. | |
28 */ | |
29 public class ThreadedInputConnection implements ChromiumBaseInputConnection { | |
30 private static final String TAG = "cr_Ime"; | |
31 | |
32 private final ImeAdapter mImeAdapter; | |
33 private final ThreadManager mThreadManager; | |
34 private int mNumNestedBatchEdits = 0; | |
Ted C
2016/02/02 23:14:43
zero is the default...no need for this.
Changwan Ryu
2016/02/11 16:21:08
Done.
| |
35 private final BlockingQueue<TextInputState> mQueue = new LinkedBlockingQueue <>(); | |
Ted C
2016/02/02 23:14:43
do we get a new TextInputState for every key strok
Changwan Ryu
2016/02/11 16:21:08
Hmm... I noticed that android.os.Message is using
| |
36 private int mPendingAccent; | |
37 // Only accessible on UI thread. | |
38 private TextInputState mLastUpdatedTextInputState; | |
39 | |
40 | |
41 ThreadedInputConnection(ImeAdapter imeAdapter, ThreadManager threadManager) { | |
42 Log.d(TAG, "constructor"); | |
43 ImeUtils.assertOnUiThread(); | |
44 mImeAdapter = imeAdapter; | |
45 mThreadManager = threadManager; | |
46 } | |
47 | |
48 void initializeOutAttrsOnUiThread(int inputType, int inputFlags, EditorInfo outAttrs) { | |
49 Log.d(TAG, "initializeOutAttrs"); | |
50 ImeUtils.assertOnUiThread(); | |
51 int initialSelStart = 0; | |
52 int initialSelEnd = 0; | |
53 mNumNestedBatchEdits = 0; | |
54 mPendingAccent = 0; | |
55 if (mLastUpdatedTextInputState != null) { | |
56 initialSelStart = mLastUpdatedTextInputState.selection().start(); | |
57 initialSelEnd = mLastUpdatedTextInputState.selection().end(); | |
58 } | |
59 ImeUtils.computeEditorInfo(inputType, inputFlags, initialSelStart, initi alSelEnd, outAttrs); | |
60 } | |
61 | |
62 @Override | |
63 public void updateStateOnUiThread(final String text, final int selectionStar t, | |
64 final int selectionEnd, final int compositionStart, final int compos itionEnd, | |
65 boolean singleLine, final boolean isNonImeChange) { | |
66 ImeUtils.assertOnUiThread(); | |
67 | |
68 final TextInputState newState = | |
69 new TextInputState(text, new Range(selectionStart, selectionEnd) , | |
70 new Range(compositionStart, compositionEnd), singleLine, !isNonImeChange); | |
71 Log.d(TAG, "updateState: %s", newState); | |
72 mLastUpdatedTextInputState = newState; | |
73 | |
74 addToQueueOnUiThread(newState); | |
75 if (isNonImeChange) { | |
76 mThreadManager.post(new Runnable() { | |
77 @Override | |
78 public void run() { | |
79 checkQueueForNonImeUpdate(); | |
80 } | |
81 }); | |
82 } | |
83 } | |
84 | |
85 /** | |
86 * @see ChromiumBaseInputConnection#getThreadManager() | |
87 */ | |
88 @Override | |
89 public ThreadManager getThreadManager() { | |
90 return mThreadManager; | |
91 } | |
92 | |
93 /** | |
94 * @see ChromiumBaseInputConnection#onRestartInputOnUiThread() | |
95 */ | |
96 @Override | |
97 public void onRestartInputOnUiThread() {} | |
98 | |
99 /** | |
100 * @see ChromiumBaseInputConnection#sendKeyEventOnUiThread(KeyEvent) | |
101 */ | |
102 @Override | |
103 public boolean sendKeyEventOnUiThread(final KeyEvent event) { | |
104 ImeUtils.assertOnUiThread(); | |
105 mThreadManager.post(new Runnable() { | |
106 @Override | |
107 public void run() { | |
108 sendKeyEvent(event); | |
109 } | |
110 }); | |
111 return true; | |
112 } | |
113 | |
114 /** | |
115 * @see ChromiumBaseInputConnection#moveCursorToSelectionEndOnUiThread() | |
116 */ | |
117 @Override | |
118 public void moveCursorToSelectionEndOnUiThread() { | |
119 mThreadManager.post(new Runnable() { | |
120 @Override | |
121 public void run() { | |
122 TextInputState textInputState = requestTextInputState(); | |
123 if (textInputState == null) return; | |
124 Range selection = textInputState.selection(); | |
125 setSelection(selection.end(), selection.end()); | |
126 } | |
127 }); | |
128 } | |
129 | |
130 @Override | |
131 public void unblockOnUiThread() { | |
132 Log.d(TAG, "unblock"); | |
133 ImeUtils.assertOnUiThread(); | |
134 addToQueueOnUiThread(new TextInputState.Unblocker()); | |
135 mThreadManager.post(new Runnable() { | |
136 @Override | |
137 public void run() { | |
138 checkQueueForUnblocker(); | |
139 } | |
140 }); | |
141 } | |
142 | |
143 private void checkQueueForNonImeUpdate() { | |
144 assertOnImeThread(); | |
145 TextInputState state = mQueue.peek(); | |
146 if (state == null) { | |
147 Log.d(TAG, "checkQueueForNonImeUpdate: no state"); | |
148 return; | |
149 } | |
150 mQueue.poll(); | |
151 ImeUtils.assertReally(!state.fromIme()); | |
152 updateSelection(state); | |
153 Log.d(TAG, "checkQueueForNonImeUpdate done: %d", mQueue.size()); | |
154 } | |
155 | |
156 private void checkQueueForUnblocker() { | |
157 Log.d(TAG, "checkQueueForUnblocker"); | |
158 assertOnImeThread(); | |
159 // Clear all the states in the queue. | |
160 while (true) { | |
161 TextInputState state = mQueue.poll(); | |
162 if (state == null) return; | |
163 if (state.shouldUnblock()) return; | |
164 } | |
165 } | |
166 | |
167 private void updateSelection(TextInputState textInputState) { | |
168 assertOnImeThread(); | |
169 if (mNumNestedBatchEdits != 0) return; | |
170 ImeUtils.assertReally(textInputState != null); | |
171 Range selection = textInputState.selection(); | |
172 Range composition = textInputState.composition(); | |
173 mImeAdapter.updateSelection( | |
174 selection.start(), selection.end(), composition.start(), composi tion.end()); | |
175 } | |
176 | |
177 private TextInputState requestTextInputState() { | |
178 Log.d(TAG, "requestTextInputState"); | |
179 ThreadUtils.postOnUiThread(new Runnable() { | |
180 @Override | |
181 public void run() { | |
182 boolean result = mImeAdapter.requestTextInputStateUpdate(); | |
183 if (!result) unblockOnUiThread(); | |
184 } | |
185 }); | |
186 return blockAndGetStateUpdate(); | |
187 } | |
188 | |
189 private void addToQueueOnUiThread(TextInputState textInputState) { | |
190 ImeUtils.assertOnUiThread(); | |
191 try { | |
192 mQueue.put(textInputState); | |
193 } catch (InterruptedException e) { | |
194 Log.e(TAG, "addToQueueOnUiThread interrupted", e); | |
195 } | |
196 Log.d(TAG, "addToQueueOnUiThread finished: %d", mQueue.size()); | |
197 } | |
198 | |
199 /** | |
200 * @return BlockingQueue for white box unit testing. | |
201 */ | |
202 BlockingQueue<TextInputState> getQueueForTest() { | |
203 return mQueue; | |
204 } | |
205 | |
206 private void assertOnImeThread() { | |
207 ImeUtils.assertReally(mThreadManager.runningOnThisThread()); | |
208 } | |
209 | |
210 /** | |
211 * Block until we get the expected state update. | |
212 * @return TextInputState if we get it successfully. null otherwise. | |
213 */ | |
214 private TextInputState blockAndGetStateUpdate() { | |
215 Log.d(TAG, "blockAndGetStateUpdate"); | |
216 assertOnImeThread(); | |
217 boolean shouldUpdateSelection = false; | |
218 while (true) { | |
219 TextInputState state; | |
220 try { | |
221 state = mQueue.take(); | |
222 } catch (InterruptedException e) { | |
223 e.printStackTrace(); | |
224 return null; | |
225 } | |
226 if (state == null) return null; | |
227 if (state.fromIme()) { | |
228 if (state.shouldUnblock()) { | |
229 Log.d(TAG, "blockAndGetStateUpdate: unblocked"); | |
230 return null; | |
231 } | |
232 if (shouldUpdateSelection) updateSelection(state); | |
233 Log.d(TAG, "blockAndGetStateUpdate done: %d", mQueue.size()); | |
234 return state; | |
235 } | |
236 // Ignore when state is not from IME, but make sure to update state when we handle | |
237 // state from IME. | |
238 shouldUpdateSelection = true; | |
239 } | |
240 } | |
241 | |
242 private void notifyUserAction() { | |
243 ThreadUtils.postOnUiThread(new Runnable() { | |
244 @Override | |
245 public void run() { | |
246 mImeAdapter.notifyUserAction(); | |
247 } | |
248 }); | |
249 } | |
250 | |
251 /** | |
252 * @see InputConnection#setComposingText(java.lang.CharSequence, int) | |
253 */ | |
254 @Override | |
255 public boolean setComposingText(final CharSequence text, final int newCursor Position) { | |
256 Log.d(TAG, "setComposingText [%s] [%d]", text, newCursorPosition); | |
257 assertOnImeThread(); | |
258 cancelCombiningAccent(); | |
259 ThreadUtils.postOnUiThread(new Runnable() { | |
260 @Override | |
261 public void run() { | |
262 mImeAdapter.sendCompositionToNative(text, newCursorPosition, fal se); | |
263 } | |
264 }); | |
265 notifyUserAction(); | |
266 return true; | |
267 } | |
268 | |
269 /** | |
270 * @see InputConnection#commitText(java.lang.CharSequence, int) | |
271 */ | |
272 @Override | |
273 public boolean commitText(final CharSequence text, final int newCursorPositi on) { | |
274 Log.d(TAG, "commitText [%s] [%d]", text, newCursorPosition); | |
275 assertOnImeThread(); | |
276 cancelCombiningAccent(); | |
277 ThreadUtils.postOnUiThread(new Runnable() { | |
278 @Override | |
279 public void run() { | |
280 mImeAdapter.sendCompositionToNative(text, newCursorPosition, tex t.length() > 0); | |
281 } | |
282 }); | |
283 notifyUserAction(); | |
284 return true; | |
285 } | |
286 | |
287 /** | |
288 * @see InputConnection#performEditorAction(int) | |
289 */ | |
290 @Override | |
291 public boolean performEditorAction(final int actionCode) { | |
292 Log.d(TAG, "performEditorAction [%d]", actionCode); | |
293 assertOnImeThread(); | |
294 ThreadUtils.postOnUiThread(new Runnable() { | |
295 @Override | |
296 public void run() { | |
297 mImeAdapter.performEditorAction(actionCode); | |
298 } | |
299 }); | |
300 return true; | |
301 } | |
302 | |
303 /** | |
304 * @see InputConnection#performContextMenuAction(int) | |
305 */ | |
306 @Override | |
307 public boolean performContextMenuAction(final int id) { | |
308 Log.d(TAG, "performContextMenuAction [%d]", id); | |
309 assertOnImeThread(); | |
310 ThreadUtils.postOnUiThread(new Runnable() { | |
311 @Override | |
312 public void run() { | |
313 mImeAdapter.performContextMenuAction(id); | |
314 } | |
315 }); | |
316 return true; | |
317 } | |
318 | |
319 /** | |
320 * @see InputConnection#getExtractedText(android.view.inputmethod.ExtractedT extRequest, | |
321 * int) | |
322 */ | |
323 @Override | |
324 public ExtractedText getExtractedText(ExtractedTextRequest request, int flag s) { | |
325 Log.d(TAG, "getExtractedText"); | |
326 assertOnImeThread(); | |
327 TextInputState textInputState = requestTextInputState(); | |
328 if (textInputState == null) return null; | |
329 ExtractedText extractedText = new ExtractedText(); | |
330 extractedText.text = textInputState.text(); | |
331 extractedText.partialEndOffset = textInputState.text().length(); | |
332 extractedText.selectionStart = textInputState.selection().start(); | |
333 extractedText.selectionEnd = textInputState.selection().end(); | |
334 extractedText.flags = textInputState.singleLine() ? ExtractedText.FLAG_S INGLE_LINE : 0; | |
335 return extractedText; | |
336 } | |
337 | |
338 /** | |
339 * @see InputConnection#beginBatchEdit() | |
340 */ | |
341 @Override | |
342 public boolean beginBatchEdit() { | |
343 Log.d(TAG, "beginBatchEdit [%b]", (mNumNestedBatchEdits == 0)); | |
344 assertOnImeThread(); | |
345 mNumNestedBatchEdits++; | |
346 return true; | |
347 } | |
348 | |
349 /** | |
350 * @see InputConnection#endBatchEdit() | |
351 */ | |
352 @Override | |
353 public boolean endBatchEdit() { | |
354 assertOnImeThread(); | |
355 if (mNumNestedBatchEdits == 0) return false; | |
356 --mNumNestedBatchEdits; | |
357 Log.d(TAG, "endBatchEdit [%b]", (mNumNestedBatchEdits == 0)); | |
358 if (mNumNestedBatchEdits == 0) { | |
359 updateSelection(requestTextInputState()); | |
360 } | |
361 return mNumNestedBatchEdits != 0; | |
362 } | |
363 | |
364 /** | |
365 * @see InputConnection#deleteSurroundingText(int, int) | |
366 */ | |
367 @Override | |
368 public boolean deleteSurroundingText(final int beforeLength, final int after Length) { | |
369 Log.d(TAG, "deleteSurroundingText [%d %d]", beforeLength, afterLength); | |
370 assertOnImeThread(); | |
371 if (mPendingAccent != 0) { | |
372 finishComposingText(); | |
373 } | |
374 ThreadUtils.postOnUiThread(new Runnable() { | |
375 @Override | |
376 public void run() { | |
377 mImeAdapter.deleteSurroundingText(beforeLength, afterLength); | |
378 } | |
379 }); | |
380 return true; | |
381 } | |
382 | |
383 /** | |
384 * @see InputConnection#sendKeyEvent(android.view.KeyEvent) | |
385 */ | |
386 @Override | |
387 public boolean sendKeyEvent(final KeyEvent event) { | |
388 Log.d(TAG, "sendKeyEvent [%d %d]", event.getAction(), event.getKeyCode() ); | |
389 assertOnImeThread(); | |
390 | |
391 if (handleCombiningAccent(event)) return true; | |
392 | |
393 ThreadUtils.postOnUiThread(new Runnable() { | |
394 @Override | |
395 public void run() { | |
396 mImeAdapter.sendKeyEvent(event); | |
397 } | |
398 }); | |
399 notifyUserAction(); | |
400 return true; | |
401 } | |
402 | |
403 private boolean handleCombiningAccent(final KeyEvent event) { | |
404 // TODO(changwan): this will break the current composition. check if we can | |
405 // implement it in the renderer instead. | |
406 int action = event.getAction(); | |
407 int unicodeChar = event.getUnicodeChar(); | |
408 | |
409 if (action != KeyEvent.ACTION_DOWN) return false; | |
410 if ((unicodeChar & KeyCharacterMap.COMBINING_ACCENT) != 0) { | |
411 int pendingAccent = unicodeChar & KeyCharacterMap.COMBINING_ACCENT_M ASK; | |
412 StringBuilder builder = new StringBuilder(); | |
413 builder.appendCodePoint(pendingAccent); | |
414 setComposingText(builder.toString(), 1); | |
415 mPendingAccent = pendingAccent; | |
416 return true; | |
417 } else if (mPendingAccent != 0 && unicodeChar != 0) { | |
418 int combined = KeyEvent.getDeadChar(mPendingAccent, unicodeChar); | |
419 if (combined != 0) { | |
420 StringBuilder builder = new StringBuilder(); | |
421 builder.appendCodePoint(combined); | |
422 commitText(builder.toString(), 1); | |
423 return true; | |
424 } | |
425 // Noncombinable character; commit the accent character and fall thr ough to sending | |
426 // the key event for the character afterwards. | |
427 finishComposingText(); | |
428 } | |
429 return false; | |
430 } | |
431 | |
432 private void cancelCombiningAccent() { | |
433 mPendingAccent = 0; | |
434 } | |
435 | |
436 /** | |
437 * @see InputConnection#finishComposingText() | |
438 */ | |
439 @Override | |
440 public boolean finishComposingText() { | |
441 Log.d(TAG, "finishComposingText"); | |
442 cancelCombiningAccent(); | |
443 // This is the only function that may be called on UI thread because | |
444 // of direct calls from InputMethodManager. | |
445 ThreadUtils.postOnUiThread(new Runnable() { | |
446 @Override | |
447 public void run() { | |
448 mImeAdapter.finishComposingText(); | |
449 } | |
450 }); | |
451 return true; | |
452 } | |
453 | |
454 /** | |
455 * @see InputConnection#setSelection(int, int) | |
456 */ | |
457 @Override | |
458 public boolean setSelection(final int start, final int end) { | |
459 Log.d(TAG, "setSelection [%d %d]", start, end); | |
460 assertOnImeThread(); | |
461 ThreadUtils.postOnUiThread(new Runnable() { | |
462 @Override | |
463 public void run() { | |
464 mImeAdapter.setEditableSelectionOffsets(start, end); | |
465 } | |
466 }); | |
467 return true; | |
468 } | |
469 | |
470 /** | |
471 * @see InputConnection#setComposingRegion(int, int) | |
472 */ | |
473 @Override | |
474 public boolean setComposingRegion(final int start, final int end) { | |
475 Log.d(TAG, "setComposingRegion [%d %d]", start, end); | |
476 assertOnImeThread(); | |
477 ThreadUtils.postOnUiThread(new Runnable() { | |
478 @Override | |
479 public void run() { | |
480 mImeAdapter.setComposingRegion(start, end); | |
481 } | |
482 }); | |
483 return true; | |
484 } | |
485 | |
486 /** | |
487 * @see InputConnection#getTextBeforeCursor(int, int) | |
488 */ | |
489 @Override | |
490 public CharSequence getTextBeforeCursor(int maxChars, int flags) { | |
491 Log.d(TAG, "getTextBeforeCursor [%d %x]", maxChars, flags); | |
492 assertOnImeThread(); | |
493 TextInputState textInputState = requestTextInputState(); | |
494 if (textInputState == null) return null; | |
495 return textInputState.getTextBeforeSelection(maxChars); | |
496 } | |
497 | |
498 /** | |
499 * @see InputConnection#getTextAfterCursor(int, int) | |
500 */ | |
501 @Override | |
502 public CharSequence getTextAfterCursor(int maxChars, int flags) { | |
503 Log.d(TAG, "getTextAfterCursor [%d %x]", maxChars, flags); | |
504 assertOnImeThread(); | |
505 TextInputState textInputState = requestTextInputState(); | |
506 if (textInputState == null) return null; | |
507 return textInputState.getTextAfterSelection(maxChars); | |
508 } | |
509 | |
510 /** | |
511 * @see InputConnection#getSelectedText(int) | |
512 */ | |
513 @Override | |
514 public CharSequence getSelectedText(int flags) { | |
515 Log.d(TAG, "getSelectedText [%x]", flags); | |
516 assertOnImeThread(); | |
517 TextInputState textInputState = requestTextInputState(); | |
518 if (textInputState == null) return null; | |
519 return textInputState.getSelectedText(); | |
520 } | |
521 | |
522 /** | |
523 * @see InputConnection#getCursorCapsMode(int) | |
524 */ | |
525 @Override | |
526 public int getCursorCapsMode(int reqModes) { | |
527 Log.d(TAG, "getCursorCapsMode [%x]", reqModes); | |
528 assertOnImeThread(); | |
529 // TODO(changwan): Auto-generated method stub | |
530 return 0; | |
531 } | |
532 | |
533 /** | |
534 * @see InputConnection#commitCompletion(android.view.inputmethod.Completion Info) | |
535 */ | |
536 @Override | |
537 public boolean commitCompletion(CompletionInfo text) { | |
538 Log.d(TAG, "commitCompletion [%s]", text); | |
539 assertOnImeThread(); | |
540 // TODO(changwan): Auto-generated method stub | |
541 return false; | |
542 } | |
543 | |
544 /** | |
545 * @see InputConnection#commitCorrection(android.view.inputmethod.Correction Info) | |
546 */ | |
547 @Override | |
548 public boolean commitCorrection(CorrectionInfo correctionInfo) { | |
549 Log.d(TAG, "commitCorrection [%s]", ImeUtils.dumpCorrectionInfo(correcti onInfo)); | |
550 assertOnImeThread(); | |
551 // TODO(changwan): Auto-generated method stub | |
552 return false; | |
553 } | |
554 | |
555 /** | |
556 * @see InputConnection#clearMetaKeyStates(int) | |
557 */ | |
558 @Override | |
559 public boolean clearMetaKeyStates(int states) { | |
560 Log.d(TAG, "clearMetaKeyStates [%x]", states); | |
561 assertOnImeThread(); | |
562 // TODO(changwan): Auto-generated method stub | |
563 return false; | |
564 } | |
565 | |
566 /** | |
567 * @see InputConnection#reportFullscreenMode(boolean) | |
568 */ | |
569 @Override | |
570 public boolean reportFullscreenMode(boolean enabled) { | |
571 Log.d(TAG, "reportFullscreenMode [%b]", enabled); | |
572 // We ignore fullscreen mode for now. That's why we set | |
573 // EditorInfo.IME_FLAG_NO_FULLSCREEN in constructor. | |
574 // Note that this may be called on UI thread. | |
575 return false; | |
576 } | |
577 | |
578 /** | |
579 * @see InputConnection#performPrivateCommand(java.lang.String, android.os.B undle) | |
580 */ | |
581 @Override | |
582 public boolean performPrivateCommand(String action, Bundle data) { | |
583 Log.d(TAG, "performPrivateCommand [%s]", action); | |
584 assertOnImeThread(); | |
585 // TODO(changwan): Auto-generated method stub | |
586 return false; | |
587 } | |
588 | |
589 /** | |
590 * @see InputConnection#requestCursorUpdates(int) | |
591 */ | |
592 @Override | |
593 public boolean requestCursorUpdates(int cursorUpdateMode) { | |
594 Log.d(TAG, "requestCursorUpdates [%x]", cursorUpdateMode); | |
595 assertOnImeThread(); | |
596 // TODO(changwan): Auto-generated method stub | |
597 return false; | |
598 } | |
599 } | |
OLD | NEW |