Integrated timecode highlighting into Audioplayer
This commit is contained in:
parent
c9c69aa7c7
commit
6b5d269185
|
@ -33,11 +33,12 @@ import de.danoeh.antennapod.service.playback.PlaybackService;
|
|||
import de.danoeh.antennapod.storage.DBReader;
|
||||
import de.danoeh.antennapod.util.playback.ExternalMedia;
|
||||
import de.danoeh.antennapod.util.playback.Playable;
|
||||
import de.danoeh.antennapod.util.playback.PlaybackController;
|
||||
|
||||
/**
|
||||
* Activity for playing audio files.
|
||||
*/
|
||||
public class AudioplayerActivity extends MediaplayerActivity {
|
||||
public class AudioplayerActivity extends MediaplayerActivity implements ItemDescriptionFragment.ItemDescriptionFragmentCallback {
|
||||
private static final int POS_COVER = 0;
|
||||
private static final int POS_DESCR = 1;
|
||||
private static final int POS_CHAPTERS = 2;
|
||||
|
@ -293,7 +294,7 @@ public class AudioplayerActivity extends MediaplayerActivity {
|
|||
case POS_DESCR:
|
||||
if (descriptionFragment == null) {
|
||||
descriptionFragment = ItemDescriptionFragment
|
||||
.newInstance(media, true);
|
||||
.newInstance(media, true, true);
|
||||
}
|
||||
currentlyShownFragment = descriptionFragment;
|
||||
break;
|
||||
|
@ -603,6 +604,11 @@ public class AudioplayerActivity extends MediaplayerActivity {
|
|||
clearStatusMsg();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlaybackController getPlaybackController() {
|
||||
return controller;
|
||||
}
|
||||
|
||||
public interface AudioplayerContentFragment {
|
||||
public void onDataSetChanged(Playable media);
|
||||
}
|
||||
|
|
|
@ -2,8 +2,11 @@ package de.danoeh.antennapod.fragment;
|
|||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.*;
|
||||
import android.content.res.TypedArray;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.ClipData;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
|
@ -12,12 +15,18 @@ import android.support.v4.app.Fragment;
|
|||
import android.support.v7.app.ActionBarActivity;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.*;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.ContextMenu.ContextMenuInfo;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.webkit.WebSettings.LayoutAlgorithm;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.widget.Toast;
|
||||
|
||||
import de.danoeh.antennapod.BuildConfig;
|
||||
import de.danoeh.antennapod.R;
|
||||
import de.danoeh.antennapod.feed.FeedItem;
|
||||
|
@ -26,11 +35,12 @@ import de.danoeh.antennapod.storage.DBReader;
|
|||
import de.danoeh.antennapod.util.ShareUtils;
|
||||
import de.danoeh.antennapod.util.ShownotesProvider;
|
||||
import de.danoeh.antennapod.util.playback.Playable;
|
||||
import org.apache.commons.lang3.StringEscapeUtils;
|
||||
import de.danoeh.antennapod.util.playback.PlaybackController;
|
||||
import de.danoeh.antennapod.util.playback.Timeline;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
/** Displays the description of a Playable object in a Webview. */
|
||||
/**
|
||||
* Displays the description of a Playable object in a Webview.
|
||||
*/
|
||||
public class ItemDescriptionFragment extends Fragment {
|
||||
|
||||
private static final String TAG = "ItemDescriptionFragment";
|
||||
|
@ -43,6 +53,7 @@ public class ItemDescriptionFragment extends Fragment {
|
|||
private static final String ARG_FEEDITEM_ID = "arg.feeditem";
|
||||
|
||||
private static final String ARG_SAVE_STATE = "arg.saveState";
|
||||
private static final String ARG_HIGHLIGHT_TIMECODES = "arg.highlightTimecodes";
|
||||
|
||||
private WebView webvDescription;
|
||||
|
||||
|
@ -63,21 +74,29 @@ public class ItemDescriptionFragment extends Fragment {
|
|||
*/
|
||||
private boolean saveState;
|
||||
|
||||
/**
|
||||
* True if Fragment should highlight timecodes (e.g. time codes in the HH:MM:SS format).
|
||||
*/
|
||||
private boolean highlightTimecodes;
|
||||
|
||||
public static ItemDescriptionFragment newInstance(Playable media,
|
||||
boolean saveState) {
|
||||
boolean saveState,
|
||||
boolean highlightTimecodes) {
|
||||
ItemDescriptionFragment f = new ItemDescriptionFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putParcelable(ARG_PLAYABLE, media);
|
||||
args.putBoolean(ARG_SAVE_STATE, saveState);
|
||||
args.putBoolean(ARG_HIGHLIGHT_TIMECODES, highlightTimecodes);
|
||||
f.setArguments(args);
|
||||
return f;
|
||||
}
|
||||
|
||||
public static ItemDescriptionFragment newInstance(FeedItem item, boolean saveState) {
|
||||
public static ItemDescriptionFragment newInstance(FeedItem item, boolean saveState, boolean highlightTimecodes) {
|
||||
ItemDescriptionFragment f = new ItemDescriptionFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putLong(ARG_FEEDITEM_ID, item.getId());
|
||||
args.putBoolean(ARG_SAVE_STATE, saveState);
|
||||
args.putBoolean(ARG_HIGHLIGHT_TIMECODES, highlightTimecodes);
|
||||
f.setArguments(args);
|
||||
return f;
|
||||
}
|
||||
|
@ -106,12 +125,22 @@ public class ItemDescriptionFragment extends Fragment {
|
|||
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||
if (Timeline.isTimecodeLink(url)) {
|
||||
int time = Timeline.getTimecodeLinkTime(url);
|
||||
if (getActivity() != null && getActivity() instanceof ItemDescriptionFragmentCallback) {
|
||||
PlaybackController pc = ((ItemDescriptionFragmentCallback) getActivity()).getPlaybackController();
|
||||
if (pc != null) {
|
||||
pc.seekTo(time);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||
try {
|
||||
startActivity(intent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
@ -178,6 +207,7 @@ public class ItemDescriptionFragment extends Fragment {
|
|||
Log.d(TAG, "Creating fragment");
|
||||
Bundle args = getArguments();
|
||||
saveState = args.getBoolean(ARG_SAVE_STATE, false);
|
||||
highlightTimecodes = args.getBoolean(ARG_HIGHLIGHT_TIMECODES, false);
|
||||
|
||||
}
|
||||
|
||||
|
@ -229,21 +259,6 @@ public class ItemDescriptionFragment extends Fragment {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the CSS style of the Webview.
|
||||
*
|
||||
* @param textColor the default color to use for the text in the webview. This
|
||||
* value is inserted directly into the CSS String.
|
||||
*/
|
||||
private String applyWebviewStyle(String textColor, String data) {
|
||||
final String WEBVIEW_STYLE = "<html><head><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; }</style></head><body>%s</body></html>";
|
||||
final int pageMargin = (int) TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP, 8, getResources()
|
||||
.getDisplayMetrics());
|
||||
return String.format(WEBVIEW_STYLE, textColor, "100%", pageMargin,
|
||||
pageMargin, pageMargin, pageMargin, data);
|
||||
}
|
||||
|
||||
private View.OnLongClickListener webViewLongClickListener = new View.OnLongClickListener() {
|
||||
|
||||
@Override
|
||||
|
@ -254,11 +269,14 @@ public class ItemDescriptionFragment extends Fragment {
|
|||
if (BuildConfig.DEBUG)
|
||||
Log.d(TAG,
|
||||
"Link of webview was long-pressed. Extra: "
|
||||
+ r.getExtra());
|
||||
+ r.getExtra()
|
||||
);
|
||||
selectedURL = r.getExtra();
|
||||
if (!Timeline.isTimecodeLink(selectedURL)) {
|
||||
webvDescription.showContextMenu();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
selectedURL = null;
|
||||
return false;
|
||||
}
|
||||
|
@ -364,22 +382,10 @@ public class ItemDescriptionFragment extends Fragment {
|
|||
if (BuildConfig.DEBUG)
|
||||
Log.d(TAG, "Loading Webview");
|
||||
try {
|
||||
Callable<String> shownotesLoadTask = shownotesProvider.loadShownotes();
|
||||
final String shownotes = shownotesLoadTask.call();
|
||||
|
||||
data = StringEscapeUtils.unescapeHtml4(shownotes);
|
||||
Activity activity = getActivity();
|
||||
if (activity != null) {
|
||||
TypedArray res = activity
|
||||
.getTheme()
|
||||
.obtainStyledAttributes(
|
||||
new int[]{android.R.attr.textColorPrimary});
|
||||
int colorResource = res.getColor(0, 0);
|
||||
String colorString = String.format("#%06X",
|
||||
0xFFFFFF & colorResource);
|
||||
Log.i(TAG, "text color: " + colorString);
|
||||
res.recycle();
|
||||
data = applyWebviewStyle(colorString, data);
|
||||
Timeline timeline = new Timeline(activity, shownotesProvider);
|
||||
data = timeline.processShownotes(highlightTimecodes);
|
||||
} else {
|
||||
cancel(true);
|
||||
}
|
||||
|
@ -409,7 +415,8 @@ public class ItemDescriptionFragment extends Fragment {
|
|||
if (BuildConfig.DEBUG)
|
||||
Log.d(TAG,
|
||||
"Saving scroll position: "
|
||||
+ webvDescription.getScrollY());
|
||||
+ webvDescription.getScrollY()
|
||||
);
|
||||
editor.putInt(PREF_SCROLL_Y, webvDescription.getScrollY());
|
||||
editor.putString(PREF_PLAYABLE_ID, media.getIdentifier()
|
||||
.toString());
|
||||
|
@ -447,4 +454,8 @@ public class ItemDescriptionFragment extends Fragment {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public interface ItemDescriptionFragmentCallback {
|
||||
public PlaybackController getPlaybackController();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,24 +80,24 @@ public final class Converter {
|
|||
}
|
||||
|
||||
/** Converts long duration string (HH:MM:SS) to milliseconds. */
|
||||
public static long durationStringLongToMs(String input) {
|
||||
public static int 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;
|
||||
return Integer.valueOf(parts[0]) * 3600 * 1000 +
|
||||
Integer.valueOf(parts[1]) * 60 * 1000 +
|
||||
Integer.valueOf(parts[2]) * 1000;
|
||||
}
|
||||
|
||||
/** Converts short duration string (HH:MM) to milliseconds. */
|
||||
public static long durationStringShortToMs(String input) {
|
||||
public static int 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;
|
||||
return Integer.valueOf(parts[0]) * 3600 * 1000 +
|
||||
Integer.valueOf(parts[1]) * 1000 * 60;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -680,6 +680,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);
|
||||
|
|
|
@ -27,7 +27,7 @@ import de.danoeh.antennapod.util.ShownotesProvider;
|
|||
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 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; } a.timecode { color: #669900; } img { display: block; margin: 10 auto; max-width: %s; height: auto; } body { margin: %dpx %dpx %dpx %dpx; }";
|
||||
|
||||
|
||||
private ShownotesProvider shownotesProvider;
|
||||
|
@ -56,7 +56,7 @@ public class Timeline {
|
|||
}
|
||||
|
||||
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 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");
|
||||
|
||||
/**
|
||||
|
@ -69,6 +69,7 @@ public class Timeline {
|
|||
* @return The processed HTML string.
|
||||
*/
|
||||
public String processShownotes(final boolean addTimecodes) {
|
||||
final Playable playable = (shownotesProvider instanceof Playable) ? (Playable) shownotesProvider : null;
|
||||
|
||||
// load shownotes
|
||||
|
||||
|
@ -103,9 +104,15 @@ public class Timeline {
|
|||
while (matcherLong.find()) {
|
||||
String h = matcherLong.group(1);
|
||||
String group = matcherLong.group(0);
|
||||
long time = (h != null) ? Converter.durationStringLongToMs(group) :
|
||||
int time = (h != null) ? Converter.durationStringLongToMs(group) :
|
||||
Converter.durationStringShortToMs(group);
|
||||
String rep = String.format(TIMECODE_LINK, time, group);
|
||||
|
||||
String rep;
|
||||
if (playable == null || playable.getDuration() > time) {
|
||||
rep = String.format(TIMECODE_LINK, time, group);
|
||||
} else {
|
||||
rep = group;
|
||||
}
|
||||
matcherLong.appendReplacement(buffer, rep);
|
||||
}
|
||||
|
||||
|
@ -129,13 +136,13 @@ public class Timeline {
|
|||
* 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) {
|
||||
public static int getTimecodeLinkTime(String link) {
|
||||
if (isTimecodeLink(link)) {
|
||||
Matcher m = TIMECODE_LINK_REGEX.matcher(link);
|
||||
|
||||
try {
|
||||
if (m.find()) {
|
||||
return Long.valueOf(m.group(1));
|
||||
return Integer.valueOf(m.group(1));
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
e.printStackTrace();
|
||||
|
|
|
@ -35,6 +35,7 @@ public class TimelineTest extends InstrumentationTestCase {
|
|||
item.setChapters(chapters);
|
||||
item.setContentEncoded(shownotes);
|
||||
FeedMedia media = new FeedMedia(item, "http://example.com/episode", 100, "audio/mp3");
|
||||
media.setDuration(Integer.MAX_VALUE);
|
||||
item.setMedia(media);
|
||||
return media;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue