blob: 05aff6425a676dcec3f94b461f8bae1446d2f927 [file] [log] [blame]
/*
* 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 static android.view.ContentInfo.SOURCE_INPUT_METHOD;
import android.annotation.CallSuper;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ClipData;
import android.content.ClipDescription;
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.ContentInfo;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.View;
import com.android.internal.util.Preconditions;
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
* {@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 */
@NonNull protected final InputMethodManager mIMM;
/**
* Target view for the input connection.
*
* <p>This could be null for a fallback input connection.
*/
@Nullable final View mTargetView;
final boolean mFallbackMode;
private Object[] mDefaultComposingSpans;
Editable mEditable;
KeyCharacterMap mKeyCharacterMap;
BaseInputConnection(@NonNull InputMethodManager mgr, boolean fullEditor) {
mIMM = mgr;
mTargetView = null;
mFallbackMode = !fullEditor;
}
public BaseInputConnection(@NonNull View targetView, boolean fullEditor) {
mIMM = (InputMethodManager)targetView.getContext().getSystemService(
Context.INPUT_METHOD_SERVICE);
mTargetView = targetView;
mFallbackMode = !fullEditor;
}
/**
* Removes the composing spans from the given text if any.
*
* @param text the spannable text to remove composing spans
*/
public static final void removeComposingSpans(@NonNull 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);
}
}
}
}
/**
* Removes the composing spans from the given text if any.
*
* @param text the spannable text to remove composing spans
*/
public static void setComposingSpans(@NonNull Spannable text) {
setComposingSpans(text, 0, text.length());
}
/** @hide */
public static void setComposingSpans(@NonNull 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);
}
/** Return the beginning of the range of composing text, or -1 if there's no composing text. */
public static int getComposingSpanStart(@NonNull Spannable text) {
return text.getSpanStart(COMPOSING);
}
/** Return the end of the range of composing text, or -1 if there's no composing text. */
public static int getComposingSpanEnd(@NonNull 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.
*
* <p>Subclasses could override this method to turn null.
*/
@Nullable
public Editable getEditable() {
if (mEditable == null) {
mEditable = Editable.Factory.getInstance().newEditable("");
Selection.setSelection(mEditable, 0);
}
return mEditable;
}
/** Default implementation does nothing. */
@Override
public boolean beginBatchEdit() {
return false;
}
/** Default implementation does nothing. */
@Override
public boolean endBatchEdit() {
return false;
}
/**
* Called after only the composing region is modified (so it isn't called if the text also
* changes).
*
* <p>Default implementation does nothing.
*
* @hide
*/
public void endComposingRegionEditInternal() {}
/**
* Default implementation calls {@link #finishComposingText()} and {@code
* setImeConsumesInput(false)}.
*/
@CallSuper
@Override
public void closeConnection() {
finishComposingText();
setImeConsumesInput(false);
}
/**
* Default implementation uses {@link MetaKeyKeyListener#clearMetaKeyState(long, int)
* MetaKeyKeyListener.clearMetaKeyState(long, int)} to clear the state.
*/
@Override
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. */
@Override
public boolean commitCompletion(CompletionInfo text) {
return false;
}
/** Default implementation does nothing and returns false. */
@Override
public boolean commitCorrection(CorrectionInfo correctionInfo) {
return false;
}
/**
* Default implementation replaces any existing composing text with the given text. In addition,
* only if fallback mode, a key event is sent for the new text and the current editable buffer
* cleared.
*/
@Override
public boolean commitText(CharSequence text, int newCursorPosition) {
if (DEBUG) Log.v(TAG, "commitText(" + text + ", " + newCursorPosition + ")");
replaceText(text, newCursorPosition, false);
sendCurrentText();
return true;
}
/**
* 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.
* @return {@code true} when selected text is deleted, {@code false} when either the selection
* is invalid or not yet attached (i.e. selection start or end is -1), or the editable text
* is {@code null}.
*/
@Override
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;
}
// Skip when the selection is not yet attached.
if (a == -1 || b == -1) {
endBatchEdit();
return false;
}
// 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;
final int numDeleteBefore = a - start;
if (a >= 0 && numDeleteBefore > 0) {
content.delete(start, a);
deleted = numDeleteBefore;
}
}
if (afterLength > 0) {
b = b - deleted;
int end = b + afterLength;
if (end > content.length()) end = content.length();
final int numDeleteAfter = end - b;
if (b >= 0 && numDeleteAfter > 0) {
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.
*
* @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.
*/
@Override
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 fallback mode, a key event is sent for the new text and the current
* editable buffer cleared.
*/
@Override
public boolean finishComposingText() {
if (DEBUG) Log.v(TAG, "finishComposingText");
final Editable content = getEditable();
if (content != null) {
beginBatchEdit();
removeComposingSpans(content);
// Note: sendCurrentText does nothing unless mFallbackMode is set
sendCurrentText();
endBatchEdit();
endComposingRegionEditInternal();
}
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 fallback mode in which case 0 is
* always returned.
*/
@Override
public int getCursorCapsMode(int reqModes) {
if (mFallbackMode) 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. */
@Override
@Nullable
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.
*/
@Override
@Nullable
public CharSequence getTextBeforeCursor(@IntRange(from = 0) int length, int flags) {
Preconditions.checkArgumentNonnegative(length);
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.
*/
@Override
@Nullable
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.
*/
@Override
@Nullable
public CharSequence getTextAfterCursor(@IntRange(from = 0) int length, int flags) {
Preconditions.checkArgumentNonnegative(length);
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;
}
int end = (int) Math.min((long) b + length, content.length());
if ((flags&GET_TEXT_WITH_STYLES) != 0) {
return content.subSequence(b, end);
}
return TextUtils.substring(content, b, end);
}
/**
* The default implementation returns the given amount of text around the current cursor
* position in the buffer.
*/
@Override
@Nullable
public SurroundingText getSurroundingText(
@IntRange(from = 0) int beforeLength, @IntRange(from = 0) int afterLength, int flags) {
Preconditions.checkArgumentNonnegative(beforeLength);
Preconditions.checkArgumentNonnegative(afterLength);
final Editable content = getEditable();
// If {@link #getEditable()} is null or {@code mEditable} is equal to {@link #getEditable()}
// (a.k.a, a fake editable), it means we cannot get valid content from the editable, so
// fallback to retrieve surrounding text from other APIs.
if (content == null || mEditable == content) {
return InputConnection.super.getSurroundingText(beforeLength, afterLength, flags);
}
int selStart = Selection.getSelectionStart(content);
int selEnd = Selection.getSelectionEnd(content);
// Guard against the case where the cursor has not been positioned yet.
if (selStart < 0 || selEnd < 0) {
return null;
}
if (selStart > selEnd) {
int tmp = selStart;
selStart = selEnd;
selEnd = tmp;
}
// Guards the start and end pos within range [0, contentLength].
int startPos = Math.max(0, selStart - beforeLength);
int endPos = (int) Math.min((long) selEnd + afterLength, content.length());
CharSequence surroundingText;
if ((flags & GET_TEXT_WITH_STYLES) != 0) {
surroundingText = content.subSequence(startPos, endPos);
} else {
surroundingText = TextUtils.substring(content, startPos, endPos);
}
return new SurroundingText(
surroundingText, selStart - startPos, selEnd - startPos, startPos);
}
/** The default implementation turns this into the enter key. */
@Override
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. */
@Override
public boolean performContextMenuAction(int id) {
return false;
}
/** The default implementation does nothing. */
@Override
public boolean performPrivateCommand(String action, Bundle data) {
return false;
}
/** The default implementation does nothing. */
@Override
public boolean requestCursorUpdates(int cursorUpdateMode) {
return false;
}
@Override
@Nullable
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.
*/
@Override
public boolean setComposingText(CharSequence text, int newCursorPosition) {
if (DEBUG) Log.v(TAG, "setComposingText(" + text + ", " + newCursorPosition + ")");
replaceText(text, newCursorPosition, true);
return true;
}
@Override
public boolean setComposingRegion(int start, int end) {
if (DEBUG) Log.v(TAG, "setComposingRegion(" + start + ", " + 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 mFallbackMode is set
sendCurrentText();
endBatchEdit();
endComposingRegionEditInternal();
}
return true;
}
/** The default implementation changes the selection position in the current editable text. */
@Override
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.
*/
@Override
public boolean sendKeyEvent(KeyEvent event) {
mIMM.dispatchKeyEventFromInputMethod(mTargetView, event);
return false;
}
/** Updates InputMethodManager with the current fullscreen mode. */
@Override
public boolean reportFullscreenMode(boolean enabled) {
return true;
}
private void sendCurrentText() {
if (!mFallbackMode) {
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 {
context = mIMM.getFallbackContextFromServedView();
}
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);
}
}
}
}
@Override
public boolean replaceText(
@IntRange(from = 0) int start,
@IntRange(from = 0) int end,
@NonNull CharSequence text,
int newCursorPosition,
@Nullable TextAttribute textAttribute) {
Preconditions.checkArgumentNonnegative(start);
Preconditions.checkArgumentNonnegative(end);
if (DEBUG) {
Log.v(
TAG,
"replaceText " + start + ", " + end + ", " + text + ", " + newCursorPosition);
}
final Editable content = getEditable();
if (content == null) {
return false;
}
beginBatchEdit();
removeComposingSpans(content);
int len = content.length();
start = Math.min(start, len);
end = Math.min(end, len);
if (end < start) {
int tmp = start;
start = end;
end = tmp;
}
replaceTextInternal(start, end, text, newCursorPosition, /*composing=*/ false);
endBatchEdit();
return true;
}
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;
}
}
replaceTextInternal(a, b, text, newCursorPosition, composing);
endBatchEdit();
}
private void replaceTextInternal(
int a, int b, CharSequence text, int newCursorPosition, boolean composing) {
final Editable content = getEditable();
if (content == null) {
return;
}
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
+ ", newCursorPosition="
+ newCursorPosition
+ ", type="
+ text.getClass().getCanonicalName());
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.
int requestedNewCursorPosition = newCursorPosition;
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);
// Replace (or insert) to the cursor (a==b==newCursorPosition) will position the cursor to
// the end of the new replaced/inserted text, we need to re-position the cursor to the start
// according the API definition: "if <= 0, this is relative to the start of the text".
if (requestedNewCursorPosition == 0 && a == b) {
Selection.setSelection(content, newCursorPosition);
}
if (DEBUG) {
LogPrinter lp = new LogPrinter(Log.VERBOSE, TAG);
lp.println("Final text:");
TextUtils.dumpSpans(content, lp, " ");
}
}
/**
* Default implementation which invokes {@link View#performReceiveContent} on the target view if
* the view {@link View#getReceiveContentMimeTypes allows} content insertion; otherwise returns
* false without any side effects.
*/
@Override
public boolean commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts) {
if (mTargetView == null) {
return false;
}
ClipDescription description = inputContentInfo.getDescription();
if (mTargetView.getReceiveContentMimeTypes() == null) {
if (DEBUG) {
Log.d(TAG, "Can't insert content from IME: content=" + description);
}
return false;
}
if ((flags & InputConnection.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
try {
inputContentInfo.requestPermission();
} catch (Exception e) {
Log.w(TAG, "Can't insert content from IME; requestPermission() failed", e);
return false;
}
}
final ClipData clip = new ClipData(inputContentInfo.getDescription(),
new ClipData.Item(inputContentInfo.getContentUri()));
final ContentInfo payload = new ContentInfo.Builder(clip, SOURCE_INPUT_METHOD)
.setLinkUri(inputContentInfo.getLinkUri())
.setExtras(opts)
.setInputContentInfo(inputContentInfo)
.build();
return mTargetView.performReceiveContent(payload) == null;
}
/**
* Default implementation that constructs {@link TextSnapshot} with information extracted from
* {@link BaseInputConnection}.
*
* @return {@code null} when {@link TextSnapshot} cannot be fully taken.
*/
@Nullable
@Override
public TextSnapshot takeSnapshot() {
final Editable content = getEditable();
if (content == null) {
return null;
}
int composingStart = getComposingSpanStart(content);
int composingEnd = getComposingSpanEnd(content);
if (composingEnd < composingStart) {
final int tmp = composingStart;
composingStart = composingEnd;
composingEnd = tmp;
}
final SurroundingText surroundingText = getSurroundingText(
EditorInfo.MEMORY_EFFICIENT_TEXT_LENGTH / 2,
EditorInfo.MEMORY_EFFICIENT_TEXT_LENGTH / 2, GET_TEXT_WITH_STYLES);
if (surroundingText == null) {
return null;
}
final int cursorCapsMode = getCursorCapsMode(TextUtils.CAP_MODE_CHARACTERS
| TextUtils.CAP_MODE_WORDS | TextUtils.CAP_MODE_SENTENCES);
return new TextSnapshot(surroundingText, composingStart, composingEnd, cursorCapsMode);
}
}