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