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