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