AdapterInputConnection.java revision a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7
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        return true;
230    }
231
232    /**
233     * @see BaseInputConnection#performContextMenuAction(int)
234     */
235    @Override
236    public boolean performContextMenuAction(int id) {
237        if (DEBUG) Log.w(TAG, "performContextMenuAction [" + id + "]");
238        switch (id) {
239            case android.R.id.selectAll:
240                return mImeAdapter.selectAll();
241            case android.R.id.cut:
242                return mImeAdapter.cut();
243            case android.R.id.copy:
244                return mImeAdapter.copy();
245            case android.R.id.paste:
246                return mImeAdapter.paste();
247            default:
248                return false;
249        }
250    }
251
252    /**
253     * @see BaseInputConnection#getExtractedText(android.view.inputmethod.ExtractedTextRequest,
254     *                                           int)
255     */
256    @Override
257    public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
258        if (DEBUG) Log.w(TAG, "getExtractedText");
259        ExtractedText et = new ExtractedText();
260        Editable editable = getEditable();
261        et.text = editable.toString();
262        et.partialEndOffset = editable.length();
263        et.selectionStart = Selection.getSelectionStart(editable);
264        et.selectionEnd = Selection.getSelectionEnd(editable);
265        et.flags = mSingleLine ? ExtractedText.FLAG_SINGLE_LINE : 0;
266        return et;
267    }
268
269    /**
270     * @see BaseInputConnection#beginBatchEdit()
271     */
272    @Override
273    public boolean beginBatchEdit() {
274        if (DEBUG) Log.w(TAG, "beginBatchEdit [" + (mNumNestedBatchEdits == 0) + "]");
275        mNumNestedBatchEdits++;
276        return true;
277    }
278
279    /**
280     * @see BaseInputConnection#endBatchEdit()
281     */
282    @Override
283    public boolean endBatchEdit() {
284        if (mNumNestedBatchEdits == 0) return false;
285        --mNumNestedBatchEdits;
286        if (DEBUG) Log.w(TAG, "endBatchEdit [" + (mNumNestedBatchEdits == 0) + "]");
287        if (mNumNestedBatchEdits == 0) updateSelectionIfRequired();
288        return mNumNestedBatchEdits != 0;
289    }
290
291    /**
292     * @see BaseInputConnection#deleteSurroundingText(int, int)
293     */
294    @Override
295    public boolean deleteSurroundingText(int beforeLength, int afterLength) {
296        if (DEBUG) {
297            Log.w(TAG, "deleteSurroundingText [" + beforeLength + " " + afterLength + "]");
298        }
299        Editable editable = getEditable();
300        int availableBefore = Selection.getSelectionStart(editable);
301        int availableAfter = editable.length() - Selection.getSelectionEnd(editable);
302        beforeLength = Math.min(beforeLength, availableBefore);
303        afterLength = Math.min(afterLength, availableAfter);
304        super.deleteSurroundingText(beforeLength, afterLength);
305        updateSelectionIfRequired();
306        return mImeAdapter.deleteSurroundingText(beforeLength, afterLength);
307    }
308
309    /**
310     * @see BaseInputConnection#sendKeyEvent(android.view.KeyEvent)
311     */
312    @Override
313    public boolean sendKeyEvent(KeyEvent event) {
314        if (DEBUG) {
315            Log.w(TAG, "sendKeyEvent [" + event.getAction() + "] [" + event.getKeyCode() + "]");
316        }
317        // If this is a key-up, and backspace/del or if the key has a character representation,
318        // need to update the underlying Editable (i.e. the local representation of the text
319        // being edited).
320        if (event.getAction() == KeyEvent.ACTION_UP) {
321            if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
322                deleteSurroundingText(1, 0);
323                return true;
324            } else if (event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL) {
325                deleteSurroundingText(0, 1);
326                return true;
327            } else {
328                int unicodeChar = event.getUnicodeChar();
329                if (unicodeChar != 0) {
330                    Editable editable = getEditable();
331                    int selectionStart = Selection.getSelectionStart(editable);
332                    int selectionEnd = Selection.getSelectionEnd(editable);
333                    if (selectionStart > selectionEnd) {
334                        int temp = selectionStart;
335                        selectionStart = selectionEnd;
336                        selectionEnd = temp;
337                    }
338                    editable.replace(selectionStart, selectionEnd,
339                            Character.toString((char) unicodeChar));
340                }
341            }
342        } else if (event.getAction() == KeyEvent.ACTION_DOWN) {
343            // TODO(aurimas): remove this workaround when crbug.com/278584 is fixed.
344            if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
345                beginBatchEdit();
346                finishComposingText();
347                mImeAdapter.translateAndSendNativeEvents(event);
348                endBatchEdit();
349                return true;
350            } else if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
351                return true;
352            } else if (event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL) {
353                return true;
354            }
355        }
356        mImeAdapter.translateAndSendNativeEvents(event);
357        return true;
358    }
359
360    /**
361     * @see BaseInputConnection#finishComposingText()
362     */
363    @Override
364    public boolean finishComposingText() {
365        if (DEBUG) Log.w(TAG, "finishComposingText");
366        Editable editable = getEditable();
367        if (getComposingSpanStart(editable) == getComposingSpanEnd(editable)) {
368            return true;
369        }
370
371        super.finishComposingText();
372        updateSelectionIfRequired();
373        mImeAdapter.finishComposingText();
374
375        return true;
376    }
377
378    /**
379     * @see BaseInputConnection#setSelection(int, int)
380     */
381    @Override
382    public boolean setSelection(int start, int end) {
383        if (DEBUG) Log.w(TAG, "setSelection [" + start + " " + end + "]");
384        int textLength = getEditable().length();
385        if (start < 0 || end < 0 || start > textLength || end > textLength) return true;
386        super.setSelection(start, end);
387        updateSelectionIfRequired();
388        return mImeAdapter.setEditableSelectionOffsets(start, end);
389    }
390
391    /**
392     * Informs the InputMethodManager and InputMethodSession (i.e. the IME) that the text
393     * state is no longer what the IME has and that it needs to be updated.
394     */
395    void restartInput() {
396        if (DEBUG) Log.w(TAG, "restartInput");
397        getInputMethodManagerWrapper().restartInput(mInternalView);
398        mNumNestedBatchEdits = 0;
399    }
400
401    /**
402     * @see BaseInputConnection#setComposingRegion(int, int)
403     */
404    @Override
405    public boolean setComposingRegion(int start, int end) {
406        if (DEBUG) Log.w(TAG, "setComposingRegion [" + start + " " + end + "]");
407        int textLength = getEditable().length();
408        int a = Math.min(start, end);
409        int b = Math.max(start, end);
410        if (a < 0) a = 0;
411        if (b < 0) b = 0;
412        if (a > textLength) a = textLength;
413        if (b > textLength) b = textLength;
414
415        if (a == b) {
416            removeComposingSpans(getEditable());
417        } else {
418            super.setComposingRegion(a, b);
419        }
420        updateSelectionIfRequired();
421        return mImeAdapter.setComposingRegion(a, b);
422    }
423
424    boolean isActive() {
425        return getInputMethodManagerWrapper().isActive(mInternalView);
426    }
427
428    private InputMethodManagerWrapper getInputMethodManagerWrapper() {
429        return mImeAdapter.getInputMethodManagerWrapper();
430    }
431
432    @VisibleForTesting
433    static class ImeState {
434        public final String text;
435        public final int selectionStart;
436        public final int selectionEnd;
437        public final int compositionStart;
438        public final int compositionEnd;
439
440        public ImeState(String text, int selectionStart, int selectionEnd,
441                int compositionStart, int compositionEnd) {
442            this.text = text;
443            this.selectionStart = selectionStart;
444            this.selectionEnd = selectionEnd;
445            this.compositionStart = compositionStart;
446            this.compositionEnd = compositionEnd;
447        }
448    }
449
450    @VisibleForTesting
451    ImeState getImeStateForTesting() {
452        Editable editable = getEditable();
453        String text = editable.toString();
454        int selectionStart = Selection.getSelectionStart(editable);
455        int selectionEnd = Selection.getSelectionEnd(editable);
456        int compositionStart = getComposingSpanStart(editable);
457        int compositionEnd = getComposingSpanEnd(editable);
458        return new ImeState(text, selectionStart, selectionEnd, compositionStart, compositionEnd);
459    }
460}
461