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