content: /*
   * Copyright (C) 2006 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.Nullable;
  import android.annotation.UnsupportedAppUsage;
  import android.graphics.BaseCanvas;
  import android.graphics.Paint;
  import android.util.Log;
  
  import com.android.internal.annotations.GuardedBy;
  import com.android.internal.util.ArrayUtils;
  import com.android.internal.util.GrowingArrayUtils;
  
  import libcore.util.EmptyArray;
  
  import java.lang.reflect.Array;
  import java.util.IdentityHashMap;
  
  /**
   * This is the class for text whose content and markup can both be changed.
   */
  public class SpannableStringBuilder implements CharSequence, GetChars, Spannable, Editable,
          Appendable, GraphicsOperations {
      private final static String TAG = "SpannableStringBuilder";
      /**
       * Create a new SpannableStringBuilder with empty contents
       */
      public SpannableStringBuilder() {
          this("");
      }
  
      /**
       * Create a new SpannableStringBuilder containing a copy of the
       * specified text, including its spans if any.
       */
      public SpannableStringBuilder(CharSequence text) {
          this(text, 0, text.length());
      }
  
      /**
       * Create a new SpannableStringBuilder containing a copy of the
       * specified slice of the specified text, including its spans if any.
       */
      public SpannableStringBuilder(CharSequence text, int start, int end) {
          int srclen = end - start;
  
          if (srclen < 0) throw new StringIndexOutOfBoundsException();
  
          mText = ArrayUtils.newUnpaddedCharArray(GrowingArrayUtils.growSize(srclen));
          mGapStart = srclen;
          mGapLength = mText.length - srclen;
  
          TextUtils.getChars(text, start, end, mText, 0);
  
          mSpanCount = 0;
          mSpanInsertCount = 0;
          mSpans = EmptyArray.OBJECT;
          mSpanStarts = EmptyArray.INT;
          mSpanEnds = EmptyArray.INT;
          mSpanFlags = EmptyArray.INT;
          mSpanMax = EmptyArray.INT;
          mSpanOrder = EmptyArray.INT;
  
          if (text instanceof Spanned) {
              Spanned sp = (Spanned) text;
              Object[] spans = sp.getSpans(start, end, Object.class);
  
              for (int i = 0; i < spans.length; i++) {
                  if (spans[i] instanceof NoCopySpan) {
                      continue;
                  }
  
                  int st = sp.getSpanStart(spans[i]) - start;
                  int en = sp.getSpanEnd(spans[i]) - start;
                  int fl = sp.getSpanFlags(spans[i]);
  
                  if (st < 0)
                      st = 0;
                  if (st > end - start)
                      st = end - start;
  
                  if (en < 0)
                      en = 0;
                  if (en > end - start)
                      en = end - start;
  
                  setSpan(false, spans[i], st, en, fl, false/*enforceParagraph*/);
              }
              restoreInvariants();
          }
      }
  
      public static SpannableStringBuilder valueOf(CharSequence source) {
          if (source instanceof SpannableStringBuilder) {
              return (SpannableStringBuilder) source;
          } else {
              return new SpannableStringBuilder(source);
          }
      }
  
      /**
       * Return the char at the specified offset within the buffer.
       */
      public char charAt(int where) {
          int len = length();
          if (where < 0) {
              throw new IndexOutOfBoundsException("charAt: " + where + " < 0");
          } else if (where >= len) {
              throw new IndexOutOfBoundsException("charAt: " + where + " >= length " + len);
          }
  
          if (where >= mGapStart)
              return mText[where + mGapLength];
          else
              return mText[where];
      }
  
      /**
       * Return the number of chars in the buffer.
       */
      public int length() {
          return mText.length - mGapLength;
      }
  
      private void resizeFor(int size) {
          final int oldLength = mText.length;
          if (size + 1 <= oldLength) {
              return;
          }
  
          char[] newText = ArrayUtils.newUnpaddedCharArray(GrowingArrayUtils.growSize(size));
          System.arraycopy(mText, 0, newText, 0, mGapStart);
          final int newLength = newText.length;
          final int delta = newLength - oldLength;
          final int after = oldLength - (mGapStart + mGapLength);
          System.arraycopy(mText, oldLength - after, newText, newLength - after, after);
          mText = newText;
  
          mGapLength += delta;
          if (mGapLength < 1)
              new Exception("mGapLength < 1").printStackTrace();
  
          if (mSpanCount != 0) {
              for (int i = 0; i < mSpanCount; i++) {
                  if (mSpanStarts[i] > mGapStart) mSpanStarts[i] += delta;
                  if (mSpanEnds[i] > mGapStart) mSpanEnds[i] += delta;
              }
              calcMax(treeRoot());
          }
      }
  
      private void moveGapTo(int where) {
          if (where == mGapStart)
              return;
  
          boolean atEnd = (where == length());
  
          if (where < mGapStart) {
              int overlap = mGapStart - where;
              System.arraycopy(mText, where, mText, mGapStart + mGapLength - overlap, overlap);
          } else /* where > mGapStart */ {
              int overlap = where - mGapStart;
              System.arraycopy(mText, where + mGapLength - overlap, mText, mGapStart, overlap);
          }
  
          // TODO: be more clever (although the win really isn't that big)
          if (mSpanCount != 0) {
              for (int i = 0; i < mSpanCount; i++) {
                  int start = mSpanStarts[i];
                  int end = mSpanEnds[i];
  
                  if (start > mGapStart)
                      start -= mGapLength;
                  if (start > where)
                      start += mGapLength;
                  else if (start == where) {
                      int flag = (mSpanFlags[i] & START_MASK) >> START_SHIFT;
  
                      if (flag == POINT || (atEnd && flag == PARAGRAPH))
                          start += mGapLength;
                  }
  
                  if (end > mGapStart)
                      end -= mGapLength;
                  if (end > where)
                      end += mGapLength;
                  else if (end == where) {
                      int flag = (mSpanFlags[i] & END_MASK);
  
                      if (flag == POINT || (atEnd && flag == PARAGRAPH))
                          end += mGapLength;
                  }
  
                  mSpanStarts[i] = start;
                  mSpanEnds[i] = end;
              }
              calcMax(treeRoot());
          }
  
          mGapStart = where;
      }
  
      // Documentation from interface
      public SpannableStringBuilder insert(int where, CharSequence tb, int start, int end) {
          return replace(where, where, tb, start, end);
      }
  
      // Documentation from interface
      public SpannableStringBuilder insert(int where, CharSequence tb) {
          return replace(where, where, tb, 0, tb.length());
      }
  
      // Documentation from interface
      public SpannableStringBuilder delete(int start, int end) {
          SpannableStringBuilder ret = replace(start, end, "", 0, 0);
  
          if (mGapLength > 2 * length())
              resizeFor(length());
  
          return ret; // == this
      }
  
      // Documentation from interface
      public void clear() {
          replace(0, length(), "", 0, 0);
          mSpanInsertCount = 0;
      }
  
      // Documentation from interface
      public void clearSpans() {
          for (int i = mSpanCount - 1; i >= 0; i--) {
              Object what = mSpans[i];
              int ostart = mSpanStarts[i];
              int oend = mSpanEnds[i];
  
              if (ostart > mGapStart)
                  ostart -= mGapLength;
              if (oend > mGapStart)
                  oend -= mGapLength;
  
              mSpanCount = i;
              mSpans[i] = null;
  
              sendSpanRemoved(what, ostart, oend);
          }
          if (mIndexOfSpan != null) {
              mIndexOfSpan.clear();
          }
          mSpanInsertCount = 0;
      }
  
      // Documentation from interface
      public SpannableStringBuilder append(CharSequence text) {
          int length = length();
          return replace(length, length, text, 0, text.length());
      }
  
      /**
       * Appends the character sequence {@code text} and spans {@code what} over the appended part.
       * See {@link Spanned} for an explanation of what the flags mean.
       * @param text the character sequence to append.
       * @param what the object to be spanned over the appended text.
       * @param flags see {@link Spanned}.
       * @return this {@code SpannableStringBuilder}.
       */
      public SpannableStringBuilder append(CharSequence text, Object what, int flags) {
          int start = length();
          append(text);
          setSpan(what, start, length(), flags);
          return this;
      }
  
      // Documentation from interface
      public SpannableStringBuilder append(CharSequence text, int start, int end) {
          int length = length();
          return replace(length, length, text, start, end);
      }
  
      // Documentation from interface
      public SpannableStringBuilder append(char text) {
          return append(String.valueOf(text));
      }
  
      // Returns true if a node was removed (so we can restart search from root)
      private boolean removeSpansForChange(int start, int end, boolean textIsRemoved, int i) {
          if ((i & 1) != 0) {
              // internal tree node
              if (resolveGap(mSpanMax[i]) >= start &&
                      removeSpansForChange(start, end, textIsRemoved, leftChild(i))) {
                  return true;
              }
          }
          if (i < mSpanCount) {
              if ((mSpanFlags[i] & Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) ==
                      Spanned.SPAN_EXCLUSIVE_EXCLUSIVE &&
                      mSpanStarts[i] >= start && mSpanStarts[i] < mGapStart + mGapLength &&
                      mSpanEnds[i] >= start && mSpanEnds[i] < mGapStart + mGapLength &&
                      // The following condition indicates that the span would become empty
                      (textIsRemoved || mSpanStarts[i] > start || mSpanEnds[i] < mGapStart)) {
                  mIndexOfSpan.remove(mSpans[i]);
                  removeSpan(i, 0 /* flags */);
                  return true;
              }
              return resolveGap(mSpanStarts[i]) <= end && (i & 1) != 0 &&
                  removeSpansForChange(start, end, textIsRemoved, rightChild(i));
          }
          return false;
      }
  
      private void change(int start, int end, CharSequence cs, int csStart, int csEnd) {
          // Can be negative
          final int replacedLength = end - start;
          final int replacementLength = csEnd - csStart;
          final int nbNewChars = replacementLength - replacedLength;
  
          boolean changed = false;
          for (int i = mSpanCount - 1; i >= 0; i--) {
              int spanStart = mSpanStarts[i];
              if (spanStart > mGapStart)
                  spanStart -= mGapLength;
  
              int spanEnd = mSpanEnds[i];
              if (spanEnd > mGapStart)
                  spanEnd -= mGapLength;
  
              if ((mSpanFlags[i] & SPAN_PARAGRAPH) == SPAN_PARAGRAPH) {
                  int ost = spanStart;
                  int oen = spanEnd;
                  int clen = length();
  
                  if (spanStart > start && spanStart <= end) {
                      for (spanStart = end; spanStart < clen; spanStart++)
                          if (spanStart > end && charAt(spanStart - 1) == '\n')
                              break;
                  }
  
                  if (spanEnd > start && spanEnd <= end) {
                      for (spanEnd = end; spanEnd < clen; spanEnd++)
                          if (spanEnd > end && charAt(spanEnd - 1) == '\n')
                              break;
                  }
  
                  if (spanStart != ost || spanEnd != oen) {
                      setSpan(false, mSpans[i], spanStart, spanEnd, mSpanFlags[i],
                              true/*enforceParagraph*/);
                      changed = true;
                  }
              }
  
              int flags = 0;
              if (spanStart == start) flags |= SPAN_START_AT_START;
              else if (spanStart == end + nbNewChars) flags |= SPAN_START_AT_END;
              if (spanEnd == start) flags |= SPAN_END_AT_START;
              else if (spanEnd == end + nbNewChars) flags |= SPAN_END_AT_END;
              mSpanFlags[i] |= flags;
          }
          if (changed) {
              restoreInvariants();
          }
  
          moveGapTo(end);
  
          if (nbNewChars >= mGapLength) {
              resizeFor(mText.length + nbNewChars - mGapLength);
          }
  
          final boolean textIsRemoved = replacementLength == 0;
          // The removal pass needs to be done before the gap is updated in order to broadcast the
          // correct previous positions to the correct intersecting SpanWatchers
          if (replacedLength > 0) { // no need for span fixup on pure insertion
              while (mSpanCount > 0 &&
                      removeSpansForChange(start, end, textIsRemoved, treeRoot())) {
                  // keep deleting spans as needed, and restart from root after every deletion
                  // because deletion can invalidate an index.
              }
          }
  
          mGapStart += nbNewChars;
          mGapLength -= nbNewChars;
  
          if (mGapLength < 1)
              new Exception("mGapLength < 1").printStackTrace();
  
          TextUtils.getChars(cs, csStart, csEnd, mText, start);
  
          if (replacedLength > 0) { // no need for span fixup on pure insertion
              // TODO potential optimization: only update bounds on intersecting spans
              final boolean atEnd = (mGapStart + mGapLength == mText.length);
  
              for (int i = 0; i < mSpanCount; i++) {
                  final int startFlag = (mSpanFlags[i] & START_MASK) >> START_SHIFT;
                  mSpanStarts[i] = updatedIntervalBound(mSpanStarts[i], start, nbNewChars, startFlag,
                          atEnd, textIsRemoved);
  
                  final int endFlag = (mSpanFlags[i] & END_MASK);
                  mSpanEnds[i] = updatedIntervalBound(mSpanEnds[i], start, nbNewChars, endFlag,
                          atEnd, textIsRemoved);
              }
              // TODO potential optimization: only fix up invariants when bounds actually changed
              restoreInvariants();
          }
  
          if (cs instanceof Spanned) {
              Spanned sp = (Spanned) cs;
              Object[] spans = sp.getSpans(csStart, csEnd, Object.class);
  
              for (int i = 0; i < spans.length; i++) {
                  int st = sp.getSpanStart(spans[i]);
                  int en = sp.getSpanEnd(spans[i]);
  
                  if (st < csStart) st = csStart;
                  if (en > csEnd) en = csEnd;
  
                  // Add span only if this object is not yet used as a span in this string
                  if (getSpanStart(spans[i]) < 0) {
                      int copySpanStart = st - csStart + start;
                      int copySpanEnd = en - csStart + start;
                      int copySpanFlags = sp.getSpanFlags(spans[i]) | SPAN_ADDED;
  
                      setSpan(false, spans[i], copySpanStart, copySpanEnd, copySpanFlags,
                              false/*enforceParagraph*/);
                  }
              }
              restoreInvariants();
          }
      }
  
      private int updatedIntervalBound(int offset, int start, int nbNewChars, int flag, boolean atEnd,
              boolean textIsRemoved) {
          if (offset >= start && offset < mGapStart + mGapLength) {
              if (flag == POINT) {
                  // A POINT located inside the replaced range should be moved to the end of the
                  // replaced text.
                  // The exception is when the point is at the start of the range and we are doing a
                  // text replacement (as opposed to a deletion): the point stays there.
                  if (textIsRemoved || offset > start) {
                      return mGapStart + mGapLength;
                  }
              } else {
                  if (flag == PARAGRAPH) {
                      if (atEnd) {
                          return mGapStart + mGapLength;
                      }
                  } else { // MARK
                      // MARKs should be moved to the start, with the exception of a mark located at
                      // the end of the range (which will be < mGapStart + mGapLength since mGapLength
                      // is > 0, which should stay 'unchanged' at the end of the replaced text.
                      if (textIsRemoved || offset < mGapStart - nbNewChars) {
                          return start;
                      } else {
                          // Move to the end of replaced text (needed if nbNewChars != 0)
                          return mGapStart;
                      }
                  }
              }
          }
          return offset;
      }
  
      // Note: caller is responsible for removing the mIndexOfSpan entry.
      private void removeSpan(int i, int flags) {
          Object object = mSpans[i];
  
          int start = mSpanStarts[i];
          int end = mSpanEnds[i];
  
          if (start > mGapStart) start -= mGapLength;
          if (end > mGapStart) end -= mGapLength;
  
          int count = mSpanCount - (i + 1);
          System.arraycopy(mSpans, i + 1, mSpans, i, count);
          System.arraycopy(mSpanStarts, i + 1, mSpanStarts, i, count);
          System.arraycopy(mSpanEnds, i + 1, mSpanEnds, i, count);
          System.arraycopy(mSpanFlags, i + 1, mSpanFlags, i, count);
          System.arraycopy(mSpanOrder, i + 1, mSpanOrder, i, count);
  
          mSpanCount--;
  
          invalidateIndex(i);
          mSpans[mSpanCount] = null;
  
          // Invariants must be restored before sending span removed notifications.
          restoreInvariants();
  
          if ((flags & Spanned.SPAN_INTERMEDIATE) == 0) {
              sendSpanRemoved(object, start, end);
          }
      }
  
      // Documentation from interface
      public SpannableStringBuilder replace(int start, int end, CharSequence tb) {
          return replace(start, end, tb, 0, tb.length());
      }
  
      // Documentation from interface
      public SpannableStringBuilder replace(final int start, final int end,
              CharSequence tb, int tbstart, int tbend) {
          checkRange("replace", start, end);
  
          int filtercount = mFilters.length;
          for (int i = 0; i < filtercount; i++) {
              CharSequence repl = mFilters[i].filter(tb, tbstart, tbend, this, start, end);
  
              if (repl != null) {
                  tb = repl;
                  tbstart = 0;
                  tbend = repl.length();
              }
          }
  
          final int origLen = end - start;
          final int newLen = tbend - tbstart;
  
          if (origLen == 0 && newLen == 0 && !hasNonExclusiveExclusiveSpanAt(tb, tbstart)) {
              // This is a no-op iif there are no spans in tb that would be added (with a 0-length)
              // Early exit so that the text watchers do not get notified
              return this;
          }
  
          TextWatcher[] textWatchers = getSpans(start, start + origLen, TextWatcher.class);
          sendBeforeTextChanged(textWatchers, start, origLen, newLen);
  
          // Try to keep the cursor / selection at the same relative position during
          // a text replacement. If replaced or replacement text length is zero, this
          // is already taken care of.
          boolean adjustSelection = origLen != 0 && newLen != 0;
          int selectionStart = 0;
          int selectionEnd = 0;
          if (adjustSelection) {
              selectionStart = Selection.getSelectionStart(this);
              selectionEnd = Selection.getSelectionEnd(this);
          }
  
          change(start, end, tb, tbstart, tbend);
  
          if (adjustSelection) {
              boolean changed = false;
              if (selectionStart > start && selectionStart < end) {
                  final long diff = selectionStart - start;
                  final int offset = Math.toIntExact(diff * newLen / origLen);
                  selectionStart = start + offset;
  
                  changed = true;
                  setSpan(false, Selection.SELECTION_START, selectionStart, selectionStart,
                          Spanned.SPAN_POINT_POINT, true/*enforceParagraph*/);
              }
              if (selectionEnd > start && selectionEnd < end) {
                  final long diff = selectionEnd - start;
                  final int offset = Math.toIntExact(diff * newLen / origLen);
                  selectionEnd = start + offset;
  
                  changed = true;
                  setSpan(false, Selection.SELECTION_END, selectionEnd, selectionEnd,
                          Spanned.SPAN_POINT_POINT, true/*enforceParagraph*/);
              }
              if (changed) {
                  restoreInvariants();
              }
          }
  
          sendTextChanged(textWatchers, start, origLen, newLen);
          sendAfterTextChanged(textWatchers);
  
          // Span watchers need to be called after text watchers, which may update the layout
          sendToSpanWatchers(start, end, newLen - origLen);
  
          return this;
      }
  
      private static boolean hasNonExclusiveExclusiveSpanAt(CharSequence text, int offset) {
          if (text instanceof Spanned) {
              Spanned spanned = (Spanned) text;
              Object[] spans = spanned.getSpans(offset, offset, Object.class);
              final int length = spans.length;
              for (int i = 0; i < length; i++) {
                  Object span = spans[i];
                  int flags = spanned.getSpanFlags(span);
                  if (flags != Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) return true;
              }
          }
          return false;
      }
  
      @UnsupportedAppUsage
      private void sendToSpanWatchers(int replaceStart, int replaceEnd, int nbNewChars) {
          for (int i = 0; i < mSpanCount; i++) {
              int spanFlags = mSpanFlags[i];
  
              // This loop handles only modified (not added) spans.
              if ((spanFlags & SPAN_ADDED) != 0) continue;
              int spanStart = mSpanStarts[i];
              int spanEnd = mSpanEnds[i];
              if (spanStart > mGapStart) spanStart -= mGapLength;
              if (spanEnd > mGapStart) spanEnd -= mGapLength;
  
              int newReplaceEnd = replaceEnd + nbNewChars;
              boolean spanChanged = false;
  
              int previousSpanStart = spanStart;
              if (spanStart > newReplaceEnd) {
                  if (nbNewChars != 0) {
                      previousSpanStart -= nbNewChars;
                      spanChanged = true;
                  }
              } else if (spanStart >= replaceStart) {
                  // No change if span start was already at replace interval boundaries before replace
                  if ((spanStart != replaceStart ||
                          ((spanFlags & SPAN_START_AT_START) != SPAN_START_AT_START)) &&
                          (spanStart != newReplaceEnd ||
                          ((spanFlags & SPAN_START_AT_END) != SPAN_START_AT_END))) {
                      // TODO A correct previousSpanStart cannot be computed at this point.
                      // It would require to save all the previous spans' positions before the replace
                      // Using an invalid -1 value to convey this would break the broacast range
                      spanChanged = true;
                  }
              }
  
              int previousSpanEnd = spanEnd;
              if (spanEnd > newReplaceEnd) {
                  if (nbNewChars != 0) {
                      previousSpanEnd -= nbNewChars;
                      spanChanged = true;
                  }
              } else if (spanEnd >= replaceStart) {
                  // No change if span start was already at replace interval boundaries before replace
                  if ((spanEnd != replaceStart ||
                          ((spanFlags & SPAN_END_AT_START) != SPAN_END_AT_START)) &&
                          (spanEnd != newReplaceEnd ||
                          ((spanFlags & SPAN_END_AT_END) != SPAN_END_AT_END))) {
                      // TODO same as above for previousSpanEnd
                      spanChanged = true;
                  }
              }
  
              if (spanChanged) {
                  sendSpanChanged(mSpans[i], previousSpanStart, previousSpanEnd, spanStart, spanEnd);
              }
              mSpanFlags[i] &= ~SPAN_START_END_MASK;
          }
  
          // Handle added spans
          for (int i = 0; i < mSpanCount; i++) {
              int spanFlags = mSpanFlags[i];
              if ((spanFlags & SPAN_ADDED) != 0) {
                  mSpanFlags[i] &= ~SPAN_ADDED;
                  int spanStart = mSpanStarts[i];
                  int spanEnd = mSpanEnds[i];
                  if (spanStart > mGapStart) spanStart -= mGapLength;
                  if (spanEnd > mGapStart) spanEnd -= mGapLength;
                  sendSpanAdded(mSpans[i], spanStart, spanEnd);
              }
          }
      }
  
      /**
       * Mark the specified range of text with the specified object.
       * The flags determine how the span will behave when text is
       * inserted at the start or end of the span's range.
       */
      public void setSpan(Object what, int start, int end, int flags) {
          setSpan(true, what, start, end, flags, true/*enforceParagraph*/);
      }
  
      // Note: if send is false, then it is the caller's responsibility to restore
      // invariants. If send is false and the span already exists, then this method
      // will not change the index of any spans.
      private void setSpan(boolean send, Object what, int start, int end, int flags,
              boolean enforceParagraph) {
          checkRange("setSpan", start, end);
  
          int flagsStart = (flags & START_MASK) >> START_SHIFT;
          if (isInvalidParagraph(start, flagsStart)) {
              if (!enforceParagraph) {
                  // do not set the span
                  return;
              }
              throw new RuntimeException("PARAGRAPH span must start at paragraph boundary"
                      + " (" + start + " follows " + charAt(start - 1) + ")");
          }
  
          int flagsEnd = flags & END_MASK;
          if (isInvalidParagraph(end, flagsEnd)) {
              if (!enforceParagraph) {
                  // do not set the span
                  return;
              }
              throw new RuntimeException("PARAGRAPH span must end at paragraph boundary"
                      + " (" + end + " follows " + charAt(end - 1) + ")");
          }
  
          // 0-length Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
          if (flagsStart == POINT && flagsEnd == MARK && start == end) {
              if (send) {
                  Log.e(TAG, "SPAN_EXCLUSIVE_EXCLUSIVE spans cannot have a zero length");
              }
              // Silently ignore invalid spans when they are created from this class.
              // This avoids the duplication of the above test code before all the
              // calls to setSpan that are done in this class
              return;
          }
  
          int nstart = start;
          int nend = end;
  
          if (start > mGapStart) {
              start += mGapLength;
          } else if (start == mGapStart) {
              if (flagsStart == POINT || (flagsStart == PARAGRAPH && start == length()))
                  start += mGapLength;
          }
  
          if (end > mGapStart) {
              end += mGapLength;
          } else if (end == mGapStart) {
              if (flagsEnd == POINT || (flagsEnd == PARAGRAPH && end == length()))
                  end += mGapLength;
          }
  
          if (mIndexOfSpan != null) {
              Integer index = mIndexOfSpan.get(what);
              if (index != null) {
                  int i = index;
                  int ostart = mSpanStarts[i];
                  int oend = mSpanEnds[i];
  
                  if (ostart > mGapStart)
                      ostart -= mGapLength;
                  if (oend > mGapStart)
                      oend -= mGapLength;
  
                  mSpanStarts[i] = start;
                  mSpanEnds[i] = end;
                  mSpanFlags[i] = flags;
  
                  if (send) {
                      restoreInvariants();
                      sendSpanChanged(what, ostart, oend, nstart, nend);
                  }
  
                  return;
              }
          }
  
          mSpans = GrowingArrayUtils.append(mSpans, mSpanCount, what);
          mSpanStarts = GrowingArrayUtils.append(mSpanStarts, mSpanCount, start);
          mSpanEnds = GrowingArrayUtils.append(mSpanEnds, mSpanCount, end);
          mSpanFlags = GrowingArrayUtils.append(mSpanFlags, mSpanCount, flags);
          mSpanOrder = GrowingArrayUtils.append(mSpanOrder, mSpanCount, mSpanInsertCount);
          invalidateIndex(mSpanCount);
          mSpanCount++;
          mSpanInsertCount++;
          // Make sure there is enough room for empty interior nodes.
          // This magic formula computes the size of the smallest perfect binary
          // tree no smaller than mSpanCount.
          int sizeOfMax = 2 * treeRoot() + 1;
          if (mSpanMax.length < sizeOfMax) {
              mSpanMax = new int[sizeOfMax];
          }
  
          if (send) {
              restoreInvariants();
              sendSpanAdded(what, nstart, nend);
          }
      }
  
      private boolean isInvalidParagraph(int index, int flag) {
          return flag == PARAGRAPH && index != 0 && index != length() && charAt(index - 1) != '\n';
      }
  
      /**
       * Remove the specified markup object from the buffer.
       */
      public void removeSpan(Object what) {
          removeSpan(what, 0 /* flags */);
      }
  
      /**
       * Remove the specified markup object from the buffer.
       *
       * @hide
       */
      public void removeSpan(Object what, int flags) {
          if (mIndexOfSpan == null) return;
          Integer i = mIndexOfSpan.remove(what);
          if (i != null) {
              removeSpan(i.intValue(), flags);
          }
      }
  
      /**
       * Return externally visible offset given offset into gapped buffer.
       */
      private int resolveGap(int i) {
          return i > mGapStart ? i - mGapLength : i;
      }
  
      /**
       * Return the buffer offset of the beginning of the specified
       * markup object, or -1 if it is not attached to this buffer.
       */
      public int getSpanStart(Object what) {
          if (mIndexOfSpan == null) return -1;
          Integer i = mIndexOfSpan.get(what);
          return i == null ? -1 : resolveGap(mSpanStarts[i]);
      }
  
      /**
       * Return the buffer offset of the end of the specified
       * markup object, or -1 if it is not attached to this buffer.
       */
      public int getSpanEnd(Object what) {
          if (mIndexOfSpan == null) return -1;
          Integer i = mIndexOfSpan.get(what);
          return i == null ? -1 : resolveGap(mSpanEnds[i]);
      }
  
      /**
       * Return the flags of the end of the specified
       * markup object, or 0 if it is not attached to this buffer.
       */
      public int getSpanFlags(Object what) {
          if (mIndexOfSpan == null) return 0;
          Integer i = mIndexOfSpan.get(what);
          return i == null ? 0 : mSpanFlags[i];
      }
  
      /**
       * Return an array of the spans of the specified type that overlap
       * the specified range of the buffer.  The kind may be Object.class to get
       * a list of all the spans regardless of type.
       */
      @SuppressWarnings("unchecked")
      public <T> T[] getSpans(int queryStart, int queryEnd, @Nullable Class<T> kind) {
          return getSpans(queryStart, queryEnd, kind, true);
      }
  
      /**
       * Return an array of the spans of the specified type that overlap
       * the specified range of the buffer.  The kind may be Object.class to get
       * a list of all the spans regardless of type.
       *
       * @param queryStart Start index.
       * @param queryEnd End index.
       * @param kind Class type to search for.
       * @param sortByInsertionOrder If true the results are sorted by the insertion order.
       * @param <T>
       * @return Array of the spans. Empty array if no results are found.
       *
       * @hide
       */
      @UnsupportedAppUsage
      public <T> T[] getSpans(int queryStart, int queryEnd, @Nullable Class<T> kind,
              boolean sortByInsertionOrder) {
          if (kind == null) return (T[]) ArrayUtils.emptyArray(Object.class);
          if (mSpanCount == 0) return ArrayUtils.emptyArray(kind);
          int count = countSpans(queryStart, queryEnd, kind, treeRoot());
          if (count == 0) {
              return ArrayUtils.emptyArray(kind);
          }
  
          // Safe conversion, but requires a suppressWarning
          T[] ret = (T[]) Array.newInstance(kind, count);
          final int[] prioSortBuffer = sortByInsertionOrder ? obtain(count) : EmptyArray.INT;
          final int[] orderSortBuffer = sortByInsertionOrder ? obtain(count) : EmptyArray.INT;
          getSpansRec(queryStart, queryEnd, kind, treeRoot(), ret, prioSortBuffer,
                  orderSortBuffer, 0, sortByInsertionOrder);
          if (sortByInsertionOrder) {
              sort(ret, prioSortBuffer, orderSortBuffer);
              recycle(prioSortBuffer);
              recycle(orderSortBuffer);
          }
          return ret;
      }
  
      private int countSpans(int queryStart, int queryEnd, Class kind, int i) {
          int count = 0;
          if ((i & 1) != 0) {
              // internal tree node
              int left = leftChild(i);
              int spanMax = mSpanMax[left];
              if (spanMax > mGapStart) {
                  spanMax -= mGapLength;
              }
              if (spanMax >= queryStart) {
                  count = countSpans(queryStart, queryEnd, kind, left);
              }
          }
          if (i < mSpanCount) {
              int spanStart = mSpanStarts[i];
              if (spanStart > mGapStart) {
                  spanStart -= mGapLength;
              }
              if (spanStart <= queryEnd) {
                  int spanEnd = mSpanEnds[i];
                  if (spanEnd > mGapStart) {
                      spanEnd -= mGapLength;
                  }
                  if (spanEnd >= queryStart &&
                      (spanStart == spanEnd || queryStart == queryEnd ||
                          (spanStart != queryEnd && spanEnd != queryStart)) &&
                          (Object.class == kind || kind.isInstance(mSpans[i]))) {
                      count++;
                  }
                  if ((i & 1) != 0) {
                      count += countSpans(queryStart, queryEnd, kind, rightChild(i));
                  }
              }
          }
          return count;
      }
  
      /**
       * Fills the result array with the spans found under the current interval tree node.
       *
       * @param queryStart Start index for the interval query.
       * @param queryEnd End index for the interval query.
       * @param kind Class type to search for.
       * @param i Index of the current tree node.
       * @param ret Array to be filled with results.
       * @param priority Buffer to keep record of the priorities of spans found.
       * @param insertionOrder Buffer to keep record of the insertion orders of spans found.
       * @param count The number of found spans.
       * @param sort Flag to fill the priority and insertion order buffers. If false then
       *             the spans with priority flag will be sorted in the result array.
       * @param <T>
       * @return The total number of spans found.
       */
      @SuppressWarnings("unchecked")
      private <T> int getSpansRec(int queryStart, int queryEnd, Class<T> kind,
              int i, T[] ret, int[] priority, int[] insertionOrder, int count, boolean sort) {
          if ((i & 1) != 0) {
              // internal tree node
              int left = leftChild(i);
              int spanMax = mSpanMax[left];
              if (spanMax > mGapStart) {
                  spanMax -= mGapLength;
              }
              if (spanMax >= queryStart) {
                  count = getSpansRec(queryStart, queryEnd, kind, left, ret, priority,
                          insertionOrder, count, sort);
              }
          }
          if (i >= mSpanCount) return count;
          int spanStart = mSpanStarts[i];
          if (spanStart > mGapStart) {
              spanStart -= mGapLength;
          }
          if (spanStart <= queryEnd) {
              int spanEnd = mSpanEnds[i];
              if (spanEnd > mGapStart) {
                  spanEnd -= mGapLength;
              }
              if (spanEnd >= queryStart &&
                      (spanStart == spanEnd || queryStart == queryEnd ||
                          (spanStart != queryEnd && spanEnd != queryStart)) &&
                          (Object.class == kind || kind.isInstance(mSpans[i]))) {
                  int spanPriority = mSpanFlags[i] & SPAN_PRIORITY;
                  int target = count;
                  if (sort) {
                      priority[target] = spanPriority;
                      insertionOrder[target] = mSpanOrder[i];
                  } else if (spanPriority != 0) {
                      //insertion sort for elements with priority
                      int j = 0;
                      for (; j < count; j++) {
                          int p = getSpanFlags(ret[j]) & SPAN_PRIORITY;
                          if (spanPriority > p) break;
                      }
                      System.arraycopy(ret, j, ret, j + 1, count - j);
                      target = j;
                  }
                  ret[target] = (T) mSpans[i];
                  count++;
              }
              if (count < ret.length && (i & 1) != 0) {
                  count = getSpansRec(queryStart, queryEnd, kind, rightChild(i), ret, priority,
                          insertionOrder, count, sort);
              }
          }
          return count;
      }
  
      /**
       * Obtain a temporary sort buffer.
       *
       * @param elementCount the size of the int[] to be returned
       * @return an int[] with elementCount length
       */
      private static int[] obtain(final int elementCount) {
          int[] result = null;
          synchronized (sCachedIntBuffer) {
              // try finding a tmp buffer with length of at least elementCount
              // if not get the first available one
              int candidateIndex = -1;
              for (int i = sCachedIntBuffer.length - 1; i >= 0; i--) {
                  if (sCachedIntBuffer[i] != null) {
                      if (sCachedIntBuffer[i].length >= elementCount) {
                          candidateIndex = i;
                          break;
                      } else if (candidateIndex == -1) {
                          candidateIndex = i;
                      }
                  }
              }
  
              if (candidateIndex != -1) {
                  result = sCachedIntBuffer[candidateIndex];
                  sCachedIntBuffer[candidateIndex] = null;
              }
          }
          result = checkSortBuffer(result, elementCount);
          return result;
      }
  
      /**
       * Recycle sort buffer.
       *
       * @param buffer buffer to be recycled
       */
      private static void recycle(int[] buffer) {
          synchronized (sCachedIntBuffer) {
              for (int i = 0; i < sCachedIntBuffer.length; i++) {
                  if (sCachedIntBuffer[i] == null || buffer.length > sCachedIntBuffer[i].length) {
                      sCachedIntBuffer[i] = buffer;
                      break;
                  }
              }
          }
      }
  
      /**
       * Check the size of the buffer and grow if required.
       *
       * @param buffer buffer to be checked.
       * @param size   required size.
       * @return Same buffer instance if the current size is greater than required size. Otherwise a
       * new instance is created and returned.
       */
      private static int[] checkSortBuffer(int[] buffer, int size) {
          if (buffer == null || size > buffer.length) {
              return ArrayUtils.newUnpaddedIntArray(GrowingArrayUtils.growSize(size));
          }
          return buffer;
      }
  
      /**
       * An iterative heap sort implementation. It will sort the spans using first their priority
       * then insertion order. A span with higher priority will be before a span with lower
       * priority. If priorities are the same, the spans will be sorted with insertion order. A
       * span with a lower insertion order will be before a span with a higher insertion order.
       *
       * @param array Span array to be sorted.
       * @param priority Priorities of the spans
       * @param insertionOrder Insertion orders of the spans
       * @param <T> Span object type.
       * @param <T>
       */
      private final <T> void sort(T[] array, int[] priority, int[] insertionOrder) {
          int size = array.length;
          for (int i = size / 2 - 1; i >= 0; i--) {
              siftDown(i, array, size, priority, insertionOrder);
          }
  
          for (int i = size - 1; i > 0; i--) {
              final T tmpSpan =  array[0];
              array[0] = array[i];
              array[i] = tmpSpan;
  
              final int tmpPriority =  priority[0];
              priority[0] = priority[i];
              priority[i] = tmpPriority;
  
              final int tmpOrder =  insertionOrder[0];
              insertionOrder[0] = insertionOrder[i];
              insertionOrder[i] = tmpOrder;
  
              siftDown(0, array, i, priority, insertionOrder);
          }
      }
  
      /**
       * Helper function for heap sort.
       *
       * @param index Index of the element to sift down.
       * @param array Span array to be sorted.
       * @param size Current heap size.
       * @param priority Priorities of the spans
       * @param insertionOrder Insertion orders of the spans
       * @param <T> Span object type.
       */
      private final <T> void siftDown(int index, T[] array, int size, int[] priority,
                                      int[] insertionOrder) {
          int left = 2 * index + 1;
          while (left < size) {
              if (left < size - 1 && compareSpans(left, left + 1, priority, insertionOrder) < 0) {
                  left++;
              }
              if (compareSpans(index, left, priority, insertionOrder) >= 0) {
                  break;
              }
  
              final T tmpSpan =  array[index];
              array[index] = array[left];
              array[left] = tmpSpan;
  
              final int tmpPriority =  priority[index];
              priority[index] = priority[left];
              priority[left] = tmpPriority;
  
              final int tmpOrder =  insertionOrder[index];
              insertionOrder[index] = insertionOrder[left];
              insertionOrder[left] = tmpOrder;
  
              index = left;
              left = 2 * index + 1;
          }
      }
  
      /**
       * Compare two span elements in an array. Comparison is based first on the priority flag of
       * the span, and then the insertion order of the span.
       *
       * @param left Index of the element to compare.
       * @param right Index of the other element to compare.
       * @param priority Priorities of the spans
       * @param insertionOrder Insertion orders of the spans
       * @return
       */
      private final int compareSpans(int left, int right, int[] priority,
                                         int[] insertionOrder) {
          int priority1 = priority[left];
          int priority2 = priority[right];
          if (priority1 == priority2) {
              return Integer.compare(insertionOrder[left], insertionOrder[right]);
          }
          // since high priority has to be before a lower priority, the arguments to compare are
          // opposite of the insertion order check.
          return Integer.compare(priority2, priority1);
      }
  
      /**
       * Return the next offset after <code>start</code> but less than or
       * equal to <code>limit</code> where a span of the specified type
       * begins or ends.
       */
      public int nextSpanTransition(int start, int limit, Class kind) {
          if (mSpanCount == 0) return limit;
          if (kind == null) {
              kind = Object.class;
          }
          return nextSpanTransitionRec(start, limit, kind, treeRoot());
      }
  
      private int nextSpanTransitionRec(int start, int limit, Class kind, int i) {
          if ((i & 1) != 0) {
              // internal tree node
              int left = leftChild(i);
              if (resolveGap(mSpanMax[left]) > start) {
                  limit = nextSpanTransitionRec(start, limit, kind, left);
              }
          }
          if (i < mSpanCount) {
              int st = resolveGap(mSpanStarts[i]);
              int en = resolveGap(mSpanEnds[i]);
              if (st > start && st < limit && kind.isInstance(mSpans[i]))
                  limit = st;
              if (en > start && en < limit && kind.isInstance(mSpans[i]))
                  limit = en;
              if (st < limit && (i & 1) != 0) {
                  limit = nextSpanTransitionRec(start, limit, kind, rightChild(i));
              }
          }
  
          return limit;
      }
  
      /**
       * Return a new CharSequence containing a copy of the specified
       * range of this buffer, including the overlapping spans.
       */
      public CharSequence subSequence(int start, int end) {
          return new SpannableStringBuilder(this, start, end);
      }
  
      /**
       * Copy the specified range of chars from this buffer into the
       * specified array, beginning at the specified offset.
       */
      public void getChars(int start, int end, char[] dest, int destoff) {
          checkRange("getChars", start, end);
  
          if (end <= mGapStart) {
              System.arraycopy(mText, start, dest, destoff, end - start);
          } else if (start >= mGapStart) {
              System.arraycopy(mText, start + mGapLength, dest, destoff, end - start);
          } else {
              System.arraycopy(mText, start, dest, destoff, mGapStart - start);
              System.arraycopy(mText, mGapStart + mGapLength,
                      dest, destoff + (mGapStart - start),
                      end - mGapStart);
          }
      }
  
      /**
       * Return a String containing a copy of the chars in this buffer.
       */
      @Override
      public String toString() {
          int len = length();
          char[] buf = new char[len];
  
          getChars(0, len, buf, 0);
          return new String(buf);
      }
  
      /**
       * Return a String containing a copy of the chars in this buffer, limited to the
       * [start, end[ range.
       * @hide
       */
      @UnsupportedAppUsage
      public String substring(int start, int end) {
          char[] buf = new char[end - start];
          getChars(start, end, buf, 0);
          return new String(buf);
      }
  
      /**
       * Returns the depth of TextWatcher callbacks. Returns 0 when the object is not handling
       * TextWatchers. A return value greater than 1 implies that a TextWatcher caused a change that
       * recursively triggered a TextWatcher.
       */
      public int getTextWatcherDepth() {
          return mTextWatcherDepth;
      }
  
      private void sendBeforeTextChanged(TextWatcher[] watchers, int start, int before, int after) {
          int n = watchers.length;
  
          mTextWatcherDepth++;
          for (int i = 0; i < n; i++) {
              watchers[i].beforeTextChanged(this, start, before, after);
          }
          mTextWatcherDepth--;
      }
  
      private void sendTextChanged(TextWatcher[] watchers, int start, int before, int after) {
          int n = watchers.length;
  
          mTextWatcherDepth++;
          for (int i = 0; i < n; i++) {
              watchers[i].onTextChanged(this, start, before, after);
          }
          mTextWatcherDepth--;
      }
  
      private void sendAfterTextChanged(TextWatcher[] watchers) {
          int n = watchers.length;
  
          mTextWatcherDepth++;
          for (int i = 0; i < n; i++) {
              watchers[i].afterTextChanged(this);
          }
          mTextWatcherDepth--;
      }
  
      private void sendSpanAdded(Object what, int start, int end) {
          SpanWatcher[] recip = getSpans(start, end, SpanWatcher.class);
          int n = recip.length;
  
          for (int i = 0; i < n; i++) {
              recip[i].onSpanAdded(this, what, start, end);
          }
      }
  
      private void sendSpanRemoved(Object what, int start, int end) {
          SpanWatcher[] recip = getSpans(start, end, SpanWatcher.class);
          int n = recip.length;
  
          for (int i = 0; i < n; i++) {
              recip[i].onSpanRemoved(this, what, start, end);
          }
      }
  
      private void sendSpanChanged(Object what, int oldStart, int oldEnd, int start, int end) {
          // The bounds of a possible SpanWatcher are guaranteed to be set before this method is
          // called, so that the order of the span does not affect this broadcast.
          SpanWatcher[] spanWatchers = getSpans(Math.min(oldStart, start),
                  Math.min(Math.max(oldEnd, end), length()), SpanWatcher.class);
          int n = spanWatchers.length;
          for (int i = 0; i < n; i++) {
              spanWatchers[i].onSpanChanged(this, what, oldStart, oldEnd, start, end);
          }
      }
  
      private static String region(int start, int end) {
          return "(" + start + " ... " + end + ")";
      }
  
      private void checkRange(final String operation, int start, int end) {
          if (end < start) {
              throw new IndexOutOfBoundsException(operation + " " +
                      region(start, end) + " has end before start");
          }
  
          int len = length();
  
          if (start > len || end > len) {
              throw new IndexOutOfBoundsException(operation + " " +
                      region(start, end) + " ends beyond length " + len);
          }
  
          if (start < 0 || end < 0) {
              throw new IndexOutOfBoundsException(operation + " " +
                      region(start, end) + " starts before 0");
          }
      }
  
      /*
      private boolean isprint(char c) { // XXX
          if (c >= ' ' && c <= '~')
              return true;
          else
              return false;
      }
  
      private static final int startFlag(int flag) {
          return (flag >> 4) & 0x0F;
      }
  
      private static final int endFlag(int flag) {
          return flag & 0x0F;
      }
  
      public void dump() { // XXX
          for (int i = 0; i < mGapStart; i++) {
              System.out.print('|');
              System.out.print(' ');
              System.out.print(isprint(mText[i]) ? mText[i] : '.');
              System.out.print(' ');
          }
  
          for (int i = mGapStart; i < mGapStart + mGapLength; i++) {
              System.out.print('|');
              System.out.print('(');
              System.out.print(isprint(mText[i]) ? mText[i] : '.');
              System.out.print(')');
          }
  
          for (int i = mGapStart + mGapLength; i < mText.length; i++) {
              System.out.print('|');
              System.out.print(' ');
              System.out.print(isprint(mText[i]) ? mText[i] : '.');
              System.out.print(' ');
          }
  
          System.out.print('\n');
  
          for (int i = 0; i < mText.length + 1; i++) {
              int found = 0;
              int wfound = 0;
  
              for (int j = 0; j < mSpanCount; j++) {
                  if (mSpanStarts[j] == i) {
                      found = 1;
                      wfound = j;
                      break;
                  }
  
                  if (mSpanEnds[j] == i) {
                      found = 2;
                      wfound = j;
                      break;
                  }
              }
  
              if (found == 1) {
                  if (startFlag(mSpanFlags[wfound]) == MARK)
                      System.out.print("(   ");
                  if (startFlag(mSpanFlags[wfound]) == PARAGRAPH)
                      System.out.print("<   ");
                  else
                      System.out.print("[   ");
              } else if (found == 2) {
                  if (endFlag(mSpanFlags[wfound]) == POINT)
                      System.out.print(")   ");
                  if (endFlag(mSpanFlags[wfound]) == PARAGRAPH)
                      System.out.print(">   ");
                  else
                      System.out.print("]   ");
              } else {
                  System.out.print("    ");
              }
          }
  
          System.out.print("\n");
      }
      */
  
      /**
       * Don't call this yourself -- exists for Canvas to use internally.
       * {@hide}
       */
      @Override
      public void drawText(BaseCanvas c, int start, int end, float x, float y, Paint p) {
          checkRange("drawText", start, end);
  
          if (end <= mGapStart) {
              c.drawText(mText, start, end - start, x, y, p);
          } else if (start >= mGapStart) {
              c.drawText(mText, start + mGapLength, end - start, x, y, p);
          } else {
              char[] buf = TextUtils.obtain(end - start);
  
              getChars(start, end, buf, 0);
              c.drawText(buf, 0, end - start, x, y, p);
              TextUtils.recycle(buf);
          }
      }
  
  
      /**
       * Don't call this yourself -- exists for Canvas to use internally.
       * {@hide}
       */
      @Override
      public void drawTextRun(BaseCanvas c, int start, int end, int contextStart, int contextEnd,
              float x, float y, boolean isRtl, Paint p) {
          checkRange("drawTextRun", start, end);
  
          int contextLen = contextEnd - contextStart;
          int len = end - start;
          if (contextEnd <= mGapStart) {
              c.drawTextRun(mText, start, len, contextStart, contextLen, x, y, isRtl, p);
          } else if (contextStart >= mGapStart) {
              c.drawTextRun(mText, start + mGapLength, len, contextStart + mGapLength,
                      contextLen, x, y, isRtl, p);
          } else {
              char[] buf = TextUtils.obtain(contextLen);
              getChars(contextStart, contextEnd, buf, 0);
              c.drawTextRun(buf, start - contextStart, len, 0, contextLen, x, y, isRtl, p);
              TextUtils.recycle(buf);
          }
      }
  
      /**
       * Don't call this yourself -- exists for Paint to use internally.
       * {@hide}
       */
      public float measureText(int start, int end, Paint p) {
          checkRange("measureText", start, end);
  
          float ret;
  
          if (end <= mGapStart) {
              ret = p.measureText(mText, start, end - start);
          } else if (start >= mGapStart) {
              ret = p.measureText(mText, start + mGapLength, end - start);
          } else {
              char[] buf = TextUtils.obtain(end - start);
  
              getChars(start, end, buf, 0);
              ret = p.measureText(buf, 0, end - start);
              TextUtils.recycle(buf);
          }
  
          return ret;
      }
  
      /**
       * Don't call this yourself -- exists for Paint to use internally.
       * {@hide}
       */
      public int getTextWidths(int start, int end, float[] widths, Paint p) {
          checkRange("getTextWidths", start, end);
  
          int ret;
  
          if (end <= mGapStart) {
              ret = p.getTextWidths(mText, start, end - start, widths);
          } else if (start >= mGapStart) {
              ret = p.getTextWidths(mText, start + mGapLength, end - start, widths);
          } else {
              char[] buf = TextUtils.obtain(end - start);
  
              getChars(start, end, buf, 0);
              ret = p.getTextWidths(buf, 0, end - start, widths);
              TextUtils.recycle(buf);
          }
  
          return ret;
      }
  
      /**
       * Don't call this yourself -- exists for Paint to use internally.
       * {@hide}
       */
      public float getTextRunAdvances(int start, int end, int contextStart, int contextEnd, boolean isRtl,
              float[] advances, int advancesPos, Paint p) {
  
          float ret;
  
          int contextLen = contextEnd - contextStart;
          int len = end - start;
  
          if (end <= mGapStart) {
              ret = p.getTextRunAdvances(mText, start, len, contextStart, contextLen,
                      isRtl, advances, advancesPos);
          } else if (start >= mGapStart) {
              ret = p.getTextRunAdvances(mText, start + mGapLength, len,
                      contextStart + mGapLength, contextLen, isRtl, advances, advancesPos);
          } else {
              char[] buf = TextUtils.obtain(contextLen);
              getChars(contextStart, contextEnd, buf, 0);
              ret = p.getTextRunAdvances(buf, start - contextStart, len,
                      0, contextLen, isRtl, advances, advancesPos);
              TextUtils.recycle(buf);
          }
  
          return ret;
      }
  
      /**
       * Returns the next cursor position in the run.  This avoids placing the cursor between
       * surrogates, between characters that form conjuncts, between base characters and combining
       * marks, or within a reordering cluster.
       *
       * <p>The context is the shaping context for cursor movement, generally the bounds of the metric
       * span enclosing the cursor in the direction of movement.
       * <code>contextStart</code>, <code>contextEnd</code> and <code>offset</code> are relative to
       * the start of the string.</p>
       *
       * <p>If cursorOpt is CURSOR_AT and the offset is not a valid cursor position,
       * this returns -1.  Otherwise this will never return a value before contextStart or after
       * contextEnd.</p>
       *
       * @param contextStart the start index of the context
       * @param contextEnd the (non-inclusive) end index of the context
       * @param dir 1 if the run is RTL, otherwise 0
       * @param offset the cursor position to move from
       * @param cursorOpt how to move the cursor, one of CURSOR_AFTER,
       * CURSOR_AT_OR_AFTER, CURSOR_BEFORE,
       * CURSOR_AT_OR_BEFORE, or CURSOR_AT
       * @param p the Paint object that is requesting this information
       * @return the offset of the next position, or -1
       * @deprecated This is an internal method, refrain from using it in your code
       */
      @Deprecated
      public int getTextRunCursor(int contextStart, int contextEnd, int dir, int offset,
              int cursorOpt, Paint p) {
          return getTextRunCursor(contextStart, contextEnd, dir == 1, offset, cursorOpt, p);
      }
  
      /** @hide */
      @Override
      public int getTextRunCursor(int contextStart, int contextEnd, boolean isRtl, int offset,
              int cursorOpt, Paint p) {
  
          int ret;
  
          int contextLen = contextEnd - contextStart;
          if (contextEnd <= mGapStart) {
              ret = p.getTextRunCursor(mText, contextStart, contextLen,
                      isRtl, offset, cursorOpt);
          } else if (contextStart >= mGapStart) {
              ret = p.getTextRunCursor(mText, contextStart + mGapLength, contextLen,
                      isRtl, offset + mGapLength, cursorOpt) - mGapLength;
          } else {
              char[] buf = TextUtils.obtain(contextLen);
              getChars(contextStart, contextEnd, buf, 0);
              ret = p.getTextRunCursor(buf, 0, contextLen,
                      isRtl, offset - contextStart, cursorOpt) + contextStart;
              TextUtils.recycle(buf);
          }
  
          return ret;
      }
  
      // Documentation from interface
      public void setFilters(InputFilter[] filters) {
          if (filters == null) {
              throw new IllegalArgumentException();
          }
  
          mFilters = filters;
      }
  
      // Documentation from interface
      public InputFilter[] getFilters() {
          return mFilters;
      }
  
      // Same as SpannableStringInternal
      @Override
      public boolean equals(Object o) {
          if (o instanceof Spanned &&
                  toString().equals(o.toString())) {
              final Spanned other = (Spanned) o;
              // Check span data
              final Object[] otherSpans = other.getSpans(0, other.length(), Object.class);
              final Object[] thisSpans = getSpans(0, length(), Object.class);
              if (mSpanCount == otherSpans.length) {
                  for (int i = 0; i < mSpanCount; ++i) {
                      final Object thisSpan = thisSpans[i];
                      final Object otherSpan = otherSpans[i];
                      if (thisSpan == this) {
                          if (other != otherSpan ||
                                  getSpanStart(thisSpan) != other.getSpanStart(otherSpan) ||
                                  getSpanEnd(thisSpan) != other.getSpanEnd(otherSpan) ||
                                  getSpanFlags(thisSpan) != other.getSpanFlags(otherSpan)) {
                              return false;
                          }
                      } else if (!thisSpan.equals(otherSpan) ||
                              getSpanStart(thisSpan) != other.getSpanStart(otherSpan) ||
                              getSpanEnd(thisSpan) != other.getSpanEnd(otherSpan) ||
                              getSpanFlags(thisSpan) != other.getSpanFlags(otherSpan)) {
                          return false;
                      }
                  }
                  return true;
              }
          }
          return false;
      }
  
      // Same as SpannableStringInternal
      @Override
      public int hashCode() {
          int hash = toString().hashCode();
          hash = hash * 31 + mSpanCount;
          for (int i = 0; i < mSpanCount; ++i) {
              Object span = mSpans[i];
              if (span != this) {
                  hash = hash * 31 + span.hashCode();
              }
              hash = hash * 31 + getSpanStart(span);
              hash = hash * 31 + getSpanEnd(span);
              hash = hash * 31 + getSpanFlags(span);
          }
          return hash;
      }
  
      // Primitives for treating span list as binary tree
  
      // The spans (along with start and end offsets and flags) are stored in linear arrays sorted
      // by start offset. For fast searching, there is a binary search structure imposed over these
      // arrays. This structure is inorder traversal of a perfect binary tree, a slightly unusual
      // but advantageous approach.
  
      // The value-containing nodes are indexed 0 <= i < n (where n = mSpanCount), thus preserving
      // logic that accesses the values as a contiguous array. Other balanced binary tree approaches
      // (such as a complete binary tree) would require some shuffling of node indices.
  
      // Basic properties of this structure: For a perfect binary tree of height m:
      // The tree has 2^(m+1) - 1 total nodes.
      // The root of the tree has index 2^m - 1.
      // All leaf nodes have even index, all interior nodes odd.
      // The height of a node of index i is the number of trailing ones in i's binary representation.
      // The left child of a node i of height h is i - 2^(h - 1).
      // The right child of a node i of height h is i + 2^(h - 1).
  
      // Note that for arbitrary n, interior nodes of this tree may be >= n. Thus, the general
      // structure of a recursive traversal of node i is:
      // * traverse left child if i is an interior node
      // * process i if i < n
      // * traverse right child if i is an interior node and i < n
  
      private int treeRoot() {
          return Integer.highestOneBit(mSpanCount) - 1;
      }
  
      // (i+1) & ~i is equal to 2^(the number of trailing ones in i)
      private static int leftChild(int i) {
          return i - (((i + 1) & ~i) >> 1);
      }
  
      private static int rightChild(int i) {
          return i + (((i + 1) & ~i) >> 1);
      }
  
      // The span arrays are also augmented by an mSpanMax[] array that represents an interval tree
      // over the binary tree structure described above. For each node, the mSpanMax[] array contains
      // the maximum value of mSpanEnds of that node and its descendants. Thus, traversals can
      // easily reject subtrees that contain no spans overlapping the area of interest.
  
      // Note that mSpanMax[] also has a valid valuefor interior nodes of index >= n, but which have
      // descendants of index < n. In these cases, it simply represents the maximum span end of its
      // descendants. This is a consequence of the perfect binary tree structure.
      private int calcMax(int i) {
          int max = 0;
          if ((i & 1) != 0) {
              // internal tree node
              max = calcMax(leftChild(i));
          }
          if (i < mSpanCount) {
              max = Math.max(max, mSpanEnds[i]);
              if ((i & 1) != 0) {
                  max = Math.max(max, calcMax(rightChild(i)));
              }
          }
          mSpanMax[i] = max;
          return max;
      }
  
      // restores binary interval tree invariants after any mutation of span structure
      private void restoreInvariants() {
          if (mSpanCount == 0) return;
  
          // invariant 1: span starts are nondecreasing
  
          // This is a simple insertion sort because we expect it to be mostly sorted.
          for (int i = 1; i < mSpanCount; i++) {
              if (mSpanStarts[i] < mSpanStarts[i - 1]) {
                  Object span = mSpans[i];
                  int start = mSpanStarts[i];
                  int end = mSpanEnds[i];
                  int flags = mSpanFlags[i];
                  int insertionOrder = mSpanOrder[i];
                  int j = i;
                  do {
                      mSpans[j] = mSpans[j - 1];
                      mSpanStarts[j] = mSpanStarts[j - 1];
                      mSpanEnds[j] = mSpanEnds[j - 1];
                      mSpanFlags[j] = mSpanFlags[j - 1];
                      mSpanOrder[j] = mSpanOrder[j - 1];
                      j--;
                  } while (j > 0 && start < mSpanStarts[j - 1]);
                  mSpans[j] = span;
                  mSpanStarts[j] = start;
                  mSpanEnds[j] = end;
                  mSpanFlags[j] = flags;
                  mSpanOrder[j] = insertionOrder;
                  invalidateIndex(j);
              }
          }
  
          // invariant 2: max is max span end for each node and its descendants
          calcMax(treeRoot());
  
          // invariant 3: mIndexOfSpan maps spans back to indices
          if (mIndexOfSpan == null) {
              mIndexOfSpan = new IdentityHashMap<Object, Integer>();
          }
          for (int i = mLowWaterMark; i < mSpanCount; i++) {
              Integer existing = mIndexOfSpan.get(mSpans[i]);
              if (existing == null || existing != i) {
                  mIndexOfSpan.put(mSpans[i], i);
              }
          }
          mLowWaterMark = Integer.MAX_VALUE;
      }
  
      // Call this on any update to mSpans[], so that mIndexOfSpan can be updated
      private void invalidateIndex(int i) {
          mLowWaterMark = Math.min(i, mLowWaterMark);
      }
  
      private static final InputFilter[] NO_FILTERS = new InputFilter[0];
  
      @GuardedBy("sCachedIntBuffer")
      private static final int[][] sCachedIntBuffer = new int[6][0];
  
      private InputFilter[] mFilters = NO_FILTERS;
  
      @UnsupportedAppUsage
      private char[] mText;
      @UnsupportedAppUsage
      private int mGapStart;
      @UnsupportedAppUsage
      private int mGapLength;
  
      @UnsupportedAppUsage
      private Object[] mSpans;
      @UnsupportedAppUsage
      private int[] mSpanStarts;
      @UnsupportedAppUsage
      private int[] mSpanEnds;
      private int[] mSpanMax;  // see calcMax() for an explanation of what this array stores
      @UnsupportedAppUsage
      private int[] mSpanFlags;
      private int[] mSpanOrder;  // store the order of span insertion
      private int mSpanInsertCount;  // counter for the span insertion
  
      @UnsupportedAppUsage
      private int mSpanCount;
      private IdentityHashMap<Object, Integer> mIndexOfSpan;
      private int mLowWaterMark;  // indices below this have not been touched
  
      // TextWatcher callbacks may trigger changes that trigger more callbacks. This keeps track of
      // how deep the callbacks go.
      private int mTextWatcherDepth;
  
      // TODO These value are tightly related to the public SPAN_MARK/POINT values in {@link Spanned}
      private static final int MARK = 1;
      private static final int POINT = 2;
      private static final int PARAGRAPH = 3;
  
      private static final int START_MASK = 0xF0;
      private static final int END_MASK = 0x0F;
      private static final int START_SHIFT = 4;
  
      // These bits are not (currently) used by SPANNED flags
      private static final int SPAN_ADDED = 0x800;
      private static final int SPAN_START_AT_START = 0x1000;
      private static final int SPAN_START_AT_END = 0x2000;
      private static final int SPAN_END_AT_START = 0x4000;
      private static final int SPAN_END_AT_END = 0x8000;
      private static final int SPAN_START_END_MASK = 0xF000;
  }
  