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 // We have to adapt and plumb android IME service and chrome text input API. | 25 /** |
25 // ImeAdapter provides an interface in both ways native <-> java: | 26 We have to adapt and plumb android IME service and chrome text input API. |
26 // 1. InputConnectionAdapter notifies native code of text composition state and | 27 ImeAdapter provides an interface in both ways native <-> java: |
27 // dispatch key events from java -> WebKit. | 28 1. InputConnectionAdapter notifies native code of text composition state and |
28 // 2. Native ImeAdapter notifies java side to clear composition text. | 29 dispatch key events from java -> WebKit. |
29 // | 30 2. Native ImeAdapter notifies java side to clear composition text. |
30 // The basic flow is: | 31 |
31 // 1. Intercept dispatchKeyEventPreIme() to record the current key event, but do | 32 The basic flow is: |
32 // nothing else. | 33 1. When InputConnectionAdapter gets called with composition or result text: |
33 // 2. When InputConnectionAdapter gets called with composition or result text: | 34 If we receive a composition text or a result text, then we just need to |
34 // a) If a key event has been recorded in dispatchKeyEventPreIme() and we | 35 dispatch a synthetic key event with special keycode 229, and then dispatch |
35 // receive a result text with single character, then we probably need to | 36 the composition or result text. |
36 // send the result text as a Char event rather than a ConfirmComposition | 37 2. Intercept dispatchKeyEvent() method for key events not handled by IME, we |
37 // event. So we need to dispatch the recorded key event followed by a | 38 need to dispatch them to webkit and check webkit's reply. Then inject a |
38 // synthetic Char event. | 39 new key event for further processing if webkit didn't handle it. |
39 // b) If we receive a composition text or a result text with more than one | 40 */ |
40 // characters, then no matter if we recorded a key event or not in | |
41 // dispatchKeyEventPreIme(), we just need to dispatch a synthetic key | |
42 // event with special keycode 229, and then dispatch the composition or | |
43 // result text. | |
44 // 3. Intercept dispatchKeyEvent() method for key events not handled by IME, we | |
45 // need to dispatch them to webkit and check webkit's reply. Then inject a | |
46 // new key event for further processing if webkit didn't handle it. | |
47 @JNINamespace("content") | 41 @JNINamespace("content") |
48 class ImeAdapter { | 42 class ImeAdapter { |
49 interface ViewEmbedder { | 43 interface ViewEmbedder { |
50 /** | 44 /** |
51 * @param isFinish whether the event is occuring because input is finish
ed. | 45 * @param isFinish whether the event is occuring because input is finish
ed. |
52 */ | 46 */ |
53 public void onImeEvent(boolean isFinish); | 47 public void onImeEvent(boolean isFinish); |
54 public void onSetFieldValue(); | 48 public void onSetFieldValue(); |
55 public void onDismissInput(); | 49 public void onDismissInput(); |
56 public View getAttachedView(); | 50 public View getAttachedView(); |
(...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
110 sTextInputTypeTel = textInputTypeTel; | 104 sTextInputTypeTel = textInputTypeTel; |
111 sTextInputTypeNumber = textInputTypeNumber; | 105 sTextInputTypeNumber = textInputTypeNumber; |
112 sTextInputTypeWeek = textInputTypeWeek; | 106 sTextInputTypeWeek = textInputTypeWeek; |
113 sTextInputTypeContentEditable = textInputTypeContentEditable; | 107 sTextInputTypeContentEditable = textInputTypeContentEditable; |
114 InputDialogContainer.initializeInputTypes(textInputTypeDate, textInputTy
peDateTime, | 108 InputDialogContainer.initializeInputTypes(textInputTypeDate, textInputTy
peDateTime, |
115 textInputTypeDateTimeLocal, textInputTypeMonth, textInputTypeTim
e); | 109 textInputTypeDateTimeLocal, textInputTypeMonth, textInputTypeTim
e); |
116 } | 110 } |
117 | 111 |
118 private int mNativeImeAdapterAndroid; | 112 private int mNativeImeAdapterAndroid; |
119 private int mTextInputType; | 113 private int mTextInputType; |
120 private int mPreImeEventCount; | |
121 | 114 |
122 private Context mContext; | 115 private Context mContext; |
123 private SelectionHandleController mSelectionHandleController; | 116 private SelectionHandleController mSelectionHandleController; |
124 private InsertionHandleController mInsertionHandleController; | 117 private InsertionHandleController mInsertionHandleController; |
125 private AdapterInputConnection mInputConnection; | 118 private AdapterInputConnection mInputConnection; |
126 private ViewEmbedder mViewEmbedder; | 119 private ViewEmbedder mViewEmbedder; |
127 private Handler mHandler; | 120 private Handler mHandler; |
128 private InputDialogContainer mInputDialogContainer; | 121 private InputDialogContainer mInputDialogContainer; |
129 | 122 |
130 private class DelayedDismissInput implements Runnable { | 123 private class DelayedDismissInput implements Runnable { |
(...skipping 14 matching lines...) Expand all Loading... |
145 | 138 |
146 // Delay introduced to avoid hiding the keyboard if new show requests are re
ceived. | 139 // Delay introduced to avoid hiding the keyboard if new show requests are re
ceived. |
147 // The time required by the unfocus-focus events triggered by tab has been m
easured in soju: | 140 // The time required by the unfocus-focus events triggered by tab has been m
easured in soju: |
148 // Mean: 18.633 ms, Standard deviation: 7.9837 ms. | 141 // Mean: 18.633 ms, Standard deviation: 7.9837 ms. |
149 // The value here should be higher enough to cover these cases, but not too
high to avoid | 142 // The value here should be higher enough to cover these cases, but not too
high to avoid |
150 // letting the user perceiving important delays. | 143 // letting the user perceiving important delays. |
151 private static final int INPUT_DISMISS_DELAY = 150; | 144 private static final int INPUT_DISMISS_DELAY = 150; |
152 | 145 |
153 ImeAdapter(Context context, SelectionHandleController selectionHandleControl
ler, | 146 ImeAdapter(Context context, SelectionHandleController selectionHandleControl
ler, |
154 InsertionHandleController insertionHandleController, ViewEmbedder em
bedder) { | 147 InsertionHandleController insertionHandleController, ViewEmbedder em
bedder) { |
155 mPreImeEventCount = 0; | |
156 mContext = context; | 148 mContext = context; |
157 mSelectionHandleController = selectionHandleController; | 149 mSelectionHandleController = selectionHandleController; |
158 mInsertionHandleController = insertionHandleController; | 150 mInsertionHandleController = insertionHandleController; |
159 mViewEmbedder = embedder; | 151 mViewEmbedder = embedder; |
160 mHandler = new Handler(); | 152 mHandler = new Handler(); |
161 mInputDialogContainer = new InputDialogContainer(context, | 153 mInputDialogContainer = new InputDialogContainer(context, |
162 new InputDialogContainer.InputActionDelegate() { | 154 new InputDialogContainer.InputActionDelegate() { |
163 | 155 |
164 @Override | 156 @Override |
165 public void replaceDateTime(String text) { | 157 public void replaceDateTime(String text) { |
(...skipping 129 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
295 } | 287 } |
296 | 288 |
297 boolean hasTextInputType() { | 289 boolean hasTextInputType() { |
298 return isTextInputType(mTextInputType); | 290 return isTextInputType(mTextInputType); |
299 } | 291 } |
300 | 292 |
301 boolean hasDialogInputType() { | 293 boolean hasDialogInputType() { |
302 return InputDialogContainer.isDialogInputType(mTextInputType); | 294 return InputDialogContainer.isDialogInputType(mTextInputType); |
303 } | 295 } |
304 | 296 |
305 void dispatchKeyEventPreIme(KeyEvent event) { | |
306 // We only register that a key was pressed, but we don't actually interc
ept | |
307 // it. | |
308 ++mPreImeEventCount; | |
309 } | |
310 | |
311 boolean dispatchKeyEvent(KeyEvent event) { | 297 boolean dispatchKeyEvent(KeyEvent event) { |
312 mPreImeEventCount = 0; | |
313 return translateAndSendNativeEvents(event); | 298 return translateAndSendNativeEvents(event); |
314 } | 299 } |
315 | 300 |
316 void commitText() { | 301 void commitText() { |
317 cancelComposition(); | 302 cancelComposition(); |
318 if (mNativeImeAdapterAndroid != 0) { | 303 if (mNativeImeAdapterAndroid != 0) { |
319 nativeCommitText(mNativeImeAdapterAndroid, ""); | 304 nativeCommitText(mNativeImeAdapterAndroid, ""); |
320 } | 305 } |
321 } | 306 } |
322 | 307 |
(...skipping 11 matching lines...) Expand all Loading... |
334 return false; | 319 return false; |
335 } | 320 } |
336 | 321 |
337 // Committing an empty string finishes the current composition. | 322 // Committing an empty string finishes the current composition. |
338 boolean isFinish = text.isEmpty(); | 323 boolean isFinish = text.isEmpty(); |
339 if (!isFinish) { | 324 if (!isFinish) { |
340 mSelectionHandleController.hideAndDisallowAutomaticShowing(); | 325 mSelectionHandleController.hideAndDisallowAutomaticShowing(); |
341 mInsertionHandleController.hideAndDisallowAutomaticShowing(); | 326 mInsertionHandleController.hideAndDisallowAutomaticShowing(); |
342 } | 327 } |
343 mViewEmbedder.onImeEvent(isFinish); | 328 mViewEmbedder.onImeEvent(isFinish); |
344 boolean hasSingleChar = mPreImeEventCount == 1 && text.length() == 1; | 329 int keyCode = shouldSendKeyEventWithKeyCode(text); |
345 int keyCode = hasSingleChar ? text.codePointAt(0) : COMPOSITION_KEY_CODE
; | |
346 int keyChar = hasSingleChar ? text.codePointAt(0) : 0; | |
347 long timeStampMs = System.currentTimeMillis(); | 330 long timeStampMs = System.currentTimeMillis(); |
348 nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeRawKeyDo
wn, | 331 |
349 timeStampMs, keyCode, keyChar); | 332 if (keyCode != COMPOSITION_KEY_CODE) { |
350 if (hasSingleChar) { | 333 sendKeyEventWithKeyCode(keyCode, |
351 nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeChar
, | 334 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE)
; |
352 timeStampMs, text.codePointAt(0), text.codePointAt(0)); | |
353 } else { | 335 } else { |
| 336 nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeRawK
eyDown, |
| 337 timeStampMs, keyCode, 0); |
354 if (isCommit) { | 338 if (isCommit) { |
355 nativeCommitText(mNativeImeAdapterAndroid, text); | 339 nativeCommitText(mNativeImeAdapterAndroid, text); |
356 } else { | 340 } else { |
357 nativeSetComposingText(mNativeImeAdapterAndroid, text, newCursor
Position); | 341 nativeSetComposingText(mNativeImeAdapterAndroid, text, newCursor
Position); |
358 } | 342 } |
| 343 nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeKeyU
p, |
| 344 timeStampMs, keyCode, 0); |
359 } | 345 } |
360 nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeKeyUp, | 346 |
361 timeStampMs, keyCode, keyChar); | |
362 mPreImeEventCount = 0; | |
363 return true; | 347 return true; |
364 } | 348 } |
365 | 349 |
| 350 private int shouldSendKeyEventWithKeyCode(String text) { |
| 351 if (text.length() != 1) return COMPOSITION_KEY_CODE; |
| 352 |
| 353 if (text.equals("\n")) return KeyEvent.KEYCODE_ENTER; |
| 354 else if (text.equals("\t")) return KeyEvent.KEYCODE_TAB; |
| 355 else return COMPOSITION_KEY_CODE; |
| 356 } |
| 357 |
| 358 private void sendKeyEventWithKeyCode(int keyCode, int flags) { |
| 359 long eventTime = System.currentTimeMillis(); |
| 360 translateAndSendNativeEvents(new KeyEvent(eventTime, eventTime, |
| 361 KeyEvent.ACTION_DOWN, keyCode, 0, 0, |
| 362 KeyCharacterMap.VIRTUAL_KEYBOARD, 0, |
| 363 flags)); |
| 364 translateAndSendNativeEvents(new KeyEvent(System.currentTimeMillis(), ev
entTime, |
| 365 KeyEvent.ACTION_UP, keyCode, 0, 0, |
| 366 KeyCharacterMap.VIRTUAL_KEYBOARD, 0, |
| 367 flags)); |
| 368 } |
| 369 |
366 private boolean translateAndSendNativeEvents(KeyEvent event) { | 370 private boolean translateAndSendNativeEvents(KeyEvent event) { |
367 if (mNativeImeAdapterAndroid == 0) { | 371 if (mNativeImeAdapterAndroid == 0) { |
368 return false; | 372 return false; |
369 } | 373 } |
370 int action = event.getAction(); | 374 int action = event.getAction(); |
371 if (action != KeyEvent.ACTION_DOWN && | 375 if (action != KeyEvent.ACTION_DOWN && |
372 action != KeyEvent.ACTION_UP) { | 376 action != KeyEvent.ACTION_UP) { |
373 // action == KeyEvent.ACTION_MULTIPLE | 377 // action == KeyEvent.ACTION_MULTIPLE |
374 // TODO(bulach): confirm the actual behavior. Apparently: | 378 // TODO(bulach): confirm the actual behavior. Apparently: |
375 // If event.getKeyCode() == KEYCODE_UNKNOWN, we can send a | 379 // If event.getKeyCode() == KEYCODE_UNKNOWN, we can send a |
(...skipping 226 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
602 // Send TAB key event | 606 // Send TAB key event |
603 long timeStampMs = System.currentTimeMillis(); | 607 long timeStampMs = System.currentTimeMillis(); |
604 mImeAdapter.sendSyntheticKeyEvent( | 608 mImeAdapter.sendSyntheticKeyEvent( |
605 sEventTypeRawKeyDown, timeStampMs, KeyEvent.KEYCODE_
TAB, 0); | 609 sEventTypeRawKeyDown, timeStampMs, KeyEvent.KEYCODE_
TAB, 0); |
606 return true; | 610 return true; |
607 case EditorInfo.IME_ACTION_GO: | 611 case EditorInfo.IME_ACTION_GO: |
608 case EditorInfo.IME_ACTION_SEARCH: | 612 case EditorInfo.IME_ACTION_SEARCH: |
609 mImeAdapter.dismissInput(true); | 613 mImeAdapter.dismissInput(true); |
610 break; | 614 break; |
611 } | 615 } |
612 | 616 mImeAdapter.sendKeyEventWithKeyCode(KeyEvent.KEYCODE_ENTER, |
613 return super.performEditorAction(actionCode); | 617 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE |
| 618 | KeyEvent.FLAG_EDITOR_ACTION); |
| 619 return true; |
614 } | 620 } |
615 | 621 |
616 @Override | 622 @Override |
617 public boolean performContextMenuAction(int id) { | 623 public boolean performContextMenuAction(int id) { |
618 switch (id) { | 624 switch (id) { |
619 case android.R.id.selectAll: | 625 case android.R.id.selectAll: |
620 return mImeAdapter.selectAll(); | 626 return mImeAdapter.selectAll(); |
621 case android.R.id.cut: | 627 case android.R.id.cut: |
622 return mImeAdapter.cut(); | 628 return mImeAdapter.cut(); |
623 case android.R.id.copy: | 629 case android.R.id.copy: |
(...skipping 50 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
674 if (selectionStart > selectionEnd) { | 680 if (selectionStart > selectionEnd) { |
675 int temp = selectionStart; | 681 int temp = selectionStart; |
676 selectionStart = selectionEnd; | 682 selectionStart = selectionEnd; |
677 selectionEnd = temp; | 683 selectionEnd = temp; |
678 } | 684 } |
679 editable.replace(selectionStart, selectionEnd, | 685 editable.replace(selectionStart, selectionEnd, |
680 Character.toString((char)unicodeChar)); | 686 Character.toString((char)unicodeChar)); |
681 } | 687 } |
682 } | 688 } |
683 } | 689 } |
684 return super.sendKeyEvent(event); | 690 mImeAdapter.translateAndSendNativeEvents(event); |
| 691 return true; |
685 } | 692 } |
686 | 693 |
687 @Override | 694 @Override |
688 public boolean finishComposingText() { | 695 public boolean finishComposingText() { |
689 if (mEditable == null | 696 if (mEditable == null |
690 || (getComposingSpanStart(mEditable) == getComposingSpanEnd(
mEditable))) { | 697 || (getComposingSpanStart(mEditable) == getComposingSpanEnd(
mEditable))) { |
691 return true; | 698 return true; |
692 } | 699 } |
693 super.finishComposingText(); | 700 super.finishComposingText(); |
694 return mImeAdapter.checkCompositionQueueAndCallNative("", 0, true); | 701 return mImeAdapter.checkCompositionQueueAndCallNative("", 0, true); |
(...skipping 109 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
804 | 811 |
805 private native void nativeDeleteSurroundingText(int nativeImeAdapterAndroid, | 812 private native void nativeDeleteSurroundingText(int nativeImeAdapterAndroid, |
806 int before, int after); | 813 int before, int after); |
807 | 814 |
808 private native void nativeUnselect(int nativeImeAdapterAndroid); | 815 private native void nativeUnselect(int nativeImeAdapterAndroid); |
809 private native void nativeSelectAll(int nativeImeAdapterAndroid); | 816 private native void nativeSelectAll(int nativeImeAdapterAndroid); |
810 private native void nativeCut(int nativeImeAdapterAndroid); | 817 private native void nativeCut(int nativeImeAdapterAndroid); |
811 private native void nativeCopy(int nativeImeAdapterAndroid); | 818 private native void nativeCopy(int nativeImeAdapterAndroid); |
812 private native void nativePaste(int nativeImeAdapterAndroid); | 819 private native void nativePaste(int nativeImeAdapterAndroid); |
813 } | 820 } |
OLD | NEW |