AdapterInputConnection.java revision 58537e28ecd584eab876aee8be7156509866d23a
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(
168            int selectionStart, int selectionEnd,
169            int compositionStart, int compositionEnd) {
170        // Avoid sending update if we sent an exact update already previously.
171        if (mLastUpdateSelectionStart == selectionStart &&
172                mLastUpdateSelectionEnd == selectionEnd &&
173                mLastUpdateCompositionStart == compositionStart &&
174                mLastUpdateCompositionEnd == compositionEnd) {
175            return;
176        }
177        if (DEBUG) {
178            Log.w(TAG, "updateSelection [" + selectionStart + " " + selectionEnd + "] ["
179                    + compositionStart + " " + compositionEnd + "]");
180        }
181        // updateSelection should be called every time the selection or composition changes
182        // if it happens not within a batch edit, or at the end of each top level batch edit.
183        getInputMethodManagerWrapper().updateSelection(mInternalView,
184                selectionStart, selectionEnd, compositionStart, compositionEnd);
185        mLastUpdateSelectionStart = selectionStart;
186        mLastUpdateSelectionEnd = selectionEnd;
187        mLastUpdateCompositionStart = compositionStart;
188        mLastUpdateCompositionEnd = compositionEnd;
189    }
190
191    /**
192     * @see BaseInputConnection#setComposingText(java.lang.CharSequence, int)
193     */
194    @Override
195    public boolean setComposingText(CharSequence text, int newCursorPosition) {
196        if (DEBUG) Log.w(TAG, "setComposingText [" + text + "] [" + newCursorPosition + "]");
197        super.setComposingText(text, newCursorPosition);
198        return mImeAdapter.checkCompositionQueueAndCallNative(text.toString(),
199                newCursorPosition, false);
200    }
201
202    /**
203     * @see BaseInputConnection#commitText(java.lang.CharSequence, int)
204     */
205    @Override
206    public boolean commitText(CharSequence text, int newCursorPosition) {
207        if (DEBUG) Log.w(TAG, "commitText [" + text + "] [" + newCursorPosition + "]");
208        super.commitText(text, newCursorPosition);
209        return mImeAdapter.checkCompositionQueueAndCallNative(text.toString(),
210                newCursorPosition, text.length() > 0);
211    }
212
213    /**
214     * @see BaseInputConnection#performEditorAction(int)
215     */
216    @Override
217    public boolean performEditorAction(int actionCode) {
218        if (DEBUG) Log.w(TAG, "performEditorAction [" + actionCode + "]");
219        if (actionCode == EditorInfo.IME_ACTION_NEXT) {
220            restartInput();
221            // Send TAB key event
222            long timeStampMs = System.currentTimeMillis();
223            mImeAdapter.sendSyntheticKeyEvent(
224                    ImeAdapter.sEventTypeRawKeyDown, timeStampMs, KeyEvent.KEYCODE_TAB, 0);
225        } else {
226            mImeAdapter.sendKeyEventWithKeyCode(KeyEvent.KEYCODE_ENTER,
227                    KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE
228                    | KeyEvent.FLAG_EDITOR_ACTION);
229        }
230        return true;
231    }
232
233    /**
234     * @see BaseInputConnection#performContextMenuAction(int)
235     */
236    @Override
237    public boolean performContextMenuAction(int id) {
238        if (DEBUG) Log.w(TAG, "performContextMenuAction [" + id + "]");
239        switch (id) {
240            case android.R.id.selectAll:
241                return mImeAdapter.selectAll();
242            case android.R.id.cut:
243                return mImeAdapter.cut();
244            case android.R.id.copy:
245                return mImeAdapter.copy();
246            case android.R.id.paste:
247                return mImeAdapter.paste();
248            default:
249                return false;
250        }
251    }
252
253    /**
254     * @see BaseInputConnection#getExtractedText(android.view.inputmethod.ExtractedTextRequest,
255     *                                           int)
256     */
257    @Override
258    public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
259        if (DEBUG) Log.w(TAG, "getExtractedText");
260        ExtractedText et = new ExtractedText();
261        Editable editable = getEditable();
262        et.text = editable.toString();
263        et.partialEndOffset = editable.length();
264        et.selectionStart = Selection.getSelectionStart(editable);
265        et.selectionEnd = Selection.getSelectionEnd(editable);
266        et.flags = mSingleLine ? ExtractedText.FLAG_SINGLE_LINE : 0;
267        return et;
268    }
269
270    /**
271     * @see BaseInputConnection#beginBatchEdit()
272     */
273    @Override
274    public boolean beginBatchEdit() {
275        if (DEBUG) Log.w(TAG, "beginBatchEdit [" + (mNumNestedBatchEdits == 0) + "]");
276        if (mNumNestedBatchEdits == 0) mImeAdapter.batchStateChanged(true);
277
278        mNumNestedBatchEdits++;
279        return false;
280    }
281
282    /**
283     * @see BaseInputConnection#endBatchEdit()
284     */
285    @Override
286    public boolean endBatchEdit() {
287        if (mNumNestedBatchEdits == 0) return false;
288
289        --mNumNestedBatchEdits;
290        if (DEBUG) Log.w(TAG, "endBatchEdit [" + (mNumNestedBatchEdits == 0) + "]");
291        if (mNumNestedBatchEdits == 0) mImeAdapter.batchStateChanged(false);
292        return false;
293    }
294
295    /**
296     * @see BaseInputConnection#deleteSurroundingText(int, int)
297     */
298    @Override
299    public boolean deleteSurroundingText(int leftLength, int rightLength) {
300        if (DEBUG) {
301            Log.w(TAG, "deleteSurroundingText [" + leftLength + " " + rightLength + "]");
302        }
303        if (!super.deleteSurroundingText(leftLength, rightLength)) {
304            return false;
305        }
306        return mImeAdapter.deleteSurroundingText(leftLength, rightLength);
307    }
308
309    /**
310     * @see BaseInputConnection#sendKeyEvent(android.view.KeyEvent)
311     */
312    @Override
313    public boolean sendKeyEvent(KeyEvent event) {
314        if (DEBUG) Log.w(TAG, "sendKeyEvent [" + event.getAction() + "]");
315
316        // If this is a key-up, and backspace/del or if the key has a character representation,
317        // need to update the underlying Editable (i.e. the local representation of the text
318        // being edited).
319        if (event.getAction() == KeyEvent.ACTION_UP) {
320            if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
321                super.deleteSurroundingText(1, 0);
322            } else if (event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL) {
323                super.deleteSurroundingText(0, 1);
324            } else {
325                int unicodeChar = event.getUnicodeChar();
326                if (unicodeChar != 0) {
327                    Editable editable = getEditable();
328                    int selectionStart = Selection.getSelectionStart(editable);
329                    int selectionEnd = Selection.getSelectionEnd(editable);
330                    if (selectionStart > selectionEnd) {
331                        int temp = selectionStart;
332                        selectionStart = selectionEnd;
333                        selectionEnd = temp;
334                    }
335                    editable.replace(selectionStart, selectionEnd,
336                            Character.toString((char)unicodeChar));
337                }
338            }
339        } else if (event.getAction() == KeyEvent.ACTION_DOWN) {
340            // TODO(aurimas): remove this workaround when crbug.com/278584 is fixed.
341            if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
342                beginBatchEdit();
343                finishComposingText();
344                mImeAdapter.translateAndSendNativeEvents(event);
345                endBatchEdit();
346                return true;
347            }
348        }
349        mImeAdapter.translateAndSendNativeEvents(event);
350        return true;
351    }
352
353    /**
354     * @see BaseInputConnection#finishComposingText()
355     */
356    @Override
357    public boolean finishComposingText() {
358        if (DEBUG) Log.w(TAG, "finishComposingText");
359        Editable editable = getEditable();
360        if (getComposingSpanStart(editable) == getComposingSpanEnd(editable)) {
361            return true;
362        }
363
364        super.finishComposingText();
365        mImeAdapter.finishComposingText();
366
367        return true;
368    }
369
370    /**
371     * @see BaseInputConnection#setSelection(int, int)
372     */
373    @Override
374    public boolean setSelection(int start, int end) {
375        if (DEBUG) Log.w(TAG, "setSelection [" + start + " " + end + "]");
376        int textLength = getEditable().length();
377        if (start < 0 || end < 0 || start > textLength || end > textLength) return true;
378        super.setSelection(start, end);
379        return mImeAdapter.setEditableSelectionOffsets(start, end);
380    }
381
382    /**
383     * Informs the InputMethodManager and InputMethodSession (i.e. the IME) that the text
384     * state is no longer what the IME has and that it needs to be updated.
385     */
386    void restartInput() {
387        if (DEBUG) Log.w(TAG, "restartInput");
388        getInputMethodManagerWrapper().restartInput(mInternalView);
389        mIgnoreTextInputStateUpdates = false;
390        mNumNestedBatchEdits = 0;
391    }
392
393    /**
394     * @see BaseInputConnection#setComposingRegion(int, int)
395     */
396    @Override
397    public boolean setComposingRegion(int start, int end) {
398        if (DEBUG) Log.w(TAG, "setComposingRegion [" + start + " " + end + "]");
399        int textLength = getEditable().length();
400        int a = Math.min(start, end);
401        int b = Math.max(start, end);
402        if (a < 0) a = 0;
403        if (b < 0) b = 0;
404        if (a > textLength) a = textLength;
405        if (b > textLength) b = textLength;
406
407        if (a == b) {
408            removeComposingSpans(getEditable());
409        } else {
410            super.setComposingRegion(a, b);
411        }
412        return mImeAdapter.setComposingRegion(a, b);
413    }
414
415    boolean isActive() {
416        return getInputMethodManagerWrapper().isActive(mInternalView);
417    }
418
419    public void setIgnoreTextInputStateUpdates(boolean shouldIgnore) {
420        mIgnoreTextInputStateUpdates = shouldIgnore;
421        if (shouldIgnore) return;
422
423        Editable editable = getEditable();
424        updateSelection(Selection.getSelectionStart(editable),
425                Selection.getSelectionEnd(editable),
426                getComposingSpanStart(editable),
427                getComposingSpanEnd(editable));
428    }
429
430    @VisibleForTesting
431    protected boolean isIgnoringTextInputStateUpdates() {
432        return mIgnoreTextInputStateUpdates;
433    }
434
435    private InputMethodManagerWrapper getInputMethodManagerWrapper() {
436        return mImeAdapter.getInputMethodManagerWrapper();
437    }
438}
439