ImeAdapter.java revision 5f1c94371a64b3196d4be9466099bb892df9b88e
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         * Called to notify the delegate about synthetic/real key events before sending to renderer.
57         */
58        void onImeEvent();
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    static char[] sSingleCharArray = new char[1];
120    static KeyCharacterMap sKeyCharacterMap;
121
122    private long mNativeImeAdapterAndroid;
123    private InputMethodManagerWrapper mInputMethodManagerWrapper;
124    private AdapterInputConnection mInputConnection;
125    private final ImeAdapterDelegate mViewEmbedder;
126    private final Handler mHandler;
127    private DelayedDismissInput mDismissInput = null;
128    private int mTextInputType;
129    private String mLastComposeText;
130
131    @VisibleForTesting
132    int mLastSyntheticKeyCode;
133
134    @VisibleForTesting
135    boolean mIsShowWithoutHideOutstanding = false;
136
137    /**
138     * @param wrapper InputMethodManagerWrapper that should receive all the call directed to
139     *                InputMethodManager.
140     * @param embedder The view that is used for callbacks from ImeAdapter.
141     */
142    public ImeAdapter(InputMethodManagerWrapper wrapper, ImeAdapterDelegate embedder) {
143        mInputMethodManagerWrapper = wrapper;
144        mViewEmbedder = embedder;
145        mHandler = new Handler();
146    }
147
148    /**
149     * Default factory for AdapterInputConnection classes.
150     */
151    public static class AdapterInputConnectionFactory {
152        public AdapterInputConnection get(View view, ImeAdapter imeAdapter,
153                Editable editable, EditorInfo outAttrs) {
154            return new AdapterInputConnection(view, imeAdapter, editable, outAttrs);
155        }
156    }
157
158    /**
159     * Overrides the InputMethodManagerWrapper that ImeAdapter uses to make calls to
160     * InputMethodManager.
161     * @param immw InputMethodManagerWrapper that should be used to call InputMethodManager.
162     */
163    @VisibleForTesting
164    public void setInputMethodManagerWrapper(InputMethodManagerWrapper immw) {
165        mInputMethodManagerWrapper = immw;
166    }
167
168    /**
169     * Should be only used by AdapterInputConnection.
170     * @return InputMethodManagerWrapper that should receive all the calls directed to
171     *         InputMethodManager.
172     */
173    InputMethodManagerWrapper getInputMethodManagerWrapper() {
174        return mInputMethodManagerWrapper;
175    }
176
177    /**
178     * Set the current active InputConnection when a new InputConnection is constructed.
179     * @param inputConnection The input connection that is currently used with IME.
180     */
181    void setInputConnection(AdapterInputConnection inputConnection) {
182        mInputConnection = inputConnection;
183        mLastComposeText = null;
184    }
185
186    /**
187     * Should be only used by AdapterInputConnection.
188     * @return The input type of currently focused element.
189     */
190    int getTextInputType() {
191        return mTextInputType;
192    }
193
194    /**
195     * @return Constant representing that a focused node is not an input field.
196     */
197    public static int getTextInputTypeNone() {
198        return sTextInputTypeNone;
199    }
200
201    private static int getModifiers(int metaState) {
202        int modifiers = 0;
203        if ((metaState & KeyEvent.META_SHIFT_ON) != 0) {
204            modifiers |= sModifierShift;
205        }
206        if ((metaState & KeyEvent.META_ALT_ON) != 0) {
207            modifiers |= sModifierAlt;
208        }
209        if ((metaState & KeyEvent.META_CTRL_ON) != 0) {
210            modifiers |= sModifierCtrl;
211        }
212        if ((metaState & KeyEvent.META_CAPS_LOCK_ON) != 0) {
213            modifiers |= sModifierCapsLockOn;
214        }
215        if ((metaState & KeyEvent.META_NUM_LOCK_ON) != 0) {
216            modifiers |= sModifierNumLockOn;
217        }
218        return modifiers;
219    }
220
221    /**
222     * Shows or hides the keyboard based on passed parameters.
223     * @param nativeImeAdapter Pointer to the ImeAdapterAndroid object that is sending the update.
224     * @param textInputType Text input type for the currently focused field in renderer.
225     * @param showIfNeeded Whether the keyboard should be shown if it is currently hidden.
226     */
227    public void updateKeyboardVisibility(long nativeImeAdapter, int textInputType,
228            boolean showIfNeeded) {
229        mHandler.removeCallbacks(mDismissInput);
230
231        // If current input type is none and showIfNeeded is false, IME should not be shown
232        // and input type should remain as none.
233        if (mTextInputType == sTextInputTypeNone && !showIfNeeded) {
234            return;
235        }
236
237        if (mNativeImeAdapterAndroid != nativeImeAdapter || mTextInputType != textInputType) {
238            // Set a delayed task to perform unfocus. This avoids hiding the keyboard when tabbing
239            // through text inputs or when JS rapidly changes focus to another text element.
240            if (textInputType == sTextInputTypeNone) {
241                mDismissInput = new DelayedDismissInput(nativeImeAdapter);
242                mHandler.postDelayed(mDismissInput, INPUT_DISMISS_DELAY);
243                return;
244            }
245
246            attach(nativeImeAdapter, textInputType);
247
248            mInputMethodManagerWrapper.restartInput(mViewEmbedder.getAttachedView());
249            if (showIfNeeded) {
250                showKeyboard();
251            }
252        } else if (hasInputType() && showIfNeeded) {
253            showKeyboard();
254        }
255    }
256
257    public void attach(long nativeImeAdapter, int textInputType) {
258        if (mNativeImeAdapterAndroid != 0) {
259            nativeResetImeAdapter(mNativeImeAdapterAndroid);
260        }
261        mNativeImeAdapterAndroid = nativeImeAdapter;
262        mTextInputType = textInputType;
263        mLastComposeText = null;
264        if (nativeImeAdapter != 0) {
265            nativeAttachImeAdapter(mNativeImeAdapterAndroid);
266        }
267        if (mTextInputType == sTextInputTypeNone) {
268            dismissInput(false);
269        }
270    }
271
272    /**
273     * Attaches the imeAdapter to its native counterpart. This is needed to start forwarding
274     * keyboard events to WebKit.
275     * @param nativeImeAdapter The pointer to the native ImeAdapter object.
276     */
277    public void attach(long nativeImeAdapter) {
278        attach(nativeImeAdapter, sTextInputTypeNone);
279    }
280
281    private void showKeyboard() {
282        mIsShowWithoutHideOutstanding = true;
283        mInputMethodManagerWrapper.showSoftInput(mViewEmbedder.getAttachedView(), 0,
284                mViewEmbedder.getNewShowKeyboardReceiver());
285    }
286
287    private void dismissInput(boolean unzoomIfNeeded) {
288        mIsShowWithoutHideOutstanding  = false;
289        View view = mViewEmbedder.getAttachedView();
290        if (mInputMethodManagerWrapper.isActive(view)) {
291            mInputMethodManagerWrapper.hideSoftInputFromWindow(view.getWindowToken(), 0,
292                    unzoomIfNeeded ? mViewEmbedder.getNewShowKeyboardReceiver() : null);
293        }
294        mViewEmbedder.onDismissInput();
295    }
296
297    private boolean hasInputType() {
298        return mTextInputType != sTextInputTypeNone;
299    }
300
301    private static boolean isTextInputType(int type) {
302        return type != sTextInputTypeNone && !InputDialogContainer.isDialogInputType(type);
303    }
304
305    public boolean hasTextInputType() {
306        return isTextInputType(mTextInputType);
307    }
308
309    /**
310     * @return true if the selected text is of password.
311     */
312    public boolean isSelectionPassword() {
313        return mTextInputType == sTextInputTypePassword;
314    }
315
316    public boolean dispatchKeyEvent(KeyEvent event) {
317        return translateAndSendNativeEvents(event);
318    }
319
320    private int shouldSendKeyEventWithKeyCode(String text) {
321        if (text.length() != 1) return COMPOSITION_KEY_CODE;
322
323        if (text.equals("\n")) return KeyEvent.KEYCODE_ENTER;
324        else if (text.equals("\t")) return KeyEvent.KEYCODE_TAB;
325        else return COMPOSITION_KEY_CODE;
326    }
327
328    /**
329     * @return Android keycode for a single unicode character.
330     */
331    private static int androidKeyCodeForCharacter(char chr) {
332        if (sKeyCharacterMap == null) {
333            sKeyCharacterMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
334        }
335        sSingleCharArray[0] = chr;
336        // TODO: Evaluate cost of this system call.
337        KeyEvent[] events = sKeyCharacterMap.getEvents(sSingleCharArray);
338        if (events == null || events.length != 2)  // One key-down event and one key-up event.
339            return KeyEvent.KEYCODE_UNKNOWN;
340        return events[0].getKeyCode();
341    }
342
343    @VisibleForTesting
344    public static int getTypedKeycodeGuess(String oldtext, String newtext) {
345        // Starting typing a new composition should add only a single character.  Any composition
346        // beginning with text longer than that must come from something other than typing so
347        // return 0.
348        if (oldtext == null) {
349            if (newtext.length() == 1) {
350                return androidKeyCodeForCharacter(newtext.charAt(0));
351            } else {
352                return 0;
353            }
354        }
355
356        // The content has grown in length: assume the last character is the key that caused it.
357        if (newtext.length() > oldtext.length() && newtext.startsWith(oldtext))
358            return androidKeyCodeForCharacter(newtext.charAt(newtext.length() - 1));
359
360        // The content has shrunk in length: assume that backspace was pressed.
361        if (oldtext.length() > newtext.length() && oldtext.startsWith(newtext))
362            return KeyEvent.KEYCODE_DEL;
363
364        // The content is unchanged or has undergone a complex change (i.e. not a simple tail
365        // modification) so return an unknown key-code.
366        return 0;
367    }
368
369    void sendKeyEventWithKeyCode(int keyCode, int flags) {
370        long eventTime = SystemClock.uptimeMillis();
371        mLastSyntheticKeyCode = keyCode;
372        translateAndSendNativeEvents(new KeyEvent(eventTime, eventTime,
373                KeyEvent.ACTION_DOWN, keyCode, 0, 0,
374                KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
375                flags));
376        translateAndSendNativeEvents(new KeyEvent(SystemClock.uptimeMillis(), eventTime,
377                KeyEvent.ACTION_UP, keyCode, 0, 0,
378                KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
379                flags));
380    }
381
382    // Calls from Java to C++
383    // TODO: Add performance tracing to more complicated functions.
384
385    boolean checkCompositionQueueAndCallNative(CharSequence text, int newCursorPosition,
386            boolean isCommit) {
387        if (mNativeImeAdapterAndroid == 0) return false;
388        mViewEmbedder.onImeEvent();
389
390        String textStr = text.toString();
391        int keyCode = shouldSendKeyEventWithKeyCode(textStr);
392        long timeStampMs = SystemClock.uptimeMillis();
393
394        if (keyCode != COMPOSITION_KEY_CODE) {
395            sendKeyEventWithKeyCode(keyCode,
396                    KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE);
397        } else {
398            keyCode = getTypedKeycodeGuess(mLastComposeText, textStr);
399            mLastComposeText = textStr;
400            mLastSyntheticKeyCode = keyCode;
401
402            // When typing, there is no issue sending KeyDown and KeyUp events around the
403            // composition event because those key events do nothing (other than call JS
404            // handlers).  Typing does not cause changes outside of a KeyPress event which
405            // we don't call here.  However, if the key-code is a control key such as
406            // KEYCODE_DEL then there never is an associated KeyPress event and the KeyDown
407            // event itself causes the action.  The net result below is that the Renderer calls
408            // cancelComposition() and then Android starts anew with setComposingRegion().
409            // This stopping and restarting of composition could be a source of problems
410            // with 3rd party keyboards.
411            //
412            // An alternative is to *not* call nativeSetComposingText() in the non-commit case
413            // below.  This avoids the restart of composition described above but fails to send
414            // an update to the composition while in composition which, strictly speaking,
415            // does not match the spec.
416            //
417            // For now, the solution is to endure the restarting of composition and only dive
418            // into the alternate solution should there be problems in the field.  --bcwhite
419
420            nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeRawKeyDown,
421                    timeStampMs, keyCode, 0);
422
423            if (isCommit) {
424                nativeCommitText(mNativeImeAdapterAndroid, textStr);
425                mLastComposeText = null;
426            } else {
427                nativeSetComposingText(mNativeImeAdapterAndroid, text, textStr, newCursorPosition);
428            }
429
430            nativeSendSyntheticKeyEvent(mNativeImeAdapterAndroid, sEventTypeKeyUp,
431                    timeStampMs, keyCode, 0);
432        }
433
434        return true;
435    }
436
437    void finishComposingText() {
438        mLastComposeText = null;
439        if (mNativeImeAdapterAndroid == 0) return;
440        nativeFinishComposingText(mNativeImeAdapterAndroid);
441    }
442
443    boolean translateAndSendNativeEvents(KeyEvent event) {
444        if (mNativeImeAdapterAndroid == 0) return false;
445
446        int action = event.getAction();
447        if (action != KeyEvent.ACTION_DOWN &&
448            action != KeyEvent.ACTION_UP) {
449            // action == KeyEvent.ACTION_MULTIPLE
450            // TODO(bulach): confirm the actual behavior. Apparently:
451            // If event.getKeyCode() == KEYCODE_UNKNOWN, we can send a
452            // composition key down (229) followed by a commit text with the
453            // string from event.getUnicodeChars().
454            // Otherwise, we'd need to send an event with a
455            // WebInputEvent::IsAutoRepeat modifier. We also need to verify when
456            // we receive ACTION_MULTIPLE: we may receive it after an ACTION_DOWN,
457            // and if that's the case, we'll need to review when to send the Char
458            // event.
459            return false;
460        }
461        mViewEmbedder.onImeEvent();
462        return nativeSendKeyEvent(mNativeImeAdapterAndroid, event, event.getAction(),
463                getModifiers(event.getMetaState()), event.getEventTime(), event.getKeyCode(),
464                             /*isSystemKey=*/false, event.getUnicodeChar());
465    }
466
467    boolean sendSyntheticKeyEvent(int eventType, long timestampMs, int keyCode, int unicodeChar) {
468        if (mNativeImeAdapterAndroid == 0) return false;
469
470        nativeSendSyntheticKeyEvent(
471                mNativeImeAdapterAndroid, eventType, timestampMs, keyCode, unicodeChar);
472        return true;
473    }
474
475    /**
476     * Send a request to the native counterpart to delete a given range of characters.
477     * @param beforeLength Number of characters to extend the selection by before the existing
478     *                     selection.
479     * @param afterLength Number of characters to extend the selection by after the existing
480     *                    selection.
481     * @return Whether the native counterpart of ImeAdapter received the call.
482     */
483    boolean deleteSurroundingText(int beforeLength, int afterLength) {
484        mViewEmbedder.onImeEvent();
485        if (mNativeImeAdapterAndroid == 0) return false;
486        nativeDeleteSurroundingText(mNativeImeAdapterAndroid, beforeLength, afterLength);
487        return true;
488    }
489
490    /**
491     * Send a request to the native counterpart to set the selection to given range.
492     * @param start Selection start index.
493     * @param end Selection end index.
494     * @return Whether the native counterpart of ImeAdapter received the call.
495     */
496    boolean setEditableSelectionOffsets(int start, int end) {
497        if (mNativeImeAdapterAndroid == 0) return false;
498        nativeSetEditableSelectionOffsets(mNativeImeAdapterAndroid, start, end);
499        return true;
500    }
501
502    /**
503     * Send a request to the native counterpart to set composing region to given indices.
504     * @param start The start of the composition.
505     * @param end The end of the composition.
506     * @return Whether the native counterpart of ImeAdapter received the call.
507     */
508    boolean setComposingRegion(CharSequence text, int start, int end) {
509        if (mNativeImeAdapterAndroid == 0) return false;
510        nativeSetComposingRegion(mNativeImeAdapterAndroid, start, end);
511        mLastComposeText = text != null ? text.toString() : null;
512        return true;
513    }
514
515    /**
516     * Send a request to the native counterpart to unselect text.
517     * @return Whether the native counterpart of ImeAdapter received the call.
518     */
519    public boolean unselect() {
520        if (mNativeImeAdapterAndroid == 0) return false;
521        nativeUnselect(mNativeImeAdapterAndroid);
522        return true;
523    }
524
525    /**
526     * Send a request to the native counterpart of ImeAdapter to select all the text.
527     * @return Whether the native counterpart of ImeAdapter received the call.
528     */
529    public boolean selectAll() {
530        if (mNativeImeAdapterAndroid == 0) return false;
531        nativeSelectAll(mNativeImeAdapterAndroid);
532        return true;
533    }
534
535    /**
536     * Send a request to the native counterpart of ImeAdapter to cut the selected text.
537     * @return Whether the native counterpart of ImeAdapter received the call.
538     */
539    public boolean cut() {
540        if (mNativeImeAdapterAndroid == 0) return false;
541        nativeCut(mNativeImeAdapterAndroid);
542        return true;
543    }
544
545    /**
546     * Send a request to the native counterpart of ImeAdapter to copy the selected text.
547     * @return Whether the native counterpart of ImeAdapter received the call.
548     */
549    public boolean copy() {
550        if (mNativeImeAdapterAndroid == 0) return false;
551        nativeCopy(mNativeImeAdapterAndroid);
552        return true;
553    }
554
555    /**
556     * Send a request to the native counterpart of ImeAdapter to paste the text from the clipboard.
557     * @return Whether the native counterpart of ImeAdapter received the call.
558     */
559    public boolean paste() {
560        if (mNativeImeAdapterAndroid == 0) return false;
561        nativePaste(mNativeImeAdapterAndroid);
562        return true;
563    }
564
565    // Calls from C++ to Java
566
567    @CalledByNative
568    private static void initializeWebInputEvents(int eventTypeRawKeyDown, int eventTypeKeyUp,
569            int eventTypeChar, int modifierShift, int modifierAlt, int modifierCtrl,
570            int modifierCapsLockOn, int modifierNumLockOn) {
571        sEventTypeRawKeyDown = eventTypeRawKeyDown;
572        sEventTypeKeyUp = eventTypeKeyUp;
573        sEventTypeChar = eventTypeChar;
574        sModifierShift = modifierShift;
575        sModifierAlt = modifierAlt;
576        sModifierCtrl = modifierCtrl;
577        sModifierCapsLockOn = modifierCapsLockOn;
578        sModifierNumLockOn = modifierNumLockOn;
579    }
580
581    @CalledByNative
582    private static void initializeTextInputTypes(int textInputTypeNone, int textInputTypeText,
583            int textInputTypeTextArea, int textInputTypePassword, int textInputTypeSearch,
584            int textInputTypeUrl, int textInputTypeEmail, int textInputTypeTel,
585            int textInputTypeNumber, int textInputTypeContentEditable) {
586        sTextInputTypeNone = textInputTypeNone;
587        sTextInputTypeText = textInputTypeText;
588        sTextInputTypeTextArea = textInputTypeTextArea;
589        sTextInputTypePassword = textInputTypePassword;
590        sTextInputTypeSearch = textInputTypeSearch;
591        sTextInputTypeUrl = textInputTypeUrl;
592        sTextInputTypeEmail = textInputTypeEmail;
593        sTextInputTypeTel = textInputTypeTel;
594        sTextInputTypeNumber = textInputTypeNumber;
595        sTextInputTypeContentEditable = textInputTypeContentEditable;
596    }
597
598    @CalledByNative
599    private void focusedNodeChanged(boolean isEditable) {
600        if (mInputConnection != null && isEditable) mInputConnection.restartInput();
601    }
602
603    @CalledByNative
604    private void populateUnderlinesFromSpans(CharSequence text, long underlines) {
605        if (!(text instanceof SpannableString)) return;
606
607        SpannableString spannableString = ((SpannableString) text);
608        CharacterStyle spans[] =
609                spannableString.getSpans(0, text.length(), CharacterStyle.class);
610        for (CharacterStyle span : spans) {
611            if (span instanceof BackgroundColorSpan) {
612                nativeAppendBackgroundColorSpan(underlines, spannableString.getSpanStart(span),
613                        spannableString.getSpanEnd(span),
614                        ((BackgroundColorSpan) span).getBackgroundColor());
615            } else if (span instanceof UnderlineSpan) {
616                nativeAppendUnderlineSpan(underlines, spannableString.getSpanStart(span),
617                        spannableString.getSpanEnd(span));
618            }
619        }
620    }
621
622    @CalledByNative
623    private void cancelComposition() {
624        if (mInputConnection != null) mInputConnection.restartInput();
625        mLastComposeText = null;
626    }
627
628    @CalledByNative
629    void detach() {
630        if (mDismissInput != null) mHandler.removeCallbacks(mDismissInput);
631        mNativeImeAdapterAndroid = 0;
632        mTextInputType = 0;
633    }
634
635    private native boolean nativeSendSyntheticKeyEvent(long nativeImeAdapterAndroid,
636            int eventType, long timestampMs, int keyCode, int unicodeChar);
637
638    private native boolean nativeSendKeyEvent(long nativeImeAdapterAndroid, KeyEvent event,
639            int action, int modifiers, long timestampMs, int keyCode, boolean isSystemKey,
640            int unicodeChar);
641
642    private static native void nativeAppendUnderlineSpan(long underlinePtr, int start, int end);
643
644    private static native void nativeAppendBackgroundColorSpan(long underlinePtr, int start,
645            int end, int backgroundColor);
646
647    private native void nativeSetComposingText(long nativeImeAdapterAndroid, CharSequence text,
648            String textStr, int newCursorPosition);
649
650    private native void nativeCommitText(long nativeImeAdapterAndroid, String textStr);
651
652    private native void nativeFinishComposingText(long nativeImeAdapterAndroid);
653
654    private native void nativeAttachImeAdapter(long nativeImeAdapterAndroid);
655
656    private native void nativeSetEditableSelectionOffsets(long nativeImeAdapterAndroid,
657            int start, int end);
658
659    private native void nativeSetComposingRegion(long nativeImeAdapterAndroid, int start, int end);
660
661    private native void nativeDeleteSurroundingText(long nativeImeAdapterAndroid,
662            int before, int after);
663
664    private native void nativeUnselect(long nativeImeAdapterAndroid);
665    private native void nativeSelectAll(long nativeImeAdapterAndroid);
666    private native void nativeCut(long nativeImeAdapterAndroid);
667    private native void nativeCopy(long nativeImeAdapterAndroid);
668    private native void nativePaste(long nativeImeAdapterAndroid);
669    private native void nativeResetImeAdapter(long nativeImeAdapterAndroid);
670}
671