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