private CharSequence applySpans(CharSequence text, int offset) {
    List<HighLight> highLights = highlightManager.getHighLights(bookView.getFileName());
    int end = offset + text.length() - 1;

    for (final HighLight highLight : highLights) {
      if (highLight.getIndex() == bookView.getIndex()
          && highLight.getStart() >= offset
          && highLight.getStart() < end) {

        LOG.debug(
            "Got highlight from "
                + highLight.getStart()
                + " to "
                + highLight.getEnd()
                + " with offset "
                + offset);

        int highLightEnd = Math.min(end, highLight.getEnd());

        ((Spannable) text)
            .setSpan(
                new HighlightSpan(highLight),
                highLight.getStart() - offset,
                highLightEnd - offset,
                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
      }
    }

    return text;
  }
    public void run() {
      Spannable buf = mBuffer;

      if (buf != null) {
        int st = Selection.getSelectionStart(buf);
        int en = Selection.getSelectionEnd(buf);

        int start = buf.getSpanStart(TextKeyListener.ACTIVE);
        int end = buf.getSpanEnd(TextKeyListener.ACTIVE);

        if (st == start && en == end) {
          Selection.setSelection(buf, Selection.getSelectionEnd(buf));
        }

        buf.removeSpan(Timeout.this);
      }
    }
Beispiel #3
0
 public static CharSequence getHtmlText(String text) {
   // fixes an android bug (?): text layout fails on text with nested style tags
   text = removeNestedTags(text, new String[] {"i", "b", "strong"});
   final Spanned htmlText = Html.fromHtml(text);
   if (htmlText.getSpans(0, htmlText.length(), URLSpan.class).length == 0) {
     return htmlText;
   }
   final Spannable newHtmlText = Spannable.Factory.getInstance().newSpannable(htmlText);
   for (URLSpan span : newHtmlText.getSpans(0, newHtmlText.length(), URLSpan.class)) {
     final int start = newHtmlText.getSpanStart(span);
     final int end = newHtmlText.getSpanEnd(span);
     final int flags = newHtmlText.getSpanFlags(span);
     final String url = NetworkLibrary.Instance().rewriteUrl(span.getURL(), true);
     newHtmlText.removeSpan(span);
     newHtmlText.setSpan(new URLSpan(url), start, end, flags);
   }
   return newHtmlText;
 }
Beispiel #4
0
 private static void fixLinks(Spannable spannable) {
   for (URLSpan span : spannable.getSpans(0, spannable.length(), URLSpan.class)) {
     final String url = span.getURL();
     int start = spannable.getSpanStart(span);
     int end = spannable.getSpanEnd(span);
     int flags = spannable.getSpanFlags(span);
     URLSpan newSpan =
         new URLSpan(url) {
           @Override
           public void updateDrawState(TextPaint paramTextPaint) {
             super.updateDrawState(paramTextPaint);
             paramTextPaint.setUnderlineText(false);
             paramTextPaint.setColor(0xff006FC8);
           }
         };
     spannable.removeSpan(span);
     spannable.setSpan(newSpan, start, end, flags);
   }
 }
Beispiel #5
0
  /**
   * If a translator has messed up the edges of paragraph-level markup, fix it to actually cover the
   * entire paragraph that it is attached to instead of just whatever range they put it on.
   */
  private static void addParagraphSpan(Spannable buffer, Object what, int start, int end) {
    int len = buffer.length();

    if (start != 0 && start != len && buffer.charAt(start - 1) != '\n') {
      for (start--; start > 0; start--) {
        if (buffer.charAt(start - 1) == '\n') {
          break;
        }
      }
    }

    if (end != 0 && end != len && buffer.charAt(end - 1) != '\n') {
      for (end++; end < len; end++) {
        if (buffer.charAt(end - 1) == '\n') {
          break;
        }
      }
    }

    buffer.setSpan(what, start, end, Spannable.SPAN_PARAGRAPH);
  }
  /**
   * Make a layout for the transformed text (password transformation being the primary example of a
   * transformation) that will be updated as the base text is changed. If ellipsize is non-null, the
   * Layout will ellipsize the text down to ellipsizedWidth. * *@hide
   */
  public DynamicLayout(
      CharSequence base,
      CharSequence display,
      TextPaint paint,
      int width,
      Alignment align,
      TextDirectionHeuristic textDir,
      float spacingmult,
      float spacingadd,
      boolean includepad,
      TextUtils.TruncateAt ellipsize,
      int ellipsizedWidth) {
    super(
        (ellipsize == null)
            ? display
            : (display instanceof Spanned)
                ? new SpannedEllipsizer(display)
                : new Ellipsizer(display),
        paint,
        width,
        align,
        textDir,
        spacingmult,
        spacingadd);

    mBase = base;
    mDisplay = display;

    if (ellipsize != null) {
      mInts = new PackedIntVector(COLUMNS_ELLIPSIZE);
      mEllipsizedWidth = ellipsizedWidth;
      mEllipsizeAt = ellipsize;
    } else {
      mInts = new PackedIntVector(COLUMNS_NORMAL);
      mEllipsizedWidth = width;
      mEllipsizeAt = null;
    }

    mObjects = new PackedObjectVector<Directions>(1);

    mIncludePad = includepad;

    /*
     * This is annoying, but we can't refer to the layout until
     * superclass construction is finished, and the superclass
     * constructor wants the reference to the display text.
     *
     * This will break if the superclass constructor ever actually
     * cares about the content instead of just holding the reference.
     */
    if (ellipsize != null) {
      Ellipsizer e = (Ellipsizer) getText();

      e.mLayout = this;
      e.mWidth = ellipsizedWidth;
      e.mMethod = ellipsize;
      mEllipsize = true;
    }

    // Initial state is a single line with 0 characters (0 to 0),
    // with top at 0 and bottom at whatever is natural, and
    // undefined ellipsis.

    int[] start;

    if (ellipsize != null) {
      start = new int[COLUMNS_ELLIPSIZE];
      start[ELLIPSIS_START] = ELLIPSIS_UNDEFINED;
    } else {
      start = new int[COLUMNS_NORMAL];
    }

    Directions[] dirs = new Directions[] {DIRS_ALL_LEFT_TO_RIGHT};

    Paint.FontMetricsInt fm = paint.getFontMetricsInt();
    int asc = fm.ascent;
    int desc = fm.descent;

    start[DIR] = DIR_LEFT_TO_RIGHT << DIR_SHIFT;
    start[TOP] = 0;
    start[DESCENT] = desc;
    mInts.insertAt(0, start);

    start[TOP] = desc - asc;
    mInts.insertAt(1, start);

    mObjects.insertAt(0, dirs);

    // Update from 0 characters to whatever the real text is

    reflow(base, 0, 0, base.length());

    if (base instanceof Spannable) {
      if (mWatcher == null) mWatcher = new ChangeWatcher(this);

      // Strip out any watchers for other DynamicLayouts.
      Spannable sp = (Spannable) base;
      ChangeWatcher[] spans = sp.getSpans(0, sp.length(), ChangeWatcher.class);
      for (int i = 0; i < spans.length; i++) sp.removeSpan(spans[i]);

      sp.setSpan(
          mWatcher,
          0,
          base.length(),
          Spannable.SPAN_INCLUSIVE_INCLUSIVE | (PRIORITY << Spannable.SPAN_PRIORITY_SHIFT));
    }
  }
Beispiel #7
0
    public void build(DialogWireframe description, int w, int h, TelegramApplication application) {
      layoutH = h;
      layoutW = w;

      if (description.getPeerType() == PeerType.PEER_USER) {
        if (description.getPeerId() == 333000) {
          isHighlighted = false;
        } else {
          User user = description.getDialogUser();
          isHighlighted = user.getLinkType() == LinkType.FOREIGN;
        }
        isGroup = false;
        isEncrypted = false;
      } else if (description.getPeerType() == PeerType.PEER_CHAT) {
        isHighlighted = false;
        isGroup = true;
        isEncrypted = false;
      } else if (description.getPeerType() == PeerType.PEER_USER_ENCRYPTED) {
        isHighlighted = false;
        isGroup = false;
        isEncrypted = true;
      }

      isBodyHighlighted = description.getContentType() != ContentType.MESSAGE_TEXT;

      if (description.getUnreadCount() != 0 && !description.isMine()) {
        isUnreadIn = true;
      } else {
        isUnreadIn = false;
      }

      time = org.telegram.android.ui.TextUtil.formatDate(description.getDate(), application);
      isRtl = application.isRTL();

      if (IS_LARGE) {
        layoutAvatarWidth = px(64);
        layoutPadding = application.getResources().getDimensionPixelSize(R.dimen.dialogs_padding);
        layoutBodyPadding = layoutAvatarWidth + layoutPadding + px(12);
        layoutAvatarTop = px(8);
        layoutTitleTop = px(34);
        layoutMainTop = px(60);
        layoutTimeTop = px(34);

        layoutMarkTop = px(44);
        layoutMarkBottom = layoutMarkTop + px(22);
        layoutMarkTextTop = layoutMarkTop + px(18);
      } else {
        layoutAvatarWidth = px(54);
        layoutPadding = application.getResources().getDimensionPixelSize(R.dimen.dialogs_padding);
        layoutBodyPadding = layoutAvatarWidth + layoutPadding + px(12);
        layoutAvatarTop = px(8);
        layoutTitleTop = px(30);
        layoutMainTop = px(54);
        layoutTimeTop = px(30);

        layoutMarkTop = px(38);
        layoutMarkBottom = layoutMarkTop + px(22);
        layoutMarkTextTop = layoutMarkTop + px(18);
      }

      layoutMainContentTop = (int) (layoutMainTop + bodyPaint.getFontMetrics().ascent);
      layoutTitleLayoutTop = (int) (layoutTitleTop + titlePaint.getFontMetrics().ascent);
      layoutStateTop = layoutTimeTop - px(10);
      layoutClockTop = layoutTimeTop - px(12);
      layoutEncryptedTop = layoutTimeTop - px(14);

      if (isRtl) {
        layoutAvatarLeft = w - layoutPadding - layoutAvatarWidth;
      } else {
        layoutAvatarLeft = layoutPadding;
      }

      int timeWidth = (int) unreadClockPaint.measureText(time);
      if (isRtl) {
        layoutTimeLeft = layoutPadding;
        layoutStateLeftDouble = layoutPadding + timeWidth + px(2);
        layoutStateLeft = layoutStateLeftDouble + px(6);
        layoutClockLeft = layoutPadding + timeWidth + px(2);
      } else {
        layoutTimeLeft = w - layoutPadding - timeWidth;
        layoutClockLeft = w - layoutPadding - timeWidth - px(14);
        layoutStateLeft = w - layoutPadding - timeWidth - px(16);
        layoutStateLeftDouble = w - layoutPadding - timeWidth - px(6 + 16);
      }

      layoutMarkRadius = px(2);
      if (description.isErrorState()
          || (description.getMessageState() == MessageState.FAILURE && description.isMine())) {
        layoutMarkWidth = px(22);
        if (isRtl) {
          layoutMarkLeft = layoutPadding; // getMeasuredWidth() - layoutMarkWidth - getPx(80);
        } else {
          layoutMarkLeft = w - layoutMarkWidth - layoutPadding;
        }
      } else {
        if (description.getUnreadCount() > 0) {
          if (description.getUnreadCount() >= 1000) {
            unreadCountText =
                I18nUtil.getInstance().correctFormatNumber(description.getUnreadCount() / 1000)
                    + "K";
          } else {
            unreadCountText =
                I18nUtil.getInstance().correctFormatNumber(description.getUnreadCount());
          }
          int width = (int) counterTitlePaint.measureText(unreadCountText);
          Rect r = new Rect();
          counterTitlePaint.getTextBounds(unreadCountText, 0, unreadCountText.length(), r);
          layoutMarkTextTop =
              layoutMarkTop + (layoutMarkBottom - layoutMarkTop + r.top) / 2 - r.top;
          if (width < px(22 - 14)) {
            layoutMarkWidth = px(22);
          } else {
            layoutMarkWidth = px(14) + width;
          }
          layoutMarkTextLeft = (layoutMarkWidth - width) / 2;

          if (isRtl) {
            layoutMarkLeft = layoutPadding; // getMeasuredWidth() - layoutMarkWidth - getPx(80);
          } else {
            layoutMarkLeft = w - layoutMarkWidth - layoutPadding;
          }
        } else {
          layoutMarkLeft = 0;
          layoutMarkWidth = 0;
        }
      }
      layoutMarkRect.set(
          layoutMarkLeft, layoutMarkTop, layoutMarkLeft + layoutMarkWidth, layoutMarkBottom);

      if (description.getPeerType() == PeerType.PEER_USER_ENCRYPTED) {
        if (isRtl) {
          if (description.isMine()) {
            layoutTitleLeft = timeWidth + px(16) + px(16);
          } else {
            layoutTitleLeft = timeWidth + px(12);
          }
          layoutTitleWidth = w - layoutTitleLeft - layoutBodyPadding - px(14) - px(6);
          layoutEncryptedLeft = w - layoutBodyPadding - px(12);
        } else {
          layoutTitleLeft = layoutBodyPadding + px(16);
          if (description.isMine()) {
            layoutTitleWidth = w - layoutTitleLeft - timeWidth - px(24);
          } else {
            layoutTitleWidth = w - layoutTitleLeft - timeWidth - px(12);
          }

          layoutEncryptedLeft = layoutBodyPadding + px(2);
        }
      } else {
        if (isRtl) {
          if (description.isMine()) {
            layoutTitleLeft = timeWidth + px(16) + px(16);
          } else {
            layoutTitleLeft = timeWidth + px(12);
          }
          layoutTitleWidth = w - layoutTitleLeft - layoutBodyPadding;
        } else {
          layoutTitleLeft = layoutBodyPadding;
          if (description.isMine()) {
            layoutTitleWidth = w - layoutTitleLeft - timeWidth - px(24) - px(12);
          } else {
            layoutTitleWidth = w - layoutTitleLeft - timeWidth - px(12);
          }
        }
      }

      layoutMainWidth = w - layoutBodyPadding - layoutPadding;
      if (isRtl) {
        layoutMainLeft = w - layoutMainWidth - layoutBodyPadding;
        if (layoutMarkWidth != 0) {
          layoutMainLeft += layoutMarkWidth + px(8);
          layoutMainWidth -= layoutMarkWidth + px(8);
        }
      } else {
        layoutMainLeft = layoutBodyPadding;
        if (layoutMarkWidth != 0) {
          layoutMainWidth -= layoutMarkWidth + px(8);
        }
      }

      avatarRect.set(
          layoutAvatarLeft,
          layoutAvatarTop,
          layoutAvatarLeft + layoutAvatarWidth,
          layoutAvatarTop + layoutAvatarWidth);

      // Building text layouts
      {
        String message = description.getMessage();
        if (message.length() > 150) {
          message = message.substring(0, 150) + "...";
        }
        message = message.replace("\n", " ");

        TextPaint bodyTextPaint;
        if (isBodyHighlighted) {
          bodyTextPaint = bodyHighlightPaint;
        } else {
          if (HIGHLIGHT_UNDEAD) {
            if (isUnreadIn) {
              bodyTextPaint = bodyUnreadPaint;
            } else {
              bodyTextPaint = bodyPaint;
            }
          } else {
            bodyTextPaint = bodyPaint;
          }
        }

        int nameLength = 0;

        if (description.getContentType() != ContentType.MESSAGE_SYSTEM) {
          if (description.isMine()) {
            String name = application.getResources().getString(R.string.st_dialog_you);
            nameLength = BidiFormatter.getInstance().unicodeWrap(name).length();
            message =
                BidiFormatter.getInstance().unicodeWrap(name)
                    + ": "
                    + BidiFormatter.getInstance().unicodeWrap(message);
          } else {
            if (isGroup) {
              User user = description.getSender();
              nameLength = BidiFormatter.getInstance().unicodeWrap(user.getFirstName()).length();
              message =
                  BidiFormatter.getInstance().unicodeWrap(user.getFirstName().replace("\n", " "))
                      + ": "
                      + BidiFormatter.getInstance().unicodeWrap(message);
            }
          }
        }

        String preSequence =
            TextUtils.ellipsize(message, bodyTextPaint, layoutMainWidth, TextUtils.TruncateAt.END)
                .toString();

        //                Spannable sequence =
        // application.getEmojiProcessor().processEmojiCutMutable(preSequence,
        // EmojiProcessor.CONFIGURATION_DIALOGS);
        //                if (nameLength != 0) {
        //                    sequence.setSpan(new ForegroundColorSpan(HIGHLIGHT_COLOR), 0,
        // Math.min(nameLength, sequence.length()), Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
        //                }
        //                CharSequence resSequence = TextUtils.ellipsize(sequence, bodyTextPaint,
        // layoutMainWidth, TextUtils.TruncateAt.END);
        //                bodyLayout = new StaticLayout(resSequence, bodyTextPaint, layoutMainWidth,
        // Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);
        //                bodyString = null;

        Spannable sequence =
            application
                .getEmojiProcessor()
                .processEmojiCutMutable(preSequence, EmojiProcessor.CONFIGURATION_DIALOGS);
        if (nameLength != 0) {
          sequence.setSpan(
              new ForegroundColorSpan(HIGHLIGHT_COLOR),
              0,
              Math.min(nameLength, sequence.length()),
              Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
        }

        CharSequence resSequence =
            TextUtils.ellipsize(sequence, bodyTextPaint, layoutMainWidth, TextUtils.TruncateAt.END);
        bodyLayout =
            new StaticLayout(
                resSequence,
                bodyTextPaint,
                layoutMainWidth,
                Layout.Alignment.ALIGN_NORMAL,
                1.0f,
                0.0f,
                false);
        bodyString = null;
        //                if (EmojiProcessor.containsEmoji(message)) {
        //                    Spannable sequence =
        // application.getEmojiProcessor().processEmojiCutMutable(preSequence,
        // EmojiProcessor.CONFIGURATION_DIALOGS);
        //                    if (nameLength != 0) {
        //                        sequence.setSpan(new ForegroundColorSpan(HIGHLIGHT_COLOR), 0,
        // Math.min(nameLength, sequence.length()), Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
        //                    }
        //
        //                    CharSequence resSequence = TextUtils.ellipsize(sequence,
        // bodyTextPaint, layoutMainWidth, TextUtils.TruncateAt.END);
        //                    bodyLayout = new StaticLayout(resSequence, bodyTextPaint,
        // layoutMainWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);
        //                    bodyString = null;
        //                } else {
        //                    bodyString = preSequence;
        //                    bodyLayout = null;
        //                }
      }

      // Title
      {
        String title = description.getDialogTitle();
        if (title.length() > 150) {
          title = title.substring(150) + "...";
        }
        title = title.replace("\n", " ");

        TextPaint paint =
            isEncrypted ? titleEncryptedPaint : (isHighlighted ? titleHighlightPaint : titlePaint);

        //                Spannable preSequence =
        // application.getEmojiProcessor().processEmojiCutMutable(title,
        // EmojiProcessor.CONFIGURATION_DIALOGS);
        //                CharSequence sequence = TextUtils.ellipsize(preSequence, paint,
        // layoutTitleWidth, TextUtils.TruncateAt.END);
        //                titleLayout = new StaticLayout(sequence, paint, layoutTitleWidth,
        // Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);
        //                titleString = null;

        if (EmojiProcessor.containsEmoji(title)) {
          Spannable preSequence =
              application
                  .getEmojiProcessor()
                  .processEmojiCutMutable(title, EmojiProcessor.CONFIGURATION_DIALOGS);
          CharSequence sequence =
              TextUtils.ellipsize(preSequence, paint, layoutTitleWidth, TextUtils.TruncateAt.END);
          titleLayout =
              new StaticLayout(
                  sequence,
                  paint,
                  layoutTitleWidth,
                  Layout.Alignment.ALIGN_NORMAL,
                  1.0f,
                  0.0f,
                  false);
          titleString = null;
        } else {
          titleString =
              TextUtils.ellipsize(title, paint, layoutTitleWidth, TextUtils.TruncateAt.END)
                  .toString();
          titleLayout = null;
        }
      }

      // Placeholder
      placeHolderName = description.getDialogName();
      placeHolderColor = Placeholders.getBgColor(description.getPeerId());

      if (placeHolderName.length() > 0) {
        usePlaceholder = true;
        placeholderLeft = layoutAvatarLeft + layoutAvatarWidth / 2;
        Rect rect = new Rect();
        placeholderTextPaint.getTextBounds(placeHolderName, 0, placeHolderName.length(), rect);
        placeholderTop = layoutAvatarTop + (layoutAvatarWidth / 2 + ((rect.bottom - rect.top) / 2));
      } else {
        usePlaceholder = false;
      }
    }
  private void build(int width) {

    positions.clear();
    width = width - getPaddingLeft() - getPaddingRight();

    if (width <= 0 || TextUtils.isEmpty(text)) {
      truncatedLayout = expandedLayout = null;
      return;
    }

    for (BubbleSpan span : spans) {
      span.resetWidth(width);
    }

    try {

      truncatedLayout =
          expandedLayout =
              new StaticLayout(
                  text, textPaint, width, Layout.Alignment.ALIGN_NORMAL, lineSpacing, 1, false);

      if (maxLines > 0 && truncatedLayout.getLineCount() > maxLines) {

        int lineEnd = truncatedLayout.getLineEnd(maxLines - 1);
        int offset = -1;
        StaticLayout sl =
            new StaticLayout(
                moreText, textPaint, width, Layout.Alignment.ALIGN_NORMAL, lineSpacing, 1, false);

        sl.getWidth();
        while (truncatedLayout.getLineCount() > maxLines && lineEnd > 0) {

          if (offset == -1
              && truncatedLayout.getLineWidth(maxLines - 1) + sl.getLineWidth(0) > width) {

            offset =
                truncatedLayout.getOffsetForHorizontal(maxLines - 1, width - sl.getLineWidth(0));

            lineEnd = offset;

          } else if (offset > 0) {
            lineEnd--;
          }

          SpannableStringBuilder textTruncated =
              new SpannableStringBuilder(text.subSequence(0, lineEnd));
          textTruncated.append(moreText);

          truncatedLayout =
              new StaticLayout(
                  textTruncated,
                  textPaint,
                  width,
                  Layout.Alignment.ALIGN_NORMAL,
                  lineSpacing,
                  1,
                  false);
        }
      }
    } catch (java.lang.ArrayIndexOutOfBoundsException e) {
      return;
    }

    if (truncated) {
      recomputeSpans((Spannable) truncatedLayout.getText());
    } else {
      recomputeSpans((Spannable) expandedLayout.getText());
    }

    for (BubbleSpan span : spans) {
      positions.put(span, span.rect(this));
    }
  }
  private void recomputeSpans(Spannable text) {

    spans.clear();

    Collections.addAll(spans, text.getSpans(0, text.length(), BubbleSpan.class));
  }
 public void onSpanChanged(Spannable buf, Object what, int s, int e, int start, int stop) {
   if (what == Selection.SELECTION_END) {
     buf.removeSpan(TextKeyListener.ACTIVE);
     removeTimeouts(buf);
   }
 }
 /** Resets all meta state to inactive. */
 public static void resetMetaState(Spannable text) {
   text.removeSpan(CAP);
   text.removeSpan(ALT);
   text.removeSpan(SYM);
   text.removeSpan(SELECTING);
 }
    private void reflow(CharSequence s, int where, int before, int after) {
      DynamicLayout ml = mLayout.get();

      if (ml != null) ml.reflow(s, where, before, after);
      else if (s instanceof Spannable) ((Spannable) s).removeSpan(this);
    }
 /**
  * Stop selecting text. This does not actually collapse the selection; call {@link
  * android.text.Selection#setSelection} too.
  *
  * @hide pending API review
  */
 public static void stopSelecting(View view, Spannable content) {
   content.removeSpan(SELECTING);
 }
 /**
  * Start selecting text.
  *
  * @hide pending API review
  */
 public static void startSelecting(View view, Spannable content) {
   content.setSpan(SELECTING, 0, 0, PRESSED);
 }
  private static void resetLock(Spannable content, Object what) {
    int current = content.getSpanFlags(what);

    if (current == LOCKED) content.removeSpan(what);
  }
  private static void adjust(Spannable content, Object what) {
    int current = content.getSpanFlags(what);

    if (current == PRESSED) content.setSpan(what, 0, 0, USED);
    else if (current == RELEASED) content.removeSpan(what);
  }
  private static void removeTimeouts(Spannable buf) {
    Timeout[] timeout = buf.getSpans(0, buf.length(), Timeout.class);

    for (int i = 0; i < timeout.length; i++) {
      Timeout t = timeout[i];

      t.removeCallbacks(t);
      t.mBuffer = null;
      buf.removeSpan(t);
    }
  }