summaryrefslogtreecommitdiff
path: root/src/de/danoeh/antennapod/util/playback
diff options
context:
space:
mode:
Diffstat (limited to 'src/de/danoeh/antennapod/util/playback')
-rw-r--r--src/de/danoeh/antennapod/util/playback/Playable.java6
-rw-r--r--src/de/danoeh/antennapod/util/playback/PlaybackController.java22
-rw-r--r--src/de/danoeh/antennapod/util/playback/Timeline.java161
3 files changed, 180 insertions, 9 deletions
diff --git a/src/de/danoeh/antennapod/util/playback/Playable.java b/src/de/danoeh/antennapod/util/playback/Playable.java
index 8eefb0be5..9ed45abfc 100644
--- a/src/de/danoeh/antennapod/util/playback/Playable.java
+++ b/src/de/danoeh/antennapod/util/playback/Playable.java
@@ -12,6 +12,7 @@ import de.danoeh.antennapod.feed.MediaType;
import de.danoeh.antennapod.storage.DBReader;
import de.danoeh.antennapod.util.ShownotesProvider;
import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.Validate;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
@@ -216,9 +217,8 @@ public interface Playable extends Parcelable,
private Playable playable;
public DefaultPlayableImageLoader(Playable playable) {
- if (playable == null) {
- throw new IllegalArgumentException("Playable must not be null");
- }
+ Validate.notNull(playable);
+
this.playable = playable;
}
diff --git a/src/de/danoeh/antennapod/util/playback/PlaybackController.java b/src/de/danoeh/antennapod/util/playback/PlaybackController.java
index 1992fce2c..64dbf4868 100644
--- a/src/de/danoeh/antennapod/util/playback/PlaybackController.java
+++ b/src/de/danoeh/antennapod/util/playback/PlaybackController.java
@@ -15,12 +15,17 @@ import android.view.View.OnClickListener;
import android.widget.ImageButton;
import android.widget.SeekBar;
import android.widget.TextView;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.Validate;
+
import de.danoeh.antennapod.BuildConfig;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.feed.Chapter;
import de.danoeh.antennapod.feed.FeedMedia;
import de.danoeh.antennapod.feed.MediaType;
import de.danoeh.antennapod.preferences.PlaybackPreferences;
+import de.danoeh.antennapod.preferences.UserPreferences;
import de.danoeh.antennapod.service.playback.PlaybackService;
import de.danoeh.antennapod.service.playback.PlaybackServiceMediaPlayer;
import de.danoeh.antennapod.service.playback.PlayerStatus;
@@ -37,7 +42,6 @@ import java.util.concurrent.*;
public abstract class PlaybackController {
private static final String TAG = "PlaybackController";
- public static final int DEFAULT_SEEK_DELTA = 30000;
public static final int INVALID_TIME = -1;
private final Activity activity;
@@ -62,8 +66,8 @@ public abstract class PlaybackController {
private boolean reinitOnPause;
public PlaybackController(Activity activity, boolean reinitOnPause) {
- if (activity == null)
- throw new IllegalArgumentException("activity = null");
+ Validate.notNull(activity);
+
this.activity = activity;
this.reinitOnPause = reinitOnPause;
schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOLSIZE,
@@ -360,7 +364,7 @@ public abstract class PlaybackController {
@Override
public void onReceive(Context context, Intent intent) {
if (isConnectedToPlaybackService()) {
- if (intent.getAction().equals(
+ if (StringUtils.equals(intent.getAction(),
PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)) {
release();
onShutdownNotification();
@@ -605,7 +609,7 @@ public abstract class PlaybackController {
@Override
public void onClick(View v) {
if (status == PlayerStatus.PLAYING) {
- playbackService.seekDelta(-DEFAULT_SEEK_DELTA);
+ playbackService.seekDelta(-UserPreferences.getSeekDeltaMs());
}
}
};
@@ -616,7 +620,7 @@ public abstract class PlaybackController {
@Override
public void onClick(View v) {
if (status == PlayerStatus.PLAYING) {
- playbackService.seekDelta(DEFAULT_SEEK_DELTA);
+ playbackService.seekDelta(UserPreferences.getSeekDeltaMs());
}
}
};
@@ -680,6 +684,12 @@ public abstract class PlaybackController {
}
}
+ public void seekTo(int time) {
+ if (playbackService != null) {
+ playbackService.seekTo(time);
+ }
+ }
+
public void setVideoSurface(SurfaceHolder holder) {
if (playbackService != null) {
playbackService.setVideoSurface(holder);
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..ceed68183
--- /dev/null
+++ b/src/de/danoeh/antennapod/util/playback/Timeline.java
@@ -0,0 +1,161 @@
+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.apache.commons.lang3.Validate;
+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 = "@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; } 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 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 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");
+
+ /**
+ * 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 (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);
+ 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());
+ }
+ }
+
+ 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 int getTimecodeLinkTime(String link) {
+ if (isTimecodeLink(link)) {
+ Matcher m = TIMECODE_LINK_REGEX.matcher(link);
+
+ try {
+ if (m.find()) {
+ return Integer.valueOf(m.group(1));
+ }
+ } catch (NumberFormatException e) {
+ e.printStackTrace();
+ }
+ }
+ return -1;
+ }
+
+
+ public void setShownotesProvider(ShownotesProvider shownotesProvider) {
+ Validate.notNull(shownotesProvider);
+ this.shownotesProvider = shownotesProvider;
+ }
+}