1 // Copyright 2012 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.
5 package org.chromium.content.browser.input;
7 import android.os.Handler;
8 import android.os.ResultReceiver;
9 import android.os.SystemClock;
10 import android.text.Editable;
11 import android.view.KeyCharacterMap;
12 import android.view.KeyEvent;
13 import android.view.View;
14 import android.view.inputmethod.EditorInfo;
16 import com.google.common.annotations.VisibleForTesting;
18 import org.chromium.base.CalledByNative;
19 import org.chromium.base.JNINamespace;
22 * Adapts and plumbs android IME service onto the chrome text input API.
23 * ImeAdapter provides an interface in both ways native <-> java:
24 * 1. InputConnectionAdapter notifies native code of text composition state and
25 * dispatch key events from java -> WebKit.
26 * 2. Native ImeAdapter notifies java side to clear composition text.
29 * 1. When InputConnectionAdapter gets called with composition or result text:
30 * If we receive a composition text or a result text, then we just need to
31 * dispatch a synthetic key event with special keycode 229, and then dispatch
32 * the composition or result text.
33 * 2. Intercept dispatchKeyEvent() method for key events not handled by IME, we
34 * need to dispatch them to webkit and check webkit's reply. Then inject a
35 * new key event for further processing if webkit didn't handle it.
37 * Note that the native peer object does not take any strong reference onto the
38 * instance of this java object, hence it is up to the client of this class (e.g.
39 * the ViewEmbedder implementor) to hold a strong reference to it for the required
40 * lifetime of the object.
42 @JNINamespace("content")
43 public class ImeAdapter {
46 * Interface for the delegate that needs to be notified of IME changes.
48 public interface ImeAdapterDelegate {
50 * @param isFinish whether the event is occurring because input is finished.
52 void onImeEvent(boolean isFinish);
55 * Called when a request to hide the keyboard is sent to InputMethodManager.
57 void onDismissInput();
60 * @return View that the keyboard should be attached to.
62 View getAttachedView();
65 * @return Object that should be called for all keyboard show and hide requests.
67 ResultReceiver getNewShowKeyboardReceiver();
70 private class DelayedDismissInput implements Runnable {
71 private final long mNativeImeAdapter;
73 DelayedDismissInput(long nativeImeAdapter) {
74 mNativeImeAdapter = nativeImeAdapter;
79 attach(mNativeImeAdapter, sTextInputTypeNone);
84 private static final int COMPOSITION_KEY_CODE = 229;
86 // Delay introduced to avoid hiding the keyboard if new show requests are received.
87 // The time required by the unfocus-focus events triggered by tab has been measured in soju:
88 // Mean: 18.633 ms, Standard deviation: 7.9837 ms.
89 // The value here should be higher enough to cover these cases, but not too high to avoid
90 // letting the user perceiving important delays.
91 private static final int INPUT_DISMISS_DELAY = 150;
93 // All the constants that are retrieved from the C++ code.
94 // They get set through initializeWebInputEvents and initializeTextInputTypes calls.
95 static int sEventTypeRawKeyDown;
96 static int sEventTypeKeyUp;
97 static int sEventTypeChar;
98 static int sTextInputTypeNone;
99 static int sTextInputTypeText;
100 static int sTextInputTypeTextArea;
101 static int sTextInputTypePassword;
102 static int sTextInputTypeSearch;
103 static int sTextInputTypeUrl;
104 static int sTextInputTypeEmail;
105 static int sTextInputTypeTel;
106 static int sTextInputTypeNumber;
107 static int sTextInputTypeContentEditable;
108 static int sModifierShift;
109 static int sModifierAlt;
110 static int sModifierCtrl;
111 static int sModifierCapsLockOn;
112 static int sModifierNumLockOn;
114 private long mNativeImeAdapterAndroid;
115 private InputMethodManagerWrapper mInputMethodManagerWrapper;
116 private AdapterInputConnection mInputConnection;
117 private final ImeAdapterDelegate mViewEmbedder;
118 private final Handler mHandler;
119 private DelayedDismissInput mDismissInput = null;
120 private int mTextInputType;
123 boolean mIsShowWithoutHideOutstanding = false;
126 * @param wrapper InputMethodManagerWrapper that should receive all the call directed to
127 * InputMethodManager.
128 * @param embedder The view that is used for callbacks from ImeAdapter.
130 public ImeAdapter(InputMethodManagerWrapper wrapper, ImeAdapterDelegate embedder) {
131 mInputMethodManagerWrapper = wrapper;
132 mViewEmbedder = embedder;
133 mHandler = new Handler();
137 * Default factory for AdapterInputConnection classes.
139 public static class AdapterInputConnectionFactory {
140 public AdapterInputConnection get(View view, ImeAdapter imeAdapter,
141 Editable editable, EditorInfo outAttrs) {
142 return new AdapterInputConnection(view, imeAdapter, editable, outAttrs);
147 * Overrides the InputMethodManagerWrapper that ImeAdapter uses to make calls to
148 * InputMethodManager.
149 * @param immw InputMethodManagerWrapper that should be used to call InputMethodManager.
152 public void setInputMethodManagerWrapper(InputMethodManagerWrapper immw) {
153 mInputMethodManagerWrapper = immw;
157 * Should be only used by AdapterInputConnection.
158 * @return InputMethodManagerWrapper that should receive all the calls directed to
159 * InputMethodManager.
161 InputMethodManagerWrapper getInputMethodManagerWrapper() {
162 return mInputMethodManagerWrapper;
166 * Set the current active InputConnection when a new InputConnection is constructed.
167 * @param inputConnection The input connection that is currently used with IME.
169 void setInputConnection(AdapterInputConnection inputConnection) {
170 mInputConnection = inputConnection;
174 * Should be only used by AdapterInputConnection.
175 * @return The input type of currently focused element.
177 int getTextInputType() {
178 return mTextInputType;
182 * @return Constant representing that a focused node is not an input field.
184 public static int getTextInputTypeNone() {
185 return sTextInputTypeNone;
188 private static int getModifiers(int metaState) {
190 if ((metaState & KeyEvent.META_SHIFT_ON) != 0) {
191 modifiers |= sModifierShift;
193 if ((metaState & KeyEvent.META_ALT_ON) != 0) {
194 modifiers |= sModifierAlt;
196 if ((metaState & KeyEvent.META_CTRL_ON) != 0) {
197 modifiers |= sModifierCtrl;
199 if ((metaState & KeyEvent.META_CAPS_LOCK_ON) != 0) {
200 modifiers |= sModifierCapsLockOn;
202 if ((metaState & KeyEvent.META_NUM_LOCK_ON) != 0) {
203 modifiers |= sModifierNumLockOn;
209 * Shows or hides the keyboard based on passed parameters.
210 * @param nativeImeAdapter Pointer to the ImeAdapterAndroid object that is sending the update.
211 * @param textInputType Text input type for the currently focused field in renderer.
212 * @param showIfNeeded Whether the keyboard should be shown if it is currently hidden.
214 public void updateKeyboardVisibility(long nativeImeAdapter, int textInputType,
215 boolean showIfNeeded) {
216 mHandler.removeCallbacks(mDismissInput);
218 // If current input type is none and showIfNeeded is false, IME should not be shown
219 // and input type should remain as none.
220 if (mTextInputType == sTextInputTypeNone && !showIfNeeded) {
224 if (mNativeImeAdapterAndroid != nativeImeAdapter || mTextInputType != textInputType) {
225 // Set a delayed task to perform unfocus. This avoids hiding the keyboard when tabbing
226 // through text inputs or when JS rapidly changes focus to another text element.
227 if (textInputType == sTextInputTypeNone) {
228 mDismissInput = new DelayedDismissInput(nativeImeAdapter);
229 mHandler.postDelayed(mDismissInput, INPUT_DISMISS_DELAY);
233 attach(nativeImeAdapter, textInputType);
235 mInputMethodManagerWrapper.restartInput(mViewEmbedder.getAttachedView());
239 } else if (hasInputType() && showIfNeeded) {
244 public void attach(long nativeImeAdapter, int textInputType) {
245 if (mNativeImeAdapterAndroid != 0) {
246 nativeResetImeAdapter(mNativeImeAdapterAndroid);
248 mNativeImeAdapterAndroid = nativeImeAdapter;
249 mTextInputType = textInputType;
250 if (nativeImeAdapter != 0) {
251 nativeAttachImeAdapter(mNativeImeAdapterAndroid);
256 * Attaches the imeAdapter to its native counterpart. This is needed to start forwarding
257 * keyboard events to WebKit.
258 * @param nativeImeAdapter The pointer to the native ImeAdapter object.
260 public void attach(long nativeImeAdapter) {
261 if (mNativeImeAdapterAndroid != 0) {
262 nativeResetImeAdapter(mNativeImeAdapterAndroid);
264 mNativeImeAdapterAndroid = nativeImeAdapter;
265 if (nativeImeAdapter != 0) {
266 nativeAttachImeAdapter(mNativeImeAdapterAndroid);
270 private void showKeyboard() {
271 mIsShowWithoutHideOutstanding = true;
272 mInputMethodManagerWrapper.showSoftInput(mViewEmbedder.getAttachedView(), 0,
273 mViewEmbedder.getNewShowKeyboardReceiver());
276 private void dismissInput(boolean unzoomIfNeeded) {
277 mIsShowWithoutHideOutstanding = false;
278 View view = mViewEmbedder.getAttachedView();
279 if (mInputMethodManagerWrapper.isActive(view)) {
280 mInputMethodManagerWrapper.hideSoftInputFromWindow(view.getWindowToken(), 0,
281 unzoomIfNeeded ? mViewEmbedder.getNewShowKeyboardReceiver() : null);
283 mViewEmbedder.onDismissInput();
286 private boolean hasInputType() {
287 return mTextInputType != sTextInputTypeNone;
290 private static boolean isTextInputType(int type) {
291 return type != sTextInputTypeNone && !InputDialogContainer.isDialogInputType(type);
294 public boolean hasTextInputType() {
295 return isTextInputType(mTextInputType);
298 public boolean dispatchKeyEvent(KeyEvent event) {
299 return translateAndSendNativeEvents(event);
302 private int shouldSendKeyEventWithKeyCode(String text) {
303 if (text.length() != 1) return COMPOSITION_KEY_CODE;
305 if (text.equals("\n")) return KeyEvent.KEYCODE_ENTER;
306 else if (text.equals("\t")) return KeyEvent.KEYCODE_TAB;
307 else return COMPOSITION_KEY_CODE;
310 void sendKeyEventWithKeyCode(int keyCode, int flags) {
311 long eventTime = SystemClock.uptimeMillis();
312 translateAndSendNativeEvents(new KeyEvent(eventTime, eventTime,
313 KeyEvent.ACTION_DOWN, keyCode, 0, 0,
314 KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
316 translateAndSendNativeEvents(new KeyEvent(SystemClock.uptimeMillis(), eventTime,
317 KeyEvent.ACTION_UP, keyCode, 0, 0,
318 KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
322 // Calls from Java to C++
324 boolean checkCompositionQueueAndCallNative(String text, int newCursorPosition,
326 if (mNativeImeAdapterAndroid == 0) return false;
328 // Committing an empty string finishes the current composition.
329 boolean isFinish = text.isEmpty();
330 mViewEmbedder.onImeEvent(isFinish);
331 int keyCode = shouldSendKeyEventWithKeyCode(text);
332 long timeStampMs = SystemClock.uptimeMillis();
334 if (keyCode != COMPOSITION_KEY_CODE) {
335 sendKeyEventWithKeyCode(keyCode,
336 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE);
338 nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeRawKeyDown,
339 timeStampMs, keyCode, 0);
341 nativeCommitText(mNativeImeAdapterAndroid, text);
343 nativeSetComposingText(mNativeImeAdapterAndroid, text, newCursorPosition);
345 nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeKeyUp,
346 timeStampMs, keyCode, 0);
352 void finishComposingText() {
353 if (mNativeImeAdapterAndroid == 0) return;
354 nativeFinishComposingText(mNativeImeAdapterAndroid);
357 boolean translateAndSendNativeEvents(KeyEvent event) {
358 if (mNativeImeAdapterAndroid == 0) return false;
360 int action = event.getAction();
361 if (action != KeyEvent.ACTION_DOWN &&
362 action != KeyEvent.ACTION_UP) {
363 // action == KeyEvent.ACTION_MULTIPLE
364 // TODO(bulach): confirm the actual behavior. Apparently:
365 // If event.getKeyCode() == KEYCODE_UNKNOWN, we can send a
366 // composition key down (229) followed by a commit text with the
367 // string from event.getUnicodeChars().
368 // Otherwise, we'd need to send an event with a
369 // WebInputEvent::IsAutoRepeat modifier. We also need to verify when
370 // we receive ACTION_MULTIPLE: we may receive it after an ACTION_DOWN,
371 // and if that's the case, we'll need to review when to send the Char
375 mViewEmbedder.onImeEvent(false);
376 return nativeSendKeyEvent(mNativeImeAdapterAndroid, event, event.getAction(),
377 getModifiers(event.getMetaState()), event.getEventTime(), event.getKeyCode(),
378 /*isSystemKey=*/false, event.getUnicodeChar());
381 boolean sendSyntheticKeyEvent(int eventType, long timestampMs, int keyCode, int unicodeChar) {
382 if (mNativeImeAdapterAndroid == 0) return false;
384 nativeSendSyntheticKeyEvent(
385 mNativeImeAdapterAndroid, eventType, timestampMs, keyCode, unicodeChar);
390 * Send a request to the native counterpart to delete a given range of characters.
391 * @param beforeLength Number of characters to extend the selection by before the existing
393 * @param afterLength Number of characters to extend the selection by after the existing
395 * @return Whether the native counterpart of ImeAdapter received the call.
397 boolean deleteSurroundingText(int beforeLength, int afterLength) {
398 if (mNativeImeAdapterAndroid == 0) return false;
399 nativeDeleteSurroundingText(mNativeImeAdapterAndroid, beforeLength, afterLength);
404 * Send a request to the native counterpart to set the selection to given range.
405 * @param start Selection start index.
406 * @param end Selection end index.
407 * @return Whether the native counterpart of ImeAdapter received the call.
409 boolean setEditableSelectionOffsets(int start, int end) {
410 if (mNativeImeAdapterAndroid == 0) return false;
411 nativeSetEditableSelectionOffsets(mNativeImeAdapterAndroid, start, end);
416 * Send a request to the native counterpart to set compositing region to given indices.
417 * @param start The start of the composition.
418 * @param end The end of the composition.
419 * @return Whether the native counterpart of ImeAdapter received the call.
421 boolean setComposingRegion(int start, int end) {
422 if (mNativeImeAdapterAndroid == 0) return false;
423 nativeSetComposingRegion(mNativeImeAdapterAndroid, start, end);
428 * Send a request to the native counterpart to unselect text.
429 * @return Whether the native counterpart of ImeAdapter received the call.
431 public boolean unselect() {
432 if (mNativeImeAdapterAndroid == 0) return false;
433 nativeUnselect(mNativeImeAdapterAndroid);
438 * Send a request to the native counterpart of ImeAdapter to select all the text.
439 * @return Whether the native counterpart of ImeAdapter received the call.
441 public boolean selectAll() {
442 if (mNativeImeAdapterAndroid == 0) return false;
443 nativeSelectAll(mNativeImeAdapterAndroid);
448 * Send a request to the native counterpart of ImeAdapter to cut the selected text.
449 * @return Whether the native counterpart of ImeAdapter received the call.
451 public boolean cut() {
452 if (mNativeImeAdapterAndroid == 0) return false;
453 nativeCut(mNativeImeAdapterAndroid);
458 * Send a request to the native counterpart of ImeAdapter to copy the selected text.
459 * @return Whether the native counterpart of ImeAdapter received the call.
461 public boolean copy() {
462 if (mNativeImeAdapterAndroid == 0) return false;
463 nativeCopy(mNativeImeAdapterAndroid);
468 * Send a request to the native counterpart of ImeAdapter to paste the text from the clipboard.
469 * @return Whether the native counterpart of ImeAdapter received the call.
471 public boolean paste() {
472 if (mNativeImeAdapterAndroid == 0) return false;
473 nativePaste(mNativeImeAdapterAndroid);
477 // Calls from C++ to Java
480 private static void initializeWebInputEvents(int eventTypeRawKeyDown, int eventTypeKeyUp,
481 int eventTypeChar, int modifierShift, int modifierAlt, int modifierCtrl,
482 int modifierCapsLockOn, int modifierNumLockOn) {
483 sEventTypeRawKeyDown = eventTypeRawKeyDown;
484 sEventTypeKeyUp = eventTypeKeyUp;
485 sEventTypeChar = eventTypeChar;
486 sModifierShift = modifierShift;
487 sModifierAlt = modifierAlt;
488 sModifierCtrl = modifierCtrl;
489 sModifierCapsLockOn = modifierCapsLockOn;
490 sModifierNumLockOn = modifierNumLockOn;
494 private static void initializeTextInputTypes(int textInputTypeNone, int textInputTypeText,
495 int textInputTypeTextArea, int textInputTypePassword, int textInputTypeSearch,
496 int textInputTypeUrl, int textInputTypeEmail, int textInputTypeTel,
497 int textInputTypeNumber, int textInputTypeContentEditable) {
498 sTextInputTypeNone = textInputTypeNone;
499 sTextInputTypeText = textInputTypeText;
500 sTextInputTypeTextArea = textInputTypeTextArea;
501 sTextInputTypePassword = textInputTypePassword;
502 sTextInputTypeSearch = textInputTypeSearch;
503 sTextInputTypeUrl = textInputTypeUrl;
504 sTextInputTypeEmail = textInputTypeEmail;
505 sTextInputTypeTel = textInputTypeTel;
506 sTextInputTypeNumber = textInputTypeNumber;
507 sTextInputTypeContentEditable = textInputTypeContentEditable;
511 private void focusedNodeChanged(boolean isEditable) {
512 if (mInputConnection != null && isEditable) mInputConnection.restartInput();
516 private void cancelComposition() {
517 if (mInputConnection != null) mInputConnection.restartInput();
522 if (mDismissInput != null) mHandler.removeCallbacks(mDismissInput);
523 mNativeImeAdapterAndroid = 0;
527 private native boolean nativeSendSyntheticKeyEvent(long nativeImeAdapterAndroid,
528 int eventType, long timestampMs, int keyCode, int unicodeChar);
530 private native boolean nativeSendKeyEvent(long nativeImeAdapterAndroid, KeyEvent event,
531 int action, int modifiers, long timestampMs, int keyCode, boolean isSystemKey,
534 private native void nativeSetComposingText(long nativeImeAdapterAndroid, String text,
535 int newCursorPosition);
537 private native void nativeCommitText(long nativeImeAdapterAndroid, String text);
539 private native void nativeFinishComposingText(long nativeImeAdapterAndroid);
541 private native void nativeAttachImeAdapter(long nativeImeAdapterAndroid);
543 private native void nativeSetEditableSelectionOffsets(long nativeImeAdapterAndroid,
546 private native void nativeSetComposingRegion(long nativeImeAdapterAndroid, int start, int end);
548 private native void nativeDeleteSurroundingText(long nativeImeAdapterAndroid,
549 int before, int after);
551 private native void nativeUnselect(long nativeImeAdapterAndroid);
552 private native void nativeSelectAll(long nativeImeAdapterAndroid);
553 private native void nativeCut(long nativeImeAdapterAndroid);
554 private native void nativeCopy(long nativeImeAdapterAndroid);
555 private native void nativePaste(long nativeImeAdapterAndroid);
556 private native void nativeResetImeAdapter(long nativeImeAdapterAndroid);