1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * 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, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16
17package android.view.inputmethod;
18
19import android.annotation.NonNull;
20import android.graphics.Matrix;
21import android.graphics.RectF;
22import android.os.Parcel;
23import android.os.Parcelable;
24import android.text.Layout;
25import android.text.SpannedString;
26import android.text.TextUtils;
27import android.view.inputmethod.SparseRectFArray.SparseRectFArrayBuilder;
28
29import java.util.Arrays;
30import java.util.Objects;
31
32/**
33 * Positional information about the text insertion point and characters in the composition string.
34 *
35 * <p>This class encapsulates locations of the text insertion point and the composition string in
36 * the screen coordinates so that IMEs can render their UI components near where the text is
37 * actually inserted.</p>
38 */
39public final class CursorAnchorInfo implements Parcelable {
40    /**
41     * The pre-computed hash code.
42     */
43    private final int mHashCode;
44
45    /**
46     * The index of the first character of the selected text (inclusive). {@code -1} when there is
47     * no text selection.
48     */
49    private final int mSelectionStart;
50    /**
51     * The index of the first character of the selected text (exclusive). {@code -1} when there is
52     * no text selection.
53     */
54    private final int mSelectionEnd;
55
56    /**
57     * The index of the first character of the composing text (inclusive). {@code -1} when there is
58     * no composing text.
59     */
60    private final int mComposingTextStart;
61    /**
62     * The text, tracked as a composing region.
63     */
64    private final CharSequence mComposingText;
65
66    /**
67     * Flags of the insertion marker. See {@link #FLAG_HAS_VISIBLE_REGION} for example.
68     */
69    private final int mInsertionMarkerFlags;
70    /**
71     * Horizontal position of the insertion marker, in the local coordinates that will be
72     * transformed with the transformation matrix when rendered on the screen. This should be
73     * calculated or compatible with {@link Layout#getPrimaryHorizontal(int)}. This can be
74     * {@code java.lang.Float.NaN} when no value is specified.
75     */
76    private final float mInsertionMarkerHorizontal;
77    /**
78     * Vertical position of the insertion marker, in the local coordinates that will be
79     * transformed with the transformation matrix when rendered on the screen. This should be
80     * calculated or compatible with {@link Layout#getLineTop(int)}. This can be
81     * {@code java.lang.Float.NaN} when no value is specified.
82     */
83    private final float mInsertionMarkerTop;
84    /**
85     * Vertical position of the insertion marker, in the local coordinates that will be
86     * transformed with the transformation matrix when rendered on the screen. This should be
87     * calculated or compatible with {@link Layout#getLineBaseline(int)}. This can be
88     * {@code java.lang.Float.NaN} when no value is specified.
89     */
90    private final float mInsertionMarkerBaseline;
91    /**
92     * Vertical position of the insertion marker, in the local coordinates that will be
93     * transformed with the transformation matrix when rendered on the screen. This should be
94     * calculated or compatible with {@link Layout#getLineBottom(int)}. This can be
95     * {@code java.lang.Float.NaN} when no value is specified.
96     */
97    private final float mInsertionMarkerBottom;
98
99    /**
100     * Container of rectangular position of characters, keyed with character index in a unit of
101     * Java chars, in the local coordinates that will be transformed with the transformation matrix
102     * when rendered on the screen.
103     */
104    private final SparseRectFArray mCharacterBoundsArray;
105
106    /**
107     * Transformation matrix that is applied to any positional information of this class to
108     * transform local coordinates into screen coordinates.
109     */
110    @NonNull
111    private final float[] mMatrixValues;
112
113    /**
114     * Flag for {@link #getInsertionMarkerFlags()} and {@link #getCharacterBoundsFlags(int)}: the
115     * insertion marker or character bounds have at least one visible region.
116     */
117    public static final int FLAG_HAS_VISIBLE_REGION = 0x01;
118
119    /**
120     * Flag for {@link #getInsertionMarkerFlags()} and {@link #getCharacterBoundsFlags(int)}: the
121     * insertion marker or character bounds have at least one invisible (clipped) region.
122     */
123    public static final int FLAG_HAS_INVISIBLE_REGION = 0x02;
124
125    /**
126     * Flag for {@link #getInsertionMarkerFlags()} and {@link #getCharacterBoundsFlags(int)}: the
127     * insertion marker or character bounds is placed at right-to-left (RTL) character.
128     */
129    public static final int FLAG_IS_RTL = 0x04;
130
131    public CursorAnchorInfo(final Parcel source) {
132        mHashCode = source.readInt();
133        mSelectionStart = source.readInt();
134        mSelectionEnd = source.readInt();
135        mComposingTextStart = source.readInt();
136        mComposingText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
137        mInsertionMarkerFlags = source.readInt();
138        mInsertionMarkerHorizontal = source.readFloat();
139        mInsertionMarkerTop = source.readFloat();
140        mInsertionMarkerBaseline = source.readFloat();
141        mInsertionMarkerBottom = source.readFloat();
142        mCharacterBoundsArray = source.readParcelable(SparseRectFArray.class.getClassLoader());
143        mMatrixValues = source.createFloatArray();
144    }
145
146    /**
147     * Used to package this object into a {@link Parcel}.
148     *
149     * @param dest The {@link Parcel} to be written.
150     * @param flags The flags used for parceling.
151     */
152    @Override
153    public void writeToParcel(Parcel dest, int flags) {
154        dest.writeInt(mHashCode);
155        dest.writeInt(mSelectionStart);
156        dest.writeInt(mSelectionEnd);
157        dest.writeInt(mComposingTextStart);
158        TextUtils.writeToParcel(mComposingText, dest, flags);
159        dest.writeInt(mInsertionMarkerFlags);
160        dest.writeFloat(mInsertionMarkerHorizontal);
161        dest.writeFloat(mInsertionMarkerTop);
162        dest.writeFloat(mInsertionMarkerBaseline);
163        dest.writeFloat(mInsertionMarkerBottom);
164        dest.writeParcelable(mCharacterBoundsArray, flags);
165        dest.writeFloatArray(mMatrixValues);
166    }
167
168    @Override
169    public int hashCode(){
170        return mHashCode;
171    }
172
173    /**
174     * Compares two float values. Returns {@code true} if {@code a} and {@code b} are
175     * {@link Float#NaN} at the same time.
176     */
177    private static boolean areSameFloatImpl(final float a, final float b) {
178        if (Float.isNaN(a) && Float.isNaN(b)) {
179            return true;
180        }
181        return a == b;
182    }
183
184    @Override
185    public boolean equals(Object obj){
186        if (obj == null) {
187            return false;
188        }
189        if (this == obj) {
190            return true;
191        }
192        if (!(obj instanceof CursorAnchorInfo)) {
193            return false;
194        }
195        final CursorAnchorInfo that = (CursorAnchorInfo) obj;
196        if (hashCode() != that.hashCode()) {
197            return false;
198        }
199
200        // Check fields that are not covered by hashCode() first.
201
202        if (mSelectionStart != that.mSelectionStart || mSelectionEnd != that.mSelectionEnd) {
203            return false;
204        }
205
206        if (mInsertionMarkerFlags != that.mInsertionMarkerFlags
207                || !areSameFloatImpl(mInsertionMarkerHorizontal, that.mInsertionMarkerHorizontal)
208                || !areSameFloatImpl(mInsertionMarkerTop, that.mInsertionMarkerTop)
209                || !areSameFloatImpl(mInsertionMarkerBaseline, that.mInsertionMarkerBaseline)
210                || !areSameFloatImpl(mInsertionMarkerBottom, that.mInsertionMarkerBottom)) {
211            return false;
212        }
213
214        if (!Objects.equals(mCharacterBoundsArray, that.mCharacterBoundsArray)) {
215            return false;
216        }
217
218        // Following fields are (partially) covered by hashCode().
219
220        if (mComposingTextStart != that.mComposingTextStart
221                || !Objects.equals(mComposingText, that.mComposingText)) {
222            return false;
223        }
224
225        // We do not use Arrays.equals(float[], float[]) to keep the previous behavior regarding
226        // NaN, 0.0f, and -0.0f.
227        if (mMatrixValues.length != that.mMatrixValues.length) {
228            return false;
229        }
230        for (int i = 0; i < mMatrixValues.length; ++i) {
231            if (mMatrixValues[i] != that.mMatrixValues[i]) {
232                return false;
233            }
234        }
235        return true;
236    }
237
238    @Override
239    public String toString() {
240        return "CursorAnchorInfo{mHashCode=" + mHashCode
241                + " mSelection=" + mSelectionStart + "," + mSelectionEnd
242                + " mComposingTextStart=" + mComposingTextStart
243                + " mComposingText=" + Objects.toString(mComposingText)
244                + " mInsertionMarkerFlags=" + mInsertionMarkerFlags
245                + " mInsertionMarkerHorizontal=" + mInsertionMarkerHorizontal
246                + " mInsertionMarkerTop=" + mInsertionMarkerTop
247                + " mInsertionMarkerBaseline=" + mInsertionMarkerBaseline
248                + " mInsertionMarkerBottom=" + mInsertionMarkerBottom
249                + " mCharacterBoundsArray=" + Objects.toString(mCharacterBoundsArray)
250                + " mMatrix=" + Arrays.toString(mMatrixValues)
251                + "}";
252    }
253
254    /**
255     * Builder for {@link CursorAnchorInfo}. This class is not designed to be thread-safe.
256     */
257    public static final class Builder {
258        private int mSelectionStart = -1;
259        private int mSelectionEnd = -1;
260        private int mComposingTextStart = -1;
261        private CharSequence mComposingText = null;
262        private float mInsertionMarkerHorizontal = Float.NaN;
263        private float mInsertionMarkerTop = Float.NaN;
264        private float mInsertionMarkerBaseline = Float.NaN;
265        private float mInsertionMarkerBottom = Float.NaN;
266        private int mInsertionMarkerFlags = 0;
267        private SparseRectFArrayBuilder mCharacterBoundsArrayBuilder = null;
268        private float[] mMatrixValues = null;
269        private boolean mMatrixInitialized = false;
270
271        /**
272         * Sets the text range of the selection. Calling this can be skipped if there is no
273         * selection.
274         */
275        public Builder setSelectionRange(final int newStart, final int newEnd) {
276            mSelectionStart = newStart;
277            mSelectionEnd = newEnd;
278            return this;
279        }
280
281        /**
282         * Sets the text range of the composing text. Calling this can be skipped if there is
283         * no composing text.
284         * @param composingTextStart index where the composing text starts.
285         * @param composingText the entire composing text.
286         */
287        public Builder setComposingText(final int composingTextStart,
288            final CharSequence composingText) {
289            mComposingTextStart = composingTextStart;
290            if (composingText == null) {
291                mComposingText = null;
292            } else {
293                // Make a snapshot of the given char sequence.
294                mComposingText = new SpannedString(composingText);
295            }
296            return this;
297        }
298
299        /**
300         * Sets the location of the text insertion point (zero width cursor) as a rectangle in
301         * local coordinates. Calling this can be skipped when there is no text insertion point;
302         * however if there is an insertion point, editors must call this method.
303         * @param horizontalPosition horizontal position of the insertion marker, in the local
304         * coordinates that will be transformed with the transformation matrix when rendered on the
305         * screen. This should be calculated or compatible with
306         * {@link Layout#getPrimaryHorizontal(int)}.
307         * @param lineTop vertical position of the insertion marker, in the local coordinates that
308         * will be transformed with the transformation matrix when rendered on the screen. This
309         * should be calculated or compatible with {@link Layout#getLineTop(int)}.
310         * @param lineBaseline vertical position of the insertion marker, in the local coordinates
311         * that will be transformed with the transformation matrix when rendered on the screen. This
312         * should be calculated or compatible with {@link Layout#getLineBaseline(int)}.
313         * @param lineBottom vertical position of the insertion marker, in the local coordinates
314         * that will be transformed with the transformation matrix when rendered on the screen. This
315         * should be calculated or compatible with {@link Layout#getLineBottom(int)}.
316         * @param flags flags of the insertion marker. See {@link #FLAG_HAS_VISIBLE_REGION} for
317         * example.
318         */
319        public Builder setInsertionMarkerLocation(final float horizontalPosition,
320                final float lineTop, final float lineBaseline, final float lineBottom,
321                final int flags){
322            mInsertionMarkerHorizontal = horizontalPosition;
323            mInsertionMarkerTop = lineTop;
324            mInsertionMarkerBaseline = lineBaseline;
325            mInsertionMarkerBottom = lineBottom;
326            mInsertionMarkerFlags = flags;
327            return this;
328        }
329
330        /**
331         * Adds the bounding box of the character specified with the index.
332         *
333         * @param index index of the character in Java chars units. Must be specified in
334         * ascending order across successive calls.
335         * @param left x coordinate of the left edge of the character in local coordinates.
336         * @param top y coordinate of the top edge of the character in local coordinates.
337         * @param right x coordinate of the right edge of the character in local coordinates.
338         * @param bottom y coordinate of the bottom edge of the character in local coordinates.
339         * @param flags flags for this character bounds. See {@link #FLAG_HAS_VISIBLE_REGION},
340         * {@link #FLAG_HAS_INVISIBLE_REGION} and {@link #FLAG_IS_RTL}. These flags must be
341         * specified when necessary.
342         * @throws IllegalArgumentException If the index is a negative value, or not greater than
343         * all of the previously called indices.
344         */
345        public Builder addCharacterBounds(final int index, final float left, final float top,
346                final float right, final float bottom, final int flags) {
347            if (index < 0) {
348                throw new IllegalArgumentException("index must not be a negative integer.");
349            }
350            if (mCharacterBoundsArrayBuilder == null) {
351                mCharacterBoundsArrayBuilder = new SparseRectFArrayBuilder();
352            }
353            mCharacterBoundsArrayBuilder.append(index, left, top, right, bottom, flags);
354            return this;
355        }
356
357        /**
358         * Sets the matrix that transforms local coordinates into screen coordinates.
359         * @param matrix transformation matrix from local coordinates into screen coordinates. null
360         * is interpreted as an identity matrix.
361         */
362        public Builder setMatrix(final Matrix matrix) {
363            if (mMatrixValues == null) {
364                mMatrixValues = new float[9];
365            }
366            (matrix != null ? matrix : Matrix.IDENTITY_MATRIX).getValues(mMatrixValues);
367            mMatrixInitialized = true;
368            return this;
369        }
370
371        /**
372         * @return {@link CursorAnchorInfo} using parameters in this {@link Builder}.
373         * @throws IllegalArgumentException if one or more positional parameters are specified but
374         * the coordinate transformation matrix is not provided via {@link #setMatrix(Matrix)}.
375         */
376        public CursorAnchorInfo build() {
377            if (!mMatrixInitialized) {
378                // Coordinate transformation matrix is mandatory when at least one positional
379                // parameter is specified.
380                final boolean hasCharacterBounds = (mCharacterBoundsArrayBuilder != null
381                        && !mCharacterBoundsArrayBuilder.isEmpty());
382                if (hasCharacterBounds
383                        || !Float.isNaN(mInsertionMarkerHorizontal)
384                        || !Float.isNaN(mInsertionMarkerTop)
385                        || !Float.isNaN(mInsertionMarkerBaseline)
386                        || !Float.isNaN(mInsertionMarkerBottom)) {
387                    throw new IllegalArgumentException("Coordinate transformation matrix is " +
388                            "required when positional parameters are specified.");
389                }
390            }
391            return new CursorAnchorInfo(this);
392        }
393
394        /**
395         * Resets the internal state so that this instance can be reused to build another
396         * instance of {@link CursorAnchorInfo}.
397         */
398        public void reset() {
399            mSelectionStart = -1;
400            mSelectionEnd = -1;
401            mComposingTextStart = -1;
402            mComposingText = null;
403            mInsertionMarkerFlags = 0;
404            mInsertionMarkerHorizontal = Float.NaN;
405            mInsertionMarkerTop = Float.NaN;
406            mInsertionMarkerBaseline = Float.NaN;
407            mInsertionMarkerBottom = Float.NaN;
408            mMatrixInitialized = false;
409            if (mCharacterBoundsArrayBuilder != null) {
410                mCharacterBoundsArrayBuilder.reset();
411            }
412        }
413    }
414
415    private CursorAnchorInfo(final Builder builder) {
416        mSelectionStart = builder.mSelectionStart;
417        mSelectionEnd = builder.mSelectionEnd;
418        mComposingTextStart = builder.mComposingTextStart;
419        mComposingText = builder.mComposingText;
420        mInsertionMarkerFlags = builder.mInsertionMarkerFlags;
421        mInsertionMarkerHorizontal = builder.mInsertionMarkerHorizontal;
422        mInsertionMarkerTop = builder.mInsertionMarkerTop;
423        mInsertionMarkerBaseline = builder.mInsertionMarkerBaseline;
424        mInsertionMarkerBottom = builder.mInsertionMarkerBottom;
425        mCharacterBoundsArray = builder.mCharacterBoundsArrayBuilder != null ?
426                builder.mCharacterBoundsArrayBuilder.build() : null;
427        mMatrixValues = new float[9];
428        if (builder.mMatrixInitialized) {
429            System.arraycopy(builder.mMatrixValues, 0, mMatrixValues, 0, 9);
430        } else {
431            Matrix.IDENTITY_MATRIX.getValues(mMatrixValues);
432        }
433
434        // To keep hash function simple, we only use some complex objects for hash.
435        int hash = Objects.hashCode(mComposingText);
436        hash *= 31;
437        hash += Arrays.hashCode(mMatrixValues);
438        mHashCode = hash;
439    }
440
441    /**
442     * Returns the index where the selection starts.
443     * @return {@code -1} if there is no selection.
444     */
445    public int getSelectionStart() {
446        return mSelectionStart;
447    }
448
449    /**
450     * Returns the index where the selection ends.
451     * @return {@code -1} if there is no selection.
452     */
453    public int getSelectionEnd() {
454        return mSelectionEnd;
455    }
456
457    /**
458     * Returns the index where the composing text starts.
459     * @return {@code -1} if there is no composing text.
460     */
461    public int getComposingTextStart() {
462        return mComposingTextStart;
463    }
464
465    /**
466     * Returns the entire composing text.
467     * @return {@code null} if there is no composition.
468     */
469    public CharSequence getComposingText() {
470        return mComposingText;
471    }
472
473    /**
474     * Returns the flag of the insertion marker.
475     * @return the flag of the insertion marker. {@code 0} if no flag is specified.
476     */
477    public int getInsertionMarkerFlags() {
478        return mInsertionMarkerFlags;
479    }
480
481    /**
482     * Returns the horizontal start of the insertion marker, in the local coordinates that will
483     * be transformed with {@link #getMatrix()} when rendered on the screen.
484     * @return x coordinate that is compatible with {@link Layout#getPrimaryHorizontal(int)}.
485     * Pay special care to RTL/LTR handling.
486     * {@code java.lang.Float.NaN} if not specified.
487     * @see Layout#getPrimaryHorizontal(int)
488     */
489    public float getInsertionMarkerHorizontal() {
490        return mInsertionMarkerHorizontal;
491    }
492
493    /**
494     * Returns the vertical top position of the insertion marker, in the local coordinates that
495     * will be transformed with {@link #getMatrix()} when rendered on the screen.
496     * @return y coordinate that is compatible with {@link Layout#getLineTop(int)}.
497     * {@code java.lang.Float.NaN} if not specified.
498     */
499    public float getInsertionMarkerTop() {
500        return mInsertionMarkerTop;
501    }
502
503    /**
504     * Returns the vertical baseline position of the insertion marker, in the local coordinates
505     * that will be transformed with {@link #getMatrix()} when rendered on the screen.
506     * @return y coordinate that is compatible with {@link Layout#getLineBaseline(int)}.
507     * {@code java.lang.Float.NaN} if not specified.
508     */
509    public float getInsertionMarkerBaseline() {
510        return mInsertionMarkerBaseline;
511    }
512
513    /**
514     * Returns the vertical bottom position of the insertion marker, in the local coordinates
515     * that will be transformed with {@link #getMatrix()} when rendered on the screen.
516     * @return y coordinate that is compatible with {@link Layout#getLineBottom(int)}.
517     * {@code java.lang.Float.NaN} if not specified.
518     */
519    public float getInsertionMarkerBottom() {
520        return mInsertionMarkerBottom;
521    }
522
523    /**
524     * Returns a new instance of {@link RectF} that indicates the location of the character
525     * specified with the index.
526     * @param index index of the character in a Java chars.
527     * @return the character bounds in local coordinates as a new instance of {@link RectF}.
528     */
529    public RectF getCharacterBounds(final int index) {
530        if (mCharacterBoundsArray == null) {
531            return null;
532        }
533        return mCharacterBoundsArray.get(index);
534    }
535
536    /**
537     * Returns the flags associated with the character bounds specified with the index.
538     * @param index index of the character in a Java chars.
539     * @return {@code 0} if no flag is specified.
540     */
541    public int getCharacterBoundsFlags(final int index) {
542        if (mCharacterBoundsArray == null) {
543            return 0;
544        }
545        return mCharacterBoundsArray.getFlags(index, 0);
546    }
547
548    /**
549     * Returns a new instance of {@link android.graphics.Matrix} that indicates the transformation
550     * matrix that is to be applied other positional data in this class.
551     * @return a new instance (copy) of the transformation matrix.
552     */
553    public Matrix getMatrix() {
554        final Matrix matrix = new Matrix();
555        matrix.setValues(mMatrixValues);
556        return matrix;
557    }
558
559    /**
560     * Used to make this class parcelable.
561     */
562    public static final Parcelable.Creator<CursorAnchorInfo> CREATOR
563            = new Parcelable.Creator<CursorAnchorInfo>() {
564        @Override
565        public CursorAnchorInfo createFromParcel(Parcel source) {
566            return new CursorAnchorInfo(source);
567        }
568
569        @Override
570        public CursorAnchorInfo[] newArray(int size) {
571            return new CursorAnchorInfo[size];
572        }
573    };
574
575    @Override
576    public int describeContents() {
577        return 0;
578    }
579}
580