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