Introduce TextBoundsInfo

This is an API for IME to request text bounds information from editor to
perform handwriting rich gestures. It's useful for IMEs to create custom
handwriting gesture that's not supported the HandwritingGesture. And
it can also be used to provide default rich gesture implementation for
handwriting gesture compatibility.

Bug: 239843501
Test: atest TextBoundsInfoTest
Change-Id: Ia7522371976a4e1fcdff4cc0d244ea427ad3d7c9
This commit is contained in:
Haoyu Zhang
2022-09-15 07:39:00 -07:00
parent 900dd54f12
commit 43aae6ff6d
10 changed files with 1344 additions and 0 deletions

View File

@@ -45561,6 +45561,14 @@ package android.text {
field public static final int DONE = -1; // 0xffffffff
}
public static class SegmentFinder.DefaultSegmentFinder extends android.text.SegmentFinder {
ctor public SegmentFinder.DefaultSegmentFinder(@NonNull int[]);
method public int nextEndBoundary(@IntRange(from=0) int);
method public int nextStartBoundary(@IntRange(from=0) int);
method public int previousEndBoundary(@IntRange(from=0) int);
method public int previousStartBoundary(@IntRange(from=0) int);
}
public class Selection {
method public static boolean extendDown(android.text.Spannable, android.text.Layout);
method public static boolean extendLeft(android.text.Spannable, android.text.Layout);
@@ -53568,6 +53576,7 @@ package android.view.inputmethod {
method public boolean reportFullscreenMode(boolean);
method public boolean requestCursorUpdates(int);
method public default boolean requestCursorUpdates(int, int);
method public default void requestTextBoundsInfo(@NonNull android.graphics.RectF, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.view.inputmethod.TextBoundsInfoResult>);
method public boolean sendKeyEvent(android.view.KeyEvent);
method public boolean setComposingRegion(int, int);
method public default boolean setComposingRegion(int, int, @Nullable android.view.inputmethod.TextAttribute);
@@ -53897,6 +53906,50 @@ package android.view.inputmethod {
method @NonNull public android.view.inputmethod.TextAttribute.Builder setTextConversionSuggestions(@NonNull java.util.List<java.lang.String>);
}
public final class TextBoundsInfo implements android.os.Parcelable {
method public int describeContents();
method @IntRange(from=0, to=125) public int getCharacterBidiLevel(int);
method @NonNull public android.graphics.RectF getCharacterBounds(int);
method public int getCharacterFlags(int);
method public int getEnd();
method @NonNull public android.text.SegmentFinder getGraphemeSegmentFinder();
method @NonNull public android.text.SegmentFinder getLineSegmentFinder();
method @NonNull public android.graphics.Matrix getMatrix();
method public int getStart();
method @NonNull public android.text.SegmentFinder getWordSegmentFinder();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.view.inputmethod.TextBoundsInfo> CREATOR;
field public static final int FLAG_CHARACTER_LINEFEED = 2; // 0x2
field public static final int FLAG_CHARACTER_PUNCTUATION = 4; // 0x4
field public static final int FLAG_CHARACTER_WHITESPACE = 1; // 0x1
field public static final int FLAG_LINE_IS_RTL = 8; // 0x8
}
public static final class TextBoundsInfo.Builder {
ctor public TextBoundsInfo.Builder();
method @NonNull public android.view.inputmethod.TextBoundsInfo build();
method @NonNull public android.view.inputmethod.TextBoundsInfo.Builder clear();
method @NonNull public android.view.inputmethod.TextBoundsInfo.Builder setCharacterBidiLevel(@NonNull int[]);
method @NonNull public android.view.inputmethod.TextBoundsInfo.Builder setCharacterBounds(@NonNull float[]);
method @NonNull public android.view.inputmethod.TextBoundsInfo.Builder setCharacterFlags(@NonNull int[]);
method @NonNull public android.view.inputmethod.TextBoundsInfo.Builder setGraphemeSegmentFinder(@NonNull android.text.SegmentFinder);
method @NonNull public android.view.inputmethod.TextBoundsInfo.Builder setLineSegmentFinder(@NonNull android.text.SegmentFinder);
method @NonNull public android.view.inputmethod.TextBoundsInfo.Builder setMatrix(@NonNull android.graphics.Matrix);
method @NonNull public android.view.inputmethod.TextBoundsInfo.Builder setStartAndEnd(@IntRange(from=0) int, @IntRange(from=0) int);
method @NonNull public android.view.inputmethod.TextBoundsInfo.Builder setWordSegmentFinder(@NonNull android.text.SegmentFinder);
}
public final class TextBoundsInfoResult {
ctor public TextBoundsInfoResult(int);
ctor public TextBoundsInfoResult(int, @NonNull android.view.inputmethod.TextBoundsInfo);
method public int getResultCode();
method @Nullable public android.view.inputmethod.TextBoundsInfo getTextBoundsInfo();
field public static final int CODE_CANCELLED = 3; // 0x3
field public static final int CODE_FAILED = 2; // 0x2
field public static final int CODE_SUCCESS = 1; // 0x1
field public static final int CODE_UNSUPPORTED = 0; // 0x0
}
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();

View File

@@ -16,10 +16,13 @@
package android.inputmethodservice;
import static android.view.inputmethod.TextBoundsInfoResult.CODE_CANCELLED;
import android.annotation.AnyThread;
import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.RectF;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.ResultReceiver;
@@ -40,6 +43,8 @@ import android.view.inputmethod.SelectGesture;
import android.view.inputmethod.SelectRangeGesture;
import android.view.inputmethod.SurroundingText;
import android.view.inputmethod.TextAttribute;
import android.view.inputmethod.TextBoundsInfo;
import android.view.inputmethod.TextBoundsInfoResult;
import com.android.internal.infra.AndroidFuture;
import com.android.internal.inputmethod.IRemoteInputConnection;
@@ -47,6 +52,7 @@ import com.android.internal.inputmethod.InputConnectionCommandHeader;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
import java.util.function.IntConsumer;
/**
@@ -97,6 +103,44 @@ final class IRemoteInputConnectionInvoker {
}
};
/**
* Subclass of {@link ResultReceiver} used by
* {@link #requestTextBoundsInfo(RectF, Executor, Consumer)} for providing
* callback.
*/
private static final class TextBoundsInfoResultReceiver extends ResultReceiver {
@Nullable
private Consumer<TextBoundsInfoResult> mConsumer;
@Nullable
private Executor mExecutor;
TextBoundsInfoResultReceiver(@NonNull Executor executor,
@NonNull Consumer<TextBoundsInfoResult> consumer) {
super(null);
mExecutor = executor;
mConsumer = consumer;
}
@Override
protected void onReceiveResult(@TextBoundsInfoResult.ResultCode int resultCode,
@Nullable Bundle resultData) {
synchronized (this) {
if (mExecutor != null && mConsumer != null) {
final TextBoundsInfoResult textBoundsInfoResult = new TextBoundsInfoResult(
resultCode, TextBoundsInfo.createFromBundle(resultData));
mExecutor.execute(() -> mConsumer.accept(textBoundsInfoResult));
// provide callback only once.
clear();
}
}
}
private void clear() {
mExecutor = null;
mConsumer = null;
}
}
/**
* Creates a new instance of {@link IRemoteInputConnectionInvoker} for the given
* {@link IRemoteInputConnection}.
@@ -735,6 +779,28 @@ final class IRemoteInputConnectionInvoker {
return future;
}
/**
* Invokes {@link IRemoteInputConnection#requestTextBoundsInfo(InputConnectionCommandHeader,
* RectF, ResultReceiver)}
* @param rectF {@code rectF} parameter to be passed.
* @param executor {@code Executor} parameter to be passed.
* @param consumer {@code Consumer} parameter to be passed.
*/
@AnyThread
public void requestTextBoundsInfo(
@NonNull RectF rectF, @NonNull @CallbackExecutor Executor executor,
@NonNull Consumer<TextBoundsInfoResult> consumer) {
Objects.requireNonNull(executor);
Objects.requireNonNull(consumer);
final ResultReceiver resultReceiver = new TextBoundsInfoResultReceiver(executor, consumer);
try {
mConnection.requestTextBoundsInfo(createHeader(), rectF, resultReceiver);
} catch (RemoteException e) {
executor.execute(() -> consumer.accept(new TextBoundsInfoResult(CODE_CANCELLED)));
}
}
/**
* Invokes {@link IRemoteInputConnection#commitContent(InputConnectionCommandHeader,
* InputContentInfo, int, Bundle, AndroidFuture)}.

View File

@@ -21,6 +21,7 @@ import android.annotation.CallbackExecutor;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.RectF;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
@@ -34,6 +35,7 @@ import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputContentInfo;
import android.view.inputmethod.SurroundingText;
import android.view.inputmethod.TextAttribute;
import android.view.inputmethod.TextBoundsInfoResult;
import com.android.internal.inputmethod.CancellationGroup;
import com.android.internal.inputmethod.CompletableFutureUtil;
@@ -44,6 +46,7 @@ import com.android.internal.inputmethod.InputConnectionProtoDumper;
import java.lang.ref.WeakReference;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
import java.util.function.IntConsumer;
/**
@@ -459,6 +462,13 @@ final class RemoteInputConnection implements InputConnection {
mCancellationGroup, MAX_WAIT_TIME_MILLIS);
}
@AnyThread
public void requestTextBoundsInfo(
@NonNull RectF rectF, @NonNull @CallbackExecutor Executor executor,
@NonNull Consumer<TextBoundsInfoResult> consumer) {
mInvoker.requestTextBoundsInfo(rectF, executor, consumer);
}
@AnyThread
public Handler getHandler() {
// Nothing should happen when called from input method.

View File

@@ -19,6 +19,13 @@ package android.text;
import android.annotation.IntRange;
import android.graphics.RectF;
import androidx.annotation.NonNull;
import com.android.internal.util.Preconditions;
import java.util.Arrays;
import java.util.Objects;
/**
* Finds text segment boundaries within text. Subclasses can implement different types of text
* segments. Grapheme clusters and words are examples of possible text segments. These are
@@ -63,4 +70,144 @@ public abstract class SegmentFinder {
* character offset, or {@code DONE} if there are none.
*/
public abstract int nextEndBoundary(@IntRange(from = 0) int offset);
/**
* The default {@link SegmentFinder} implementation based on given segment ranges.
*/
public static class DefaultSegmentFinder extends SegmentFinder {
private final int[] mSegments;
/**
* Create a SegmentFinder with segments stored in an array, where i-th segment's start is
* stored at segments[2 * i] and end is stored at segments[2 * i + 1] respectively.
*
* <p> It is required that segments do not overlap, and are already sorted by their start
* indices. </p>
* @param segments the array that stores the segment ranges.
* @throws IllegalArgumentException if the given segments array's length is not even; the
* given segments are not sorted or there are segments overlap with others.
*/
public DefaultSegmentFinder(@NonNull int[] segments) {
checkSegmentsValid(segments);
mSegments = segments;
}
/** {@inheritDoc} */
@Override
public int previousStartBoundary(@IntRange(from = 0) int offset) {
return findPrevious(offset, /* isStart = */ true);
}
/** {@inheritDoc} */
@Override
public int previousEndBoundary(@IntRange(from = 0) int offset) {
return findPrevious(offset, /* isStart = */ false);
}
/** {@inheritDoc} */
@Override
public int nextStartBoundary(@IntRange(from = 0) int offset) {
return findNext(offset, /* isStart = */ true);
}
/** {@inheritDoc} */
@Override
public int nextEndBoundary(@IntRange(from = 0) int offset) {
return findNext(offset, /* isStart = */ false);
}
private int findNext(int offset, boolean isStart) {
if (offset < 0) return DONE;
if (mSegments.length < 1 || offset > mSegments[mSegments.length - 1]) return DONE;
if (offset < mSegments[0]) {
return isStart ? mSegments[0] : mSegments[1];
}
int index = Arrays.binarySearch(mSegments, offset);
if (index >= 0) {
// mSegments may have duplicate elements (The previous segments end equals
// to the following segments start.) Move the index forwards since we are searching
// for the next segment.
if (index + 1 < mSegments.length && mSegments[index + 1] == offset) {
index = index + 1;
}
// Point the index to the first segment boundary larger than the given offset.
index += 1;
} else {
// binarySearch returns the insertion point, it's the first segment boundary larger
// than the given offset.
index = -(index + 1);
}
if (index >= mSegments.length) return DONE;
// +---------------------------------------+
// | | isStart | isEnd |
// |---------------+-----------+-----------|
// | indexIsStart | index | index + 1 |
// |---------------+-----------+-----------|
// | indexIsEnd | index + 1 | index |
// +---------------------------------------+
boolean indexIsStart = index % 2 == 0;
if (isStart != indexIsStart) {
return (index + 1 < mSegments.length) ? mSegments[index + 1] : DONE;
}
return mSegments[index];
}
private int findPrevious(int offset, boolean isStart) {
if (mSegments.length < 1 || offset < mSegments[0]) return DONE;
if (offset > mSegments[mSegments.length - 1]) {
return isStart ? mSegments[mSegments.length - 2] : mSegments[mSegments.length - 1];
}
int index = Arrays.binarySearch(mSegments, offset);
if (index >= 0) {
// mSegments may have duplicate elements (when the previous segments end equal
// to the following segments start). Move the index backwards since we are searching
// for the previous segment.
if (index > 0 && mSegments[index - 1] == offset) {
index = index - 1;
}
// Point the index to the first segment boundary smaller than the given offset.
index -= 1;
} else {
// binarySearch returns the insertion point, insertionPoint - 1 is the first
// segment boundary smaller than the given offset.
index = -(index + 1) - 1;
}
if (index < 0) return DONE;
// +---------------------------------------+
// | | isStart | isEnd |
// |---------------+-----------+-----------|
// | indexIsStart | index | index - 1 |
// |---------------+-----------+-----------|
// | indexIsEnd | index - 1 | index |
// +---------------------------------------+
boolean indexIsStart = index % 2 == 0;
if (isStart != indexIsStart) {
return (index > 0) ? mSegments[index - 1] : DONE;
}
return mSegments[index];
}
private static void checkSegmentsValid(int[] segments) {
Objects.requireNonNull(segments);
Preconditions.checkArgument(segments.length % 2 == 0,
"the length of segments must be even");
if (segments.length == 0) return;
int lastSegmentEnd = Integer.MIN_VALUE;
for (int index = 0; index < segments.length; index += 2) {
if (segments[index] < lastSegmentEnd) {
throw new IllegalArgumentException("segments can't overlap");
}
if (segments[index] >= segments[index + 1]) {
throw new IllegalArgumentException("the segment range can't be empty");
}
lastSegmentEnd = segments[index + 1];
}
}
}
}

View File

@@ -16,11 +16,14 @@
package android.view.inputmethod;
import static android.view.inputmethod.TextBoundsInfoResult.CODE_UNSUPPORTED;
import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.RectF;
import android.inputmethodservice.InputMethodService;
import android.os.Bundle;
import android.os.Handler;
@@ -32,7 +35,9 @@ import com.android.internal.util.Preconditions;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
import java.util.function.IntConsumer;
/**
@@ -1205,6 +1210,40 @@ public interface InputConnection {
return false;
}
/**
* Called by input method to request the {@link TextBoundsInfo} for a range of text which is
* covered by or in vicinity of the given {@code RectF}. It can be used as a supplementary
* method to implement the handwriting gesture API -
* {@link #performHandwritingGesture(HandwritingGesture, Executor, IntConsumer)}.
*
* <p><strong>Editor authors</strong>: It's preferred that the editor returns a
* {@link TextBoundsInfo} of all the text lines whose bounds intersect with the given
* {@code rectF}.
* </p>
*
* <p><strong>IME authors</strong>: This method is expensive when the text is long. Please
* consider that both the text bounds computation and IPC round-trip to send the data are time
* consuming. It's preferable to only request text bounds in smaller areas.
* </p>
*
* @param rectF the interested area where the text bounds are requested, in the screen
* coordinates.
* @param executor the executor to run the callback.
* @param consumer the callback invoked by editor to return the result. It must return a
* non-null object.
*
* @see TextBoundsInfo
* @see android.view.inputmethod.TextBoundsInfoResult
*/
default void requestTextBoundsInfo(
@NonNull RectF rectF, @NonNull @CallbackExecutor Executor executor,
@NonNull Consumer<TextBoundsInfoResult> consumer) {
Objects.requireNonNull(executor);
Objects.requireNonNull(consumer);
executor.execute(() -> consumer.accept(new TextBoundsInfoResult(CODE_UNSUPPORTED)));
}
/**
* Called by the system to enable application developers to specify a dedicated thread on which
* {@link InputConnection} methods are called back.

View File

@@ -20,6 +20,7 @@ import android.annotation.CallbackExecutor;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.RectF;
import android.os.Bundle;
import android.os.Handler;
import android.view.KeyEvent;
@@ -27,6 +28,7 @@ import android.view.KeyEvent;
import com.android.internal.util.Preconditions;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
import java.util.function.IntConsumer;
/**
@@ -342,6 +344,17 @@ public class InputConnectionWrapper implements InputConnection {
return mTarget.requestCursorUpdates(cursorUpdateMode);
}
/**
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
@Override
public void requestTextBoundsInfo(
@NonNull RectF rectF, @NonNull @CallbackExecutor Executor executor,
@NonNull Consumer<TextBoundsInfoResult> consumer) {
mTarget.requestTextBoundsInfo(rectF, executor, consumer);
}
/**
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.

View File

@@ -28,6 +28,7 @@ import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.annotation.AnyThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.RectF;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
@@ -1145,6 +1146,36 @@ final class RemoteInputConnectionImpl extends IRemoteInputConnection.Stub {
}
}
@Dispatching(cancellable = true)
@Override
public void requestTextBoundsInfo(
InputConnectionCommandHeader header, RectF rectF,
@NonNull ResultReceiver resultReceiver) {
dispatchWithTracing("requestTextBoundsInfo", () -> {
if (header.mSessionId != mCurrentSessionId.get()) {
resultReceiver.send(TextBoundsInfoResult.CODE_CANCELLED, null);
return; // cancelled
}
InputConnection ic = getInputConnection();
if (ic == null || !isActive()) {
Log.w(TAG, "requestTextBoundsInfo on inactive InputConnection");
resultReceiver.send(TextBoundsInfoResult.CODE_CANCELLED, null);
return;
}
ic.requestTextBoundsInfo(
rectF,
Runnable::run,
(textBoundsInfoResult) -> {
final int resultCode = textBoundsInfoResult.getResultCode();
final TextBoundsInfo textBoundsInfo =
textBoundsInfoResult.getTextBoundsInfo();
resultReceiver.send(resultCode,
textBoundsInfo == null ? null : textBoundsInfo.toBundle());
});
});
}
@Dispatching(cancellable = true)
@Override
public void commitContent(InputConnectionCommandHeader header,

View File

@@ -0,0 +1,844 @@
/*
* Copyright (C) 2022 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.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.Matrix;
import android.graphics.RectF;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.SegmentFinder;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.GrowingArrayUtils;
import com.android.internal.util.Preconditions;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
/**
* The text bounds information of a slice of text in the editor.
*
* <p> This class provides IME the layout information of the text within the range from
* {@link #getStart()} to {@link #getEnd()}. It's intended to be used by IME as a supplementary API
* to support handwriting gestures.
* </p>
*/
public final class TextBoundsInfo implements Parcelable {
/**
* The flag indicating that the character is a whitespace.
*
* @see Builder#setCharacterFlags(int[])
* @see #getCharacterFlags(int)
*/
public static final int FLAG_CHARACTER_WHITESPACE = 1;
/**
* The flag indicating that the character is a linefeed character.
*
* @see Builder#setCharacterFlags(int[])
* @see #getCharacterFlags(int)
*/
public static final int FLAG_CHARACTER_LINEFEED = 1 << 1;
/**
* The flag indicating that the character is a punctuation.
*
* @see Builder#setCharacterFlags(int[])
* @see #getCharacterFlags(int)
*/
public static final int FLAG_CHARACTER_PUNCTUATION = 1 << 2;
/**
* The flag indicating that the line this character belongs to has RTL line direction. It's
* required that all characters in the same line must have the same direction.
*
* @see Builder#setCharacterFlags(int[])
* @see #getCharacterFlags(int)
*/
public static final int FLAG_LINE_IS_RTL = 1 << 3;
/** @hide */
@IntDef(prefix = "FLAG_", flag = true, value = {
FLAG_CHARACTER_WHITESPACE,
FLAG_CHARACTER_LINEFEED,
FLAG_CHARACTER_PUNCTUATION,
FLAG_LINE_IS_RTL
})
@Retention(RetentionPolicy.SOURCE)
public @interface CharacterFlags {}
/** All the valid flags. */
private static final int KNOWN_CHARACTER_FLAGS = FLAG_CHARACTER_WHITESPACE
| FLAG_CHARACTER_LINEFEED | FLAG_CHARACTER_PUNCTUATION | FLAG_LINE_IS_RTL;
/**
* The amount of shift to get the character's BiDi level from the internal character flags.
*/
private static final int BIDI_LEVEL_SHIFT = 19;
/**
* The mask used to get the character's BiDi level from the internal character flags.
*/
private static final int BIDI_LEVEL_MASK = 0x7F << BIDI_LEVEL_SHIFT;
/**
* The flag indicating that the character at the index is the start of a line segment.
* This flag is only used internally to serialize the {@link SegmentFinder}.
*
* @see #writeToParcel(Parcel, int)
*/
private static final int FLAG_LINE_SEGMENT_START = 1 << 31;
/**
* The flag indicating that the character at the index is the end of a line segment.
* This flag is only used internally to serialize the {@link SegmentFinder}.
*
* @see #writeToParcel(Parcel, int)
*/
private static final int FLAG_LINE_SEGMENT_END = 1 << 30;
/**
* The flag indicating that the character at the index is the start of a word segment.
* This flag is only used internally to serialize the {@link SegmentFinder}.
*
* @see #writeToParcel(Parcel, int)
*/
private static final int FLAG_WORD_SEGMENT_START = 1 << 29;
/**
* The flag indicating that the character at the index is the end of a word segment.
* This flag is only used internally to serialize the {@link SegmentFinder}.
*
* @see #writeToParcel(Parcel, int)
*/
private static final int FLAG_WORD_SEGMENT_END = 1 << 28;
/**
* The flag indicating that the character at the index is the start of a grapheme segment.
* It's only used internally to serialize the {@link SegmentFinder}.
*
* @see #writeToParcel(Parcel, int)
*/
private static final int FLAG_GRAPHEME_SEGMENT_START = 1 << 27;
/**
* The flag indicating that the character at the index is the end of a grapheme segment.
* It's only used internally to serialize the {@link SegmentFinder}.
*
* @see #writeToParcel(Parcel, int)
*/
private static final int FLAG_GRAPHEME_SEGMENT_END = 1 << 26;
private final int mStart;
private final int mEnd;
private final float[] mMatrixValues;
private final float[] mCharacterBounds;
/**
* The array that encodes character and BiDi levels. They are stored together to save memory
* space, and it's easier during serialization.
*/
private final int[] mInternalCharacterFlags;
private final SegmentFinder mLineSegmentFinder;
private final SegmentFinder mWordSegmentFinder;
private final SegmentFinder mGraphemeSegmentFinder;
/**
* Returns a new instance of {@link android.graphics.Matrix} that indicates the transformation
* matrix that is to be applied other positional data in this class.
*
* @return a new instance (copy) of the transformation matrix.
*/
@NonNull
public Matrix getMatrix() {
final Matrix matrix = new Matrix();
matrix.setValues(mMatrixValues);
return matrix;
}
/**
* Returns the index of the first character whose bounds information is available in this
* {@link TextBoundsInfo}, inclusive.
*
* @see Builder#setStartAndEnd(int, int)
*/
public int getStart() {
return mStart;
}
/**
* Returns the index of the last character whose bounds information is available in this
* {@link TextBoundsInfo}, exclusive.
*
* @see Builder#setStartAndEnd(int, int)
*/
public int getEnd() {
return mEnd;
}
/**
* Return the bounds of the character at the given {@code index}, in the coordinates of the
* editor.
*
* @param index the index of the queried character.
* @return the bounding box of the queried character.
*
* @throws IndexOutOfBoundsException if the given {@code index} is out of the range from
* the {@code start} to the {@code end}.
*/
@NonNull
public RectF getCharacterBounds(int index) {
if (index < mStart || index >= mEnd) {
throw new IndexOutOfBoundsException("Index is out of the bounds of "
+ "[" + mStart + ", " + mEnd + ").");
}
final int offset = 4 * (index - mStart);
return new RectF(mCharacterBounds[offset], mCharacterBounds[offset + 1],
mCharacterBounds[offset + 2], mCharacterBounds[offset + 3]);
}
/**
* Return the flags associated with the character at the given {@code index}.
* The flags contain the following information:
* <ul>
* <li>The {@link #FLAG_CHARACTER_WHITESPACE} flag, indicating the character is a
* whitespace. </li>
* <li>The {@link #FLAG_CHARACTER_LINEFEED} flag, indicating the character is a
* linefeed. </li>
* <li>The {@link #FLAG_CHARACTER_PUNCTUATION} flag, indicating the character is a
* punctuation. </li>
* <li>The {@link #FLAG_LINE_IS_RTL} flag, indicating the line this character belongs to
* has RTL line direction. All characters in the same line must have the same line
* direction. Check {@link #getLineSegmentFinder()} for more information of
* line boundaries. </li>
* </ul>
*
* @param index the index of the queried character.
* @return the flags associated with the queried character.
*
* @throws IndexOutOfBoundsException if the given {@code index} is out of the range from
* the {@code start} to the {@code end}.
*
* @see #FLAG_CHARACTER_WHITESPACE
* @see #FLAG_CHARACTER_LINEFEED
* @see #FLAG_CHARACTER_PUNCTUATION
* @see #FLAG_LINE_IS_RTL
*/
@CharacterFlags
public int getCharacterFlags(int index) {
if (index < mStart || index >= mEnd) {
throw new IndexOutOfBoundsException("Index is out of the bounds of "
+ "[" + mStart + ", " + mEnd + ").");
}
final int offset = index - mStart;
return mInternalCharacterFlags[offset] & KNOWN_CHARACTER_FLAGS;
}
/**
* The BiDi level of the character at the given {@code index}. <br/>
* BiDi level is defined by
* <a href="https://unicode.org/reports/tr9/#Basic_Display_Algorithm" >the unicode
* bidirectional algorithm </a>. One can determine whether a character's direction is
* right-to-left (RTL) or left-to-right (LTR) by checking the last bit of the BiDi level.
* If it's 1, the character is RTL, otherwise the character is LTR. The BiDi level of a
* character must be in the range of [0, 125].
*
* @param index the index of the queried character.
* @return the BiDi level of the character, which is an integer in the range of [0, 125].
* @throws IndexOutOfBoundsException if the given {@code index} is out of the range from
* the {@code start} to the {@code end}.
*
* @see Builder#setCharacterBidiLevel(int[])
*/
@IntRange(from = 0, to = 125)
public int getCharacterBidiLevel(int index) {
if (index < mStart || index >= mEnd) {
throw new IndexOutOfBoundsException("Index is out of the bounds of "
+ "[" + mStart + ", " + mEnd + ").");
}
final int offset = index - mStart;
return (mInternalCharacterFlags[offset] & BIDI_LEVEL_MASK) >> BIDI_LEVEL_SHIFT;
}
/**
* Returns the {@link SegmentFinder} that locates the word boundaries.
*
* @see Builder#setWordSegmentFinder(SegmentFinder)
*/
@NonNull
public SegmentFinder getWordSegmentFinder() {
return mWordSegmentFinder;
}
/**
* Returns the {@link SegmentFinder} that locates the grapheme boundaries.
*
* @see Builder#setGraphemeSegmentFinder(SegmentFinder)
*/
@NonNull
public SegmentFinder getGraphemeSegmentFinder() {
return mGraphemeSegmentFinder;
}
/**
* Returns the {@link SegmentFinder} that locates the line boundaries.
*
* @see Builder#setLineSegmentFinder(SegmentFinder)
*/
@NonNull
public SegmentFinder getLineSegmentFinder() {
return mLineSegmentFinder;
}
/**
* Describe the kinds of special objects contained in this Parcelable
* instance's marshaled representation. For example, if the object will
* include a file descriptor in the output of {@link #writeToParcel(Parcel, int)},
* the return value of this method must include the
* {@link #CONTENTS_FILE_DESCRIPTOR} bit.
*
* @return a bitmask indicating the set of special object types marshaled
* by this Parcelable object instance.
*/
@Override
public int describeContents() {
return 0;
}
/**
* Flatten this object in to a Parcel.
*
* @param dest The Parcel in which the object should be written.
* @param flags Additional flags about how the object should be written.
* May be 0 or {@link #PARCELABLE_WRITE_RETURN_VALUE}.
*/
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeInt(mStart);
dest.writeInt(mEnd);
dest.writeFloatArray(mMatrixValues);
dest.writeFloatArray(mCharacterBounds);
// The end can also be a break position. We need an extra space to encode the breaks.
final int[] encodedFlags = Arrays.copyOf(mInternalCharacterFlags, mEnd - mStart + 1);
encodeSegmentFinder(encodedFlags, FLAG_GRAPHEME_SEGMENT_START, FLAG_GRAPHEME_SEGMENT_END,
mStart, mEnd, mGraphemeSegmentFinder);
encodeSegmentFinder(encodedFlags, FLAG_WORD_SEGMENT_START, FLAG_WORD_SEGMENT_END, mStart,
mEnd, mWordSegmentFinder);
encodeSegmentFinder(encodedFlags, FLAG_LINE_SEGMENT_START, FLAG_LINE_SEGMENT_END, mStart,
mEnd, mLineSegmentFinder);
dest.writeIntArray(encodedFlags);
}
private TextBoundsInfo(Parcel source) {
mStart = source.readInt();
mEnd = source.readInt();
mMatrixValues = Objects.requireNonNull(source.createFloatArray());
mCharacterBounds = Objects.requireNonNull(source.createFloatArray());
final int[] encodedFlags = Objects.requireNonNull(source.createIntArray());
mGraphemeSegmentFinder = decodeSegmentFinder(encodedFlags, FLAG_GRAPHEME_SEGMENT_START,
FLAG_GRAPHEME_SEGMENT_END, mStart, mEnd);
mWordSegmentFinder = decodeSegmentFinder(encodedFlags, FLAG_WORD_SEGMENT_START,
FLAG_WORD_SEGMENT_END, mStart, mEnd);
mLineSegmentFinder = decodeSegmentFinder(encodedFlags, FLAG_LINE_SEGMENT_START,
FLAG_LINE_SEGMENT_END, mStart, mEnd);
final int length = mEnd - mStart;
final int flagsMask = KNOWN_CHARACTER_FLAGS | BIDI_LEVEL_MASK;
mInternalCharacterFlags = new int[length];
for (int i = 0; i < length; ++i) {
// Remove the flags used to encoded segment boundaries.
mInternalCharacterFlags[i] = encodedFlags[i] & flagsMask;
}
}
private TextBoundsInfo(Builder builder) {
mStart = builder.mStart;
mEnd = builder.mEnd;
mMatrixValues = Arrays.copyOf(builder.mMatrixValues, 9);
final int length = mEnd - mStart;
mCharacterBounds = Arrays.copyOf(builder.mCharacterBounds, 4 * length);
// Store characterFlags and characterBidiLevels to save memory.
mInternalCharacterFlags = new int[length];
for (int index = 0; index < length; ++index) {
mInternalCharacterFlags[index] = builder.mCharacterFlags[index]
| (builder.mCharacterBidiLevels[index] << BIDI_LEVEL_SHIFT);
}
mGraphemeSegmentFinder = builder.mGraphemeSegmentFinder;
mWordSegmentFinder = builder.mWordSegmentFinder;
mLineSegmentFinder = builder.mLineSegmentFinder;
}
/**
* The CREATOR to make this class Parcelable.
*/
@NonNull
public static final Parcelable.Creator<TextBoundsInfo> CREATOR = new Creator<TextBoundsInfo>() {
@Override
public TextBoundsInfo createFromParcel(Parcel source) {
return new TextBoundsInfo(source);
}
@Override
public TextBoundsInfo[] newArray(int size) {
return new TextBoundsInfo[size];
}
};
private static final String TEXT_BOUNDS_INFO_KEY = "android.view.inputmethod.TextBoundsInfo";
/**
* Store the {@link TextBoundsInfo} into a {@link Bundle}. This method is used by
* {@link RemoteInputConnectionImpl} to transfer the {@link TextBoundsInfo} from the editor
* to IME.
*
* @see TextBoundsInfoResult
* @see InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer)
* @hide
*/
@NonNull
public Bundle toBundle() {
final Bundle bundle = new Bundle();
bundle.putParcelable(TEXT_BOUNDS_INFO_KEY, this);
return bundle;
}
/** @hide */
@Nullable
public static TextBoundsInfo createFromBundle(@Nullable Bundle bundle) {
if (bundle == null) return null;
return bundle.getParcelable(TEXT_BOUNDS_INFO_KEY, TextBoundsInfo.class);
}
/**
* The builder class to create a {@link TextBoundsInfo} object.
*/
public static final class Builder {
private final float[] mMatrixValues = new float[9];
private boolean mMatrixInitialized;
private int mStart;
private int mEnd;
private float[] mCharacterBounds;
private int[] mCharacterFlags;
private int[] mCharacterBidiLevels;
private SegmentFinder mLineSegmentFinder;
private SegmentFinder mWordSegmentFinder;
private SegmentFinder mGraphemeSegmentFinder;
/** Clear all the parameters set on this {@link Builder} to reuse it. */
@NonNull
public Builder clear() {
mMatrixInitialized = false;
mStart = -1;
mEnd = -1;
mCharacterBounds = null;
mCharacterFlags = null;
mLineSegmentFinder = null;
mWordSegmentFinder = null;
mGraphemeSegmentFinder = null;
return this;
}
/**
* Sets the matrix that transforms local coordinates into screen coordinates.
*
* @param matrix transformation matrix from local coordinates into screen coordinates.
* @throws NullPointerException if the given {@code matrix} is {@code null}.
*/
@NonNull
public Builder setMatrix(@NonNull Matrix matrix) {
Objects.requireNonNull(matrix).getValues(mMatrixValues);
mMatrixInitialized = true;
return this;
}
/**
* Set the start and end index of the {@link TextBoundsInfo}. It's the range of the
* characters whose information is available in the {@link TextBoundsInfo}.
*
* @param start the start index of the {@link TextBoundsInfo}, inclusive.
* @param end the end index of the {@link TextBoundsInfo}, exclusive.
* @throws IllegalArgumentException if the given {@code start} or {@code end} is negative,
* or {@code end} is smaller than the {@code start}.
*/
@NonNull
@SuppressWarnings("MissingGetterMatchingBuilder")
public Builder setStartAndEnd(@IntRange(from = 0) int start, @IntRange(from = 0) int end) {
Preconditions.checkArgument(start >= 0);
Preconditions.checkArgumentInRange(start, 0, end, "start");
mStart = start;
mEnd = end;
return this;
}
/**
* Set the characters bounds, in the coordinates of the editor. <br/>
*
* The given array should be divided into groups of four where each element represents
* left, top, right and bottom of the character bounds respectively.
* The bounds of the i-th character in the editor should be stored at index
* 4 * (i - start). The length of the given array must equal to 4 * (end - start). <br/>
*
* Sometimes multiple characters in a single grapheme are rendered as one symbol on the
* screen. So those characters only have one shared bounds. In this case, we recommend the
* editor to assign all the width to the bounds of the first character in the grapheme,
* and make the rest characters' bounds zero-width. <br/>
*
* For example, the string "'0xD83D' '0xDE00'" is rendered as one grapheme - a grinning face
* emoji. If the bounds of the grapheme is: Rect(5, 10, 15, 20), the character bounds of the
* string should be: [ Rect(5, 10, 15, 20), Rect(15, 10, 15, 20) ].
*
* @param characterBounds the array of the flattened character bounds.
* @throws NullPointerException if the given {@code characterBounds} is {@code null}.
*/
@NonNull
public Builder setCharacterBounds(@NonNull float[] characterBounds) {
mCharacterBounds = Objects.requireNonNull(characterBounds);
return this;
}
/**
* Set the flags of the characters. The flags of the i-th character in the editor is stored
* at index (i - start). The length of the given array must equal to (end - start).
* The flags contain the following information:
* <ul>
* <li>The {@link #FLAG_CHARACTER_WHITESPACE} flag, indicating the character is a
* whitespace. </li>
* <li>The {@link #FLAG_CHARACTER_LINEFEED} flag, indicating the character is a
* linefeed. </li>
* <li>The {@link #FLAG_CHARACTER_PUNCTUATION} flag, indicating the character is a
* punctuation. </li>
* <li>The {@link #FLAG_LINE_IS_RTL} flag, indicating the line this character belongs to
* is RTL. All all character in the same line must have the same line direction. Check
* {@link #getLineSegmentFinder()} for more information of line boundaries. </li>
* </ul>
*
* @param characterFlags the array of the character's flags.
* @throws NullPointerException if the given {@code characterFlags} is {@code null}.
* @throws IllegalArgumentException if the given {@code characterFlags} contains invalid
* flags.
*
* @see #getCharacterFlags(int)
*/
@NonNull
public Builder setCharacterFlags(@NonNull int[] characterFlags) {
Objects.requireNonNull(characterFlags);
for (int characterFlag : characterFlags) {
if ((characterFlag & (~KNOWN_CHARACTER_FLAGS)) != 0) {
throw new IllegalArgumentException("characterFlags contains invalid flags.");
}
}
mCharacterFlags = characterFlags;
return this;
}
/**
* Set the BiDi levels for the character. The bidiLevel of the i-th character in the editor
* is stored at index (i - start). The length of the given array must equal to
* (end - start). <br/>
*
* BiDi level is defined by
* <a href="https://unicode.org/reports/tr9/#Basic_Display_Algorithm" >the unicode
* bidirectional algorithm </a>. One can determine whether a character's direction is
* right-to-left (RTL) or left-to-right (LTR) by checking the last bit of the BiDi level.
* If it's 1, the character is RTL, otherwise the character is LTR. The BiDi level of a
* character must be in the range of [0, 125].
* @param characterBidiLevels the array of the character's BiDi level.
*
* @throws NullPointerException if the given {@code characterBidiLevels} is {@code null}.
* @throws IllegalArgumentException if the given {@code characterBidiLevels} contains an
* element that's out of the range [0, 125].
*
* @see #getCharacterBidiLevel(int)
*/
@NonNull
public Builder setCharacterBidiLevel(@NonNull int[] characterBidiLevels) {
Objects.requireNonNull(characterBidiLevels);
for (int index = 0; index < characterBidiLevels.length; ++index) {
Preconditions.checkArgumentInRange(characterBidiLevels[index], 0, 125,
"bidiLevels[" + index + "]");
}
mCharacterBidiLevels = characterBidiLevels;
return this;
}
/**
* Set the {@link SegmentFinder} that locates the grapheme cluster boundaries. Grapheme is
* defined in <a href="https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries">
* the unicode annex #29: unicode text segmentation<a/>. It's a user-perspective character.
* And it's usually the minimal unit for selection, backspace, deletion etc. <br/>
*
* Please note that only the grapheme segments within the range from start to end will
* be available to the IME. The remaining information will be discarded during serialization
* for better performance.
*
* @param graphemeSegmentFinder the {@link SegmentFinder} that locates the grapheme cluster
* boundaries.
* @throws NullPointerException if the given {@code graphemeSegmentFinder} is {@code null}.
*
* @see #getGraphemeSegmentFinder()
* @see SegmentFinder
* @see SegmentFinder.DefaultSegmentFinder
*/
@NonNull
public Builder setGraphemeSegmentFinder(@NonNull SegmentFinder graphemeSegmentFinder) {
mGraphemeSegmentFinder = Objects.requireNonNull(graphemeSegmentFinder);
return this;
}
/**
* Set the {@link SegmentFinder} that locates the word boundaries. <br/>
*
* Please note that only the word segments within the range from start to end will
* be available to the IME. The remaining information will be discarded during serialization
* for better performance.
* @param wordSegmentFinder set the {@link SegmentFinder} that locates the word boundaries.
* @throws NullPointerException if the given {@code wordSegmentFinder} is {@code null}.
*
* @see #getWordSegmentFinder()
* @see SegmentFinder
* @see SegmentFinder.DefaultSegmentFinder
*/
@NonNull
public Builder setWordSegmentFinder(@NonNull SegmentFinder wordSegmentFinder) {
mWordSegmentFinder = Objects.requireNonNull(wordSegmentFinder);
return this;
}
/**
* Set the {@link SegmentFinder} that locates the line boundaries. Aside from the hard
* breaks in the text, it should also locate the soft line breaks added by the editor.
* It is expected that the characters within the same line is rendered on the same baseline.
* (Except for some text formatted as subscript and superscript.) <br/>
*
* Please note that only the line segments within the range from start to end will
* be available to the IME. The remaining information will be discarded during serialization
* for better performance.
* @param lineSegmentFinder set the {@link SegmentFinder} that locates the line boundaries.
* @throws NullPointerException if the given {@code lineSegmentFinder} is {@code null}.
*
* @see #getLineSegmentFinder()
* @see SegmentFinder
* @see SegmentFinder.DefaultSegmentFinder
*/
@NonNull
public Builder setLineSegmentFinder(@NonNull SegmentFinder lineSegmentFinder) {
mLineSegmentFinder = Objects.requireNonNull(lineSegmentFinder);
return this;
}
/**
* Create the {@link TextBoundsInfo} using the parameters in this {@link Builder}.
*
* @throws IllegalStateException in the following conditions:
* <ul>
* <li>if the {@code start} or {@code end} is not set.</li>
* <li>if the {@code matrix} is not set.</li>
* <li>if {@code characterBounds} is not set or its length doesn't equal to
* 4 * ({@code end} - {@code start}).</li>
* <li>if the {@code characterFlags} is not set or its length doesn't equal to
* ({@code end} - {@code start}).</li>
* <li>if {@code graphemeSegmentFinder}, {@code wordSegmentFinder} or
* {@code lineSegmentFinder} is not set.</li>
* <li>if characters in the same line has inconsistent {@link #FLAG_LINE_IS_RTL}
* flag.</li>
* </ul>
*/
@NonNull
public TextBoundsInfo build() {
if (mStart < 0 || mEnd < 0) {
throw new IllegalStateException("Start and end must be set.");
}
if (!mMatrixInitialized) {
throw new IllegalStateException("Matrix must be set.");
}
if (mCharacterBounds == null) {
throw new IllegalStateException("CharacterBounds must be set.");
}
if (mCharacterFlags == null) {
throw new IllegalStateException("CharacterFlags must be set.");
}
if (mCharacterBidiLevels == null) {
throw new IllegalStateException("CharacterBidiLevel must be set.");
}
if (mCharacterBounds.length != 4 * (mEnd - mStart)) {
throw new IllegalStateException("The length of characterBounds doesn't match the "
+ "length of the given start and end."
+ " Expected length: " + (4 * (mEnd - mStart))
+ " characterBounds length: " + mCharacterBounds.length);
}
if (mCharacterFlags.length != mEnd - mStart) {
throw new IllegalStateException("The length of characterFlags doesn't match the "
+ "length of the given start and end."
+ " Expected length: " + (mEnd - mStart)
+ " characterFlags length: " + mCharacterFlags.length);
}
if (mCharacterBidiLevels.length != mEnd - mStart) {
throw new IllegalStateException("The length of characterBidiLevels doesn't match"
+ " the length of the given start and end."
+ " Expected length: " + (mEnd - mStart)
+ " characterFlags length: " + mCharacterBidiLevels.length);
}
if (mGraphemeSegmentFinder == null) {
throw new IllegalStateException("GraphemeSegmentFinder must be set.");
}
if (mWordSegmentFinder == null) {
throw new IllegalStateException("WordSegmentFinder must be set.");
}
if (mLineSegmentFinder == null) {
throw new IllegalStateException("LineSegmentFinder must be set.");
}
if (!isLineDirectionFlagConsistent(mCharacterFlags, mLineSegmentFinder, mStart, mEnd)) {
throw new IllegalStateException("characters in the same line must have the same "
+ "FLAG_LINE_IS_RTL flag value.");
}
return new TextBoundsInfo(this);
}
}
/**
* Encode the segment start and end positions in {@link SegmentFinder} to a flags array.
*
* For example:
* Text: "A BC DE"
* Input:
* start: 2, end: 7 // substring "BC DE"
* SegmentFinder: segment ranges = [(2, 4), (5, 7)] // a word break iterator
* flags: [0x0000, 0x0000, 0x0080, 0x0000, 0x0000, 0x0000] // 0x0080 is whitespace
* segmentStartFlag: 0x0100
* segmentEndFlag: 0x0200
* Output:
* flags: [0x0100, 0x0000, 0x0280, 0x0100, 0x0000, 0x0200]
* The index 2 and 5 encode segment starts, the index 4 and 7 encode a segment end.
*
* @param flags the flags array to receive the results.
* @param segmentStartFlag the flag used to encode the segment start.
* @param segmentEndFlag the flag used to encode the segment end.
* @param start the start index of the encoded range, inclusive.
* @param end the end index of the encoded range, inclusive.
* @param segmentFinder the SegmentFinder to be encoded.
*
* @see #decodeSegmentFinder(int[], int, int, int, int)
*/
private static void encodeSegmentFinder(@NonNull int[] flags, int segmentStartFlag,
int segmentEndFlag, int start, int end, @NonNull SegmentFinder segmentFinder) {
if (end - start + 1 != flags.length) {
throw new IllegalStateException("The given flags array must have the same length as"
+ " the given range. flags length: " + flags.length
+ " range: [" + start + ", " + end + "]");
}
int segmentEnd = segmentFinder.nextEndBoundary(start);
if (segmentEnd == SegmentFinder.DONE) return;
int segmentStart = segmentFinder.previousStartBoundary(segmentEnd);
while (segmentEnd != SegmentFinder.DONE && segmentEnd <= end) {
if (segmentStart >= start) {
flags[segmentStart - start] |= segmentStartFlag;
flags[segmentEnd - start] |= segmentEndFlag;
}
segmentStart = segmentFinder.nextStartBoundary(segmentStart);
segmentEnd = segmentFinder.nextEndBoundary(segmentEnd);
}
}
/**
* Decode a {@link SegmentFinder} from a flags array.
*
* For example:
* Text: "A BC DE"
* Input:
* start: 2, end: 7 // substring "BC DE"
* flags: [0x0100, 0x0000, 0x0280, 0x0100, 0x0000, 0x0200]
* segmentStartFlag: 0x0100
* segmentEndFlag: 0x0200
* Output:
* SegmentFinder: segment ranges = [(2, 4), (5, 7)]
*
* @param flags the flags array to decode the SegmentFinder.
* @param segmentStartFlag the flag to decode a segment start.
* @param segmentEndFlag the flag to decode a segment end.
* @param start the start index of the interested range, inclusive.
* @param end the end index of the interested range, inclusive.
*
* @see #encodeSegmentFinder(int[], int, int, int, int, SegmentFinder)
*/
private static SegmentFinder decodeSegmentFinder(int[] flags, int segmentStartFlag,
int segmentEndFlag, int start, int end) {
if (end - start + 1 != flags.length) {
throw new IllegalStateException("The given flags array must have the same length as"
+ " the given range. flags length: " + flags.length
+ " range: [" + start + ", " + end + "]");
}
int[] breaks = ArrayUtils.newUnpaddedIntArray(10);
int count = 0;
for (int offset = 0; offset < flags.length; ++offset) {
if ((flags[offset] & segmentStartFlag) == segmentStartFlag) {
breaks = GrowingArrayUtils.append(breaks, count++, start + offset);
}
if ((flags[offset] & segmentEndFlag) == segmentEndFlag) {
breaks = GrowingArrayUtils.append(breaks, count++, start + offset);
}
}
return new SegmentFinder.DefaultSegmentFinder(Arrays.copyOf(breaks, count));
}
/**
* Check whether the {@link #FLAG_LINE_IS_RTL} is the same for characters in the same line.
* @return true if all characters in the same line has the same {@link #FLAG_LINE_IS_RTL} flag.
*/
private static boolean isLineDirectionFlagConsistent(int[] characterFlags,
SegmentFinder lineSegmentFinder, int start, int end) {
int segmentEnd = lineSegmentFinder.nextEndBoundary(start);
if (segmentEnd == SegmentFinder.DONE) return true;
int segmentStart = lineSegmentFinder.previousStartBoundary(segmentEnd);
while (segmentStart != SegmentFinder.DONE && segmentStart < end) {
final int lineStart = Math.max(segmentStart, start);
final int lineEnd = Math.min(segmentEnd, end);
final boolean lineIsRtl = (characterFlags[lineStart - start] & FLAG_LINE_IS_RTL) != 0;
for (int index = lineStart + 1; index < lineEnd; ++index) {
final int flags = characterFlags[index - start];
final boolean characterLineIsRtl = (flags & FLAG_LINE_IS_RTL) != 0;
if (characterLineIsRtl != lineIsRtl) {
return false;
}
}
segmentStart = lineSegmentFinder.nextStartBoundary(segmentStart);
segmentEnd = lineSegmentFinder.nextEndBoundary(segmentEnd);
}
return true;
}
}

