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

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

Powered by Google App Engine
This is Rietveld 408576698