Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(89)

Side by Side Diff: content/public/android/java/src/org/chromium/content/browser/input/ThreadedInputConnection.java

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

Powered by Google App Engine
This is Rietveld 408576698