diff options
author | daniel oeh <daniel.oeh@gmail.com> | 2014-06-29 02:38:25 +0200 |
---|---|---|
committer | daniel oeh <daniel.oeh@gmail.com> | 2014-06-29 02:50:09 +0200 |
commit | c9c69aa7c796da59c29fad2c4d4c9464d353416b (patch) | |
tree | 57d0d94d11a46fda5ea38b85d35573393f89ed35 /src | |
parent | 2633d17046ab2373d0f1a8976a16a3e0ee15a5cf (diff) | |
download | AntennaPod-c9c69aa7c796da59c29fad2c4d4c9464d353416b.zip |
Added first implementation of the Timeline class
Diffstat (limited to 'src')
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")); + + } +} |