AdapterInputConnection.java revision a93a17c8d99d686bd4a1511e5504e5e6cc9fcadf
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        outAttrs.inputType = EditorInfo.TYPE_CLASS_TEXT
56                | EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT;
57
58        if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeText) {
59            // Normal text field
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            // TYPE_TEXT_VARIATION_URI prevents Tab key from showing, so
80            // exclude it for now.
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.getInitialSelectionStart();
101    }
102
103    /**
104     * Updates the AdapterInputConnection's internal representation of the text
105     * being edited and its selection and composition properties. The resulting
106     * Editable is accessible through the getEditable() method.
107     * If the text has not changed, this also calls updateSelection on the InputMethodManager.
108     * @param text The String contents of the field being edited
109     * @param selectionStart The character offset of the selection start, or the caret
110     * position if there is no selection
111     * @param selectionEnd The character offset of the selection end, or the caret
112     * position if there is no selection
113     * @param compositionStart The character offset of the composition start, or -1
114     * if there is no composition
115     * @param compositionEnd The character offset of the composition end, or -1
116     * if there is no selection
117     */
118    public void setEditableText(String text, int selectionStart, int selectionEnd,
119            int compositionStart, int compositionEnd) {
120        if (DEBUG) {
121            Log.w(TAG, "setEditableText [" + text + "] [" + selectionStart + " " + selectionEnd
122                    + "] [" + compositionStart + " " + compositionEnd + "]");
123        }
124        // Non-breaking spaces can cause the IME to get confused. Replace with normal spaces.
125        text = text.replace('\u00A0', ' ');
126
127        Editable editable = getEditable();
128
129        int prevSelectionStart = Selection.getSelectionStart(editable);
130        int prevSelectionEnd = Selection.getSelectionEnd(editable);
131        int prevCompositionStart = getComposingSpanStart(editable);
132        int prevCompositionEnd = getComposingSpanEnd(editable);
133        String prevText = editable.toString();
134
135        selectionStart = Math.min(selectionStart, text.length());
136        selectionEnd = Math.min(selectionEnd, text.length());
137        compositionStart = Math.min(compositionStart, text.length());
138        compositionEnd = Math.min(compositionEnd, text.length());
139
140        boolean textUnchanged = prevText.equals(text);
141
142        if (!textUnchanged) {
143            editable.replace(0, editable.length(), text);
144        }
145
146        if (prevSelectionStart == selectionStart && prevSelectionEnd == selectionEnd
147                && prevCompositionStart == compositionStart
148                && prevCompositionEnd == compositionEnd) {
149            // Nothing has changed; don't need to do anything
150            return;
151        }
152
153        Selection.setSelection(editable, selectionStart, selectionEnd);
154
155        if (compositionStart == compositionEnd) {
156            removeComposingSpans(editable);
157        } else {
158            super.setComposingRegion(compositionStart, compositionEnd);
159        }
160
161        if (mIgnoreTextInputStateUpdates) return;
162        updateSelection(selectionStart, selectionEnd, compositionStart, compositionEnd);
163    }
164
165    @VisibleForTesting
166    protected void updateSelection(
167            int selectionStart, int selectionEnd,
168            int compositionStart, 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 leftLength, int rightLength) {
299        if (DEBUG) {
300            Log.w(TAG, "deleteSurroundingText [" + leftLength + " " + rightLength + "]");
301        }
302        if (!super.deleteSurroundingText(leftLength, rightLength)) {
303            return false;
304        }
305        return mImeAdapter.deleteSurroundingText(leftLength, rightLength);
306    }
307
308    /**
309     * @see BaseInputConnection#sendKeyEvent(android.view.KeyEvent)
310     */
311    @Override
312    public boolean sendKeyEvent(KeyEvent event) {
313        if (DEBUG) Log.w(TAG, "sendKeyEvent [" + event.getAction() + "]");
314        mImeAdapter.hideSelectionAndInsertionHandleControllers();
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        }
340        mImeAdapter.translateAndSendNativeEvents(event);
341        return true;
342    }
343
344    /**
345     * @see BaseInputConnection#finishComposingText()
346     */
347    @Override
348    public boolean finishComposingText() {
349        if (DEBUG) Log.w(TAG, "finishComposingText");
350        Editable editable = getEditable();
351        if (getComposingSpanStart(editable) == getComposingSpanEnd(editable)) {
352            return true;
353        }
354
355        // TODO(aurimas): remove this workaround of changing composition before confirmComposition
356        //                Blink should support keeping the cursor (http://crbug.com/239923)
357        int selectionStart = Selection.getSelectionStart(editable);
358        int compositionStart = getComposingSpanStart(editable);
359        super.finishComposingText();
360
361        beginBatchEdit();
362        if (compositionStart != -1 && compositionStart < selectionStart
363                && !mImeAdapter.setComposingRegion(compositionStart, selectionStart)) {
364            return false;
365        }
366        if (!mImeAdapter.checkCompositionQueueAndCallNative("", 0, true)) return false;
367        endBatchEdit();
368        return true;
369    }
370
371    /**
372     * @see BaseInputConnection#setSelection(int, int)
373     */
374    @Override
375    public boolean setSelection(int start, int end) {
376        if (DEBUG) Log.w(TAG, "setSelection");
377        if (start < 0 || end < 0) 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 a = Math.min(start, end);
400        int b = Math.max(start, end);
401        if (a < 0) a = 0;
402        if (b < 0) b = 0;
403
404        if (a == b) {
405            removeComposingSpans(getEditable());
406        } else {
407            super.setComposingRegion(a, b);
408        }
409        return mImeAdapter.setComposingRegion(a, b);
410    }
411
412    boolean isActive() {
413        return getInputMethodManagerWrapper().isActive(mInternalView);
414    }
415
416    public void setIgnoreTextInputStateUpdates(boolean shouldIgnore) {
417        mIgnoreTextInputStateUpdates = shouldIgnore;
418        if (shouldIgnore) return;
419
420        Editable editable = getEditable();
421        updateSelection(Selection.getSelectionStart(editable),
422                Selection.getSelectionEnd(editable),
423                getComposingSpanStart(editable),
424                getComposingSpanEnd(editable));
425    }
426
427    @VisibleForTesting
428    protected boolean isIgnoringTextInputStateUpdates() {
429        return mIgnoreTextInputStateUpdates;
430    }
431
432    private InputMethodManagerWrapper getInputMethodManagerWrapper() {
433        return mImeAdapter.getInputMethodManagerWrapper();
434    }
435}
436