IMA API InputConnection#replaceText()
Add InputConnecton#replaceText(int, int, CharSequence, int, TextAttribute) API to support replacing the specific range in the editor with suggested text. Add default implementation in InputConnection which delegates to call finishComposingText()+setSelection()+commitText(). Add implementation in BaseInputConnection which will address the issue of selection flash. Bug: 241503197 Test: atest FrameworksCoreTests:BaseInputConnectionTest Test: atest CtsInputMethodTestCases:BaseInputConnectionTest Change-Id: Ia0e42270cfae112fcf19205a7151e4ab4ac4afdd
This commit is contained in:
@@ -53291,6 +53291,7 @@ package android.view.inputmethod {
|
||||
method public default void performHandwritingGesture(@NonNull android.view.inputmethod.HandwritingGesture, @Nullable java.util.concurrent.Executor, @Nullable java.util.function.IntConsumer);
|
||||
method public boolean performPrivateCommand(String, android.os.Bundle);
|
||||
method public default boolean performSpellCheck();
|
||||
method public default boolean replaceText(@IntRange(from=0) int, @IntRange(from=0) int, @NonNull CharSequence, int, @Nullable android.view.inputmethod.TextAttribute);
|
||||
method public boolean reportFullscreenMode(boolean);
|
||||
method public boolean requestCursorUpdates(int);
|
||||
method public default boolean requestCursorUpdates(int, int);
|
||||
|
||||
@@ -775,4 +775,33 @@ final class IRemoteInputConnectionInvoker {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes {@link IRemoteInputConnection#replaceText(InputConnectionCommandHeader, int, int,
|
||||
* CharSequence, TextAttribute)}.
|
||||
*
|
||||
* @param start the character index where the replacement should start.
|
||||
* @param end the character index where the replacement should end.
|
||||
* @param newCursorPosition the new cursor position around the text. If > 0, this is relative to
|
||||
* the end of the text - 1; if <= 0, this is relative to the start of the text. So a value
|
||||
* of 1 will always advance you to the position after the full text being inserted. Note
|
||||
* that this means you can't position the cursor within the text.
|
||||
* @param text the text to replace. This may include styles.
|
||||
* @param textAttribute The extra information about the text. This value may be null.
|
||||
*/
|
||||
@AnyThread
|
||||
public boolean replaceText(
|
||||
int start,
|
||||
int end,
|
||||
@NonNull CharSequence text,
|
||||
int newCursorPosition,
|
||||
@Nullable TextAttribute textAttribute) {
|
||||
try {
|
||||
mConnection.replaceText(
|
||||
createHeader(), start, end, text, newCursorPosition, textAttribute);
|
||||
return true;
|
||||
} catch (RemoteException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,6 +498,17 @@ final class RemoteInputConnection implements InputConnection {
|
||||
return mInvoker.setImeConsumesInput(imeConsumesInput);
|
||||
}
|
||||
|
||||
/** See {@link InputConnection#replaceText(int, int, CharSequence, int, TextAttribute)}. */
|
||||
@AnyThread
|
||||
public boolean replaceText(
|
||||
int start,
|
||||
int end,
|
||||
@NonNull CharSequence text,
|
||||
int newCursorPosition,
|
||||
@Nullable TextAttribute textAttribute) {
|
||||
return mInvoker.replaceText(start, end, text, newCursorPosition, textAttribute);
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
@@ -897,8 +897,43 @@ public class BaseInputConnection implements InputConnection {
|
||||
}
|
||||
}
|
||||
|
||||
private void replaceText(CharSequence text, int newCursorPosition,
|
||||
boolean composing) {
|
||||
@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;
|
||||
@@ -931,6 +966,16 @@ public class BaseInputConnection implements InputConnection {
|
||||
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;
|
||||
@@ -974,7 +1019,6 @@ public class BaseInputConnection implements InputConnection {
|
||||
if (newCursorPosition < 0) newCursorPosition = 0;
|
||||
if (newCursorPosition > content.length()) newCursorPosition = content.length();
|
||||
Selection.setSelection(content, newCursorPosition);
|
||||
|
||||
content.replace(a, b, text);
|
||||
|
||||
if (DEBUG) {
|
||||
@@ -982,8 +1026,6 @@ public class BaseInputConnection implements InputConnection {
|
||||
lp.println("Final text:");
|
||||
TextUtils.dumpSpans(content, lp, " ");
|
||||
}
|
||||
|
||||
endBatchEdit();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1329,4 +1329,44 @@ public interface InputConnection {
|
||||
// existing APIs.
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the specific range in the editor with suggested text.
|
||||
*
|
||||
* <p>This method finishes whatever composing text is currently active and leaves the text
|
||||
* as-it, replaces the specific range of text with the passed CharSequence, and then moves the
|
||||
* cursor according to {@code newCursorPosition}. This behaves like calling {@link
|
||||
* #finishComposingText()}, {@link #setSelection(int, int) setSelection(start, end)}, and then
|
||||
* {@link #commitText(CharSequence, int, TextAttribute) commitText(text, newCursorPosition,
|
||||
* textAttribute)}.
|
||||
*
|
||||
* <p>Similar to {@link #setSelection(int, int)}, the order of start and end is not important.
|
||||
* In effect, the region from start to end and the region from end to start is the same. Editor
|
||||
* authors, be ready to accept a start that is greater than end.
|
||||
*
|
||||
* @param start the character index where the replacement should start.
|
||||
* @param end the character index where the replacement should end.
|
||||
* @param newCursorPosition the new cursor position around the text. If > 0, this is relative to
|
||||
* the end of the text - 1; if <= 0, this is relative to the start of the text. So a value
|
||||
* of 1 will always advance you to the position after the full text being inserted. Note
|
||||
* that this means you can't position the cursor within the text.
|
||||
* @param text the text to replace. This may include styles.
|
||||
* @param textAttribute The extra information about the text. This value may be null.
|
||||
*/
|
||||
default 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);
|
||||
|
||||
beginBatchEdit();
|
||||
finishComposingText();
|
||||
setSelection(start, end);
|
||||
commitText(text, newCursorPosition, textAttribute);
|
||||
endBatchEdit();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,4 +137,7 @@ import com.android.internal.inputmethod.InputConnectionCommandHeader;
|
||||
int afterLength, int flags, in AndroidFuture future /* T=SurroundingText */);
|
||||
|
||||
void setImeConsumesInput(in InputConnectionCommandHeader header, boolean imeConsumesInput);
|
||||
|
||||
void replaceText(in InputConnectionCommandHeader header, int start, int end, CharSequence text,
|
||||
int newCursorPosition,in TextAttribute textAttribute);
|
||||
}
|
||||
|
||||
@@ -1185,6 +1185,30 @@ public final class RemoteInputConnectionImpl extends IRemoteInputConnection.Stub
|
||||
});
|
||||
}
|
||||
|
||||
@Dispatching(cancellable = true)
|
||||
@Override
|
||||
public void replaceText(
|
||||
InputConnectionCommandHeader header,
|
||||
int start,
|
||||
int end,
|
||||
@NonNull CharSequence text,
|
||||
int newCursorPosition,
|
||||
@Nullable TextAttribute textAttribute) {
|
||||
dispatchWithTracing(
|
||||
"replaceText",
|
||||
() -> {
|
||||
if (header.mSessionId != mCurrentSessionId.get()) {
|
||||
return; // cancelled
|
||||
}
|
||||
InputConnection ic = getInputConnection();
|
||||
if (ic == null || !isActive()) {
|
||||
Log.w(TAG, "replaceText on inactive InputConnection");
|
||||
return;
|
||||
}
|
||||
ic.replaceText(start, end, text, newCursorPosition, textAttribute);
|
||||
});
|
||||
}
|
||||
|
||||
private final IRemoteAccessibilityInputConnection mAccessibilityInputConnection =
|
||||
new IRemoteAccessibilityInputConnection.Stub() {
|
||||
@Dispatching(cancellable = true)
|
||||
|
||||
@@ -619,6 +619,74 @@ public class BaseInputConnectionTest {
|
||||
verifyTextSnapshotContentEquals(mBaseInputConnection.takeSnapshot(), expectedTextSnapshot);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReplaceText_toEditorWithoutSelectionAndComposing() {
|
||||
// before replace: "|"
|
||||
// after replace: "text1|"
|
||||
assertThat(mBaseInputConnection.replaceText(0, 0, "text1", 1, null)).isTrue();
|
||||
verifyContent("text1", 5, 5, -1, -1);
|
||||
|
||||
// before replace: "text1|"
|
||||
// after replace: "text2|"
|
||||
assertThat(mBaseInputConnection.replaceText(0, 5, "text2", 1, null)).isTrue();
|
||||
verifyContent("text2", 5, 5, -1, -1);
|
||||
|
||||
// before replace: "text1|"
|
||||
// after replace: "|text3"
|
||||
assertThat(mBaseInputConnection.replaceText(0, 5, "text3", -1, null)).isTrue();
|
||||
verifyContent("text3", 0, 0, -1, -1);
|
||||
|
||||
// before replace: "|text3"
|
||||
// after replace: "ttext4|t3"
|
||||
// BUG(b/21476564): this behavior is inconsistent with API description.
|
||||
assertThat(mBaseInputConnection.replaceText(1, 3, "text4", 1, null)).isTrue();
|
||||
verifyContent("ttext4t3", 6, 6, -1, -1);
|
||||
|
||||
// before replace: "ttext4|t3"
|
||||
// after replace: "|text5t3"
|
||||
assertThat(mBaseInputConnection.replaceText(0, 6, "text5", -1, null)).isTrue();
|
||||
verifyContent("text5t3", 0, 0, -1, -1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReplaceText_toEditorWithSelection() {
|
||||
// before replace: "123|456|789"
|
||||
// before replace: "123text|6789"
|
||||
prepareContent("123456789", 3, 6, -1, -1);
|
||||
assertThat(mBaseInputConnection.replaceText(3, 5, "text", 1, null)).isTrue();
|
||||
verifyContent("123text6789", 7, 7, -1, -1);
|
||||
|
||||
// before replace: "|123|"
|
||||
// before replace: "|text23"
|
||||
prepareContent("123", 0, 3, -1, -1);
|
||||
assertThat(mBaseInputConnection.replaceText(0, 1, "text", 0, null)).isTrue();
|
||||
verifyContent("text23", 0, 0, -1, -1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReplaceText_toEditorWithComposing() {
|
||||
// before replace: "123456|789"
|
||||
// ---
|
||||
// before replace: "123456text|"
|
||||
prepareContent("123456789", 6, 6, 3, 6);
|
||||
assertThat(mBaseInputConnection.replaceText(6, 9, "text", 1, null)).isTrue();
|
||||
verifyContent("123456text", 10, 10, -1, -1);
|
||||
|
||||
// before replace: "123456789|"
|
||||
// ---
|
||||
// before replace: "text|123456789"
|
||||
prepareContent("123456789", 9, 9, 3, 6);
|
||||
assertThat(mBaseInputConnection.replaceText(0, 0, "text", 1, null)).isTrue();
|
||||
verifyContent("text123456789", 4, 4, -1, -1);
|
||||
|
||||
// before replace: "|123456789|"
|
||||
// ---
|
||||
// before replace: "12text|9"
|
||||
prepareContent("123456789", 0, 9, 3, 6);
|
||||
assertThat(mBaseInputConnection.replaceText(2, 8, "text", 1, null)).isTrue();
|
||||
verifyContent("12text9", 6, 6, -1, -1);
|
||||
}
|
||||
|
||||
private void prepareContent(
|
||||
CharSequence text,
|
||||
int selectionStart,
|
||||
|
||||
Reference in New Issue
Block a user