AdapterInputConnection.java revision 868fa2fe829687343ffae624259930155e16dbd8
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.imeOptions |= EditorInfo.IME_ACTION_GO;
62        } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeTextArea ||
63                imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeContentEditable) {
64            // TextArea or contenteditable.
65            outAttrs.inputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE
66                    | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES
67                    | EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT;
68            outAttrs.imeOptions |= EditorInfo.IME_ACTION_NONE;
69            mSingleLine = false;
70        } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypePassword) {
71            // Password
72            outAttrs.inputType = InputType.TYPE_CLASS_TEXT
73                    | InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD;
74            outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO;
75        } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeSearch) {
76            // Search
77            outAttrs.imeOptions |= EditorInfo.IME_ACTION_SEARCH;
78        } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeUrl) {
79            // Url
80            // TYPE_TEXT_VARIATION_URI prevents Tab key from showing, so
81            // exclude it for now.
82            outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO;
83        } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeEmail) {
84            // Email
85            outAttrs.inputType = InputType.TYPE_CLASS_TEXT
86                    | InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS;
87            outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO;
88        } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeTel) {
89            // Telephone
90            // Number and telephone do not have both a Tab key and an
91            // action in default OSK, so set the action to NEXT
92            outAttrs.inputType = InputType.TYPE_CLASS_PHONE;
93            outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT;
94        } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeNumber) {
95            // Number
96            outAttrs.inputType = InputType.TYPE_CLASS_NUMBER
97                    | InputType.TYPE_NUMBER_VARIATION_NORMAL;
98            outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT;
99        }
100        outAttrs.initialSelStart = imeAdapter.getInitialSelectionStart();
101        outAttrs.initialSelEnd = imeAdapter.getInitialSelectionStart();
102    }
103
104    /**
105     * Updates the AdapterInputConnection's internal representation of the text
106     * being edited and its selection and composition properties. The resulting
107     * Editable is accessible through the getEditable() method.
108     * If the text has not changed, this also calls updateSelection on the InputMethodManager.
109     * @param text The String contents of the field being edited
110     * @param selectionStart The character offset of the selection start, or the caret
111     * position if there is no selection
112     * @param selectionEnd The character offset of the selection end, or the caret
113     * position if there is no selection
114     * @param compositionStart The character offset of the composition start, or -1
115     * if there is no composition
116     * @param compositionEnd The character offset of the composition end, or -1
117     * if there is no selection
118     */
119    public void setEditableText(String text, int selectionStart, int selectionEnd,
120            int compositionStart, int compositionEnd) {
121        if (DEBUG) {
122            Log.w(TAG, "setEditableText [" + text + "] [" + selectionStart + " " + selectionEnd
123                    + "] [" + compositionStart + " " + compositionEnd + "]");
124        }
125        // Non-breaking spaces can cause the IME to get confused. Replace with normal spaces.
126        text = text.replace('\u00A0', ' ');
127
128        selectionStart = Math.min(selectionStart, text.length());
129        selectionEnd = Math.min(selectionEnd, text.length());
130        compositionStart = Math.min(compositionStart, text.length());
131        compositionEnd = Math.min(compositionEnd, text.length());
132
133        Editable editable = getEditable();
134        String prevText = editable.toString();
135        boolean textUnchanged = prevText.equals(text);
136
137        if (!textUnchanged) {
138            editable.replace(0, editable.length(), text);
139        }
140
141        int prevSelectionStart = Selection.getSelectionStart(editable);
142        int prevSelectionEnd = Selection.getSelectionEnd(editable);
143        int prevCompositionStart = getComposingSpanStart(editable);
144        int prevCompositionEnd = getComposingSpanEnd(editable);
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