Merge "Introduce TextBoundsInfo"

This commit is contained in:
Haoyu Zhang
2022-11-01 05:00:28 +00:00
committed by Android (Google) Code Review
10 changed files with 1344 additions and 0 deletions

View File

@@ -45582,6 +45582,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);
@@ -53603,6 +53611,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);
@@ -53932,6 +53941,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;
@@ -34,6 +37,8 @@ import android.view.inputmethod.InputContentInfo;
import android.view.inputmethod.ParcelableHandwritingGesture;
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;
@@ -41,6 +46,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;
/**
@@ -91,6 +97,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}.
@@ -698,6 +742,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;
@@ -35,6 +36,7 @@ import android.view.inputmethod.InputContentInfo;
import android.view.inputmethod.ParcelableHandwritingGesture;
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;
@@ -45,6 +47,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;
/**
@@ -461,6 +464,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;
@@ -1092,6 +1093,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;
@@ -106,6 +107,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 */);