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