Added first implementation of the Timeline class
This commit is contained in:
parent
2633d17046
commit
c9c69aa7c7
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
152
src/de/danoeh/antennapod/util/playback/Timeline.java
Normal file
152
src/de/danoeh/antennapod/util/playback/Timeline.java
Normal file
@ -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;
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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"));
|
||||
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user