package com.automattic.simplenote.utils;
import android.os.Handler;
import android.text.Spannable;
import android.text.Spanned;
import android.text.TextUtils;
import android.widget.TextView;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Scanner;
public class MatchOffsetHighlighter implements Runnable {
static public final String CHARSET = "UTF-8";
protected static OnMatchListener sListener = new DefaultMatcher();
private static List<Object> mMatchedSpans = Collections.synchronizedList(new ArrayList<>());
private SpanFactory mFactory;
private Thread mThread;
private TextView mTextView;
private String mMatches;
private int mIndex;
private Spannable mText;
private boolean mStopped = false;
private OnMatchListener mListener = new OnMatchListener() {
@Override
public void onMatch(final SpanFactory factory, final Spannable text, final int start, final int end) {
if (mTextView == null) return;
Handler handler = mTextView.getHandler();
if (handler == null) return;
handler.post(new Runnable() {
@Override
public void run() {
if (mStopped) return;
sListener.onMatch(factory, text, start, end);
}
});
}
};
public MatchOffsetHighlighter(SpanFactory factory, TextView textView) {
mFactory = factory;
mTextView = textView;
}
public static void highlightMatches(Spannable content, String matches, int columnIndex,
SpanFactory factory) {
highlightMatches(content, matches, columnIndex, factory, new DefaultMatcher());
}
public static void highlightMatches(Spannable content, String matches, int columnIndex,
SpanFactory factory, OnMatchListener listener) {
if (TextUtils.isEmpty(matches)) return;
Scanner scanner = new Scanner(matches);
// TODO: keep track of offsets and last index so we don't have to recalculate the entire byte length for every match which is pretty memory intensive
while (scanner.hasNext()) {
if (Thread.interrupted()) return;
int column = scanner.nextInt();
scanner.nextInt(); // token
int start = scanner.nextInt();
int length = scanner.nextInt();
if (column != columnIndex) continue;
int span_start = start + getByteOffset(content, 0, start);
int span_end = span_start + length + getByteOffset(content, start, start + length);
if (Thread.interrupted()) return;
listener.onMatch(factory, content, span_start, span_end);
}
}
// TODO: get ride of memory pressure by preventing the toString()
protected static int getByteOffset(CharSequence text, int start, int end) {
String source = text.toString();
String substring;
int length = source.length();
// starting index cannot be negative
if (start < 0) {
start = 0;
}
if (start > length - 1) {
// if start is past the end of string
return 0;
} else if (end > length - 1) {
// end is past the end of the string, so cap at string's end
substring = source.substring(start, length - 1);
} else {
// start and end are both valid indices
substring = source.substring(start, end);
}
try {
return substring.length() - substring.getBytes(CHARSET).length;
} catch (UnsupportedEncodingException e) {
return 0;
}
}
@Override
public void run() {
highlightMatches(mText, mMatches, mIndex, mFactory, mListener);
}
public void start() {
// if there are no matches, we don't have to do anything
if (TextUtils.isEmpty(mMatches)) return;
mThread = new Thread(this);
mStopped = false;
mThread.start();
}
public void stop() {
mStopped = true;
if (mThread != null) mThread.interrupt();
}
public void highlightMatches(String matches, int columnIndex) {
synchronized (this) {
stop();
mText = mTextView.getEditableText();
mMatches = matches;
mIndex = columnIndex;
start();
}
}
public synchronized void removeMatches() {
stop();
if (mText != null && mMatchedSpans != null) {
for (Object span : mMatchedSpans) {
mText.removeSpan(span);
}
mMatchedSpans.clear();
}
}
public interface SpanFactory {
Object[] buildSpans();
}
public interface OnMatchListener {
void onMatch(SpanFactory factory, Spannable text, int start, int end);
}
private static class DefaultMatcher implements OnMatchListener {
@Override
public void onMatch(SpanFactory factory, Spannable content, int start, int end) {
Object[] spans = factory.buildSpans();
for (Object span : spans) {
if (start >= 0 && end >= start && end <= content.length() - 1) {
content.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
mMatchedSpans.add(span);
}
}
}
}
}