Allow to stream episodes from unsubscribed podcast

This commit is contained in:
ByteHamster 2020-02-22 01:09:59 +01:00
parent 03d1f41e9b
commit f7411d2d98
9 changed files with 183 additions and 99 deletions

View File

@ -1,23 +1,29 @@
package de.danoeh.antennapod.adapter; package de.danoeh.antennapod.adapter;
import android.content.Context; import android.content.Context;
import android.os.Build;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.TextView; import android.widget.TextView;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.util.NetworkUtils;
import de.danoeh.antennapod.core.util.playback.RemoteMedia;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.util.DateUtils;
import de.danoeh.antennapod.core.util.playback.Playable;
import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter;
import de.danoeh.antennapod.dialog.StreamingConfirmationDialog;
import java.util.List; import java.util.List;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.util.DateUtils;
/** /**
* List adapter for showing a list of FeedItems with their title and description. * List adapter for showing a list of FeedItems with their title and description.
*/ */
public class FeedItemlistDescriptionAdapter extends ArrayAdapter<FeedItem> { public class FeedItemlistDescriptionAdapter extends ArrayAdapter<FeedItem> {
private static final int MAX_LINES_COLLAPSED = 3;
public FeedItemlistDescriptionAdapter(Context context, int resource, List<FeedItem> objects) { public FeedItemlistDescriptionAdapter(Context context, int resource, List<FeedItem> objects) {
super(context, resource, objects); super(context, resource, objects);
@ -32,12 +38,12 @@ public class FeedItemlistDescriptionAdapter extends ArrayAdapter<FeedItem> {
// Inflate layout // Inflate layout
if (convertView == null) { if (convertView == null) {
holder = new Holder(); holder = new Holder();
LayoutInflater inflater = (LayoutInflater) getContext() LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = inflater.inflate(R.layout.itemdescription_listitem, parent, false); convertView = inflater.inflate(R.layout.itemdescription_listitem, parent, false);
holder.title = convertView.findViewById(R.id.txtvTitle); holder.title = convertView.findViewById(R.id.txtvTitle);
holder.pubDate = convertView.findViewById(R.id.txtvPubDate); holder.pubDate = convertView.findViewById(R.id.txtvPubDate);
holder.description = convertView.findViewById(R.id.txtvDescription); holder.description = convertView.findViewById(R.id.txtvDescription);
holder.preview = convertView.findViewById(R.id.butPreview);
convertView.setTag(holder); convertView.setTag(holder);
} else { } else {
@ -52,18 +58,34 @@ public class FeedItemlistDescriptionAdapter extends ArrayAdapter<FeedItem> {
.replaceAll("\\s+", " ") .replaceAll("\\s+", " ")
.trim(); .trim();
holder.description.setText(description); holder.description.setText(description);
final int MAX_LINES_COLLAPSED = 3;
holder.description.setMaxLines(MAX_LINES_COLLAPSED); holder.description.setMaxLines(MAX_LINES_COLLAPSED);
holder.description.setOnClickListener(v -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
&& holder.description.getMaxLines() > MAX_LINES_COLLAPSED) {
holder.description.setMaxLines(MAX_LINES_COLLAPSED);
} else {
holder.description.setMaxLines(2000);
}
});
} }
holder.description.setTag(Boolean.FALSE); // not expanded
holder.preview.setVisibility(View.GONE);
holder.preview.setOnClickListener(v -> {
Playable playable = new RemoteMedia(item);
if (!NetworkUtils.isStreamingAllowed()) {
new StreamingConfirmationDialog(getContext(), playable).show();
return;
}
new PlaybackServiceStarter(getContext(), playable)
.shouldStream(true)
.startWhenPrepared(true)
.callEvenIfRunning(true)
.start();
getContext().startActivity(PlaybackService.getPlayerActivityIntent(getContext(), playable));
});
convertView.setOnClickListener(v -> {
if (holder.description.getTag() == Boolean.TRUE) {
holder.description.setMaxLines(MAX_LINES_COLLAPSED);
holder.preview.setVisibility(View.GONE);
holder.description.setTag(Boolean.FALSE);
} else {
holder.description.setMaxLines(2000);
holder.preview.setVisibility(View.VISIBLE);
holder.description.setTag(Boolean.TRUE);
}
});
return convertView; return convertView;
} }
@ -71,5 +93,6 @@ public class FeedItemlistDescriptionAdapter extends ArrayAdapter<FeedItem> {
TextView title; TextView title;
TextView pubDate; TextView pubDate;
TextView description; TextView description;
Button preview;
} }
} }

