AdapterInputConnection.java revision 116680a4aac90f2aa7413d9095a592090648e557
1// Copyright 2013 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.SystemClock;
8import android.text.Editable;
9import android.text.InputType;
10import android.text.Selection;
11import android.text.TextUtils;
12import android.util.Log;
13import android.view.KeyEvent;
14import android.view.View;
15import android.view.inputmethod.BaseInputConnection;
16import android.view.inputmethod.EditorInfo;
17import android.view.inputmethod.ExtractedText;
18import android.view.inputmethod.ExtractedTextRequest;
19
20import com.google.common.annotations.VisibleForTesting;
21
22/**
23 * InputConnection is created by ContentView.onCreateInputConnection.
24 * It then adapts android's IME to chrome's RenderWidgetHostView using the
25 * native ImeAdapterAndroid via the class ImeAdapter.
26 */
27public class AdapterInputConnection extends BaseInputConnection {
28    private static final String TAG = "AdapterInputConnection";
29    private static final boolean DEBUG = false;
30    /**
31     * Selection value should be -1 if not known. See EditorInfo.java for details.
32     */
33    public static final int INVALID_SELECTION = -1;
34    public static final int INVALID_COMPOSITION = -1;
35
36    private final View mInternalView;
37    private final ImeAdapter mImeAdapter;
38    private final Editable mEditable;
39
40    private boolean mSingleLine;
41    private int mNumNestedBatchEdits = 0;
42
43    private int mLastUpdateSelectionStart = INVALID_SELECTION;
44    private int mLastUpdateSelectionEnd = INVALID_SELECTION;
45    private int mLastUpdateCompositionStart = INVALID_COMPOSITION;
46    private int mLastUpdateCompositionEnd = INVALID_COMPOSITION;
47
48    @VisibleForTesting
49    AdapterInputConnection(View view, ImeAdapter imeAdapter, Editable editable,
50            EditorInfo outAttrs) {
51        super(view, true);
52        mInternalView = view;
53        mImeAdapter = imeAdapter;
54        mImeAdapter.setInputConnection(this);
55        mEditable = editable;
56        // The editable passed in might have been in use by a prior keyboard and could have had
57        // prior composition spans set.  To avoid keyboard conflicts, remove all composing spans
58        // when taking ownership of an existing Editable.
59        removeComposingSpans(mEditable);
60        mSingleLine = true;
61        outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN
62                | EditorInfo.IME_FLAG_NO_EXTRACT_UI;
63        outAttrs.inputType = EditorInfo.TYPE_CLASS_TEXT
64                | EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT;
65
66        if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeText) {
67            // Normal text field
68            outAttrs.inputType |= EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT;
69            outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO;
70        } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeTextArea ||
71                imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeContentEditable) {
72            // TextArea or contenteditable.
73            outAttrs.inputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE
74                    | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES
75                    | EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT;
76            outAttrs.imeOptions |= EditorInfo.IME_ACTION_NONE;
77            mSingleLine = false;
78        } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypePassword) {
79            // Password
80            outAttrs.inputType = InputType.TYPE_CLASS_TEXT
81                    | InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD;
82            outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO;
83        } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeSearch) {
84            // Search
85            outAttrs.imeOptions |= EditorInfo.IME_ACTION_SEARCH;
86        } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeUrl) {
87            // Url
88            outAttrs.inputType = InputType.TYPE_CLASS_TEXT
89                    | InputType.TYPE_TEXT_VARIATION_URI;
90            outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO;
91        } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeEmail) {
92            // Email
93            outAttrs.inputType = InputType.TYPE_CLASS_TEXT
94                    | InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS;
95            outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO;
96        } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeTel) {
97            // Telephone
98            // Number and telephone do not have both a Tab key and an
99            // action in default OSK, so set the action to NEXT
100            outAttrs.inputType = InputType.TYPE_CLASS_PHONE;
101            outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT;
102        } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeNumber) {
103            // Number
104            outAttrs.inputType = InputType.TYPE_CLASS_NUMBER
105                    | InputType.TYPE_NUMBER_VARIATION_NORMAL
106                    | InputType.TYPE_NUMBER_FLAG_DECIMAL;
107            outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT;
108        }
109        outAttrs.initialSelStart = Selection.getSelectionStart(mEditable);
110        outAttrs.initialSelEnd = Selection.getSelectionEnd(mEditable);
111        mLastUpdateSelectionStart = Selection.getSelectionStart(mEditable);
112        mLastUpdateSelectionEnd = Selection.getSelectionEnd(mEditable);
113
114        Selection.setSelection(mEditable, outAttrs.initialSelStart, outAttrs.initialSelEnd);
115        updateSelectionIfRequired();
116    }
117
118    /**
119     * Updates the AdapterInputConnection's internal representation of the text being edited and
120     * its selection and composition properties. The resulting Editable is accessible through the
121     * getEditable() method. If the text has not changed, this also calls updateSelection on the
122     * InputMethodManager.
123     *
124     * @param text The String contents of the field being edited.
125     * @param selectionStart The character offset of the selection start, or the caret position if
126     *                       there is no selection.
127     * @param selectionEnd The character offset of the selection end, or the caret position if there
128     *                     is no selection.
129     * @param compositionStart The character offset of the composition start, or -1 if there is no
130     *                         composition.
131     * @param compositionEnd The character offset of the composition end, or -1 if there is no
132     *                       selection.
133     * @param isNonImeChange True when the update was caused by non-IME (e.g. Javascript).
134     */
135    @VisibleForTesting
136    public void updateState(String text, int selectionStart, int selectionEnd, int compositionStart,
137            int compositionEnd, boolean isNonImeChange) {
138        if (DEBUG) {
139            Log.w(TAG, "updateState [" + text + "] [" + selectionStart + " " + selectionEnd + "] ["
140                    + compositionStart + " " + compositionEnd + "] [" + isNonImeChange + "]");
141        }
142        // If this update is from the IME, no further state modification is necessary because the
143        // state should have been updated already by the IM framework directly.
144        if (!isNonImeChange) return;
145
146        // Non-breaking spaces can cause the IME to get confused. Replace with normal spaces.
147        text = text.replace('\u00A0', ' ');
148
149        selectionStart = Math.min(selectionStart, text.length());
150        selectionEnd = Math.min(selectionEnd, text.length());
151        compositionStart = Math.min(compositionStart, text.length());
152        compositionEnd = Math.min(compositionEnd, text.length());
153
154        String prevText = mEditable.toString();
155        boolean textUnchanged = prevText.equals(text);
156
157        if (!textUnchanged) {
158            mEditable.replace(0, mEditable.length(), text);
159        }
160
161        Selection.setSelection(mEditable, selectionStart, selectionEnd);
162
163        if (compositionStart == compositionEnd) {
164            removeComposingSpans(mEditable);
165        } else {
166            super.setComposingRegion(compositionStart, compositionEnd);
167        }
168        updateSelectionIfRequired();
169    }
170
171    /**
172     * @return Editable object which contains the state of current focused editable element.
173     */
174    @Override
175    public Editable getEditable() {
176        return mEditable;
177    }
178
179    /**
180     * Sends selection update to the InputMethodManager unless we are currently in a batch edit or
181     * if the exact same selection and composition update was sent already.
182     */
183    private void updateSelectionIfRequired() {
184        if (mNumNestedBatchEdits != 0) return;
185        int selectionStart = Selection.getSelectionStart(mEditable);
186        int selectionEnd = Selection.getSelectionEnd(mEditable);
187        int compositionStart = getComposingSpanStart(mEditable);
188        int compositionEnd = getComposingSpanEnd(mEditable);
189        // Avoid sending update if we sent an exact update already previously.
190        if (mLastUpdateSelectionStart == selectionStart &&
191                mLastUpdateSelectionEnd == selectionEnd &&
192                mLastUpdateCompositionStart == compositionStart &&
193                mLastUpdateCompositionEnd == compositionEnd) {
194            return;
195        }
196        if (DEBUG) {
197            Log.w(TAG, "updateSelectionIfRequired [" + selectionStart + " " + selectionEnd + "] ["
198                    + compositionStart + " " + compositionEnd + "]");
199        }
200        // updateSelection should be called every time the selection or composition changes
201        // if it happens not within a batch edit, or at the end of each top level batch edit.
202        getInputMethodManagerWrapper().updateSelection(mInternalView,
203                selectionStart, selectionEnd, compositionStart, compositionEnd);
204        mLastUpdateSelectionStart = selectionStart;
205        mLastUpdateSelectionEnd = selectionEnd;
206        mLastUpdateCompositionStart = compositionStart;
207        mLastUpdateCompositionEnd = compositionEnd;
208    }
209
210    /**
211     * @see BaseInputConnection#setComposingText(java.lang.CharSequence, int)
212     */
213    @Override
214    public boolean setComposingText(CharSequence text, int newCursorPosition) {
215        if (DEBUG) Log.w(TAG, "setComposingText [" + text + "] [" + newCursorPosition + "]");
216        if (maybePerformEmptyCompositionWorkaround(text)) return true;
217        super.setComposingText(text, newCursorPosition);
218        updateSelectionIfRequired();
219        return mImeAdapter.checkCompositionQueueAndCallNative(text, newCursorPosition, false);
220    }
221
222    /**
223     * @see BaseInputConnection#commitText(java.lang.CharSequence, int)
224     */
225    @Override
226    public boolean commitText(CharSequence text, int newCursorPosition) {
227        if (DEBUG) Log.w(TAG, "commitText [" + text + "] [" + newCursorPosition + "]");
228        if (maybePerformEmptyCompositionWorkaround(text)) return true;
229        super.commitText(text, newCursorPosition);
230        updateSelectionIfRequired();
231        return mImeAdapter.checkCompositionQueueAndCallNative(text, newCursorPosition,
232                text.length() > 0);
233    }
234
235    /**
236     * @see BaseInputConnection#performEditorAction(int)
237     */
238    @Override
239    public boolean performEditorAction(int actionCode) {
240        if (DEBUG) Log.w(TAG, "performEditorAction [" + actionCode + "]");
241        if (actionCode == EditorInfo.IME_ACTION_NEXT) {
242            restartInput();
243            // Send TAB key event
244            long timeStampMs = SystemClock.uptimeMillis();
245            mImeAdapter.sendSyntheticKeyEvent(
246                    ImeAdapter.sEventTypeRawKeyDown, timeStampMs, KeyEvent.KEYCODE_TAB, 0);
247        } else {
248            mImeAdapter.sendKeyEventWithKeyCode(KeyEvent.KEYCODE_ENTER,
249                    KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE
250                    | KeyEvent.FLAG_EDITOR_ACTION);
251        }
252        return true;
253    }
254
255    /**
256     * @see BaseInputConnection#performContextMenuAction(int)
257     */
258    @Override
259    public boolean performContextMenuAction(int id) {
260        if (DEBUG) Log.w(TAG, "performContextMenuAction [" + id + "]");
261        switch (id) {
262            case android.R.id.selectAll:
263                return mImeAdapter.selectAll();
264            case android.R.id.cut:
265                return mImeAdapter.cut();
266            case android.R.id.copy:
267                return mImeAdapter.copy();
268            case android.R.id.paste:
269                return mImeAdapter.paste();
270            default:
271                return false;
272        }
273    }
274
275    /**
276     * @see BaseInputConnection#getExtractedText(android.view.inputmethod.ExtractedTextRequest,
277     *                                           int)
278     */
279    @Override
280    public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
281        if (DEBUG) Log.w(TAG, "getExtractedText");
282        ExtractedText et = new ExtractedText();
283        et.text = mEditable.toString();
284        et.partialEndOffset = mEditable.length();
285        et.selectionStart = Selection.getSelectionStart(mEditable);
286        et.selectionEnd = Selection.getSelectionEnd(mEditable);
287        et.flags = mSingleLine ? ExtractedText.FLAG_SINGLE_LINE : 0;
288        return et;
289    }
290
291    /**
292     * @see BaseInputConnection#beginBatchEdit()
293     */
294    @Override
295    public boolean beginBatchEdit() {
296        if (DEBUG) Log.w(TAG, "beginBatchEdit [" + (mNumNestedBatchEdits == 0) + "]");
297        mNumNestedBatchEdits++;
298        return true;
299    }
300
301    /**
302     * @see BaseInputConnection#endBatchEdit()
303     */
304    @Override
305    public boolean endBatchEdit() {
306        if (mNumNestedBatchEdits == 0) return false;
307        --mNumNestedBatchEdits;
308        if (DEBUG) Log.w(TAG, "endBatchEdit [" + (mNumNestedBatchEdits == 0) + "]");
309        if (mNumNestedBatchEdits == 0) updateSelectionIfRequired();
310        return mNumNestedBatchEdits != 0;
311    }
312
313    /**
314     * @see BaseInputConnection#deleteSurroundingText(int, int)
315     */
316    @Override
317    public boolean deleteSurroundingText(int beforeLength, int afterLength) {
318        if (DEBUG) {
319            Log.w(TAG, "deleteSurroundingText [" + beforeLength + " " + afterLength + "]");
320        }
321        int originalBeforeLength = beforeLength;
322        int originalAfterLength = afterLength;
323        int availableBefore = Selection.getSelectionStart(mEditable);
324        int availableAfter = mEditable.length() - Selection.getSelectionEnd(mEditable);
325        beforeLength = Math.min(beforeLength, availableBefore);
326        afterLength = Math.min(afterLength, availableAfter);
327        super.deleteSurroundingText(beforeLength, afterLength);
328        updateSelectionIfRequired();
329
330        // For single-char deletion calls |ImeAdapter.sendKeyEventWithKeyCode| with the real key
331        // code. For multi-character deletion, executes deletion by calling
332        // |ImeAdapter.deleteSurroundingText| and sends synthetic key events with a dummy key code.
333        int keyCode = KeyEvent.KEYCODE_UNKNOWN;
334        if (originalBeforeLength == 1 && originalAfterLength == 0)
335            keyCode = KeyEvent.KEYCODE_DEL;
336        else if (originalBeforeLength == 0 && originalAfterLength == 1)
337            keyCode = KeyEvent.KEYCODE_FORWARD_DEL;
338
339        boolean result = true;
340        if (keyCode == KeyEvent.KEYCODE_UNKNOWN) {
341            result = mImeAdapter.sendSyntheticKeyEvent(
342                    ImeAdapter.sEventTypeRawKeyDown, SystemClock.uptimeMillis(), keyCode, 0);
343            result &= mImeAdapter.deleteSurroundingText(beforeLength, afterLength);
344            result &= mImeAdapter.sendSyntheticKeyEvent(
345                    ImeAdapter.sEventTypeKeyUp, SystemClock.uptimeMillis(), keyCode, 0);
346        } else {
347            mImeAdapter.sendKeyEventWithKeyCode(
348                    keyCode, KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE);
349        }
350        return result;
351    }
352
353    /**
354     * @see BaseInputConnection#sendKeyEvent(android.view.KeyEvent)
355     */
356    @Override
357    public boolean sendKeyEvent(KeyEvent event) {
358        if (DEBUG) {
359            Log.w(TAG, "sendKeyEvent [" + event.getAction() + "] [" + event.getKeyCode() + "]");
360        }
361        // If this is a key-up, and backspace/del or if the key has a character representation,
362        // need to update the underlying Editable (i.e. the local representation of the text
363        // being edited).
364        if (event.getAction() == KeyEvent.ACTION_UP) {
365            if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
366                deleteSurroundingText(1, 0);
367                return true;
368            } else if (event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL) {
369                deleteSurroundingText(0, 1);
370                return true;
371            } else {
372                int unicodeChar = event.getUnicodeChar();
373                if (unicodeChar != 0) {
374                    int selectionStart = Selection.getSelectionStart(mEditable);
375                    int selectionEnd = Selection.getSelectionEnd(mEditable);
376                    if (selectionStart > selectionEnd) {
377                        int temp = selectionStart;
378                        selectionStart = selectionEnd;
379                        selectionEnd = temp;
380                    }
381                    mEditable.replace(selectionStart, selectionEnd,
382                            Character.toString((char) unicodeChar));
383                }
384            }
385        } else if (event.getAction() == KeyEvent.ACTION_DOWN) {
386            // TODO(aurimas): remove this workaround when crbug.com/278584 is fixed.
387            if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
388                beginBatchEdit();
389                finishComposingText();
390                mImeAdapter.translateAndSendNativeEvents(event);
391                endBatchEdit();
392                return true;
393            } else if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
394                return true;
395            } else if (event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL) {
396                return true;
397            }
398        }
399        mImeAdapter.translateAndSendNativeEvents(event);
400        return true;
401    }
402
403    /**
404     * @see BaseInputConnection#finishComposingText()
405     */
406    @Override
407    public boolean finishComposingText() {
408        if (DEBUG) Log.w(TAG, "finishComposingText");
409        if (getComposingSpanStart(mEditable) == getComposingSpanEnd(mEditable)) {
410            return true;
411        }
412
413        super.finishComposingText();
414        updateSelectionIfRequired();
415        mImeAdapter.finishComposingText();
416
417        return true;
418    }
419
420    /**
421     * @see BaseInputConnection#setSelection(int, int)
422     */
423    @Override
424    public boolean setSelection(int start, int end) {
425        if (DEBUG) Log.w(TAG, "setSelection [" + start + " " + end + "]");
426        int textLength = mEditable.length();
427        if (start < 0 || end < 0 || start > textLength || end > textLength) return true;
428        super.setSelection(start, end);
429        updateSelectionIfRequired();
430        return mImeAdapter.setEditableSelectionOffsets(start, end);
431    }
432
433    /**
434     * Informs the InputMethodManager and InputMethodSession (i.e. the IME) that the text
435     * state is no longer what the IME has and that it needs to be updated.
436     */
437    void restartInput() {
438        if (DEBUG) Log.w(TAG, "restartInput");
439        getInputMethodManagerWrapper().restartInput(mInternalView);
440        mNumNestedBatchEdits = 0;
441    }
442
443    /**
444     * @see BaseInputConnection#setComposingRegion(int, int)
445     */
446    @Override
447    public boolean setComposingRegion(int start, int end) {
448        if (DEBUG) Log.w(TAG, "setComposingRegion [" + start + " " + end + "]");
449        int textLength = mEditable.length();
450        int a = Math.min(start, end);
451        int b = Math.max(start, end);
452        if (a < 0) a = 0;
453        if (b < 0) b = 0;
454        if (a > textLength) a = textLength;
455        if (b > textLength) b = textLength;
456
457        if (a == b) {
458            removeComposingSpans(mEditable);
459        } else {
460            super.setComposingRegion(a, b);
461        }
462        updateSelectionIfRequired();
463        return mImeAdapter.setComposingRegion(a, b);
464    }
465
466    boolean isActive() {
467        return getInputMethodManagerWrapper().isActive(mInternalView);
468    }
469
470    private InputMethodManagerWrapper getInputMethodManagerWrapper() {
471        return mImeAdapter.getInputMethodManagerWrapper();
472    }
473
474    /**
475     * This method works around the issue crbug.com/373934 where Blink does not cancel
476     * the composition when we send a commit with the empty text.
477     *
478     * TODO(aurimas) Remove this once crbug.com/373934 is fixed.
479     *
480     * @param text Text that software keyboard requested to commit.
481     * @return Whether the workaround was performed.
482     */
483    private boolean maybePerformEmptyCompositionWorkaround(CharSequence text) {
484        int selectionStart = Selection.getSelectionStart(mEditable);
485        int selectionEnd = Selection.getSelectionEnd(mEditable);
486        int compositionStart = getComposingSpanStart(mEditable);
487        int compositionEnd = getComposingSpanEnd(mEditable);
488        if (TextUtils.isEmpty(text) && (selectionStart == selectionEnd)
489                && compositionStart != INVALID_COMPOSITION
490                && compositionEnd != INVALID_COMPOSITION) {
491            beginBatchEdit();
492            finishComposingText();
493            int selection = Selection.getSelectionStart(mEditable);
494            deleteSurroundingText(selection - compositionStart, selection - compositionEnd);
495            endBatchEdit();
496            return true;
497        }
498        return false;
499    }
500
501    @VisibleForTesting
502    static class ImeState {
503        public final String text;
504        public final int selectionStart;
505        public final int selectionEnd;
506        public final int compositionStart;
507        public final int compositionEnd;
508
509        public ImeState(String text, int selectionStart, int selectionEnd,
510                int compositionStart, int compositionEnd) {
511            this.text = text;
512            this.selectionStart = selectionStart;
513            this.selectionEnd = selectionEnd;
514            this.compositionStart = compositionStart;
515            this.compositionEnd = compositionEnd;
516        }
517    }
518
519    @VisibleForTesting
520    ImeState getImeStateForTesting() {
521        String text = mEditable.toString();
522        int selectionStart = Selection.getSelectionStart(mEditable);
523        int selectionEnd = Selection.getSelectionEnd(mEditable);
524        int compositionStart = getComposingSpanStart(mEditable);
525        int compositionEnd = getComposingSpanEnd(mEditable);
526        return new ImeState(text, selectionStart, selectionEnd, compositionStart, compositionEnd);
527    }
528}
529