AdapterInputConnection.java revision 0529e5d033099cbfc42635f6f6183833b09dff6e
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                    | InputType.TYPE_NUMBER_FLAG_DECIMAL;
106            outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT;
107        }
108        outAttrs.initialSelStart = Selection.getSelectionStart(mEditable);
109        outAttrs.initialSelEnd = Selection.getSelectionEnd(mEditable);
110        mLastUpdateSelectionStart = Selection.getSelectionStart(mEditable);
111        mLastUpdateSelectionEnd = Selection.getSelectionEnd(mEditable);
112
113        Selection.setSelection(mEditable, outAttrs.initialSelStart, outAttrs.initialSelEnd);
114        updateSelectionIfRequired();
115    }
116
117    /**
118     * Updates the AdapterInputConnection's internal representation of the text being edited and
119     * its selection and composition properties. The resulting Editable is accessible through the
120     * getEditable() method. If the text has not changed, this also calls updateSelection on the
121     * InputMethodManager.
122     *
123     * @param text The String contents of the field being edited.
124     * @param selectionStart The character offset of the selection start, or the caret position if
125     *                       there is no selection.
126     * @param selectionEnd The character offset of the selection end, or the caret position if there
127     *                     is no selection.
128     * @param compositionStart The character offset of the composition start, or -1 if there is no
129     *                         composition.
130     * @param compositionEnd The character offset of the composition end, or -1 if there is no
131     *                       selection.
132     * @param isNonImeChange True when the update was caused by non-IME (e.g. Javascript).
133     */
134    @VisibleForTesting
135    public void updateState(String text, int selectionStart, int selectionEnd, int compositionStart,
136            int compositionEnd, boolean isNonImeChange) {
137        if (DEBUG) {
138            Log.w(TAG, "updateState [" + text + "] [" + selectionStart + " " + selectionEnd + "] ["
139                    + compositionStart + " " + compositionEnd + "] [" + isNonImeChange + "]");
140        }
141        // If this update is from the IME, no further state modification is necessary because the
142        // state should have been updated already by the IM framework directly.
143        if (!isNonImeChange) return;
144
145        // Non-breaking spaces can cause the IME to get confused. Replace with normal spaces.
146        text = text.replace('\u00A0', ' ');
147
148        selectionStart = Math.min(selectionStart, text.length());
149        selectionEnd = Math.min(selectionEnd, text.length());
150        compositionStart = Math.min(compositionStart, text.length());
151        compositionEnd = Math.min(compositionEnd, text.length());
152
153        String prevText = mEditable.toString();
154        boolean textUnchanged = prevText.equals(text);
155
156        if (!textUnchanged) {
157            mEditable.replace(0, mEditable.length(), text);
158        }
159
160        Selection.setSelection(mEditable, selectionStart, selectionEnd);
161
162        if (compositionStart == compositionEnd) {
163            removeComposingSpans(mEditable);
164        } else {
165            super.setComposingRegion(compositionStart, compositionEnd);
166        }
167        updateSelectionIfRequired();
168    }
169
170    /**
171     * @return Editable object which contains the state of current focused editable element.
172     */
173    @Override
174    public Editable getEditable() {
175        return mEditable;
176    }
177
178    /**
179     * Sends selection update to the InputMethodManager unless we are currently in a batch edit or
180     * if the exact same selection and composition update was sent already.
181     */
182    private void updateSelectionIfRequired() {
183        if (mNumNestedBatchEdits != 0) return;
184        int selectionStart = Selection.getSelectionStart(mEditable);
185        int selectionEnd = Selection.getSelectionEnd(mEditable);
186        int compositionStart = getComposingSpanStart(mEditable);
187        int compositionEnd = getComposingSpanEnd(mEditable);
188        // Avoid sending update if we sent an exact update already previously.
189        if (mLastUpdateSelectionStart == selectionStart &&
190                mLastUpdateSelectionEnd == selectionEnd &&
191                mLastUpdateCompositionStart == compositionStart &&
192                mLastUpdateCompositionEnd == compositionEnd) {
193            return;
194        }
195        if (DEBUG) {
196            Log.w(TAG, "updateSelectionIfRequired [" + selectionStart + " " + selectionEnd + "] ["
197                    + compositionStart + " " + compositionEnd + "]");
198        }
199        // updateSelection should be called every time the selection or composition changes
200        // if it happens not within a batch edit, or at the end of each top level batch edit.
201        getInputMethodManagerWrapper().updateSelection(mInternalView,
202                selectionStart, selectionEnd, compositionStart, compositionEnd);
203        mLastUpdateSelectionStart = selectionStart;
204        mLastUpdateSelectionEnd = selectionEnd;
205        mLastUpdateCompositionStart = compositionStart;
206        mLastUpdateCompositionEnd = compositionEnd;
207    }
208
209    /**
210     * @see BaseInputConnection#setComposingText(java.lang.CharSequence, int)
211     */
212    @Override
213    public boolean setComposingText(CharSequence text, int newCursorPosition) {
214        if (DEBUG) Log.w(TAG, "setComposingText [" + text + "] [" + newCursorPosition + "]");
215        super.setComposingText(text, newCursorPosition);
216        updateSelectionIfRequired();
217        return mImeAdapter.checkCompositionQueueAndCallNative(text.toString(),
218                newCursorPosition, false);
219    }
220
221    /**
222     * @see BaseInputConnection#commitText(java.lang.CharSequence, int)
223     */
224    @Override
225    public boolean commitText(CharSequence text, int newCursorPosition) {
226        if (DEBUG) Log.w(TAG, "commitText [" + text + "] [" + newCursorPosition + "]");
227        super.commitText(text, newCursorPosition);
228        updateSelectionIfRequired();
229        return mImeAdapter.checkCompositionQueueAndCallNative(text.toString(),
230                newCursorPosition, text.length() > 0);
231    }
232
233    /**
234     * @see BaseInputConnection#performEditorAction(int)
235     */
236    @Override
237    public boolean performEditorAction(int actionCode) {
238        if (DEBUG) Log.w(TAG, "performEditorAction [" + actionCode + "]");
239        if (actionCode == EditorInfo.IME_ACTION_NEXT) {
240            restartInput();
241            // Send TAB key event
242            long timeStampMs = SystemClock.uptimeMillis();
243            mImeAdapter.sendSyntheticKeyEvent(
244                    ImeAdapter.sEventTypeRawKeyDown, timeStampMs, KeyEvent.KEYCODE_TAB, 0);
245        } else {
246            mImeAdapter.sendKeyEventWithKeyCode(KeyEvent.KEYCODE_ENTER,
247                    KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE
248                    | KeyEvent.FLAG_EDITOR_ACTION);
249        }
250        return true;
251    }
252
253    /**
254     * @see BaseInputConnection#performContextMenuAction(int)
255     */
256    @Override
257    public boolean performContextMenuAction(int id) {
258        if (DEBUG) Log.w(TAG, "performContextMenuAction [" + id + "]");
259        switch (id) {
260            case android.R.id.selectAll:
261                return mImeAdapter.selectAll();
262            case android.R.id.cut:
263                return mImeAdapter.cut();
264            case android.R.id.copy:
265                return mImeAdapter.copy();
266            case android.R.id.paste:
267                return mImeAdapter.paste();
268            default:
269                return false;
270        }
271    }
272
273    /**
274     * @see BaseInputConnection#getExtractedText(android.view.inputmethod.ExtractedTextRequest,
275     *                                           int)
276     */
277    @Override
278    public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
279        if (DEBUG) Log.w(TAG, "getExtractedText");
280        ExtractedText et = new ExtractedText();
281        et.text = mEditable.toString();
282        et.partialEndOffset = mEditable.length();
283        et.selectionStart = Selection.getSelectionStart(mEditable);
284        et.selectionEnd = Selection.getSelectionEnd(mEditable);
285        et.flags = mSingleLine ? ExtractedText.FLAG_SINGLE_LINE : 0;
286        return et;
287    }
288
289    /**
290     * @see BaseInputConnection#beginBatchEdit()
291     */
292    @Override
293    public boolean beginBatchEdit() {
294        if (DEBUG) Log.w(TAG, "beginBatchEdit [" + (mNumNestedBatchEdits == 0) + "]");
295        mNumNestedBatchEdits++;
296        return true;
297    }
298
299    /**
300     * @see BaseInputConnection#endBatchEdit()
301     */
302    @Override
303    public boolean endBatchEdit() {
304        if (mNumNestedBatchEdits == 0) return false;
305        --mNumNestedBatchEdits;
306        if (DEBUG) Log.w(TAG, "endBatchEdit [" + (mNumNestedBatchEdits == 0) + "]");
307        if (mNumNestedBatchEdits == 0) updateSelectionIfRequired();
308        return mNumNestedBatchEdits != 0;
309    }
310
311    /**
312     * @see BaseInputConnection#deleteSurroundingText(int, int)
313     */
314    @Override
315    public boolean deleteSurroundingText(int beforeLength, int afterLength) {
316        if (DEBUG) {
317            Log.w(TAG, "deleteSurroundingText [" + beforeLength + " " + afterLength + "]");
318        }
319        int availableBefore = Selection.getSelectionStart(mEditable);
320        int availableAfter = mEditable.length() - Selection.getSelectionEnd(mEditable);
321        beforeLength = Math.min(beforeLength, availableBefore);
322        afterLength = Math.min(afterLength, availableAfter);
323        super.deleteSurroundingText(beforeLength, afterLength);
324        updateSelectionIfRequired();
325        return mImeAdapter.deleteSurroundingText(beforeLength, afterLength);
326    }
327
328    /**
329     * @see BaseInputConnection#sendKeyEvent(android.view.KeyEvent)
330     */
331    @Override
332    public boolean sendKeyEvent(KeyEvent event) {
333        if (DEBUG) {
334            Log.w(TAG, "sendKeyEvent [" + event.getAction() + "] [" + event.getKeyCode() + "]");
335        }
336        // If this is a key-up, and backspace/del or if the key has a character representation,
337        // need to update the underlying Editable (i.e. the local representation of the text
338        // being edited).
339        if (event.getAction() == KeyEvent.ACTION_UP) {
340            if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
341                deleteSurroundingText(1, 0);
342                return true;
343            } else if (event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL) {
344                deleteSurroundingText(0, 1);
345                return true;
346            } else {
347                int unicodeChar = event.getUnicodeChar();
348                if (unicodeChar != 0) {
349                    int selectionStart = Selection.getSelectionStart(mEditable);
350                    int selectionEnd = Selection.getSelectionEnd(mEditable);
351                    if (selectionStart > selectionEnd) {
352                        int temp = selectionStart;
353                        selectionStart = selectionEnd;
354                        selectionEnd = temp;
355                    }
356                    mEditable.replace(selectionStart, selectionEnd,
357                            Character.toString((char) unicodeChar));
358                }
359            }
360        } else if (event.getAction() == KeyEvent.ACTION_DOWN) {
361            // TODO(aurimas): remove this workaround when crbug.com/278584 is fixed.
362            if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
363                beginBatchEdit();
364                finishComposingText();
365                mImeAdapter.translateAndSendNativeEvents(event);
366                endBatchEdit();
367                return true;
368            } else if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
369                return true;
370            } else if (event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL) {
371                return true;
372            }
373        }
374        mImeAdapter.translateAndSendNativeEvents(event);
375        return true;
376    }
377
378    /**
379     * @see BaseInputConnection#finishComposingText()
380     */
381    @Override
382    public boolean finishComposingText() {
383        if (DEBUG) Log.w(TAG, "finishComposingText");
384        if (getComposingSpanStart(mEditable) == getComposingSpanEnd(mEditable)) {
385            return true;
386        }
387
388        super.finishComposingText();
389        updateSelectionIfRequired();
390        mImeAdapter.finishComposingText();
391
392        return true;
393    }
394
395    /**
396     * @see BaseInputConnection#setSelection(int, int)
397     */
398    @Override
399    public boolean setSelection(int start, int end) {
400        if (DEBUG) Log.w(TAG, "setSelection [" + start + " " + end + "]");
401        int textLength = mEditable.length();
402        if (start < 0 || end < 0 || start > textLength || end > textLength) return true;
403        super.setSelection(start, end);
404        updateSelectionIfRequired();
405        return mImeAdapter.setEditableSelectionOffsets(start, end);
406    }
407
408    /**
409     * Informs the InputMethodManager and InputMethodSession (i.e. the IME) that the text
410     * state is no longer what the IME has and that it needs to be updated.
411     */
412    void restartInput() {
413        if (DEBUG) Log.w(TAG, "restartInput");
414        getInputMethodManagerWrapper().restartInput(mInternalView);
415        mNumNestedBatchEdits = 0;
416    }
417
418    /**
419     * @see BaseInputConnection#setComposingRegion(int, int)
420     */
421    @Override
422    public boolean setComposingRegion(int start, int end) {
423        if (DEBUG) Log.w(TAG, "setComposingRegion [" + start + " " + end + "]");
424        int textLength = mEditable.length();
425        int a = Math.min(start, end);
426        int b = Math.max(start, end);
427        if (a < 0) a = 0;
428        if (b < 0) b = 0;
429        if (a > textLength) a = textLength;
430        if (b > textLength) b = textLength;
431
432        if (a == b) {
433            removeComposingSpans(mEditable);
434        } else {
435            super.setComposingRegion(a, b);
436        }
437        updateSelectionIfRequired();
438        return mImeAdapter.setComposingRegion(a, b);
439    }
440
441    boolean isActive() {
442        return getInputMethodManagerWrapper().isActive(mInternalView);
443    }
444
445    private InputMethodManagerWrapper getInputMethodManagerWrapper() {
446        return mImeAdapter.getInputMethodManagerWrapper();
447    }
448
449    @VisibleForTesting
450    static class ImeState {
451        public final String text;
452        public final int selectionStart;
453        public final int selectionEnd;
454        public final int compositionStart;
455        public final int compositionEnd;
456
457        public ImeState(String text, int selectionStart, int selectionEnd,
458                int compositionStart, int compositionEnd) {
459            this.text = text;
460            this.selectionStart = selectionStart;
461            this.selectionEnd = selectionEnd;
462            this.compositionStart = compositionStart;
463            this.compositionEnd = compositionEnd;
464        }
465    }
466
467    @VisibleForTesting
468    ImeState getImeStateForTesting() {
469        String text = mEditable.toString();
470        int selectionStart = Selection.getSelectionStart(mEditable);
471        int selectionEnd = Selection.getSelectionEnd(mEditable);
472        int compositionStart = getComposingSpanStart(mEditable);
473        int compositionEnd = getComposingSpanEnd(mEditable);
474        return new ImeState(text, selectionStart, selectionEnd, compositionStart, compositionEnd);
475    }
476}
477