From 02b50d49442e074fec2c5db3a3951ef2218b769a Mon Sep 17 00:00:00 2001 From: Lan Wei Date: Thu, 24 Sep 2020 12:05:21 +0800 Subject: [PATCH] IME API: InputConnection#getSurroudingText(int, int, int) Introduce a new class SurroundingText and a new API in InputConnection to support retrieving surrounding text as an atomic request. SurroundingText is the class for wrapping the text and sggestion info. InputConnection#getSurroudingText() will return an SurroudingText object if the protocol is supported. Test: atest FrameworksCoreTests:SurroundingTextTest Test: atest CtsInputMethodTestCases:BaseInputConnectionTest BUG: 167947745 Change-Id: I2eb9ef5ba61a0e033007089da80f81548108621e --- api/current.txt | 12 ++ .../view/inputmethod/BaseInputConnection.java | 44 +++++ .../view/inputmethod/InputConnection.java | 81 +++++++++- .../inputmethod/InputConnectionInspector.java | 19 +++ .../inputmethod/InputConnectionWrapper.java | 11 ++ .../view/inputmethod/SurroundingText.aidl | 19 +++ .../view/inputmethod/SurroundingText.java | 150 ++++++++++++++++++ core/java/android/widget/AbsListView.java | 7 + .../inputmethod/CancellationGroup.java | 20 +++ .../ISurroundingTextResultCallback.aidl | 23 +++ .../internal/inputmethod/ResultCallbacks.java | 28 ++++ .../view/IInputConnectionWrapper.java | 46 ++++++ .../android/internal/view/IInputContext.aidl | 4 + .../internal/view/InputConnectionWrapper.java | 33 ++++ .../view/inputmethod/SurroundingTextTest.java | 73 +++++++++ non-updatable-api/current.txt | 12 ++ 16 files changed, 575 insertions(+), 7 deletions(-) create mode 100644 core/java/android/view/inputmethod/SurroundingText.aidl create mode 100644 core/java/android/view/inputmethod/SurroundingText.java create mode 100644 core/java/com/android/internal/inputmethod/ISurroundingTextResultCallback.aidl create mode 100644 core/tests/coretests/src/android/view/inputmethod/SurroundingTextTest.java diff --git a/api/current.txt b/api/current.txt index 0c6670d206849..309062846a638 100644 --- a/api/current.txt +++ b/api/current.txt @@ -57319,6 +57319,7 @@ package android.view.inputmethod { method public android.view.inputmethod.ExtractedText getExtractedText(android.view.inputmethod.ExtractedTextRequest, int); method public android.os.Handler getHandler(); method public CharSequence getSelectedText(int); + method @Nullable public default android.view.inputmethod.SurroundingText getSurroundingText(@IntRange(from=0) int, @IntRange(from=0) int, int); method public CharSequence getTextAfterCursor(int, int); method public CharSequence getTextBeforeCursor(int, int); method public boolean performContextMenuAction(int); @@ -57527,6 +57528,17 @@ package android.view.inputmethod { method public android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder setSubtypeNameResId(int); } + public final class SurroundingText implements android.os.Parcelable { + ctor public SurroundingText(@NonNull CharSequence, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0xffffffff) int); + method public int describeContents(); + method @IntRange(from=0xffffffff) public int getOffset(); + method @IntRange(from=0) public int getSelectionEnd(); + method @IntRange(from=0) public int getSelectionStart(); + method @NonNull public CharSequence getText(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + } + } package android.view.inspector { diff --git a/core/java/android/view/inputmethod/BaseInputConnection.java b/core/java/android/view/inputmethod/BaseInputConnection.java index 73636f81369fc..e0711132f459c 100644 --- a/core/java/android/view/inputmethod/BaseInputConnection.java +++ b/core/java/android/view/inputmethod/BaseInputConnection.java @@ -19,6 +19,8 @@ package android.view.inputmethod; import static android.view.OnReceiveContentCallback.Payload.SOURCE_INPUT_METHOD; import android.annotation.CallSuper; +import android.annotation.IntRange; +import android.annotation.Nullable; import android.content.ClipData; import android.content.Context; import android.content.res.TypedArray; @@ -584,6 +586,48 @@ public class BaseInputConnection implements InputConnection { return TextUtils.substring(content, b, b + length); } + /** + * The default implementation returns the given amount of text around the current cursor + * position in the buffer. + */ + @Nullable + public SurroundingText getSurroundingText( + @IntRange(from = 0) int beforeLength, @IntRange(from = 0) int afterLength, int flags) { + final Editable content = getEditable(); + if (content == null) return null; + + 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; + } + + int contentLength = content.length(); + int startPos = selStart - beforeLength; + int endPos = selEnd + afterLength; + + // Guards the start and end pos within range [0, contentLength]. + startPos = Math.max(0, startPos); + endPos = Math.min(contentLength, endPos); + + 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. */ diff --git a/core/java/android/view/inputmethod/InputConnection.java b/core/java/android/view/inputmethod/InputConnection.java index 4337ed5109db9..c7acd298cd208 100644 --- a/core/java/android/view/inputmethod/InputConnection.java +++ b/core/java/android/view/inputmethod/InputConnection.java @@ -16,14 +16,20 @@ package android.view.inputmethod; +import android.annotation.IntDef; +import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.inputmethodservice.InputMethodService; import android.os.Bundle; import android.os.Handler; +import android.text.TextUtils; import android.view.KeyCharacterMap; import android.view.KeyEvent; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * The InputConnection interface is the communication channel from an * {@link InputMethod} back to the application that is receiving its @@ -122,14 +128,20 @@ import android.view.KeyEvent; * of each other, and the IME may use them however they see fit.

