AdapterInputConnection.java revision d0247b1b59f9c528cb6df88b4f2b9afaf80d181e
1// Copyright (c) 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 com.google.common.annotations.VisibleForTesting;
8
9import android.text.Editable;
10import android.text.InputType;
11import android.text.Selection;
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
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 =
27            "org.chromium.content.browser.input.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
38    private boolean mSingleLine;
39    private int mNumNestedBatchEdits = 0;
40    private boolean mIgnoreTextInputStateUpdates = false;
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, EditorInfo outAttrs) {
49        super(view, true);
50        mInternalView = view;
51        mImeAdapter = imeAdapter;
52        mImeAdapter.setInputConnection(this);
53        mSingleLine = true;
54        outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN
55                | EditorInfo.IME_FLAG_NO_EXTRACT_UI;
56        outAttrs.inputType = EditorInfo.TYPE_CLASS_TEXT
57                | EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT;
58
59        if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeText) {
60            // Normal text field
61            outAttrs.inputType |= EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT;
62            outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO;
63        } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeTextArea ||
64                imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeContentEditable) {
65            // TextArea or contenteditable.
66            outAttrs.inputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE
67                    | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES
68                    | EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT;
69            outAttrs.imeOptions |= EditorInfo.IME_ACTION_NONE;
70            mSingleLine = false;
71        } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypePassword) {
72            // Password
73            outAttrs.inputType = InputType.TYPE_CLASS_TEXT
74                    | InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD;
75            outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO;
76        } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeSearch) {
77            // Search
78            outAttrs.imeOptions |= EditorInfo.IME_ACTION_SEARCH;
79        } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeUrl) {
80            // Url
81            // TYPE_TEXT_VARIATION_URI prevents Tab key from showing, so
82            // exclude it for now.
83            outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO;
84        } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeEmail) {
85            // Email
86            outAttrs.inputType = InputType.TYPE_CLASS_TEXT
87                    | InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS;
88            outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO;
89        } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeTel) {
90            // Telephone
91            // Number and telephone do not have both a Tab key and an
92            // action in default OSK, so set the action to NEXT
93            outAttrs.inputType = InputType.TYPE_CLASS_PHONE;
94            outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT;
95        } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeNumber) {
96            // Number
97            outAttrs.inputType = InputType.TYPE_CLASS_NUMBER
98                    | InputType.TYPE_NUMBER_VARIATION_NORMAL;
99            outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT;
100        }
101        outAttrs.initialSelStart = imeAdapter.getInitialSelectionStart();
102        outAttrs.initialSelEnd = imeAdapter.getInitialSelectionStart();
103    }
104
105    /**
106     * Updates the AdapterInputConnection's internal representation of the text
107     * being edited and its selection and composition properties. The resulting
108     * Editable is accessible through the getEditable() method.
109     * If the text has not changed, this also calls updateSelection on the InputMethodManager.
110     * @param text The String contents of the field being edited
111     * @param selectionStart The character offset of the selection start, or the caret
112     * position if there is no selection
113     * @param selectionEnd The character offset of the selection end, or the caret
114     * position if there is no selection
115     * @param compositionStart The character offset of the composition start, or -1
116     * if there is no composition
117     * @param compositionEnd The character offset of the composition end, or -1
118     * if there is no selection
119     */
120    public void setEditableText(String text, int selectionStart, int selectionEnd,
121            int compositionStart, int compositionEnd) {
122        if (DEBUG) {
123            Log.w(TAG, "setEditableText [" + text + "] [" + selectionStart + " " + selectionEnd
124                    + "] [" + compositionStart + " " + compositionEnd + "]");
125        }
126        // Non-breaking spaces can cause the IME to get confused. Replace with normal spaces.
127        text = text.replace('\u00A0', ' ');
128
129        selectionStart = Math.min(selectionStart, text.length());
130        selectionEnd = Math.min(selectionEnd, text.length());
131        compositionStart = Math.min(compositionStart, text.length());
132        compositionEnd = Math.min(compositionEnd, text.length());
133
134        Editable editable = getEditable();
135        String prevText = editable.toString();
136        boolean textUnchanged = prevText.equals(text);
137
138        if (!textUnchanged) {
139            editable.replace(0, editable.length(), text);
140        }
141
142        int prevSelectionStart = Selection.getSelectionStart(editable);
143        int prevSelectionEnd = Selection.getSelectionEnd(editable);
144        int prevCompositionStart = getComposingSpanStart(editable);
145        int prevCompositionEnd = getComposingSpanEnd(editable);
146
147        if (prevSelectionStart == selectionStart && prevSelectionEnd == selectionEnd
148                && prevCompositionStart == compositionStart
149                && prevCompositionEnd == compositionEnd) {
150            // Nothing has changed; don't need to do anything
151            return;
152        }
153
154        Selection.setSelection(editable, selectionStart, selectionEnd);
155
156        if (compositionStart == compositionEnd) {
157            removeComposingSpans(editable);
158        } else {
159            super.setComposingRegion(compositionStart, compositionEnd);
160        }
161
162        if (mIgnoreTextInputStateUpdates) return;
163        updateSelection(selectionStart, selectionEnd, compositionStart, compositionEnd);
164    }
165
166    @VisibleForTesting
167    protected void updateSelection(int selectionStart, int selectionEnd, int compositionStart,
168            int compositionEnd) {
169        // Avoid sending update if we sent an exact update already previously.
170        if (mLastUpdateSelectionStart == selectionStart &&
171                mLastUpdateSelectionEnd == selectionEnd &&
172                mLastUpdateCompositionStart == compositionStart &&
173                mLastUpdateCompositionEnd == compositionEnd) {
174            return;
175        }
176        if (DEBUG) {
177            Log.w(TAG, "updateSelection [" + selectionStart + " " + selectionEnd + "] ["
178                    + compositionStart + " " + compositionEnd + "]");
179        }
180        // updateSelection should be called every time the selection or composition changes
181        // if it happens not within a batch edit, or at the end of each top level batch edit.
182        getInputMethodManagerWrapper().updateSelection(mInternalView,
183                selectionStart, selectionEnd, compositionStart, compositionEnd);
184        mLastUpdateSelectionStart = selectionStart;
185        mLastUpdateSelectionEnd = selectionEnd;
186        mLastUpdateCompositionStart = compositionStart;
187        mLastUpdateCompositionEnd = compositionEnd;
188    }
189
190    /**
191     * @see BaseInputConnection#setComposingText(java.lang.CharSequence, int)
192     */
193    @Override
194    public boolean setComposingText(CharSequence text, int newCursorPosition) {
195        if (DEBUG) Log.w(TAG, "setComposingText [" + text + "] [" + newCursorPosition + "]");
196        super.setComposingText(text, newCursorPosition);
197        return mImeAdapter.checkCompositionQueueAndCallNative(text.toString(),
198                newCursorPosition, false);
199    }
200
201    /**
202     * @see BaseInputConnection#commitText(java.lang.CharSequence, int)
203     */
204    @Override
205    public boolean commitText(CharSequence text, int newCursorPosition) {
206        if (DEBUG) Log.w(TAG, "commitText [" + text + "] [" + newCursorPosition + "]");
207        super.commitText(text, newCursorPosition);
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        if (mNumNestedBatchEdits == 0) mImeAdapter.batchStateChanged(true);
276
277        mNumNestedBatchEdits++;
278        return false;
279    }
280
281    /**
282     * @see BaseInputConnection#endBatchEdit()
283     */
284    @Override
285    public boolean endBatchEdit() {
286        if (mNumNestedBatchEdits == 0) return false;
287
288        --mNumNestedBatchEdits;
289        if (DEBUG) Log.w(TAG, "endBatchEdit [" + (mNumNestedBatchEdits == 0) + "]");
290        if (mNumNestedBatchEdits == 0) mImeAdapter.batchStateChanged(false);
291        return false;
292    }
293
294    /**
295     * @see BaseInputConnection#deleteSurroundingText(int, int)
296     */
297    @Override
298    public boolean deleteSurroundingText(int beforeLength, int afterLength) {
299        if (DEBUG) {
300            Log.w(TAG, "deleteSurroundingText [" + beforeLength + " " + afterLength + "]");
301        }
302        Editable editable = getEditable();
303        int availableBefore = Selection.getSelectionStart(editable);
304        int availableAfter = editable.length() - Selection.getSelectionEnd(editable);
305        beforeLength = Math.min(beforeLength, availableBefore);
306        afterLength = Math.min(afterLength, availableAfter);
307        super.deleteSurroundingText(beforeLength, afterLength);
308        return mImeAdapter.deleteSurroundingText(beforeLength, afterLength);
309    }
310
311    /**
312     * @see BaseInputConnection#sendKeyEvent(android.view.KeyEvent)
313     */
314    @Override
315    public boolean sendKeyEvent(KeyEvent event) {
316        if (DEBUG) Log.w(TAG, "sendKeyEvent [" + event.getAction() + "]");
317
318        // If this is a key-up, and backspace/del or if the key has a character representation,
319        // need to update the underlying Editable (i.e. the local representation of the text
320        // being edited).
321        if (event.getAction() == KeyEvent.ACTION_UP) {
322            if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
323                super.deleteSurroundingText(1, 0);
324            } else if (event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL) {
325                super.deleteSurroundingText(0, 1);
326            } else {
327                int unicodeChar = event.getUnicodeChar();
328                if (unicodeChar != 0) {
329                    Editable editable = getEditable();
330                    int selectionStart = Selection.getSelectionStart(editable);
331                    int selectionEnd = Selection.getSelectionEnd(editable);
332                    if (selectionStart > selectionEnd) {
333                        int temp = selectionStart;
334                        selectionStart = selectionEnd;
335                        selectionEnd = temp;
336                    }
337                    editable.replace(selectionStart, selectionEnd,
338                            Character.toString((char)unicodeChar));
339                }
340            }
341        } else if (event.getAction() == KeyEvent.ACTION_DOWN) {
342            // TODO(aurimas): remove this workaround when crbug.com/278584 is fixed.
343            if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
344                beginBatchEdit();
345                finishComposingText();
346                mImeAdapter.translateAndSendNativeEvents(event);
347                endBatchEdit();
348                return true;
349            }
350        }
351        mImeAdapter.translateAndSendNativeEvents(event);
352        return true;
353    }
354
355    /**
356     * @see BaseInputConnection#finishComposingText()
357     */
358    @Override
359    public boolean finishComposingText() {
360        if (DEBUG) Log.w(TAG, "finishComposingText");
361        Editable editable = getEditable();
362        if (getComposingSpanStart(editable) == getComposingSpanEnd(editable)) {
363            return true;
364        }
365
366        super.finishComposingText();
367        mImeAdapter.finishComposingText();
368
369        return true;
370    }
371
372    /**
373     * @see BaseInputConnection#setSelection(int, int)
374     */
375    @Override
376    public boolean setSelection(int start, int end) {
377        if (DEBUG) Log.w(TAG, "setSelection [" + start + " " + end + "]");
378        int textLength = getEditable().length();
379        if (start < 0 || end < 0 || start > textLength || end > textLength) return true;
380        super.setSelection(start, end);
381        return mImeAdapter.setEditableSelectionOffsets(start, end);
382    }
383
384    /**
385     * Informs the InputMethodManager and InputMethodSession (i.e. the IME) that the text
386     * state is no longer what the IME has and that it needs to be updated.
387     */
388    void restartInput() {
389        if (DEBUG) Log.w(TAG, "restartInput");
390        getInputMethodManagerWrapper().restartInput(mInternalView);
391        mIgnoreTextInputStateUpdates = false;
392        mNumNestedBatchEdits = 0;
393    }
394
395    /**
396     * @see BaseInputConnection#setComposingRegion(int, int)
397     */
398    @Override
399    public boolean setComposingRegion(int start, int end) {
400        if (DEBUG) Log.w(TAG, "setComposingRegion [" + start + " " + end + "]");
401        int textLength = getEditable().length();
402        int a = Math.min(start, end);
403        int b = Math.max(start, end);
404        if (a < 0) a = 0;
405        if (b < 0) b = 0;
406        if (a > textLength) a = textLength;
407        if (b > textLength) b = textLength;
408
409        if (a == b) {
410            removeComposingSpans(getEditable());
411        } else {
412            super.setComposingRegion(a, b);
413        }
414        return mImeAdapter.setComposingRegion(a, b);
415    }
416
417    boolean isActive() {
418        return getInputMethodManagerWrapper().isActive(mInternalView);
419    }
420
421    public void setIgnoreTextInputStateUpdates(boolean shouldIgnore) {
422        mIgnoreTextInputStateUpdates = shouldIgnore;
423        if (shouldIgnore) return;
424
425        Editable editable = getEditable();
426        updateSelection(Selection.getSelectionStart(editable),
427                Selection.getSelectionEnd(editable),
428                getComposingSpanStart(editable),
429                getComposingSpanEnd(editable));
430    }
431
432    @VisibleForTesting
433    protected boolean isIgnoringTextInputStateUpdates() {
434        return mIgnoreTextInputStateUpdates;
435    }
436
437    private InputMethodManagerWrapper getInputMethodManagerWrapper() {
438        return mImeAdapter.getInputMethodManagerWrapper();
439    }
440}
441