/*
 * Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package android.view.inputmethod;

import android.annotation.CallSuper;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Bundle;
import android.os.Handler;
import android.os.SystemClock;
import android.text.Editable;
import android.text.NoCopySpan;
import android.text.Selection;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.method.MetaKeyKeyListener;
import android.util.Log;
import android.util.LogPrinter;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.View;

class ComposingText implements NoCopySpan {
}

Base class for implementors of the InputConnection interface, taking care of most of the common behavior for providing a connection to an Editable. Implementors of this class will want to be sure to implement getEditable to provide access to their own editable object, and to refer to the documentation in InputConnection.
/** * Base class for implementors of the InputConnection interface, taking care * of most of the common behavior for providing a connection to an Editable. * Implementors of this class will want to be sure to implement * {@link #getEditable} to provide access to their own editable object, and * to refer to the documentation in {@link InputConnection}. */
public class BaseInputConnection implements InputConnection { private static final boolean DEBUG = false; private static final String TAG = "BaseInputConnection"; static final Object COMPOSING = new ComposingText();
@hide
/** @hide */
protected final InputMethodManager mIMM; final View mTargetView; final boolean mDummyMode; private Object[] mDefaultComposingSpans; Editable mEditable; KeyCharacterMap mKeyCharacterMap; BaseInputConnection(InputMethodManager mgr, boolean fullEditor) { mIMM = mgr; mTargetView = null; mDummyMode = !fullEditor; } public BaseInputConnection(View targetView, boolean fullEditor) { mIMM = (InputMethodManager)targetView.getContext().getSystemService( Context.INPUT_METHOD_SERVICE); mTargetView = targetView; mDummyMode = !fullEditor; } public static final void removeComposingSpans(Spannable text) { text.removeSpan(COMPOSING); Object[] sps = text.getSpans(0, text.length(), Object.class); if (sps != null) { for (int i=sps.length-1; i>=0; i--) { Object o = sps[i]; if ((text.getSpanFlags(o)&Spanned.SPAN_COMPOSING) != 0) { text.removeSpan(o); } } } } public static void setComposingSpans(Spannable text) { setComposingSpans(text, 0, text.length()); }
@hide
/** @hide */
public static void setComposingSpans(Spannable text, int start, int end) { final Object[] sps = text.getSpans(start, end, Object.class); if (sps != null) { for (int i=sps.length-1; i>=0; i--) { final Object o = sps[i]; if (o == COMPOSING) { text.removeSpan(o); continue; } final int fl = text.getSpanFlags(o); if ((fl & (Spanned.SPAN_COMPOSING | Spanned.SPAN_POINT_MARK_MASK)) != (Spanned.SPAN_COMPOSING | Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)) { text.setSpan(o, text.getSpanStart(o), text.getSpanEnd(o), (fl & ~Spanned.SPAN_POINT_MARK_MASK) | Spanned.SPAN_COMPOSING | Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } } } text.setSpan(COMPOSING, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING); } public static int getComposingSpanStart(Spannable text) { return text.getSpanStart(COMPOSING); } public static int getComposingSpanEnd(Spannable text) { return text.getSpanEnd(COMPOSING); }
Return the target of edit operations. The default implementation returns its own fake editable that is just used for composing text; subclasses that are real text editors should override this and supply their own.
/** * Return the target of edit operations. The default implementation * returns its own fake editable that is just used for composing text; * subclasses that are real text editors should override this and * supply their own. */
public Editable getEditable() { if (mEditable == null) { mEditable = Editable.Factory.getInstance().newEditable(""); Selection.setSelection(mEditable, 0); } return mEditable; }
Default implementation does nothing.
/** * Default implementation does nothing. */
public boolean beginBatchEdit() { return false; }
Default implementation does nothing.
/** * Default implementation does nothing. */
public boolean endBatchEdit() { return false; }
Default implementation calls finishComposingText().
/** * Default implementation calls {@link #finishComposingText()}. */
@CallSuper public void closeConnection() { finishComposingText(); }
Default implementation uses MetaKeyKeyListener.clearMetaKeyState(long, int) to clear the state.
/** * Default implementation uses * {@link MetaKeyKeyListener#clearMetaKeyState(long, int) * MetaKeyKeyListener.clearMetaKeyState(long, int)} to clear the state. */
public boolean clearMetaKeyStates(int states) { final Editable content = getEditable(); if (content == null) return false; MetaKeyKeyListener.clearMetaKeyState(content, states); return true; }
Default implementation does nothing and returns false.
/** * Default implementation does nothing and returns false. */
public boolean commitCompletion(CompletionInfo text) { return false; }
Default implementation does nothing and returns false.
/** * Default implementation does nothing and returns false. */
public boolean commitCorrection(CorrectionInfo correctionInfo) { return false; }
Default implementation replaces any existing composing text with the given text. In addition, only if dummy mode, a key event is sent for the new text and the current editable buffer cleared.
/** * Default implementation replaces any existing composing text with * the given text. In addition, only if dummy mode, a key event is * sent for the new text and the current editable buffer cleared. */
public boolean commitText(CharSequence text, int newCursorPosition) { if (DEBUG) Log.v(TAG, "commitText " + text); replaceText(text, newCursorPosition, false); sendCurrentText(); return true; }
The default implementation performs the deletion around the current selection position of the editable text.
Params:
  • beforeLength – The number of characters before the cursor to be deleted, in code unit. If this is greater than the number of existing characters between the beginning of the text and the cursor, then this method does not fail but deletes all the characters in that range.
  • afterLength – The number of characters after the cursor to be deleted, in code unit. If this is greater than the number of existing characters between the cursor and the end of the text, then this method does not fail but deletes all the characters in that range.
/** * The default implementation performs the deletion around the current selection position of the * editable text. * * @param beforeLength The number of characters before the cursor to be deleted, in code unit. * If this is greater than the number of existing characters between the beginning of the * text and the cursor, then this method does not fail but deletes all the characters in * that range. * @param afterLength The number of characters after the cursor to be deleted, in code unit. * If this is greater than the number of existing characters between the cursor and * the end of the text, then this method does not fail but deletes all the characters in * that range. */
public boolean deleteSurroundingText(int beforeLength, int afterLength) { if (DEBUG) Log.v(TAG, "deleteSurroundingText " + beforeLength + " / " + afterLength); final Editable content = getEditable(); if (content == null) return false; beginBatchEdit(); int a = Selection.getSelectionStart(content); int b = Selection.getSelectionEnd(content); if (a > b) { int tmp = a; a = b; b = tmp; } // Ignore the composing text. int ca = getComposingSpanStart(content); int cb = getComposingSpanEnd(content); if (cb < ca) { int tmp = ca; ca = cb; cb = tmp; } if (ca != -1 && cb != -1) { if (ca < a) a = ca; if (cb > b) b = cb; } int deleted = 0; if (beforeLength > 0) { int start = a - beforeLength; if (start < 0) start = 0; content.delete(start, a); deleted = a - start; } if (afterLength > 0) { b = b - deleted; int end = b + afterLength; if (end > content.length()) end = content.length(); content.delete(b, end); } endBatchEdit(); return true; } private static int INVALID_INDEX = -1; private static int findIndexBackward(final CharSequence cs, final int from, final int numCodePoints) { int currentIndex = from; boolean waitingHighSurrogate = false; final int N = cs.length(); if (currentIndex < 0 || N < currentIndex) { return INVALID_INDEX; // The starting point is out of range. } if (numCodePoints < 0) { return INVALID_INDEX; // Basically this should not happen. } int remainingCodePoints = numCodePoints; while (true) { if (remainingCodePoints == 0) { return currentIndex; // Reached to the requested length in code points. } --currentIndex; if (currentIndex < 0) { if (waitingHighSurrogate) { return INVALID_INDEX; // An invalid surrogate pair is found. } return 0; // Reached to the beginning of the text w/o any invalid surrogate pair. } final char c = cs.charAt(currentIndex); if (waitingHighSurrogate) { if (!java.lang.Character.isHighSurrogate(c)) { return INVALID_INDEX; // An invalid surrogate pair is found. } waitingHighSurrogate = false; --remainingCodePoints; continue; } if (!java.lang.Character.isSurrogate(c)) { --remainingCodePoints; continue; } if (java.lang.Character.isHighSurrogate(c)) { return INVALID_INDEX; // A invalid surrogate pair is found. } waitingHighSurrogate = true; } } private static int findIndexForward(final CharSequence cs, final int from, final int numCodePoints) { int currentIndex = from; boolean waitingLowSurrogate = false; final int N = cs.length(); if (currentIndex < 0 || N < currentIndex) { return INVALID_INDEX; // The starting point is out of range. } if (numCodePoints < 0) { return INVALID_INDEX; // Basically this should not happen. } int remainingCodePoints = numCodePoints; while (true) { if (remainingCodePoints == 0) { return currentIndex; // Reached to the requested length in code points. } if (currentIndex >= N) { if (waitingLowSurrogate) { return INVALID_INDEX; // An invalid surrogate pair is found. } return N; // Reached to the end of the text w/o any invalid surrogate pair. } final char c = cs.charAt(currentIndex); if (waitingLowSurrogate) { if (!java.lang.Character.isLowSurrogate(c)) { return INVALID_INDEX; // An invalid surrogate pair is found. } --remainingCodePoints; waitingLowSurrogate = false; ++currentIndex; continue; } if (!java.lang.Character.isSurrogate(c)) { --remainingCodePoints; ++currentIndex; continue; } if (java.lang.Character.isLowSurrogate(c)) { return INVALID_INDEX; // A invalid surrogate pair is found. } waitingLowSurrogate = true; ++currentIndex; } }
The default implementation performs the deletion around the current selection position of the editable text.
Params:
  • beforeLength – The number of characters before the cursor to be deleted, in code points. If this is greater than the number of existing characters between the beginning of the text and the cursor, then this method does not fail but deletes all the characters in that range.
  • afterLength – The number of characters after the cursor to be deleted, in code points. If this is greater than the number of existing characters between the cursor and the end of the text, then this method does not fail but deletes all the characters in that range.
/** * The default implementation performs the deletion around the current selection position of the * editable text. * @param beforeLength The number of characters before the cursor to be deleted, in code points. * If this is greater than the number of existing characters between the beginning of the * text and the cursor, then this method does not fail but deletes all the characters in * that range. * @param afterLength The number of characters after the cursor to be deleted, in code points. * If this is greater than the number of existing characters between the cursor and * the end of the text, then this method does not fail but deletes all the characters in * that range. */
public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) { if (DEBUG) Log.v(TAG, "deleteSurroundingText " + beforeLength + " / " + afterLength); final Editable content = getEditable(); if (content == null) return false; beginBatchEdit(); int a = Selection.getSelectionStart(content); int b = Selection.getSelectionEnd(content); if (a > b) { int tmp = a; a = b; b = tmp; } // Ignore the composing text. int ca = getComposingSpanStart(content); int cb = getComposingSpanEnd(content); if (cb < ca) { int tmp = ca; ca = cb; cb = tmp; } if (ca != -1 && cb != -1) { if (ca < a) a = ca; if (cb > b) b = cb; } if (a >= 0 && b >= 0) { final int start = findIndexBackward(content, a, Math.max(beforeLength, 0)); if (start != INVALID_INDEX) { final int end = findIndexForward(content, b, Math.max(afterLength, 0)); if (end != INVALID_INDEX) { final int numDeleteBefore = a - start; if (numDeleteBefore > 0) { content.delete(start, a); } final int numDeleteAfter = end - b; if (numDeleteAfter > 0) { content.delete(b - numDeleteBefore, end - numDeleteBefore); } } } // NOTE: You may think we should return false here if start and/or end is INVALID_INDEX, // but the truth is that IInputConnectionWrapper running in the middle of IPC calls // always returns true to the IME without waiting for the completion of this method as // IInputConnectionWrapper#isAtive() returns true. This is actually why some methods // including this method look like asynchronous calls from the IME. } endBatchEdit(); return true; }
The default implementation removes the composing state from the current editable text. In addition, only if dummy mode, a key event is sent for the new text and the current editable buffer cleared.
/** * The default implementation removes the composing state from the * current editable text. In addition, only if dummy mode, a key event is * sent for the new text and the current editable buffer cleared. */
public boolean finishComposingText() { if (DEBUG) Log.v(TAG, "finishComposingText"); final Editable content = getEditable(); if (content != null) { beginBatchEdit(); removeComposingSpans(content); // Note: sendCurrentText does nothing unless mDummyMode is set sendCurrentText(); endBatchEdit(); } return true; }
The default implementation uses TextUtils.getCapsMode to get the cursor caps mode for the current selection position in the editable text, unless in dummy mode in which case 0 is always returned.
/** * The default implementation uses TextUtils.getCapsMode to get the * cursor caps mode for the current selection position in the editable * text, unless in dummy mode in which case 0 is always returned. */
public int getCursorCapsMode(int reqModes) { if (mDummyMode) return 0; final Editable content = getEditable(); if (content == null) return 0; int a = Selection.getSelectionStart(content); int b = Selection.getSelectionEnd(content); if (a > b) { int tmp = a; a = b; b = tmp; } return TextUtils.getCapsMode(content, a, reqModes); }
The default implementation always returns null.
/** * The default implementation always returns null. */
public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { return null; }
The default implementation returns the given amount of text from the current cursor position in the buffer.
/** * The default implementation returns the given amount of text from the * current cursor position in the buffer. */
public CharSequence getTextBeforeCursor(int length, int flags) { final Editable content = getEditable(); if (content == null) return null; int a = Selection.getSelectionStart(content); int b = Selection.getSelectionEnd(content); if (a > b) { int tmp = a; a = b; b = tmp; } if (a <= 0) { return ""; } if (length > a) { length = a; } if ((flags&GET_TEXT_WITH_STYLES) != 0) { return content.subSequence(a - length, a); } return TextUtils.substring(content, a - length, a); }
The default implementation returns the text currently selected, or null if none is selected.
/** * The default implementation returns the text currently selected, or null if none is * selected. */
public CharSequence getSelectedText(int flags) { final Editable content = getEditable(); if (content == null) return null; int a = Selection.getSelectionStart(content); int b = Selection.getSelectionEnd(content); if (a > b) { int tmp = a; a = b; b = tmp; } if (a == b || a < 0) return null; if ((flags&GET_TEXT_WITH_STYLES) != 0) { return content.subSequence(a, b); } return TextUtils.substring(content, a, b); }
The default implementation returns the given amount of text from the current cursor position in the buffer.
/** * The default implementation returns the given amount of text from the * current cursor position in the buffer. */
public CharSequence getTextAfterCursor(int length, int flags) { final Editable content = getEditable(); if (content == null) return null; int a = Selection.getSelectionStart(content); int b = Selection.getSelectionEnd(content); if (a > b) { int tmp = a; a = b; b = tmp; } // Guard against the case where the cursor has not been positioned yet. if (b < 0) { b = 0; } if (b + length > content.length()) { length = content.length() - b; } if ((flags&GET_TEXT_WITH_STYLES) != 0) { return content.subSequence(b, b + length); } return TextUtils.substring(content, b, b + length); }
The default implementation turns this into the enter key.
/** * The default implementation turns this into the enter key. */
public boolean performEditorAction(int actionCode) { long eventTime = SystemClock.uptimeMillis(); sendKeyEvent(new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE | KeyEvent.FLAG_EDITOR_ACTION)); sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE | KeyEvent.FLAG_EDITOR_ACTION)); return true; }
The default implementation does nothing.
/** * The default implementation does nothing. */
public boolean performContextMenuAction(int id) { return false; }
The default implementation does nothing.
/** * The default implementation does nothing. */
public boolean performPrivateCommand(String action, Bundle data) { return false; }
The default implementation does nothing.
/** * The default implementation does nothing. */
public boolean requestCursorUpdates(int cursorUpdateMode) { return false; } public Handler getHandler() { return null; }
The default implementation places the given text into the editable, replacing any existing composing text. The new text is marked as in a composing state with the composing style.
/** * The default implementation places the given text into the editable, * replacing any existing composing text. The new text is marked as * in a composing state with the composing style. */
public boolean setComposingText(CharSequence text, int newCursorPosition) { if (DEBUG) Log.v(TAG, "setComposingText " + text); replaceText(text, newCursorPosition, true); return true; } public boolean setComposingRegion(int start, int end) { final Editable content = getEditable(); if (content != null) { beginBatchEdit(); removeComposingSpans(content); int a = start; int b = end; if (a > b) { int tmp = a; a = b; b = tmp; } // Clip the end points to be within the content bounds. final int length = content.length(); if (a < 0) a = 0; if (b < 0) b = 0; if (a > length) a = length; if (b > length) b = length; ensureDefaultComposingSpans(); if (mDefaultComposingSpans != null) { for (int i = 0; i < mDefaultComposingSpans.length; ++i) { content.setSpan(mDefaultComposingSpans[i], a, b, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING); } } content.setSpan(COMPOSING, a, b, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING); // Note: sendCurrentText does nothing unless mDummyMode is set sendCurrentText(); endBatchEdit(); } return true; }
The default implementation changes the selection position in the current editable text.
/** * The default implementation changes the selection position in the * current editable text. */
public boolean setSelection(int start, int end) { if (DEBUG) Log.v(TAG, "setSelection " + start + ", " + end); final Editable content = getEditable(); if (content == null) return false; int len = content.length(); if (start > len || end > len || start < 0 || end < 0) { // If the given selection is out of bounds, just ignore it. // Most likely the text was changed out from under the IME, // and the IME is going to have to update all of its state // anyway. return true; } if (start == end && MetaKeyKeyListener.getMetaState(content, MetaKeyKeyListener.META_SELECTING) != 0) { // If we are in selection mode, then we want to extend the // selection instead of replacing it. Selection.extendSelection(content, start); } else { Selection.setSelection(content, start, end); } return true; }
Provides standard implementation for sending a key event to the window attached to the input connection's view.
/** * Provides standard implementation for sending a key event to the window * attached to the input connection's view. */
public boolean sendKeyEvent(KeyEvent event) { mIMM.dispatchKeyEventFromInputMethod(mTargetView, event); return false; }
Updates InputMethodManager with the current fullscreen mode.
/** * Updates InputMethodManager with the current fullscreen mode. */
public boolean reportFullscreenMode(boolean enabled) { return true; } private void sendCurrentText() { if (!mDummyMode) { return; } Editable content = getEditable(); if (content != null) { final int N = content.length(); if (N == 0) { return; } if (N == 1) { // If it's 1 character, we have a chance of being // able to generate normal key events... if (mKeyCharacterMap == null) { mKeyCharacterMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); } char[] chars = new char[1]; content.getChars(0, 1, chars, 0); KeyEvent[] events = mKeyCharacterMap.getEvents(chars); if (events != null) { for (int i=0; i<events.length; i++) { if (DEBUG) Log.v(TAG, "Sending: " + events[i]); sendKeyEvent(events[i]); } content.clear(); return; } } // Otherwise, revert to the special key event containing // the actual characters. KeyEvent event = new KeyEvent(SystemClock.uptimeMillis(), content.toString(), KeyCharacterMap.VIRTUAL_KEYBOARD, 0); sendKeyEvent(event); content.clear(); } } private void ensureDefaultComposingSpans() { if (mDefaultComposingSpans == null) { Context context; if (mTargetView != null) { context = mTargetView.getContext(); } else if (mIMM.mServedView != null) { context = mIMM.mServedView.getContext(); } else { context = null; } if (context != null) { TypedArray ta = context.getTheme() .obtainStyledAttributes(new int[] { com.android.internal.R.attr.candidatesTextStyleSpans }); CharSequence style = ta.getText(0); ta.recycle(); if (style != null && style instanceof Spanned) { mDefaultComposingSpans = ((Spanned)style).getSpans( 0, style.length(), Object.class); } } } } private void replaceText(CharSequence text, int newCursorPosition, boolean composing) { final Editable content = getEditable(); if (content == null) { return; } beginBatchEdit(); // delete composing text set previously. int a = getComposingSpanStart(content); int b = getComposingSpanEnd(content); if (DEBUG) Log.v(TAG, "Composing span: " + a + " to " + b); if (b < a) { int tmp = a; a = b; b = tmp; } if (a != -1 && b != -1) { removeComposingSpans(content); } else { a = Selection.getSelectionStart(content); b = Selection.getSelectionEnd(content); if (a < 0) a = 0; if (b < 0) b = 0; if (b < a) { int tmp = a; a = b; b = tmp; } } if (composing) { Spannable sp = null; if (!(text instanceof Spannable)) { sp = new SpannableStringBuilder(text); text = sp; ensureDefaultComposingSpans(); if (mDefaultComposingSpans != null) { for (int i = 0; i < mDefaultComposingSpans.length; ++i) { sp.setSpan(mDefaultComposingSpans[i], 0, sp.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING); } } } else { sp = (Spannable)text; } setComposingSpans(sp); } if (DEBUG) Log.v(TAG, "Replacing from " + a + " to " + b + " with \"" + text + "\", composing=" + composing + ", type=" + text.getClass().getCanonicalName()); if (DEBUG) { LogPrinter lp = new LogPrinter(Log.VERBOSE, TAG); lp.println("Current text:"); TextUtils.dumpSpans(content, lp, " "); lp.println("Composing text:"); TextUtils.dumpSpans(text, lp, " "); } // Position the cursor appropriately, so that after replacing the // desired range of text it will be located in the correct spot. // This allows us to deal with filters performing edits on the text // we are providing here. if (newCursorPosition > 0) { newCursorPosition += b - 1; } else { newCursorPosition += a; } if (newCursorPosition < 0) newCursorPosition = 0; if (newCursorPosition > content.length()) newCursorPosition = content.length(); Selection.setSelection(content, newCursorPosition); content.replace(a, b, text); if (DEBUG) { LogPrinter lp = new LogPrinter(Log.VERBOSE, TAG); lp.println("Final text:"); TextUtils.dumpSpans(content, lp, " "); } endBatchEdit(); }
The default implementation does nothing.
/** * The default implementation does nothing. */
public boolean commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts) { return false; } }