1112 lines
38 KiB
Java
1112 lines
38 KiB
Java
/*
|
|
* 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);
|
|
}
|
|
}
|