ImeAdapter.java revision cedac228d2dd51db4b79ea1e72c7f249408ee061
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    /**
299     * @return true if the selected text is of password.
300     */
301    public boolean isSelectionPassword() {
302        return mTextInputType == sTextInputTypePassword;
303    }
304
305    public boolean dispatchKeyEvent(KeyEvent event) {
306        return translateAndSendNativeEvents(event);
307    }
308
309    private int shouldSendKeyEventWithKeyCode(String text) {
310        if (text.length() != 1) return COMPOSITION_KEY_CODE;
311
312        if (text.equals("\n")) return KeyEvent.KEYCODE_ENTER;
313        else if (text.equals("\t")) return KeyEvent.KEYCODE_TAB;
314        else return COMPOSITION_KEY_CODE;
315    }
316
317    void sendKeyEventWithKeyCode(int keyCode, int flags) {
318        long eventTime = SystemClock.uptimeMillis();
319        translateAndSendNativeEvents(new KeyEvent(eventTime, eventTime,
320                KeyEvent.ACTION_DOWN, keyCode, 0, 0,
321                KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
322                flags));
323        translateAndSendNativeEvents(new KeyEvent(SystemClock.uptimeMillis(), eventTime,
324                KeyEvent.ACTION_UP, keyCode, 0, 0,
325                KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
326                flags));
327    }
328
329    // Calls from Java to C++
330
331    boolean checkCompositionQueueAndCallNative(String text, int newCursorPosition,
332            boolean isCommit) {
333        if (mNativeImeAdapterAndroid == 0) return false;
334
335        // Committing an empty string finishes the current composition.
336        boolean isFinish = text.isEmpty();
337        mViewEmbedder.onImeEvent(isFinish);
338        int keyCode = shouldSendKeyEventWithKeyCode(text);
339        long timeStampMs = SystemClock.uptimeMillis();
340
341        if (keyCode != COMPOSITION_KEY_CODE) {
342            sendKeyEventWithKeyCode(keyCode,
343                    KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE);
344        } else {
345            nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeRawKeyDown,
346                    timeStampMs, keyCode, 0);
347            if (isCommit) {
348                nativeCommitText(mNativeImeAdapterAndroid, text);
349            } else {
350                nativeSetComposingText(mNativeImeAdapterAndroid, text, newCursorPosition);
351            }
352            nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeKeyUp,
353                    timeStampMs, keyCode, 0);
354        }
355
356        return true;
357    }
358
359    void finishComposingText() {
360        if (mNativeImeAdapterAndroid == 0) return;
361        nativeFinishComposingText(mNativeImeAdapterAndroid);
362    }
363
364    boolean translateAndSendNativeEvents(KeyEvent event) {
365        if (mNativeImeAdapterAndroid == 0) return false;
366
367        int action = event.getAction();
368        if (action != KeyEvent.ACTION_DOWN &&
369            action != KeyEvent.ACTION_UP) {
370            // action == KeyEvent.ACTION_MULTIPLE
371            // TODO(bulach): confirm the actual behavior. Apparently:
372            // If event.getKeyCode() == KEYCODE_UNKNOWN, we can send a
373            // composition key down (229) followed by a commit text with the
374            // string from event.getUnicodeChars().
375            // Otherwise, we'd need to send an event with a
376            // WebInputEvent::IsAutoRepeat modifier. We also need to verify when
377            // we receive ACTION_MULTIPLE: we may receive it after an ACTION_DOWN,
378            // and if that's the case, we'll need to review when to send the Char
379            // event.
380            return false;
381        }
382        mViewEmbedder.onImeEvent(false);
383        return nativeSendKeyEvent(mNativeImeAdapterAndroid, event, event.getAction(),
384                getModifiers(event.getMetaState()), event.getEventTime(), event.getKeyCode(),
385                             /*isSystemKey=*/false, event.getUnicodeChar());
386    }
387
388    boolean sendSyntheticKeyEvent(int eventType, long timestampMs, int keyCode, int unicodeChar) {
389        if (mNativeImeAdapterAndroid == 0) return false;
390
391        nativeSendSyntheticKeyEvent(
392                mNativeImeAdapterAndroid, eventType, timestampMs, keyCode, unicodeChar);
393        return true;
394    }
395
396    /**
397     * Send a request to the native counterpart to delete a given range of characters.
398     * @param beforeLength Number of characters to extend the selection by before the existing
399     *                     selection.
400     * @param afterLength Number of characters to extend the selection by after the existing
401     *                    selection.
402     * @return Whether the native counterpart of ImeAdapter received the call.
403     */
404    boolean deleteSurroundingText(int beforeLength, int afterLength) {
405        if (mNativeImeAdapterAndroid == 0) return false;
406        nativeDeleteSurroundingText(mNativeImeAdapterAndroid, beforeLength, afterLength);
407        return true;
408    }
409
410    /**
411     * Send a request to the native counterpart to set the selection to given range.
412     * @param start Selection start index.
413     * @param end Selection end index.
414     * @return Whether the native counterpart of ImeAdapter received the call.
415     */
416    boolean setEditableSelectionOffsets(int start, int end) {
417        if (mNativeImeAdapterAndroid == 0) return false;
418        nativeSetEditableSelectionOffsets(mNativeImeAdapterAndroid, start, end);
419        return true;
420    }
421
422    /**
423     * Send a request to the native counterpart to set compositing region to given indices.
424     * @param start The start of the composition.
425     * @param end The end of the composition.
426     * @return Whether the native counterpart of ImeAdapter received the call.
427     */
428    boolean setComposingRegion(int start, int end) {
429        if (mNativeImeAdapterAndroid == 0) return false;
430        nativeSetComposingRegion(mNativeImeAdapterAndroid, start, end);
431        return true;
432    }
433
434    /**
435     * Send a request to the native counterpart to unselect text.
436     * @return Whether the native counterpart of ImeAdapter received the call.
437     */
438    public boolean unselect() {
439        if (mNativeImeAdapterAndroid == 0) return false;
440        nativeUnselect(mNativeImeAdapterAndroid);
441        return true;
442    }
443
444    /**
445     * Send a request to the native counterpart of ImeAdapter to select all the text.
446     * @return Whether the native counterpart of ImeAdapter received the call.
447     */
448    public boolean selectAll() {
449        if (mNativeImeAdapterAndroid == 0) return false;
450        nativeSelectAll(mNativeImeAdapterAndroid);
451        return true;
452    }
453
454    /**
455     * Send a request to the native counterpart of ImeAdapter to cut the selected text.
456     * @return Whether the native counterpart of ImeAdapter received the call.
457     */
458    public boolean cut() {
459        if (mNativeImeAdapterAndroid == 0) return false;
460        nativeCut(mNativeImeAdapterAndroid);
461        return true;
462    }
463
464    /**
465     * Send a request to the native counterpart of ImeAdapter to copy the selected text.
466     * @return Whether the native counterpart of ImeAdapter received the call.
467     */
468    public boolean copy() {
469        if (mNativeImeAdapterAndroid == 0) return false;
470        nativeCopy(mNativeImeAdapterAndroid);
471        return true;
472    }
473
474    /**
475     * Send a request to the native counterpart of ImeAdapter to paste the text from the clipboard.
476     * @return Whether the native counterpart of ImeAdapter received the call.
477     */
478    public boolean paste() {
479        if (mNativeImeAdapterAndroid == 0) return false;
480        nativePaste(mNativeImeAdapterAndroid);
481        return true;
482    }
483
484    // Calls from C++ to Java
485
486    @CalledByNative
487    private static void initializeWebInputEvents(int eventTypeRawKeyDown, int eventTypeKeyUp,
488            int eventTypeChar, int modifierShift, int modifierAlt, int modifierCtrl,
489            int modifierCapsLockOn, int modifierNumLockOn) {
490        sEventTypeRawKeyDown = eventTypeRawKeyDown;
491        sEventTypeKeyUp = eventTypeKeyUp;
492        sEventTypeChar = eventTypeChar;
493        sModifierShift = modifierShift;
494        sModifierAlt = modifierAlt;
495        sModifierCtrl = modifierCtrl;
496        sModifierCapsLockOn = modifierCapsLockOn;
497        sModifierNumLockOn = modifierNumLockOn;
498    }
499
500    @CalledByNative
501    private static void initializeTextInputTypes(int textInputTypeNone, int textInputTypeText,
502            int textInputTypeTextArea, int textInputTypePassword, int textInputTypeSearch,
503            int textInputTypeUrl, int textInputTypeEmail, int textInputTypeTel,
504            int textInputTypeNumber, int textInputTypeContentEditable) {
505        sTextInputTypeNone = textInputTypeNone;
506        sTextInputTypeText = textInputTypeText;
507        sTextInputTypeTextArea = textInputTypeTextArea;
508        sTextInputTypePassword = textInputTypePassword;
509        sTextInputTypeSearch = textInputTypeSearch;
510        sTextInputTypeUrl = textInputTypeUrl;
511        sTextInputTypeEmail = textInputTypeEmail;
512        sTextInputTypeTel = textInputTypeTel;
513        sTextInputTypeNumber = textInputTypeNumber;
514        sTextInputTypeContentEditable = textInputTypeContentEditable;
515    }
516
517    @CalledByNative
518    private void focusedNodeChanged(boolean isEditable) {
519        if (mInputConnection != null && isEditable) mInputConnection.restartInput();
520    }
521
522    @CalledByNative
523    private void cancelComposition() {
524        if (mInputConnection != null) mInputConnection.restartInput();
525    }
526
527    @CalledByNative
528    void detach() {
529        if (mDismissInput != null) mHandler.removeCallbacks(mDismissInput);
530        mNativeImeAdapterAndroid = 0;
531        mTextInputType = 0;
532    }
533
534    private native boolean nativeSendSyntheticKeyEvent(long nativeImeAdapterAndroid,
535            int eventType, long timestampMs, int keyCode, int unicodeChar);
536
537    private native boolean nativeSendKeyEvent(long nativeImeAdapterAndroid, KeyEvent event,
538            int action, int modifiers, long timestampMs, int keyCode, boolean isSystemKey,
539            int unicodeChar);
540
541    private native void nativeSetComposingText(long nativeImeAdapterAndroid, String text,
542            int newCursorPosition);
543
544    private native void nativeCommitText(long nativeImeAdapterAndroid, String text);
545
546    private native void nativeFinishComposingText(long nativeImeAdapterAndroid);
547
548    private native void nativeAttachImeAdapter(long nativeImeAdapterAndroid);
549
550    private native void nativeSetEditableSelectionOffsets(long nativeImeAdapterAndroid,
551            int start, int end);
552
553    private native void nativeSetComposingRegion(long nativeImeAdapterAndroid, int start, int end);
554
555    private native void nativeDeleteSurroundingText(long nativeImeAdapterAndroid,
556            int before, int after);
557
558    private native void nativeUnselect(long nativeImeAdapterAndroid);
559    private native void nativeSelectAll(long nativeImeAdapterAndroid);
560    private native void nativeCut(long nativeImeAdapterAndroid);
561    private native void nativeCopy(long nativeImeAdapterAndroid);
562    private native void nativePaste(long nativeImeAdapterAndroid);
563    private native void nativeResetImeAdapter(long nativeImeAdapterAndroid);
564}
565