AdapterInputConnection.java revision cedac228d2dd51db4b79ea1e72c7f249408ee061
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.toString(),
220                newCursorPosition, false);
221    }
222
223    /**
224     * @see BaseInputConnection#commitText(java.lang.CharSequence, int)
225     */
226    @Override
227    public boolean commitText(CharSequence text, int newCursorPosition) {
228        if (DEBUG) Log.w(TAG, "commitText [" + text + "] [" + newCursorPosition + "]");
229        if (maybePerformEmptyCompositionWorkaround(text)) return true;
230        super.commitText(text, newCursorPosition);
231        updateSelectionIfRequired();
232        return mImeAdapter.checkCompositionQueueAndCallNative(text.toString(),
233                newCursorPosition, text.length() > 0);
234    }
235
236    /**
237     * @see BaseInputConnection#performEditorAction(int)
238     */
239    @Override
240    public boolean performEditorAction(int actionCode) {
241        if (DEBUG) Log.w(TAG, "performEditorAction [" + actionCode + "]");
242        if (actionCode == EditorInfo.IME_ACTION_NEXT) {
243            restartInput();
244            // Send TAB key event
245            long timeStampMs = SystemClock.uptimeMillis();
246            mImeAdapter.sendSyntheticKeyEvent(
247                    ImeAdapter.sEventTypeRawKeyDown, timeStampMs, KeyEvent.KEYCODE_TAB, 0);
248        } else {
249            mImeAdapter.sendKeyEventWithKeyCode(KeyEvent.KEYCODE_ENTER,
250                    KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE
251                    | KeyEvent.FLAG_EDITOR_ACTION);
252        }
253        return true;
254    }
255
256    /**
257     * @see BaseInputConnection#performContextMenuAction(int)
258     */
259    @Override
260    public boolean performContextMenuAction(int id) {
261        if (DEBUG) Log.w(TAG, "performContextMenuAction [" + id + "]");
262        switch (id) {
263            case android.R.id.selectAll:
264                return mImeAdapter.selectAll();
265            case android.R.id.cut:
266                return mImeAdapter.cut();
267            case android.R.id.copy:
268                return mImeAdapter.copy();
269            case android.R.id.paste:
270                return mImeAdapter.paste();
271            default:
272                return false;
273        }
274    }
275
276    /**
277     * @see BaseInputConnection#getExtractedText(android.view.inputmethod.ExtractedTextRequest,
278     *                                           int)
279     */
280    @Override
281    public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
282        if (DEBUG) Log.w(TAG, "getExtractedText");
283        ExtractedText et = new ExtractedText();
284        et.text = mEditable.toString();
285        et.partialEndOffset = mEditable.length();
286        et.selectionStart = Selection.getSelectionStart(mEditable);
287        et.selectionEnd = Selection.getSelectionEnd(mEditable);
288        et.flags = mSingleLine ? ExtractedText.FLAG_SINGLE_LINE : 0;
289        return et;
290    }
291
292    /**
293     * @see BaseInputConnection#beginBatchEdit()
294     */
295    @Override
296    public boolean beginBatchEdit() {
297        if (DEBUG) Log.w(TAG, "beginBatchEdit [" + (mNumNestedBatchEdits == 0) + "]");
298        mNumNestedBatchEdits++;
299        return true;
300    }
301
302    /**
303     * @see BaseInputConnection#endBatchEdit()
304     */
305    @Override
306    public boolean endBatchEdit() {
307        if (mNumNestedBatchEdits == 0) return false;
308        --mNumNestedBatchEdits;
309        if (DEBUG) Log.w(TAG, "endBatchEdit [" + (mNumNestedBatchEdits == 0) + "]");
310        if (mNumNestedBatchEdits == 0) updateSelectionIfRequired();
311        return mNumNestedBatchEdits != 0;
312    }
313
314    /**
315     * @see BaseInputConnection#deleteSurroundingText(int, int)
316     */
317    @Override
318    public boolean deleteSurroundingText(int beforeLength, int afterLength) {
319        if (DEBUG) {
320            Log.w(TAG, "deleteSurroundingText [" + beforeLength + " " + afterLength + "]");
321        }
322        int availableBefore = Selection.getSelectionStart(mEditable);
323        int availableAfter = mEditable.length() - Selection.getSelectionEnd(mEditable);
324        beforeLength = Math.min(beforeLength, availableBefore);
325        afterLength = Math.min(afterLength, availableAfter);
326        super.deleteSurroundingText(beforeLength, afterLength);
327        updateSelectionIfRequired();
328        return mImeAdapter.deleteSurroundingText(beforeLength, afterLength);
329    }
330
331    /**
332     * @see BaseInputConnection#sendKeyEvent(android.view.KeyEvent)
333     */
334    @Override
335    public boolean sendKeyEvent(KeyEvent event) {
336        if (DEBUG) {
337            Log.w(TAG, "sendKeyEvent [" + event.getAction() + "] [" + event.getKeyCode() + "]");
338        }
339        // If this is a key-up, and backspace/del or if the key has a character representation,
340        // need to update the underlying Editable (i.e. the local representation of the text
341        // being edited).
342        if (event.getAction() == KeyEvent.ACTION_UP) {
343            if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
344                deleteSurroundingText(1, 0);
345                return true;
346            } else if (event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL) {
347                deleteSurroundingText(0, 1);
348                return true;
349            } else {
350                int unicodeChar = event.getUnicodeChar();
351                if (unicodeChar != 0) {
352                    int selectionStart = Selection.getSelectionStart(mEditable);
353                    int selectionEnd = Selection.getSelectionEnd(mEditable);
354                    if (selectionStart > selectionEnd) {
355                        int temp = selectionStart;
356                        selectionStart = selectionEnd;
357                        selectionEnd = temp;
358                    }
359                    mEditable.replace(selectionStart, selectionEnd,
360                            Character.toString((char) unicodeChar));
361                }
362            }
363        } else if (event.getAction() == KeyEvent.ACTION_DOWN) {
364            // TODO(aurimas): remove this workaround when crbug.com/278584 is fixed.
365            if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
366                beginBatchEdit();
367                finishComposingText();
368                mImeAdapter.translateAndSendNativeEvents(event);
369                endBatchEdit();
370                return true;
371            } else if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
372                return true;
373            } else if (event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL) {
374                return true;
375            }
376        }
377        mImeAdapter.translateAndSendNativeEvents(event);
378        return true;
379    }
380
381    /**
382     * @see BaseInputConnection#finishComposingText()
383     */
384    @Override
385    public boolean finishComposingText() {
386        if (DEBUG) Log.w(TAG, "finishComposingText");
387        if (getComposingSpanStart(mEditable) == getComposingSpanEnd(mEditable)) {
388            return true;
389        }
390
391        super.finishComposingText();
392        updateSelectionIfRequired();
393        mImeAdapter.finishComposingText();
394
395        return true;
396    }
397
398    /**
399     * @see BaseInputConnection#setSelection(int, int)
400     */
401    @Override
402    public boolean setSelection(int start, int end) {
403        if (DEBUG) Log.w(TAG, "setSelection [" + start + " " + end + "]");
404        int textLength = mEditable.length();
405        if (start < 0 || end < 0 || start > textLength || end > textLength) return true;
406        super.setSelection(start, end);
407        updateSelectionIfRequired();
408        return mImeAdapter.setEditableSelectionOffsets(start, end);
409    }
410
411    /**
412     * Informs the InputMethodManager and InputMethodSession (i.e. the IME) that the text
413     * state is no longer what the IME has and that it needs to be updated.
414     */
415    void restartInput() {
416        if (DEBUG) Log.w(TAG, "restartInput");
417        getInputMethodManagerWrapper().restartInput(mInternalView);
418        mNumNestedBatchEdits = 0;
419    }
420
421    /**
422     * @see BaseInputConnection#setComposingRegion(int, int)
423     */
424    @Override
425    public boolean setComposingRegion(int start, int end) {
426        if (DEBUG) Log.w(TAG, "setComposingRegion [" + start + " " + end + "]");
427        int textLength = mEditable.length();
428        int a = Math.min(start, end);
429        int b = Math.max(start, end);
430        if (a < 0) a = 0;
431        if (b < 0) b = 0;
432        if (a > textLength) a = textLength;
433        if (b > textLength) b = textLength;
434
435        if (a == b) {
436            removeComposingSpans(mEditable);
437        } else {
438            super.setComposingRegion(a, b);
439        }
440        updateSelectionIfRequired();
441        return mImeAdapter.setComposingRegion(a, b);
442    }
443
444    boolean isActive() {
445        return getInputMethodManagerWrapper().isActive(mInternalView);
446    }
447
448    private InputMethodManagerWrapper getInputMethodManagerWrapper() {
449        return mImeAdapter.getInputMethodManagerWrapper();
450    }
451
452    /**
453     * This method works around the issue crbug.com/373934 where Blink does not cancel
454     * the composition when we send a commit with the empty text.
455     *
456     * TODO(aurimas) Remove this once crbug.com/373934 is fixed.
457     *
458     * @param text Text that software keyboard requested to commit.
459     * @return Whether the workaround was performed.
460     */
461    private boolean maybePerformEmptyCompositionWorkaround(CharSequence text) {
462        int selectionStart = Selection.getSelectionStart(mEditable);
463        int selectionEnd = Selection.getSelectionEnd(mEditable);
464        int compositionStart = getComposingSpanStart(mEditable);
465        int compositionEnd = getComposingSpanEnd(mEditable);
466        if (TextUtils.isEmpty(text) && (selectionStart == selectionEnd)
467                && compositionStart != INVALID_COMPOSITION
468                && compositionEnd != INVALID_COMPOSITION) {
469            beginBatchEdit();
470            finishComposingText();
471            int selection = Selection.getSelectionStart(mEditable);
472            deleteSurroundingText(selection - compositionStart, selection - compositionEnd);
473            endBatchEdit();
474            return true;
475        }
476        return false;
477    }
478
479    @VisibleForTesting
480    static class ImeState {
481        public final String text;
482        public final int selectionStart;
483        public final int selectionEnd;
484        public final int compositionStart;
485        public final int compositionEnd;
486
487        public ImeState(String text, int selectionStart, int selectionEnd,
488                int compositionStart, int compositionEnd) {
489            this.text = text;
490            this.selectionStart = selectionStart;
491            this.selectionEnd = selectionEnd;
492            this.compositionStart = compositionStart;
493            this.compositionEnd = compositionEnd;
494        }
495    }
496
497    @VisibleForTesting
498    ImeState getImeStateForTesting() {
499        String text = mEditable.toString();
500        int selectionStart = Selection.getSelectionStart(mEditable);
501        int selectionEnd = Selection.getSelectionEnd(mEditable);
502        int compositionStart = getComposingSpanStart(mEditable);
503        int compositionEnd = getComposingSpanEnd(mEditable);
504        return new ImeState(text, selectionStart, selectionEnd, compositionStart, compositionEnd);
505    }
506}
507