// Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.content.browser.input; import android.text.Editable; import android.text.InputType; import android.text.Selection; import android.util.Log; import android.view.KeyEvent; import android.view.View; import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; import com.google.common.annotations.VisibleForTesting; /** * InputConnection is created by ContentView.onCreateInputConnection. * It then adapts android's IME to chrome's RenderWidgetHostView using the * native ImeAdapterAndroid via the class ImeAdapter. */ public class AdapterInputConnection extends BaseInputConnection { private static final String TAG = "AdapterInputConnection"; private static final boolean DEBUG = false; /** * Selection value should be -1 if not known. See EditorInfo.java for details. */ public static final int INVALID_SELECTION = -1; public static final int INVALID_COMPOSITION = -1; private final View mInternalView; private final ImeAdapter mImeAdapter; private boolean mSingleLine; private int mNumNestedBatchEdits = 0; private int mLastUpdateSelectionStart = INVALID_SELECTION; private int mLastUpdateSelectionEnd = INVALID_SELECTION; private int mLastUpdateCompositionStart = INVALID_COMPOSITION; private int mLastUpdateCompositionEnd = INVALID_COMPOSITION; @VisibleForTesting AdapterInputConnection(View view, ImeAdapter imeAdapter, EditorInfo outAttrs) { super(view, true); mInternalView = view; mImeAdapter = imeAdapter; mImeAdapter.setInputConnection(this); mSingleLine = true; outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN | EditorInfo.IME_FLAG_NO_EXTRACT_UI; outAttrs.inputType = EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT; if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeText) { // Normal text field outAttrs.inputType |= EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT; outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO; } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeTextArea || imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeContentEditable) { // TextArea or contenteditable. outAttrs.inputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES | EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT; outAttrs.imeOptions |= EditorInfo.IME_ACTION_NONE; mSingleLine = false; } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypePassword) { // Password outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD; outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO; } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeSearch) { // Search outAttrs.imeOptions |= EditorInfo.IME_ACTION_SEARCH; } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeUrl) { // Url outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI; outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO; } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeEmail) { // Email outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS; outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO; } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeTel) { // Telephone // Number and telephone do not have both a Tab key and an // action in default OSK, so set the action to NEXT outAttrs.inputType = InputType.TYPE_CLASS_PHONE; outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT; } else if (imeAdapter.getTextInputType() == ImeAdapter.sTextInputTypeNumber) { // Number outAttrs.inputType = InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_NORMAL; outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT; } outAttrs.initialSelStart = imeAdapter.getInitialSelectionStart(); outAttrs.initialSelEnd = imeAdapter.getInitialSelectionEnd(); mLastUpdateSelectionStart = imeAdapter.getInitialSelectionStart(); mLastUpdateSelectionEnd = imeAdapter.getInitialSelectionEnd(); } /** * Updates the AdapterInputConnection's internal representation of the text being edited and * its selection and composition properties. The resulting Editable is accessible through the * getEditable() method. If the text has not changed, this also calls updateSelection on the * InputMethodManager. * * @param text The String contents of the field being edited. * @param selectionStart The character offset of the selection start, or the caret position if * there is no selection. * @param selectionEnd The character offset of the selection end, or the caret position if there * is no selection. * @param compositionStart The character offset of the composition start, or -1 if there is no * composition. * @param compositionEnd The character offset of the composition end, or -1 if there is no * selection. * @param requireAck True when the update was not caused by IME, false otherwise. */ public void updateState(String text, int selectionStart, int selectionEnd, int compositionStart, int compositionEnd, boolean requireAck) { if (DEBUG) { Log.w(TAG, "updateState [" + text + "] [" + selectionStart + " " + selectionEnd + "] [" + compositionStart + " " + compositionEnd + "] [" + requireAck + "]"); } if (!requireAck) return; // Non-breaking spaces can cause the IME to get confused. Replace with normal spaces. text = text.replace('\u00A0', ' '); selectionStart = Math.min(selectionStart, text.length()); selectionEnd = Math.min(selectionEnd, text.length()); compositionStart = Math.min(compositionStart, text.length()); compositionEnd = Math.min(compositionEnd, text.length()); Editable editable = getEditable(); String prevText = editable.toString(); boolean textUnchanged = prevText.equals(text); if (!textUnchanged) { editable.replace(0, editable.length(), text); } Selection.setSelection(editable, selectionStart, selectionEnd); if (compositionStart == compositionEnd) { removeComposingSpans(editable); } else { super.setComposingRegion(compositionStart, compositionEnd); } updateSelectionIfRequired(); } /** * Sends selection update to the InputMethodManager unless we are currently in a batch edit or * if the exact same selection and composition update was sent already. */ private void updateSelectionIfRequired() { if (mNumNestedBatchEdits != 0) return; Editable editable = getEditable(); int selectionStart = Selection.getSelectionStart(editable); int selectionEnd = Selection.getSelectionEnd(editable); int compositionStart = getComposingSpanStart(editable); int compositionEnd = getComposingSpanEnd(editable); // Avoid sending update if we sent an exact update already previously. if (mLastUpdateSelectionStart == selectionStart && mLastUpdateSelectionEnd == selectionEnd && mLastUpdateCompositionStart == compositionStart && mLastUpdateCompositionEnd == compositionEnd) { return; } if (DEBUG) { Log.w(TAG, "updateSelectionIfRequired [" + selectionStart + " " + selectionEnd + "] [" + compositionStart + " " + compositionEnd + "]"); } // updateSelection should be called every time the selection or composition changes // if it happens not within a batch edit, or at the end of each top level batch edit. getInputMethodManagerWrapper().updateSelection(mInternalView, selectionStart, selectionEnd, compositionStart, compositionEnd); mLastUpdateSelectionStart = selectionStart; mLastUpdateSelectionEnd = selectionEnd; mLastUpdateCompositionStart = compositionStart; mLastUpdateCompositionEnd = compositionEnd; } /** * @see BaseInputConnection#setComposingText(java.lang.CharSequence, int) */ @Override public boolean setComposingText(CharSequence text, int newCursorPosition) { if (DEBUG) Log.w(TAG, "setComposingText [" + text + "] [" + newCursorPosition + "]"); super.setComposingText(text, newCursorPosition); updateSelectionIfRequired(); return mImeAdapter.checkCompositionQueueAndCallNative(text.toString(), newCursorPosition, false); } /** * @see BaseInputConnection#commitText(java.lang.CharSequence, int) */ @Override public boolean commitText(CharSequence text, int newCursorPosition) { if (DEBUG) Log.w(TAG, "commitText [" + text + "] [" + newCursorPosition + "]"); super.commitText(text, newCursorPosition); updateSelectionIfRequired(); return mImeAdapter.checkCompositionQueueAndCallNative(text.toString(), newCursorPosition, text.length() > 0); } /** * @see BaseInputConnection#performEditorAction(int) */ @Override public boolean performEditorAction(int actionCode) { if (DEBUG) Log.w(TAG, "performEditorAction [" + actionCode + "]"); if (actionCode == EditorInfo.IME_ACTION_NEXT) { restartInput(); // Send TAB key event long timeStampMs = System.currentTimeMillis(); mImeAdapter.sendSyntheticKeyEvent( ImeAdapter.sEventTypeRawKeyDown, timeStampMs, KeyEvent.KEYCODE_TAB, 0); } else { mImeAdapter.sendKeyEventWithKeyCode(KeyEvent.KEYCODE_ENTER, KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE | KeyEvent.FLAG_EDITOR_ACTION); } return true; } /** * @see BaseInputConnection#performContextMenuAction(int) */ @Override public boolean performContextMenuAction(int id) { if (DEBUG) Log.w(TAG, "performContextMenuAction [" + id + "]"); switch (id) { case android.R.id.selectAll: return mImeAdapter.selectAll(); case android.R.id.cut: return mImeAdapter.cut(); case android.R.id.copy: return mImeAdapter.copy(); case android.R.id.paste: return mImeAdapter.paste(); default: return false; } } /** * @see BaseInputConnection#getExtractedText(android.view.inputmethod.ExtractedTextRequest, * int) */ @Override public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { if (DEBUG) Log.w(TAG, "getExtractedText"); ExtractedText et = new ExtractedText(); Editable editable = getEditable(); et.text = editable.toString(); et.partialEndOffset = editable.length(); et.selectionStart = Selection.getSelectionStart(editable); et.selectionEnd = Selection.getSelectionEnd(editable); et.flags = mSingleLine ? ExtractedText.FLAG_SINGLE_LINE : 0; return et; } /** * @see BaseInputConnection#beginBatchEdit() */ @Override public boolean beginBatchEdit() { if (DEBUG) Log.w(TAG, "beginBatchEdit [" + (mNumNestedBatchEdits == 0) + "]"); mNumNestedBatchEdits++; return true; } /** * @see BaseInputConnection#endBatchEdit() */ @Override public boolean endBatchEdit() { if (mNumNestedBatchEdits == 0) return false; --mNumNestedBatchEdits; if (DEBUG) Log.w(TAG, "endBatchEdit [" + (mNumNestedBatchEdits == 0) + "]"); if (mNumNestedBatchEdits == 0) updateSelectionIfRequired(); return mNumNestedBatchEdits != 0; } /** * @see BaseInputConnection#deleteSurroundingText(int, int) */ @Override public boolean deleteSurroundingText(int beforeLength, int afterLength) { if (DEBUG) { Log.w(TAG, "deleteSurroundingText [" + beforeLength + " " + afterLength + "]"); } Editable editable = getEditable(); int availableBefore = Selection.getSelectionStart(editable); int availableAfter = editable.length() - Selection.getSelectionEnd(editable); beforeLength = Math.min(beforeLength, availableBefore); afterLength = Math.min(afterLength, availableAfter); super.deleteSurroundingText(beforeLength, afterLength); updateSelectionIfRequired(); return mImeAdapter.deleteSurroundingText(beforeLength, afterLength); } /** * @see BaseInputConnection#sendKeyEvent(android.view.KeyEvent) */ @Override public boolean sendKeyEvent(KeyEvent event) { if (DEBUG) { Log.w(TAG, "sendKeyEvent [" + event.getAction() + "] [" + event.getKeyCode() + "]"); } // If this is a key-up, and backspace/del or if the key has a character representation, // need to update the underlying Editable (i.e. the local representation of the text // being edited). if (event.getAction() == KeyEvent.ACTION_UP) { if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) { deleteSurroundingText(1, 0); return true; } else if (event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL) { deleteSurroundingText(0, 1); return true; } else { int unicodeChar = event.getUnicodeChar(); if (unicodeChar != 0) { Editable editable = getEditable(); int selectionStart = Selection.getSelectionStart(editable); int selectionEnd = Selection.getSelectionEnd(editable); if (selectionStart > selectionEnd) { int temp = selectionStart; selectionStart = selectionEnd; selectionEnd = temp; } editable.replace(selectionStart, selectionEnd, Character.toString((char) unicodeChar)); } } } else if (event.getAction() == KeyEvent.ACTION_DOWN) { // TODO(aurimas): remove this workaround when crbug.com/278584 is fixed. if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) { beginBatchEdit(); finishComposingText(); mImeAdapter.translateAndSendNativeEvents(event); endBatchEdit(); return true; } else if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) { return true; } else if (event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL) { return true; } } mImeAdapter.translateAndSendNativeEvents(event); return true; } /** * @see BaseInputConnection#finishComposingText() */ @Override public boolean finishComposingText() { if (DEBUG) Log.w(TAG, "finishComposingText"); Editable editable = getEditable(); if (getComposingSpanStart(editable) == getComposingSpanEnd(editable)) { return true; } super.finishComposingText(); updateSelectionIfRequired(); mImeAdapter.finishComposingText(); return true; } /** * @see BaseInputConnection#setSelection(int, int) */ @Override public boolean setSelection(int start, int end) { if (DEBUG) Log.w(TAG, "setSelection [" + start + " " + end + "]"); int textLength = getEditable().length(); if (start < 0 || end < 0 || start > textLength || end > textLength) return true; super.setSelection(start, end); updateSelectionIfRequired(); return mImeAdapter.setEditableSelectionOffsets(start, end); } /** * Informs the InputMethodManager and InputMethodSession (i.e. the IME) that the text * state is no longer what the IME has and that it needs to be updated. */ void restartInput() { if (DEBUG) Log.w(TAG, "restartInput"); getInputMethodManagerWrapper().restartInput(mInternalView); mNumNestedBatchEdits = 0; } /** * @see BaseInputConnection#setComposingRegion(int, int) */ @Override public boolean setComposingRegion(int start, int end) { if (DEBUG) Log.w(TAG, "setComposingRegion [" + start + " " + end + "]"); int textLength = getEditable().length(); int a = Math.min(start, end); int b = Math.max(start, end); if (a < 0) a = 0; if (b < 0) b = 0; if (a > textLength) a = textLength; if (b > textLength) b = textLength; if (a == b) { removeComposingSpans(getEditable()); } else { super.setComposingRegion(a, b); } updateSelectionIfRequired(); return mImeAdapter.setComposingRegion(a, b); } boolean isActive() { return getInputMethodManagerWrapper().isActive(mInternalView); } private InputMethodManagerWrapper getInputMethodManagerWrapper() { return mImeAdapter.getInputMethodManagerWrapper(); } @VisibleForTesting static class ImeState { public final String text; public final int selectionStart; public final int selectionEnd; public final int compositionStart; public final int compositionEnd; public ImeState(String text, int selectionStart, int selectionEnd, int compositionStart, int compositionEnd) { this.text = text; this.selectionStart = selectionStart; this.selectionEnd = selectionEnd; this.compositionStart = compositionStart; this.compositionEnd = compositionEnd; } } @VisibleForTesting ImeState getImeStateForTesting() { Editable editable = getEditable(); String text = editable.toString(); int selectionStart = Selection.getSelectionStart(editable); int selectionEnd = Selection.getSelectionEnd(editable); int compositionStart = getComposingSpanStart(editable); int compositionEnd = getComposingSpanEnd(editable); return new ImeState(text, selectionStart, selectionEnd, compositionStart, compositionEnd); } }