1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.view.inputmethod;
18 
19 import android.annotation.IntDef;
20 import android.annotation.IntRange;
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.graphics.Matrix;
24 import android.graphics.RectF;
25 import android.os.Bundle;
26 import android.os.Parcel;
27 import android.os.Parcelable;
28 import android.text.Layout;
29 import android.text.SegmentFinder;
30 
31 import com.android.internal.util.ArrayUtils;
32 import com.android.internal.util.GrowingArrayUtils;
33 import com.android.internal.util.Preconditions;
34 
35 import java.lang.annotation.Retention;
36 import java.lang.annotation.RetentionPolicy;
37 import java.util.Arrays;
38 import java.util.Objects;
39 import java.util.concurrent.Executor;
40 import java.util.function.Consumer;
41 
42 /**
43  * The text bounds information of a slice of text in the editor.
44  *
45  * <p> This class provides IME the layout information of the text within the range from
46  * {@link #getStartIndex()} to {@link #getEndIndex()}. It's intended to be used by IME as a
47  * supplementary API to support handwriting gestures.
48  * </p>
49  */
50 public final class TextBoundsInfo implements Parcelable {
51     /**
52      * The flag indicating that the character is a whitespace.
53      *
54      * @see Builder#setCharacterFlags(int[])
55      * @see #getCharacterFlags(int)
56      */
57     public static final int FLAG_CHARACTER_WHITESPACE = 1;
58 
59     /**
60      * The flag indicating that the character is a linefeed character.
61      *
62      * @see Builder#setCharacterFlags(int[])
63      * @see #getCharacterFlags(int)
64      */
65     public static final int FLAG_CHARACTER_LINEFEED = 1 << 1;
66 
67     /**
68      * The flag indicating that the character is a punctuation.
69      *
70      * @see Builder#setCharacterFlags(int[])
71      * @see #getCharacterFlags(int)
72      */
73     public static final int FLAG_CHARACTER_PUNCTUATION = 1 << 2;
74 
75     /**
76      * The flag indicating that the line this character belongs to has RTL line direction. It's
77      * required that all characters in the same line must have the same direction.
78      *
79      * @see Builder#setCharacterFlags(int[])
80      * @see #getCharacterFlags(int)
81      */
82     public static final int FLAG_LINE_IS_RTL = 1 << 3;
83 
84 
85     /** @hide */
86     @IntDef(prefix = "FLAG_", flag = true, value = {
87             FLAG_CHARACTER_WHITESPACE,
88             FLAG_CHARACTER_LINEFEED,
89             FLAG_CHARACTER_PUNCTUATION,
90             FLAG_LINE_IS_RTL
91     })
92     @Retention(RetentionPolicy.SOURCE)
93     public @interface CharacterFlags {}
94 
95     /** All the valid flags. */
96     private static final int KNOWN_CHARACTER_FLAGS = FLAG_CHARACTER_WHITESPACE
97             | FLAG_CHARACTER_LINEFEED | FLAG_CHARACTER_PUNCTUATION  | FLAG_LINE_IS_RTL;
98 
99     /**
100      * The amount of shift to get the character's BiDi level from the internal character flags.
101      */
102     private static final int BIDI_LEVEL_SHIFT = 19;
103 
104     /**
105      * The mask used to get the character's BiDi level from the internal character flags.
106      */
107     private static final int BIDI_LEVEL_MASK = 0x7F << BIDI_LEVEL_SHIFT;
108 
109     /**
110      * The flag indicating that the character at the index is the start of a line segment.
111      * This flag is only used internally to serialize the {@link SegmentFinder}.
112      *
113      * @see #writeToParcel(Parcel, int)
114      */
115     private static final int FLAG_LINE_SEGMENT_START = 1 << 31;
116 
117     /**
118      * The flag indicating that the character at the index is the end of a line segment.
119      * This flag is only used internally to serialize the {@link SegmentFinder}.
120      *
121      * @see #writeToParcel(Parcel, int)
122      */
123     private static final int FLAG_LINE_SEGMENT_END = 1 << 30;
124 
125     /**
126      * The flag indicating that the character at the index is the start of a word segment.
127      * This flag is only used internally to serialize the {@link SegmentFinder}.
128      *
129      * @see #writeToParcel(Parcel, int)
130      */
131     private static final int FLAG_WORD_SEGMENT_START = 1 << 29;
132 
133     /**
134      * The flag indicating that the character at the index is the end of a word segment.
135      * This flag is only used internally to serialize the {@link SegmentFinder}.
136      *
137      * @see #writeToParcel(Parcel, int)
138      */
139     private static final int FLAG_WORD_SEGMENT_END = 1 << 28;
140 
141     /**
142      * The flag indicating that the character at the index is the start of a grapheme segment.
143      * It's only used internally to serialize the {@link SegmentFinder}.
144      *
145      * @see #writeToParcel(Parcel, int)
146      */
147     private static final int FLAG_GRAPHEME_SEGMENT_START = 1 << 27;
148 
149     /**
150      * The flag indicating that the character at the index is the end of a grapheme segment.
151      * It's only used internally to serialize the {@link SegmentFinder}.
152      *
153      * @see #writeToParcel(Parcel, int)
154      */
155     private static final int FLAG_GRAPHEME_SEGMENT_END = 1 << 26;
156 
157     private final int mStart;
158     private final int mEnd;
159     private final float[] mMatrixValues;
160     private final float[] mCharacterBounds;
161     /**
162      * The array that encodes character and BiDi levels. They are stored together to save memory
163      * space, and it's easier during serialization.
164      */
165     private final int[] mInternalCharacterFlags;
166     private final SegmentFinder mLineSegmentFinder;
167     private final SegmentFinder mWordSegmentFinder;
168     private final SegmentFinder mGraphemeSegmentFinder;
169 
170     /**
171      * Set the given {@link android.graphics.Matrix} to be the transformation
172      * matrix that is to be applied other positional data in this class.
173      */
174     @NonNull
getMatrix(@onNull Matrix matrix)175     public void getMatrix(@NonNull Matrix matrix) {
176         Objects.requireNonNull(matrix);
177         matrix.setValues(mMatrixValues);
178     }
179 
180     /**
181      * Returns the index of the first character whose bounds information is available in this
182      * {@link TextBoundsInfo}, inclusive.
183      *
184      * @see Builder#setStartAndEnd(int, int)
185      */
getStartIndex()186     public int getStartIndex() {
187         return mStart;
188     }
189 
190     /**
191      * Returns the index of the last character whose bounds information is available in this
192      * {@link TextBoundsInfo}, exclusive.
193      *
194      * @see Builder#setStartAndEnd(int, int)
195      */
getEndIndex()196     public int getEndIndex() {
197         return mEnd;
198     }
199 
200     /**
201      * Set the bounds of the character at the given {@code index} to the given {@link RectF}, in
202      * the coordinates of the editor.
203      *
204      * @param index the index of the queried character.
205      * @param bounds the {@link RectF} used to receive the result.
206      *
207      * @throws IndexOutOfBoundsException if the given {@code index} is out of the range from
208      * the {@code start} to the {@code end}.
209      */
210     @NonNull
getCharacterBounds(int index, @NonNull RectF bounds)211     public void getCharacterBounds(int index, @NonNull RectF bounds) {
212         if (index < mStart || index >= mEnd) {
213             throw new IndexOutOfBoundsException("Index is out of the bounds of "
214                     + "[" + mStart + ", " + mEnd + ").");
215         }
216         final int offset = 4 * (index - mStart);
217         bounds.set(mCharacterBounds[offset], mCharacterBounds[offset + 1],
218                 mCharacterBounds[offset + 2], mCharacterBounds[offset + 3]);
219     }
220 
221     /**
222      * Return the flags associated with the character at the given {@code index}.
223      * The flags contain the following information:
224      * <ul>
225      *     <li>The {@link #FLAG_CHARACTER_WHITESPACE} flag, indicating the character is a
226      *     whitespace. </li>
227      *     <li>The {@link #FLAG_CHARACTER_LINEFEED} flag, indicating the character is a
228      *     linefeed. </li>
229      *     <li>The {@link #FLAG_CHARACTER_PUNCTUATION} flag, indicating the character is a
230      *     punctuation. </li>
231      *     <li>The {@link #FLAG_LINE_IS_RTL} flag, indicating the line this character belongs to
232      *     has RTL line direction. All characters in the same line must have the same line
233      *     direction. Check {@link #getLineSegmentFinder()} for more information of
234      *     line boundaries. </li>
235      * </ul>
236      *
237      * @param index the index of the queried character.
238      * @return the flags associated with the queried character.
239      *
240      * @throws IndexOutOfBoundsException if the given {@code index} is out of the range from
241      * the {@code start} to the {@code end}.
242      *
243      * @see #FLAG_CHARACTER_WHITESPACE
244      * @see #FLAG_CHARACTER_LINEFEED
245      * @see #FLAG_CHARACTER_PUNCTUATION
246      * @see #FLAG_LINE_IS_RTL
247      */
248     @CharacterFlags
getCharacterFlags(int index)249     public int getCharacterFlags(int index) {
250         if (index < mStart || index >= mEnd) {
251             throw new IndexOutOfBoundsException("Index is out of the bounds of "
252                     + "[" + mStart + ", " + mEnd + ").");
253         }
254         final int offset = index - mStart;
255         return mInternalCharacterFlags[offset] & KNOWN_CHARACTER_FLAGS;
256     }
257 
258     /**
259      * The BiDi level of the character at the given {@code index}. <br/>
260      * BiDi level is defined by
261      * <a href="https://unicode.org/reports/tr9/#Basic_Display_Algorithm" >the unicode
262      * bidirectional algorithm </a>. One can determine whether a character's direction is
263      * right-to-left (RTL) or left-to-right (LTR) by checking the last bit of the BiDi level.
264      * If it's 1, the character is RTL, otherwise the character is LTR. The BiDi level of a
265      * character must be in the range of [0, 125].
266      *
267      * @param index the index of the queried character.
268      * @return the BiDi level of the character, which is an integer in the range of [0, 125].
269      * @throws IndexOutOfBoundsException if the given {@code index} is out of the range from
270      * the {@code start} to the {@code end}.
271      *
272      * @see Builder#setCharacterBidiLevel(int[])
273      */
274     @IntRange(from = 0, to = 125)
getCharacterBidiLevel(int index)275     public int getCharacterBidiLevel(int index) {
276         if (index < mStart || index >= mEnd) {
277             throw new IndexOutOfBoundsException("Index is out of the bounds of "
278                     + "[" + mStart + ", " + mEnd + ").");
279         }
280         final int offset = index - mStart;
281         return (mInternalCharacterFlags[offset] & BIDI_LEVEL_MASK) >> BIDI_LEVEL_SHIFT;
282     }
283 
284     /**
285      * Returns the {@link SegmentFinder} that locates the word boundaries.
286      *
287      * @see Builder#setWordSegmentFinder(SegmentFinder)
288      */
289     @NonNull
getWordSegmentFinder()290     public SegmentFinder getWordSegmentFinder() {
291         return mWordSegmentFinder;
292     }
293 
294     /**
295      * Returns the {@link SegmentFinder} that locates the grapheme boundaries.
296      *
297      * @see Builder#setGraphemeSegmentFinder(SegmentFinder)
298      */
299     @NonNull
getGraphemeSegmentFinder()300     public SegmentFinder getGraphemeSegmentFinder() {
301         return mGraphemeSegmentFinder;
302     }
303 
304     /**
305      * Returns the {@link SegmentFinder} that locates the line boundaries.
306      *
307      * @see Builder#setLineSegmentFinder(SegmentFinder)
308      */
309     @NonNull
getLineSegmentFinder()310     public SegmentFinder getLineSegmentFinder() {
311         return mLineSegmentFinder;
312     }
313 
314     /**
315      * Return the index of the closest character to the given position.
316      * It's similar to the text layout API {@link Layout#getOffsetForHorizontal(int, float)}.
317      * And it's mainly used to find the cursor index (the index of the character before which the
318      * cursor should be placed) for the given position. It's guaranteed that the returned index is
319      * a grapheme break. Check {@link #getGraphemeSegmentFinder()} for more information.
320      *
321      * <p>It's assumed that the editor lays out text in horizontal lines from top to bottom and each
322      * line is laid out according to the display algorithm specified in
323      * <a href="https://unicode.org/reports/tr9/#Basic_Display_Algorithm"> unicode bidirectional
324      * algorithm</a>.
325      * </p>
326      *
327      * <p> This method won't check the text ranges whose line information is missing. For example,
328      * the {@link TextBoundsInfo}'s range is from index 5 to 15. If the associated
329      * {@link SegmentFinder} only identifies one line range from 7 to 12. Then this method
330      * won't check the text in the ranges of [5, 7) and [12, 15).
331      * </p>
332      *
333      * <p> Under the following conditions, this method will return -1 indicating that no valid
334      * character is found:
335      * <ul>
336      *   <li> The given {@code y} coordinate is above the first line or below the last line (the
337      *   first line or the last line is identified by the {@link SegmentFinder} returned from
338      *   {@link #getLineSegmentFinder()}). </li>
339      *   <li> There is no character in this {@link TextBoundsInfo}. </li>
340      * </ul>
341      * </p>
342      *
343      * @param x the x coordinates of the interested location, in the editor's coordinates.
344      * @param y the y coordinates of the interested location, in the editor's coordinates.
345      * @return the index of the character whose position is closest to the given location. It will
346      * return -1 if it can't find a character.
347      *
348      * @see Layout#getOffsetForHorizontal(int, float)
349      */
getOffsetForPosition(float x, float y)350     public int getOffsetForPosition(float x, float y) {
351         final int[] lineRange = new int[2];
352         final RectF lineBounds = new RectF();
353         getLineInfo(y, lineRange, lineBounds);
354         // No line is found, return -1;
355         if (lineRange[0] == -1 || lineRange[1] == -1) return -1;
356         final int lineStart = lineRange[0];
357         final int lineEnd = lineRange[1];
358 
359         final boolean lineEndsWithLinefeed =
360                 (getCharacterFlags(lineEnd - 1) & FLAG_CHARACTER_LINEFEED) != 0;
361 
362         // Consider the following 2 cases:
363         // Case 1:
364         //   Text: "AB\nCD"
365         //   Layout: AB
366         //           CD
367         // Case 2:
368         //   Text: "ABCD"
369         //   Layout: AB
370         //           CD
371         // If user wants to insert a 'X' character at the end of the first line:
372         //   In case 1, 'X' is inserted before the last character '\n'.
373         //   In case 2, 'X' is inserted after the last character 'B'.
374         // So if a line ends with linefeed, it shouldn't check the cursor position after the last
375         // character.
376         final int lineLimit;
377         if (lineEndsWithLinefeed) {
378             lineLimit = lineEnd;
379         } else {
380             lineLimit = lineEnd + 1;
381         }
382         // Point graphemeStart to the start of the first grapheme segment intersects with the line.
383         int graphemeStart = mGraphemeSegmentFinder.nextEndBoundary(lineStart);
384         // The grapheme information is missing.
385         if (graphemeStart == SegmentFinder.DONE) return -1;
386         graphemeStart = mGraphemeSegmentFinder.previousStartBoundary(graphemeStart);
387 
388         int target = -1;
389         float minDistance = Float.MAX_VALUE;
390         while (graphemeStart != SegmentFinder.DONE && graphemeStart < lineLimit) {
391             if (graphemeStart >= lineStart) {
392                 float cursorPosition = getCursorHorizontalPosition(graphemeStart, lineStart,
393                         lineEnd, lineBounds.left, lineBounds.right);
394                 final float distance = Math.abs(cursorPosition - x);
395                 if (distance < minDistance) {
396                     minDistance = distance;
397                     target = graphemeStart;
398                 }
399             }
400             graphemeStart = mGraphemeSegmentFinder.nextStartBoundary(graphemeStart);
401         }
402 
403         return target;
404     }
405 
406     /**
407      * Whether the primary position at the given index is the previous character's trailing
408      * position. <br/>
409      *
410      * For LTR character, trailing position is its right edge. For RTL character, trailing position
411      * is its left edge.
412      *
413      * The primary position is defined as the position of a newly inserted character with the
414      * context direction at the given offset. In contrast, the secondary position is the position
415      * of a newly inserted character with the context's opposite direction at the given offset.
416      *
417      * In Android, the trailing position is used for primary position when the direction run after
418      * the given index has a higher level than the current direction run.
419      *
420      * <p>
421      * For example:
422      * (L represents LTR character, and R represents RTL character. The number is the index)
423      * <pre>
424      * input text:          L0 L1 L2 R3 R4 R5 L6 L7 L8
425      * render result:       L0 L1 L2 R5 R4 R3 L6 L7 L8
426      * BiDi Run:            [ Run 0 ][ Run 1 ][ Run 2 ]
427      * BiDi Level:          0  0  0  1  1  1  0  0  0
428      * </pre>
429      *
430      * The index 3 is a BiDi transition point, the cursor can be placed either after L2 or before
431      * R3. Because the bidi level of run 1 is higher than the run 0, this method returns true. And
432      * the cursor should be placed after L2.
433      * <pre>
434      * render result:       L0 L1 L2 R5 R4 R3 L6 L7 L8
435      * position after L2:           |
436      * position before R3:                   |
437      * result position:             |
438      * </pre>
439      *
440      * The index 6 is also a Bidi transition point, the 2 possible cursor positions are exactly the
441      * same as index 3. However, since the bidi level of run 2 is higher than the run 1, this
442      * method returns false. And the cursor should be placed before L6.
443      * <pre>
444      * render result:       L0 L1 L2 R5 R4 R3 L6 L7 L8
445      * position after R5:           |
446      * position before L6:                   |
447      * result position:                      |
448      * </pre>
449      *
450      * This method helps guarantee that the cursor index and the cursor position forms a one to
451      * one relation.
452      * </p>
453      *
454      * @param offset the offset of the character in front of which the cursor is placed. It must be
455      *              the start index of a grapheme. And it must be in the range from lineStart to
456      *              lineEnd. An offset equal to lineEnd is allowed. It indicates that the cursor is
457      *              placed at the end of current line instead of the start of the following line.
458      * @param lineStart the start index of the line that index belongs to, inclusive.
459      * @param lineEnd the end index of the line that index belongs to, exclusive.
460      * @return true if primary position is the trailing position of the previous character.
461      *
462      * @see #getCursorHorizontalPosition(int, int, int, float, float)
463      */
primaryIsTrailingPrevious(int offset, int lineStart, int lineEnd)464     private boolean primaryIsTrailingPrevious(int offset, int lineStart, int lineEnd) {
465         final int bidiLevel;
466         if (offset < lineEnd) {
467             bidiLevel = getCharacterBidiLevel(offset);
468         } else {
469             // index equals to lineEnd, use line's BiDi level for the BiDi run.
470             boolean lineIsRtl =
471                     (getCharacterFlags(offset - 1) & FLAG_LINE_IS_RTL) == FLAG_LINE_IS_RTL;
472             bidiLevel = lineIsRtl ? 1 : 0;
473         }
474         final int bidiLevelBefore;
475         if (offset > lineStart) {
476             // Here it assumes index is always the start of a grapheme. And (index - 1) belongs to
477             // the previous grapheme.
478             bidiLevelBefore = getCharacterBidiLevel(offset - 1);
479         } else {
480             // index equals to lineStart, use line's BiDi level for previous BiDi run.
481             boolean lineIsRtl =
482                     (getCharacterFlags(offset) & FLAG_LINE_IS_RTL) == FLAG_LINE_IS_RTL;
483             bidiLevelBefore = lineIsRtl ? 1 : 0;
484         }
485         return bidiLevelBefore < bidiLevel;
486     }
487 
488     /**
489      * Returns the x coordinates of the cursor at the given index. (The index of the character
490      * before which the cursor should be placed.)
491      *
492      * @param index the character index before which the cursor is placed. It must be the start
493      *              index of a grapheme. It must be in the range from lineStart to lineEnd.
494      *              An index equal to lineEnd is allowed. It indicates that the cursor is
495      *              placed at the end of current line instead of the start of the following line.
496      * @param lineStart start index of the line that index belongs to, inclusive.
497      * @param lineEnd end index of the line that index belongs, exclusive.
498      * @return the x coordinates of the cursor at the given index,
499      *
500      * @see #primaryIsTrailingPrevious(int, int, int)
501      */
getCursorHorizontalPosition(int index, int lineStart, int lineEnd, float lineLeft, float lineRight)502     private float getCursorHorizontalPosition(int index, int lineStart, int lineEnd,
503             float lineLeft, float lineRight) {
504         Preconditions.checkArgumentInRange(index, lineStart, lineEnd, "index");
505         final boolean lineIsRtl = (getCharacterFlags(lineStart) & FLAG_LINE_IS_RTL) != 0;
506         final boolean isPrimaryIsTrailingPrevious =
507                 primaryIsTrailingPrevious(index, lineStart, lineEnd);
508 
509         // The index of the character used to compute the cursor position.
510         final int targetIndex;
511         // Whether to use the start position of the character.
512         // For LTR character start is the left edge. For RTL character, start is the right edge.
513         final boolean isStart;
514         if (isPrimaryIsTrailingPrevious) {
515             // (index - 1) belongs to the previous line(if any), return the line start position.
516             if (index <= lineStart) {
517                 return lineIsRtl ? lineRight : lineLeft;
518             }
519             targetIndex = index - 1;
520             isStart = false;
521         } else {
522             // index belongs to the next line(if any), return the line end position.
523             if (index >= lineEnd) {
524                 return lineIsRtl ? lineLeft : lineRight;
525             }
526             targetIndex = index;
527             isStart = true;
528         }
529 
530         // The BiDi level is odd when the character is RTL.
531         final boolean isRtl = (getCharacterBidiLevel(targetIndex) & 1) != 0;
532         final int offset = targetIndex - mStart;
533         // If the character is RTL, the start is the right edge. Otherwise, the start is the
534         // left edge:
535         //  +-----------------------+
536         //  |       | start | end   |
537         //  |-------+-------+-------|
538         //  | RTL   | right | left  |
539         //  |-------+-------+-------|
540         //  | LTR   | left  | right |
541         //  +-------+-------+-------+
542         return (isRtl != isStart) ? mCharacterBounds[4 * offset] : mCharacterBounds[4 * offset + 2];
543     }
544 
545     /**
546      * Return the minimal rectangle that contains all the characters in the given range.
547      *
548      * @param start the start index of the given range, inclusive.
549      * @param end the end index of the given range, exclusive.
550      * @param rectF the {@link RectF} to receive the bounds.
551      */
getBoundsForRange(int start, int end, @NonNull RectF rectF)552     private void getBoundsForRange(int start, int end, @NonNull RectF rectF) {
553         Preconditions.checkArgumentInRange(start, mStart, mEnd - 1, "start");
554         Preconditions.checkArgumentInRange(end, start, mEnd, "end");
555         if (end <= start) {
556             rectF.setEmpty();
557             return;
558         }
559 
560         rectF.left = Float.MAX_VALUE;
561         rectF.top = Float.MAX_VALUE;
562         rectF.right = Float.MIN_VALUE;
563         rectF.bottom = Float.MIN_VALUE;
564         for (int index = start; index < end; ++index) {
565             final int offset = index - mStart;
566             rectF.left = Math.min(rectF.left, mCharacterBounds[4 * offset]);
567             rectF.top = Math.min(rectF.top, mCharacterBounds[4 * offset + 1]);
568             rectF.right = Math.max(rectF.right, mCharacterBounds[4 * offset + 2]);
569             rectF.bottom = Math.max(rectF.bottom, mCharacterBounds[4 * offset + 3]);
570         }
571     }
572 
573     /**
574      * Return the character range and bounds of the closest line to the given {@code y} coordinate,
575      * in the editor's local coordinates.
576      *
577      * If the given y is above the first line or below the last line -1 will be returned for line
578      * start and end.
579      *
580      * This method assumes that the lines are laid out from the top to bottom.
581      *
582      * @param y the y coordinates used to search for the line.
583      * @param characterRange a two element array used to receive the character range of the line.
584      *                       If no valid line is found -1 will be returned for both start and end.
585      * @param bounds {@link RectF} to receive the line bounds result, nullable. If given, it can
586      *                            still be modified even if no valid line is found.
587      */
getLineInfo(float y, @NonNull int[] characterRange, @Nullable RectF bounds)588     private void getLineInfo(float y, @NonNull int[] characterRange, @Nullable RectF bounds) {
589         characterRange[0] = -1;
590         characterRange[1] = -1;
591 
592         // Starting from the first line.
593         int currentLineEnd = mLineSegmentFinder.nextEndBoundary(mStart);
594         if (currentLineEnd == SegmentFinder.DONE) return;
595         int currentLineStart = mLineSegmentFinder.previousStartBoundary(currentLineEnd);
596 
597         float top = Float.MAX_VALUE;
598         float bottom = Float.MIN_VALUE;
599         float minDistance = Float.MAX_VALUE;
600         final RectF currentLineBounds = new RectF();
601         while (currentLineStart != SegmentFinder.DONE && currentLineStart < mEnd) {
602             final int lineStartInRange = Math.max(mStart, currentLineStart);
603             final int lineEndInRange = Math.min(mEnd, currentLineEnd);
604             getBoundsForRange(lineStartInRange, lineEndInRange, currentLineBounds);
605 
606             top = Math.min(currentLineBounds.top, top);
607             bottom = Math.max(currentLineBounds.bottom, bottom);
608 
609             final float distance = verticalDistance(currentLineBounds, y);
610 
611             if (distance == 0f) {
612                 characterRange[0] = currentLineStart;
613                 characterRange[1] = currentLineEnd;
614                 if (bounds != null) {
615                     bounds.set(currentLineBounds);
616                 }
617                 return;
618             }
619 
620             if (distance < minDistance) {
621                 minDistance = distance;
622                 characterRange[0] = currentLineStart;
623                 characterRange[1] = currentLineEnd;
624                 if (bounds != null) {
625                     bounds.set(currentLineBounds);
626                 }
627             }
628             if (y < bounds.top) break;
629             currentLineStart = mLineSegmentFinder.nextStartBoundary(currentLineStart);
630             currentLineEnd = mLineSegmentFinder.nextEndBoundary(currentLineEnd);
631         }
632 
633         // y is above the first line or below the last line. The founded line is still invalid,
634         // clear the result.
635         if (y < top || y > bottom) {
636             characterRange[0] = -1;
637             characterRange[1] = -1;
638             if (bounds != null) {
639                 bounds.setEmpty();
640             }
641         }
642     }
643 
644     /**
645      * Finds the range of text which is inside the specified rectangle area. This method is a
646      * counterpart of the
647      * {@link Layout#getRangeForRect(RectF, SegmentFinder, Layout.TextInclusionStrategy)}.
648      *
649      * <p>It's assumed that the editor lays out text in horizontal lines from top to bottom
650      * and each line is laid out according to the display algorithm specified in
651      * <a href="https://unicode.org/reports/tr9/#Basic_Display_Algorithm"> unicode bidirectional
652      * algorithm</a>.
653      * </p>
654      *
655      * <p> This method won't check the text ranges whose line information is missing. For example,
656      * the {@link TextBoundsInfo}'s range is from index 5 to 15. If the associated line
657      * {@link SegmentFinder} only identifies one line range from 7 to 12. Then this method
658      * won't check the text in the ranges of [5, 7) and [12, 15).
659      * </p>
660      *
661      * @param area area for which the text range will be found
662      * @param segmentFinder SegmentFinder for determining the ranges of text to be considered as a
663      *     text segment
664      * @param inclusionStrategy strategy for determining whether a text segment is inside the
665      *          specified area
666      * @return the text range stored in a two element int array. The first element is the
667      * start (inclusive) of the text range, and the second element is the end (exclusive) character
668      * offsets of the text range, or null if there are no text segments inside the area.
669      *
670      * @see Layout#getRangeForRect(RectF, SegmentFinder, Layout.TextInclusionStrategy)
671      */
672     @Nullable
getRangeForRect(@onNull RectF area, @NonNull SegmentFinder segmentFinder, @NonNull Layout.TextInclusionStrategy inclusionStrategy)673     public int[] getRangeForRect(@NonNull RectF area, @NonNull SegmentFinder segmentFinder,
674             @NonNull Layout.TextInclusionStrategy inclusionStrategy) {
675         int lineEnd = mLineSegmentFinder.nextEndBoundary(mStart);
676         // Line information is missing.
677         if (lineEnd == SegmentFinder.DONE) return null;
678         int lineStart = mLineSegmentFinder.previousStartBoundary(lineEnd);
679 
680         int start = -1;
681         while (lineStart != SegmentFinder.DONE && start == -1) {
682             start = getStartForRectWithinLine(lineStart, lineEnd, area, segmentFinder,
683                     inclusionStrategy);
684             lineStart = mLineSegmentFinder.nextStartBoundary(lineStart);
685             lineEnd = mLineSegmentFinder.nextEndBoundary(lineEnd);
686         }
687 
688         // Can't find the start index; the specified contains no valid segment.
689         if (start == -1) return null;
690 
691         lineStart = mLineSegmentFinder.previousStartBoundary(mEnd);
692         // Line information is missing.
693         if (lineStart == SegmentFinder.DONE) return null;
694         lineEnd = mLineSegmentFinder.nextEndBoundary(lineStart);
695         int end = -1;
696         while (lineEnd > start && end == -1) {
697             end = getEndForRectWithinLine(lineStart, lineEnd, area, segmentFinder,
698                     inclusionStrategy);
699             lineStart = mLineSegmentFinder.previousStartBoundary(lineStart);
700             lineEnd = mLineSegmentFinder.previousEndBoundary(lineEnd);
701         }
702 
703         // We've already found start, end is guaranteed to be found at this point.
704         start = segmentFinder.previousStartBoundary(start + 1);
705         end = segmentFinder.nextEndBoundary(end - 1);
706         return new int[] { start, end };
707     }
708 
709     /**
710      * Find the start character index of the first text segments within a line inside the specified
711      * {@code area}.
712      *
713      * @param lineStart the start of this line, inclusive .
714      * @param lineEnd the end of this line, exclusive.
715      * @param area the area inside which the text segments will be found.
716      * @param segmentFinder SegmentFinder for determining the ranges of text to be considered a
717      *                      text segment.
718      * @param inclusionStrategy strategy for determining whether a text segment is inside the
719      *                          specified area.
720      * @return the start index of the first segment in the area.
721      */
getStartForRectWithinLine(int lineStart, int lineEnd, @NonNull RectF area, @NonNull SegmentFinder segmentFinder, @NonNull Layout.TextInclusionStrategy inclusionStrategy)722     private int getStartForRectWithinLine(int lineStart, int lineEnd, @NonNull RectF area,
723             @NonNull SegmentFinder segmentFinder,
724             @NonNull Layout.TextInclusionStrategy inclusionStrategy) {
725         if (lineStart >= lineEnd) return -1;
726 
727         int runStart = lineStart;
728         int runLevel = -1;
729         // Check the BiDi runs and search for the start index.
730         for (int index = lineStart; index < lineEnd; ++index) {
731             final int level = getCharacterBidiLevel(index);
732             if (level != runLevel) {
733                 final int start = getStartForRectWithinRun(runStart, index, area, segmentFinder,
734                         inclusionStrategy);
735                 if (start != -1) {
736                     return start;
737                 }
738 
739                 runStart = index;
740                 runLevel = level;
741             }
742         }
743         return getStartForRectWithinRun(runStart, lineEnd, area, segmentFinder, inclusionStrategy);
744     }
745 
746     /**
747      * Find the start character index of the first text segments within the directional run inside
748      * the specified {@code area}.
749      *
750      * @param runStart the start of this directional run, inclusive.
751      * @param runEnd the end of this directional run, exclusive.
752      * @param area the area inside which the text segments will be found.
753      * @param segmentFinder SegmentFinder for determining the ranges of text to be considered a
754      *                      text segment.
755      * @param inclusionStrategy strategy for determining whether a text segment is inside the
756      *                          specified area.
757      * @return the start index of the first segment in the area.
758      */
getStartForRectWithinRun(int runStart, int runEnd, @NonNull RectF area, @NonNull SegmentFinder segmentFinder, @NonNull Layout.TextInclusionStrategy inclusionStrategy)759     private int getStartForRectWithinRun(int runStart, int runEnd, @NonNull RectF area,
760             @NonNull SegmentFinder segmentFinder,
761             @NonNull Layout.TextInclusionStrategy inclusionStrategy) {
762         if (runStart >= runEnd) return -1;
763 
764         int segmentEndOffset = segmentFinder.nextEndBoundary(runStart);
765         // No segment is found in run.
766         if (segmentEndOffset == SegmentFinder.DONE) return -1;
767         int segmentStartOffset = segmentFinder.previousStartBoundary(segmentEndOffset);
768 
769         final RectF segmentBounds = new RectF();
770         while (segmentStartOffset != SegmentFinder.DONE && segmentStartOffset < runEnd) {
771             final int start = Math.max(runStart, segmentStartOffset);
772             final int end = Math.min(runEnd, segmentEndOffset);
773             getBoundsForRange(start, end, segmentBounds);
774             // Find the first segment inside the area, return the start.
775             if (inclusionStrategy.isSegmentInside(segmentBounds, area)) return start;
776 
777             segmentStartOffset = segmentFinder.nextStartBoundary(segmentStartOffset);
778             segmentEndOffset = segmentFinder.nextEndBoundary(segmentEndOffset);
779         }
780         return -1;
781     }
782 
783     /**
784      * Find the end character index of the last text segments within a line inside the specified
785      * {@code area}.
786      *
787      * @param lineStart the start of this line, inclusive .
788      * @param lineEnd the end of this line, exclusive.
789      * @param area the area inside which the text segments will be found.
790      * @param segmentFinder SegmentFinder for determining the ranges of text to be considered a
791      *                      text segment.
792      * @param inclusionStrategy strategy for determining whether a text segment is inside the
793      *                          specified area.
794      * @return the end index of the last segment in the area.
795      */
getEndForRectWithinLine(int lineStart, int lineEnd, @NonNull RectF area, @NonNull SegmentFinder segmentFinder, @NonNull Layout.TextInclusionStrategy inclusionStrategy)796     private int getEndForRectWithinLine(int lineStart, int lineEnd, @NonNull RectF area,
797             @NonNull SegmentFinder segmentFinder,
798             @NonNull Layout.TextInclusionStrategy inclusionStrategy) {
799         if (lineStart >= lineEnd) return -1;
800         lineStart = Math.max(lineStart, mStart);
801         lineEnd = Math.min(lineEnd, mEnd);
802 
803         // The exclusive run end index.
804         int runEnd = lineEnd;
805         int runLevel = -1;
806         // Check the BiDi runs backwards and search for the end index.
807         for (int index = lineEnd - 1; index >= lineStart; --index) {
808             final int level = getCharacterBidiLevel(index);
809             if (level != runLevel) {
810                 final int end = getEndForRectWithinRun(index + 1, runEnd, area, segmentFinder,
811                         inclusionStrategy);
812                 if (end != -1) return end;
813 
814                 runEnd = index + 1;
815                 runLevel = level;
816             }
817         }
818         return getEndForRectWithinRun(lineStart, runEnd, area, segmentFinder, inclusionStrategy);
819     }
820 
821     /**
822      * Find the end character index of the last text segments within the directional run inside the
823      * specified {@code area}.
824      *
825      * @param runStart the start of this directional run, inclusive.
826      * @param runEnd the end of this directional run, exclusive.
827      * @param area the area inside which the text segments will be found.
828      * @param segmentFinder SegmentFinder for determining the ranges of text to be considered a
829      *                      text segment.
830      * @param inclusionStrategy strategy for determining whether a text segment is inside the
831      *                          specified area.
832      * @return the end index of the last segment in the area.
833      */
getEndForRectWithinRun(int runStart, int runEnd, @NonNull RectF area, @NonNull SegmentFinder segmentFinder, @NonNull Layout.TextInclusionStrategy inclusionStrategy)834     private int getEndForRectWithinRun(int runStart, int runEnd, @NonNull RectF area,
835             @NonNull SegmentFinder segmentFinder,
836             @NonNull Layout.TextInclusionStrategy inclusionStrategy) {
837         if (runStart >= runEnd) return -1;
838 
839         int segmentStart = segmentFinder.previousStartBoundary(runEnd);
840         // No segment is found before the runEnd.
841         if (segmentStart == SegmentFinder.DONE) return -1;
842         int segmentEnd = segmentFinder.nextEndBoundary(segmentStart);
843 
844         final RectF segmentBounds = new RectF();
845         while (segmentEnd != SegmentFinder.DONE && segmentEnd > runStart) {
846             final int start = Math.max(runStart, segmentStart);
847             final int end = Math.min(runEnd, segmentEnd);
848             getBoundsForRange(start, end, segmentBounds);
849             // Find the last segment inside the area, return the end.
850             if (inclusionStrategy.isSegmentInside(segmentBounds, area)) return end;
851 
852             segmentStart = segmentFinder.previousStartBoundary(segmentStart);
853             segmentEnd = segmentFinder.previousEndBoundary(segmentEnd);
854         }
855         return -1;
856     }
857 
858     /**
859      * Get the vertical distance from the {@code pointF} to the {@code rectF}. It's useful to find
860      * the corresponding line for a given point.
861      */
verticalDistance(@onNull RectF rectF, float y)862     private static float verticalDistance(@NonNull RectF rectF, float y) {
863         if (rectF.top <= y && y < rectF.bottom) {
864             return 0f;
865         }
866         if (y < rectF.top) {
867             return rectF.top - y;
868         }
869         return y - rectF.bottom;
870     }
871 
872     /**
873      * Describe the kinds of special objects contained in this Parcelable
874      * instance's marshaled representation. For example, if the object will
875      * include a file descriptor in the output of {@link #writeToParcel(Parcel, int)},
876      * the return value of this method must include the
877      * {@link #CONTENTS_FILE_DESCRIPTOR} bit.
878      *
879      * @return a bitmask indicating the set of special object types marshaled
880      * by this Parcelable object instance.
881      */
882     @Override
describeContents()883     public int describeContents() {
884         return 0;
885     }
886 
887     /**
888      * Flatten this object in to a Parcel.
889      *
890      * @param dest  The Parcel in which the object should be written.
891      * @param flags Additional flags about how the object should be written.
892      *              May be 0 or {@link #PARCELABLE_WRITE_RETURN_VALUE}.
893      */
894     @Override
writeToParcel(@onNull Parcel dest, int flags)895     public void writeToParcel(@NonNull Parcel dest, int flags) {
896         dest.writeInt(mStart);
897         dest.writeInt(mEnd);
898         dest.writeFloatArray(mMatrixValues);
899         dest.writeFloatArray(mCharacterBounds);
900 
901         // The end can also be a break position. We need an extra space to encode the breaks.
902         final int[] encodedFlags = Arrays.copyOf(mInternalCharacterFlags, mEnd - mStart + 1);
903         encodeSegmentFinder(encodedFlags, FLAG_GRAPHEME_SEGMENT_START, FLAG_GRAPHEME_SEGMENT_END,
904                 mStart, mEnd, mGraphemeSegmentFinder);
905         encodeSegmentFinder(encodedFlags, FLAG_WORD_SEGMENT_START, FLAG_WORD_SEGMENT_END, mStart,
906                 mEnd, mWordSegmentFinder);
907         encodeSegmentFinder(encodedFlags, FLAG_LINE_SEGMENT_START, FLAG_LINE_SEGMENT_END, mStart,
908                 mEnd, mLineSegmentFinder);
909         dest.writeIntArray(encodedFlags);
910     }
911 
TextBoundsInfo(Parcel source)912     private TextBoundsInfo(Parcel source) {
913         mStart = source.readInt();
914         mEnd  = source.readInt();
915         mMatrixValues = Objects.requireNonNull(source.createFloatArray());
916         mCharacterBounds = Objects.requireNonNull(source.createFloatArray());
917         final int[] encodedFlags = Objects.requireNonNull(source.createIntArray());
918 
919         mGraphemeSegmentFinder = decodeSegmentFinder(encodedFlags, FLAG_GRAPHEME_SEGMENT_START,
920                 FLAG_GRAPHEME_SEGMENT_END, mStart, mEnd);
921         mWordSegmentFinder = decodeSegmentFinder(encodedFlags, FLAG_WORD_SEGMENT_START,
922                 FLAG_WORD_SEGMENT_END, mStart, mEnd);
923         mLineSegmentFinder = decodeSegmentFinder(encodedFlags, FLAG_LINE_SEGMENT_START,
924                 FLAG_LINE_SEGMENT_END, mStart, mEnd);
925 
926         final int length = mEnd - mStart;
927         final int flagsMask = KNOWN_CHARACTER_FLAGS | BIDI_LEVEL_MASK;
928         mInternalCharacterFlags = new int[length];
929         for (int i = 0; i < length; ++i) {
930             // Remove the flags used to encoded segment boundaries.
931             mInternalCharacterFlags[i] = encodedFlags[i] & flagsMask;
932         }
933     }
934 
TextBoundsInfo(Builder builder)935     private TextBoundsInfo(Builder builder) {
936         mStart = builder.mStart;
937         mEnd = builder.mEnd;
938         mMatrixValues = Arrays.copyOf(builder.mMatrixValues, 9);
939         final int length = mEnd - mStart;
940         mCharacterBounds = Arrays.copyOf(builder.mCharacterBounds, 4 * length);
941         // Store characterFlags and characterBidiLevels to save memory.
942         mInternalCharacterFlags = new int[length];
943         for (int index = 0; index < length; ++index) {
944             mInternalCharacterFlags[index] = builder.mCharacterFlags[index]
945                     | (builder.mCharacterBidiLevels[index] << BIDI_LEVEL_SHIFT);
946         }
947         mGraphemeSegmentFinder = builder.mGraphemeSegmentFinder;
948         mWordSegmentFinder = builder.mWordSegmentFinder;
949         mLineSegmentFinder = builder.mLineSegmentFinder;
950     }
951 
952     /**
953      * The CREATOR to make this class Parcelable.
954      */
955     @NonNull
956     public static final Parcelable.Creator<TextBoundsInfo> CREATOR = new Creator<TextBoundsInfo>() {
957         @Override
958         public TextBoundsInfo createFromParcel(Parcel source) {
959             return new TextBoundsInfo(source);
960         }
961 
962         @Override
963         public TextBoundsInfo[] newArray(int size) {
964             return new TextBoundsInfo[size];
965         }
966     };
967 
968     private static final String TEXT_BOUNDS_INFO_KEY = "android.view.inputmethod.TextBoundsInfo";
969 
970     /**
971      * Store the {@link TextBoundsInfo} into a {@link Bundle}. This method is used by
972      * {@link RemoteInputConnectionImpl} to transfer the {@link TextBoundsInfo} from the editor
973      * to IME.
974      *
975      * @see TextBoundsInfoResult
976      * @see InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer)
977      * @hide
978      */
979     @NonNull
toBundle()980     public Bundle toBundle() {
981         final Bundle bundle = new Bundle();
982         bundle.putParcelable(TEXT_BOUNDS_INFO_KEY, this);
983         return bundle;
984 
985     }
986 
987     /** @hide */
988     @Nullable
createFromBundle(@ullable Bundle bundle)989     public static TextBoundsInfo createFromBundle(@Nullable Bundle bundle) {
990         if (bundle == null) return null;
991         return bundle.getParcelable(TEXT_BOUNDS_INFO_KEY, TextBoundsInfo.class);
992     }
993 
994     /**
995      * The builder class to create a {@link TextBoundsInfo} object.
996      */
997     public static final class Builder {
998         private final float[] mMatrixValues = new float[9];
999         private boolean mMatrixInitialized;
1000         private int mStart = -1;
1001         private int mEnd = -1;
1002         private float[] mCharacterBounds;
1003         private int[] mCharacterFlags;
1004         private int[] mCharacterBidiLevels;
1005         private SegmentFinder mLineSegmentFinder;
1006         private SegmentFinder mWordSegmentFinder;
1007         private SegmentFinder mGraphemeSegmentFinder;
1008 
1009         /**
1010          * Create a builder for {@link TextBoundsInfo}.
1011          * @param start the start index of the {@link TextBoundsInfo}, inclusive.
1012          * @param end the end index of the {@link TextBoundsInfo}, exclusive.
1013          * @throws IllegalArgumentException if the given {@code start} or {@code end} is negative,
1014          * or {@code end} is smaller than the {@code start}.
1015          */
Builder(int start, int end)1016         public Builder(int start, int end) {
1017             setStartAndEnd(start, end);
1018         }
1019 
1020         /** Clear all the parameters set on this {@link Builder} to reuse it. */
1021         @NonNull
clear()1022         public Builder clear() {
1023             mMatrixInitialized = false;
1024             mStart = -1;
1025             mEnd = -1;
1026             mCharacterBounds = null;
1027             mCharacterFlags = null;
1028             mCharacterBidiLevels = null;
1029             mLineSegmentFinder = null;
1030             mWordSegmentFinder = null;
1031             mGraphemeSegmentFinder = null;
1032             return this;
1033         }
1034 
1035         /**
1036          * Sets the matrix that transforms local coordinates into screen coordinates.
1037          *
1038          * @param matrix transformation matrix from local coordinates into screen coordinates.
1039          * @throws NullPointerException if the given {@code matrix} is {@code null}.
1040          */
1041         @NonNull
setMatrix(@onNull Matrix matrix)1042         public Builder setMatrix(@NonNull Matrix matrix) {
1043             Objects.requireNonNull(matrix).getValues(mMatrixValues);
1044             mMatrixInitialized = true;
1045             return this;
1046         }
1047 
1048         /**
1049          * Set the start and end index of the {@link TextBoundsInfo}. It's the range of the
1050          * characters whose information is available in the {@link TextBoundsInfo}.
1051          *
1052          * @param start the start index of the {@link TextBoundsInfo}, inclusive.
1053          * @param end the end index of the {@link TextBoundsInfo}, exclusive.
1054          * @throws IllegalArgumentException if the given {@code start} or {@code end} is negative,
1055          * or {@code end} is smaller than the {@code start}.
1056          */
1057         @NonNull
1058         @SuppressWarnings("MissingGetterMatchingBuilder")
setStartAndEnd(@ntRangefrom = 0) int start, @IntRange(from = 0) int end)1059         public Builder setStartAndEnd(@IntRange(from = 0) int start, @IntRange(from = 0) int end) {
1060             Preconditions.checkArgument(start >= 0);
1061             Preconditions.checkArgumentInRange(start, 0, end, "start");
1062             mStart = start;
1063             mEnd = end;
1064             return this;
1065         }
1066 
1067         /**
1068          * Set the characters bounds, in the coordinates of the editor. <br/>
1069          *
1070          * The given array should be divided into groups of four where each element represents
1071          * left, top, right and bottom of the character bounds respectively.
1072          * The bounds of the i-th character in the editor should be stored at index
1073          * 4 * (i - start). The length of the given array must equal to 4 * (end - start). <br/>
1074          *
1075          * Sometimes multiple characters in a single grapheme are rendered as one symbol on the
1076          * screen. So those characters only have one shared bounds. In this case, we recommend the
1077          * editor to assign all the width to the bounds of the first character in the grapheme,
1078          * and make the rest characters' bounds zero-width. <br/>
1079          *
1080          * For example, the string "'0xD83D' '0xDE00'" is rendered as one grapheme - a grinning face
1081          * emoji. If the bounds of the grapheme is: Rect(5, 10, 15, 20), the character bounds of the
1082          * string should be: [ Rect(5, 10, 15, 20), Rect(15, 10, 15, 20) ].
1083          *
1084          * @param characterBounds the array of the flattened character bounds.
1085          * @throws NullPointerException if the given {@code characterBounds} is {@code null}.
1086          */
1087         @NonNull
setCharacterBounds(@onNull float[] characterBounds)1088         public Builder setCharacterBounds(@NonNull float[] characterBounds) {
1089             mCharacterBounds = Objects.requireNonNull(characterBounds);
1090             return this;
1091         }
1092 
1093         /**
1094          * Set the flags of the characters. The flags of the i-th character in the editor is stored
1095          * at index (i - start). The length of the given array must equal to (end - start).
1096          * The flags contain the following information:
1097          * <ul>
1098          *     <li>The {@link #FLAG_CHARACTER_WHITESPACE} flag, indicating the character is a
1099          *     whitespace. </li>
1100          *     <li>The {@link #FLAG_CHARACTER_LINEFEED} flag, indicating the character is a
1101          *     linefeed. </li>
1102          *     <li>The {@link #FLAG_CHARACTER_PUNCTUATION} flag, indicating the character is a
1103          *     punctuation. </li>
1104          *     <li>The {@link #FLAG_LINE_IS_RTL} flag, indicating the line this character belongs to
1105          *     is RTL. All all character in the same line must have the same line direction. Check
1106          *     {@link #getLineSegmentFinder()} for more information of line boundaries. </li>
1107          * </ul>
1108          *
1109          * @param characterFlags the array of the character's flags.
1110          * @throws NullPointerException if the given {@code characterFlags} is {@code null}.
1111          * @throws IllegalArgumentException if the given {@code characterFlags} contains invalid
1112          * flags.
1113          *
1114          * @see #getCharacterFlags(int)
1115          */
1116         @NonNull
setCharacterFlags(@onNull int[] characterFlags)1117         public Builder setCharacterFlags(@NonNull int[] characterFlags) {
1118             Objects.requireNonNull(characterFlags);
1119             for (int characterFlag : characterFlags) {
1120                 if ((characterFlag & (~KNOWN_CHARACTER_FLAGS)) != 0) {
1121                     throw new IllegalArgumentException("characterFlags contains invalid flags.");
1122                 }
1123             }
1124             mCharacterFlags = characterFlags;
1125             return this;
1126         }
1127 
1128         /**
1129          * Set the BiDi levels for the character. The bidiLevel of the i-th character in the editor
1130          * is stored at index (i - start). The length of the given array must equal to
1131          * (end - start). <br/>
1132          *
1133          * BiDi level is defined by
1134          * <a href="https://unicode.org/reports/tr9/#Basic_Display_Algorithm" >the unicode
1135          * bidirectional algorithm </a>. One can determine whether a character's direction is
1136          * right-to-left (RTL) or left-to-right (LTR) by checking the last bit of the BiDi level.
1137          * If it's 1, the character is RTL, otherwise the character is LTR. The BiDi level of a
1138          * character must be in the range of [0, 125].
1139          * @param characterBidiLevels the array of the character's BiDi level.
1140          *
1141          * @throws NullPointerException if the given {@code characterBidiLevels} is {@code null}.
1142          * @throws IllegalArgumentException if the given {@code characterBidiLevels} contains an
1143          * element that's out of the range [0, 125].
1144          *
1145          * @see #getCharacterBidiLevel(int)
1146          */
1147         @NonNull
setCharacterBidiLevel(@onNull int[] characterBidiLevels)1148         public Builder setCharacterBidiLevel(@NonNull int[] characterBidiLevels) {
1149             Objects.requireNonNull(characterBidiLevels);
1150             for (int index = 0; index < characterBidiLevels.length; ++index) {
1151                 Preconditions.checkArgumentInRange(characterBidiLevels[index], 0, 125,
1152                         "bidiLevels[" + index + "]");
1153             }
1154             mCharacterBidiLevels = characterBidiLevels;
1155             return this;
1156         }
1157 
1158         /**
1159          * Set the {@link SegmentFinder} that locates the grapheme cluster boundaries. Grapheme is
1160          * defined in <a href="https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries">
1161          * the unicode annex #29: unicode text segmentation<a/>. It's a user-perspective character.
1162          * And it's usually the minimal unit for selection, backspace, deletion etc. <br/>
1163          *
1164          * Please note that only the grapheme segments within the range from start to end will
1165          * be available to the IME. The remaining information will be discarded during serialization
1166          * for better performance.
1167          *
1168          * @param graphemeSegmentFinder the {@link SegmentFinder} that locates the grapheme cluster
1169          *                              boundaries.
1170          * @throws NullPointerException if the given {@code graphemeSegmentFinder} is {@code null}.
1171          *
1172          * @see #getGraphemeSegmentFinder()
1173          * @see SegmentFinder
1174          * @see SegmentFinder.PrescribedSegmentFinder
1175          */
1176         @NonNull
setGraphemeSegmentFinder(@onNull SegmentFinder graphemeSegmentFinder)1177         public Builder setGraphemeSegmentFinder(@NonNull SegmentFinder graphemeSegmentFinder) {
1178             mGraphemeSegmentFinder = Objects.requireNonNull(graphemeSegmentFinder);
1179             return this;
1180         }
1181 
1182         /**
1183          * Set the {@link SegmentFinder} that locates the word boundaries. <br/>
1184          *
1185          * Please note that only the word segments within the range from start to end will
1186          * be available to the IME. The remaining information will be discarded during serialization
1187          * for better performance.
1188          * @param wordSegmentFinder set the {@link SegmentFinder} that locates the word boundaries.
1189          * @throws NullPointerException if the given {@code wordSegmentFinder} is {@code null}.
1190          *
1191          * @see #getWordSegmentFinder()
1192          * @see SegmentFinder
1193          * @see SegmentFinder.PrescribedSegmentFinder
1194          */
1195         @NonNull
setWordSegmentFinder(@onNull SegmentFinder wordSegmentFinder)1196         public Builder setWordSegmentFinder(@NonNull SegmentFinder wordSegmentFinder) {
1197             mWordSegmentFinder = Objects.requireNonNull(wordSegmentFinder);
1198             return this;
1199         }
1200 
1201         /**
1202          * Set the {@link SegmentFinder} that locates the line boundaries. Aside from the hard
1203          * breaks in the text, it should also locate the soft line breaks added by the editor.
1204          * It is expected that the characters within the same line is rendered on the same baseline.
1205          * (Except for some text formatted as subscript and superscript.) <br/>
1206          *
1207          * Please note that only the line segments within the range from start to end will
1208          * be available to the IME. The remaining information will be discarded during serialization
1209          * for better performance.
1210          * @param lineSegmentFinder set the {@link SegmentFinder} that locates the line boundaries.
1211          * @throws NullPointerException if the given {@code lineSegmentFinder} is {@code null}.
1212          *
1213          * @see #getLineSegmentFinder()
1214          * @see SegmentFinder
1215          * @see SegmentFinder.PrescribedSegmentFinder
1216          */
1217         @NonNull
setLineSegmentFinder(@onNull SegmentFinder lineSegmentFinder)1218         public Builder setLineSegmentFinder(@NonNull SegmentFinder lineSegmentFinder) {
1219             mLineSegmentFinder = Objects.requireNonNull(lineSegmentFinder);
1220             return this;
1221         }
1222 
1223         /**
1224          * Create the {@link TextBoundsInfo} using the parameters in this {@link Builder}.
1225          *
1226          * @throws IllegalStateException in the following conditions:
1227          * <ul>
1228          *     <li>if the {@code start} or {@code end} is not set.</li>
1229          *     <li>if the {@code matrix} is not set.</li>
1230          *     <li>if {@code characterBounds} is not set or its length doesn't equal to
1231          *     4 * ({@code end} - {@code start}).</li>
1232          *     <li>if the {@code characterFlags} is not set or its length doesn't equal to
1233          *     ({@code end} - {@code start}).</li>
1234          *     <li>if {@code graphemeSegmentFinder}, {@code wordSegmentFinder} or
1235          *     {@code lineSegmentFinder} is not set.</li>
1236          *     <li>if characters in the same line has inconsistent {@link #FLAG_LINE_IS_RTL}
1237          *     flag.</li>
1238          * </ul>
1239          */
1240         @NonNull
build()1241         public TextBoundsInfo build() {
1242             if (mStart < 0 || mEnd < 0) {
1243                 throw new IllegalStateException("Start and end must be set.");
1244             }
1245 
1246             if (!mMatrixInitialized) {
1247                 throw new IllegalStateException("Matrix must be set.");
1248             }
1249 
1250             if (mCharacterBounds == null) {
1251                 throw new IllegalStateException("CharacterBounds must be set.");
1252             }
1253 
1254             if (mCharacterFlags == null) {
1255                 throw new IllegalStateException("CharacterFlags must be set.");
1256             }
1257 
1258             if (mCharacterBidiLevels == null) {
1259                 throw new IllegalStateException("CharacterBidiLevel must be set.");
1260             }
1261 
1262             if (mCharacterBounds.length != 4 * (mEnd - mStart)) {
1263                 throw new IllegalStateException("The length of characterBounds doesn't match the "
1264                         + "length of the given start and end."
1265                         + " Expected length: " + (4 * (mEnd - mStart))
1266                         + " characterBounds length: " + mCharacterBounds.length);
1267             }
1268             if (mCharacterFlags.length != mEnd - mStart) {
1269                 throw new IllegalStateException("The length of characterFlags doesn't match the "
1270                         + "length of the given start and end."
1271                         + " Expected length: " + (mEnd - mStart)
1272                         + " characterFlags length: " + mCharacterFlags.length);
1273             }
1274             if (mCharacterBidiLevels.length != mEnd - mStart) {
1275                 throw new IllegalStateException("The length of characterBidiLevels doesn't match"
1276                         + " the length of the given start and end."
1277                         + " Expected length: " + (mEnd - mStart)
1278                         + " characterFlags length: " + mCharacterBidiLevels.length);
1279             }
1280             if (mGraphemeSegmentFinder == null) {
1281                 throw new IllegalStateException("GraphemeSegmentFinder must be set.");
1282             }
1283             if (mWordSegmentFinder == null) {
1284                 throw new IllegalStateException("WordSegmentFinder must be set.");
1285             }
1286             if (mLineSegmentFinder == null) {
1287                 throw new IllegalStateException("LineSegmentFinder must be set.");
1288             }
1289 
1290             if (!isLineDirectionFlagConsistent(mCharacterFlags, mLineSegmentFinder, mStart, mEnd)) {
1291                 throw new IllegalStateException("characters in the same line must have the same "
1292                         + "FLAG_LINE_IS_RTL flag value.");
1293             }
1294             return new TextBoundsInfo(this);
1295         }
1296     }
1297 
1298     /**
1299      * Encode the segment start and end positions in {@link SegmentFinder} to a flags array.
1300      *
1301      * For example:
1302      * Text: "A BC DE"
1303      * Input:
1304      *     start: 2, end: 7                                     // substring "BC DE"
1305      *     SegmentFinder: segment ranges = [(2, 4), (5, 7)]     // a word break iterator
1306      *     flags: [0x0000, 0x0000, 0x0080, 0x0000, 0x0000, 0x0000] // 0x0080 is whitespace
1307      *     segmentStartFlag: 0x0100
1308      *     segmentEndFlag: 0x0200
1309      * Output:
1310      *     flags: [0x0100, 0x0000, 0x0280, 0x0100, 0x0000, 0x0200]
1311      *  The index 2 and 5 encode segment starts, the index 4 and 7 encode a segment end.
1312      *
1313      * @param flags the flags array to receive the results.
1314      * @param segmentStartFlag the flag used to encode the segment start.
1315      * @param segmentEndFlag the flag used to encode the segment end.
1316      * @param start the start index of the encoded range, inclusive.
1317      * @param end the end index of the encoded range, inclusive.
1318      * @param segmentFinder the SegmentFinder to be encoded.
1319      *
1320      * @see #decodeSegmentFinder(int[], int, int, int, int)
1321      */
encodeSegmentFinder(@onNull int[] flags, int segmentStartFlag, int segmentEndFlag, int start, int end, @NonNull SegmentFinder segmentFinder)1322     private static void encodeSegmentFinder(@NonNull int[] flags, int segmentStartFlag,
1323             int segmentEndFlag, int start, int end, @NonNull SegmentFinder segmentFinder) {
1324         if (end - start + 1 != flags.length) {
1325             throw new IllegalStateException("The given flags array must have the same length as"
1326                     + " the given range. flags length: " + flags.length
1327                     + " range: [" + start + ", " + end + "]");
1328         }
1329 
1330         int segmentEnd = segmentFinder.nextEndBoundary(start);
1331         if (segmentEnd == SegmentFinder.DONE) return;
1332         int segmentStart = segmentFinder.previousStartBoundary(segmentEnd);
1333 
1334         while (segmentEnd != SegmentFinder.DONE && segmentEnd <= end) {
1335             if (segmentStart >= start) {
1336                 flags[segmentStart - start] |= segmentStartFlag;
1337                 flags[segmentEnd - start] |= segmentEndFlag;
1338             }
1339             segmentStart = segmentFinder.nextStartBoundary(segmentStart);
1340             segmentEnd = segmentFinder.nextEndBoundary(segmentEnd);
1341         }
1342     }
1343 
1344     /**
1345      * Decode a {@link SegmentFinder} from a flags array.
1346      *
1347      * For example:
1348      * Text: "A BC DE"
1349      * Input:
1350      *     start: 2, end: 7                                     // substring "BC DE"
1351      *     flags: [0x0100, 0x0000, 0x0280, 0x0100, 0x0000, 0x0200]
1352      *     segmentStartFlag: 0x0100
1353      *     segmentEndFlag: 0x0200
1354      * Output:
1355      *     SegmentFinder: segment ranges = [(2, 4), (5, 7)]
1356      *
1357      * @param flags the flags array to decode the SegmentFinder.
1358      * @param segmentStartFlag the flag to decode a segment start.
1359      * @param segmentEndFlag the flag to decode a segment end.
1360      * @param start the start index of the interested range, inclusive.
1361      * @param end the end index of the interested range, inclusive.
1362      *
1363      * @see #encodeSegmentFinder(int[], int, int, int, int, SegmentFinder)
1364      */
decodeSegmentFinder(int[] flags, int segmentStartFlag, int segmentEndFlag, int start, int end)1365     private static SegmentFinder decodeSegmentFinder(int[] flags, int segmentStartFlag,
1366             int segmentEndFlag, int start, int end) {
1367         if (end - start + 1 != flags.length) {
1368             throw new IllegalStateException("The given flags array must have the same length as"
1369                     + " the given range. flags length: " + flags.length
1370                     + " range: [" + start + ", " + end + "]");
1371         }
1372         int[] breaks = ArrayUtils.newUnpaddedIntArray(10);
1373         int count = 0;
1374         for (int offset = 0; offset < flags.length; ++offset) {
1375             if ((flags[offset] & segmentStartFlag) == segmentStartFlag) {
1376                 breaks = GrowingArrayUtils.append(breaks, count++, start + offset);
1377             }
1378             if ((flags[offset] & segmentEndFlag) == segmentEndFlag) {
1379                 breaks = GrowingArrayUtils.append(breaks, count++, start + offset);
1380             }
1381         }
1382         return new SegmentFinder.PrescribedSegmentFinder(Arrays.copyOf(breaks, count));
1383     }
1384 
1385     /**
1386      * Check whether the {@link #FLAG_LINE_IS_RTL} is the same for characters in the same line.
1387      * @return true if all characters in the same line has the same {@link #FLAG_LINE_IS_RTL} flag.
1388      */
isLineDirectionFlagConsistent(int[] characterFlags, SegmentFinder lineSegmentFinder, int start, int end)1389     private static boolean isLineDirectionFlagConsistent(int[] characterFlags,
1390             SegmentFinder lineSegmentFinder, int start, int end) {
1391         int segmentEnd = lineSegmentFinder.nextEndBoundary(start);
1392         if (segmentEnd == SegmentFinder.DONE) return true;
1393         int segmentStart = lineSegmentFinder.previousStartBoundary(segmentEnd);
1394 
1395         while (segmentStart != SegmentFinder.DONE && segmentStart < end) {
1396             final int lineStart = Math.max(segmentStart, start);
1397             final int lineEnd = Math.min(segmentEnd, end);
1398             final boolean lineIsRtl = (characterFlags[lineStart - start] & FLAG_LINE_IS_RTL) != 0;
1399             for (int index = lineStart + 1; index < lineEnd; ++index) {
1400                 final int flags = characterFlags[index - start];
1401                 final boolean characterLineIsRtl = (flags & FLAG_LINE_IS_RTL) != 0;
1402                 if (characterLineIsRtl != lineIsRtl) {
1403                     return false;
1404                 }
1405             }
1406 
1407             segmentStart = lineSegmentFinder.nextStartBoundary(segmentStart);
1408             segmentEnd = lineSegmentFinder.nextEndBoundary(segmentEnd);
1409         }
1410         return true;
1411     }
1412 }
1413