OLD | NEW |
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 package org.chromium.content.browser; | 5 package org.chromium.content.browser; |
6 | 6 |
7 import android.content.Context; | 7 import android.content.Context; |
8 import android.os.Handler; | 8 import android.os.Handler; |
9 import android.os.ResultReceiver; | 9 import android.os.ResultReceiver; |
10 import android.text.Editable; | 10 import android.text.Editable; |
11 import android.text.InputType; | 11 import android.text.InputType; |
12 import android.text.Selection; | 12 import android.text.Selection; |
| 13 import android.view.KeyCharacterMap; |
13 import android.view.KeyEvent; | 14 import android.view.KeyEvent; |
14 import android.view.View; | 15 import android.view.View; |
15 import android.view.inputmethod.BaseInputConnection; | 16 import android.view.inputmethod.BaseInputConnection; |
16 import android.view.inputmethod.EditorInfo; | 17 import android.view.inputmethod.EditorInfo; |
17 import android.view.inputmethod.ExtractedText; | 18 import android.view.inputmethod.ExtractedText; |
18 import android.view.inputmethod.ExtractedTextRequest; | 19 import android.view.inputmethod.ExtractedTextRequest; |
19 import android.view.inputmethod.InputMethodManager; | 20 import android.view.inputmethod.InputMethodManager; |
20 | 21 |
21 import org.chromium.base.CalledByNative; | 22 import org.chromium.base.CalledByNative; |
22 import org.chromium.base.JNINamespace; | 23 import org.chromium.base.JNINamespace; |
23 | 24 |
24 /** | 25 /** |
25 We have to adapt and plumb android IME service and chrome text input API. | 26 We have to adapt and plumb android IME service and chrome text input API. |
26 ImeAdapter provides an interface in both ways native <-> java: | 27 ImeAdapter provides an interface in both ways native <-> java: |
27 1. InputConnectionAdapter notifies native code of text composition state and | 28 1. InputConnectionAdapter notifies native code of text composition state and |
28 dispatch key events from java -> WebKit. | 29 dispatch key events from java -> WebKit. |
29 2. Native ImeAdapter notifies java side to clear composition text. | 30 2. Native ImeAdapter notifies java side to clear composition text. |
30 | 31 |
31 The basic flow is: | 32 The basic flow is: |
32 1. Intercept dispatchKeyEventPreIme() to record the current key event, but do | 33 1. When InputConnectionAdapter gets called with composition or result text: |
33 nothing else. | 34 If we receive a composition text or a result text, then we just need to |
34 2. When InputConnectionAdapter gets called with composition or result text: | 35 dispatch a synthetic key event with special keycode 229, and then dispatch |
35 a) If a key event has been recorded in dispatchKeyEventPreIme() and we | 36 the composition or result text. |
36 receive a result text with single character, then we probably need to | 37 2. Intercept dispatchKeyEvent() method for key events not handled by IME, we |
37 send the result text as a Char event rather than a ConfirmComposition | 38 need to dispatch them to webkit and check webkit's reply. Then inject a |
38 event. So we need to dispatch the recorded key event followed by a | 39 new key event for further processing if webkit didn't handle it. |
39 synthetic Char event. | |
40 b) If we receive a composition text or a result text with more than one | |
41 characters, then no matter if we recorded a key event or not in | |
42 dispatchKeyEventPreIme(), we just need to dispatch a synthetic key | |
43 event with special keycode 229, and then dispatch the composition or | |
44 result text. | |
45 3. Intercept dispatchKeyEvent() method for key events not handled by IME, we | |
46 need to dispatch them to WebKit and check webkit's reply. Then inject a | |
47 new key event for further processing if WebKit didn't handle it. | |
48 */ | 40 */ |
49 @JNINamespace("content") | 41 @JNINamespace("content") |
50 class ImeAdapter { | 42 class ImeAdapter { |
51 interface ViewEmbedder { | 43 interface ViewEmbedder { |
52 /** | 44 /** |
53 * @param isFinish whether the event is occurring because input is finis
hed. | 45 * @param isFinish whether the event is occurring because input is finis
hed. |
54 */ | 46 */ |
55 public void onImeEvent(boolean isFinish); | 47 public void onImeEvent(boolean isFinish); |
56 public void onSetFieldValue(); | 48 public void onSetFieldValue(); |
57 public void onDismissInput(); | 49 public void onDismissInput(); |
(...skipping 52 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
110 sTextInputTypeUrl = textInputTypeUrl; | 102 sTextInputTypeUrl = textInputTypeUrl; |
111 sTextInputTypeEmail = textInputTypeEmail; | 103 sTextInputTypeEmail = textInputTypeEmail; |
112 sTextInputTypeTel = textInputTypeTel; | 104 sTextInputTypeTel = textInputTypeTel; |
113 sTextInputTypeNumber = textInputTypeNumber; | 105 sTextInputTypeNumber = textInputTypeNumber; |
114 sTextInputTypeWeek = textInputTypeWeek; | 106 sTextInputTypeWeek = textInputTypeWeek; |
115 sTextInputTypeContentEditable = textInputTypeContentEditable; | 107 sTextInputTypeContentEditable = textInputTypeContentEditable; |
116 } | 108 } |
117 | 109 |
118 private int mNativeImeAdapterAndroid; | 110 private int mNativeImeAdapterAndroid; |
119 private int mTextInputType; | 111 private int mTextInputType; |
120 private int mPreImeEventCount; | |
121 | 112 |
122 private Context mContext; | 113 private Context mContext; |
123 private SelectionHandleController mSelectionHandleController; | 114 private SelectionHandleController mSelectionHandleController; |
124 private InsertionHandleController mInsertionHandleController; | 115 private InsertionHandleController mInsertionHandleController; |
125 private AdapterInputConnection mInputConnection; | 116 private AdapterInputConnection mInputConnection; |
126 private ViewEmbedder mViewEmbedder; | 117 private ViewEmbedder mViewEmbedder; |
127 private Handler mHandler; | 118 private Handler mHandler; |
128 | 119 |
129 private class DelayedDismissInput implements Runnable { | 120 private class DelayedDismissInput implements Runnable { |
130 private int mNativeImeAdapter; | 121 private int mNativeImeAdapter; |
(...skipping 13 matching lines...) Expand all Loading... |
144 | 135 |
145 // Delay introduced to avoid hiding the keyboard if new show requests are re
ceived. | 136 // Delay introduced to avoid hiding the keyboard if new show requests are re
ceived. |
146 // The time required by the unfocus-focus events triggered by tab has been m
easured in soju: | 137 // The time required by the unfocus-focus events triggered by tab has been m
easured in soju: |
147 // Mean: 18.633 ms, Standard deviation: 7.9837 ms. | 138 // Mean: 18.633 ms, Standard deviation: 7.9837 ms. |
148 // The value here should be higher enough to cover these cases, but not too
high to avoid | 139 // The value here should be higher enough to cover these cases, but not too
high to avoid |
149 // letting the user perceiving important delays. | 140 // letting the user perceiving important delays. |
150 private static final int INPUT_DISMISS_DELAY = 150; | 141 private static final int INPUT_DISMISS_DELAY = 150; |
151 | 142 |
152 ImeAdapter(Context context, SelectionHandleController selectionHandleControl
ler, | 143 ImeAdapter(Context context, SelectionHandleController selectionHandleControl
ler, |
153 InsertionHandleController insertionHandleController, ViewEmbedder em
bedder) { | 144 InsertionHandleController insertionHandleController, ViewEmbedder em
bedder) { |
154 mPreImeEventCount = 0; | |
155 mContext = context; | 145 mContext = context; |
156 mSelectionHandleController = selectionHandleController; | 146 mSelectionHandleController = selectionHandleController; |
157 mInsertionHandleController = insertionHandleController; | 147 mInsertionHandleController = insertionHandleController; |
158 mViewEmbedder = embedder; | 148 mViewEmbedder = embedder; |
159 mHandler = new Handler(); | 149 mHandler = new Handler(); |
160 } | 150 } |
161 | 151 |
162 boolean isFor(int nativeImeAdapter, int textInputType) { | 152 boolean isFor(int nativeImeAdapter, int textInputType) { |
163 return mNativeImeAdapterAndroid == nativeImeAdapter && | 153 return mNativeImeAdapterAndroid == nativeImeAdapter && |
164 mTextInputType == textInputType; | 154 mTextInputType == textInputType; |
(...skipping 92 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
257 } | 247 } |
258 | 248 |
259 static boolean isTextInputType(int type) { | 249 static boolean isTextInputType(int type) { |
260 return type != sTextInputTypeNone && !InputDialogContainer.isDialogInput
Type(type); | 250 return type != sTextInputTypeNone && !InputDialogContainer.isDialogInput
Type(type); |
261 } | 251 } |
262 | 252 |
263 boolean hasTextInputType() { | 253 boolean hasTextInputType() { |
264 return isTextInputType(mTextInputType); | 254 return isTextInputType(mTextInputType); |
265 } | 255 } |
266 | 256 |
267 void dispatchKeyEventPreIme(KeyEvent event) { | |
268 // We only register that a key was pressed, but we don't actually interc
ept | |
269 // it. | |
270 ++mPreImeEventCount; | |
271 } | |
272 | |
273 boolean dispatchKeyEvent(KeyEvent event) { | 257 boolean dispatchKeyEvent(KeyEvent event) { |
274 mPreImeEventCount = 0; | |
275 return translateAndSendNativeEvents(event); | 258 return translateAndSendNativeEvents(event); |
276 } | 259 } |
277 | 260 |
278 void commitText() { | 261 void commitText() { |
279 cancelComposition(); | 262 cancelComposition(); |
280 if (mNativeImeAdapterAndroid != 0) { | 263 if (mNativeImeAdapterAndroid != 0) { |
281 nativeCommitText(mNativeImeAdapterAndroid, ""); | 264 nativeCommitText(mNativeImeAdapterAndroid, ""); |
282 } | 265 } |
283 } | 266 } |
284 | 267 |
(...skipping 11 matching lines...) Expand all Loading... |
296 return false; | 279 return false; |
297 } | 280 } |
298 | 281 |
299 // Committing an empty string finishes the current composition. | 282 // Committing an empty string finishes the current composition. |
300 boolean isFinish = text.isEmpty(); | 283 boolean isFinish = text.isEmpty(); |
301 if (!isFinish) { | 284 if (!isFinish) { |
302 mSelectionHandleController.hideAndDisallowAutomaticShowing(); | 285 mSelectionHandleController.hideAndDisallowAutomaticShowing(); |
303 mInsertionHandleController.hideAndDisallowAutomaticShowing(); | 286 mInsertionHandleController.hideAndDisallowAutomaticShowing(); |
304 } | 287 } |
305 mViewEmbedder.onImeEvent(isFinish); | 288 mViewEmbedder.onImeEvent(isFinish); |
306 boolean hasSingleChar = mPreImeEventCount == 1 && text.length() == 1; | 289 int keyCode = shouldSendKeyEventWithKeyCode(text); |
307 int keyCode = hasSingleChar ? text.codePointAt(0) : COMPOSITION_KEY_CODE
; | |
308 int keyChar = hasSingleChar ? text.codePointAt(0) : 0; | |
309 long timeStampMs = System.currentTimeMillis(); | 290 long timeStampMs = System.currentTimeMillis(); |
310 nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeRawKeyDo
wn, | 291 |
311 timeStampMs, keyCode, keyChar); | 292 if (keyCode != COMPOSITION_KEY_CODE) { |
312 if (hasSingleChar) { | 293 sendKeyEventWithKeyCode(keyCode, |
313 nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeChar
, | 294 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE)
; |
314 timeStampMs, text.codePointAt(0), text.codePointAt(0)); | |
315 } else { | 295 } else { |
| 296 nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeRawK
eyDown, |
| 297 timeStampMs, keyCode, 0); |
316 if (isCommit) { | 298 if (isCommit) { |
317 nativeCommitText(mNativeImeAdapterAndroid, text); | 299 nativeCommitText(mNativeImeAdapterAndroid, text); |
318 } else { | 300 } else { |
319 nativeSetComposingText(mNativeImeAdapterAndroid, text, newCursor
Position); | 301 nativeSetComposingText(mNativeImeAdapterAndroid, text, newCursor
Position); |
320 } | 302 } |
| 303 nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeKeyU
p, |
| 304 timeStampMs, keyCode, 0); |
321 } | 305 } |
322 nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeKeyUp, | 306 |
323 timeStampMs, keyCode, keyChar); | |
324 mPreImeEventCount = 0; | |
325 return true; | 307 return true; |
326 } | 308 } |
327 | 309 |
| 310 private int shouldSendKeyEventWithKeyCode(String text) { |
| 311 if (text.length() != 1) return COMPOSITION_KEY_CODE; |
| 312 |
| 313 if (text.equals("\n")) return KeyEvent.KEYCODE_ENTER; |
| 314 else if (text.equals("\t")) return KeyEvent.KEYCODE_TAB; |
| 315 else return COMPOSITION_KEY_CODE; |
| 316 } |
| 317 |
| 318 private void sendKeyEventWithKeyCode(int keyCode, int flags) { |
| 319 long eventTime = System.currentTimeMillis(); |
| 320 translateAndSendNativeEvents(new KeyEvent(eventTime, eventTime, |
| 321 KeyEvent.ACTION_DOWN, keyCode, 0, 0, |
| 322 KeyCharacterMap.VIRTUAL_KEYBOARD, 0, |
| 323 flags)); |
| 324 translateAndSendNativeEvents(new KeyEvent(System.currentTimeMillis(), ev
entTime, |
| 325 KeyEvent.ACTION_UP, keyCode, 0, 0, |
| 326 KeyCharacterMap.VIRTUAL_KEYBOARD, 0, |
| 327 flags)); |
| 328 } |
| 329 |
328 private boolean translateAndSendNativeEvents(KeyEvent event) { | 330 private boolean translateAndSendNativeEvents(KeyEvent event) { |
329 if (mNativeImeAdapterAndroid == 0) { | 331 if (mNativeImeAdapterAndroid == 0) { |
330 return false; | 332 return false; |
331 } | 333 } |
332 int action = event.getAction(); | 334 int action = event.getAction(); |
333 if (action != KeyEvent.ACTION_DOWN && | 335 if (action != KeyEvent.ACTION_DOWN && |
334 action != KeyEvent.ACTION_UP) { | 336 action != KeyEvent.ACTION_UP) { |
335 // action == KeyEvent.ACTION_MULTIPLE | 337 // action == KeyEvent.ACTION_MULTIPLE |
336 // TODO(bulach): confirm the actual behavior. Apparently: | 338 // TODO(bulach): confirm the actual behavior. Apparently: |
337 // If event.getKeyCode() == KEYCODE_UNKNOWN, we can send a | 339 // If event.getKeyCode() == KEYCODE_UNKNOWN, we can send a |
(...skipping 214 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
552 // Send TAB key event | 554 // Send TAB key event |
553 long timeStampMs = System.currentTimeMillis(); | 555 long timeStampMs = System.currentTimeMillis(); |
554 mImeAdapter.sendSyntheticKeyEvent( | 556 mImeAdapter.sendSyntheticKeyEvent( |
555 sEventTypeRawKeyDown, timeStampMs, KeyEvent.KEYCODE_
TAB, 0); | 557 sEventTypeRawKeyDown, timeStampMs, KeyEvent.KEYCODE_
TAB, 0); |
556 return true; | 558 return true; |
557 case EditorInfo.IME_ACTION_GO: | 559 case EditorInfo.IME_ACTION_GO: |
558 case EditorInfo.IME_ACTION_SEARCH: | 560 case EditorInfo.IME_ACTION_SEARCH: |
559 mImeAdapter.dismissInput(true); | 561 mImeAdapter.dismissInput(true); |
560 break; | 562 break; |
561 } | 563 } |
562 | 564 mImeAdapter.sendKeyEventWithKeyCode(KeyEvent.KEYCODE_ENTER, |
563 return super.performEditorAction(actionCode); | 565 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE |
| 566 | KeyEvent.FLAG_EDITOR_ACTION); |
| 567 return true; |
564 } | 568 } |
565 | 569 |
566 @Override | 570 @Override |
567 public boolean performContextMenuAction(int id) { | 571 public boolean performContextMenuAction(int id) { |
568 switch (id) { | 572 switch (id) { |
569 case android.R.id.selectAll: | 573 case android.R.id.selectAll: |
570 return mImeAdapter.selectAll(); | 574 return mImeAdapter.selectAll(); |
571 case android.R.id.cut: | 575 case android.R.id.cut: |
572 return mImeAdapter.cut(); | 576 return mImeAdapter.cut(); |
573 case android.R.id.copy: | 577 case android.R.id.copy: |
(...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
621 if (selectionStart > selectionEnd) { | 625 if (selectionStart > selectionEnd) { |
622 int temp = selectionStart; | 626 int temp = selectionStart; |
623 selectionStart = selectionEnd; | 627 selectionStart = selectionEnd; |
624 selectionEnd = temp; | 628 selectionEnd = temp; |
625 } | 629 } |
626 editable.replace(selectionStart, selectionEnd, | 630 editable.replace(selectionStart, selectionEnd, |
627 Character.toString((char)unicodeChar)); | 631 Character.toString((char)unicodeChar)); |
628 } | 632 } |
629 } | 633 } |
630 } | 634 } |
631 return super.sendKeyEvent(event); | 635 mImeAdapter.translateAndSendNativeEvents(event); |
| 636 return true; |
632 } | 637 } |
633 | 638 |
634 @Override | 639 @Override |
635 public boolean finishComposingText() { | 640 public boolean finishComposingText() { |
636 Editable editable = getEditable(); | 641 Editable editable = getEditable(); |
637 if (getComposingSpanStart(editable) == getComposingSpanEnd(editable)
) { | 642 if (getComposingSpanStart(editable) == getComposingSpanEnd(editable)
) { |
638 return true; | 643 return true; |
639 } | 644 } |
640 super.finishComposingText(); | 645 super.finishComposingText(); |
641 return mImeAdapter.checkCompositionQueueAndCallNative("", 0, true); | 646 return mImeAdapter.checkCompositionQueueAndCallNative("", 0, true); |
(...skipping 105 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
747 | 752 |
748 private native void nativeDeleteSurroundingText(int nativeImeAdapterAndroid, | 753 private native void nativeDeleteSurroundingText(int nativeImeAdapterAndroid, |
749 int before, int after); | 754 int before, int after); |
750 | 755 |
751 private native void nativeUnselect(int nativeImeAdapterAndroid); | 756 private native void nativeUnselect(int nativeImeAdapterAndroid); |
752 private native void nativeSelectAll(int nativeImeAdapterAndroid); | 757 private native void nativeSelectAll(int nativeImeAdapterAndroid); |
753 private native void nativeCut(int nativeImeAdapterAndroid); | 758 private native void nativeCut(int nativeImeAdapterAndroid); |
754 private native void nativeCopy(int nativeImeAdapterAndroid); | 759 private native void nativeCopy(int nativeImeAdapterAndroid); |
755 private native void nativePaste(int nativeImeAdapterAndroid); | 760 private native void nativePaste(int nativeImeAdapterAndroid); |
756 } | 761 } |
OLD | NEW |