diff --git a/core/api/current.txt b/core/api/current.txt index 0f0e928194fd0..01e29f18c7d59 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -52153,6 +52153,7 @@ package android.view.inputmethod { method public boolean setComposingText(CharSequence, int); method public default boolean setImeConsumesInput(boolean); method public boolean setSelection(int, int); + method @Nullable public default android.view.inputmethod.TextSnapshot takeSnapshot(); field public static final int CURSOR_UPDATE_IMMEDIATE = 1; // 0x1 field public static final int CURSOR_UPDATE_MONITOR = 2; // 0x2 field public static final int GET_EXTRACTED_TEXT_MONITOR = 1; // 0x1 @@ -52365,6 +52366,16 @@ package android.view.inputmethod { field @NonNull public static final android.os.Parcelable.Creator CREATOR; } + public final class TextSnapshot { + ctor public TextSnapshot(@NonNull android.view.inputmethod.SurroundingText, @IntRange(from=0xffffffff) int, @IntRange(from=0xffffffff) int, int); + method @IntRange(from=0xffffffff) public int getCompositionEnd(); + method @IntRange(from=0xffffffff) public int getCompositionStart(); + method public int getCursorCapsMode(); + method @IntRange(from=0xffffffff) public int getSelectionEnd(); + method @IntRange(from=0xffffffff) public int getSelectionStart(); + method @NonNull public android.view.inputmethod.SurroundingText getSurroundingText(); + } + } package android.view.inspector { diff --git a/core/java/android/view/inputmethod/BaseInputConnection.java b/core/java/android/view/inputmethod/BaseInputConnection.java index c4540b0d8726f..624937ff1d990 100644 --- a/core/java/android/view/inputmethod/BaseInputConnection.java +++ b/core/java/android/view/inputmethod/BaseInputConnection.java @@ -979,4 +979,38 @@ public class BaseInputConnection implements InputConnection { .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); + } } diff --git a/core/java/android/view/inputmethod/InputConnection.java b/core/java/android/view/inputmethod/InputConnection.java index fdcc208d8edd6..c3d7836a4786b 100644 --- a/core/java/android/view/inputmethod/InputConnection.java +++ b/core/java/android/view/inputmethod/InputConnection.java @@ -1034,4 +1034,27 @@ public interface InputConnection { default boolean setImeConsumesInput(boolean imeConsumesInput) { return false; } + + /** + * Called by the system when it needs to take a snapshot of multiple text-related data in an + * atomic manner. + * + *

Editor authors: Supporting this method is strongly encouraged. Atomically + * taken {@link TextSnapshot} is going to be really helpful for the system when optimizing IPCs + * in a safe and deterministic manner. Return {@code null} if an atomically taken + * {@link TextSnapshot} is unavailable. The system continues supporting such a scenario + * gracefully.

+ * + *

IME authors: Currently IMEs cannot call this method directly and always + * receive {@code null} as the result.

+ * + * @return {@code null} if {@link TextSnapshot} is unavailable and/or this API is called from + * IMEs. + */ + @Nullable + default TextSnapshot takeSnapshot() { + // Returning null by default because the composing text range cannot be retrieved from + // existing APIs. + return null; + } } diff --git a/core/java/android/view/inputmethod/TextSnapshot.java b/core/java/android/view/inputmethod/TextSnapshot.java new file mode 100644 index 0000000000000..33ce282780e5a --- /dev/null +++ b/core/java/android/view/inputmethod/TextSnapshot.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2021 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 java.util.Objects; + +/** + * An immutable data snapshot of text editing state. + */ +public final class TextSnapshot { + @NonNull + private final SurroundingText mSurroundingText; + @IntRange(from = -1) + private final int mCompositionStart; + @IntRange(from = -1) + private final int mCompositionEnd; + private final int mCursorCapsMode; + + /** + * Creates a new instance of {@link TextSnapshot} + * + * @param surroundingText {@link SurroundingText} of the current edit field. + * @param compositionStart The start index of the composing text. + * {@code -1} if there is no composing text. + * @param compositionEnd The end index of the composing text. + * {@code -1} if there is no composing text. + * @param cursorCapsMode The capitalization mode of the first character being edited in the + * text. See {@link EditorInfo#initialCapsMode}. + * @throws NullPointerException if {@code surroundingText} is {@code null}. + * @throws IllegalArgumentException if {@code compositionStart} and/or {@code compositionEnd} + * is less than {@code -1}. + */ + public TextSnapshot(@NonNull SurroundingText surroundingText, + @IntRange(from = -1) int compositionStart, @IntRange(from = -1) int compositionEnd, + int cursorCapsMode) { + Objects.requireNonNull(surroundingText); + mSurroundingText = surroundingText; + if (compositionStart < -1) { + throw new IllegalArgumentException("compositionStart must be -1 or higher but was " + + compositionStart); + } + if (compositionEnd < -1) { + throw new IllegalArgumentException("compositionEnd must be -1 or higher but was " + + compositionEnd); + } + if (compositionStart == -1 && compositionEnd != -1) { + throw new IllegalArgumentException("compositionEnd must be -1 if compositionStart is " + + "-1 but was " + compositionEnd); + } + if (compositionStart != -1 && compositionEnd == -1) { + throw new IllegalArgumentException("compositionStart must be -1 if compositionEnd is " + + "-1 but was " + compositionStart); + } + if (compositionStart > compositionEnd) { + throw new IllegalArgumentException("compositionStart=" + compositionStart + " must be" + + " equal to or greater than compositionEnd=" + compositionEnd); + } + mCompositionStart = compositionStart; + mCompositionEnd = compositionEnd; + mCursorCapsMode = cursorCapsMode; + } + + /** + * @return {@link SurroundingText} of the current edit field. + */ + @NonNull + public SurroundingText getSurroundingText() { + return mSurroundingText; + } + + /** + * @return The start index of the selection range. {@code -1} if it is not available. + */ + @IntRange(from = -1) + public int getSelectionStart() { + if (mSurroundingText.getOffset() < 0) { + return -1; + } + return mSurroundingText.getSelectionStart() + mSurroundingText.getOffset(); + } + + /** + * @return The end index of the selection range. {@code -1} if it is not available. + */ + @IntRange(from = -1) + public int getSelectionEnd() { + if (mSurroundingText.getOffset() < 0) { + return -1; + } + return mSurroundingText.getSelectionEnd() + mSurroundingText.getOffset(); + } + + /** + * @return The end index of the composing text. {@code -1} if there is no composing text. + */ + @IntRange(from = -1) + public int getCompositionStart() { + return mCompositionStart; + } + + /** + * @return The end index of the composing text. {@code -1} if there is no composing text. + */ + @IntRange(from = -1) + public int getCompositionEnd() { + return mCompositionEnd; + } + + /** + * The capitalization mode of the first character being edited in the text. + * + *

Values may be any combination of the following values:

+ * + * + *

You should generally just take a non-zero value to mean "start out in caps mode" though. + *

+ * @see EditorInfo#initialCapsMode + */ + public int getCursorCapsMode() { + return mCursorCapsMode; + } +} +