| // 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.os.SystemClock; |
| import android.text.Editable; |
| import android.text.InputType; |
| import android.text.Selection; |
| import android.text.TextUtils; |
| import android.util.Log; |
| import android.view.KeyCharacterMap; |
| 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 org.chromium.base.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; |
| private static final int NO_ACCENT = 0; |
| /** |
| * 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 final Editable mEditable; |
| |
| private boolean mSingleLine; |
| private int mNumNestedBatchEdits = 0; |
| private int mPendingAccent; |
| |
| 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, Editable editable, |
| EditorInfo outAttrs) { |
| super(view, true); |
| mInternalView = view; |
| mImeAdapter = imeAdapter; |
| mImeAdapter.setInputConnection(this); |
| mEditable = editable; |
| // The editable passed in might have been in use by a prior keyboard and could have had |
| // prior composition spans set. To avoid keyboard conflicts, remove all composing spans |
| // when taking ownership of an existing Editable. |
| removeComposingSpans(mEditable); |
| 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; |
| |
| int inputType = imeAdapter.getTextInputType(); |
| int inputFlags = imeAdapter.getTextInputFlags(); |
| if ((inputFlags & imeAdapter.sTextInputFlagAutocompleteOff) != 0) { |
| outAttrs.inputType |= EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS; |
| } |
| |
| if (inputType == ImeAdapter.sTextInputTypeText) { |
| // Normal text field |
| outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO; |
| if ((inputFlags & imeAdapter.sTextInputFlagAutocorrectOff) == 0) { |
| outAttrs.inputType |= EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT; |
| } |
| } else if (inputType == ImeAdapter.sTextInputTypeTextArea || |
| inputType == ImeAdapter.sTextInputTypeContentEditable) { |
| // TextArea or contenteditable. |
| outAttrs.inputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE |
| | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES; |
| if ((inputFlags & imeAdapter.sTextInputFlagAutocorrectOff) == 0) { |
| outAttrs.inputType |= EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT; |
| } |
| outAttrs.imeOptions |= EditorInfo.IME_ACTION_NONE; |
| mSingleLine = false; |
| } else if (inputType == ImeAdapter.sTextInputTypePassword) { |
| // Password |
| outAttrs.inputType = InputType.TYPE_CLASS_TEXT |
| | InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD; |
| outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO; |
| } else if (inputType == ImeAdapter.sTextInputTypeSearch) { |
| // Search |
| outAttrs.imeOptions |= EditorInfo.IME_ACTION_SEARCH; |
| } else if (inputType == ImeAdapter.sTextInputTypeUrl) { |
| // Url |
| outAttrs.inputType = InputType.TYPE_CLASS_TEXT |
| | InputType.TYPE_TEXT_VARIATION_URI; |
| outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO; |
| } else if (inputType == ImeAdapter.sTextInputTypeEmail) { |
| // Email |
| outAttrs.inputType = InputType.TYPE_CLASS_TEXT |
| | InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS; |
| outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO; |
| } else if (inputType == 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 (inputType == ImeAdapter.sTextInputTypeNumber) { |
| // Number |
| outAttrs.inputType = InputType.TYPE_CLASS_NUMBER |
| | InputType.TYPE_NUMBER_VARIATION_NORMAL |
| | InputType.TYPE_NUMBER_FLAG_DECIMAL; |
| outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT; |
| } |
| outAttrs.initialSelStart = Selection.getSelectionStart(mEditable); |
| outAttrs.initialSelEnd = Selection.getSelectionEnd(mEditable); |
| mLastUpdateSelectionStart = Selection.getSelectionStart(mEditable); |
| mLastUpdateSelectionEnd = Selection.getSelectionEnd(mEditable); |
| |
| Selection.setSelection(mEditable, outAttrs.initialSelStart, outAttrs.initialSelEnd); |
| updateSelectionIfRequired(); |
| } |
| |
| public static int maybeAddAccentToCharacter(int accentChar, int unicodeChar) { |
| if (accentChar != NO_ACCENT) { |
| int combinedChar = KeyEvent.getDeadChar(accentChar, unicodeChar); |
| if (combinedChar != 0) { |
| return combinedChar; |
| } |
| } |
| return unicodeChar; |
| } |
| |
| /** |
| * 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 isNonImeChange True when the update was caused by non-IME (e.g. Javascript). |
| */ |
| @VisibleForTesting |
| public void updateState(String text, int selectionStart, int selectionEnd, int compositionStart, |
| int compositionEnd, boolean isNonImeChange) { |
| if (DEBUG) { |
| Log.w(TAG, "updateState [" + text + "] [" + selectionStart + " " + selectionEnd + "] [" |
| + compositionStart + " " + compositionEnd + "] [" + isNonImeChange + "]"); |
| } |
| // If this update is from the IME, no further state modification is necessary because the |
| // state should have been updated already by the IM framework directly. |
| if (!isNonImeChange) 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()); |
| |
| String prevText = mEditable.toString(); |
| boolean textUnchanged = prevText.equals(text); |
| |
| if (!textUnchanged) { |
| mEditable.replace(0, mEditable.length(), text); |
| } |
| |
| Selection.setSelection(mEditable, selectionStart, selectionEnd); |
| |
| if (compositionStart == compositionEnd) { |
| removeComposingSpans(mEditable); |
| } else { |
| super.setComposingRegion(compositionStart, compositionEnd); |
| } |
| updateSelectionIfRequired(); |
| } |
| |
| /** |
| * @return Editable object which contains the state of current focused editable element. |
| */ |
| @Override |
| public Editable getEditable() { |
| return mEditable; |
| } |
| |
| /** |
| * 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; |
| int selectionStart = Selection.getSelectionStart(mEditable); |
| int selectionEnd = Selection.getSelectionEnd(mEditable); |
| int compositionStart = getComposingSpanStart(mEditable); |
| int compositionEnd = getComposingSpanEnd(mEditable); |
| // 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; |
| // Change in selection or cursor position invalidates any pending accent. |
| mPendingAccent = NO_ACCENT; |
| } |
| |
| /** |
| * @see BaseInputConnection#setComposingText(java.lang.CharSequence, int) |
| */ |
| @Override |
| public boolean setComposingText(CharSequence text, int newCursorPosition) { |
| if (DEBUG) Log.w(TAG, "setComposingText [" + text + "] [" + newCursorPosition + "]"); |
| if (maybePerformEmptyCompositionWorkaround(text)) return true; |
| super.setComposingText(text, newCursorPosition); |
| updateSelectionIfRequired(); |
| return mImeAdapter.checkCompositionQueueAndCallNative(text, 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 + "]"); |
| if (maybePerformEmptyCompositionWorkaround(text)) return true; |
| super.commitText(text, newCursorPosition); |
| updateSelectionIfRequired(); |
| return mImeAdapter.checkCompositionQueueAndCallNative(text, 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 = SystemClock.uptimeMillis(); |
| mImeAdapter.sendSyntheticKeyEvent( |
| ImeAdapter.sEventTypeRawKeyDown, timeStampMs, KeyEvent.KEYCODE_TAB, 0, 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(); |
| et.text = mEditable.toString(); |
| et.partialEndOffset = mEditable.length(); |
| et.selectionStart = Selection.getSelectionStart(mEditable); |
| et.selectionEnd = Selection.getSelectionEnd(mEditable); |
| 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 + "]"); |
| } |
| int originalBeforeLength = beforeLength; |
| int originalAfterLength = afterLength; |
| int availableBefore = Selection.getSelectionStart(mEditable); |
| int availableAfter = mEditable.length() - Selection.getSelectionEnd(mEditable); |
| beforeLength = Math.min(beforeLength, availableBefore); |
| afterLength = Math.min(afterLength, availableAfter); |
| super.deleteSurroundingText(beforeLength, afterLength); |
| updateSelectionIfRequired(); |
| |
| // For single-char deletion calls |ImeAdapter.sendKeyEventWithKeyCode| with the real key |
| // code. For multi-character deletion, executes deletion by calling |
| // |ImeAdapter.deleteSurroundingText| and sends synthetic key events with a dummy key code. |
| int keyCode = KeyEvent.KEYCODE_UNKNOWN; |
| if (originalBeforeLength == 1 && originalAfterLength == 0) |
| keyCode = KeyEvent.KEYCODE_DEL; |
| else if (originalBeforeLength == 0 && originalAfterLength == 1) |
| keyCode = KeyEvent.KEYCODE_FORWARD_DEL; |
| |
| boolean result = true; |
| if (keyCode == KeyEvent.KEYCODE_UNKNOWN) { |
| result = mImeAdapter.sendSyntheticKeyEvent( |
| ImeAdapter.sEventTypeRawKeyDown, SystemClock.uptimeMillis(), keyCode, 0, 0); |
| result &= mImeAdapter.deleteSurroundingText(beforeLength, afterLength); |
| result &= mImeAdapter.sendSyntheticKeyEvent( |
| ImeAdapter.sEventTypeKeyUp, SystemClock.uptimeMillis(), keyCode, 0, 0); |
| } else { |
| mImeAdapter.sendKeyEventWithKeyCode( |
| keyCode, KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE); |
| } |
| return result; |
| } |
| |
| /** |
| * @see BaseInputConnection#sendKeyEvent(android.view.KeyEvent) |
| */ |
| @Override |
| public boolean sendKeyEvent(KeyEvent event) { |
| if (DEBUG) { |
| Log.w(TAG, "sendKeyEvent [" + event.getAction() + "] [" + event.getKeyCode() + "]"); |
| } |
| |
| // Short-cut modifier keys so they're not affected by accents. |
| if (KeyEvent.isModifierKey(event.getKeyCode())) { |
| return mImeAdapter.translateAndSendNativeEvents(event, NO_ACCENT); |
| } |
| |
| int unicodeChar = event.getUnicodeChar(); |
| |
| // 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 if (unicodeChar != 0) { |
| int selectionStart = Selection.getSelectionStart(mEditable); |
| int selectionEnd = Selection.getSelectionEnd(mEditable); |
| if (selectionStart > selectionEnd) { |
| int temp = selectionStart; |
| selectionStart = selectionEnd; |
| selectionEnd = temp; |
| } |
| int combinedChar = maybeAddAccentToCharacter(mPendingAccent, unicodeChar); |
| mEditable.replace(selectionStart, selectionEnd, |
| Character.toString((char) combinedChar)); |
| } |
| } 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, 0); |
| 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, mPendingAccent); |
| |
| // Physical keyboards also have their events come through here though not |
| // by BaseInputConnection. In order to support "accent" key sequences |
| // such as "~n" or "^o" we have to record that one has been pressed |
| // and, if an accentable letter follows, delete the accent glyph and |
| // insert the composed character. |
| |
| // Copy class variable to local because class version may get indirectly |
| // cleared by the deleteSurroundingText() call below. |
| int pendingAccent = mPendingAccent; |
| |
| if ((unicodeChar & KeyCharacterMap.COMBINING_ACCENT) != 0) { |
| pendingAccent = unicodeChar & KeyCharacterMap.COMBINING_ACCENT_MASK; |
| } else if (pendingAccent != NO_ACCENT) { |
| if (event.getAction() == KeyEvent.ACTION_DOWN) { |
| int combined = KeyEvent.getDeadChar(pendingAccent, unicodeChar); |
| if (combined != 0) { |
| // Previous accent combines with new character to create |
| // a new accented character. First delete the displayed |
| // accent so it appears overwritten by the composition. |
| super.deleteSurroundingText(1, 0); |
| mImeAdapter.deleteSurroundingText(1, 0); |
| } else { |
| // Previous accent doesn't combine with this character |
| // so assume both are completely independent. |
| pendingAccent = NO_ACCENT; |
| } |
| } |
| |
| if (event.getAction() == KeyEvent.ACTION_UP) { |
| // Forget accent after release of key being accented. |
| pendingAccent = NO_ACCENT; |
| } |
| } |
| |
| mPendingAccent = pendingAccent; |
| return true; |
| } |
| |
| /** |
| * @see BaseInputConnection#finishComposingText() |
| */ |
| @Override |
| public boolean finishComposingText() { |
| if (DEBUG) Log.w(TAG, "finishComposingText"); |
| if (getComposingSpanStart(mEditable) == getComposingSpanEnd(mEditable)) { |
| 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 = mEditable.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; |
| mPendingAccent = NO_ACCENT; |
| } |
| |
| /** |
| * @see BaseInputConnection#setComposingRegion(int, int) |
| */ |
| @Override |
| public boolean setComposingRegion(int start, int end) { |
| if (DEBUG) Log.w(TAG, "setComposingRegion [" + start + " " + end + "]"); |
| int textLength = mEditable.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(mEditable); |
| } else { |
| super.setComposingRegion(a, b); |
| } |
| updateSelectionIfRequired(); |
| |
| CharSequence regionText = null; |
| if (b > a) { |
| regionText = mEditable.subSequence(a, b); |
| } |
| return mImeAdapter.setComposingRegion(regionText, a, b); |
| } |
| |
| boolean isActive() { |
| return getInputMethodManagerWrapper().isActive(mInternalView); |
| } |
| |
| private InputMethodManagerWrapper getInputMethodManagerWrapper() { |
| return mImeAdapter.getInputMethodManagerWrapper(); |
| } |
| |
| /** |
| * This method works around the issue crbug.com/373934 where Blink does not cancel |
| * the composition when we send a commit with the empty text. |
| * |
| * TODO(aurimas) Remove this once crbug.com/373934 is fixed. |
| * |
| * @param text Text that software keyboard requested to commit. |
| * @return Whether the workaround was performed. |
| */ |
| private boolean maybePerformEmptyCompositionWorkaround(CharSequence text) { |
| int selectionStart = Selection.getSelectionStart(mEditable); |
| int selectionEnd = Selection.getSelectionEnd(mEditable); |
| int compositionStart = getComposingSpanStart(mEditable); |
| int compositionEnd = getComposingSpanEnd(mEditable); |
| if (TextUtils.isEmpty(text) && (selectionStart == selectionEnd) |
| && compositionStart != INVALID_COMPOSITION |
| && compositionEnd != INVALID_COMPOSITION) { |
| beginBatchEdit(); |
| finishComposingText(); |
| int selection = Selection.getSelectionStart(mEditable); |
| deleteSurroundingText(selection - compositionStart, selection - compositionEnd); |
| endBatchEdit(); |
| return true; |
| } |
| return false; |
| } |
| |
| @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() { |
| String text = mEditable.toString(); |
| int selectionStart = Selection.getSelectionStart(mEditable); |
| int selectionEnd = Selection.getSelectionEnd(mEditable); |
| int compositionStart = getComposingSpanStart(mEditable); |
| int compositionEnd = getComposingSpanEnd(mEditable); |
| return new ImeState(text, selectionStart, selectionEnd, compositionStart, compositionEnd); |
| } |
| } |