package de.danoeh.antennapod.core.util.playback;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.util.Log;
import android.util.TypedValue;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.core.util.Converter;
import de.danoeh.antennapod.core.util.ShownotesProvider;
/**
* Connects chapter information and shownotes of a shownotesProvider, for example by making it possible to use the
* shownotes to navigate to another position in the podcast or by highlighting certain parts of the shownotesProvider's
* shownotes.
* <p/>
* A timeline object needs a shownotesProvider from which the chapter information is retrieved and shownotes are generated.
*/
public class Timeline {
private static final String TAG = "Timeline";
private static final String WEBVIEW_STYLE = "@font-face { font-family: 'Roboto-Light'; src: url('file:///android_asset/Roboto-Light.ttf'); } * { color: %s; font-family: roboto-Light; font-size: 13pt; } a { font-style: normal; text-decoration: none; font-weight: normal; color: #00A8DF; } a.timecode { color: #669900; } img { display: block; margin: 10 auto; max-width: %s; height: auto; } body { margin: %dpx %dpx %dpx %dpx; }";
private ShownotesProvider shownotesProvider;
private final String noShownotesLabel;
private final String colorPrimaryString;
private final String colorSecondaryString;
private final int pageMargin;
public Timeline(Context context, ShownotesProvider shownotesProvider) {
if (shownotesProvider == null) throw new IllegalArgumentException("shownotesProvider = null");
this.shownotesProvider = shownotesProvider;
noShownotesLabel = context.getString(R.string.no_shownotes_label);
TypedArray res = context.getTheme().obtainStyledAttributes(
new int[]{ android.R.attr.textColorPrimary});
@ColorInt int col = res.getColor(0, 0);
colorPrimaryString = "rgba(" + Color.red(col) + "," + Color.green(col) + "," +
Color.blue(col) + "," + (Color.alpha(col)/256.0) + ")";
res.recycle();
res = context.getTheme().obtainStyledAttributes(
new int[]{android.R.attr.textColorSecondary});
col = res.getColor(0, 0);
colorSecondaryString = "rgba(" + Color.red(col) + "," + Color.green(col) + "," +
Color.blue(col) + "," + (Color.alpha(col)/256.0) + ")";
res.recycle();
pageMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8,
context.getResources().getDisplayMetrics()
);
}
private static final Pattern TIMECODE_LINK_REGEX = Pattern.compile("antennapod://timecode/((\\d+))");
private static final String TIMECODE_LINK = "<a class=\"timecode\" href=\"antennapod://timecode/%d\">%s</a>";
private static final Pattern TIMECODE_REGEX = Pattern.compile("\\b(?:(?:(([0-9][0-9])):))?(([0-9][0-9])):(([0-9][0-9]))\\b");
private static final Pattern LINE_BREAK_REGEX = Pattern.compile("<br */?>");
/**
* Applies an app-specific CSS stylesheet and adds timecode links (optional).
* <p/>
* This method does NOT change the original shownotes string of the shownotesProvider object and it should
* also not be changed by the caller.
*
* @param addTimecodes True if this method should add timecode links
* @return The processed HTML string.
*/
public String processShownotes(final boolean addTimecodes) {
final Playable playable = (shownotesProvider instanceof Playable) ? (Playable) shownotesProvider : null;
// load shownotes
String shownotes;
try {
shownotes = shownotesProvider.loadShownotes().call();
} catch (Exception e) {
e.printStackTrace();
return null;
}
if(TextUtils.isEmpty(shownotes)) {
Log.d(TAG, "shownotesProvider contained no shownotes. Returning 'no shownotes' message");
shownotes ="<html>" +
"<head>" +
"<style type='text/css'>" +
"html, body { margin: 0; padding: 0; width: 100%; height: 100%; } " +
"html { display: table; }" +
"body { display: table-cell; vertical-align: middle; text-align:center;" +
"-webkit-text-size-adjust: none; font-size: 87%; color: " + colorSecondaryString + ";} " +
"</style>" +
"</head>" +
"<body>" +
"<p>" + noShownotesLabel + "</p>" +
"</body>" +
"</html>";
Log.d(TAG, "shownotes: " + shownotes);
return shownotes;
}
// replace ASCII line breaks with HTML ones if shownotes don't contain HTML line breaks already
if(!LINE_BREAK_REGEX.matcher(shownotes).find() && !shownotes.contains("<p>")) {
shownotes = shownotes.replace("\n", "<br />");
}
Document document = Jsoup.parse(shownotes);
// apply style
String styleStr = String.format(WEBVIEW_STYLE, colorPrimaryString, "100%", pageMargin,
pageMargin, pageMargin, pageMargin);
document.head().appendElement("style").attr("type", "text/css").text(styleStr);
// apply timecode links
if (addTimecodes) {
Elements elementsWithTimeCodes = document.body().getElementsMatchingOwnText(TIMECODE_REGEX);
Log.d(TAG, "Recognized " + elementsWithTimeCodes.size() + " timecodes");
for (Element element : elementsWithTimeCodes) {
Matcher matcherLong = TIMECODE_REGEX.matcher(element.html());
StringBuffer buffer = new StringBuffer();
while (matcherLong.find()) {
String h = matcherLong.group(1);
String group = matcherLong.group(0);
int time = (h != null) ? Converter.durationStringLongToMs(group) :
Converter.durationStringShortToMs(group);
String rep;
if (playable == null || playable.getDuration() > time) {
rep = String.format(TIMECODE_LINK, time, group);
} else {
rep = group;
}
matcherLong.appendReplacement(buffer, rep);
}
matcherLong.appendTail(buffer);
element.html(buffer.toString());
}
}
return document.toString();
}
/**
* Returns true if the given link is a timecode link.
*/
public static boolean isTimecodeLink(String link) {
return link != null && link.matches(TIMECODE_LINK_REGEX.pattern());
}
/**
* Returns the time in milliseconds that is attached to this link or -1
* if the link is no valid timecode link.
*/
public static int getTimecodeLinkTime(String link) {
if (isTimecodeLink(link)) {
Matcher m = TIMECODE_LINK_REGEX.matcher(link);
try {
if (m.find()) {
return Integer.parseInt(m.group(1));
}
} catch (NumberFormatException e) {
e.printStackTrace();
}
}
return -1;
}
public void setShownotesProvider(@NonNull ShownotesProvider shownotesProvider) {
this.shownotesProvider = shownotesProvider;
}
}