*/ public interface InputConnection { + /** @hide */ + @IntDef(flag = true, prefix = { "GET_TEXT_" }, value = { + GET_TEXT_WITH_STYLES, + }) + @Retention(RetentionPolicy.SOURCE) + @interface GetTextType {} + /** - * Flag for use with {@link #getTextAfterCursor} and - * {@link #getTextBeforeCursor} to have style information returned - * along with the text. If not set, {@link #getTextAfterCursor} - * sends only the raw text, without style or other spans. If set, - * it may return a complex CharSequence of both text and style - * spans. Editor authors: you should strive to - * send text with styles if possible, but it is not required. + * Flag for use with {@link #getTextAfterCursor}, {@link #getTextBeforeCursor} and + * {@link #getSurroundingText} to have style information returned along with the text. If not + * set, {@link #getTextAfterCursor} sends only the raw text, without style or other spans. If + * set, it may return a complex CharSequence of both text and style spans. + * Editor authors: you should strive to send text with styles if possible, but + * it is not required. */ int GET_TEXT_WITH_STYLES = 0x0001; @@ -263,6 +275,61 @@ public interface InputConnection { */ CharSequence getSelectedText(int flags); + /** + * Gets the surrounding text around the current cursor, with beforeLength characters + * of text before the cursor (start of the selection), afterLength characters of text + * after the cursor (end of the selection), and all of the selected text. + * + *

This method may fail either if the input connection has become invalid (such as its + * process crashing), or the client is taking too long to respond with the text (it is given a + * couple seconds to return), or the protocol is not supported. In any of these cases, null is + * returned. + * + *

This method does not affect the text in the editor in any way, nor does it affect the + * selection or composing spans.

+ * + *

If {@link #GET_TEXT_WITH_STYLES} is supplied as flags, the editor should return a + * {@link android.text.Spanned} with all the spans set on the text.

+ * + *

IME authors: please consider this will trigger an IPC round-trip that + * will take some time. Assume this method consumes a lot of time. If you are using this to get + * the initial surrounding text around the cursor, you may consider using + * {@link EditorInfo#getInitialTextBeforeCursor(int, int)}, + * {@link EditorInfo#getInitialSelectedText(int)}, and + * {@link EditorInfo#getInitialTextAfterCursor(int, int)} to prevent IPC costs.

+ * + * @param beforeLength The expected length of the text before the cursor. + * @param afterLength The expected length of the text after the cursor. + * @param flags Supplies additional options controlling how the text is returned. Defined by the + * constants. + * @return an {@link android.view.inputmethod.SurroundingText} object describing the surrounding + * text and state of selection, or null if the input connection is no longer valid, or the + * editor can't comply with the request for some reason, or the application does not implement + * this method. The length of the returned text might be less than the sum of + * beforeLength and afterLength . + */ + @Nullable + default SurroundingText getSurroundingText( + @IntRange(from = 0) int beforeLength, @IntRange(from = 0) int afterLength, + @GetTextType int flags) { + CharSequence textBeforeCursor = getTextBeforeCursor(beforeLength, flags); + if (textBeforeCursor == null) { + textBeforeCursor = ""; + } + CharSequence selectedText = getSelectedText(flags); + if (selectedText == null) { + selectedText = ""; + } + CharSequence textAfterCursor = getTextAfterCursor(afterLength, flags); + if (textAfterCursor == null) { + textAfterCursor = ""; + } + CharSequence surroundingText = + TextUtils.concat(textBeforeCursor, selectedText, textAfterCursor); + return new SurroundingText(surroundingText, textBeforeCursor.length(), + textBeforeCursor.length() + selectedText.length(), -1); + } + /** * Retrieve the current capitalization mode in effect at the * current cursor position in the text. See diff --git a/core/java/android/view/inputmethod/InputConnectionInspector.java b/core/java/android/view/inputmethod/InputConnectionInspector.java index 5f25bf58ce57f..7621da7cef1bc 100644 --- a/core/java/android/view/inputmethod/InputConnectionInspector.java +++ b/core/java/android/view/inputmethod/InputConnectionInspector.java @@ -44,6 +44,7 @@ public final class InputConnectionInspector { MissingMethodFlags.GET_HANDLER, MissingMethodFlags.CLOSE_CONNECTION, MissingMethodFlags.COMMIT_CONTENT, + MissingMethodFlags.GET_SURROUNDING_TEXT }) public @interface MissingMethodFlags { /** @@ -86,6 +87,11 @@ public final class InputConnectionInspector { * {@link android.os.Build.VERSION_CODES#N} MR-1 and later. */ int COMMIT_CONTENT = 1 << 7; + /** + * {@link InputConnection#getSurroundingText(int, int, int)} is available in + * {@link android.os.Build.VERSION_CODES#S} and later. + */ + int GET_SURROUNDING_TEXT = 1 << 8; } private static final Map sMissingMethodsMap = Collections.synchronizedMap( @@ -138,6 +144,9 @@ public final class InputConnectionInspector { if (!hasCommitContent(clazz)) { flags |= MissingMethodFlags.COMMIT_CONTENT; } + if (!hasGetSurroundingText(clazz)) { + flags |= MissingMethodFlags.GET_SURROUNDING_TEXT; + } sMissingMethodsMap.put(clazz, flags); return flags; } @@ -216,6 +225,16 @@ public final class InputConnectionInspector { } } + private static boolean hasGetSurroundingText(@NonNull final Class clazz) { + try { + final Method method = clazz.getMethod("getSurroundingText", int.class, int.class, + int.class); + return !Modifier.isAbstract(method.getModifiers()); + } catch (NoSuchMethodException e) { + return false; + } + } + public static String getMissingMethodFlagsAsString(@MissingMethodFlags final int flags) { final StringBuilder sb = new StringBuilder(); boolean isEmpty = true; diff --git a/core/java/android/view/inputmethod/InputConnectionWrapper.java b/core/java/android/view/inputmethod/InputConnectionWrapper.java index f671e22b49225..ec7fa60a62ba8 100644 --- a/core/java/android/view/inputmethod/InputConnectionWrapper.java +++ b/core/java/android/view/inputmethod/InputConnectionWrapper.java @@ -16,6 +16,7 @@ package android.view.inputmethod; +import android.annotation.Nullable; import android.os.Bundle; import android.os.Handler; import android.view.KeyEvent; @@ -97,6 +98,16 @@ public class InputConnectionWrapper implements InputConnection { return mTarget.getSelectedText(flags); } + /** + * {@inheritDoc} + * @throws NullPointerException if the target is {@code null}. + */ + @Nullable + @Override + public SurroundingText getSurroundingText(int beforeLength, int afterLength, int flags) { + return mTarget.getSurroundingText(beforeLength, afterLength, flags); + } + /** * {@inheritDoc} * @throws NullPointerException if the target is {@code null}. diff --git a/core/java/android/view/inputmethod/SurroundingText.aidl b/core/java/android/view/inputmethod/SurroundingText.aidl new file mode 100644 index 0000000000000..7a9898ec6a434 --- /dev/null +++ b/core/java/android/view/inputmethod/SurroundingText.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2020 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; + +parcelable SurroundingText; \ No newline at end of file diff --git a/core/java/android/view/inputmethod/SurroundingText.java b/core/java/android/view/inputmethod/SurroundingText.java new file mode 100644 index 0000000000000..506f95a4ac7bc --- /dev/null +++ b/core/java/android/view/inputmethod/SurroundingText.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2020 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.IntRange; +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +/** + * Information about the surrounding text around the cursor for use by an input method. + * + *

This contains information about the text and the selection relative to the text.

+ */ +public final class SurroundingText implements Parcelable { + /** + * The surrounding text around the cursor. + */ + @NonNull + private final CharSequence mText; + + /** + * The text offset of the start of the selection in the surrounding text. + * + *

This needs to be the position relative to the {@link #mText} instead of the real position + * in the editor.

+ */ + @IntRange(from = 0) + private final int mSelectionStart; + + /** + * The text offset of the end of the selection in the surrounding text. + * + *

This needs to be the position relative to the {@link #mText} instead of the real position + * in the editor.

+ */ + @IntRange(from = 0) + private final int mSelectionEnd; + + /** + * The text offset between the start of the editor's text and the start of the surrounding text. + * + *

-1 indicates the offset information is unknown.

+ */ + @IntRange(from = -1) + private final int mOffset; + + /** + * Constructor. + * + * @param text The surrounding text. + * @param selectionStart The text offset of the start of the selection in the surrounding text. + * Reversed selection is allowed. + * @param selectionEnd The text offset of the end of the selection in the surrounding text. + * Reversed selection is allowed. + * @param offset The text offset between the start of the editor's text and the start of the + * surrounding text. -1 indicates the offset is unknown. + */ + public SurroundingText(@NonNull final CharSequence text, + @IntRange(from = 0) int selectionStart, @IntRange(from = 0) int selectionEnd, + @IntRange(from = -1) int offset) { + mText = text; + mSelectionStart = selectionStart; + mSelectionEnd = selectionEnd; + mOffset = offset; + } + + /** + * Returns the surrounding text around the cursor. + */ + @NonNull + public CharSequence getText() { + return mText; + } + + /** + * Returns the text offset of the start of the selection in the surrounding text. + */ + @IntRange(from = 0) + public int getSelectionStart() { + return mSelectionStart; + } + + /** + * Returns the text offset of the end of the selection in the surrounding text. + */ + @IntRange(from = 0) + public int getSelectionEnd() { + return mSelectionEnd; + } + + /** + * Returns text offset between the start of the editor's text and the start of the surrounding + * text. + * + *

-1 indicates the offset information is unknown.

+ */ + @IntRange(from = -1) + public int getOffset() { + return mOffset; + } + + @Override + public void writeToParcel(@NonNull Parcel out, int flags) { + TextUtils.writeToParcel(mText, out, flags); + out.writeInt(mSelectionStart); + out.writeInt(mSelectionEnd); + out.writeInt(mOffset); + } + + @Override + public int describeContents() { + return 0; + } + + @NonNull + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @NonNull + public SurroundingText createFromParcel(Parcel in) { + final CharSequence text = + TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + final int selectionHead = in.readInt(); + final int selectionEnd = in.readInt(); + final int offset = in.readInt(); + return new SurroundingText( + text == null ? "" : text, selectionHead, selectionEnd, offset); + } + + @NonNull + public SurroundingText[] newArray(int size) { + return new SurroundingText[size]; + } + }; +} diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index 97e0689ce64fc..45b21c569cead 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -82,6 +82,7 @@ import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputContentInfo; import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.SurroundingText; import android.view.inspector.InspectableProperty; import android.view.inspector.InspectableProperty.EnumEntry; import android.widget.RemoteViews.OnClickHandler; @@ -5996,6 +5997,12 @@ public abstract class AbsListView extends AdapterView implements Te return mTarget.getSelectedText(flags); } + @Override + public SurroundingText getSurroundingText(int beforeLength, int afterLength, int flags) { + if (mTarget == null) return null; + return mTarget.getSurroundingText(beforeLength, afterLength, flags); + } + @Override public int getCursorCapsMode(int reqModes) { if (mTarget == null) return InputType.TYPE_TEXT_FLAG_CAP_SENTENCES; diff --git a/core/java/com/android/internal/inputmethod/CancellationGroup.java b/core/java/com/android/internal/inputmethod/CancellationGroup.java index 09c9d128553bc..a4a220880d46e 100644 --- a/core/java/com/android/internal/inputmethod/CancellationGroup.java +++ b/core/java/com/android/internal/inputmethod/CancellationGroup.java @@ -263,6 +263,16 @@ public final class CancellationGroup { super(factory); } } + + /** + * Completable object of {@link android.view.inputmethod.SurroundingText}. + */ + public static final class SurroundingText + extends Values { + private SurroundingText(@NonNull CancellationGroup factory) { + super(factory); + } + } } /** @@ -292,6 +302,16 @@ public final class CancellationGroup { return new Completable.ExtractedText(this); } + /** + * @return an instance of {@link Completable.SurroundingText} that is associated with this + * {@link CancellationGroup}. + */ + @AnyThread + public Completable.SurroundingText createCompletableSurroundingText() { + return new Completable.SurroundingText(this); + } + + @AnyThread private boolean registerLatch(@NonNull CountDownLatch latch) { synchronized (mLock) { diff --git a/core/java/com/android/internal/inputmethod/ISurroundingTextResultCallback.aidl b/core/java/com/android/internal/inputmethod/ISurroundingTextResultCallback.aidl new file mode 100644 index 0000000000000..6c4f3d58ed028 --- /dev/null +++ b/core/java/com/android/internal/inputmethod/ISurroundingTextResultCallback.aidl @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2020 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 com.android.internal.inputmethod; + +import android.view.inputmethod.SurroundingText; + +oneway interface ISurroundingTextResultCallback { + void onResult(in SurroundingText result); +} \ No newline at end of file diff --git a/core/java/com/android/internal/inputmethod/ResultCallbacks.java b/core/java/com/android/internal/inputmethod/ResultCallbacks.java index 44a8a83b519fd..5eba898fde8aa 100644 --- a/core/java/com/android/internal/inputmethod/ResultCallbacks.java +++ b/core/java/com/android/internal/inputmethod/ResultCallbacks.java @@ -129,4 +129,32 @@ public final class ResultCallbacks { } }; } + + /** + * Creates {@link ISurroundingTextResultCallback.Stub} that is to set + * {@link CancellationGroup.Completable.SurroundingText} when receiving the result. + * + * @param value {@link CancellationGroup.Completable.SurroundingText} to be set when receiving + * the result. + * @return {@link ISurroundingTextResultCallback.Stub} that can be passed as a binder IPC + * parameter. + */ + @AnyThread + public static ISurroundingTextResultCallback.Stub of( + @NonNull CancellationGroup.Completable.SurroundingText value) { + final AtomicReference> + atomicRef = new AtomicReference<>(new WeakReference<>(value)); + + return new ISurroundingTextResultCallback.Stub() { + @BinderThread + @Override + public void onResult(android.view.inputmethod.SurroundingText result) { + final CancellationGroup.Completable.SurroundingText value = unwrap(atomicRef); + if (value == null) { + return; + } + value.onComplete(result); + } + }; + } } diff --git a/core/java/com/android/internal/view/IInputConnectionWrapper.java b/core/java/com/android/internal/view/IInputConnectionWrapper.java index 9257c6d191481..cd5502c9f270e 100644 --- a/core/java/com/android/internal/view/IInputConnectionWrapper.java +++ b/core/java/com/android/internal/view/IInputConnectionWrapper.java @@ -35,11 +35,13 @@ import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputConnectionInspector; import android.view.inputmethod.InputConnectionInspector.MissingMethodFlags; import android.view.inputmethod.InputContentInfo; +import android.view.inputmethod.SurroundingText; import com.android.internal.annotations.GuardedBy; import com.android.internal.inputmethod.ICharSequenceResultCallback; import com.android.internal.inputmethod.IExtractedTextResultCallback; import com.android.internal.inputmethod.IIntResultCallback; +import com.android.internal.inputmethod.ISurroundingTextResultCallback; import com.android.internal.os.SomeArgs; public abstract class IInputConnectionWrapper extends IInputContext.Stub { @@ -70,6 +72,8 @@ public abstract class IInputConnectionWrapper extends IInputContext.Stub { private static final int DO_REQUEST_UPDATE_CURSOR_ANCHOR_INFO = 140; private static final int DO_CLOSE_CONNECTION = 150; private static final int DO_COMMIT_CONTENT = 160; + private static final int DO_GET_SURROUNDING_TEXT = 41; + @GuardedBy("mLock") @Nullable @@ -127,6 +131,21 @@ public abstract class IInputConnectionWrapper extends IInputContext.Stub { dispatchMessage(mH.obtainMessage(DO_GET_SELECTED_TEXT, flags, 0 /* unused */, callback)); } + /** + * Dispatches the request for retrieving surrounding text. + * + *

See {@link InputConnection#getSurroundingText(int, int, int)}. + */ + public void getSurroundingText(int beforeLength, int afterLength, int flags, + ISurroundingTextResultCallback callback) { + final SomeArgs args = SomeArgs.obtain(); + args.arg1 = beforeLength; + args.arg2 = afterLength; + args.arg3 = flags; + args.arg4 = callback; + dispatchMessage(mH.obtainMessage(DO_GET_SURROUNDING_TEXT, flags, 0 /* unused */, args)); + } + public void getCursorCapsMode(int reqModes, IIntResultCallback callback) { dispatchMessage( mH.obtainMessage(DO_GET_CURSOR_CAPS_MODE, reqModes, 0 /* unused */, callback)); @@ -293,6 +312,33 @@ public abstract class IInputConnectionWrapper extends IInputContext.Stub { } return; } + case DO_GET_SURROUNDING_TEXT: { + final SomeArgs args = (SomeArgs) msg.obj; + try { + int beforeLength = (int) args.arg1; + int afterLength = (int) args.arg2; + int flags = (int) args.arg3; + final ISurroundingTextResultCallback callback = + (ISurroundingTextResultCallback) args.arg4; + final InputConnection ic = getInputConnection(); + final SurroundingText result; + if (ic == null || !isActive()) { + Log.w(TAG, "getSurroundingText on inactive InputConnection"); + result = null; + } else { + result = ic.getSurroundingText(beforeLength, afterLength, flags); + } + try { + callback.onResult(result); + } catch (RemoteException e) { + Log.w(TAG, "Failed to return the result to getSurroundingText()." + + " result=" + result, e); + } + } finally { + args.recycle(); + } + return; + } case DO_GET_CURSOR_CAPS_MODE: { final IIntResultCallback callback = (IIntResultCallback) msg.obj; final InputConnection ic = getInputConnection(); diff --git a/core/java/com/android/internal/view/IInputContext.aidl b/core/java/com/android/internal/view/IInputContext.aidl index 86f1293c014fb..074908acdf2aa 100644 --- a/core/java/com/android/internal/view/IInputContext.aidl +++ b/core/java/com/android/internal/view/IInputContext.aidl @@ -26,6 +26,7 @@ import android.view.inputmethod.InputContentInfo; import com.android.internal.inputmethod.ICharSequenceResultCallback; import com.android.internal.inputmethod.IExtractedTextResultCallback; import com.android.internal.inputmethod.IIntResultCallback; +import com.android.internal.inputmethod.ISurroundingTextResultCallback; /** * Interface from an input method to the application, allowing it to perform @@ -79,4 +80,7 @@ import com.android.internal.inputmethod.IIntResultCallback; void commitContent(in InputContentInfo inputContentInfo, int flags, in Bundle opts, IIntResultCallback callback); + + void getSurroundingText(int beforeLength, int afterLength, int flags, + ISurroundingTextResultCallback callback); } diff --git a/core/java/com/android/internal/view/InputConnectionWrapper.java b/core/java/com/android/internal/view/InputConnectionWrapper.java index 0bf52345bc7e6..f086dd79758b7 100644 --- a/core/java/com/android/internal/view/InputConnectionWrapper.java +++ b/core/java/com/android/internal/view/InputConnectionWrapper.java @@ -33,6 +33,7 @@ import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputConnectionInspector; import android.view.inputmethod.InputConnectionInspector.MissingMethodFlags; import android.view.inputmethod.InputContentInfo; +import android.view.inputmethod.SurroundingText; import com.android.internal.inputmethod.CancellationGroup; import com.android.internal.inputmethod.ResultCallbacks; @@ -157,6 +158,38 @@ public class InputConnectionWrapper implements InputConnection { return getResultOrNull(value, "getSelectedText()"); } + /** + * Get {@link SurroundingText} around the current cursor, with beforeLength + * characters of text before the cursor, afterLength characters of text after the + * cursor, and all of the selected text. + * @param beforeLength The expected length of the text before the cursor + * @param afterLength The expected length of the text after the cursor + * @param flags Supplies additional options controlling how the text is returned. May be either + * 0 or {@link #GET_TEXT_WITH_STYLES}. + * @return the surrounding text around the cursor position; the length of the returned text + * might be less than requested. It could also be {@code null} when the editor or system could + * not support this protocol. + */ + @AnyThread + public SurroundingText getSurroundingText(int beforeLength, int afterLength, int flags) { + if (mCancellationGroup.isCanceled()) { + return null; + } + if (isMethodMissing(MissingMethodFlags.GET_SURROUNDING_TEXT)) { + // This method is not implemented. + return null; + } + final CancellationGroup.Completable.SurroundingText value = + mCancellationGroup.createCompletableSurroundingText(); + try { + mIInputContext.getSurroundingText(beforeLength, afterLength, flags, + ResultCallbacks.of(value)); + } catch (RemoteException e) { + return null; + } + return getResultOrNull(value, "getSurroundingText()"); + } + @AnyThread public int getCursorCapsMode(int reqModes) { if (mCancellationGroup.isCanceled()) { diff --git a/core/tests/coretests/src/android/view/inputmethod/SurroundingTextTest.java b/core/tests/coretests/src/android/view/inputmethod/SurroundingTextTest.java new file mode 100644 index 0000000000000..dfbc39c764893 --- /dev/null +++ b/core/tests/coretests/src/android/view/inputmethod/SurroundingTextTest.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2020 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 org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import android.os.Parcel; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class SurroundingTextTest { + + @Test + public void testSurroundingTextBasicCreation() { + SurroundingText surroundingText1 = new SurroundingText("test", 0, 0, 0); + assertThat(surroundingText1.getText(), is("test")); + assertThat(surroundingText1.getSelectionStart(), is(0)); + assertThat(surroundingText1.getSelectionEnd(), is(0)); + assertThat(surroundingText1.getOffset(), is(0)); + + SurroundingText surroundingText2 = new SurroundingText("", -1, -1, -1); + assertThat(surroundingText2.getText(), is("")); + assertThat(surroundingText2.getSelectionStart(), is(-1)); + assertThat(surroundingText2.getSelectionEnd(), is(-1)); + assertThat(surroundingText2.getOffset(), is(-1)); + + SurroundingText surroundingText3 = new SurroundingText("hello", 0, 5, 0); + assertThat(surroundingText3.getText(), is("hello")); + assertThat(surroundingText3.getSelectionStart(), is(0)); + assertThat(surroundingText3.getSelectionEnd(), is(5)); + assertThat(surroundingText3.getOffset(), is(0)); + } + + @Test + public void testSurroundingTextWriteToParcel() { + SurroundingText surroundingText = new SurroundingText("text", 0, 1, 2); + Parcel parcel = Parcel.obtain(); + surroundingText.writeToParcel(parcel, 0); + + parcel.setDataPosition(0); + SurroundingText surroundingTextFromParcel = + SurroundingText.CREATOR.createFromParcel(parcel); + assertThat(surroundingText.getText(), is("text")); + assertThat(surroundingText.getSelectionStart(), is(0)); + assertThat(surroundingText.getSelectionEnd(), is(1)); + assertThat(surroundingText.getOffset(), is(2)); + assertThat(surroundingTextFromParcel.getText(), is("text")); + assertThat(surroundingTextFromParcel.getSelectionStart(), is(0)); + assertThat(surroundingTextFromParcel.getSelectionEnd(), is(1)); + assertThat(surroundingTextFromParcel.getOffset(), is(2)); + } +} diff --git a/non-updatable-api/current.txt b/non-updatable-api/current.txt index 24fbe00ad4b8c..b0070a3fac988 100644 --- a/non-updatable-api/current.txt +++ b/non-updatable-api/current.txt @@ -55449,6 +55449,7 @@ package android.view.inputmethod { method public android.view.inputmethod.ExtractedText getExtractedText(android.view.inputmethod.ExtractedTextRequest, int); method public android.os.Handler getHandler(); method public CharSequence getSelectedText(int); + method @Nullable public default android.view.inputmethod.SurroundingText getSurroundingText(@IntRange(from=0) int, @IntRange(from=0) int, int); method public CharSequence getTextAfterCursor(int, int); method public CharSequence getTextBeforeCursor(int, int); method public boolean performContextMenuAction(int); @@ -55657,6 +55658,17 @@ package android.view.inputmethod { method public android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder setSubtypeNameResId(int); } + public final class SurroundingText implements android.os.Parcelable { + ctor public SurroundingText(@NonNull CharSequence, @IntRange(from=0) int, @IntRange(from=0) int, @IntRange(from=0xffffffff) int); + method public int describeContents(); + method @IntRange(from=0xffffffff) public int getOffset(); + method @IntRange(from=0) public int getSelectionEnd(); + method @IntRange(from=0) public int getSelectionStart(); + method @NonNull public CharSequence getText(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + } + } package android.view.inspector {