View File

@ -5,17 +5,17 @@ import android.view.View;
import android.widget.CheckBox; import android.widget.CheckBox;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import de.danoeh.antennapod.R; import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.util.playback.Playable;
import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter; import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter;
public class StreamingConfirmationDialog { public class StreamingConfirmationDialog {
private final Context context; private final Context context;
private final FeedMedia media; private final Playable playable;
public StreamingConfirmationDialog(Context context, FeedMedia media) { public StreamingConfirmationDialog(Context context, Playable playable) {
this.context = context; this.context = context;
this.media = media; this.playable = playable;
} }
public void show() { public void show() {
@ -30,7 +30,7 @@ public class StreamingConfirmationDialog {
if (checkDoNotShowAgain.isChecked()) { if (checkDoNotShowAgain.isChecked()) {
UserPreferences.setAllowMobileStreaming(true); UserPreferences.setAllowMobileStreaming(true);
} }
new PlaybackServiceStarter(context, media) new PlaybackServiceStarter(context, playable)
.callEvenIfRunning(true) .callEvenIfRunning(true)
.startWhenPrepared(true) .startWhenPrepared(true)
.shouldStream(true) .shouldStream(true)

View File

@ -56,4 +56,14 @@
tools:text="Feed item description" tools:text="Feed item description"
tools:background="@android:color/holo_green_dark" /> tools:background="@android:color/holo_green_dark" />
<Button
android:id="@+id/butPreview"
android:layout_below="@id/txtvDescription"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
style="?android:attr/buttonBarButtonStyle"
android:text="@string/preview_episode"/>
</RelativeLayout> </RelativeLayout>

View File

@ -1,35 +1,26 @@
package de.danoeh.antennapod.core.cast; package de.danoeh.antennapod.core.util.playback;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import androidx.annotation.Nullable;
import android.text.TextUtils; import android.text.TextUtils;
import androidx.annotation.Nullable;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaMetadata;
import com.google.android.gms.common.images.WebImage;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.concurrent.Callable;
import de.danoeh.antennapod.core.feed.Chapter; import de.danoeh.antennapod.core.feed.Chapter;
import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.MediaType; import de.danoeh.antennapod.core.feed.MediaType;
import de.danoeh.antennapod.core.util.ChapterUtils; import de.danoeh.antennapod.core.util.ChapterUtils;
import de.danoeh.antennapod.core.util.playback.Playable; import java.util.Date;
import java.util.List;
import java.util.concurrent.Callable;
import org.apache.commons.lang3.builder.HashCodeBuilder;
/** /**
* Playable implementation for media on a Cast Device for which a local version of * Playable implementation for media for which a local version of
* {@link de.danoeh.antennapod.core.feed.FeedMedia} hasn't been found. * {@link de.danoeh.antennapod.core.feed.FeedMedia} hasn't been found.
* Used for Casting and for previewing unsubscribed feeds.
*/ */
public class RemoteMedia implements Playable { public class RemoteMedia implements Playable {
public static final String TAG = "RemoteMedia"; public static final String TAG = "RemoteMedia";
@ -45,7 +36,7 @@ public class RemoteMedia implements Playable {
private String feedAuthor; private String feedAuthor;
private String imageUrl; private String imageUrl;
private String feedLink; private String feedLink;
private String mime_type; private String mimeType;
private Date pubDate; private Date pubDate;
private String notes; private String notes;
private List<Chapter> chapters; private List<Chapter> chapters;
@ -55,7 +46,7 @@ public class RemoteMedia implements Playable {
public RemoteMedia(String downloadUrl, String itemId, String feedUrl, String feedTitle, public RemoteMedia(String downloadUrl, String itemId, String feedUrl, String feedTitle,
String episodeTitle, String episodeLink, String feedAuthor, String episodeTitle, String episodeLink, String feedAuthor,
String imageUrl, String feedLink, String mime_type, Date pubDate) { String imageUrl, String feedLink, String mimeType, Date pubDate) {
this.downloadUrl = downloadUrl; this.downloadUrl = downloadUrl;
this.itemIdentifier = itemId; this.itemIdentifier = itemId;
this.feedUrl = feedUrl; this.feedUrl = feedUrl;
@ -65,63 +56,28 @@ public class RemoteMedia implements Playable {
this.feedAuthor = feedAuthor; this.feedAuthor = feedAuthor;
this.imageUrl = imageUrl; this.imageUrl = imageUrl;
this.feedLink = feedLink; this.feedLink = feedLink;
this.mime_type = mime_type; this.mimeType = mimeType;
this.pubDate = pubDate; this.pubDate = pubDate;
} }
public RemoteMedia(FeedItem item) {
this.downloadUrl = item.getMedia().getDownload_url();
this.itemIdentifier = item.getItemIdentifier();
this.feedUrl = item.getFeed().getDownload_url();
this.feedTitle = item.getFeed().getTitle();
this.episodeTitle = item.getTitle();
this.episodeLink = item.getLink();
this.feedAuthor = item.getFeed().getAuthor();
this.imageUrl = item.getImageUrl();
this.feedLink = item.getFeed().getLink();
this.mimeType = item.getMedia().getMime_type();
this.pubDate = item.getPubDate();
}
public void setNotes(String notes) { public void setNotes(String notes) {
this.notes = notes; this.notes = notes;
} }
public MediaInfo extractMediaInfo() {
MediaMetadata metadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_GENERIC);
metadata.putString(MediaMetadata.KEY_TITLE, episodeTitle);
metadata.putString(MediaMetadata.KEY_SUBTITLE, feedTitle);
if (!TextUtils.isEmpty(imageUrl)) {
metadata.addImage(new WebImage(Uri.parse(imageUrl)));
}
Calendar calendar = Calendar.getInstance();
calendar.setTime(pubDate);
metadata.putDate(MediaMetadata.KEY_RELEASE_DATE, calendar);
if (!TextUtils.isEmpty(feedAuthor)) {
metadata.putString(MediaMetadata.KEY_ARTIST, feedAuthor);
}
if (!TextUtils.isEmpty(feedUrl)) {
metadata.putString(CastUtils.KEY_FEED_URL, feedUrl);
}
if (!TextUtils.isEmpty(feedLink)) {
metadata.putString(CastUtils.KEY_FEED_WEBSITE, feedLink);
}
if (!TextUtils.isEmpty(itemIdentifier)) {
metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, itemIdentifier);
} else {
metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, downloadUrl);
}
if (!TextUtils.isEmpty(episodeLink)) {
metadata.putString(CastUtils.KEY_EPISODE_LINK, episodeLink);
}
String notes = this.notes;
if (notes != null) {
if (notes.length() > CastUtils.EPISODE_NOTES_MAX_LENGTH) {
notes = notes.substring(0, CastUtils.EPISODE_NOTES_MAX_LENGTH);
}
metadata.putString(CastUtils.KEY_EPISODE_NOTES, notes);
}
// Default id value
metadata.putInt(CastUtils.KEY_MEDIA_ID, 0);
metadata.putInt(CastUtils.KEY_FORMAT_VERSION, CastUtils.FORMAT_VERSION_VALUE);
MediaInfo.Builder builder = new MediaInfo.Builder(downloadUrl)
.setContentType(mime_type)
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
.setMetadata(metadata);
if (duration > 0) {
builder.setStreamDuration(duration);
}
return builder.build();
}
public String getEpisodeIdentifier() { public String getEpisodeIdentifier() {
return itemIdentifier; return itemIdentifier;
} }
@ -130,6 +86,38 @@ public class RemoteMedia implements Playable {
return feedUrl; return feedUrl;
} }
public String getDownloadUrl() {
return downloadUrl;
}
public String getEpisodeLink() {
return episodeLink;
}
public String getFeedAuthor() {
return feedAuthor;
}
public String getImageUrl() {
return imageUrl;
}
public String getFeedLink() {
return feedLink;
}
public String getMimeType() {
return mimeType;
}
public Date getPubDate() {
return pubDate;
}
public String getNotes() {
return notes;
}
@Override @Override
public void writeToPreferences(SharedPreferences.Editor prefEditor) { public void writeToPreferences(SharedPreferences.Editor prefEditor) {
//it seems pointless to do it, since the session should be kept by the remote device. //it seems pointless to do it, since the session should be kept by the remote device.
@ -196,7 +184,7 @@ public class RemoteMedia implements Playable {
@Override @Override
public MediaType getMediaType() { public MediaType getMediaType() {
return MediaType.fromMimeType(mime_type); return MediaType.fromMimeType(mimeType);
} }
@Override @Override
@ -293,7 +281,7 @@ public class RemoteMedia implements Playable {
dest.writeString(feedAuthor); dest.writeString(feedAuthor);
dest.writeString(imageUrl); dest.writeString(imageUrl);
dest.writeString(feedLink); dest.writeString(feedLink);
dest.writeString(mime_type); dest.writeString(mimeType);
dest.writeLong(pubDate.getTime()); dest.writeLong(pubDate.getTime());
dest.writeString(notes); dest.writeString(notes);
dest.writeInt(duration); dest.writeInt(duration);
@ -324,9 +312,9 @@ public class RemoteMedia implements Playable {
public boolean equals(Object other) { public boolean equals(Object other) {
if (other instanceof RemoteMedia) { if (other instanceof RemoteMedia) {
RemoteMedia rm = (RemoteMedia) other; RemoteMedia rm = (RemoteMedia) other;
return TextUtils.equals(downloadUrl, rm.downloadUrl) && return TextUtils.equals(downloadUrl, rm.downloadUrl)
TextUtils.equals(feedUrl, rm.feedUrl) && && TextUtils.equals(feedUrl, rm.feedUrl)
TextUtils.equals(itemIdentifier, rm.itemIdentifier); && TextUtils.equals(itemIdentifier, rm.itemIdentifier);
} }
if (other instanceof FeedMedia) { if (other instanceof FeedMedia) {
FeedMedia fm = (FeedMedia) other; FeedMedia fm = (FeedMedia) other;

View File

@ -657,6 +657,7 @@
<!-- Online feed view --> <!-- Online feed view -->
<string name="subscribe_label">Subscribe</string> <string name="subscribe_label">Subscribe</string>
<string name="subscribing_label">Subscribing&#8230;</string> <string name="subscribing_label">Subscribing&#8230;</string>
<string name="preview_episode">Preview</string>
<!-- Content descriptions for image buttons --> <!-- Content descriptions for image buttons -->
<string name="rewind_label">Rewind</string> <string name="rewind_label">Rewind</string>

View File

@ -9,6 +9,7 @@ import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaMetadata; import com.google.android.gms.cast.MediaMetadata;
import com.google.android.gms.common.images.WebImage; import com.google.android.gms.common.images.WebImage;
import de.danoeh.antennapod.core.util.playback.RemoteMedia;
import java.util.Calendar; import java.util.Calendar;
import java.util.List; import java.util.List;

View File

@ -0,0 +1,60 @@
package de.danoeh.antennapod.core.cast;
import android.net.Uri;
import android.text.TextUtils;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaMetadata;
import com.google.android.gms.common.images.WebImage;
import de.danoeh.antennapod.core.util.playback.RemoteMedia;
import java.util.Calendar;
public class MediaInfoCreator {
public static MediaInfo from(RemoteMedia media) {
MediaMetadata metadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_GENERIC);
metadata.putString(MediaMetadata.KEY_TITLE, media.getEpisodeTitle());
metadata.putString(MediaMetadata.KEY_SUBTITLE, media.getFeedTitle());
if (!TextUtils.isEmpty(media.getImageLocation())) {
metadata.addImage(new WebImage(Uri.parse(media.getImageLocation())));
}
Calendar calendar = Calendar.getInstance();
calendar.setTime(media.getPubDate());
metadata.putDate(MediaMetadata.KEY_RELEASE_DATE, calendar);
if (!TextUtils.isEmpty(media.getFeedAuthor())) {
metadata.putString(MediaMetadata.KEY_ARTIST, media.getFeedAuthor());
}
if (!TextUtils.isEmpty(media.getFeedUrl())) {
metadata.putString(CastUtils.KEY_FEED_URL, media.getFeedUrl());
}
if (!TextUtils.isEmpty(media.getFeedLink())) {
metadata.putString(CastUtils.KEY_FEED_WEBSITE, media.getFeedLink());
}
if (!TextUtils.isEmpty(media.getEpisodeIdentifier())) {
metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, media.getEpisodeIdentifier());
} else {
metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, media.getDownloadUrl());
}
if (!TextUtils.isEmpty(media.getEpisodeLink())) {
metadata.putString(CastUtils.KEY_EPISODE_LINK, media.getEpisodeLink());
}
String notes = media.getNotes();
if (notes != null) {
if (notes.length() > CastUtils.EPISODE_NOTES_MAX_LENGTH) {
notes = notes.substring(0, CastUtils.EPISODE_NOTES_MAX_LENGTH);
}
metadata.putString(CastUtils.KEY_EPISODE_NOTES, notes);
}
// Default id value
metadata.putInt(CastUtils.KEY_MEDIA_ID, 0);
metadata.putInt(CastUtils.KEY_FORMAT_VERSION, CastUtils.FORMAT_VERSION_VALUE);
MediaInfo.Builder builder = new MediaInfo.Builder(media.getDownloadUrl())
.setContentType(media.getMimeType())
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
.setMetadata(metadata);
if (media.getDuration() > 0) {
builder.setStreamDuration(media.getDuration());
}
return builder.build();
}
}

View File

@ -1,6 +1,6 @@
package de.danoeh.antennapod.core.feed; package de.danoeh.antennapod.core.feed;
import de.danoeh.antennapod.core.cast.RemoteMedia; import de.danoeh.antennapod.core.util.playback.RemoteMedia;
/** /**
* Implements methods for FeedMedia that are flavor dependent. * Implements methods for FeedMedia that are flavor dependent.

View File

@ -15,6 +15,7 @@ import com.google.android.libraries.cast.companionlibrary.cast.exceptions.CastEx
import com.google.android.libraries.cast.companionlibrary.cast.exceptions.NoConnectionException; import com.google.android.libraries.cast.companionlibrary.cast.exceptions.NoConnectionException;
import com.google.android.libraries.cast.companionlibrary.cast.exceptions.TransientNetworkDisconnectionException; import com.google.android.libraries.cast.companionlibrary.cast.exceptions.TransientNetworkDisconnectionException;
import de.danoeh.antennapod.core.cast.MediaInfoCreator;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.FutureTask; import java.util.concurrent.FutureTask;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@ -24,7 +25,7 @@ import de.danoeh.antennapod.core.cast.CastConsumer;
import de.danoeh.antennapod.core.cast.CastManager; import de.danoeh.antennapod.core.cast.CastManager;
import de.danoeh.antennapod.core.cast.CastUtils; import de.danoeh.antennapod.core.cast.CastUtils;
import de.danoeh.antennapod.core.cast.DefaultCastConsumer; import de.danoeh.antennapod.core.cast.DefaultCastConsumer;
import de.danoeh.antennapod.core.cast.RemoteMedia; import de.danoeh.antennapod.core.util.playback.RemoteMedia;
import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.MediaType; import de.danoeh.antennapod.core.feed.MediaType;
import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences;
@ -165,7 +166,7 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
return CastUtils.convertFromFeedMedia((FeedMedia) playable); return CastUtils.convertFromFeedMedia((FeedMedia) playable);
} }
if (playable instanceof RemoteMedia) { if (playable instanceof RemoteMedia) {
return ((RemoteMedia) playable).extractMediaInfo(); return MediaInfoCreator.from((RemoteMedia) playable);
} }
return null; return null;
} }