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