Integrated timecode highlighting into Audioplayer

This commit is contained in:
daniel oeh 2014-06-29 03:33:22 +02:00
parent c9c69aa7c7
commit 6b5d269185
6 changed files with 94 additions and 63 deletions

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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();

View File

@ -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;
}