ImeAdapter.java revision c2db58bd994c04d98e4ee2cd7565b71548655fe3
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        if (mNativeImeAdapterAndroid == 0) return;
366        nativeFinishComposingText(mNativeImeAdapterAndroid);
367    }
368
369    boolean translateAndSendNativeEvents(KeyEvent event) {
370        if (mNativeImeAdapterAndroid == 0) return false;
371
372        int action = event.getAction();
373        if (action != KeyEvent.ACTION_DOWN &&
374            action != KeyEvent.ACTION_UP) {
375            // action == KeyEvent.ACTION_MULTIPLE
376            // TODO(bulach): confirm the actual behavior. Apparently:
377            // If event.getKeyCode() == KEYCODE_UNKNOWN, we can send a
378            // composition key down (229) followed by a commit text with the
379            // string from event.getUnicodeChars().
380            // Otherwise, we'd need to send an event with a
381            // WebInputEvent::IsAutoRepeat modifier. We also need to verify when
382            // we receive ACTION_MULTIPLE: we may receive it after an ACTION_DOWN,
383            // and if that's the case, we'll need to review when to send the Char
384            // event.
385            return false;
386        }
387        mViewEmbedder.onImeEvent(false);
388        return nativeSendKeyEvent(mNativeImeAdapterAndroid, event, event.getAction(),
389                getModifiers(event.getMetaState()), event.getEventTime(), event.getKeyCode(),
390                                event.isSystem(), event.getUnicodeChar());
391    }
392
393    boolean sendSyntheticKeyEvent(
394            int eventType, long timestampMs, int keyCode, int unicodeChar) {
395        if (mNativeImeAdapterAndroid == 0) return false;
396
397        nativeSendSyntheticKeyEvent(
398                mNativeImeAdapterAndroid, eventType, timestampMs, keyCode, unicodeChar);
399        return true;
400    }
401
402    boolean deleteSurroundingText(int leftLength, int rightLength) {
403        if (mNativeImeAdapterAndroid == 0) return false;
404        nativeDeleteSurroundingText(mNativeImeAdapterAndroid, leftLength, rightLength);
405        return true;
406    }
407
408    @VisibleForTesting
409    protected boolean setEditableSelectionOffsets(int start, int end) {
410        if (mNativeImeAdapterAndroid == 0) return false;
411        nativeSetEditableSelectionOffsets(mNativeImeAdapterAndroid, start, end);
412        return true;
413    }
414
415    void batchStateChanged(boolean isBegin) {
416        if (mNativeImeAdapterAndroid == 0) return;
417        nativeImeBatchStateChanged(mNativeImeAdapterAndroid, isBegin);
418    }
419
420    void commitText() {
421        cancelComposition();
422        if (mNativeImeAdapterAndroid != 0) {
423            nativeCommitText(mNativeImeAdapterAndroid, "");
424        }
425    }
426
427    /**
428     * Send a request to the native counterpart to set compositing region to given indices.
429     * @param start The start of the composition.
430     * @param end The end of the composition.
431     * @return Whether the native counterpart of ImeAdapter received the call.
432     */
433    boolean setComposingRegion(int start, int end) {
434        if (mNativeImeAdapterAndroid == 0) return false;
435        nativeSetComposingRegion(mNativeImeAdapterAndroid, start, end);
436        return true;
437    }
438
439    /**
440     * Send a request to the native counterpart to unselect text.
441     * @return Whether the native counterpart of ImeAdapter received the call.
442     */
443    public boolean unselect() {
444        if (mNativeImeAdapterAndroid == 0) return false;
445        nativeUnselect(mNativeImeAdapterAndroid);
446        return true;
447    }
448
449    /**
450     * Send a request to the native counterpart of ImeAdapter to select all the text.
451     * @return Whether the native counterpart of ImeAdapter received the call.
452     */
453    public boolean selectAll() {
454        if (mNativeImeAdapterAndroid == 0) return false;
455        nativeSelectAll(mNativeImeAdapterAndroid);
456        return true;
457    }
458
459    /**
460     * Send a request to the native counterpart of ImeAdapter to cut the selected text.
461     * @return Whether the native counterpart of ImeAdapter received the call.
462     */
463    public boolean cut() {
464        if (mNativeImeAdapterAndroid == 0) return false;
465        nativeCut(mNativeImeAdapterAndroid);
466        return true;
467    }
468
469    /**
470     * Send a request to the native counterpart of ImeAdapter to copy the selected text.
471     * @return Whether the native counterpart of ImeAdapter received the call.
472     */
473    public boolean copy() {
474        if (mNativeImeAdapterAndroid == 0) return false;
475        nativeCopy(mNativeImeAdapterAndroid);
476        return true;
477    }
478
479    /**
480     * Send a request to the native counterpart of ImeAdapter to paste the text from the clipboard.
481     * @return Whether the native counterpart of ImeAdapter received the call.
482     */
483    public boolean paste() {
484        if (mNativeImeAdapterAndroid == 0) return false;
485        nativePaste(mNativeImeAdapterAndroid);
486        return true;
487    }
488
489    // Calls from C++ to Java
490
491    @CalledByNative
492    private static void initializeWebInputEvents(int eventTypeRawKeyDown, int eventTypeKeyUp,
493            int eventTypeChar, int modifierShift, int modifierAlt, int modifierCtrl,
494            int modifierCapsLockOn, int modifierNumLockOn) {
495        sEventTypeRawKeyDown = eventTypeRawKeyDown;
496        sEventTypeKeyUp = eventTypeKeyUp;
497        sEventTypeChar = eventTypeChar;
498        sModifierShift = modifierShift;
499        sModifierAlt = modifierAlt;
500        sModifierCtrl = modifierCtrl;
501        sModifierCapsLockOn = modifierCapsLockOn;
502        sModifierNumLockOn = modifierNumLockOn;
503    }
504
505    @CalledByNative
506    private static void initializeTextInputTypes(int textInputTypeNone, int textInputTypeText,
507            int textInputTypeTextArea, int textInputTypePassword, int textInputTypeSearch,
508            int textInputTypeUrl, int textInputTypeEmail, int textInputTypeTel,
509            int textInputTypeNumber, int textInputTypeDate, int textInputTypeDateTime,
510            int textInputTypeDateTimeLocal, int textInputTypeMonth, int textInputTypeTime,
511            int textInputTypeWeek, int textInputTypeContentEditable) {
512        sTextInputTypeNone = textInputTypeNone;
513        sTextInputTypeText = textInputTypeText;
514        sTextInputTypeTextArea = textInputTypeTextArea;
515        sTextInputTypePassword = textInputTypePassword;
516        sTextInputTypeSearch = textInputTypeSearch;
517        sTextInputTypeUrl = textInputTypeUrl;
518        sTextInputTypeEmail = textInputTypeEmail;
519        sTextInputTypeTel = textInputTypeTel;
520        sTextInputTypeNumber = textInputTypeNumber;
521        sTextInputTypeWeek = textInputTypeWeek;
522        sTextInputTypeContentEditable = textInputTypeContentEditable;
523    }
524
525    @CalledByNative
526    private void cancelComposition() {
527        if (mInputConnection != null) {
528            mInputConnection.restartInput();
529        }
530    }
531
532    @CalledByNative
533    void detach() {
534        mNativeImeAdapterAndroid = 0;
535        mTextInputType = 0;
536    }
537
538    private native boolean nativeSendSyntheticKeyEvent(int nativeImeAdapterAndroid,
539            int eventType, long timestampMs, int keyCode, int unicodeChar);
540
541    private native boolean nativeSendKeyEvent(int nativeImeAdapterAndroid, KeyEvent event,
542            int action, int modifiers, long timestampMs, int keyCode, boolean isSystemKey,
543            int unicodeChar);
544
545    private native void nativeSetComposingText(int nativeImeAdapterAndroid, String text,
546            int newCursorPosition);
547
548    private native void nativeCommitText(int nativeImeAdapterAndroid, String text);
549
550    private native void nativeFinishComposingText(int nativeImeAdapterAndroid);
551
552    private native void nativeAttachImeAdapter(int nativeImeAdapterAndroid);
553
554    private native void nativeSetEditableSelectionOffsets(int nativeImeAdapterAndroid,
555            int start, int end);
556
557    private native void nativeSetComposingRegion(int nativeImeAdapterAndroid, int start, int end);
558
559    private native void nativeDeleteSurroundingText(int nativeImeAdapterAndroid,
560            int before, int after);
561
562    private native void nativeImeBatchStateChanged(int nativeImeAdapterAndroid, boolean isBegin);
563
564    private native void nativeUnselect(int nativeImeAdapterAndroid);
565    private native void nativeSelectAll(int nativeImeAdapterAndroid);
566    private native void nativeCut(int nativeImeAdapterAndroid);
567    private native void nativeCopy(int nativeImeAdapterAndroid);
568    private native void nativePaste(int nativeImeAdapterAndroid);
569    private native void nativeResetImeAdapter(int nativeImeAdapterAndroid);
570}
571