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