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:
Lan Wei
2022-08-29 15:20:58 +08:00
parent 85a962599d
commit d5d530ad63
8 changed files with 223 additions and 5 deletions

View File

@@ -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);

View File

@@ -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;
}
}
}

View File

@@ -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() {

View File

@@ -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();
}
/**

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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)

View File

@@ -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,