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.os.Handler; | |
9 import android.os.Looper; | |
10 import android.view.KeyEvent; | |
11 import android.view.inputmethod.CompletionInfo; | |
12 import android.view.inputmethod.CorrectionInfo; | |
13 import android.view.inputmethod.EditorInfo; | |
14 import android.view.inputmethod.ExtractedText; | |
15 import android.view.inputmethod.ExtractedTextRequest; | |
16 import android.view.inputmethod.InputConnection; | |
17 | |
18 import org.chromium.base.Log; | |
19 import org.chromium.base.ThreadUtils; | |
20 | |
21 import java.util.concurrent.BlockingQueue; | |
22 import java.util.concurrent.Callable; | |
23 import java.util.concurrent.ExecutionException; | |
24 import java.util.concurrent.LinkedBlockingQueue; | |
25 | |
26 /** | |
27 * Chromium's implementation of {@link InputConnection} to communicate with exte rnal input method | |
28 * apps. | |
29 */ | |
30 public class ChromiumInputConnection implements ChromiumBaseInputConnection { | |
31 private static final String TAG = "cr_Ime"; | |
32 | |
33 static class ThreadManager implements ChromiumBaseInputConnection.ThreadMana ger { | |
34 private final Handler mHandler; | |
35 | |
36 public ThreadManager(Handler handler) { | |
37 mHandler = handler; | |
38 } | |
39 | |
40 public Handler getHandler() { | |
41 return mHandler; | |
42 } | |
43 | |
44 @Override | |
45 public boolean runningOnThisThread() { | |
46 return getHandler().getLooper() == Looper.myLooper(); | |
47 } | |
48 | |
49 @Override | |
50 public void post(Runnable runnable) { | |
51 getHandler().post(runnable); | |
52 } | |
53 } | |
54 | |
55 private final ImeAdapter mImeAdapter; | |
56 private final ThreadManager mThreadManager; | |
57 | |
58 private int mNumNestedBatchEdits = 0; | |
59 | |
60 private final BlockingQueue<TextInputState> mQueue = new LinkedBlockingQueue <>(); | |
61 private TextInputState mBatchEditPendingState; | |
62 // Only accessible on UI thread. | |
63 private TextInputState mLastUpdatedTextInputState; | |
64 | |
65 ChromiumInputConnection(ImeAdapter imeAdapter, ThreadManager threadManager) { | |
66 Log.d(TAG, "constructor"); | |
67 ImeUtils.assertOnUiThread(); | |
68 mImeAdapter = imeAdapter; | |
69 mThreadManager = threadManager; | |
70 } | |
71 | |
72 void initializeOutAttrsOnUiThread(int inputType, int inputFlags, EditorInfo outAttrs) { | |
73 Log.d(TAG, "initialize"); | |
74 ImeUtils.assertOnUiThread(); | |
75 int initialSelStart = 0; | |
76 int initialSelEnd = 0; | |
77 if (mLastUpdatedTextInputState != null) { | |
78 initialSelStart = mLastUpdatedTextInputState.selection().start(); | |
79 initialSelEnd = mLastUpdatedTextInputState.selection().end(); | |
80 } | |
81 ImeUtils.computeEditorInfo(inputType, inputFlags, initialSelStart, initi alSelEnd, outAttrs); | |
82 } | |
83 | |
84 @Override | |
85 public void updateStateOnUiThread(final String text, final int selectionStar t, | |
86 final int selectionEnd, final int compositionStart, final int compos itionEnd, | |
87 boolean singleLine, final boolean isNonImeChange) { | |
88 Log.d(TAG, "updateState [%s] [%s %s] [%s %s] [%b] [%b]", text, selection Start, selectionEnd, | |
89 compositionStart, compositionEnd, singleLine, isNonImeChange); | |
90 ImeUtils.assertOnUiThread(); | |
91 | |
92 final TextInputState newState = | |
93 new TextInputState(text, new Range(selectionStart, selectionEnd) , | |
94 new Range(compositionStart, compositionEnd), singleLine) ; | |
95 mLastUpdatedTextInputState = newState; | |
96 | |
97 if (!isNonImeChange) { | |
98 addToQueueOnUiThread(newState); | |
99 } | |
100 | |
101 mThreadManager.post(new Runnable() { | |
aelias_OOO_until_Jul13
2016/01/23 03:21:39
By removing the "else" around this and removing th
Changwan Ryu
2016/01/25 09:07:27
The problem was as follows:
1) IME-originating st
aelias_OOO_until_Jul13
2016/01/26 00:33:05
Got it. Sorry about not considering that when I p
Changwan Ryu
2016/01/26 03:25:28
Thanks, that's what I was considering, too. It was
| |
102 @Override | |
103 public void run() { | |
104 updateSelection(newState); | |
105 } | |
106 }); | |
107 Log.d(TAG, "updateState finished"); | |
108 } | |
109 | |
110 @Override | |
111 public ThreadManager getThreadManager() { | |
112 return mThreadManager; | |
113 } | |
114 | |
115 @Override | |
116 public void onRestartInputOnUiThread() {} | |
117 | |
118 private void updateSelection(TextInputState textInputState) { | |
119 assertOnImeThread(); | |
120 if (mNumNestedBatchEdits != 0) { | |
121 mBatchEditPendingState = textInputState; | |
122 return; | |
123 } | |
124 ImeUtils.assertReally(textInputState != null); | |
125 Range selection = textInputState.selection(); | |
126 Range composition = textInputState.composition(); | |
127 mImeAdapter.updateSelection( | |
128 selection.start(), selection.end(), composition.start(), composi tion.end()); | |
129 } | |
130 | |
131 private TextInputState requestTextInputState() { | |
132 Log.d(TAG, "requestTextInputState"); | |
133 ThreadUtils.postOnUiThread(new Runnable() { | |
134 @Override | |
135 public void run() { | |
136 boolean result = mImeAdapter.requestTextInputStateUpdate(); | |
137 if (!result) unblock(); | |
138 } | |
139 }); | |
140 return blockAndGetStateUpdate(); | |
141 } | |
142 | |
143 private void addToQueueOnUiThread(TextInputState textInputState) { | |
144 ImeUtils.assertOnUiThread(); | |
145 try { | |
146 mQueue.put(textInputState); | |
147 } catch (InterruptedException e) { | |
148 Log.e(TAG, "onImeOriginatingStateUpdateOnUiThread interrupted", e); | |
149 } | |
150 Log.d(TAG, "onImeOriginatingStateUpdateOnUiThread finished: %d", mQueue. size()); | |
151 } | |
152 | |
153 /** | |
154 * Unblock the blockAndGetStateUpdate() function if we found that we will | |
155 * never get state update. | |
156 */ | |
157 private void unblock() { | |
158 ImeUtils.assertOnUiThread(); | |
159 addToQueueOnUiThread(new TextInputState.Unblocker()); | |
160 } | |
161 | |
162 private void assertOnImeThread() { | |
163 ImeUtils.assertReally(mThreadManager.runningOnThisThread()); | |
164 } | |
165 | |
166 /** | |
167 * Block until we get the expected state update. | |
168 * @return TextInputState if we get it successfully. null otherwise. | |
169 */ | |
170 private TextInputState blockAndGetStateUpdate() { | |
171 assertOnImeThread(); | |
172 while (true) { | |
173 TextInputState result = mQueue.poll(); | |
174 if (result != null) { | |
175 if (result.shouldUnblock()) { | |
176 Log.d(TAG, "blockAndGetStateUpdate: unblocked"); | |
177 return null; | |
178 } | |
179 Log.d(TAG, "blockAndGetStateUpdate finished: %s", result.toStrin g()); | |
180 return result; | |
181 } | |
182 } | |
183 } | |
184 | |
185 /** | |
186 * @see InputConnection#setComposingText(java.lang.CharSequence, int) | |
187 */ | |
188 @Override | |
189 public boolean setComposingText(final CharSequence text, final int newCursor Position) { | |
190 Log.d(TAG, "setComposingText [%s] [%d]", text, newCursorPosition); | |
191 assertOnImeThread(); | |
192 ThreadUtils.postOnUiThread(new Runnable() { | |
193 @Override | |
194 public void run() { | |
195 mImeAdapter.sendCompositionToNative(text, newCursorPosition, fal se); | |
196 } | |
197 }); | |
198 | |
199 return requestTextInputState() != null; | |
200 } | |
201 | |
202 /** | |
203 * @see InputConnection#commitText(java.lang.CharSequence, int) | |
204 */ | |
205 @Override | |
206 public boolean commitText(final CharSequence text, final int newCursorPositi on) { | |
207 Log.d(TAG, "commitText [%s] [%d]", text, newCursorPosition); | |
208 assertOnImeThread(); | |
209 ThreadUtils.postOnUiThread(new Runnable() { | |
210 @Override | |
211 public void run() { | |
212 mImeAdapter.sendCompositionToNative(text, newCursorPosition, tex t.length() > 0); | |
213 } | |
214 }); | |
215 return requestTextInputState() != null; | |
216 } | |
217 | |
218 /** | |
219 * @see InputConnection#performEditorAction(int) | |
220 */ | |
221 @Override | |
222 public boolean performEditorAction(final int actionCode) { | |
223 Log.d(TAG, "performEditorAction [%d]", actionCode); | |
224 assertOnImeThread(); | |
225 try { | |
226 return ThreadUtils.runOnUiThreadBlocking(new Callable<Boolean>() { | |
227 @Override | |
228 public Boolean call() { | |
229 return mImeAdapter.performEditorAction(actionCode); | |
230 } | |
231 }); | |
232 } catch (ExecutionException e) { | |
233 e.printStackTrace(); | |
234 return false; | |
235 } | |
236 } | |
237 | |
238 /** | |
239 * @see InputConnection#performContextMenuAction(int) | |
240 */ | |
241 @Override | |
242 public boolean performContextMenuAction(final int id) { | |
243 Log.d(TAG, "performContextMenuAction [%d]", id); | |
244 assertOnImeThread(); | |
245 try { | |
246 return ThreadUtils.runOnUiThreadBlocking(new Callable<Boolean>() { | |
247 @Override | |
248 public Boolean call() { | |
249 return mImeAdapter.performContextMenuAction(id); | |
250 } | |
251 }); | |
252 } catch (ExecutionException e) { | |
253 e.printStackTrace(); | |
254 return false; | |
255 } | |
256 } | |
257 | |
258 /** | |
259 * @see InputConnection#getExtractedText(android.view.inputmethod.ExtractedT extRequest, | |
260 * int) | |
261 */ | |
262 @Override | |
263 public ExtractedText getExtractedText(ExtractedTextRequest request, int flag s) { | |
264 Log.d(TAG, "getExtractedText"); | |
265 assertOnImeThread(); | |
266 TextInputState textInputState = requestTextInputState(); | |
267 if (textInputState == null) return null; | |
268 ExtractedText extractedText = new ExtractedText(); | |
269 extractedText.text = textInputState.text(); | |
270 extractedText.partialEndOffset = textInputState.text().length(); | |
271 extractedText.selectionStart = textInputState.selection().start(); | |
272 extractedText.selectionEnd = textInputState.selection().end(); | |
273 extractedText.flags = textInputState.singleLine() ? ExtractedText.FLAG_S INGLE_LINE : 0; | |
274 return extractedText; | |
275 } | |
276 | |
277 /** | |
278 * @see InputConnection#beginBatchEdit() | |
279 */ | |
280 @Override | |
281 public boolean beginBatchEdit() { | |
282 Log.d(TAG, "beginBatchEdit [%b]", (mNumNestedBatchEdits == 0)); | |
283 assertOnImeThread(); | |
284 mNumNestedBatchEdits++; | |
285 return true; | |
286 } | |
287 | |
288 /** | |
289 * @see InputConnection#endBatchEdit() | |
290 */ | |
291 @Override | |
292 public boolean endBatchEdit() { | |
293 assertOnImeThread(); | |
294 if (mNumNestedBatchEdits == 0) return false; | |
295 --mNumNestedBatchEdits; | |
296 Log.d(TAG, "endBatchEdit [%b]", (mNumNestedBatchEdits == 0)); | |
297 if (mNumNestedBatchEdits == 0) { | |
298 if (mBatchEditPendingState != null) { | |
299 // TODO(changwan): check if there is a chance that this gets cal led | |
aelias_OOO_until_Jul13
2016/01/23 03:21:39
Yes, this looks like a race condition. I propose
Changwan Ryu
2016/01/26 03:25:28
Good idea. Done.
| |
300 // before updateSelection from a nested command. | |
301 updateSelection(mBatchEditPendingState); | |
302 mBatchEditPendingState = null; | |
303 } | |
304 } | |
305 return mNumNestedBatchEdits != 0; | |
306 } | |
307 | |
308 /** | |
309 * @see InputConnection#deleteSurroundingText(int, int) | |
310 */ | |
311 @Override | |
312 public boolean deleteSurroundingText(final int beforeLength, final int after Length) { | |
313 Log.d(TAG, "deleteSurroundingText [%d %d]", beforeLength, afterLength); | |
314 assertOnImeThread(); | |
315 ThreadUtils.postOnUiThread(new Runnable() { | |
316 @Override | |
317 public void run() { | |
318 mImeAdapter.deleteSurroundingText(beforeLength, afterLength); | |
319 } | |
320 }); | |
321 return requestTextInputState() != null; | |
322 } | |
323 | |
324 /** | |
325 * @see ChromiumBaseInputConnection#sendKeyEventOnUiThread(KeyEvent) | |
326 */ | |
327 @Override | |
328 public boolean sendKeyEventOnUiThread(final KeyEvent event) { | |
329 ImeUtils.assertOnUiThread(); | |
330 mThreadManager.post(new Runnable() { | |
331 @Override | |
332 public void run() { | |
333 sendKeyEvent(event); | |
334 } | |
335 }); | |
336 return true; | |
337 } | |
338 | |
339 /** | |
340 * @see InputConnection#sendKeyEvent(android.view.KeyEvent) | |
341 */ | |
342 @Override | |
343 public boolean sendKeyEvent(final KeyEvent event) { | |
344 Log.d(TAG, "sendKeyEvent [%d %d]", event.getAction(), event.getKeyCode() ); | |
345 assertOnImeThread(); | |
346 ThreadUtils.postOnUiThread(new Runnable() { | |
347 @Override | |
348 public void run() { | |
349 mImeAdapter.sendKeyEvent(event); | |
350 } | |
351 }); | |
352 return requestTextInputState() != null; | |
353 } | |
354 | |
355 /** | |
356 * @see InputConnection#finishComposingText() | |
357 */ | |
358 @Override | |
359 public boolean finishComposingText() { | |
360 Log.d(TAG, "finishComposingText"); | |
361 // This is the only function that may be called on UI thread because | |
362 // of direct calls from InputMethodManager. | |
363 | |
364 ThreadUtils.postOnUiThread(new Runnable() { | |
365 @Override | |
366 public void run() { | |
367 mImeAdapter.finishComposingText(); | |
368 } | |
369 }); | |
370 | |
371 if (ThreadUtils.runningOnUiThread()) { | |
372 Log.d(TAG, "finishComposingText was called on UI thread."); | |
373 return true; | |
374 } else { | |
375 return requestTextInputState() != null; | |
376 } | |
377 } | |
378 | |
379 /** | |
380 * @see InputConnection#setSelection(int, int) | |
381 */ | |
382 @Override | |
383 public boolean setSelection(final int start, final int end) { | |
384 Log.d(TAG, "setSelection [%d %d]", start, end); | |
385 assertOnImeThread(); | |
386 ThreadUtils.postOnUiThread(new Runnable() { | |
387 @Override | |
388 public void run() { | |
389 mImeAdapter.setEditableSelectionOffsets(start, end); | |
390 } | |
391 }); | |
392 return requestTextInputState() != null; | |
393 } | |
394 | |
395 /** | |
396 * @see InputConnection#setComposingRegion(int, int) | |
397 */ | |
398 @Override | |
399 public boolean setComposingRegion(final int start, final int end) { | |
400 Log.d(TAG, "setComposingRegion [%d %d]", start, end); | |
401 assertOnImeThread(); | |
402 ThreadUtils.postOnUiThread(new Runnable() { | |
403 @Override | |
404 public void run() { | |
405 mImeAdapter.setComposingRegion(start, end); | |
406 } | |
407 }); | |
408 return requestTextInputState() != null; | |
409 } | |
410 | |
411 /** | |
412 * @see InputConnection#getTextBeforeCursor(int, int) | |
413 */ | |
414 @Override | |
415 public CharSequence getTextBeforeCursor(int maxChars, int flags) { | |
416 Log.d(TAG, "getTextBeforeCursor [%d %x]", maxChars, flags); | |
417 assertOnImeThread(); | |
418 TextInputState textInputState = requestTextInputState(); | |
419 if (textInputState == null) return null; | |
420 return textInputState.getTextBeforeSelection(maxChars); | |
421 } | |
422 | |
423 /** | |
424 * @see InputConnection#getTextAfterCursor(int, int) | |
425 */ | |
426 @Override | |
427 public CharSequence getTextAfterCursor(int maxChars, int flags) { | |
428 Log.d(TAG, "getTextAfterCursor [%d %x]", maxChars, flags); | |
429 assertOnImeThread(); | |
430 TextInputState textInputState = requestTextInputState(); | |
431 if (textInputState == null) return null; | |
432 return textInputState.getTextAfterSelection(maxChars); | |
433 } | |
434 | |
435 /** | |
436 * @see InputConnection#getSelectedText(int) | |
437 */ | |
438 @Override | |
439 public CharSequence getSelectedText(int flags) { | |
440 Log.d(TAG, "getSelectedText [%x]", flags); | |
441 assertOnImeThread(); | |
442 TextInputState textInputState = requestTextInputState(); | |
443 if (textInputState == null) return null; | |
444 return textInputState.getSelectedText(); | |
445 } | |
446 | |
447 @Override | |
448 public void moveCursorToSelectionEndOnUiThread() { | |
449 mThreadManager.post(new Runnable() { | |
450 @Override | |
451 public void run() { | |
452 TextInputState textInputState = requestTextInputState(); | |
453 if (textInputState == null) return; | |
454 Range selection = textInputState.selection(); | |
455 setSelection(selection.end(), selection.end()); | |
456 } | |
457 }); | |
458 } | |
459 | |
460 /** | |
461 * @see InputConnection#getCursorCapsMode(int) | |
462 */ | |
463 @Override | |
464 public int getCursorCapsMode(int reqModes) { | |
465 Log.d(TAG, "getCursorCapsMode [%x]", reqModes); | |
466 assertOnImeThread(); | |
467 // TODO(changwan): Auto-generated method stub | |
468 return 0; | |
469 } | |
470 | |
471 /** | |
472 * @see InputConnection#commitCompletion(android.view.inputmethod.Completion Info) | |
473 */ | |
474 @Override | |
475 public boolean commitCompletion(CompletionInfo text) { | |
476 Log.d(TAG, "commitCompletion [%s]", text); | |
477 assertOnImeThread(); | |
478 // TODO(changwan): Auto-generated method stub | |
479 return false; | |
480 } | |
481 | |
482 /** | |
483 * @see InputConnection#commitCorrection(android.view.inputmethod.Correction Info) | |
484 */ | |
485 @Override | |
486 public boolean commitCorrection(CorrectionInfo correctionInfo) { | |
487 Log.d(TAG, "commitCorrection [%s]", ImeUtils.dumpCorrectionInfo(correcti onInfo)); | |
488 assertOnImeThread(); | |
489 // TODO(changwan): Auto-generated method stub | |
490 return false; | |
491 } | |
492 | |
493 /** | |
494 * @see InputConnection#clearMetaKeyStates(int) | |
495 */ | |
496 @Override | |
497 public boolean clearMetaKeyStates(int states) { | |
498 Log.d(TAG, "clearMetaKeyStates [%x]", states); | |
499 assertOnImeThread(); | |
500 // TODO(changwan): Auto-generated method stub | |
501 return false; | |
502 } | |
503 | |
504 /** | |
505 * @see InputConnection#reportFullscreenMode(boolean) | |
506 */ | |
507 @Override | |
508 public boolean reportFullscreenMode(boolean enabled) { | |
509 Log.d(TAG, "reportFullscreenMode [%b]", enabled); | |
510 // We ignore fullscreen mode for now. That's why we set | |
511 // EditorInfo.IME_FLAG_NO_FULLSCREEN in constructor. | |
512 // Note that this may be called on UI thread. | |
513 return false; | |
514 } | |
515 | |
516 /** | |
517 * @see InputConnection#performPrivateCommand(java.lang.String, android.os.B undle) | |
518 */ | |
519 @Override | |
520 public boolean performPrivateCommand(String action, Bundle data) { | |
521 Log.d(TAG, "performPrivateCommand [%s]", action); | |
522 assertOnImeThread(); | |
523 // TODO(changwan): Auto-generated method stub | |
524 return false; | |
525 } | |
526 | |
527 /** | |
528 * @see InputConnection#requestCursorUpdates(int) | |
529 */ | |
530 @Override | |
531 public boolean requestCursorUpdates(int cursorUpdateMode) { | |
532 Log.d(TAG, "requestCursorUpdates [%x]", cursorUpdateMode); | |
533 assertOnImeThread(); | |
534 // TODO(changwan): Auto-generated method stub | |
535 return false; | |
536 } | |
537 } | |
OLD | NEW |