content: /*
   * Copyright (C) 2017 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package android.text;
  
  import android.annotation.FloatRange;
  import android.annotation.IntDef;
  import android.annotation.IntRange;
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.graphics.Rect;
  import android.text.style.MetricAffectingSpan;
  
  import com.android.internal.util.Preconditions;
  
  import java.lang.annotation.Retention;
  import java.lang.annotation.RetentionPolicy;
  import java.util.ArrayList;
  import java.util.Objects;
  
  /**
   * A text which has the character metrics data.
   *
   * A text object that contains the character metrics data and can be used to improve the performance
   * of text layout operations. When a PrecomputedText is created with a given {@link CharSequence},
   * it will measure the text metrics during the creation. This PrecomputedText instance can be set on
   * {@link android.widget.TextView} or {@link StaticLayout}. Since the text layout information will
   * be included in this instance, {@link android.widget.TextView} or {@link StaticLayout} will not
   * have to recalculate this information.
   *
   * Note that the {@link PrecomputedText} created from different parameters of the target {@link
   * android.widget.TextView} will be rejected internally and compute the text layout again with the
   * current {@link android.widget.TextView} parameters.
   *
   * <pre>
   * An example usage is:
   * <code>
   *  static void asyncSetText(TextView textView, final String longString, Executor bgExecutor) {
   *      // construct precompute related parameters using the TextView that we will set the text on.
   *      final PrecomputedText.Params params = textView.getTextMetricsParams();
   *      final Reference textViewRef = new WeakReference<>(textView);
   *      bgExecutor.submit(() -> {
   *          TextView textView = textViewRef.get();
   *          if (textView == null) return;
   *          final PrecomputedText precomputedText = PrecomputedText.create(longString, params);
   *          textView.post(() -> {
   *              TextView textView = textViewRef.get();
   *              if (textView == null) return;
   *              textView.setText(precomputedText);
   *          });
   *      });
   *  }
   * </code>
   * </pre>
   *
   * Note that the {@link PrecomputedText} created from different parameters of the target
   * {@link android.widget.TextView} will be rejected.
   *
   * Note that any {@link android.text.NoCopySpan} attached to the original text won't be passed to
   * PrecomputedText.
   */
  public class PrecomputedText implements Spannable {
      private static final char LINE_FEED = '\n';
  
      /**
       * The information required for building {@link PrecomputedText}.
       *
       * Contains information required for precomputing text measurement metadata, so it can be done
       * in isolation of a {@link android.widget.TextView} or {@link StaticLayout}, when final layout
       * constraints are not known.
       */
      public static final class Params {
          // The TextPaint used for measurement.
          private final @NonNull TextPaint mPaint;
  
          // The requested text direction.
          private final @NonNull TextDirectionHeuristic mTextDir;
  
          // The break strategy for this measured text.
          private final @Layout.BreakStrategy int mBreakStrategy;
  
          // The hyphenation frequency for this measured text.
          private final @Layout.HyphenationFrequency int mHyphenationFrequency;
  
          /**
           * A builder for creating {@link Params}.
           */
          public static class Builder {
              // The TextPaint used for measurement.
              private final @NonNull TextPaint mPaint;
  
              // The requested text direction.
              private TextDirectionHeuristic mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR;
  
              // The break strategy for this measured text.
              private @Layout.BreakStrategy int mBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY;
  
              // The hyphenation frequency for this measured text.
              private @Layout.HyphenationFrequency int mHyphenationFrequency =
                      Layout.HYPHENATION_FREQUENCY_NORMAL;
  
              /**
               * Builder constructor.
               *
               * @param paint the paint to be used for drawing
               */
              public Builder(@NonNull TextPaint paint) {
                  mPaint = paint;
              }
  
              /**
               * Builder constructor from existing params.
               */
              public Builder(@NonNull Params params) {
                  mPaint = params.mPaint;
                  mTextDir = params.mTextDir;
                  mBreakStrategy = params.mBreakStrategy;
                  mHyphenationFrequency = params.mHyphenationFrequency;
              }
  
              /**
               * Set the line break strategy.
               *
               * The default value is {@link Layout#BREAK_STRATEGY_HIGH_QUALITY}.
               *
               * @param strategy the break strategy
               * @return this builder, useful for chaining
               * @see StaticLayout.Builder#setBreakStrategy
               * @see android.widget.TextView#setBreakStrategy
               */
              public Builder setBreakStrategy(@Layout.BreakStrategy int strategy) {
                  mBreakStrategy = strategy;
                  return this;
              }
  
              /**
               * Set the hyphenation frequency.
               *
               * The default value is {@link Layout#HYPHENATION_FREQUENCY_NORMAL}.
               *
               * @param frequency the hyphenation frequency
               * @return this builder, useful for chaining
               * @see StaticLayout.Builder#setHyphenationFrequency
               * @see android.widget.TextView#setHyphenationFrequency
               */
              public Builder setHyphenationFrequency(@Layout.HyphenationFrequency int frequency) {
                  mHyphenationFrequency = frequency;
                  return this;
              }
  
              /**
               * Set the text direction heuristic.
               *
               * The default value is {@link TextDirectionHeuristics#FIRSTSTRONG_LTR}.
               *
               * @param textDir the text direction heuristic for resolving bidi behavior
               * @return this builder, useful for chaining
               * @see StaticLayout.Builder#setTextDirection
               */
              public Builder setTextDirection(@NonNull TextDirectionHeuristic textDir) {
                  mTextDir = textDir;
                  return this;
              }
  
              /**
               * Build the {@link Params}.
               *
               * @return the layout parameter
               */
              public @NonNull Params build() {
                  return new Params(mPaint, mTextDir, mBreakStrategy, mHyphenationFrequency);
              }
          }
  
          // This is public hidden for internal use.
          // For the external developers, use Builder instead.
          /** @hide */
          public Params(@NonNull TextPaint paint, @NonNull TextDirectionHeuristic textDir,
                  @Layout.BreakStrategy int strategy, @Layout.HyphenationFrequency int frequency) {
              mPaint = paint;
              mTextDir = textDir;
              mBreakStrategy = strategy;
              mHyphenationFrequency = frequency;
          }
  
          /**
           * Returns the {@link TextPaint} for this text.
           *
           * @return A {@link TextPaint}
           */
          public @NonNull TextPaint getTextPaint() {
              return mPaint;
          }
  
          /**
           * Returns the {@link TextDirectionHeuristic} for this text.
           *
           * @return A {@link TextDirectionHeuristic}
           */
          public @NonNull TextDirectionHeuristic getTextDirection() {
              return mTextDir;
          }
  
          /**
           * Returns the break strategy for this text.
           *
           * @return A line break strategy
           */
          public @Layout.BreakStrategy int getBreakStrategy() {
              return mBreakStrategy;
          }
  
          /**
           * Returns the hyphenation frequency for this text.
           *
           * @return A hyphenation frequency
           */
          public @Layout.HyphenationFrequency int getHyphenationFrequency() {
              return mHyphenationFrequency;
          }
  
          /** @hide */
          @IntDef(value = { UNUSABLE, NEED_RECOMPUTE, USABLE })
          @Retention(RetentionPolicy.SOURCE)
          public @interface CheckResultUsableResult {}
  
          /**
           * Constant for returning value of checkResultUsable indicating that given parameter is not
           * compatible.
           * @hide
           */
          public static final int UNUSABLE = 0;
  
          /**
           * Constant for returning value of checkResultUsable indicating that given parameter is not
           * compatible but partially usable for creating new PrecomputedText.
           * @hide
           */
          public static final int NEED_RECOMPUTE = 1;
  
          /**
           * Constant for returning value of checkResultUsable indicating that given parameter is
           * compatible.
           * @hide
           */
          public static final int USABLE = 2;
  
          /** @hide */
          public @CheckResultUsableResult int checkResultUsable(@NonNull TextPaint paint,
                  @NonNull TextDirectionHeuristic textDir, @Layout.BreakStrategy int strategy,
                  @Layout.HyphenationFrequency int frequency) {
              if (mBreakStrategy == strategy && mHyphenationFrequency == frequency
                      && mPaint.equalsForTextMeasurement(paint)) {
                  return mTextDir == textDir ? USABLE : NEED_RECOMPUTE;
              } else {
                  return UNUSABLE;
              }
          }
  
          /**
           * Check if the same text layout.
           *
           * @return true if this and the given param result in the same text layout
           */
          @Override
          public boolean equals(@Nullable Object o) {
              if (o == this) {
                  return true;
              }
              if (o == null || !(o instanceof Params)) {
                  return false;
              }
              Params param = (Params) o;
              return checkResultUsable(param.mPaint, param.mTextDir, param.mBreakStrategy,
                      param.mHyphenationFrequency) == Params.USABLE;
          }
  
          @Override
          public int hashCode() {
              // TODO: implement MinikinPaint::hashCode and use it to keep consistency with equals.
              return Objects.hash(mPaint.getTextSize(), mPaint.getTextScaleX(), mPaint.getTextSkewX(),
                      mPaint.getLetterSpacing(), mPaint.getWordSpacing(), mPaint.getFlags(),
                      mPaint.getTextLocales(), mPaint.getTypeface(),
                      mPaint.getFontVariationSettings(), mPaint.isElegantTextHeight(), mTextDir,
                      mBreakStrategy, mHyphenationFrequency);
          }
  
          @Override
          public String toString() {
              return "{"
                  + "textSize=" + mPaint.getTextSize()
                  + ", textScaleX=" + mPaint.getTextScaleX()
                  + ", textSkewX=" + mPaint.getTextSkewX()
                  + ", letterSpacing=" + mPaint.getLetterSpacing()
                  + ", textLocale=" + mPaint.getTextLocales()
                  + ", typeface=" + mPaint.getTypeface()
                  + ", variationSettings=" + mPaint.getFontVariationSettings()
                  + ", elegantTextHeight=" + mPaint.isElegantTextHeight()
                  + ", textDir=" + mTextDir
                  + ", breakStrategy=" + mBreakStrategy
                  + ", hyphenationFrequency=" + mHyphenationFrequency
                  + "}";
          }
      };
  
      /** @hide */
      public static class ParagraphInfo {
          public final @IntRange(from = 0) int paragraphEnd;
          public final @NonNull MeasuredParagraph measured;
  
          /**
           * @param paraEnd the end offset of this paragraph
           * @param measured a measured paragraph
           */
          public ParagraphInfo(@IntRange(from = 0) int paraEnd, @NonNull MeasuredParagraph measured) {
              this.paragraphEnd = paraEnd;
              this.measured = measured;
          }
      };
  
  
      // The original text.
      private final @NonNull SpannableString mText;
  
      // The inclusive start offset of the measuring target.
      private final @IntRange(from = 0) int mStart;
  
      // The exclusive end offset of the measuring target.
      private final @IntRange(from = 0) int mEnd;
  
      private final @NonNull Params mParams;
  
      // The list of measured paragraph info.
      private final @NonNull ParagraphInfo[] mParagraphInfo;
  
      /**
       * Create a new {@link PrecomputedText} which will pre-compute text measurement and glyph
       * positioning information.
       * <p>
       * This can be expensive, so computing this on a background thread before your text will be
       * presented can save work on the UI thread.
       * </p>
       *
       * Note that any {@link android.text.NoCopySpan} attached to the text won't be passed to the
       * created PrecomputedText.
       *
       * @param text the text to be measured
       * @param params parameters that define how text will be precomputed
       * @return A {@link PrecomputedText}
       */
      public static PrecomputedText create(@NonNull CharSequence text, @NonNull Params params) {
          ParagraphInfo[] paraInfo = null;
          if (text instanceof PrecomputedText) {
              final PrecomputedText hintPct = (PrecomputedText) text;
              final PrecomputedText.Params hintParams = hintPct.getParams();
              final @Params.CheckResultUsableResult int checkResult =
                      hintParams.checkResultUsable(params.mPaint, params.mTextDir,
                              params.mBreakStrategy, params.mHyphenationFrequency);
              switch (checkResult) {
                  case Params.USABLE:
                      return hintPct;
                  case Params.NEED_RECOMPUTE:
                      // To be able to use PrecomputedText for new params, at least break strategy and
                      // hyphenation frequency must be the same.
                      if (params.getBreakStrategy() == hintParams.getBreakStrategy()
                              && params.getHyphenationFrequency()
                                  == hintParams.getHyphenationFrequency()) {
                          paraInfo = createMeasuredParagraphsFromPrecomputedText(
                                  hintPct, params, true /* compute layout */);
                      }
                      break;
                  case Params.UNUSABLE:
                      // Unable to use anything in PrecomputedText. Create PrecomputedText as the
                      // normal text input.
              }
  
          }
          if (paraInfo == null) {
              paraInfo = createMeasuredParagraphs(
                      text, params, 0, text.length(), true /* computeLayout */);
          }
          return new PrecomputedText(text, 0, text.length(), params, paraInfo);
      }
  
      private static ParagraphInfo[] createMeasuredParagraphsFromPrecomputedText(
              @NonNull PrecomputedText pct, @NonNull Params params, boolean computeLayout) {
          final boolean needHyphenation = params.getBreakStrategy() != Layout.BREAK_STRATEGY_SIMPLE
                  && params.getHyphenationFrequency() != Layout.HYPHENATION_FREQUENCY_NONE;
          ArrayList<ParagraphInfo> result = new ArrayList<>();
          for (int i = 0; i < pct.getParagraphCount(); ++i) {
              final int paraStart = pct.getParagraphStart(i);
              final int paraEnd = pct.getParagraphEnd(i);
              result.add(new ParagraphInfo(paraEnd, MeasuredParagraph.buildForStaticLayout(
                      params.getTextPaint(), pct, paraStart, paraEnd, params.getTextDirection(),
                      needHyphenation, computeLayout, pct.getMeasuredParagraph(i),
                      null /* no recycle */)));
          }
          return result.toArray(new ParagraphInfo[result.size()]);
      }
  
      /** @hide */
      public static ParagraphInfo[] createMeasuredParagraphs(
              @NonNull CharSequence text, @NonNull Params params,
              @IntRange(from = 0) int start, @IntRange(from = 0) int end, boolean computeLayout) {
          ArrayList<ParagraphInfo> result = new ArrayList<>();
  
          Preconditions.checkNotNull(text);
          Preconditions.checkNotNull(params);
          final boolean needHyphenation = params.getBreakStrategy() != Layout.BREAK_STRATEGY_SIMPLE
                  && params.getHyphenationFrequency() != Layout.HYPHENATION_FREQUENCY_NONE;
  
          int paraEnd = 0;
          for (int paraStart = start; paraStart < end; paraStart = paraEnd) {
              paraEnd = TextUtils.indexOf(text, LINE_FEED, paraStart, end);
              if (paraEnd < 0) {
                  // No LINE_FEED(U+000A) character found. Use end of the text as the paragraph
                  // end.
                  paraEnd = end;
              } else {
                  paraEnd++;  // Includes LINE_FEED(U+000A) to the prev paragraph.
              }
  
              result.add(new ParagraphInfo(paraEnd, MeasuredParagraph.buildForStaticLayout(
                      params.getTextPaint(), text, paraStart, paraEnd, params.getTextDirection(),
                      needHyphenation, computeLayout, null /* no hint */,
                      null /* no recycle */)));
          }
          return result.toArray(new ParagraphInfo[result.size()]);
      }
  
      // Use PrecomputedText.create instead.
      private PrecomputedText(@NonNull CharSequence text, @IntRange(from = 0) int start,
              @IntRange(from = 0) int end, @NonNull Params params,
              @NonNull ParagraphInfo[] paraInfo) {
          mText = new SpannableString(text, true /* ignoreNoCopySpan */);
          mStart = start;
          mEnd = end;
          mParams = params;
          mParagraphInfo = paraInfo;
      }
  
      /**
       * Return the underlying text.
       * @hide
       */
      public @NonNull CharSequence getText() {
          return mText;
      }
  
      /**
       * Returns the inclusive start offset of measured region.
       * @hide
       */
      public @IntRange(from = 0) int getStart() {
          return mStart;
      }
  
      /**
       * Returns the exclusive end offset of measured region.
       * @hide
       */
      public @IntRange(from = 0) int getEnd() {
          return mEnd;
      }
  
      /**
       * Returns the layout parameters used to measure this text.
       */
      public @NonNull Params getParams() {
          return mParams;
      }
  
      /**
       * Returns the count of paragraphs.
       */
      public @IntRange(from = 0) int getParagraphCount() {
          return mParagraphInfo.length;
      }
  
      /**
       * Returns the paragraph start offset of the text.
       */
      public @IntRange(from = 0) int getParagraphStart(@IntRange(from = 0) int paraIndex) {
          Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex");
          return paraIndex == 0 ? mStart : getParagraphEnd(paraIndex - 1);
      }
  
      /**
       * Returns the paragraph end offset of the text.
       */
      public @IntRange(from = 0) int getParagraphEnd(@IntRange(from = 0) int paraIndex) {
          Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex");
          return mParagraphInfo[paraIndex].paragraphEnd;
      }
  
      /** @hide */
      public @NonNull MeasuredParagraph getMeasuredParagraph(@IntRange(from = 0) int paraIndex) {
          return mParagraphInfo[paraIndex].measured;
      }
  
      /** @hide */
      public @NonNull ParagraphInfo[] getParagraphInfo() {
          return mParagraphInfo;
      }
  
      /**
       * Returns true if the given TextPaint gives the same result of text layout for this text.
       * @hide
       */
      public @Params.CheckResultUsableResult int checkResultUsable(@IntRange(from = 0) int start,
              @IntRange(from = 0) int end, @NonNull TextDirectionHeuristic textDir,
              @NonNull TextPaint paint, @Layout.BreakStrategy int strategy,
              @Layout.HyphenationFrequency int frequency) {
          if (mStart != start || mEnd != end) {
              return Params.UNUSABLE;
          } else {
              return mParams.checkResultUsable(paint, textDir, strategy, frequency);
          }
      }
  
      /** @hide */
      public int findParaIndex(@IntRange(from = 0) int pos) {
          // TODO: Maybe good to remove paragraph concept from PrecomputedText and add substring
          //       layout support to StaticLayout.
          for (int i = 0; i < mParagraphInfo.length; ++i) {
              if (pos < mParagraphInfo[i].paragraphEnd) {
                  return i;
              }
          }
          throw new IndexOutOfBoundsException(
              "pos must be less than " + mParagraphInfo[mParagraphInfo.length - 1].paragraphEnd
              + ", gave " + pos);
      }
  
      /**
       * Returns text width for the given range.
       * Both {@code start} and {@code end} offset need to be in the same paragraph, otherwise
       * IllegalArgumentException will be thrown.
       *
       * @param start the inclusive start offset in the text
       * @param end the exclusive end offset in the text
       * @return the text width
       * @throws IllegalArgumentException if start and end offset are in the different paragraph.
       */
      public @FloatRange(from = 0) float getWidth(@IntRange(from = 0) int start,
              @IntRange(from = 0) int end) {
          Preconditions.checkArgument(0 <= start && start <= mText.length(), "invalid start offset");
          Preconditions.checkArgument(0 <= end && end <= mText.length(), "invalid end offset");
          Preconditions.checkArgument(start <= end, "start offset can not be larger than end offset");
  
          if (start == end) {
              return 0;
          }
          final int paraIndex = findParaIndex(start);
          final int paraStart = getParagraphStart(paraIndex);
          final int paraEnd = getParagraphEnd(paraIndex);
          if (start < paraStart || paraEnd < end) {
              throw new IllegalArgumentException("Cannot measured across the paragraph:"
                  + "para: (" + paraStart + ", " + paraEnd + "), "
                  + "request: (" + start + ", " + end + ")");
          }
          return getMeasuredParagraph(paraIndex).getWidth(start - paraStart, end - paraStart);
      }
  
      /**
       * Retrieves the text bounding box for the given range.
       * Both {@code start} and {@code end} offset need to be in the same paragraph, otherwise
       * IllegalArgumentException will be thrown.
       *
       * @param start the inclusive start offset in the text
       * @param end the exclusive end offset in the text
       * @param bounds the output rectangle
       * @throws IllegalArgumentException if start and end offset are in the different paragraph.
       */
      public void getBounds(@IntRange(from = 0) int start, @IntRange(from = 0) int end,
              @NonNull Rect bounds) {
          Preconditions.checkArgument(0 <= start && start <= mText.length(), "invalid start offset");
          Preconditions.checkArgument(0 <= end && end <= mText.length(), "invalid end offset");
          Preconditions.checkArgument(start <= end, "start offset can not be larger than end offset");
          Preconditions.checkNotNull(bounds);
          if (start == end) {
              bounds.set(0, 0, 0, 0);
              return;
          }
          final int paraIndex = findParaIndex(start);
          final int paraStart = getParagraphStart(paraIndex);
          final int paraEnd = getParagraphEnd(paraIndex);
          if (start < paraStart || paraEnd < end) {
              throw new IllegalArgumentException("Cannot measured across the paragraph:"
                  + "para: (" + paraStart + ", " + paraEnd + "), "
                  + "request: (" + start + ", " + end + ")");
          }
          getMeasuredParagraph(paraIndex).getBounds(start - paraStart, end - paraStart, bounds);
      }
  
      /**
       * Returns a width of a character at offset
       *
       * @param offset an offset of the text.
       * @return a width of the character.
       * @hide
       */
      public float getCharWidthAt(@IntRange(from = 0) int offset) {
          Preconditions.checkArgument(0 <= offset && offset < mText.length(), "invalid offset");
          final int paraIndex = findParaIndex(offset);
          final int paraStart = getParagraphStart(paraIndex);
          final int paraEnd = getParagraphEnd(paraIndex);
          return getMeasuredParagraph(paraIndex).getCharWidthAt(offset - paraStart);
      }
  
      /**
       * Returns the size of native PrecomputedText memory usage.
       *
       * Note that this is not guaranteed to be accurate. Must be used only for testing purposes.
       * @hide
       */
      public int getMemoryUsage() {
          int r = 0;
          for (int i = 0; i < getParagraphCount(); ++i) {
              r += getMeasuredParagraph(i).getMemoryUsage();
          }
          return r;
      }
  
      ///////////////////////////////////////////////////////////////////////////////////////////////
      // Spannable overrides
      //
      // Do not allow to modify MetricAffectingSpan
  
      /**
       * @throws IllegalArgumentException if {@link MetricAffectingSpan} is specified.
       */
      @Override
      public void setSpan(Object what, int start, int end, int flags) {
          if (what instanceof MetricAffectingSpan) {
              throw new IllegalArgumentException(
                      "MetricAffectingSpan can not be set to PrecomputedText.");
          }
          mText.setSpan(what, start, end, flags);
      }
  
      /**
       * @throws IllegalArgumentException if {@link MetricAffectingSpan} is specified.
       */
      @Override
      public void removeSpan(Object what) {
          if (what instanceof MetricAffectingSpan) {
              throw new IllegalArgumentException(
                      "MetricAffectingSpan can not be removed from PrecomputedText.");
          }
          mText.removeSpan(what);
      }
  
      ///////////////////////////////////////////////////////////////////////////////////////////////
      // Spanned overrides
      //
      // Just proxy for underlying mText if appropriate.
  
      @Override
      public <T> T[] getSpans(int start, int end, Class<T> type) {
          return mText.getSpans(start, end, type);
      }
  
      @Override
      public int getSpanStart(Object tag) {
          return mText.getSpanStart(tag);
      }
  
      @Override
      public int getSpanEnd(Object tag) {
          return mText.getSpanEnd(tag);
      }
  
      @Override
      public int getSpanFlags(Object tag) {
          return mText.getSpanFlags(tag);
      }
  
      @Override
      public int nextSpanTransition(int start, int limit, Class type) {
          return mText.nextSpanTransition(start, limit, type);
      }
  
      ///////////////////////////////////////////////////////////////////////////////////////////////
      // CharSequence overrides.
      //
      // Just proxy for underlying mText.
  
      @Override
      public int length() {
          return mText.length();
      }
  
      @Override
      public char charAt(int index) {
          return mText.charAt(index);
      }
  
      @Override
      public CharSequence subSequence(int start, int end) {
          return PrecomputedText.create(mText.subSequence(start, end), mParams);
      }
  
      @Override
      public String toString() {
          return mText.toString();
      }
  }
  