summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/de/danoeh/antennapod/util/Converter.java21
-rw-r--r--src/de/danoeh/antennapod/util/playback/Timeline.java152
-rw-r--r--src/instrumentationTest/de/test/antennapod/util/ConverterTest.java35
-rw-r--r--src/instrumentationTest/de/test/antennapod/util/playback/TimelineTest.java96
4 files changed, 304 insertions, 0 deletions
diff --git a/src/de/danoeh/antennapod/util/Converter.java b/src/de/danoeh/antennapod/util/Converter.java
index 46a0d30b4..bc3d9edd3 100644
--- a/src/de/danoeh/antennapod/util/Converter.java
+++ b/src/de/danoeh/antennapod/util/Converter.java
@@ -78,5 +78,26 @@ public final class Converter {
return String.format("%02d:%02d", h, m);
}
+
+ /** Converts long duration string (HH:MM:SS) to milliseconds. */
+ public static long durationStringLongToMs(String input) {
+ String[] parts = input.split(":");
+ if (parts.length != 3) {
+ return 0;
+ }
+ return Long.valueOf(parts[0]) * 3600 * 1000 +
+ Long.valueOf(parts[1]) * 60 * 1000 +
+ Long.valueOf(parts[2]) * 1000;
+ }
+
+ /** Converts short duration string (HH:MM) to milliseconds. */
+ public static long durationStringShortToMs(String input) {
+ String[] parts = input.split(":");
+ if (parts.length != 2) {
+ return 0;
+ }
+ return Long.valueOf(parts[0]) * 3600 * 1000 +
+ Long.valueOf(parts[1]) * 1000 * 60;
+ }
}
diff --git a/src/de/danoeh/antennapod/util/playback/Timeline.java b/src/de/danoeh/antennapod/util/playback/Timeline.java
new file mode 100644
index 000000000..6e00f725c
--- /dev/null
+++ b/src/de/danoeh/antennapod/util/playback/Timeline.java
@@ -0,0 +1,152 @@
+package de.danoeh.antennapod.util.playback;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+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.BuildConfig;
+import de.danoeh.antennapod.util.Converter;
+import de.danoeh.antennapod.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 = "<style type=\"text/css\"> @font-face { font-family: 'Roboto-Light'; src: url('file:///android_asset/Roboto-Light.ttf'); } * { color: %s; font-family: roboto-Light; font-size: 11pt; } a { font-style: normal; text-decoration: none; font-weight: normal; color: #00A8DF; } img { display: block; margin: 10 auto; max-width: %s; height: auto; } body { margin: %dpx %dpx %dpx %dpx; }";
+
+
+ private ShownotesProvider shownotesProvider;
+
+
+ private final String colorString;
+ private final int pageMargin;
+
+ public Timeline(Context context, ShownotesProvider shownotesProvider) {
+ if (shownotesProvider == null) throw new IllegalArgumentException("shownotesProvider = null");
+ this.shownotesProvider = shownotesProvider;
+
+ TypedArray res = context
+ .getTheme()
+ .obtainStyledAttributes(
+ new int[]{android.R.attr.textColorPrimary});
+ int colorResource = res.getColor(0, 0);
+ colorString = String.format("#%06X",
+ 0xFFFFFF & colorResource);
+ 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 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");
+
+ /**
+ * 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) {
+
+ // load shownotes
+
+ String shownotes;
+ try {
+ shownotes = shownotesProvider.loadShownotes().call();
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ if (shownotes == null) {
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "shownotesProvider contained no shownotes. Returning empty string");
+ return "";
+ }
+
+ Document document = Jsoup.parse(shownotes);
+
+ // apply style
+ String styleStr = String.format(WEBVIEW_STYLE, colorString, "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);
+ if (BuildConfig.DEBUG)
+ Log.d(TAG, "Recognized " + elementsWithTimeCodes.size() + " timecodes");
+ for (Element element : elementsWithTimeCodes) {
+ Matcher matcherLong = TIMECODE_REGEX.matcher(element.text());
+ StringBuffer buffer = new StringBuffer();
+ while (matcherLong.find()) {
+ String h = matcherLong.group(1);
+ String group = matcherLong.group(0);
+ long time = (h != null) ? Converter.durationStringLongToMs(group) :
+ Converter.durationStringShortToMs(group);
+ String rep = String.format(TIMECODE_LINK, time, group);
+ matcherLong.appendReplacement(buffer, rep);
+ }
+
+ element.html(buffer.toString());
+ }
+ }
+
+ Log.i(TAG, "Out: " + document.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 long getTimecodeLinkTime(String link) {
+ if (isTimecodeLink(link)) {
+ Matcher m = TIMECODE_LINK_REGEX.matcher(link);
+
+ try {
+ if (m.find()) {
+ return Long.valueOf(m.group(1));
+ }
+ } catch (NumberFormatException e) {
+ e.printStackTrace();
+ }
+ }
+ return -1;
+ }
+
+
+ public void setShownotesProvider(ShownotesProvider shownotesProvider) {
+ if (shownotesProvider == null) throw new IllegalArgumentException("shownotesProvider = null");
+ this.shownotesProvider = shownotesProvider;
+ }
+}
diff --git a/src/instrumentationTest/de/test/antennapod/util/ConverterTest.java b/src/instrumentationTest/de/test/antennapod/util/ConverterTest.java
new file mode 100644
index 000000000..8e5674b06
--- /dev/null
+++ b/src/instrumentationTest/de/test/antennapod/util/ConverterTest.java
@@ -0,0 +1,35 @@
+package instrumentationTest.de.test.antennapod.util;
+
+import android.test.AndroidTestCase;
+
+import de.danoeh.antennapod.util.Converter;
+
+/**
+ * Test class for converter
+ */
+public class ConverterTest extends AndroidTestCase {
+
+ public void testGetDurationStringLong() throws Exception {
+ String expected = "13:05:10";
+ int input = 47110000;
+ assertEquals(expected, Converter.getDurationStringLong(input));
+ }
+
+ public void testGetDurationStringShort() throws Exception {
+ String expected = "13:05";
+ int input = 47110000;
+ assertEquals(expected, Converter.getDurationStringShort(input));
+ }
+
+ public void testDurationStringLongToMs() throws Exception {
+ String input = "01:20:30";
+ long expected = 4830000;
+ assertEquals(expected, Converter.durationStringLongToMs(input));
+ }
+
+ public void testDurationStringShortToMs() throws Exception {
+ String input = "8:30";
+ long expected = 30600000;
+ assertEquals(expected, Converter.durationStringShortToMs(input));
+ }
+}
diff --git a/src/instrumentationTest/de/test/antennapod/util/playback/TimelineTest.java b/src/instrumentationTest/de/test/antennapod/util/playback/TimelineTest.java
new file mode 100644
index 000000000..a89be210e
--- /dev/null
+++ b/src/instrumentationTest/de/test/antennapod/util/playback/TimelineTest.java
@@ -0,0 +1,96 @@
+package instrumentationTest.de.test.antennapod.util.playback;
+
+import android.content.Context;
+import android.test.InstrumentationTestCase;
+
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+
+import java.util.Date;
+import java.util.List;
+
+import de.danoeh.antennapod.feed.Chapter;
+import de.danoeh.antennapod.feed.FeedItem;
+import de.danoeh.antennapod.feed.FeedMedia;
+import de.danoeh.antennapod.util.playback.Playable;
+import de.danoeh.antennapod.util.playback.Timeline;
+
+/**
+ * Test class for timeline
+ */
+public class TimelineTest extends InstrumentationTestCase {
+
+ private Context context;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ context = getInstrumentation().getTargetContext();
+ }
+
+ private Playable newTestPlayable(List<Chapter> chapters, String shownotes) {
+ FeedItem item = new FeedItem(0, "Item", "item-id", "http://example.com/item", new Date(), true, null);
+ item.setChapters(chapters);
+ item.setContentEncoded(shownotes);
+ FeedMedia media = new FeedMedia(item, "http://example.com/episode", 100, "audio/mp3");
+ item.setMedia(media);
+ return media;
+ }
+
+ public void testProcessShownotesAddTimecodeHHMMSSNoChapters() throws Exception {
+ final String timeStr = "10:11:12";
+ final long time = 3600 * 1000 * 10 + 60 * 1000 * 11 + 12 * 1000;
+
+ Playable p = newTestPlayable(null, "<p> Some test text with a timecode " + timeStr + " here.</p>");
+ Timeline t = new Timeline(context, p);
+ String res = t.processShownotes(true);
+ checkLinkCorrect(res, new long[]{time}, new String[]{timeStr});
+ }
+
+ public void testProcessShownotesAddTimecodeHHMMNoChapters() throws Exception {
+ final String timeStr = "10:11";
+ final long time = 3600 * 1000 * 10 + 60 * 1000 * 11;
+
+ Playable p = newTestPlayable(null, "<p> Some test text with a timecode " + timeStr + " here.</p>");
+ Timeline t = new Timeline(context, p);
+ String res = t.processShownotes(true);
+ checkLinkCorrect(res, new long[]{time}, new String[]{timeStr});
+ }
+
+ private void checkLinkCorrect(String res, long[] timecodes, String[] timecodeStr) {
+ assertNotNull(res);
+ Document d = Jsoup.parse(res);
+ Elements links = d.body().getElementsByTag("a");
+ int countedLinks = 0;
+ for (Element link : links) {
+ String href = link.attributes().get("href");
+ String text = link.text();
+ if (href.startsWith("antennapod://")) {
+ assertTrue(href.endsWith(String.valueOf(timecodes[countedLinks])));
+ assertEquals(timecodeStr[countedLinks], text);
+ countedLinks++;
+ assertTrue("Contains too many links: " + countedLinks + " > " + timecodes.length, countedLinks <= timecodes.length);
+ }
+ }
+ assertEquals(timecodes.length, countedLinks);
+ }
+
+ public void testIsTimecodeLink() throws Exception {
+ assertFalse(Timeline.isTimecodeLink(null));
+ assertFalse(Timeline.isTimecodeLink("http://antennapod/timecode/123123"));
+ assertFalse(Timeline.isTimecodeLink("antennapod://timecode/"));
+ assertFalse(Timeline.isTimecodeLink("antennapod://123123"));
+ assertFalse(Timeline.isTimecodeLink("antennapod://timecode/123123a"));
+ assertTrue(Timeline.isTimecodeLink("antennapod://timecode/123"));
+ assertTrue(Timeline.isTimecodeLink("antennapod://timecode/1"));
+ }
+
+ public void testGetTimecodeLinkTime() throws Exception {
+ assertEquals(-1, Timeline.getTimecodeLinkTime(null));
+ assertEquals(-1, Timeline.getTimecodeLinkTime("http://timecode/123"));
+ assertEquals(123, Timeline.getTimecodeLinkTime("antennapod://timecode/123"));
+
+ }
+}