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.text.TextUtils;
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
20import com.google.common.annotations.VisibleForTesting;
21
22/**
23 * InputConnection is created by ContentView.onCreateInputConnection.
24 * It then adapts android's IME to chrome's RenderWidgetHostView using the
25 * native ImeAdapterAndroid via the class ImeAdapter.
26 */
27public class AdapterInputConnection extends BaseInputConnection {
28    private static final String TAG = "AdapterInputConnection";
29    private static final boolean DEBUG = false;
30    /**
31     * Selection value should be -1 if not known. See EditorInfo.java for details.
32     */
33    public static final int INVALID_SELECTION = -1;
34    public static final int INVALID_COMPOSITION = -1;
35
36    private final View mInternalView;
37    private final ImeAdapter mImeAdapter;
38    private final Editable mEditable;
39
40    private boolean mSingleLine;
41    private int mNumNestedBatchEdits = 0;
42
43    private int mLastUpdateSelectionStart = INVALID_SELECTION;
44    private int mLastUpdateSelectionEnd = INVALID_SELECTION;
45    private int mLastUpdateCompositionStart = INVALID_COMPOSITION;
46    private int mLastUpdateCompositionEnd = INVALID_COMPOSITION;
47
48    @VisibleForTesting
49    AdapterInputConnection(View view, ImeAdapter imeAdapter, Editable editable,
50            EditorInfo outAttrs) {
51        super(view, true);
52        mInternalView = view;
53        mImeAdapter = imeAdapter;
54        mImeAdapter.setInputConnection(this);
55        mEditable = editable;
56        // The editable passed in might have been in use by a prior keyboard and could have had
57        // prior composition spans set.  To avoid keyboard conflicts, remove all composing spans
58        // when taking ownership of an existing Editable.
59        removeComposingSpans(mEditable);
60        mSingleLine = true;
61        outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN
62                | EditorInfo.IME_FLAG_NO_EXTRACT_UI;
63        outAttrs.inputType = EditorInfo.TYPE_CLASS_TEXT
64                | EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT;
65
66        if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeText) {
67            // Normal text field
68            outAttrs.inputType |= EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT;
69            outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO;
70        } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeTextArea ||
71                imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeContentEditable) {
72            // TextArea or contenteditable.
73            outAttrs.inputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE
74                    | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES
75                    | EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT;
76            outAttrs.imeOptions |= EditorInfo.IME_ACTION_NONE;
77            mSingleLine = false;
78        } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypePassword) {
79            // Password
80            outAttrs.inputType = InputType.TYPE_CLASS_TEXT
81                    | InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD;
82            outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO;
83        } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeSearch) {
84            // Search
85            outAttrs.imeOptions |= EditorInfo.IME_ACTION_SEARCH;
86        } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeUrl) {
87            // Url
88            outAttrs.inputType = InputType.TYPE_CLASS_TEXT
89                    | InputType.TYPE_TEXT_VARIATION_URI;
90            outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO;
91        } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeEmail) {
92            // Email
93            outAttrs.inputType = InputType.TYPE_CLASS_TEXT
94                    | InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS;
95            outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO;
96        } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeTel) {
97            // Telephone
98            // Number and telephone do not have both a Tab key and an
99            // action in default OSK, so set the action to NEXT
100            outAttrs.inputType = InputType.TYPE_CLASS_PHONE;
101            outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT;
102        } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeNumber) {
103            // Number
104            outAttrs.inputType = InputType.TYPE_CLASS_NUMBER
105                    | InputType.TYPE_NUMBER_VARIATION_NORMAL
106                    | InputType.TYPE_NUMBER_FLAG_DECIMAL;
107            outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT;
108        }
109        outAttrs.initialSelStart = Selection.getSelectionStart(mEditable);
110        outAttrs.initialSelEnd = Selection.getSelectionEnd(mEditable);
111        mLastUpdateSelectionStart = Selection.getSelectionStart(mEditable);
112        mLastUpdateSelectionEnd = Selection.getSelectionEnd(mEditable);
113
114        Selection.setSelection(mEditable, outAttrs.initialSelStart, outAttrs.initialSelEnd);
115        updateSelectionIfRequired();
116    }
117
118    /**
119     * Updates the AdapterInputConnection's internal representation of the text being edited and
120     * its selection and composition properties. The resulting Editable is accessible through the
121     * getEditable() method. If the text has not changed, this also calls updateSelection on the
122     * InputMethodManager.
123     *
124     * @param text The String contents of the field being edited.
125     * @param selectionStart The character offset of the selection start, or the caret position if
126     *                       there is no selection.
127     * @param selectionEnd The character offset of the selection end, or the caret position if there
128     *                     is no selection.
129     * @param compositionStart The character offset of the composition start, or -1 if there is no
130     *                         composition.
131     * @param compositionEnd The character offset of the composition end, or -1 if there is no
132     *                       selection.
133     * @param isNonImeChange True when the update was caused by non-IME (e.g. Javascript).
134     */
135    @VisibleForTesting
136    public void updateState(String text, int selectionStart, int selectionEnd, int compositionStart,
137            int compositionEnd, boolean isNonImeChange) {
138        if (DEBUG) {
139            Log.w(TAG, "updateState [" + text + "] [" + selectionStart + " " + selectionEnd + "] ["
140                    + compositionStart + " " + compositionEnd + "] [" + isNonImeChange + "]");
141        }
142        // If this update is from the IME, no further state modification is necessary because the
143        // state should have been updated already by the IM framework directly.
144        if (!isNonImeChange) return;
145
146        // Non-breaking spaces can cause the IME to get confused. Replace with normal spaces.
147        text = text.replace('\u00A0', ' ');
148
149        selectionStart = Math.min(selectionStart, text.length());
150        selectionEnd = Math.min(selectionEnd, text.length());
151        compositionStart = Math.min(compositionStart, text.length());
152        compositionEnd = Math.min(compositionEnd, text.length());
153
154        String prevText = mEditable.toString();
155        boolean textUnchanged = prevText.equals(text);
156
157        if (!textUnchanged) {
158            mEditable.replace(0, mEditable.length(), text);
159        }
160
161        Selection.setSelection(mEditable, selectionStart, selectionEnd);
162
163        if (compositionStart == compositionEnd) {
164            removeComposingSpans(mEditable);
165        } else {
166            super.setComposingRegion(compositionStart, compositionEnd);
167        }
168        updateSelectionIfRequired();
169    }
170
171    /**
172     * @return Editable object which contains the state of current focused editable element.
173     */
174    @Override
175    public Editable getEditable() {
176        return mEditable;
177    }
178
179    /**
180     * Sends selection update to the InputMethodManager unless we are currently in a batch edit or
181     * if the exact same selection and composition update was sent already.
182     */
183    private void updateSelectionIfRequired() {
184        if (mNumNestedBatchEdits != 0) return;
185        int selectionStart = Selection.getSelectionStart(mEditable);
186        int selectionEnd = Selection.getSelectionEnd(mEditable);
187        int compositionStart = getComposingSpanStart(mEditable);
188        int compositionEnd = getComposingSpanEnd(mEditable);
189        // Avoid sending update if we sent an exact update already previously.
190        if (mLastUpdateSelectionStart == selectionStart &&
191                mLastUpdateSelectionEnd == selectionEnd &&
192                mLastUpdateCompositionStart == compositionStart &&
193                mLastUpdateCompositionEnd == compositionEnd) {
194            return;
195        }
196        if (DEBUG) {
197            Log.w(TAG, "updateSelectionIfRequired [" + selectionStart + " " + selectionEnd + "] ["
198                    + compositionStart + " " + compositionEnd + "]");
199        }
200        // updateSelection should be called every time the selection or composition changes
201        // if it happens not within a batch edit, or at the end of each top level batch edit.
202        getInputMethodManagerWrapper().updateSelection(mInternalView,
203                selectionStart, selectionEnd, compositionStart, compositionEnd);
204        mLastUpdateSelectionStart = selectionStart;
205        mLastUpdateSelectionEnd = selectionEnd;
206        mLastUpdateCompositionStart = compositionStart;
207        mLastUpdateCompositionEnd = compositionEnd;
208    }
209
210    /**
211     * @see BaseInputConnection#setComposingText(java.lang.CharSequence, int)
212     */
213    @Override
214    public boolean setComposingText(CharSequence text, int newCursorPosition) {
215        if (DEBUG) Log.w(TAG, "setComposingText [" + text + "] [" + newCursorPosition + "]");
216        if (maybePerformEmptyCompositionWorkaround(text)) return true;
217        super.setComposingText(text, newCursorPosition);
218        updateSelectionIfRequired();
219        return mImeAdapter.checkCompositionQueueAndCallNative(text, newCursorPosition, false);
220    }
221
222    /**
223     * @see BaseInputConnection#commitText(java.lang.CharSequence, int)
224     */
225    @Override
226    public boolean commitText(CharSequence text, int newCursorPosition) {
227        if (DEBUG) Log.w(TAG, "commitText [" + text + "] [" + newCursorPosition + "]");
228        if (maybePerformEmptyCompositionWorkaround(text)) return true;
229        super.commitText(text, newCursorPosition);
230        updateSelectionIfRequired();
231        return mImeAdapter.checkCompositionQueueAndCallNative(text, newCursorPosition,
232                text.length() > 0);
233    }
234
235    /**
236     * @see BaseInputConnection#performEditorAction(int)
237     */
238    @Override
239    public boolean performEditorAction(int actionCode) {
240        if (DEBUG) Log.w(TAG, "performEditorAction [" + actionCode + "]");
241        if (actionCode == EditorInfo.IME_ACTION_NEXT) {
242            restartInput();
243            // Send TAB key event
244            long timeStampMs = SystemClock.uptimeMillis();
245            mImeAdapter.sendSyntheticKeyEvent(
246                    ImeAdapter.sEventTypeRawKeyDown, timeStampMs, KeyEvent.KEYCODE_TAB, 0);
247        } else {
248            mImeAdapter.sendKeyEventWithKeyCode(KeyEvent.KEYCODE_ENTER,
249                    KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE
250                    | KeyEvent.FLAG_EDITOR_ACTION);
251        }
252        return true;
253    }
254
255    /**
256     * @see BaseInputConnection#performContextMenuAction(int)
257     */
258    @Override
259    public boolean performContextMenuAction(int id) {
260        if (DEBUG) Log.w(TAG, "performContextMenuAction [" + id + "]");
261        switch (id) {
262            case android.R.id.selectAll:
263                return mImeAdapter.selectAll();
264            case android.R.id.cut:
265                return mImeAdapter.cut();
266            case android.R.id.copy:
267                return mImeAdapter.copy();
268            case android.R.id.paste:
269                return mImeAdapter.paste();
270            default:
271                return false;
272        }
273    }
274
275    /**
276     * @see BaseInputConnection#getExtractedText(android.view.inputmethod.ExtractedTextRequest,
277     *                                           int)
278     */
279    @Override
280    public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
281        if (DEBUG) Log.w(TAG, "getExtractedText");
282        ExtractedText et = new ExtractedText();
283        et.text = mEditable.toString();
284        et.partialEndOffset = mEditable.length();
285        et.selectionStart = Selection.getSelectionStart(mEditable);
286        et.selectionEnd = Selection.getSelectionEnd(mEditable);
287        et.flags = mSingleLine ? ExtractedText.FLAG_SINGLE_LINE : 0;
288        return et;
289    }
290
291    /**
292     * @see BaseInputConnection#beginBatchEdit()
293     */
294    @Override
295    public boolean beginBatchEdit() {
296        if (DEBUG) Log.w(TAG, "beginBatchEdit [" + (mNumNestedBatchEdits == 0) + "]");
297        mNumNestedBatchEdits++;
298        return true;
299    }
300
301    /**
302     * @see BaseInputConnection#endBatchEdit()
303     */
304    @Override
305    public boolean endBatchEdit() {
306        if (mNumNestedBatchEdits == 0) return false;
307        --mNumNestedBatchEdits;
308        if (DEBUG) Log.w(TAG, "endBatchEdit [" + (mNumNestedBatchEdits == 0) + "]");
309        if (mNumNestedBatchEdits == 0) updateSelectionIfRequired();
310        return mNumNestedBatchEdits != 0;
311    }
312
313    /**
314     * @see BaseInputConnection#deleteSurroundingText(int, int)
315     */
316    @Override
317    public boolean deleteSurroundingText(int beforeLength, int afterLength) {
318        if (DEBUG) {
319            Log.w(TAG, "deleteSurroundingText [" + beforeLength + " " + afterLength + "]");
320        }
321        int availableBefore = Selection.getSelectionStart(mEditable);
322        int availableAfter = mEditable.length() - Selection.getSelectionEnd(mEditable);
323        beforeLength = Math.min(beforeLength, availableBefore);
324        afterLength = Math.min(afterLength, availableAfter);
325        super.deleteSurroundingText(beforeLength, afterLength);
326        updateSelectionIfRequired();
327        return mImeAdapter.deleteSurroundingText(beforeLength, afterLength);
328    }
329
330    /**
331     * @see BaseInputConnection#sendKeyEvent(android.view.KeyEvent)
332     */
333    @Override
334    public boolean sendKeyEvent(KeyEvent event) {
335        if (DEBUG) {
336            Log.w(TAG, "sendKeyEvent [" + event.getAction() + "] [" + event.getKeyCode() + "]");
337        }
338        // If this is a key-up, and backspace/del or if the key has a character representation,
339        // need to update the underlying Editable (i.e. the local representation of the text
340        // being edited).
341        if (event.getAction() == KeyEvent.ACTION_UP) {
342            if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
343                deleteSurroundingText(1, 0);
344                return true;
345            } else if (event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL) {
346                deleteSurroundingText(0, 1);
347                return true;
348            } else {
349                int unicodeChar = event.getUnicodeChar();
350                if (unicodeChar != 0) {
351                    int selectionStart = Selection.getSelectionStart(mEditable);
352                    int selectionEnd = Selection.getSelectionEnd(mEditable);
353                    if (selectionStart > selectionEnd) {
354                        int temp = selectionStart;
355                        selectionStart = selectionEnd;
356                        selectionEnd = temp;
357                    }
358                    mEditable.replace(selectionStart, selectionEnd,
359                            Character.toString((char) unicodeChar));
360                }
361            }
362        } else if (event.getAction() == KeyEvent.ACTION_DOWN) {
363            // TODO(aurimas): remove this workaround when crbug.com/278584 is fixed.
364            if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
365                beginBatchEdit();
366                finishComposingText();
367                mImeAdapter.translateAndSendNativeEvents(event);
368                endBatchEdit();
369                return true;
370            } else if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
371                return true;
372            } else if (event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL) {
373                return true;
374            }
375        }
376        mImeAdapter.translateAndSendNativeEvents(event);
377        return true;
378    }
379
380    /**
381     * @see BaseInputConnection#finishComposingText()
382     */
383    @Override
384    public boolean finishComposingText() {
385        if (DEBUG) Log.w(TAG, "finishComposingText");
386        if (getComposingSpanStart(mEditable) == getComposingSpanEnd(mEditable)) {
387            return true;
388        }
389
390        super.finishComposingText();
391        updateSelectionIfRequired();
392        mImeAdapter.finishComposingText();
393
394        return true;
395    }
396
397    /**
398     * @see BaseInputConnection#setSelection(int, int)
399     */
400    @Override
401    public boolean setSelection(int start, int end) {
402        if (DEBUG) Log.w(TAG, "setSelection [" + start + " " + end + "]");
403        int textLength = mEditable.length();
404        if (start < 0 || end < 0 || start > textLength || end > textLength) return true;
405        super.setSelection(start, end);
406        updateSelectionIfRequired();
407        return mImeAdapter.setEditableSelectionOffsets(start, end);
408    }
409
410    /**
411     * Informs the InputMethodManager and InputMethodSession (i.e. the IME) that the text
412     * state is no longer what the IME has and that it needs to be updated.
413     */
414    void restartInput() {
415        if (DEBUG) Log.w(TAG, "restartInput");
416        getInputMethodManagerWrapper().restartInput(mInternalView);
417        mNumNestedBatchEdits = 0;
418    }
419
420    /**
421     * @see BaseInputConnection#setComposingRegion(int, int)
422     */
423    @Override
424    public boolean setComposingRegion(int start, int end) {
425        if (DEBUG) Log.w(TAG, "setComposingRegion [" + start + " " + end + "]");
426        int textLength = mEditable.length();
427        int a = Math.min(start, end);
428        int b = Math.max(start, end);
429        if (a < 0) a = 0;
430        if (b < 0) b = 0;
431        if (a > textLength) a = textLength;
432        if (b > textLength) b = textLength;
433
434        if (a == b) {
435            removeComposingSpans(mEditable);
436        } else {
437            super.setComposingRegion(a, b);
438        }
439        updateSelectionIfRequired();
440        return mImeAdapter.setComposingRegion(a, b);
441    }
442
443    boolean isActive() {
444        return getInputMethodManagerWrapper().isActive(mInternalView);
445    }
446
447    private InputMethodManagerWrapper getInputMethodManagerWrapper() {
448        return mImeAdapter.getInputMethodManagerWrapper();
449    }
450
451    /**
452     * This method works around the issue crbug.com/373934 where Blink does not cancel
453     * the composition when we send a commit with the empty text.
454     *
455     * TODO(aurimas) Remove this once crbug.com/373934 is fixed.
456     *
457     * @param text Text that software keyboard requested to commit.
458     * @return Whether the workaround was performed.
459     */
460    private boolean maybePerformEmptyCompositionWorkaround(CharSequence text) {
461        int selectionStart = Selection.getSelectionStart(mEditable);
462        int selectionEnd = Selection.getSelectionEnd(mEditable);
463        int compositionStart = getComposingSpanStart(mEditable);
464        int compositionEnd = getComposingSpanEnd(mEditable);
465        if (TextUtils.isEmpty(text) && (selectionStart == selectionEnd)
466                && compositionStart != INVALID_COMPOSITION
467                && compositionEnd != INVALID_COMPOSITION) {
468            beginBatchEdit();
469            finishComposingText();
470            int selection = Selection.getSelectionStart(mEditable);
471            deleteSurroundingText(selection - compositionStart, selection - compositionEnd);
472            endBatchEdit();
473            return true;
474        }
475        return false;
476    }
477
478    @VisibleForTesting
479    static class ImeState {
480        public final String text;
481        public final int selectionStart;
482        public final int selectionEnd;
483        public final int compositionStart;
484        public final int compositionEnd;
485
486        public ImeState(String text, int selectionStart, int selectionEnd,
487                int compositionStart, int compositionEnd) {
488            this.text = text;
489            this.selectionStart = selectionStart;
490            this.selectionEnd = selectionEnd;
491            this.compositionStart = compositionStart;
492            this.compositionEnd = compositionEnd;
493        }
494    }
495
496    @VisibleForTesting
497    ImeState getImeStateForTesting() {
498        String text = mEditable.toString();
499        int selectionStart = Selection.getSelectionStart(mEditable);
500        int selectionEnd = Selection.getSelectionEnd(mEditable);
501        int compositionStart = getComposingSpanStart(mEditable);
502        int compositionEnd = getComposingSpanEnd(mEditable);
503        return new ImeState(text, selectionStart, selectionEnd, compositionStart, compositionEnd);
504    }
505}
506