View File

@@ -0,0 +1,137 @@
/*
* Copyright (C) 2022 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.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.RectF;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
/**
* The object that holds the result of the
* {@link InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer)} call.
*
* @see InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer)
*/
public final class TextBoundsInfoResult {
private final int mResultCode;
private final TextBoundsInfo mTextBoundsInfo;
/**
* Result for {@link InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer)} when the
* editor doesn't implement the method.
*/
public static final int CODE_UNSUPPORTED = 0;
/**
* Result for {@link InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer)} when the
* editor successfully returns a {@link TextBoundsInfo}.
*/
public static final int CODE_SUCCESS = 1;
/**
* Result for {@link InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer)} when the
* request failed. This result code is returned when the editor can't provide a valid
* {@link TextBoundsInfo}. (e.g. The editor view is not laid out.)
*/
public static final int CODE_FAILED = 2;
/**
* Result for {@link InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer)} when the
* request is cancelled. This happens when the {@link InputConnection} is or becomes
* invalidated while requesting the
* {@link TextBoundsInfo}, for example because a new {@code InputConnection} was started, or
* due to {@link InputMethodManager#invalidateInput}.
*/
public static final int CODE_CANCELLED = 3;
/** @hide */
@IntDef(prefix = { "CODE_" }, value = {
CODE_UNSUPPORTED,
CODE_SUCCESS,
CODE_FAILED,
CODE_CANCELLED,
})
@Retention(RetentionPolicy.SOURCE)
public @interface ResultCode {}
/**
* Create a {@link TextBoundsInfoResult} object with no {@link TextBoundsInfo}.
* The given {@code resultCode} can't be {@link #CODE_SUCCESS}.
* @param resultCode the result code of the
* {@link InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer)} call.
*/
public TextBoundsInfoResult(@ResultCode int resultCode) {
this(resultCode, null);
}
/**
* Create a {@link TextBoundsInfoResult} object.
*
* @param resultCode the result code of the
* {@link InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer)} call.
* @param textBoundsInfo the returned {@link TextBoundsInfo} of the
* {@link InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer)} call. It can't be
* null if the {@code resultCode} is {@link #CODE_SUCCESS}.
*
* @throws IllegalStateException if the resultCode is
* {@link #CODE_SUCCESS} but the given {@code textBoundsInfo}
* is null.
*/
public TextBoundsInfoResult(@ResultCode int resultCode,
@NonNull TextBoundsInfo textBoundsInfo) {
if (resultCode == CODE_SUCCESS && textBoundsInfo == null) {
throw new IllegalStateException("TextBoundsInfo must be provided when the resultCode "
+ "is CODE_SUCCESS.");
}
mResultCode = resultCode;
mTextBoundsInfo = textBoundsInfo;
}
/**
* Return the result code of the
* {@link InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer)} call.
* Its value is one of the {@link #CODE_UNSUPPORTED}, {@link #CODE_SUCCESS},
* {@link #CODE_FAILED} and {@link #CODE_CANCELLED}.
*/
@ResultCode
public int getResultCode() {
return mResultCode;
}
/**
* Return the {@link TextBoundsInfo} provided by the editor. It is non-null if the
* {@code resultCode} is {@link #CODE_SUCCESS}.
* Otherwise, it can be null in the following conditions:
* <ul>
* <li>the editor doesn't support
* {@link InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer)}.</li>
* <li>the editor doesn't have the text bounds information at the moment. (e.g. the editor
* view is not laid out yet.) </li>
* <li> the {@link InputConnection} is or become inactive during the request. </li>
* <ul/>
*/
@Nullable
public TextBoundsInfo getTextBoundsInfo() {
return mTextBoundsInfo;
}
}

View File

@@ -16,6 +16,7 @@
package com.android.internal.inputmethod;
import android.graphics.RectF;
import android.os.Bundle;
import android.os.ResultReceiver;
import android.view.KeyEvent;
@@ -130,6 +131,9 @@ import com.android.internal.inputmethod.InputConnectionCommandHeader;
int cursorUpdateMode, int cursorUpdateFilter, int imeDisplayId,
in AndroidFuture future /* T=Boolean */);
void requestTextBoundsInfo(in InputConnectionCommandHeader header, in RectF rect,
in ResultReceiver resultReceiver /* T=TextBoundsInfoResult */);
void commitContent(in InputConnectionCommandHeader header, in InputContentInfo inputContentInfo,
int flags, in Bundle opts, in AndroidFuture future /* T=Boolean */);