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