ImeAdapter.java revision 7dbb3d5cf0c15f500944d211057644d6a2f37371
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
3// found in the LICENSE file.
4
5package org.chromium.content.browser.input;
6
7import android.os.Handler;
8import android.os.ResultReceiver;
9import android.view.KeyCharacterMap;
10import android.view.KeyEvent;
11import android.view.View;
12import android.view.inputmethod.EditorInfo;
13
14import com.google.common.annotations.VisibleForTesting;
15
16import org.chromium.base.CalledByNative;
17import org.chromium.base.JNINamespace;
18
19/**
20 * Adapts and plumbs android IME service onto the chrome text input API.
21 * ImeAdapter provides an interface in both ways native <-> java:
22 * 1. InputConnectionAdapter notifies native code of text composition state and
23 *    dispatch key events from java -> WebKit.
24 * 2. Native ImeAdapter notifies java side to clear composition text.
25 *
26 * The basic flow is:
27 * 1. When InputConnectionAdapter gets called with composition or result text:
28 *    If we receive a composition text or a result text, then we just need to
29 *    dispatch a synthetic key event with special keycode 229, and then dispatch
30 *    the composition or result text.
31 * 2. Intercept dispatchKeyEvent() method for key events not handled by IME, we
32 *   need to dispatch them to webkit and check webkit's reply. Then inject a
33 *   new key event for further processing if webkit didn't handle it.
34 *
35 * Note that the native peer object does not take any strong reference onto the
36 * instance of this java object, hence it is up to the client of this class (e.g.
37 * the ViewEmbedder implementor) to hold a strong reference to it for the required
38 * lifetime of the object.
39 */
40@JNINamespace("content")
41public class ImeAdapter {
42    public interface ImeAdapterDelegate {
43        /**
44         * @param isFinish whether the event is occurring because input is finished.
45         */
46        void onImeEvent(boolean isFinish);
47        void onSetFieldValue();
48        void onDismissInput();
49        View getAttachedView();
50        ResultReceiver getNewShowKeyboardReceiver();
51        void hideSelectionAndInsertionHandles();
52    }
53
54    private class DelayedDismissInput implements Runnable {
55        private final int mNativeImeAdapter;
56
57        DelayedDismissInput(int nativeImeAdapter) {
58            mNativeImeAdapter = nativeImeAdapter;
59        }
60
61        @Override
62        public void run() {
63            attach(mNativeImeAdapter, sTextInputTypeNone, AdapterInputConnection.INVALID_SELECTION,
64                    AdapterInputConnection.INVALID_SELECTION);
65            dismissInput(true);
66        }
67    }
68
69    private static final int COMPOSITION_KEY_CODE = 229;
70
71    // Delay introduced to avoid hiding the keyboard if new show requests are received.
72    // The time required by the unfocus-focus events triggered by tab has been measured in soju:
73    // Mean: 18.633 ms, Standard deviation: 7.9837 ms.
74    // The value here should be higher enough to cover these cases, but not too high to avoid
75    // letting the user perceiving important delays.
76    private static final int INPUT_DISMISS_DELAY = 150;
77
78    // All the constants that are retrieved from the C++ code.
79    // They get set through initializeWebInputEvents and initializeTextInputTypes calls.
80    static int sEventTypeRawKeyDown;
81    static int sEventTypeKeyUp;
82    static int sEventTypeChar;
83    static int sTextInputTypeNone;
84    static int sTextInputTypeText;
85    static int sTextInputTypeTextArea;
86    static int sTextInputTypePassword;
87    static int sTextInputTypeSearch;
88    static int sTextInputTypeUrl;
89    static int sTextInputTypeEmail;
90    static int sTextInputTypeTel;
91    static int sTextInputTypeNumber;
92    static int sTextInputTypeWeek;
93    static int sTextInputTypeContentEditable;
94    static int sModifierShift;
95    static int sModifierAlt;
96    static int sModifierCtrl;
97    static int sModifierCapsLockOn;
98    static int sModifierNumLockOn;
99
100    private int mNativeImeAdapterAndroid;
101    private InputMethodManagerWrapper mInputMethodManagerWrapper;
102    private AdapterInputConnection mInputConnection;
103    private final ImeAdapterDelegate mViewEmbedder;
104    private final Handler mHandler;
105    private DelayedDismissInput mDismissInput = null;
106    private int mTextInputType;
107    private int mInitialSelectionStart;
108    private int mInitialSelectionEnd;
109
110    @VisibleForTesting
111    boolean mIsShowWithoutHideOutstanding = false;
112
113    /**
114     * @param wrapper InputMethodManagerWrapper that should receive all the call directed to
115     *                InputMethodManager.
116     * @param embedder The view that is used for callbacks from ImeAdapter.
117     */
118    public ImeAdapter(InputMethodManagerWrapper wrapper, ImeAdapterDelegate embedder) {
119        mInputMethodManagerWrapper = wrapper;
120        mViewEmbedder = embedder;
121        mHandler = new Handler();
122    }
123
124    public static class AdapterInputConnectionFactory {
125        public AdapterInputConnection get(View view, ImeAdapter imeAdapter,
126                EditorInfo outAttrs) {
127            return new AdapterInputConnection(view, imeAdapter, outAttrs);
128        }
129    }
130
131    @VisibleForTesting
132    public void setInputMethodManagerWrapper(InputMethodManagerWrapper immw) {
133        mInputMethodManagerWrapper = immw;
134    }
135
136    /**
137     * Should be only used by AdapterInputConnection.
138     * @return InputMethodManagerWrapper that should receive all the calls directed to
139     *         InputMethodManager.
140     */
141    InputMethodManagerWrapper getInputMethodManagerWrapper() {
142        return mInputMethodManagerWrapper;
143    }
144
145    /**
146     * Set the current active InputConnection when a new InputConnection is constructed.
147     * @param inputConnection The input connection that is currently used with IME.
148     */
149    void setInputConnection(AdapterInputConnection inputConnection) {
150        mInputConnection = inputConnection;
151    }
152
153    /**
154     * Should be only used by AdapterInputConnection.
155     * @return The input type of currently focused element.
156     */
157    int getTextInputType() {
158        return mTextInputType;
159    }
160
161    /**
162     * Should be only used by AdapterInputConnection.
163     * @return The starting index of the initial text selection.
164     */
165    int getInitialSelectionStart() {
166        return mInitialSelectionStart;
167    }
168
169    /**
170     * Should be only used by AdapterInputConnection.
171     * @return The ending index of the initial text selection.
172     */
173    int getInitialSelectionEnd() {
174        return mInitialSelectionEnd;
175    }
176
177    public static int getTextInputTypeNone() {
178        return sTextInputTypeNone;
179    }
180
181    private static int getModifiers(int metaState) {
182        int modifiers = 0;
183        if ((metaState & KeyEvent.META_SHIFT_ON) != 0) {
184          modifiers |= sModifierShift;
185        }
186        if ((metaState & KeyEvent.META_ALT_ON) != 0) {
187          modifiers |= sModifierAlt;
188        }
189        if ((metaState & KeyEvent.META_CTRL_ON) != 0) {
190          modifiers |= sModifierCtrl;
191        }
192        if ((metaState & KeyEvent.META_CAPS_LOCK_ON) != 0) {
193          modifiers |= sModifierCapsLockOn;
194        }
195        if ((metaState & KeyEvent.META_NUM_LOCK_ON) != 0) {
196          modifiers |= sModifierNumLockOn;
197        }
198        return modifiers;
199    }
200
201    void hideSelectionAndInsertionHandleControllers() {
202        mViewEmbedder.hideSelectionAndInsertionHandles();
203    }
204
205    public boolean isActive() {
206        return mInputConnection != null && mInputConnection.isActive();
207    }
208
209    private boolean isFor(int nativeImeAdapter, int textInputType) {
210        return mNativeImeAdapterAndroid == nativeImeAdapter &&
211               mTextInputType == textInputType;
212    }
213
214    public void attachAndShowIfNeeded(int nativeImeAdapter, int textInputType,
215            int selectionStart, int selectionEnd, boolean showIfNeeded) {
216        mHandler.removeCallbacks(mDismissInput);
217
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) {
221            return;
222        }
223
224        if (!isFor(nativeImeAdapter, 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);
230                return;
231            }
232
233            int previousType = mTextInputType;
234            attach(nativeImeAdapter, textInputType, selectionStart, selectionEnd);
235
236            mInputMethodManagerWrapper.restartInput(mViewEmbedder.getAttachedView());
237            if (showIfNeeded) {
238                showKeyboard();
239            }
240        } else if (hasInputType() && showIfNeeded) {
241            showKeyboard();
242        }
243    }
244
245    public void attach(int nativeImeAdapter, int textInputType, int selectionStart,
246            int selectionEnd) {
247        if (mNativeImeAdapterAndroid != 0) {
248            nativeResetImeAdapter(mNativeImeAdapterAndroid);
249        }
250        mNativeImeAdapterAndroid = nativeImeAdapter;
251        mTextInputType = textInputType;
252        mInitialSelectionStart = selectionStart;
253        mInitialSelectionEnd = selectionEnd;
254        nativeAttachImeAdapter(mNativeImeAdapterAndroid);
255    }
256
257    /**
258     * Attaches the imeAdapter to its native counterpart. This is needed to start forwarding
259     * keyboard events to WebKit.
260     * @param nativeImeAdapter The pointer to the native ImeAdapter object.
261     */
262    public void attach(int nativeImeAdapter) {
263        if (mNativeImeAdapterAndroid != 0) {
264            nativeResetImeAdapter(mNativeImeAdapterAndroid);
265        }
266        mNativeImeAdapterAndroid = nativeImeAdapter;
267        if (nativeImeAdapter != 0) {
268            nativeAttachImeAdapter(mNativeImeAdapterAndroid);
269        }
270    }
271
272    /**
273     * Used to check whether the native counterpart of the ImeAdapter has been attached yet.
274     * @return Whether native ImeAdapter has been attached and its pointer is currently nonzero.
275     */
276    public boolean isNativeImeAdapterAttached() {
277        return mNativeImeAdapterAndroid != 0;
278    }
279
280    private void showKeyboard() {
281        mIsShowWithoutHideOutstanding = true;
282        mInputMethodManagerWrapper.showSoftInput(mViewEmbedder.getAttachedView(), 0,
283                mViewEmbedder.getNewShowKeyboardReceiver());
284    }
285
286    private void dismissInput(boolean unzoomIfNeeded) {
287        hideKeyboard(unzoomIfNeeded);
288        mViewEmbedder.onDismissInput();
289    }
290
291    private void hideKeyboard(boolean unzoomIfNeeded) {
292        mIsShowWithoutHideOutstanding  = false;
293        View view = mViewEmbedder.getAttachedView();
294        if (mInputMethodManagerWrapper.isActive(view)) {
295            mInputMethodManagerWrapper.hideSoftInputFromWindow(view.getWindowToken(), 0,
296                    unzoomIfNeeded ? mViewEmbedder.getNewShowKeyboardReceiver() : null);
297        }
298    }
299
300    private boolean hasInputType() {
301        return mTextInputType != sTextInputTypeNone;
302    }
303
304    static boolean isTextInputType(int type) {
305        return type != sTextInputTypeNone && !InputDialogContainer.isDialogInputType(type);
306    }
307
308    public boolean hasTextInputType() {
309        return isTextInputType(mTextInputType);
310    }
311
312    public boolean dispatchKeyEvent(KeyEvent event) {
313        return translateAndSendNativeEvents(event);
314    }
315
316    private int shouldSendKeyEventWithKeyCode(String text) {
317        if (text.length() != 1) return COMPOSITION_KEY_CODE;
318
319        if (text.equals("\n")) return KeyEvent.KEYCODE_ENTER;
320        else if (text.equals("\t")) return KeyEvent.KEYCODE_TAB;
321        else return COMPOSITION_KEY_CODE;
322    }
323
324    void sendKeyEventWithKeyCode(int keyCode, int flags) {
325        long eventTime = System.currentTimeMillis();
326        translateAndSendNativeEvents(new KeyEvent(eventTime, eventTime,
327                KeyEvent.ACTION_DOWN, keyCode, 0, 0,
328                KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
329                flags));
330        translateAndSendNativeEvents(new KeyEvent(System.currentTimeMillis(), eventTime,
331                KeyEvent.ACTION_UP, keyCode, 0, 0,
332                KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
333                flags));
334    }
335
336    // Calls from Java to C++
337
338    @VisibleForTesting
339    boolean checkCompositionQueueAndCallNative(String text, int newCursorPosition,
340            boolean isCommit) {
341        if (mNativeImeAdapterAndroid == 0) return false;
342
343        // Committing an empty string finishes the current composition.
344        boolean isFinish = text.isEmpty();
345        if (!isFinish) {
346            mViewEmbedder.hideSelectionAndInsertionHandles();
347        }
348        mViewEmbedder.onImeEvent(isFinish);
349        int keyCode = shouldSendKeyEventWithKeyCode(text);
350        long timeStampMs = System.currentTimeMillis();
351
352        if (keyCode != COMPOSITION_KEY_CODE) {
353            sendKeyEventWithKeyCode(keyCode,
354                    KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE);
355        } else {
356            nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeRawKeyDown,
357                    timeStampMs, keyCode, 0);
358            if (isCommit) {
359                nativeCommitText(mNativeImeAdapterAndroid, text);
360            } else {
361                nativeSetComposingText(mNativeImeAdapterAndroid, text, newCursorPosition);
362            }
363            nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeKeyUp,
364                    timeStampMs, keyCode, 0);
365        }
366
367        return true;
368    }
369
370    void finishComposingText() {
371        nativeFinishComposingText(mNativeImeAdapterAndroid);
372    }
373
374    boolean translateAndSendNativeEvents(KeyEvent event) {
375        if (mNativeImeAdapterAndroid == 0) return false;
376
377        int action = event.getAction();
378        if (action != KeyEvent.ACTION_DOWN &&
379            action != KeyEvent.ACTION_UP) {
380            // action == KeyEvent.ACTION_MULTIPLE
381            // TODO(bulach): confirm the actual behavior. Apparently:
382            // If event.getKeyCode() == KEYCODE_UNKNOWN, we can send a
383            // composition key down (229) followed by a commit text with the
384            // string from event.getUnicodeChars().
385            // Otherwise, we'd need to send an event with a
386            // WebInputEvent::IsAutoRepeat modifier. We also need to verify when
387            // we receive ACTION_MULTIPLE: we may receive it after an ACTION_DOWN,
388            // and if that's the case, we'll need to review when to send the Char
389            // event.
390            return false;
391        }
392        mViewEmbedder.onImeEvent(false);
393        return nativeSendKeyEvent(mNativeImeAdapterAndroid, event, event.getAction(),
394                getModifiers(event.getMetaState()), event.getEventTime(), event.getKeyCode(),
395                                event.isSystem(), event.getUnicodeChar());
396    }
397
398    boolean sendSyntheticKeyEvent(
399            int eventType, long timestampMs, int keyCode, int unicodeChar) {
400        if (mNativeImeAdapterAndroid == 0) return false;
401
402        nativeSendSyntheticKeyEvent(
403                mNativeImeAdapterAndroid, eventType, timestampMs, keyCode, unicodeChar);
404        return true;
405    }
406
407    boolean deleteSurroundingText(int leftLength, int rightLength) {
408        if (mNativeImeAdapterAndroid == 0) return false;
409        nativeDeleteSurroundingText(mNativeImeAdapterAndroid, leftLength, rightLength);
410        return true;
411    }
412
413    @VisibleForTesting
414    protected boolean setEditableSelectionOffsets(int start, int end) {
415        if (mNativeImeAdapterAndroid == 0) return false;
416        nativeSetEditableSelectionOffsets(mNativeImeAdapterAndroid, start, end);
417        return true;
418    }
419
420    void batchStateChanged(boolean isBegin) {
421        if (mNativeImeAdapterAndroid == 0) return;
422        nativeImeBatchStateChanged(mNativeImeAdapterAndroid, isBegin);
423    }
424
425    void commitText() {
426        cancelComposition();
427        if (mNativeImeAdapterAndroid != 0) {
428            nativeCommitText(mNativeImeAdapterAndroid, "");
429        }
430    }
431
432    /**
433     * Send a request to the native counterpart to set compositing region to given indices.
434     * @param start The start of the composition.
435     * @param end The end of the composition.
436     * @return Whether the native counterpart of ImeAdapter received the call.
437     */
438    boolean setComposingRegion(int start, int end) {
439        if (mNativeImeAdapterAndroid == 0) return false;
440        nativeSetComposingRegion(mNativeImeAdapterAndroid, start, end);
441        return true;
442    }
443
444    /**
445     * Send a request to the native counterpart to unselect text.
446     * @return Whether the native counterpart of ImeAdapter received the call.
447     */
448    public boolean unselect() {
449        if (mNativeImeAdapterAndroid == 0) return false;
450        nativeUnselect(mNativeImeAdapterAndroid);
451        return true;
452    }
453
454    /**
455     * Send a request to the native counterpart of ImeAdapter to select all the text.
456     * @return Whether the native counterpart of ImeAdapter received the call.
457     */
458    public boolean selectAll() {
459        if (mNativeImeAdapterAndroid == 0) return false;
460        nativeSelectAll(mNativeImeAdapterAndroid);
461        return true;
462    }
463
464    /**
465     * Send a request to the native counterpart of ImeAdapter to cut the selected text.
466     * @return Whether the native counterpart of ImeAdapter received the call.
467     */
468    public boolean cut() {
469        if (mNativeImeAdapterAndroid == 0) return false;
470        nativeCut(mNativeImeAdapterAndroid);
471        return true;
472    }
473
474    /**
475     * Send a request to the native counterpart of ImeAdapter to copy the selected text.
476     * @return Whether the native counterpart of ImeAdapter received the call.
477     */
478    public boolean copy() {
479        if (mNativeImeAdapterAndroid == 0) return false;
480        nativeCopy(mNativeImeAdapterAndroid);
481        return true;
482    }
483
484    /**
485     * Send a request to the native counterpart of ImeAdapter to paste the text from the clipboard.
486     * @return Whether the native counterpart of ImeAdapter received the call.
487     */
488    public boolean paste() {
489        if (mNativeImeAdapterAndroid == 0) return false;
490        nativePaste(mNativeImeAdapterAndroid);
491        return true;
492    }
493
494    // Calls from C++ to Java
495
496    @CalledByNative
497    private static void initializeWebInputEvents(int eventTypeRawKeyDown, int eventTypeKeyUp,
498            int eventTypeChar, int modifierShift, int modifierAlt, int modifierCtrl,
499            int modifierCapsLockOn, int modifierNumLockOn) {
500        sEventTypeRawKeyDown = eventTypeRawKeyDown;
501        sEventTypeKeyUp = eventTypeKeyUp;
502        sEventTypeChar = eventTypeChar;
503        sModifierShift = modifierShift;
504        sModifierAlt = modifierAlt;
505        sModifierCtrl = modifierCtrl;
506        sModifierCapsLockOn = modifierCapsLockOn;
507        sModifierNumLockOn = modifierNumLockOn;
508    }
509
510    @CalledByNative
511    private static void initializeTextInputTypes(int textInputTypeNone, int textInputTypeText,
512            int textInputTypeTextArea, int textInputTypePassword, int textInputTypeSearch,
513            int textInputTypeUrl, int textInputTypeEmail, int textInputTypeTel,
514            int textInputTypeNumber, int textInputTypeDate, int textInputTypeDateTime,
515            int textInputTypeDateTimeLocal, int textInputTypeMonth, int textInputTypeTime,
516            int textInputTypeWeek, int textInputTypeContentEditable) {
517        sTextInputTypeNone = textInputTypeNone;
518        sTextInputTypeText = textInputTypeText;
519        sTextInputTypeTextArea = textInputTypeTextArea;
520        sTextInputTypePassword = textInputTypePassword;
521        sTextInputTypeSearch = textInputTypeSearch;
522        sTextInputTypeUrl = textInputTypeUrl;
523        sTextInputTypeEmail = textInputTypeEmail;
524        sTextInputTypeTel = textInputTypeTel;
525        sTextInputTypeNumber = textInputTypeNumber;
526        sTextInputTypeWeek = textInputTypeWeek;
527        sTextInputTypeContentEditable = textInputTypeContentEditable;
528    }
529
530    @CalledByNative
531    private void cancelComposition() {
532        if (mInputConnection != null) {
533            mInputConnection.restartInput();
534        }
535    }
536
537    @CalledByNative
538    void detach() {
539        mNativeImeAdapterAndroid = 0;
540        mTextInputType = 0;
541    }
542
543    private native boolean nativeSendSyntheticKeyEvent(int nativeImeAdapterAndroid,
544            int eventType, long timestampMs, int keyCode, int unicodeChar);
545
546    private native boolean nativeSendKeyEvent(int nativeImeAdapterAndroid, KeyEvent event,
547            int action, int modifiers, long timestampMs, int keyCode, boolean isSystemKey,
548            int unicodeChar);
549
550    private native void nativeSetComposingText(int nativeImeAdapterAndroid, String text,
551            int newCursorPosition);
552
553    private native void nativeCommitText(int nativeImeAdapterAndroid, String text);
554
555    private native void nativeFinishComposingText(int nativeImeAdapterAndroid);
556
557    private native void nativeAttachImeAdapter(int nativeImeAdapterAndroid);
558
559    private native void nativeSetEditableSelectionOffsets(int nativeImeAdapterAndroid,
560            int start, int end);
561
562    private native void nativeSetComposingRegion(int nativeImeAdapterAndroid, int start, int end);
563
564    private native void nativeDeleteSurroundingText(int nativeImeAdapterAndroid,
565            int before, int after);
566
567    private native void nativeImeBatchStateChanged(int nativeImeAdapterAndroid, boolean isBegin);
568
569    private native void nativeUnselect(int nativeImeAdapterAndroid);
570    private native void nativeSelectAll(int nativeImeAdapterAndroid);
571    private native void nativeCut(int nativeImeAdapterAndroid);
572    private native void nativeCopy(int nativeImeAdapterAndroid);
573    private native void nativePaste(int nativeImeAdapterAndroid);
574    private native void nativeResetImeAdapter(int nativeImeAdapterAndroid);
575}
576