/*
* Overchan Android (Meta Imageboard Client)
* Copyright (C) 2014-2016 miku-nyan <https://github.com/miku-nyan>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package nya.miku.wishmaster.ui.presentation;
import java.text.DateFormat;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import nya.miku.wishmaster.R;
import nya.miku.wishmaster.api.ChanModule;
import nya.miku.wishmaster.api.models.AttachmentModel;
import nya.miku.wishmaster.api.models.PostModel;
import nya.miku.wishmaster.api.models.UrlPageModel;
import nya.miku.wishmaster.api.util.ChanModels;
import nya.miku.wishmaster.common.MainApplication;
import nya.miku.wishmaster.ui.presentation.ClickableURLSpan.URLSpanClickListener;
import nya.miku.wishmaster.ui.presentation.FlowTextHelper.FloatingModel;
import nya.miku.wishmaster.ui.presentation.HtmlParser.ImageGetter;
import nya.miku.wishmaster.ui.theme.ThemeUtils;
import nya.miku.wishmaster.ui.theme.ThemeUtils.ThemeColors;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.Typeface;
import android.os.Parcel;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.RelativeSizeSpan;
import android.text.style.StyleSpan;
/**
* Элемент из PresentationModel (подготовленный к показу пост или ОП-пост треда).<br>
* Конструктор вызывать асинхронно (вне основного потока UI)
* @author miku-nyan
*
*/
public class PresentationItemModel {
static final Pattern REPLY_LINK_FULL_PATTERN = Pattern.compile("<a.+?>(?:>>|>>)(\\w+)(?:.*?)</a>", Pattern.DOTALL);
public static final String ALL_REFERENCES_URI = "references://all?from=";
public static final String POST_REFERER = "refpost://";
private Resources resources = MainApplication.getInstance().resources;
private URLSpanClickListener spanClickListener;
private ImageGetter imageGetter;
private ThemeColors themeColors;
private boolean openSpoilers;
private FloatingModel[] floatingModels;
private String chanName;
private ChanModule chanModule;
private String boardName;
private String threadNumber;
/** Исходная модель поста */
public PostModel sourceModel;
/** Хэш исходной модели поста */
public int sourceModelHash;
/** Удален ли пост */
public boolean isDeleted;
/** Дата и время поста (строковое представление) */
public String dateString;
/** Строка с количеством постов и вложений (для заголовка треда).
* Необходимо предварительно построить ({@link #buildPostsCountString(int, int)}) */
public String postsCountString;
/** Строка с информацией о том, что тред прикреплен или закрыт (для заголовка треда).
* Необходимо предварительно построить ({@link #buildStickyClosedString(boolean, boolean)}) */
public String stickyClosedString;
/** spannable строка с комментарием поста */
public Spanned spannedComment;
/** spanned строка со ссылками на данный пост.
* Необходимо предварительно добавить ссылки ({@link #addReferenceFrom(String)}) и построить ({@link #buildReferencesString()}).
* Построение возможно, только если элемент - пост со страницы треда. */
public Spanned referencesString;
/** spanned строка со ссылкой на список ссылок на данный пост (т.е. количество ответов).
* Необходимо предварительно добавить ссылки ({@link #addReferenceFrom(String)}) и построить ({@link #buildReferencesString()}).
* Построение возможно, только если элемент - пост со страницы треда. */
public Spanned referencesQuantityString;
/** spanned строка с заголовком поста.
* Если нужна, необходимо предварительно построить (метод {@link #buildSpannedHeader(int, int, String)}) */
public Spanned spannedHeader;
/** список ссылок из этого поста */
public Set<String> referencesTo = new HashSet<String>();
private List<String> referencesFrom = new LinkedList<String>();
/** массив хэш-сумм вложений */
public String[] attachmentHashes;
/** общее описание всех значков бейджа (название страны, полит.предпочтения и т.д.). Может быть null */
public String badgeTitle;
/** массив хэшей картинок со значками бейджа (флаг страны, полит.предпочтения и т.д.). Может быть null */
public String[] badgeHashes;
/** true, если активно обтекание картинки-превью текстом комментария */
public boolean floating;
/** true, если элемент является скрытым (скрытый пост/скрытый тред).
* Поле заполняется в PresentationModel, не заполняется автоматически в конструкторе этого класса (PresentationItemModel).
* По умолчанию false. */
public boolean hidden = false;
/** Причина автоскрытия (строка с регулярным выражением), если элемент был скрыт по правилу автоскрытия (только если {@link #hidden} равно true).
* Поле заполняется в PresentationModel, не заполняется автоматически в конструкторе этого класса (PresentationItemModel).
* По умолчанию null. */
public String autohideReason = null;
/**
* Конструктор
* @param source исходная модель {@link PostModel}
* @param chanName название чана (внутреннее название модуля)
* @param boardName название доски (короткое)
* @param threadNumber номер треда, если создаваемый элемент - пост со страницы треда. В противном случае - null
* @param dateFormat объект {@link java.text.DateFormat}, которым будет отформатирована дата/время поста
* @param spanClickListener обработчик нажатий на ссылки
* @param imageGetter загрузчик картинок, содержищихся в теле поста (например, смайлики на 1 апреля)
* @param themeColors объект {@link ThemeUtils.ThemeColors}, содержащий цвета текущей темы
* @param openSpoilers отображать спойлеры открытыми
* @param floatingModels массив из двух моделей обтекания картинки текстом
* Первый элемент - обычная превьюшка, второй - со строкой с дополнительной информацией о вложении (gif, видео, аудио).
* Допустимо значение null, если обтекание не нужно вообще.
*/
public PresentationItemModel(PostModel source, String chanName, String boardName, String threadNumber, DateFormat dateFormat,
URLSpanClickListener spanClickListener, ImageGetter imageGetter, ThemeColors themeColors, boolean openSpoilers,
FloatingModel[] floatingModels, String[] subscriptions) {
this.sourceModel = source;
this.sourceModelHash = ChanModels.hashPostModel(source);
this.isDeleted = source.deleted;
this.chanName = chanName;
this.chanModule = MainApplication.getInstance().getChanModule(chanName);
this.boardName = boardName;
this.threadNumber = threadNumber;
this.spanClickListener = spanClickListener;
this.imageGetter = imageGetter;
this.themeColors = themeColors;
this.openSpoilers = openSpoilers;
this.floatingModels = floatingModels;
this.spannedComment = HtmlParser.createSpanned(source.subject, source.comment, spanClickListener, imageGetter, themeColors, openSpoilers,
POST_REFERER + source.number);
this.dateString = source.timestamp != 0 ? dateFormat.format(source.timestamp) : "";
parseReferences();
if (subscriptions != null) findSubscriptions(subscriptions);
parseBadge();
computeThumbnailsHash();
if (floatingModels != null) {
tryFlow(floatingModels);
}
}
/**
* Класс-контейнер для дополнительной spanned-строки с комментарием и флагом обтекания этой строки
*/
public class SpannedCommentContainer {
public final Spanned spanned;
public final boolean floating;
public SpannedCommentContainer(Spanned spanned, boolean floating) {
this.spanned = spanned;
this.floating = floating;
}
}
/**
* Установить новые модели обтекания картинки текстом (и перестроить текст комментария в случае необходимости)
* @param floatingModels массив из двух моделей обтекания картинки текстом
*/
public void changeFloatingModels(FloatingModel[] floatingModels) {
this.floatingModels = floatingModels;
if (floating) {
this.spannedComment = HtmlParser.createSpanned(sourceModel.subject, sourceModel.comment, spanClickListener, imageGetter,
themeColors, openSpoilers, POST_REFERER + sourceModel.number);
tryFlow(floatingModels);
}
}
/**
* Получить spanned-строку с комментарием поста для вывода на TextView с заданной шириной (для всплывающих диалогов) с корректным обтеканием
* @param textFullWidth ширина текста TextView
* @return объект класса {@link SpannedCommentContainer}
*/
public SpannedCommentContainer getSpannedCommentForCustomWidth(int textFullWidth) {
return getSpannedCommentForCustomWidth(textFullWidth, floatingModels);
}
/**
* Получить spanned-строку с комментарием поста для вывода на TextView с заданной шириной (для всплывающих диалогов) с корректным обтеканием
* @param textFullWidth ширина текста TextView
* @param floatingModels массив из двух моделей обтекания картинки текстом
* @return объект класса {@link SpannedCommentContainer}
*/
public SpannedCommentContainer getSpannedCommentForCustomWidth(int textFullWidth, FloatingModel[] floatingModels) {
if (sourceModel.attachments == null || sourceModel.attachments.length != 1 || floatingModels == null) {
return new SpannedCommentContainer(spannedComment, false);
}
boolean flow;
SpannableStringBuilder builder = HtmlParser.createSpanned(sourceModel.subject, sourceModel.comment, spanClickListener, imageGetter,
themeColors, openSpoilers, POST_REFERER + sourceModel.number);
int attachmentType = sourceModel.attachments[0].type;
if (attachmentType == AttachmentModel.TYPE_IMAGE_STATIC || attachmentType == AttachmentModel.TYPE_OTHER_NOTFILE) {
flow = FlowTextHelper.flowText(builder, floatingModels[0], textFullWidth);
} else {
flow = FlowTextHelper.flowText(builder, floatingModels[1], textFullWidth);
}
return new SpannedCommentContainer(builder, flow);
}
private void tryFlow(FloatingModel[] floatingModels) {
if (sourceModel.attachments != null && sourceModel.attachments.length == 1) {
int attachmentType = sourceModel.attachments[0].type;
if (attachmentType == AttachmentModel.TYPE_IMAGE_STATIC || attachmentType == AttachmentModel.TYPE_OTHER_NOTFILE) {
floating = FlowTextHelper.flowText((SpannableStringBuilder)spannedComment, floatingModels[0]);
} else {
floating = FlowTextHelper.flowText((SpannableStringBuilder)spannedComment, floatingModels[1]);
}
} else {
floating = false;
}
}
private void parseBadge() {
if (sourceModel.icons == null || sourceModel.icons.length == 0) return;
badgeHashes = new String[sourceModel.icons.length];
StringBuilder titleBuilder = new StringBuilder();
boolean firstTitle = true;
for (int i=0; i<sourceModel.icons.length; ++i) {
badgeHashes[i] = ChanModels.hashBadgeIconModel(sourceModel.icons[i], chanName);
if (sourceModel.icons[i].description != null && sourceModel.icons[i].description.length() != 0) {
if (!firstTitle) titleBuilder.append("; ");
titleBuilder.append(sourceModel.icons[i].description);
firstTitle = false;
}
}
if (titleBuilder.length() != 0) {
badgeTitle = titleBuilder.toString();
}
}
private void computeThumbnailsHash() {
int attachmentsCount = sourceModel.attachments != null ? sourceModel.attachments.length : 0;
attachmentHashes = new String[attachmentsCount];
for (int i=0; i<attachmentsCount; ++i) {
AttachmentModel attachment = sourceModel.attachments[i];
attachmentHashes[i] = ChanModels.hashAttachmentModel(attachment);
}
}
private void parseReferences() {
String comment = sourceModel.comment;
if (comment == null) return;
Matcher m = REPLY_LINK_FULL_PATTERN.matcher(comment);
while (m.find()) referencesTo.add(m.group(1));
}
/**
* Построить заголовок поста.
* @param index индекс (номер поста по порядку), или -1, если не требуется выводить индекс
* @param bumpLimit бамп лимит на данной доске (индексы начиная с этого значения будут отображаться красным)
* @param defaultName имя пользователя на данной доске по умолчанию (будет скрываться)
* @param threadNumber номер треда, только если страница является результатом поиска (в противном случае - null)
*/
public void buildSpannedHeader(int index, int bumpLimit, String defaultName, String threadNumber, boolean isSubscribed) {
String opMark = resources.getString(R.string.postitem_op_mark);
String sageMark = resources.getString(R.string.postitem_sage_mark);
int positionStart;
int positionEnd;
SpannableStringBuilder builder = null;
if (index != -1) {
builder = new SpannableStringBuilder(Integer.toString(index));
positionStart = 0;
positionEnd = builder.length();
int indexColor = index < bumpLimit ? themeColors.indexForeground : themeColors.indexOverBumpLimit;
builder.setSpan(new ForegroundColorSpan(indexColor), positionStart, positionEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
builder.setSpan(new StyleSpan(Typeface.BOLD), positionStart, positionEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
positionStart = positionEnd + 1;
builder.append(" ");
} else {
builder = new SpannableStringBuilder();
positionStart = 0;
}
String numberString = threadNumber == null || sourceModel.number.equals(sourceModel.parentThread) ? sourceModel.number :
resources.getString(R.string.postitem_post_number_search_header, sourceModel.number, threadNumber);
positionEnd = positionStart + numberString.length();
builder.append(numberString);
builder.setSpan(new ForegroundColorSpan(themeColors.numberForeground), positionStart, positionEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
if (isSubscribed) builder.setSpan(new SubscriptionSpan(themeColors.subscriptionBackground),
positionStart, positionEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
if (sourceModel.op) {
positionStart = positionEnd + 1;
positionEnd = positionStart + opMark.length();
builder.append(" ").append(opMark);
builder.setSpan(new ForegroundColorSpan(themeColors.opForeground), positionStart, positionEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (sourceModel.sage) {
positionStart = positionEnd + 1;
positionEnd = positionStart + sageMark.length();
builder.append(" ").append(sageMark);
builder.setSpan(new ForegroundColorSpan(themeColors.sageForeground), positionStart, positionEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
builder.setSpan(new RelativeSizeSpan(0.8f), positionStart, positionEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (sourceModel.color != Color.TRANSPARENT) {
positionStart = positionEnd + 1;
positionEnd = positionStart + 1;
builder.append(" \u25A0");
builder.setSpan(new ForegroundColorSpan(Color.TRANSPARENT), positionStart, positionEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
builder.setSpan(new BackgroundColorSpan(sourceModel.color), positionStart, positionEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
String name = sourceModel.name;
if (defaultName != null && name.startsWith(defaultName)) {
int trim = defaultName.length();
while (trim < name.length() && (name.charAt(trim) == ' ' || name.charAt(trim) == '\u00A0')) ++trim;
if (name.startsWith("ID:", trim)) {
trim += 3;
while (trim < name.length() && (name.charAt(trim) == ' ' || name.charAt(trim) == '\u00A0')) ++trim;
}
name = name.substring(trim);
}
if (sourceModel.email != null && !sourceModel.email.equals("") && !(name.equals("") && sourceModel.sage)) {
if (name.equals("")) name = sourceModel.name.equals("") ? sourceModel.email : sourceModel.name;
positionStart = positionEnd + 1;
positionEnd = positionStart + name.length();
builder.append(" ").append(name);
ClickableURLSpan mailLinkSpan = new ClickableURLSpan("mailto:"+sourceModel.email);
builder.setSpan(mailLinkSpan, positionStart, positionEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
builder.setSpan(new ForegroundColorSpan(themeColors.urlLinkForeground), positionStart, positionEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
mailLinkSpan.setOnClickListener(spanClickListener);
} else {
if (!name.equals("")) {
positionStart = positionEnd + 1;
positionEnd = positionStart + name.length();
builder.append(" ").append(name);
builder.setSpan(new ForegroundColorSpan(themeColors.nameForeground), positionStart, positionEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
if (sourceModel.trip != null && !sourceModel.trip.equals("")) {
positionStart = positionEnd + 1;
positionEnd = positionStart + sourceModel.trip.length();
builder.append(" ").append(sourceModel.trip);
builder.setSpan(new ForegroundColorSpan(themeColors.tripForeground), positionStart, positionEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
spannedHeader = builder;
}
/**
* Добавить к данному посту ссылку из другого поста.
* При этом текущая spannable строка со ссылками обнуляется (необходимо будет построить заново).
* @param postNumber номер поста
*/
public void addReferenceFrom(String postNumber) {
referencesFrom.add(postNumber);
referencesString = null;
referencesQuantityString = null;
}
/**
* Построить spannable строку со ссылками на этот пост.
*/
public void buildReferencesString() {
if (threadNumber == null || referencesFrom.isEmpty()) {
referencesString = null;
referencesQuantityString = null;
return;
}
SpannableStringBuilder builder = new SpannableStringBuilder(resources.getString(R.string.postitem_replies));
builder.append(" ");
String prefix = ", ";
boolean first = true;
int positionStart;
int positionEnd = builder.length();
for (String reference : referencesFrom) {
if (!first) {
builder.append(prefix);
positionEnd += prefix.length();
}
first = false;
UrlPageModel urlModel = new UrlPageModel();
urlModel.type = UrlPageModel.TYPE_THREADPAGE;
urlModel.chanName = chanName;
urlModel.boardName = boardName;
urlModel.threadNumber = threadNumber;
urlModel.postNumber = reference;
String refUrl = chanModule.buildUrl(urlModel);
builder.append(">>").append(reference);
positionStart = positionEnd;
positionEnd = builder.length();
ClickableURLSpan refLinkSpan = new ClickableURLSpan(refUrl);
builder.setSpan(refLinkSpan, positionStart, positionEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
builder.setSpan(new ForegroundColorSpan(themeColors.urlLinkForeground), positionStart, positionEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
refLinkSpan.setOnClickListener(spanClickListener);
refLinkSpan.setReferer(POST_REFERER + sourceModel.number);
}
builder.setSpan(new StyleSpan(Typeface.ITALIC), 0, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
referencesString = builder;
int repliesQuantity = referencesFrom.size();
builder = new SpannableStringBuilder(resources.getQuantityString(R.plurals.postitem_replies_quantity, repliesQuantity, repliesQuantity));
ClickableURLSpan refQuantityLinkSpan = new ClickableURLSpan(ALL_REFERENCES_URI + sourceModel.number);
builder.setSpan(refQuantityLinkSpan, 0, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
builder.setSpan(new ForegroundColorSpan(themeColors.urlLinkForeground), 0, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
refQuantityLinkSpan.setOnClickListener(spanClickListener);
referencesQuantityString = builder;
}
/**
* Построить строку с количеством постов и вложений
* @param postsCount количество постов
* @param attachmentsCount количество вложений
*/
public void buildPostsCountString(int postsCount, int attachmentsCount) {
if (postsCount > 0) {
postsCountString = resources.getQuantityString(R.plurals.postitem_posts_quantity, postsCount, postsCount);
if (attachmentsCount > 0) {
postsCountString += ", " + resources.getQuantityString(R.plurals.postitem_files_quantity, attachmentsCount, attachmentsCount);
}
} else {
if (attachmentsCount > 0) {
postsCountString = resources.getQuantityString(R.plurals.postitem_files_quantity, attachmentsCount, attachmentsCount);
} else {
postsCountString = null;
}
}
}
/**
* Построить строку с информацией о том, что тред прикреплен или закрыт (для заголовка треда).
* @param isSticky является ли тред прикреплённым
* @param isClosed является ли тред закрытым для обсуждения
*/
public void buildStickyClosedString(boolean isSticky, boolean isClosed) {
if (isSticky) stickyClosedString = resources.getString(R.string.postitem_sticky_thread);
else stickyClosedString = null;
if (isClosed) {
if (isSticky) stickyClosedString += ", " + resources.getString(R.string.postitem_closed_thread);
else stickyClosedString = resources.getString(R.string.postitem_closed_thread);
}
}
private void findSubscriptions(String[] subscriptions) {
char[] buf = null;
SpannableStringBuilder spanned = (SpannableStringBuilder) spannedComment;
for (String subscription : subscriptions) {
if (!referencesTo.contains(subscription)) continue;
ClickableURLSpan[] spans = spanned.getSpans(0, spanned.length(), ClickableURLSpan.class);
if (spans == null || spans.length == 0) continue;
char[] search = (">>" + subscription).toCharArray();
if (buf == null || buf.length != search.length) buf = new char[search.length];
for (ClickableURLSpan span : spans) {
int startIndex = spanned.getSpanStart(span);
if (startIndex + buf.length > spanned.length()) continue;
spanned.getChars(startIndex, startIndex + buf.length, buf, 0);
if (Arrays.equals(search, buf)) {
spanned.setSpan(new SubscriptionSpan(themeColors.subscriptionBackground),
startIndex, startIndex + buf.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
}
/**
* В случае необходимости перестроить комментарий и заголовок поста (если был построен) после добавления подписки на пост newSubscription.
* Если этот содержит ссылки на пост newSubscription, они будут выделены в тексте комментария;
* если этот пост сам является newSubscription, он будет выделен в заголовке.
* @param newSubscription пост, на который добавлена подписка
*/
public void onSubscribe(String newSubscription) {
if (spannedHeader != null && newSubscription.equals(sourceModel.number)) {
SpannableStringBuilder spanned = (SpannableStringBuilder) spannedHeader;
int start = spanned.toString().indexOf(newSubscription);
if (start >= 0) spanned.setSpan(new SubscriptionSpan(themeColors.subscriptionBackground),
start, start + newSubscription.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (!referencesTo.contains(newSubscription)) return;
SpannableStringBuilder spanned = (SpannableStringBuilder) spannedComment;
ClickableURLSpan[] spans = spanned.getSpans(0, spanned.length(), ClickableURLSpan.class);
if (spans == null || spans.length == 0) return;
char[] search = (">>" + newSubscription).toCharArray();
char[] buf = new char[search.length];
for (ClickableURLSpan span : spans) {
int startIndex = spanned.getSpanStart(span);
if (startIndex + buf.length > spanned.length()) continue;
spanned.getChars(startIndex, startIndex + buf.length, buf, 0);
if (Arrays.equals(search, buf)) {
spanned.setSpan(new SubscriptionSpan(themeColors.subscriptionBackground),
startIndex, startIndex + buf.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
/**
* В случае необходимости перестроить комментарий и заголовок поста (если был построен) после удаления подписки на пост oldSubscription.
* Если этот содержит выделенные ссылки на пост oldSubscription в тексте комментария, выделение будет снято;
* если этот пост сам является oldSubscription, выделение будет снято в заголовке.
* @param oldSubscription пост, на который удалена подписка
*/
public void onUnsubscribe(String oldSubscription) {
if (spannedHeader != null && oldSubscription.equals(sourceModel.number)) {
SpannableStringBuilder spanned = (SpannableStringBuilder) spannedHeader;
for (SubscriptionSpan span : spanned.getSpans(0, spanned.length(), SubscriptionSpan.class))
spanned.removeSpan(span);
}
if (!referencesTo.contains(oldSubscription)) return;
SpannableStringBuilder spanned = (SpannableStringBuilder) spannedComment;
SubscriptionSpan[] spans = spanned.getSpans(0, spanned.length(), SubscriptionSpan.class);
if (spans == null || spans.length == 0) return;
char[] search = (">>" + oldSubscription).toCharArray();
char[] buf = new char[search.length];
for (SubscriptionSpan span : spans) {
int startIndex = spannedComment.getSpanStart(span);
if (startIndex + buf.length > spanned.length()) continue;
spanned.getChars(startIndex, startIndex + buf.length, buf, 0);
if (Arrays.equals(search, buf)) spanned.removeSpan(span);
}
}
private static class SubscriptionSpan extends BackgroundColorSpan {
public SubscriptionSpan(int color) {
super(color);
}
public SubscriptionSpan(Parcel src) {
super(src);
}
}
}