1 /*
2  * Copyright (C) 2017 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.text;
18 
19 import android.annotation.FloatRange;
20 import android.annotation.IntRange;
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.graphics.Rect;
24 import android.text.style.MetricAffectingSpan;
25 
26 import com.android.internal.util.Preconditions;
27 
28 import java.util.ArrayList;
29 import java.util.Objects;
30 
31 /**
32  * A text which has the character metrics data.
33  *
34  * A text object that contains the character metrics data and can be used to improve the performance
35  * of text layout operations. When a PrecomputedText is created with a given {@link CharSequence},
36  * it will measure the text metrics during the creation. This PrecomputedText instance can be set on
37  * {@link android.widget.TextView} or {@link StaticLayout}. Since the text layout information will
38  * be included in this instance, {@link android.widget.TextView} or {@link StaticLayout} will not
39  * have to recalculate this information.
40  *
41  * Note that the {@link PrecomputedText} created from different parameters of the target {@link
42  * android.widget.TextView} will be rejected internally and compute the text layout again with the
43  * current {@link android.widget.TextView} parameters.
44  *
45  * <pre>
46  * An example usage is:
47  * <code>
48  *  static void asyncSetText(TextView textView, final String longString, Executor bgExecutor) {
49  *      // construct precompute related parameters using the TextView that we will set the text on.
50  *      final PrecomputedText.Params params = textView.getTextMetricsParams();
51  *      final Reference textViewRef = new WeakReference<>(textView);
52  *      bgExecutor.submit(() -> {
53  *          TextView textView = textViewRef.get();
54  *          if (textView == null) return;
55  *          final PrecomputedText precomputedText = PrecomputedText.create(longString, params);
56  *          textView.post(() -> {
57  *              TextView textView = textViewRef.get();
58  *              if (textView == null) return;
59  *              textView.setText(precomputedText);
60  *          });
61  *      });
62  *  }
63  * </code>
64  * </pre>
65  *
66  * Note that the {@link PrecomputedText} created from different parameters of the target
67  * {@link android.widget.TextView} will be rejected.
68  *
69  * Note that any {@link android.text.NoCopySpan} attached to the original text won't be passed to
70  * PrecomputedText.
71  */
72 public class PrecomputedText implements Spannable {
73     private static final char LINE_FEED = '\n';
74 
75     /**
76      * The information required for building {@link PrecomputedText}.
77      *
78      * Contains information required for precomputing text measurement metadata, so it can be done
79      * in isolation of a {@link android.widget.TextView} or {@link StaticLayout}, when final layout
80      * constraints are not known.
81      */
82     public static final class Params {
83         // The TextPaint used for measurement.
84         private final @NonNull TextPaint mPaint;
85 
86         // The requested text direction.
87         private final @NonNull TextDirectionHeuristic mTextDir;
88 
89         // The break strategy for this measured text.
90         private final @Layout.BreakStrategy int mBreakStrategy;
91 
92         // The hyphenation frequency for this measured text.
93         private final @Layout.HyphenationFrequency int mHyphenationFrequency;
94 
95         /**
96          * A builder for creating {@link Params}.
97          */
98         public static class Builder {
99             // The TextPaint used for measurement.
100             private final @NonNull TextPaint mPaint;
101 
102             // The requested text direction.
103             private TextDirectionHeuristic mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR;
104 
105             // The break strategy for this measured text.
106             private @Layout.BreakStrategy int mBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY;
107 
108             // The hyphenation frequency for this measured text.
109             private @Layout.HyphenationFrequency int mHyphenationFrequency =
110                     Layout.HYPHENATION_FREQUENCY_NORMAL;
111 
112             /**
113              * Builder constructor.
114              *
115              * @param paint the paint to be used for drawing
116              */
Builder(@onNull TextPaint paint)117             public Builder(@NonNull TextPaint paint) {
118                 mPaint = paint;
119             }
120 
121             /**
122              * Set the line break strategy.
123              *
124              * The default value is {@link Layout#BREAK_STRATEGY_HIGH_QUALITY}.
125              *
126              * @param strategy the break strategy
127              * @return this builder, useful for chaining
128              * @see StaticLayout.Builder#setBreakStrategy
129              * @see android.widget.TextView#setBreakStrategy
130              */
setBreakStrategy(@ayout.BreakStrategy int strategy)131             public Builder setBreakStrategy(@Layout.BreakStrategy int strategy) {
132                 mBreakStrategy = strategy;
133                 return this;
134             }
135 
136             /**
137              * Set the hyphenation frequency.
138              *
139              * The default value is {@link Layout#HYPHENATION_FREQUENCY_NORMAL}.
140              *
141              * @param frequency the hyphenation frequency
142              * @return this builder, useful for chaining
143              * @see StaticLayout.Builder#setHyphenationFrequency
144              * @see android.widget.TextView#setHyphenationFrequency
145              */
setHyphenationFrequency(@ayout.HyphenationFrequency int frequency)146             public Builder setHyphenationFrequency(@Layout.HyphenationFrequency int frequency) {
147                 mHyphenationFrequency = frequency;
148                 return this;
149             }
150 
151             /**
152              * Set the text direction heuristic.
153              *
154              * The default value is {@link TextDirectionHeuristics#FIRSTSTRONG_LTR}.
155              *
156              * @param textDir the text direction heuristic for resolving bidi behavior
157              * @return this builder, useful for chaining
158              * @see StaticLayout.Builder#setTextDirection
159              */
setTextDirection(@onNull TextDirectionHeuristic textDir)160             public Builder setTextDirection(@NonNull TextDirectionHeuristic textDir) {
161                 mTextDir = textDir;
162                 return this;
163             }
164 
165             /**
166              * Build the {@link Params}.
167              *
168              * @return the layout parameter
169              */
build()170             public @NonNull Params build() {
171                 return new Params(mPaint, mTextDir, mBreakStrategy, mHyphenationFrequency);
172             }
173         }
174 
175         // This is public hidden for internal use.
176         // For the external developers, use Builder instead.
177         /** @hide */
Params(@onNull TextPaint paint, @NonNull TextDirectionHeuristic textDir, @Layout.BreakStrategy int strategy, @Layout.HyphenationFrequency int frequency)178         public Params(@NonNull TextPaint paint, @NonNull TextDirectionHeuristic textDir,
179                 @Layout.BreakStrategy int strategy, @Layout.HyphenationFrequency int frequency) {
180             mPaint = paint;
181             mTextDir = textDir;
182             mBreakStrategy = strategy;
183             mHyphenationFrequency = frequency;
184         }
185 
186         /**
187          * Returns the {@link TextPaint} for this text.
188          *
189          * @return A {@link TextPaint}
190          */
getTextPaint()191         public @NonNull TextPaint getTextPaint() {
192             return mPaint;
193         }
194 
195         /**
196          * Returns the {@link TextDirectionHeuristic} for this text.
197          *
198          * @return A {@link TextDirectionHeuristic}
199          */
getTextDirection()200         public @NonNull TextDirectionHeuristic getTextDirection() {
201             return mTextDir;
202         }
203 
204         /**
205          * Returns the break strategy for this text.
206          *
207          * @return A line break strategy
208          */
getBreakStrategy()209         public @Layout.BreakStrategy int getBreakStrategy() {
210             return mBreakStrategy;
211         }
212 
213         /**
214          * Returns the hyphenation frequency for this text.
215          *
216          * @return A hyphenation frequency
217          */
getHyphenationFrequency()218         public @Layout.HyphenationFrequency int getHyphenationFrequency() {
219             return mHyphenationFrequency;
220         }
221 
222         /** @hide */
isSameTextMetricsInternal(@onNull TextPaint paint, @NonNull TextDirectionHeuristic textDir, @Layout.BreakStrategy int strategy, @Layout.HyphenationFrequency int frequency)223         public boolean isSameTextMetricsInternal(@NonNull TextPaint paint,
224                 @NonNull TextDirectionHeuristic textDir, @Layout.BreakStrategy int strategy,
225                 @Layout.HyphenationFrequency int frequency) {
226             return mTextDir == textDir
227                 && mBreakStrategy == strategy
228                 && mHyphenationFrequency == frequency
229                 && mPaint.equalsForTextMeasurement(paint);
230         }
231 
232         /**
233          * Check if the same text layout.
234          *
235          * @return true if this and the given param result in the same text layout
236          */
237         @Override
equals(@ullable Object o)238         public boolean equals(@Nullable Object o) {
239             if (o == this) {
240                 return true;
241             }
242             if (o == null || !(o instanceof Params)) {
243                 return false;
244             }
245             Params param = (Params) o;
246             return isSameTextMetricsInternal(param.mPaint, param.mTextDir, param.mBreakStrategy,
247                     param.mHyphenationFrequency);
248         }
249 
250         @Override
hashCode()251         public int hashCode() {
252             // TODO: implement MinikinPaint::hashCode and use it to keep consistency with equals.
253             return Objects.hash(mPaint.getTextSize(), mPaint.getTextScaleX(), mPaint.getTextSkewX(),
254                     mPaint.getLetterSpacing(), mPaint.getWordSpacing(), mPaint.getFlags(),
255                     mPaint.getTextLocales(), mPaint.getTypeface(),
256                     mPaint.getFontVariationSettings(), mPaint.isElegantTextHeight(), mTextDir,
257                     mBreakStrategy, mHyphenationFrequency);
258         }
259 
260         @Override
toString()261         public String toString() {
262             return "{"
263                 + "textSize=" + mPaint.getTextSize()
264                 + ", textScaleX=" + mPaint.getTextScaleX()
265                 + ", textSkewX=" + mPaint.getTextSkewX()
266                 + ", letterSpacing=" + mPaint.getLetterSpacing()
267                 + ", textLocale=" + mPaint.getTextLocales()
268                 + ", typeface=" + mPaint.getTypeface()
269                 + ", variationSettings=" + mPaint.getFontVariationSettings()
270                 + ", elegantTextHeight=" + mPaint.isElegantTextHeight()
271                 + ", textDir=" + mTextDir
272                 + ", breakStrategy=" + mBreakStrategy
273                 + ", hyphenationFrequency=" + mHyphenationFrequency
274                 + "}";
275         }
276     };
277 
278     /** @hide */
279     public static class ParagraphInfo {
280         public final @IntRange(from = 0) int paragraphEnd;
281         public final @NonNull MeasuredParagraph measured;
282 
283         /**
284          * @param paraEnd the end offset of this paragraph
285          * @param measured a measured paragraph
286          */
ParagraphInfo(@ntRangefrom = 0) int paraEnd, @NonNull MeasuredParagraph measured)287         public ParagraphInfo(@IntRange(from = 0) int paraEnd, @NonNull MeasuredParagraph measured) {
288             this.paragraphEnd = paraEnd;
289             this.measured = measured;
290         }
291     };
292 
293 
294     // The original text.
295     private final @NonNull SpannableString mText;
296 
297     // The inclusive start offset of the measuring target.
298     private final @IntRange(from = 0) int mStart;
299 
300     // The exclusive end offset of the measuring target.
301     private final @IntRange(from = 0) int mEnd;
302 
303     private final @NonNull Params mParams;
304 
305     // The list of measured paragraph info.
306     private final @NonNull ParagraphInfo[] mParagraphInfo;
307 
308     /**
309      * Create a new {@link PrecomputedText} which will pre-compute text measurement and glyph
310      * positioning information.
311      * <p>
312      * This can be expensive, so computing this on a background thread before your text will be
313      * presented can save work on the UI thread.
314      * </p>
315      *
316      * Note that any {@link android.text.NoCopySpan} attached to the text won't be passed to the
317      * created PrecomputedText.
318      *
319      * @param text the text to be measured
320      * @param params parameters that define how text will be precomputed
321      * @return A {@link PrecomputedText}
322      */
create(@onNull CharSequence text, @NonNull Params params)323     public static PrecomputedText create(@NonNull CharSequence text, @NonNull Params params) {
324         ParagraphInfo[] paraInfo = createMeasuredParagraphs(
325                 text, params, 0, text.length(), true /* computeLayout */);
326         return new PrecomputedText(text, 0, text.length(), params, paraInfo);
327     }
328 
329     /** @hide */
createMeasuredParagraphs( @onNull CharSequence text, @NonNull Params params, @IntRange(from = 0) int start, @IntRange(from = 0) int end, boolean computeLayout)330     public static ParagraphInfo[] createMeasuredParagraphs(
331             @NonNull CharSequence text, @NonNull Params params,
332             @IntRange(from = 0) int start, @IntRange(from = 0) int end, boolean computeLayout) {
333         ArrayList<ParagraphInfo> result = new ArrayList<>();
334 
335         Preconditions.checkNotNull(text);
336         Preconditions.checkNotNull(params);
337         final boolean needHyphenation = params.getBreakStrategy() != Layout.BREAK_STRATEGY_SIMPLE
338                 && params.getHyphenationFrequency() != Layout.HYPHENATION_FREQUENCY_NONE;
339 
340         int paraEnd = 0;
341         for (int paraStart = start; paraStart < end; paraStart = paraEnd) {
342             paraEnd = TextUtils.indexOf(text, LINE_FEED, paraStart, end);
343             if (paraEnd < 0) {
344                 // No LINE_FEED(U+000A) character found. Use end of the text as the paragraph
345                 // end.
346                 paraEnd = end;
347             } else {
348                 paraEnd++;  // Includes LINE_FEED(U+000A) to the prev paragraph.
349             }
350 
351             result.add(new ParagraphInfo(paraEnd, MeasuredParagraph.buildForStaticLayout(
352                     params.getTextPaint(), text, paraStart, paraEnd, params.getTextDirection(),
353                     needHyphenation, computeLayout, null /* no recycle */)));
354         }
355         return result.toArray(new ParagraphInfo[result.size()]);
356     }
357 
358     // Use PrecomputedText.create instead.
PrecomputedText(@onNull CharSequence text, @IntRange(from = 0) int start, @IntRange(from = 0) int end, @NonNull Params params, @NonNull ParagraphInfo[] paraInfo)359     private PrecomputedText(@NonNull CharSequence text, @IntRange(from = 0) int start,
360             @IntRange(from = 0) int end, @NonNull Params params,
361             @NonNull ParagraphInfo[] paraInfo) {
362         mText = new SpannableString(text, true /* ignoreNoCopySpan */);
363         mStart = start;
364         mEnd = end;
365         mParams = params;
366         mParagraphInfo = paraInfo;
367     }
368 
369     /**
370      * Return the underlying text.
371      * @hide
372      */
getText()373     public @NonNull CharSequence getText() {
374         return mText;
375     }
376 
377     /**
378      * Returns the inclusive start offset of measured region.
379      * @hide
380      */
getStart()381     public @IntRange(from = 0) int getStart() {
382         return mStart;
383     }
384 
385     /**
386      * Returns the exclusive end offset of measured region.
387      * @hide
388      */
getEnd()389     public @IntRange(from = 0) int getEnd() {
390         return mEnd;
391     }
392 
393     /**
394      * Returns the layout parameters used to measure this text.
395      */
getParams()396     public @NonNull Params getParams() {
397         return mParams;
398     }
399 
400     /**
401      * Returns the count of paragraphs.
402      */
getParagraphCount()403     public @IntRange(from = 0) int getParagraphCount() {
404         return mParagraphInfo.length;
405     }
406 
407     /**
408      * Returns the paragraph start offset of the text.
409      */
getParagraphStart(@ntRangefrom = 0) int paraIndex)410     public @IntRange(from = 0) int getParagraphStart(@IntRange(from = 0) int paraIndex) {
411         Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex");
412         return paraIndex == 0 ? mStart : getParagraphEnd(paraIndex - 1);
413     }
414 
415     /**
416      * Returns the paragraph end offset of the text.
417      */
getParagraphEnd(@ntRangefrom = 0) int paraIndex)418     public @IntRange(from = 0) int getParagraphEnd(@IntRange(from = 0) int paraIndex) {
419         Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex");
420         return mParagraphInfo[paraIndex].paragraphEnd;
421     }
422 
423     /** @hide */
getMeasuredParagraph(@ntRangefrom = 0) int paraIndex)424     public @NonNull MeasuredParagraph getMeasuredParagraph(@IntRange(from = 0) int paraIndex) {
425         return mParagraphInfo[paraIndex].measured;
426     }
427 
428     /** @hide */
getParagraphInfo()429     public @NonNull ParagraphInfo[] getParagraphInfo() {
430         return mParagraphInfo;
431     }
432 
433     /**
434      * Returns true if the given TextPaint gives the same result of text layout for this text.
435      * @hide
436      */
canUseMeasuredResult(@ntRangefrom = 0) int start, @IntRange(from = 0) int end, @NonNull TextDirectionHeuristic textDir, @NonNull TextPaint paint, @Layout.BreakStrategy int strategy, @Layout.HyphenationFrequency int frequency)437     public boolean canUseMeasuredResult(@IntRange(from = 0) int start, @IntRange(from = 0) int end,
438             @NonNull TextDirectionHeuristic textDir, @NonNull TextPaint paint,
439             @Layout.BreakStrategy int strategy, @Layout.HyphenationFrequency int frequency) {
440         final TextPaint mtPaint = mParams.getTextPaint();
441         return mStart == start
442             && mEnd == end
443             && mParams.isSameTextMetricsInternal(paint, textDir, strategy, frequency);
444     }
445 
446     /** @hide */
findParaIndex(@ntRangefrom = 0) int pos)447     public int findParaIndex(@IntRange(from = 0) int pos) {
448         // TODO: Maybe good to remove paragraph concept from PrecomputedText and add substring
449         //       layout support to StaticLayout.
450         for (int i = 0; i < mParagraphInfo.length; ++i) {
451             if (pos < mParagraphInfo[i].paragraphEnd) {
452                 return i;
453             }
454         }
455         throw new IndexOutOfBoundsException(
456             "pos must be less than " + mParagraphInfo[mParagraphInfo.length - 1].paragraphEnd
457             + ", gave " + pos);
458     }
459 
460     /**
461      * Returns text width for the given range.
462      * Both {@code start} and {@code end} offset need to be in the same paragraph, otherwise
463      * IllegalArgumentException will be thrown.
464      *
465      * @param start the inclusive start offset in the text
466      * @param end the exclusive end offset in the text
467      * @return the text width
468      * @throws IllegalArgumentException if start and end offset are in the different paragraph.
469      */
getWidth(@ntRangefrom = 0) int start, @IntRange(from = 0) int end)470     public @FloatRange(from = 0) float getWidth(@IntRange(from = 0) int start,
471             @IntRange(from = 0) int end) {
472         Preconditions.checkArgument(0 <= start && start <= mText.length(), "invalid start offset");
473         Preconditions.checkArgument(0 <= end && end <= mText.length(), "invalid end offset");
474         Preconditions.checkArgument(start <= end, "start offset can not be larger than end offset");
475 
476         if (start == end) {
477             return 0;
478         }
479         final int paraIndex = findParaIndex(start);
480         final int paraStart = getParagraphStart(paraIndex);
481         final int paraEnd = getParagraphEnd(paraIndex);
482         if (start < paraStart || paraEnd < end) {
483             throw new IllegalArgumentException("Cannot measured across the paragraph:"
484                 + "para: (" + paraStart + ", " + paraEnd + "), "
485                 + "request: (" + start + ", " + end + ")");
486         }
487         return getMeasuredParagraph(paraIndex).getWidth(start - paraStart, end - paraStart);
488     }
489 
490     /**
491      * Retrieves the text bounding box for the given range.
492      * Both {@code start} and {@code end} offset need to be in the same paragraph, otherwise
493      * IllegalArgumentException will be thrown.
494      *
495      * @param start the inclusive start offset in the text
496      * @param end the exclusive end offset in the text
497      * @param bounds the output rectangle
498      * @throws IllegalArgumentException if start and end offset are in the different paragraph.
499      */
getBounds(@ntRangefrom = 0) int start, @IntRange(from = 0) int end, @NonNull Rect bounds)500     public void getBounds(@IntRange(from = 0) int start, @IntRange(from = 0) int end,
501             @NonNull Rect bounds) {
502         Preconditions.checkArgument(0 <= start && start <= mText.length(), "invalid start offset");
503         Preconditions.checkArgument(0 <= end && end <= mText.length(), "invalid end offset");
504         Preconditions.checkArgument(start <= end, "start offset can not be larger than end offset");
505         Preconditions.checkNotNull(bounds);
506         if (start == end) {
507             bounds.set(0, 0, 0, 0);
508             return;
509         }
510         final int paraIndex = findParaIndex(start);
511         final int paraStart = getParagraphStart(paraIndex);
512         final int paraEnd = getParagraphEnd(paraIndex);
513         if (start < paraStart || paraEnd < end) {
514             throw new IllegalArgumentException("Cannot measured across the paragraph:"
515                 + "para: (" + paraStart + ", " + paraEnd + "), "
516                 + "request: (" + start + ", " + end + ")");
517         }
518         getMeasuredParagraph(paraIndex).getBounds(start - paraStart, end - paraStart, bounds);
519     }
520 
521     /**
522      * Returns the size of native PrecomputedText memory usage.
523      *
524      * Note that this is not guaranteed to be accurate. Must be used only for testing purposes.
525      * @hide
526      */
getMemoryUsage()527     public int getMemoryUsage() {
528         int r = 0;
529         for (int i = 0; i < getParagraphCount(); ++i) {
530             r += getMeasuredParagraph(i).getMemoryUsage();
531         }
532         return r;
533     }
534 
535     ///////////////////////////////////////////////////////////////////////////////////////////////
536     // Spannable overrides
537     //
538     // Do not allow to modify MetricAffectingSpan
539 
540     /**
541      * @throws IllegalArgumentException if {@link MetricAffectingSpan} is specified.
542      */
543     @Override
setSpan(Object what, int start, int end, int flags)544     public void setSpan(Object what, int start, int end, int flags) {
545         if (what instanceof MetricAffectingSpan) {
546             throw new IllegalArgumentException(
547                     "MetricAffectingSpan can not be set to PrecomputedText.");
548         }
549         mText.setSpan(what, start, end, flags);
550     }
551 
552     /**
553      * @throws IllegalArgumentException if {@link MetricAffectingSpan} is specified.
554      */
555     @Override
removeSpan(Object what)556     public void removeSpan(Object what) {
557         if (what instanceof MetricAffectingSpan) {
558             throw new IllegalArgumentException(
559                     "MetricAffectingSpan can not be removed from PrecomputedText.");
560         }
561         mText.removeSpan(what);
562     }
563 
564     ///////////////////////////////////////////////////////////////////////////////////////////////
565     // Spanned overrides
566     //
567     // Just proxy for underlying mText if appropriate.
568 
569     @Override
getSpans(int start, int end, Class<T> type)570     public <T> T[] getSpans(int start, int end, Class<T> type) {
571         return mText.getSpans(start, end, type);
572     }
573 
574     @Override
getSpanStart(Object tag)575     public int getSpanStart(Object tag) {
576         return mText.getSpanStart(tag);
577     }
578 
579     @Override
getSpanEnd(Object tag)580     public int getSpanEnd(Object tag) {
581         return mText.getSpanEnd(tag);
582     }
583 
584     @Override
getSpanFlags(Object tag)585     public int getSpanFlags(Object tag) {
586         return mText.getSpanFlags(tag);
587     }
588 
589     @Override
nextSpanTransition(int start, int limit, Class type)590     public int nextSpanTransition(int start, int limit, Class type) {
591         return mText.nextSpanTransition(start, limit, type);
592     }
593 
594     ///////////////////////////////////////////////////////////////////////////////////////////////
595     // CharSequence overrides.
596     //
597     // Just proxy for underlying mText.
598 
599     @Override
length()600     public int length() {
601         return mText.length();
602     }
603 
604     @Override
charAt(int index)605     public char charAt(int index) {
606         return mText.charAt(index);
607     }
608 
609     @Override
subSequence(int start, int end)610     public CharSequence subSequence(int start, int end) {
611         return PrecomputedText.create(mText.subSequence(start, end), mParams);
612     }
613 
614     @Override
toString()615     public String toString() {
616         return mText.toString();
617     }
618 }
619