summaryrefslogtreecommitdiff
path: root/model
diff options
context:
space:
mode:
authorByteHamster <info@bytehamster.com>2021-04-22 23:17:11 +0200
committerByteHamster <info@bytehamster.com>2021-04-22 23:17:11 +0200
commitba66ae76337133d92963fbf9c8ead27ee81ef148 (patch)
treefd08fbf6c70d43a39130a988deac97f80589cff3 /model
parent2a47f49fde3327ee3a1b3c2d66b2c950cda7e14e (diff)
downloadantennapod-ba66ae76337133d92963fbf9c8ead27ee81ef148.zip
Moved model to its own module
Diffstat (limited to 'model')
-rw-r--r--model/README.md3
-rw-r--r--model/build.gradle55
-rw-r--r--model/src/main/AndroidManifest.xml1
-rw-r--r--model/src/main/java/de/danoeh/antennapod/model/feed/Chapter.java65
-rw-r--r--model/src/main/java/de/danoeh/antennapod/model/feed/Feed.java502
-rw-r--r--model/src/main/java/de/danoeh/antennapod/model/feed/FeedComponent.java65
-rw-r--r--model/src/main/java/de/danoeh/antennapod/model/feed/FeedFile.java107
-rw-r--r--model/src/main/java/de/danoeh/antennapod/model/feed/FeedFilter.java112
-rw-r--r--model/src/main/java/de/danoeh/antennapod/model/feed/FeedFunding.java91
-rw-r--r--model/src/main/java/de/danoeh/antennapod/model/feed/FeedItem.java421
-rw-r--r--model/src/main/java/de/danoeh/antennapod/model/feed/FeedItemFilter.java60
-rw-r--r--model/src/main/java/de/danoeh/antennapod/model/feed/FeedMedia.java484
-rw-r--r--model/src/main/java/de/danoeh/antennapod/model/feed/FeedPreferences.java214
-rw-r--r--model/src/main/java/de/danoeh/antennapod/model/feed/SortOrder.java71
-rw-r--r--model/src/main/java/de/danoeh/antennapod/model/feed/VolumeAdaptionSetting.java32
-rw-r--r--model/src/main/java/de/danoeh/antennapod/model/playback/MediaType.java56
-rw-r--r--model/src/main/java/de/danoeh/antennapod/model/playback/Playable.java153
-rw-r--r--model/src/main/java/de/danoeh/antennapod/model/playback/RemoteMedia.java316
18 files changed, 2808 insertions, 0 deletions
diff --git a/model/README.md b/model/README.md
new file mode 100644
index 000000000..42bba70c2
--- /dev/null
+++ b/model/README.md
@@ -0,0 +1,3 @@
+# :model
+
+This module provides basic model classes like `Feed` and `Chapter`.
diff --git a/model/build.gradle b/model/build.gradle
new file mode 100644
index 000000000..4d6d5b56d
--- /dev/null
+++ b/model/build.gradle
@@ -0,0 +1,55 @@
+apply plugin: "com.android.library"
+
+android {
+ compileSdkVersion rootProject.ext.compileSdkVersion
+
+ defaultConfig {
+ minSdkVersion rootProject.ext.minSdkVersion
+ targetSdkVersion rootProject.ext.targetSdkVersion
+
+ vectorDrawables.useSupportLibrary false
+ multiDexEnabled false
+
+ testApplicationId "de.danoeh.antennapod.core.tests"
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile("proguard-android.txt")
+ }
+ debug {
+ // debug build has method count over 64k single-dex threshold.
+ // For building debug build to use on Android < 21 (pre-Android 5) devices,
+ // you need to manually change class
+ // de.danoeh.antennapod.PodcastApp to extend MultiDexApplication .
+ // See Issue #2813
+ multiDexEnabled true
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ testOptions {
+ unitTests {
+ includeAndroidResources = true
+ }
+ }
+
+ lintOptions {
+ disable 'GradleDependency'
+ warningsAsErrors true
+ abortOnError true
+ }
+}
+
+dependencies {
+ annotationProcessor "androidx.annotation:annotation:$annotationVersion"
+ implementation "androidx.appcompat:appcompat:$appcompatVersion"
+ implementation "androidx.media:media:$mediaVersion"
+
+ implementation "org.apache.commons:commons-lang3:$commonslangVersion"
+}
diff --git a/model/src/main/AndroidManifest.xml b/model/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..968649054
--- /dev/null
+++ b/model/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+<manifest package="de.danoeh.antennapod.model" />
diff --git a/model/src/main/java/de/danoeh/antennapod/model/feed/Chapter.java b/model/src/main/java/de/danoeh/antennapod/model/feed/Chapter.java
new file mode 100644
index 000000000..0508df901
--- /dev/null
+++ b/model/src/main/java/de/danoeh/antennapod/model/feed/Chapter.java
@@ -0,0 +1,65 @@
+package de.danoeh.antennapod.model.feed;
+
+public abstract class Chapter extends FeedComponent {
+
+ /** Defines starting point in milliseconds. */
+ private long start;
+ private String title;
+ private String link;
+ private String imageUrl;
+
+ protected Chapter() {
+ }
+
+ protected Chapter(long start) {
+ super();
+ this.start = start;
+ }
+
+ protected Chapter(long start, String title, String link, String imageUrl) {
+ super();
+ this.start = start;
+ this.title = title;
+ this.link = link;
+ this.imageUrl = imageUrl;
+ }
+
+ public abstract int getChapterType();
+
+ public long getStart() {
+ return start;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public String getLink() {
+ return link;
+ }
+
+ public void setStart(long start) {
+ this.start = start;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public void setLink(String link) {
+ this.link = link;
+ }
+
+ public String getImageUrl() {
+ return imageUrl;
+ }
+
+ public void setImageUrl(String imageUrl) {
+ this.imageUrl = imageUrl;
+ }
+
+ @Override
+ public String getHumanReadableIdentifier() {
+ return title;
+ }
+}
diff --git a/model/src/main/java/de/danoeh/antennapod/model/feed/Feed.java b/model/src/main/java/de/danoeh/antennapod/model/feed/Feed.java
new file mode 100644
index 000000000..e570f9bce
--- /dev/null
+++ b/model/src/main/java/de/danoeh/antennapod/model/feed/Feed.java
@@ -0,0 +1,502 @@
+package de.danoeh.antennapod.model.feed;
+
+import android.text.TextUtils;
+
+import androidx.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Data Object for a whole feed.
+ *
+ * @author daniel
+ */
+public class Feed extends FeedFile {
+
+ public static final int FEEDFILETYPE_FEED = 0;
+ public static final String TYPE_RSS2 = "rss";
+ public static final String TYPE_ATOM1 = "atom";
+ public static final String PREFIX_LOCAL_FOLDER = "antennapod_local:";
+
+ /**
+ * title as defined by the feed.
+ */
+ private String feedTitle;
+
+ /**
+ * custom title set by the user.
+ */
+ private String customTitle;
+
+ /**
+ * Contains 'id'-element in Atom feed.
+ */
+ private String feedIdentifier;
+ /**
+ * Link to the website.
+ */
+ private String link;
+ private String description;
+ private String language;
+ /**
+ * Name of the author.
+ */
+ private String author;
+ private String imageUrl;
+ private List<FeedItem> items;
+
+ /**
+ * String that identifies the last update (adopted from Last-Modified or ETag header).
+ */
+ private String lastUpdate;
+
+ private ArrayList<FeedFunding> fundingList;
+ /**
+ * Feed type, for example RSS 2 or Atom.
+ */
+ private String type;
+
+ /**
+ * Feed preferences.
+ */
+ private FeedPreferences preferences;
+
+ /**
+ * The page number that this feed is on. Only feeds with page number "0" should be stored in the
+ * database, feed objects with a higher page number only exist temporarily and should be merged
+ * into feeds with page number "0".
+ * <p/>
+ * This attribute's value is not saved in the database
+ */
+ private int pageNr;
+
+ /**
+ * True if this is a "paged feed", i.e. there exist other feed files that belong to the same
+ * logical feed.
+ */
+ private boolean paged;
+
+ /**
+ * Link to the next page of this feed. If this feed object represents a logical feed (i.e. a feed
+ * that is saved in the database) this might be null while still being a paged feed.
+ */
+ private String nextPageLink;
+
+ private boolean lastUpdateFailed;
+
+ /**
+ * Contains property strings. If such a property applies to a feed item, it is not shown in the feed list
+ */
+ private FeedItemFilter itemfilter;
+
+ /**
+ * User-preferred sortOrder for display.
+ * Only those of scope {@link SortOrder.Scope#INTRA_FEED} is allowed.
+ */
+ @Nullable
+ private SortOrder sortOrder;
+
+ /**
+ * This constructor is used for restoring a feed from the database.
+ */
+ public Feed(long id, String lastUpdate, String title, String customTitle, String link,
+ String description, String paymentLinks, String author, String language,
+ String type, String feedIdentifier, String imageUrl, String fileUrl,
+ String downloadUrl, boolean downloaded, boolean paged, String nextPageLink,
+ String filter, @Nullable SortOrder sortOrder, boolean lastUpdateFailed) {
+ super(fileUrl, downloadUrl, downloaded);
+ this.id = id;
+ this.feedTitle = title;
+ this.customTitle = customTitle;
+ this.lastUpdate = lastUpdate;
+ this.link = link;
+ this.description = description;
+ this.fundingList = FeedFunding.extractPaymentLinks(paymentLinks);
+ this.author = author;
+ this.language = language;
+ this.type = type;
+ this.feedIdentifier = feedIdentifier;
+ this.imageUrl = imageUrl;
+ this.paged = paged;
+ this.nextPageLink = nextPageLink;
+ this.items = new ArrayList<>();
+ if (filter != null) {
+ this.itemfilter = new FeedItemFilter(filter);
+ } else {
+ this.itemfilter = new FeedItemFilter(new String[0]);
+ }
+ setSortOrder(sortOrder);
+ this.lastUpdateFailed = lastUpdateFailed;
+ }
+
+ /**
+ * This constructor is used for test purposes.
+ */
+ public Feed(long id, String lastUpdate, String title, String link, String description, String paymentLink,
+ String author, String language, String type, String feedIdentifier, String imageUrl, String fileUrl,
+ String downloadUrl, boolean downloaded) {
+ this(id, lastUpdate, title, null, link, description, paymentLink, author, language, type, feedIdentifier, imageUrl,
+ fileUrl, downloadUrl, downloaded, false, null, null, null, false);
+ }
+
+ /**
+ * This constructor can be used when parsing feed data. Only the 'lastUpdate' and 'items' field are initialized.
+ */
+ public Feed() {
+ super();
+ }
+
+ /**
+ * This constructor is used for requesting a feed download (it must not be used for anything else!). It should NOT be
+ * used if the title of the feed is already known.
+ */
+ public Feed(String url, String lastUpdate) {
+ super(null, url, false);
+ this.lastUpdate = lastUpdate;
+ }
+
+ /**
+ * This constructor is used for requesting a feed download (it must not be used for anything else!). It should be
+ * used if the title of the feed is already known.
+ */
+ public Feed(String url, String lastUpdate, String title) {
+ this(url, lastUpdate);
+ this.feedTitle = title;
+ }
+
+ /**
+ * This constructor is used for requesting a feed download (it must not be used for anything else!). It should be
+ * used if the title of the feed is already known.
+ */
+ public Feed(String url, String lastUpdate, String title, String username, String password) {
+ this(url, lastUpdate, title);
+ preferences = new FeedPreferences(0, true, FeedPreferences.AutoDeleteAction.GLOBAL, VolumeAdaptionSetting.OFF, username, password);
+ }
+
+ /**
+ * Returns the item at the specified index.
+ *
+ */
+ public FeedItem getItemAtIndex(int position) {
+ return items.get(position);
+ }
+
+ /**
+ * Returns the value that uniquely identifies this Feed. If the
+ * feedIdentifier attribute is not null, it will be returned. Else it will
+ * try to return the title. If the title is not given, it will use the link
+ * of the feed.
+ */
+ public String getIdentifyingValue() {
+ if (feedIdentifier != null && !feedIdentifier.isEmpty()) {
+ return feedIdentifier;
+ } else if (download_url != null && !download_url.isEmpty()) {
+ return download_url;
+ } else if (feedTitle != null && !feedTitle.isEmpty()) {
+ return feedTitle;
+ } else {
+ return link;
+ }
+ }
+
+ @Override
+ public String getHumanReadableIdentifier() {
+ if (!TextUtils.isEmpty(customTitle)) {
+ return customTitle;
+ } else if (!TextUtils.isEmpty(feedTitle)) {
+ return feedTitle;
+ } else {
+ return download_url;
+ }
+ }
+
+ public void updateFromOther(Feed other) {
+ // don't update feed's download_url, we do that manually if redirected
+ // see AntennapodHttpClient
+ if (other.imageUrl != null) {
+ this.imageUrl = other.imageUrl;
+ }
+ if (other.feedTitle != null) {
+ feedTitle = other.feedTitle;
+ }
+ if (other.feedIdentifier != null) {
+ feedIdentifier = other.feedIdentifier;
+ }
+ if (other.link != null) {
+ link = other.link;
+ }
+ if (other.description != null) {
+ description = other.description;
+ }
+ if (other.language != null) {
+ language = other.language;
+ }
+ if (other.author != null) {
+ author = other.author;
+ }
+ if (other.fundingList != null) {
+ fundingList = other.fundingList;
+ }
+ // this feed's nextPage might already point to a higher page, so we only update the nextPage value
+ // if this feed is not paged and the other feed is.
+ if (!this.paged && other.paged) {
+ this.paged = other.paged;
+ this.nextPageLink = other.nextPageLink;
+ }
+ }
+
+ public boolean compareWithOther(Feed other) {
+ if (super.compareWithOther(other)) {
+ return true;
+ }
+ if (other.imageUrl != null) {
+ if (imageUrl == null || !TextUtils.equals(imageUrl, other.imageUrl)) {
+ return true;
+ }
+ }
+ if (!TextUtils.equals(feedTitle, other.feedTitle)) {
+ return true;
+ }
+ if (other.feedIdentifier != null) {
+ if (feedIdentifier == null || !feedIdentifier.equals(other.feedIdentifier)) {
+ return true;
+ }
+ }
+ if (other.link != null) {
+ if (link == null || !link.equals(other.link)) {
+ return true;
+ }
+ }
+ if (other.description != null) {
+ if (description == null || !description.equals(other.description)) {
+ return true;
+ }
+ }
+ if (other.language != null) {
+ if (language == null || !language.equals(other.language)) {
+ return true;
+ }
+ }
+ if (other.author != null) {
+ if (author == null || !author.equals(other.author)) {
+ return true;
+ }
+ }
+ if (other.fundingList != null) {
+ if (fundingList == null || !fundingList.equals(other.fundingList)) {
+ return true;
+ }
+ }
+ if (other.isPaged() && !this.isPaged()) {
+ return true;
+ }
+ if (!TextUtils.equals(other.getNextPageLink(), this.getNextPageLink())) {
+ return true;
+ }
+ return false;
+ }
+
+ public FeedItem getMostRecentItem() {
+ // we could sort, but we don't need to, a simple search is fine...
+ Date mostRecentDate = new Date(0);
+ FeedItem mostRecentItem = null;
+ for (FeedItem item : items) {
+ if (item.getPubDate() != null && item.getPubDate().after(mostRecentDate)) {
+ mostRecentDate = item.getPubDate();
+ mostRecentItem = item;
+ }
+ }
+ return mostRecentItem;
+ }
+
+ @Override
+ public int getTypeAsInt() {
+ return FEEDFILETYPE_FEED;
+ }
+
+ public String getTitle() {
+ return !TextUtils.isEmpty(customTitle) ? customTitle : feedTitle;
+ }
+
+ public void setTitle(String title) {
+ this.feedTitle = title;
+ }
+
+ public String getFeedTitle() {
+ return this.feedTitle;
+ }
+
+ @Nullable
+ public String getCustomTitle() {
+ return this.customTitle;
+ }
+
+ public void setCustomTitle(String customTitle) {
+ if (customTitle == null || customTitle.equals(feedTitle)) {
+ this.customTitle = null;
+ } else {
+ this.customTitle = customTitle;
+ }
+ }
+
+ public String getLink() {
+ return link;
+ }
+
+ public void setLink(String link) {
+ this.link = link;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getImageUrl() {
+ return imageUrl;
+ }
+
+ public void setImageUrl(String imageUrl) {
+ this.imageUrl = imageUrl;
+ }
+
+ public List<FeedItem> getItems() {
+ return items;
+ }
+
+ public void setItems(List<FeedItem> list) {
+ this.items = list;
+ }
+
+ public String getLastUpdate() {
+ return lastUpdate;
+ }
+
+ public void setLastUpdate(String lastModified) {
+ this.lastUpdate = lastModified;
+ }
+
+ public String getFeedIdentifier() {
+ return feedIdentifier;
+ }
+
+ public void setFeedIdentifier(String feedIdentifier) {
+ this.feedIdentifier = feedIdentifier;
+ }
+
+ public void addPayment(FeedFunding funding) {
+ if (fundingList == null) {
+ fundingList = new ArrayList<FeedFunding>();
+ }
+ fundingList.add(funding);
+ }
+
+ public ArrayList<FeedFunding> getPaymentLinks() {
+ return fundingList;
+ }
+
+ public String getLanguage() {
+ return language;
+ }
+
+ public void setLanguage(String language) {
+ this.language = language;
+ }
+
+ public String getAuthor() {
+ return author;
+ }
+
+ public void setAuthor(String author) {
+ this.author = author;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public void setPreferences(FeedPreferences preferences) {
+ this.preferences = preferences;
+ }
+
+ public FeedPreferences getPreferences() {
+ return preferences;
+ }
+
+ @Override
+ public void setId(long id) {
+ super.setId(id);
+ if (preferences != null) {
+ preferences.setFeedID(id);
+ }
+ }
+
+ public int getPageNr() {
+ return pageNr;
+ }
+
+ public void setPageNr(int pageNr) {
+ this.pageNr = pageNr;
+ }
+
+ public boolean isPaged() {
+ return paged;
+ }
+
+ public void setPaged(boolean paged) {
+ this.paged = paged;
+ }
+
+ public String getNextPageLink() {
+ return nextPageLink;
+ }
+
+ public void setNextPageLink(String nextPageLink) {
+ this.nextPageLink = nextPageLink;
+ }
+
+ @Nullable
+ public FeedItemFilter getItemFilter() {
+ return itemfilter;
+ }
+
+ public void setItemFilter(String[] properties) {
+ if (properties != null) {
+ this.itemfilter = new FeedItemFilter(properties);
+ }
+ }
+
+ @Nullable
+ public SortOrder getSortOrder() {
+ return sortOrder;
+ }
+
+ public void setSortOrder(@Nullable SortOrder sortOrder) {
+ if (sortOrder != null && sortOrder.scope != SortOrder.Scope.INTRA_FEED) {
+ throw new IllegalArgumentException("The specified sortOrder " + sortOrder
+ + " is invalid. Only those with INTRA_FEED scope are allowed.");
+ }
+ this.sortOrder = sortOrder;
+ }
+
+ public boolean hasLastUpdateFailed() {
+ return this.lastUpdateFailed;
+ }
+
+ public void setLastUpdateFailed(boolean lastUpdateFailed) {
+ this.lastUpdateFailed = lastUpdateFailed;
+ }
+
+ public boolean isLocalFeed() {
+ return download_url.startsWith(PREFIX_LOCAL_FOLDER);
+ }
+}
diff --git a/model/src/main/java/de/danoeh/antennapod/model/feed/FeedComponent.java b/model/src/main/java/de/danoeh/antennapod/model/feed/FeedComponent.java
new file mode 100644
index 000000000..fa0ace3dc
--- /dev/null
+++ b/model/src/main/java/de/danoeh/antennapod/model/feed/FeedComponent.java
@@ -0,0 +1,65 @@
+package de.danoeh.antennapod.model.feed;
+
+/**
+ * Represents every possible component of a feed
+ *
+ * @author daniel
+ */
+public abstract class FeedComponent {
+
+ long id;
+
+ FeedComponent() {
+ super();
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ /**
+ * Update this FeedComponent's attributes with the attributes from another
+ * FeedComponent. This method should only update attributes which where read from
+ * the feed.
+ */
+ void updateFromOther(FeedComponent other) {
+ }
+
+ /**
+ * Compare's this FeedComponent's attribute values with another FeedComponent's
+ * attribute values. This method will only compare attributes which were
+ * read from the feed.
+ *
+ * @return true if attribute values are different, false otherwise
+ */
+ boolean compareWithOther(FeedComponent other) {
+ return false;
+ }
+
+
+ /**
+ * Should return a non-null, human-readable String so that the item can be
+ * identified by the user. Can be title, download-url, etc.
+ */
+ public abstract String getHumanReadableIdentifier();
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof FeedComponent)) return false;
+
+ FeedComponent that = (FeedComponent) o;
+
+ return id == that.id;
+
+ }
+
+ @Override
+ public int hashCode() {
+ return (int) (id ^ (id >>> 32));
+ }
+}
diff --git a/model/src/main/java/de/danoeh/antennapod/model/feed/FeedFile.java b/model/src/main/java/de/danoeh/antennapod/model/feed/FeedFile.java
new file mode 100644
index 000000000..c04fa0bb9
--- /dev/null
+++ b/model/src/main/java/de/danoeh/antennapod/model/feed/FeedFile.java
@@ -0,0 +1,107 @@
+package de.danoeh.antennapod.model.feed;
+
+import android.text.TextUtils;
+
+import java.io.File;
+
+/**
+ * Represents a component of a Feed that has to be downloaded
+ */
+public abstract class FeedFile extends FeedComponent {
+
+ String file_url;
+ protected String download_url;
+ boolean downloaded;
+
+ /**
+ * Creates a new FeedFile object.
+ *
+ * @param file_url The location of the FeedFile. If this is null, the downloaded-attribute
+ * will automatically be set to false.
+ * @param download_url The location where the FeedFile can be downloaded.
+ * @param downloaded true if the FeedFile has been downloaded, false otherwise. This parameter
+ * will automatically be interpreted as false if the file_url is null.
+ */
+ public FeedFile(String file_url, String download_url, boolean downloaded) {
+ super();
+ this.file_url = file_url;
+ this.download_url = download_url;
+ this.downloaded = (file_url != null) && downloaded;
+ }
+
+ public FeedFile() {
+ this(null, null, false);
+ }
+
+ public abstract int getTypeAsInt();
+
+ /**
+ * Update this FeedFile's attributes with the attributes from another
+ * FeedFile. This method should only update attributes which where read from
+ * the feed.
+ */
+ void updateFromOther(FeedFile other) {
+ super.updateFromOther(other);
+ this.download_url = other.download_url;
+ }
+
+ /**
+ * Compare's this FeedFile's attribute values with another FeedFile's
+ * attribute values. This method will only compare attributes which were
+ * read from the feed.
+ *
+ * @return true if attribute values are different, false otherwise
+ */
+ boolean compareWithOther(FeedFile other) {
+ if (super.compareWithOther(other)) {
+ return true;
+ }
+ if (!TextUtils.equals(download_url, other.download_url)) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the file exists at file_url.
+ */
+ public boolean fileExists() {
+ if (file_url == null) {
+ return false;
+ } else {
+ File f = new File(file_url);
+ return f.exists();
+ }
+ }
+
+ public String getFile_url() {
+ return file_url;
+ }
+
+ /**
+ * Changes the file_url of this FeedFile. Setting this value to
+ * null will also set the downloaded-attribute to false.
+ */
+ public void setFile_url(String file_url) {
+ this.file_url = file_url;
+ if (file_url == null) {
+ downloaded = false;
+ }
+ }
+
+ public String getDownload_url() {
+ return download_url;
+ }
+
+ public void setDownload_url(String download_url) {
+ this.download_url = download_url;
+ }
+
+ public boolean isDownloaded() {
+ return downloaded;
+ }
+
+ public void setDownloaded(boolean downloaded) {
+ this.downloaded = downloaded;
+ }
+}
diff --git a/model/src/main/java/de/danoeh/antennapod/model/feed/FeedFilter.java b/model/src/main/java/de/danoeh/antennapod/model/feed/FeedFilter.java
new file mode 100644
index 000000000..4d1d76f1f
--- /dev/null
+++ b/model/src/main/java/de/danoeh/antennapod/model/feed/FeedFilter.java
@@ -0,0 +1,112 @@
+package de.danoeh.antennapod.model.feed;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class FeedFilter implements Serializable {
+
+ private static final String TAG = "FeedFilter";
+
+ private final String includeFilter;
+ private final String excludeFilter;
+
+ public FeedFilter() {
+ this("", "");
+ }
+
+ public FeedFilter(String includeFilter, String excludeFilter) {
+ // We're storing the strings and not the parsed terms because
+ // 1. It's easier to show the user exactly what they typed in this way
+ // (we don't have to recreate it)
+ // 2. We don't know if we'll actually be asked to parse anything anyways.
+ this.includeFilter = includeFilter;
+ this.excludeFilter = excludeFilter;
+ }
+
+ /**
+ * Parses the text in to a list of single words or quoted strings.
+ * Example: "One "Two Three"" returns ["One", "Two Three"]
+ * @param filter string to parse in to terms
+ * @return list of terms
+ */
+ private List<String> parseTerms(String filter) {
+ // from http://stackoverflow.com/questions/7804335/split-string-on-spaces-in-java-except-if-between-quotes-i-e-treat-hello-wor
+ List<String> list = new ArrayList<>();
+ Matcher m = Pattern.compile("([^\"]\\S*|\".+?\")\\s*").matcher(filter);
+ while (m.find())
+ list.add(m.group(1).replace("\"", ""));
+ return list;
+ }
+
+ /**
+ * @param item
+ * @return true if the item should be downloaded
+ */
+ public boolean shouldAutoDownload(FeedItem item) {
+
+ List<String> includeTerms = parseTerms(includeFilter);
+ List<String> excludeTerms = parseTerms(excludeFilter);
+
+ if (includeTerms.size() == 0 && excludeTerms.size() == 0) {
+ // nothing has been specified, so include everything
+ return true;
+ }
+
+ // check using lowercase so the users don't have to worry about case.
+ String title = item.getTitle().toLowerCase();
+
+ // if it's explicitly excluded, it shouldn't be autodownloaded
+ // even if it has include terms
+ for (String term : excludeTerms) {
+ if (title.contains(term.trim().toLowerCase())) {
+ return false;
+ }
+ }
+
+ for (String term : includeTerms) {
+ if (title.contains(term.trim().toLowerCase())) {
+ return true;
+ }
+ }
+
+ // now's the tricky bit
+ // if they haven't set an include filter, but they have set an exclude filter
+ // default to including, but if they've set both, then exclude
+ if (!hasIncludeFilter() && hasExcludeFilter()) {
+ return true;
+ }
+
+ return false;
+ }
+
+ public String getIncludeFilter() {
+ return includeFilter;
+ }
+
+ public String getExcludeFilter() { return excludeFilter; }
+
+ /**
+ * @return true if only include is set
+ */
+ public boolean includeOnly() {
+ return hasIncludeFilter() && !hasExcludeFilter();
+ }
+
+ /**
+ * @return true if only exclude is set
+ */
+ public boolean excludeOnly() {
+ return hasExcludeFilter() && !hasIncludeFilter();
+ }
+
+ public boolean hasIncludeFilter() {
+ return includeFilter.length() > 0;
+ }
+
+ public boolean hasExcludeFilter() {
+ return excludeFilter.length() > 0;
+ }
+}
diff --git a/model/src/main/java/de/danoeh/antennapod/model/feed/FeedFunding.java b/model/src/main/java/de/danoeh/antennapod/model/feed/FeedFunding.java
new file mode 100644
index 000000000..23c0abb97
--- /dev/null
+++ b/model/src/main/java/de/danoeh/antennapod/model/feed/FeedFunding.java
@@ -0,0 +1,91 @@
+package de.danoeh.antennapod.model.feed;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.ArrayList;
+
+public class FeedFunding {
+ public static final String FUNDING_ENTRIES_SEPARATOR = "\u001e";
+ public static final String FUNDING_TITLE_SEPARATOR = "\u001f";
+
+ public String url;
+ public String content;
+
+ public FeedFunding(String url, String content) {
+ this.url = url;
+ this.content = content;
+ }
+
+ public void setContent(String content) {
+ this.content = content;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null || !obj.getClass().equals(this.getClass())) {
+ return false;
+ }
+
+ FeedFunding funding = (FeedFunding) obj;
+ if (url == null && funding.url == null && content == null && funding.content == null) {
+ return true;
+ }
+ if (url != null && url.equals(funding.url) && content != null && content.equals(funding.content)) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return (url + FUNDING_TITLE_SEPARATOR + content).hashCode();
+ }
+
+ public static ArrayList<FeedFunding> extractPaymentLinks(String payLinks) {
+ if (StringUtils.isBlank(payLinks)) {
+ return null;
+ }
+ // old format before we started with PodcastIndex funding tag
+ ArrayList<FeedFunding> funding = new ArrayList<FeedFunding>();
+ if (!payLinks.contains(FeedFunding.FUNDING_ENTRIES_SEPARATOR)
+ && !payLinks.contains(FeedFunding.FUNDING_TITLE_SEPARATOR)) {
+ funding.add(new FeedFunding(payLinks, ""));
+ return funding;
+ }
+ String [] list = payLinks.split(FeedFunding.FUNDING_ENTRIES_SEPARATOR);
+ if (list.length == 0) {
+ return null;
+ }
+
+ for (String str : list) {
+ String [] linkContent = str.split(FeedFunding.FUNDING_TITLE_SEPARATOR);
+ if (StringUtils.isBlank(linkContent[0])) {
+ continue;
+ }
+ String url = linkContent[0];
+ String title = "";
+ if (linkContent.length > 1 && ! StringUtils.isBlank(linkContent[1])) {
+ title = linkContent[1];
+ }
+ funding.add(new FeedFunding(url, title));
+ }
+ return funding;
+ }
+
+ public static String getPaymentLinksAsString(ArrayList<FeedFunding> fundingList) {
+ StringBuilder result = new StringBuilder();
+ if (fundingList == null) {
+ return null;
+ }
+ for (FeedFunding fund : fundingList) {
+ result.append(fund.url).append(FeedFunding.FUNDING_TITLE_SEPARATOR).append(fund.content);
+ result.append(FeedFunding.FUNDING_ENTRIES_SEPARATOR);
+ }
+ return StringUtils.removeEnd(result.toString(), FeedFunding.FUNDING_ENTRIES_SEPARATOR);
+ }
+
+}
diff --git a/model/src/main/java/de/danoeh/antennapod/model/feed/FeedItem.java b/model/src/main/java/de/danoeh/antennapod/model/feed/FeedItem.java
new file mode 100644
index 000000000..235c3fb8a
--- /dev/null
+++ b/model/src/main/java/de/danoeh/antennapod/model/feed/FeedItem.java
@@ -0,0 +1,421 @@
+package de.danoeh.antennapod.model.feed;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Item (episode) within a feed.
+ *
+ * @author daniel
+ */
+public class FeedItem extends FeedComponent implements Serializable {
+
+ /** tag that indicates this item is in the queue */
+ public static final String TAG_QUEUE = "Queue";
+ /** tag that indicates this item is in favorites */
+ public static final String TAG_FAVORITE = "Favorite";
+
+ /**
+ * The id/guid that can be found in the rss/atom feed. Might not be set.
+ */
+ private String itemIdentifier;
+ private String title;
+ /**
+ * The description of a feeditem.
+ */
+ private String description;
+
+ private String link;
+ private Date pubDate;
+ private FeedMedia media;
+
+ private transient Feed feed;
+ private long feedId;
+
+ private int state;
+ public static final int NEW = -1;
+ public static final int UNPLAYED = 0;
+ public static final int PLAYED = 1;
+
+ private String paymentLink;
+
+ /**
+ * Is true if the database contains any chapters that belong to this item. This attribute is only
+ * written once by DBReader on initialization.
+ * The FeedItem might still have a non-null chapters value. In this case, the list of chapters
+ * has not been saved in the database yet.
+ * */
+ private final boolean hasChapters;
+
+ /**
+ * The list of chapters of this item. This might be null even if there are chapters of this item
+ * in the database. The 'hasChapters' attribute should be used to check if this item has any chapters.
+ * */
+ private transient List<Chapter> chapters;
+ private String imageUrl;
+
+ /*
+ * 0: auto download disabled
+ * 1: auto download enabled (default)
+ * > 1: auto download enabled, (approx.) timestamp of the last failed attempt
+ * where last digit denotes the number of failed attempts
+ */
+ private long autoDownload = 1;
+
+ /**
+ * Any tags assigned to this item
+ */
+ private final Set<String> tags = new HashSet<>();
+
+ public FeedItem() {
+ this.state = UNPLAYED;
+ this.hasChapters = false;
+ }
+
+ /**
+ * This constructor is used by DBReader.
+ * */
+ public FeedItem(long id, String title, String link, Date pubDate, String paymentLink, long feedId,
+ boolean hasChapters, String imageUrl, int state,
+ String itemIdentifier, long autoDownload) {
+ this.id = id;
+ this.title = title;
+ this.link = link;
+ this.pubDate = pubDate;
+ this.paymentLink = paymentLink;
+ this.feedId = feedId;
+ this.hasChapters = hasChapters;
+ this.imageUrl = imageUrl;
+ this.state = state;
+ this.itemIdentifier = itemIdentifier;
+ this.autoDownload = autoDownload;
+ }
+
+ /**
+ * This constructor should be used for creating test objects.
+ */
+ public FeedItem(long id, String title, String itemIdentifier, String link, Date pubDate, int state, Feed feed) {
+ this.id = id;
+ this.title = title;
+ this.itemIdentifier = itemIdentifier;
+ this.link = link;
+ this.pubDate = (pubDate != null) ? (Date) pubDate.clone() : null;
+ this.state = state;
+ this.feed = feed;
+ this.hasChapters = false;
+ }
+
+ /**
+ * This constructor should be used for creating test objects involving chapter marks.
+ */
+ public FeedItem(long id, String title, String itemIdentifier, String link, Date pubDate, int state, Feed feed, boolean hasChapters) {
+ this.id = id;
+ this.title = title;
+ this.itemIdentifier = itemIdentifier;
+ this.link = link;
+ this.pubDate = (pubDate != null) ? (Date) pubDate.clone() : null;
+ this.state = state;
+ this.feed = feed;
+ this.hasChapters = hasChapters;
+ }
+
+ public void updateFromOther(FeedItem other) {
+ super.updateFromOther(other);
+ if (other.imageUrl != null) {
+ this.imageUrl = other.imageUrl;
+ }
+ if (other.title != null) {
+ title = other.title;
+ }
+ if (other.getDescription() != null) {
+ description = other.getDescription();
+ }
+ if (other.link != null) {
+ link = other.link;
+ }
+ if (other.pubDate != null && !other.pubDate.equals(pubDate)) {
+ pubDate = other.pubDate;
+ }
+ if (other.media != null) {
+ if (media == null) {
+ setMedia(other.media);
+ // reset to new if feed item did link to a file before
+ setNew();
+ } else if (media.compareWithOther(other.media)) {
+ media.updateFromOther(other.media);
+ }
+ }
+ if (other.paymentLink != null) {
+ paymentLink = other.paymentLink;
+ }
+ if (other.chapters != null) {
+ if (!hasChapters) {
+ chapters = other.chapters;
+ }
+ }
+ }
+
+ /**
+ * Returns the value that uniquely identifies this FeedItem. If the
+ * itemIdentifier attribute is not null, it will be returned. Else it will
+ * try to return the title. If the title is not given, it will use the link
+ * of the entry.
+ */
+ public String getIdentifyingValue() {
+ if (itemIdentifier != null && !itemIdentifier.isEmpty()) {
+ return itemIdentifier;
+ } else if (title != null && !title.isEmpty()) {
+ return title;
+ } else if (hasMedia() && media.getDownload_url() != null) {
+ return media.getDownload_url();
+ } else {
+ return link;
+ }
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public String getLink() {
+ return link;
+ }
+
+ public void setLink(String link) {
+ this.link = link;
+ }
+
+ public Date getPubDate() {
+ if (pubDate != null) {
+ return (Date) pubDate.clone();
+ } else {
+ return null;
+ }
+ }
+
+ public void setPubDate(Date pubDate) {
+ if (pubDate != null) {
+ this.pubDate = (Date) pubDate.clone();
+ } else {
+ this.pubDate = null;
+ }
+ }
+
+ @Nullable
+ public FeedMedia getMedia() {
+ return media;
+ }
+
+ /**
+ * Sets the media object of this FeedItem. If the given
+ * FeedMedia object is not null, it's 'item'-attribute value
+ * will also be set to this item.
+ */
+ public void setMedia(FeedMedia media) {
+ this.media = media;
+ if (media != null && media.getItem() != this) {
+ media.setItem(this);
+ }
+ }
+
+ public Feed getFeed() {
+ return feed;
+ }
+
+ public void setFeed(Feed feed) {
+ this.feed = feed;
+ }
+
+ public boolean isNew() {
+ return state == NEW;
+ }
+
+
+ public void setNew() {
+ state = NEW;
+ }
+
+ public boolean isPlayed() {
+ return state == PLAYED;
+ }
+
+ public void setPlayed(boolean played) {
+ if (played) {
+ state = PLAYED;
+ } else {
+ state = UNPLAYED;
+ }
+ }
+
+ public boolean isInProgress() {
+ return (media != null && media.isInProgress());
+ }
+
+ /**
+ * Updates this item's description property if the given argument is longer than the already stored description
+ * @param newDescription The new item description, content:encoded, itunes:description, etc.
+ */
+ public void setDescriptionIfLonger(String newDescription) {
+ if (newDescription == null) {
+ return;
+ }
+ if (this.description == null) {
+ this.description = newDescription;
+ } else if (this.description.length() < newDescription.length()) {
+ this.description = newDescription;
+ }
+ }
+
+ public String getPaymentLink() {
+ return paymentLink;
+ }
+
+ public void setPaymentLink(String paymentLink) {
+ this.paymentLink = paymentLink;
+ }
+
+ public List<Chapter> getChapters() {
+ return chapters;
+ }
+
+ public void setChapters(List<Chapter> chapters) {
+ this.chapters = chapters;
+ }
+
+ public String getItemIdentifier() {
+ return itemIdentifier;
+ }
+
+ public void setItemIdentifier(String itemIdentifier) {
+ this.itemIdentifier = itemIdentifier;
+ }
+
+ public boolean hasMedia() {
+ return media != null;
+ }
+
+ public String getImageLocation() {
+ if (imageUrl != null) {
+ return imageUrl;
+ } else if (media != null && media.hasEmbeddedPicture()) {
+ return FeedMedia.FILENAME_PREFIX_EMBEDDED_COVER + media.getLocalMediaUrl();
+ } else if (feed != null) {
+ return feed.getImageUrl();
+ } else {
+ return null;
+ }
+ }
+
+ public enum State {
+ UNREAD, IN_PROGRESS, READ, PLAYING
+ }
+
+ public long getFeedId() {
+ return feedId;
+ }
+
+ public void setFeedId(long feedId) {
+ this.feedId = feedId;
+ }
+
+ /**
+ * Returns the image of this item, as specified in the feed.
+ * To load the image that can be displayed to the user, use {@link #getImageLocation},
+ * which also considers embedded pictures or the feed picture if no other picture is present.
+ */
+ public String getImageUrl() {
+ return imageUrl;
+ }
+
+ public void setImageUrl(String imageUrl) {
+ this.imageUrl = imageUrl;
+ }
+
+ @Override
+ public String getHumanReadableIdentifier() {
+ return title;
+ }
+
+ public boolean hasChapters() {
+ return hasChapters;
+ }
+
+ public void setAutoDownload(boolean autoDownload) {
+ this.autoDownload = autoDownload ? 1 : 0;
+ }
+
+ public boolean getAutoDownload() {
+ return this.autoDownload > 0;
+ }
+
+ public int getFailedAutoDownloadAttempts() {
+ if (autoDownload <= 1) {
+ return 0;
+ }
+ int failedAttempts = (int)(autoDownload % 10);
+ if (failedAttempts == 0) {
+ failedAttempts = 10;
+ }
+ return failedAttempts;
+ }
+
+ public boolean isAutoDownloadable() {
+ if (media == null || media.isDownloaded() || autoDownload == 0) {
+ return false;
+ }
+ if (autoDownload == 1) {
+ return true;
+ }
+ int failedAttempts = getFailedAutoDownloadAttempts();
+ double magicValue = 1.767; // 1.767^(10[=#maxNumAttempts]-1) = 168 hours / 7 days
+ int millisecondsInHour = 3600000;
+ long waitingTime = (long) (Math.pow(magicValue, failedAttempts - 1) * millisecondsInHour);
+ long grace = TimeUnit.MINUTES.toMillis(5);
+ return System.currentTimeMillis() > (autoDownload + waitingTime - grace);
+ }
+
+ /**
+ * @return true if the item has this tag
+ */
+ public boolean isTagged(String tag) {
+ return tags.contains(tag);
+ }
+
+ /**
+ * @param tag adds this tag to the item. NOTE: does NOT persist to the database
+ */
+ public void addTag(String tag) {
+ tags.add(tag);
+ }
+
+ /**
+ * @param tag the to remove
+ */
+ public void removeTag(String tag) {
+ tags.remove(tag);
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
+ }
+}
diff --git a/model/src/main/java/de/danoeh/antennapod/model/feed/FeedItemFilter.java b/model/src/main/java/de/danoeh/antennapod/model/feed/FeedItemFilter.java
new file mode 100644
index 000000000..20434213a
--- /dev/null
+++ b/model/src/main/java/de/danoeh/antennapod/model/feed/FeedItemFilter.java
@@ -0,0 +1,60 @@
+package de.danoeh.antennapod.model.feed;
+
+import android.text.TextUtils;
+import java.util.Arrays;
+
+public class FeedItemFilter {
+
+ private final String[] properties;
+
+ public final boolean showPlayed;
+ public final boolean showUnplayed;
+ public final boolean showPaused;
+ public final boolean showNotPaused;
+ public final boolean showQueued;
+ public final boolean showNotQueued;
+ public final boolean showDownloaded;
+ public final boolean showNotDownloaded;
+ public final boolean showHasMedia;
+ public final boolean showNoMedia;
+ public final boolean showIsFavorite;
+ public final boolean showNotFavorite;
+
+ public static FeedItemFilter unfiltered() {
+ return new FeedItemFilter("");
+ }
+
+ public FeedItemFilter(String properties) {
+ this(TextUtils.split(properties, ","));
+ }
+
+ public FeedItemFilter(String[] properties) {
+ this.properties = properties;
+
+ // see R.arrays.feed_filter_values
+ showUnplayed = hasProperty("unplayed");
+ showPaused = hasProperty("paused");
+ showNotPaused = hasProperty("not_paused");
+ showPlayed = hasProperty("played");
+ showQueued = hasProperty("queued");
+ showNotQueued = hasProperty("not_queued");
+ showDownloaded = hasProperty("downloaded");
+ showNotDownloaded = hasProperty("not_downloaded");
+ showHasMedia = hasProperty("has_media");
+ showNoMedia = hasProperty("no_media");
+ showIsFavorite = hasProperty("is_favorite");
+ showNotFavorite = hasProperty("not_favorite");
+ }
+
+ private boolean hasProperty(String property) {
+ return Arrays.asList(properties).contains(property);
+ }
+
+ public String[] getValues() {
+ return properties.clone();
+ }
+
+ public boolean isShowDownloaded() {
+ return showDownloaded;
+ }
+}
diff --git a/model/src/main/java/de/danoeh/antennapod/model/feed/FeedMedia.java b/model/src/main/java/de/danoeh/antennapod/model/feed/FeedMedia.java
new file mode 100644
index 000000000..d2e4e4556
--- /dev/null
+++ b/model/src/main/java/de/danoeh/antennapod/model/feed/FeedMedia.java
@@ -0,0 +1,484 @@
+package de.danoeh.antennapod.model.feed;
+
+import android.content.Context;
+import android.content.SharedPreferences.Editor;
+import android.media.MediaMetadataRetriever;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import androidx.annotation.Nullable;
+import android.support.v4.media.MediaBrowserCompat;
+import android.support.v4.media.MediaDescriptionCompat;
+import de.danoeh.antennapod.model.playback.MediaType;
+import de.danoeh.antennapod.model.playback.Playable;
+import de.danoeh.antennapod.model.playback.RemoteMedia;
+
+import java.util.Date;
+import java.util.List;
+
+public class FeedMedia extends FeedFile implements Playable {
+ public static final int FEEDFILETYPE_FEEDMEDIA = 2;
+ public static final int PLAYABLE_TYPE_FEEDMEDIA = 1;
+ public static final String FILENAME_PREFIX_EMBEDDED_COVER = "metadata-retriever:";
+
+ public static final String PREF_MEDIA_ID = "FeedMedia.PrefMediaId";
+ private static final String PREF_FEED_ID = "FeedMedia.PrefFeedId";
+
+ /**
+ * Indicates we've checked on the size of the item via the network
+ * and got an invalid response. Using Integer.MIN_VALUE because
+ * 1) we'll still check on it in case it gets downloaded (it's <= 0)
+ * 2) By default all FeedMedia have a size of 0 if we don't know it,
+ * so this won't conflict with existing practice.
+ */
+ private static final int CHECKED_ON_SIZE_BUT_UNKNOWN = Integer.MIN_VALUE;
+
+ private int duration;
+ private int position; // Current position in file
+ private long lastPlayedTime; // Last time this media was played (in ms)
+ private int played_duration; // How many ms of this file have been played
+ private long size; // File size in Byte
+ private String mime_type;
+ @Nullable private volatile FeedItem item;
+ private Date playbackCompletionDate;
+ private int startPosition = -1;
+ private int playedDurationWhenStarted;
+
+ // if null: unknown, will be checked
+ private Boolean hasEmbeddedPicture;
+
+ /* Used for loading item when restoring from parcel. */
+ private long itemID;
+
+ public FeedMedia(FeedItem i, String download_url, long size,
+ String mime_type) {
+ super(null, download_url, false);
+ this.item = i;
+ this.size = size;
+ this.mime_type = mime_type;
+ }
+
+ public FeedMedia(long id, FeedItem item, int duration, int position,
+ long size, String mime_type, String file_url, String download_url,
+ boolean downloaded, Date playbackCompletionDate, int played_duration,
+ long lastPlayedTime) {
+ super(file_url, download_url, downloaded);
+ this.id = id;
+ this.item = item;
+ this.duration = duration;
+ this.position = position;
+ this.played_duration = played_duration;
+ this.playedDurationWhenStarted = played_duration;
+ this.size = size;
+ this.mime_type = mime_type;
+ this.playbackCompletionDate = playbackCompletionDate == null
+ ? null : (Date) playbackCompletionDate.clone();
+ this.lastPlayedTime = lastPlayedTime;
+ }
+
+ public FeedMedia(long id, FeedItem item, int duration, int position,
+ long size, String mime_type, String file_url, String download_url,
+ boolean downloaded, Date playbackCompletionDate, int played_duration,
+ Boolean hasEmbeddedPicture, long lastPlayedTime) {
+ this(id, item, duration, position, size, mime_type, file_url, download_url, downloaded,
+ playbackCompletionDate, played_duration, lastPlayedTime);
+ this.hasEmbeddedPicture = hasEmbeddedPicture;
+ }
+
+ @Override
+ public String getHumanReadableIdentifier() {
+ if (item != null && item.getTitle() != null) {
+ return item.getTitle();
+ } else {
+ return download_url;
+ }
+ }
+
+ /**
+ * Returns a MediaItem representing the FeedMedia object.
+ * This is used by the MediaBrowserService
+ */
+ public MediaBrowserCompat.MediaItem getMediaItem() {
+ Playable p = this;
+ MediaDescriptionCompat.Builder builder = new MediaDescriptionCompat.Builder()
+ .setMediaId(String.valueOf(id))
+ .setTitle(p.getEpisodeTitle())
+ .setDescription(p.getFeedTitle())
+ .setSubtitle(p.getFeedTitle());
+ if (item != null) {
+ // getImageLocation() also loads embedded images, which we can not send to external devices
+ if (item.getImageUrl() != null) {
+ builder.setIconUri(Uri.parse(item.getImageUrl()));
+ } else if (item.getFeed() != null && item.getFeed().getImageUrl() != null) {
+ builder.setIconUri(Uri.parse(item.getFeed().getImageUrl()));
+ }
+ }
+ return new MediaBrowserCompat.MediaItem(builder.build(), MediaBrowserCompat.MediaItem.FLAG_PLAYABLE);
+ }
+
+ /**
+ * Uses mimetype to determine the type of media.
+ */
+ public MediaType getMediaType() {
+ return MediaType.fromMimeType(mime_type);
+ }
+
+ public void updateFromOther(FeedMedia other) {
+ super.updateFromOther(other);
+ if (other.size > 0) {
+ size = other.size;
+ }
+ if (other.mime_type != null) {
+ mime_type = other.mime_type;
+ }
+ }
+
+ public boolean compareWithOther(FeedMedia other) {
+ if (super.compareWithOther(other)) {
+ return true;
+ }
+ if (other.mime_type != null) {
+ if (mime_type == null || !mime_type.equals(other.mime_type)) {
+ return true;
+ }
+ }
+ if (other.size > 0 && other.size != size) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public int getTypeAsInt() {
+ return FEEDFILETYPE_FEEDMEDIA;
+ }
+
+ public int getDuration() {
+ return duration;
+ }
+
+ public void setDuration(int duration) {
+ this.duration = duration;
+ }
+
+ @Override
+ public void setLastPlayedTime(long lastPlayedTime) {
+ this.lastPlayedTime = lastPlayedTime;
+ }
+
+ public int getPlayedDuration() {
+ return played_duration;
+ }
+
+ public int getPlayedDurationWhenStarted() {
+ return playedDurationWhenStarted;
+ }
+
+ public void setPlayedDuration(int played_duration) {
+ this.played_duration = played_duration;
+ }
+
+ public int getPosition() {
+ return position;
+ }
+
+ @Override
+ public long getLastPlayedTime() {
+ return lastPlayedTime;
+ }
+
+ public void setPosition(int position) {
+ this.position = position;
+ if(position > 0 && item != null && item.isNew()) {
+ this.item.setPlayed(false);
+ }
+ }
+
+ public long getSize() {
+ return size;
+ }
+
+ public void setSize(long size) {
+ this.size = size;
+ }
+
+ @Override
+ public String getDescription() {
+ if (item != null) {
+ return item.getDescription();
+ }
+ return null;
+ }
+
+ /**
+ * Indicates we asked the service what the size was, but didn't
+ * get a valid answer and we shoudln't check using the network again.
+ */
+ public void setCheckedOnSizeButUnknown() {
+ this.size = CHECKED_ON_SIZE_BUT_UNKNOWN;
+ }
+
+ public boolean checkedOnSizeButUnknown() {
+ return (CHECKED_ON_SIZE_BUT_UNKNOWN == this.size);
+ }
+
+ public String getMime_type() {
+ return mime_type;
+ }
+
+ public void setMime_type(String mime_type) {
+ this.mime_type = mime_type;
+ }
+
+ @Nullable
+ public FeedItem getItem() {
+ return item;
+ }
+
+ /**
+ * Sets the item object of this FeedMedia. If the given
+ * FeedItem object is not null, it's 'media'-attribute value
+ * will also be set to this media object.
+ */
+ public void setItem(FeedItem item) {
+ this.item = item;
+ if (item != null && item.getMedia() != this) {
+ item.setMedia(this);
+ }
+ }
+
+ public Date getPlaybackCompletionDate() {
+ return playbackCompletionDate == null
+ ? null : (Date) playbackCompletionDate.clone();
+ }
+
+ public void setPlaybackCompletionDate(Date playbackCompletionDate) {
+ this.playbackCompletionDate = playbackCompletionDate == null
+ ? null : (Date) playbackCompletionDate.clone();
+ }
+
+ public boolean isInProgress() {
+ return (this.position > 0);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public boolean hasEmbeddedPicture() {
+ if(hasEmbeddedPicture == null) {
+ checkEmbeddedPicture();
+ }
+ return hasEmbeddedPicture;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(id);
+ dest.writeLong(item != null ? item.getId() : 0L);
+
+ dest.writeInt(duration);
+ dest.writeInt(position);
+ dest.writeLong(size);
+ dest.writeString(mime_type);
+ dest.writeString(file_url);
+ dest.writeString(download_url);
+ dest.writeByte((byte) ((downloaded) ? 1 : 0));
+ dest.writeLong((playbackCompletionDate != null) ? playbackCompletionDate.getTime() : 0);
+ dest.writeInt(played_duration);
+ dest.writeLong(lastPlayedTime);
+ }
+
+ @Override
+ public void writeToPreferences(Editor prefEditor) {
+ if(item != null && item.getFeed() != null) {
+ prefEditor.putLong(PREF_FEED_ID, item.getFeed().getId());
+ } else {
+ prefEditor.putLong(PREF_FEED_ID, 0L);
+ }
+ prefEditor.putLong(PREF_MEDIA_ID, id);
+ }
+
+ @Override
+ public String getEpisodeTitle() {
+ if (item == null) {
+ return null;
+ }
+ if (item.getTitle() != null) {
+ return item.getTitle();
+ } else {
+ return item.getIdentifyingValue();
+ }
+ }
+
+ @Override
+ public List<Chapter> getChapters() {
+ if (item == null) {
+ return null;
+ }
+ return item.getChapters();
+ }
+
+ @Override
+ public String getWebsiteLink() {
+ if (item == null) {
+ return null;
+ }
+ return item.getLink();
+ }
+
+ @Override
+ public String getFeedTitle() {
+ if (item == null || item.getFeed() == null) {
+ return null;
+ }
+ return item.getFeed().getTitle();
+ }
+
+ @Override
+ public Object getIdentifier() {
+ return id;
+ }
+
+ @Override
+ public String getLocalMediaUrl() {
+ return file_url;
+ }
+
+ @Override
+ public String getStreamUrl() {
+ return download_url;
+ }
+
+ public int getStartPosition() {
+ return startPosition;
+ }
+
+ @Override
+ public Date getPubDate() {
+ if (item == null) {
+ return null;
+ }
+ if (item.getPubDate() != null) {
+ return item.getPubDate();
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public boolean localFileAvailable() {
+ return isDownloaded() && file_url != null;
+ }
+
+ public long getItemId() {
+ return itemID;
+ }
+
+ @Override
+ public void onPlaybackStart() {
+ startPosition = Math.max(position, 0);
+ playedDurationWhenStarted = played_duration;
+ }
+
+ @Override
+ public void onPlaybackPause(Context context) {
+ if (position > startPosition) {
+ played_duration = playedDurationWhenStarted + position - startPosition;
+ playedDurationWhenStarted = played_duration;
+ }
+ startPosition = position;
+ }
+
+ @Override
+ public void onPlaybackCompleted(Context context) {
+ startPosition = -1;
+ }
+
+ @Override
+ public int getPlayableType() {
+ return PLAYABLE_TYPE_FEEDMEDIA;
+ }
+
+ @Override
+ public void setChapters(List<Chapter> chapters) {
+ if (item != null) {
+ item.setChapters(chapters);
+ }
+ }
+
+ public static final Parcelable.Creator<FeedMedia> CREATOR = new Parcelable.Creator<FeedMedia>() {
+ public FeedMedia createFromParcel(Parcel in) {
+ final long id = in.readLong();
+ final long itemID = in.readLong();
+ FeedMedia result = new FeedMedia(id, null, in.readInt(), in.readInt(), in.readLong(), in.readString(), in.readString(),
+ in.readString(), in.readByte() != 0, new Date(in.readLong()), in.readInt(), in.readLong());
+ result.itemID = itemID;
+ return result;
+ }
+
+ public FeedMedia[] newArray(int size) {
+ return new FeedMedia[size];
+ }
+ };
+
+ @Override
+ public String getImageLocation() {
+ if (item != null) {
+ return item.getImageLocation();
+ } else if (hasEmbeddedPicture()) {
+ return FILENAME_PREFIX_EMBEDDED_COVER + getLocalMediaUrl();
+ } else {
+ return null;
+ }
+ }
+
+ public void setHasEmbeddedPicture(Boolean hasEmbeddedPicture) {
+ this.hasEmbeddedPicture = hasEmbeddedPicture;
+ }
+
+ @Override
+ public void setDownloaded(boolean downloaded) {
+ super.setDownloaded(downloaded);
+ if(item != null && downloaded && item.isNew()) {
+ item.setPlayed(false);
+ }
+ }
+
+ @Override
+ public void setFile_url(String file_url) {
+ super.setFile_url(file_url);
+ }
+
+ public void checkEmbeddedPicture() {
+ if (!localFileAvailable()) {
+ hasEmbeddedPicture = Boolean.FALSE;
+ return;
+ }
+ MediaMetadataRetriever mmr = new MediaMetadataRetriever();
+ try {
+ mmr.setDataSource(getLocalMediaUrl());
+ byte[] image = mmr.getEmbeddedPicture();
+ if(image != null) {
+ hasEmbeddedPicture = Boolean.TRUE;
+ } else {
+ hasEmbeddedPicture = Boolean.FALSE;
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ hasEmbeddedPicture = Boolean.FALSE;
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null) {
+ return false;
+ }
+ if (o instanceof RemoteMedia) {
+ return o.equals(this);
+ }
+ return super.equals(o);
+ }
+}
diff --git a/model/src/main/java/de/danoeh/antennapod/model/feed/FeedPreferences.java b/model/src/main/java/de/danoeh/antennapod/model/feed/FeedPreferences.java
new file mode 100644
index 000000000..cbbb3c2e7
--- /dev/null
+++ b/model/src/main/java/de/danoeh/antennapod/model/feed/FeedPreferences.java
@@ -0,0 +1,214 @@
+package de.danoeh.antennapod.model.feed;
+
+import androidx.annotation.NonNull;
+import android.text.TextUtils;
+
+import java.io.Serializable;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Contains preferences for a single feed.
+ */
+public class FeedPreferences implements Serializable {
+
+ public static final float SPEED_USE_GLOBAL = -1;
+ public static final String TAG_ROOT = "#root";
+ public static final String TAG_SEPARATOR = "\u001e";
+
+ public enum AutoDeleteAction {
+ GLOBAL,
+ YES,
+ NO
+ }
+
+ @NonNull
+ private FeedFilter filter;
+ private long feedID;
+ private boolean autoDownload;
+ private boolean keepUpdated;
+ private AutoDeleteAction autoDeleteAction;
+ private VolumeAdaptionSetting volumeAdaptionSetting;
+ private String username;
+ private String password;
+ private float feedPlaybackSpeed;
+ private int feedSkipIntro;
+ private int feedSkipEnding;
+ private boolean showEpisodeNotification;
+ private final Set<String> tags = new HashSet<>();
+
+ public FeedPreferences(long feedID, boolean autoDownload, AutoDeleteAction autoDeleteAction,
+ VolumeAdaptionSetting volumeAdaptionSetting, String username, String password) {
+ this(feedID, autoDownload, true, autoDeleteAction, volumeAdaptionSetting,
+ username, password, new FeedFilter(), SPEED_USE_GLOBAL, 0, 0, false, new HashSet<>());
+ }
+
+ public FeedPreferences(long feedID, boolean autoDownload, boolean keepUpdated,
+ AutoDeleteAction autoDeleteAction, VolumeAdaptionSetting volumeAdaptionSetting,
+ String username, String password, @NonNull FeedFilter filter, float feedPlaybackSpeed,
+ int feedSkipIntro, int feedSkipEnding, boolean showEpisodeNotification,
+ Set<String> tags) {
+ this.feedID = feedID;
+ this.autoDownload = autoDownload;
+ this.keepUpdated = keepUpdated;
+ this.autoDeleteAction = autoDeleteAction;
+ this.volumeAdaptionSetting = volumeAdaptionSetting;
+ this.username = username;
+ this.password = password;
+ this.filter = filter;
+ this.feedPlaybackSpeed = feedPlaybackSpeed;
+ this.feedSkipIntro = feedSkipIntro;
+ this.feedSkipEnding = feedSkipEnding;
+ this.showEpisodeNotification = showEpisodeNotification;
+ this.tags.addAll(tags);
+ }
+
+ /**
+ * @return the filter for this feed
+ */
+ @NonNull public FeedFilter getFilter() {
+ return filter;
+ }
+
+ public void setFilter(@NonNull FeedFilter filter) {
+ this.filter = filter;
+ }
+
+ /**
+ * @return true if this feed should be refreshed when everything else is being refreshed
+ * if false the feed should only be refreshed if requested directly.
+ */
+ public boolean getKeepUpdated() {
+ return keepUpdated;
+ }
+
+ public void setKeepUpdated(boolean keepUpdated) {
+ this.keepUpdated = keepUpdated;
+ }
+
+ /**
+ * Compare another FeedPreferences with this one. The feedID, autoDownload and AutoDeleteAction attribute are excluded from the
+ * comparison.
+ *
+ * @return True if the two objects are different.
+ */
+ public boolean compareWithOther(FeedPreferences other) {
+ if (other == null) {
+ return true;
+ }
+ if (!TextUtils.equals(username, other.username)) {
+ return true;
+ }
+ if (!TextUtils.equals(password, other.password)) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Update this FeedPreferences object from another one. The feedID, autoDownload and AutoDeleteAction attributes are excluded
+ * from the update.
+ */
+ public void updateFromOther(FeedPreferences other) {
+ if (other == null)
+ return;
+ this.username = other.username;
+ this.password = other.password;
+ }
+
+ public long getFeedID() {
+ return feedID;
+ }
+
+ public void setFeedID(long feedID) {
+ this.feedID = feedID;
+ }
+
+ public boolean getAutoDownload() {
+ return autoDownload;
+ }
+
+ public void setAutoDownload(boolean autoDownload) {
+ this.autoDownload = autoDownload;
+ }
+
+ public AutoDeleteAction getAutoDeleteAction() {
+ return autoDeleteAction;
+ }
+
+ public VolumeAdaptionSetting getVolumeAdaptionSetting() {
+ return volumeAdaptionSetting;
+ }
+
+ public void setAutoDeleteAction(AutoDeleteAction autoDeleteAction) {
+ this.autoDeleteAction = autoDeleteAction;
+ }
+
+ public void setVolumeAdaptionSetting(VolumeAdaptionSetting volumeAdaptionSetting) {
+ this.volumeAdaptionSetting = volumeAdaptionSetting;
+ }
+
+ public AutoDeleteAction getCurrentAutoDelete() {
+ return autoDeleteAction;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public float getFeedPlaybackSpeed() {
+ return feedPlaybackSpeed;
+ }
+
+ public void setFeedPlaybackSpeed(float playbackSpeed) {
+ feedPlaybackSpeed = playbackSpeed;
+ }
+
+ public void setFeedSkipIntro(int skipIntro) {
+ feedSkipIntro = skipIntro;
+ }
+
+ public int getFeedSkipIntro() {
+ return feedSkipIntro;
+ }
+
+ public void setFeedSkipEnding(int skipEnding) {
+ feedSkipEnding = skipEnding;
+ }
+
+ public int getFeedSkipEnding() {
+ return feedSkipEnding;
+ }
+
+ public Set<String> getTags() {
+ return tags;
+ }
+
+ public String getTagsAsString() {
+ return TextUtils.join(TAG_SEPARATOR, tags);
+ }
+
+ /**
+ * getter for preference if notifications should be display for new episodes.
+ * @return true for displaying notifications
+ */
+ public boolean getShowEpisodeNotification() {
+ return showEpisodeNotification;
+ }
+
+ public void setShowEpisodeNotification(boolean showEpisodeNotification) {
+ this.showEpisodeNotification = showEpisodeNotification;
+ }
+}
diff --git a/model/src/main/java/de/danoeh/antennapod/model/feed/SortOrder.java b/model/src/main/java/de/danoeh/antennapod/model/feed/SortOrder.java
new file mode 100644
index 000000000..cbc56fd11
--- /dev/null
+++ b/model/src/main/java/de/danoeh/antennapod/model/feed/SortOrder.java
@@ -0,0 +1,71 @@
+package de.danoeh.antennapod.model.feed;
+
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import static de.danoeh.antennapod.model.feed.SortOrder.Scope.INTER_FEED;
+import static de.danoeh.antennapod.model.feed.SortOrder.Scope.INTRA_FEED;
+
+/**
+ * Provides sort orders to sort a list of episodes.
+ */
+public enum SortOrder {
+ DATE_OLD_NEW(1, INTRA_FEED),
+ DATE_NEW_OLD(2, INTRA_FEED),
+ EPISODE_TITLE_A_Z(3, INTRA_FEED),
+ EPISODE_TITLE_Z_A(4, INTRA_FEED),
+ DURATION_SHORT_LONG(5, INTRA_FEED),
+ DURATION_LONG_SHORT(6, INTRA_FEED),
+ FEED_TITLE_A_Z(101, INTER_FEED),
+ FEED_TITLE_Z_A(102, INTER_FEED),
+ RANDOM(103, INTER_FEED),
+ SMART_SHUFFLE_OLD_NEW(104, INTER_FEED),
+ SMART_SHUFFLE_NEW_OLD(105, INTER_FEED);
+
+ public enum Scope {
+ INTRA_FEED, INTER_FEED
+ }
+
+ public final int code;
+
+ @NonNull
+ public final Scope scope;
+
+ SortOrder(int code, @NonNull Scope scope) {
+ this.code = code;
+ this.scope = scope;
+ }
+
+ /**
+ * Converts the string representation to its enum value. If the string value is unknown,
+ * the given default value is returned.
+ */
+ public static SortOrder parseWithDefault(String value, SortOrder defaultValue) {
+ try {
+ return valueOf(value);
+ } catch (IllegalArgumentException e) {
+ return defaultValue;
+ }
+ }
+
+ @Nullable
+ public static SortOrder fromCodeString(@Nullable String codeStr) {
+ if (TextUtils.isEmpty(codeStr)) {
+ return null;
+ }
+ int code = Integer.parseInt(codeStr);
+ for (SortOrder sortOrder : values()) {
+ if (sortOrder.code == code) {
+ return sortOrder;
+ }
+ }
+ throw new IllegalArgumentException("Unsupported code: " + code);
+ }
+
+ @Nullable
+ public static String toCodeString(@Nullable SortOrder sortOrder) {
+ return sortOrder != null ? Integer.toString(sortOrder.code) : null;
+ }
+}
diff --git a/model/src/main/java/de/danoeh/antennapod/model/feed/VolumeAdaptionSetting.java b/model/src/main/java/de/danoeh/antennapod/model/feed/VolumeAdaptionSetting.java
new file mode 100644
index 000000000..67e30e5c8
--- /dev/null
+++ b/model/src/main/java/de/danoeh/antennapod/model/feed/VolumeAdaptionSetting.java
@@ -0,0 +1,32 @@
+package de.danoeh.antennapod.model.feed;
+
+public enum VolumeAdaptionSetting {
+ OFF(0, 1.0f),
+ LIGHT_REDUCTION(1, 0.5f),
+ HEAVY_REDUCTION(2, 0.2f);
+
+ private final int value;
+ private float adaptionFactor;
+
+ VolumeAdaptionSetting(int value, float adaptionFactor) {
+ this.value = value;
+ this.adaptionFactor = adaptionFactor;
+ }
+
+ public static VolumeAdaptionSetting fromInteger(int value) {
+ for (VolumeAdaptionSetting setting : values()) {
+ if (setting.value == value) {
+ return setting;
+ }
+ }
+ throw new IllegalArgumentException("Cannot map value to VolumeAdaptionSetting: " + value);
+ }
+
+ public int toInteger() {
+ return value;
+ }
+
+ public float getAdaptionFactor() {
+ return adaptionFactor;
+ }
+}
diff --git a/model/src/main/java/de/danoeh/antennapod/model/playback/MediaType.java b/model/src/main/java/de/danoeh/antennapod/model/playback/MediaType.java
new file mode 100644
index 000000000..6a7b36097
--- /dev/null
+++ b/model/src/main/java/de/danoeh/antennapod/model/playback/MediaType.java
@@ -0,0 +1,56 @@
+package de.danoeh.antennapod.model.playback;
+
+import android.text.TextUtils;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+public enum MediaType {
+ AUDIO, VIDEO, UNKNOWN;
+
+ private static final Set<String> AUDIO_APPLICATION_MIME_STRINGS = new HashSet<>(Arrays.asList(
+ "application/ogg",
+ "application/opus",
+ "application/x-flac"
+ ));
+
+ // based on https://developer.android.com/guide/topics/media/media-formats
+ static final Set<String> AUDIO_FILE_EXTENSIONS = new HashSet<>(Arrays.asList(
+ "3gp", "aac", "amr", "flac", "imy", "m4a", "mid", "mkv", "mp3", "mp4", "mxmf", "oga",
+ "ogg", "ogx", "opus", "ota", "rtttl", "rtx", "wav", "xmf"
+ ));
+
+ static final Set<String> VIDEO_FILE_EXTENSIONS = new HashSet<>(Arrays.asList(
+ "3gp", "mkv", "mp4", "ogg", "ogv", "ogx", "webm"
+ ));
+
+ public static MediaType fromMimeType(String mimeType) {
+ if (TextUtils.isEmpty(mimeType)) {
+ return MediaType.UNKNOWN;
+ } else if (mimeType.startsWith("audio")) {
+ return MediaType.AUDIO;
+ } else if (mimeType.startsWith("video")) {
+ return MediaType.VIDEO;
+ } else if (AUDIO_APPLICATION_MIME_STRINGS.contains(mimeType)) {
+ return MediaType.AUDIO;
+ }
+ return MediaType.UNKNOWN;
+ }
+
+ /**
+ * @param extensionWithoutDot the file extension (suffix) without the dot
+ * @return the {@link MediaType} that likely corresponds to the extension. However, since the
+ * extension is not always enough to determine whether a file is an audio or video (3gp
+ * can be both, for example), this may not be correct. As a result, where possible,
+ * {@link #fromMimeType(String) fromMimeType} should always be tried first.
+ */
+ public static MediaType fromFileExtension(String extensionWithoutDot) {
+ if (AUDIO_FILE_EXTENSIONS.contains(extensionWithoutDot)) {
+ return MediaType.AUDIO;
+ } else if (VIDEO_FILE_EXTENSIONS.contains(extensionWithoutDot)) {
+ return MediaType.VIDEO;
+ }
+ return MediaType.UNKNOWN;
+ }
+}
diff --git a/model/src/main/java/de/danoeh/antennapod/model/playback/Playable.java b/model/src/main/java/de/danoeh/antennapod/model/playback/Playable.java
new file mode 100644
index 000000000..151be92e6
--- /dev/null
+++ b/model/src/main/java/de/danoeh/antennapod/model/playback/Playable.java
@@ -0,0 +1,153 @@
+package de.danoeh.antennapod.model.playback;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Parcelable;
+
+import androidx.annotation.Nullable;
+import de.danoeh.antennapod.model.feed.Chapter;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Interface for objects that can be played by the PlaybackService.
+ */
+public interface Playable extends Parcelable {
+ int INVALID_TIME = -1;
+
+ /**
+ * Save information about the playable in a preference so that it can be
+ * restored later via PlayableUtils.createInstanceFromPreferences.
+ * Implementations must NOT call commit() after they have written the values
+ * to the preferences file.
+ */
+ void writeToPreferences(SharedPreferences.Editor prefEditor);
+
+ /**
+ * Returns the title of the episode that this playable represents
+ */
+ String getEpisodeTitle();
+
+ /**
+ * Returns a list of chapter marks or null if this Playable has no chapters.
+ */
+ List<Chapter> getChapters();
+
+ /**
+ * Returns a link to a website that is meant to be shown in a browser
+ */
+ String getWebsiteLink();
+
+ /**
+ * Returns the title of the feed this Playable belongs to.
+ */
+ String getFeedTitle();
+
+ /**
+ * Returns the published date
+ */
+ Date getPubDate();
+
+ /**
+ * Returns a unique identifier, for example a file url or an ID from a
+ * database.
+ */
+ Object getIdentifier();
+
+ /**
+ * Return duration of object or 0 if duration is unknown.
+ */
+ int getDuration();
+
+ /**
+ * Return position of object or 0 if position is unknown.
+ */
+ int getPosition();
+
+ /**
+ * Returns last time (in ms) when this playable was played or 0
+ * if last played time is unknown.
+ */
+ long getLastPlayedTime();
+
+ /**
+ * Returns the description of the item, if available.
+ * For FeedItems, the description needs to be loaded from the database first.
+ */
+ @Nullable
+ String getDescription();
+
+ /**
+ * Returns the type of media.
+ */
+ MediaType getMediaType();
+
+ /**
+ * Returns an url to a local file that can be played or null if this file
+ * does not exist.
+ */
+ String getLocalMediaUrl();
+
+ /**
+ * Returns an url to a file that can be streamed by the player or null if
+ * this url is not known.
+ */
+ String getStreamUrl();
+
+ /**
+ * Returns true if a local file that can be played is available. getFileUrl
+ * MUST return a non-null string if this method returns true.
+ */
+ boolean localFileAvailable();
+
+ void setPosition(int newPosition);
+
+ void setDuration(int newDuration);
+
+ /**
+ * @param lastPlayedTimestamp timestamp in ms
+ */
+ void setLastPlayedTime(long lastPlayedTimestamp);
+
+ /**
+ * This method should be called every time playback starts on this object.
+ * <p/>
+ * Position held by this Playable should be set accurately before a call to this method is made.
+ */
+ void onPlaybackStart();
+
+ /**
+ * This method should be called every time playback pauses or stops on this object,
+ * including just before a seeking operation is performed, after which a call to
+ * {@link #onPlaybackStart()} should be made. If playback completes, calling this method is not
+ * necessary, as long as a call to {@link #onPlaybackCompleted(Context)} is made.
+ * <p/>
+ * Position held by this Playable should be set accurately before a call to this method is made.
+ */
+ void onPlaybackPause(Context context);
+
+ /**
+ * This method should be called when playback completes for this object.
+ * @param context
+ */
+ void onPlaybackCompleted(Context context);
+
+ /**
+ * Returns an integer that must be unique among all Playable classes. The
+ * return value is later used by PlayableUtils to determine the type of the
+ * Playable object that is restored.
+ */
+ int getPlayableType();
+
+ void setChapters(List<Chapter> chapters);
+
+ /**
+ * Returns the location of the image or null if no image is available.
+ * This can be the feed item image URL, the local embedded media image path, the feed image URL,
+ * or the remote media image URL, depending on what's available.
+ */
+ @Nullable
+ String getImageLocation();
+
+}
diff --git a/model/src/main/java/de/danoeh/antennapod/model/playback/RemoteMedia.java b/model/src/main/java/de/danoeh/antennapod/model/playback/RemoteMedia.java
new file mode 100644
index 000000000..d4d91c79c
--- /dev/null
+++ b/model/src/main/java/de/danoeh/antennapod/model/playback/RemoteMedia.java
@@ -0,0 +1,316 @@
+package de.danoeh.antennapod.model.playback;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import androidx.annotation.Nullable;
+import de.danoeh.antennapod.model.feed.Chapter;
+import de.danoeh.antennapod.model.feed.Feed;
+import de.danoeh.antennapod.model.feed.FeedItem;
+import de.danoeh.antennapod.model.feed.FeedMedia;
+
+import java.util.Date;
+import java.util.List;
+
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+
+/**
+ * Playable implementation for media for which a local version of
+ * {@link FeedMedia} hasn't been found.
+ * Used for Casting and for previewing unsubscribed feeds.
+ */
+public class RemoteMedia implements Playable {
+ public static final String TAG = "RemoteMedia";
+
+ public static final int PLAYABLE_TYPE_REMOTE_MEDIA = 3;
+
+ private final String downloadUrl;
+ private final String itemIdentifier;
+ private final String feedUrl;
+ private final String feedTitle;
+ private final String episodeTitle;
+ private final String episodeLink;
+ private final String feedAuthor;
+ private final String imageUrl;
+ private final String feedLink;
+ private final String mimeType;
+ private final Date pubDate;
+ private final String notes;
+ private List<Chapter> chapters;
+ private int duration;
+ private int position;
+ private long lastPlayedTime;
+
+ public RemoteMedia(String downloadUrl, String itemId, String feedUrl, String feedTitle,
+ String episodeTitle, String episodeLink, String feedAuthor,
+ String imageUrl, String feedLink, String mimeType, Date pubDate,
+ String notes) {
+ this.downloadUrl = downloadUrl;
+ this.itemIdentifier = itemId;
+ this.feedUrl = feedUrl;
+ this.feedTitle = feedTitle;
+ this.episodeTitle = episodeTitle;
+ this.episodeLink = episodeLink;
+ this.feedAuthor = feedAuthor;
+ this.imageUrl = imageUrl;
+ this.feedLink = feedLink;
+ this.mimeType = mimeType;
+ this.pubDate = pubDate;
+ this.notes = notes;
+ }
+
+ 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();
+ if (!TextUtils.isEmpty(item.getImageUrl())) {
+ this.imageUrl = item.getImageUrl();
+ } else {
+ this.imageUrl = item.getFeed().getImageUrl();
+ }
+ this.feedLink = item.getFeed().getLink();
+ this.mimeType = item.getMedia().getMime_type();
+ this.pubDate = item.getPubDate();
+ this.notes = item.getDescription();
+ }
+
+ public String getEpisodeIdentifier() {
+ return itemIdentifier;
+ }
+
+ public String getFeedUrl() {
+ 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
+ public void writeToPreferences(SharedPreferences.Editor prefEditor) {
+ //it seems pointless to do it, since the session should be kept by the remote device.
+ }
+
+ @Override
+ public String getEpisodeTitle() {
+ return episodeTitle;
+ }
+
+ @Override
+ public List<Chapter> getChapters() {
+ return chapters;
+ }
+
+ @Override
+ public String getWebsiteLink() {
+ if (episodeLink != null) {
+ return episodeLink;
+ } else {
+ return feedUrl;
+ }
+ }
+
+ @Override
+ public String getFeedTitle() {
+ return feedTitle;
+ }
+
+ @Override
+ public Object getIdentifier() {
+ return itemIdentifier + "@" + feedUrl;
+ }
+
+ @Override
+ public int getDuration() {
+ return duration;
+ }
+
+ @Override
+ public int getPosition() {
+ return position;
+ }
+
+ @Override
+ public long getLastPlayedTime() {
+ return lastPlayedTime;
+ }
+
+ @Override
+ public MediaType getMediaType() {
+ return MediaType.fromMimeType(mimeType);
+ }
+
+ @Override
+ public String getLocalMediaUrl() {
+ return null;
+ }
+
+ @Override
+ public String getStreamUrl() {
+ return downloadUrl;
+ }
+
+ @Override
+ public boolean localFileAvailable() {
+ return false;
+ }
+
+ @Override
+ public void setPosition(int newPosition) {
+ position = newPosition;
+ }
+
+ @Override
+ public void setDuration(int newDuration) {
+ duration = newDuration;
+ }
+
+ @Override
+ public void setLastPlayedTime(long lastPlayedTimestamp) {
+ lastPlayedTime = lastPlayedTimestamp;
+ }
+
+ @Override
+ public void onPlaybackStart() {
+ // no-op
+ }
+
+ @Override
+ public void onPlaybackPause(Context context) {
+ // no-op
+ }
+
+ @Override
+ public void onPlaybackCompleted(Context context) {
+ // no-op
+ }
+
+ @Override
+ public int getPlayableType() {
+ return PLAYABLE_TYPE_REMOTE_MEDIA;
+ }
+
+ @Override
+ public void setChapters(List<Chapter> chapters) {
+ this.chapters = chapters;
+ }
+
+ @Override
+ @Nullable
+ public String getImageLocation() {
+ return imageUrl;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public String getDescription() {
+ return notes;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(downloadUrl);
+ dest.writeString(itemIdentifier);
+ dest.writeString(feedUrl);
+ dest.writeString(feedTitle);
+ dest.writeString(episodeTitle);
+ dest.writeString(episodeLink);
+ dest.writeString(feedAuthor);
+ dest.writeString(imageUrl);
+ dest.writeString(feedLink);
+ dest.writeString(mimeType);
+ dest.writeLong((pubDate != null) ? pubDate.getTime() : 0);
+ dest.writeString(notes);
+ dest.writeInt(duration);
+ dest.writeInt(position);
+ dest.writeLong(lastPlayedTime);
+ }
+
+ public static final Parcelable.Creator<RemoteMedia> CREATOR = new Parcelable.Creator<RemoteMedia>() {
+ @Override
+ public RemoteMedia createFromParcel(Parcel in) {
+ RemoteMedia result = new RemoteMedia(in.readString(), in.readString(), in.readString(),
+ in.readString(), in.readString(), in.readString(), in.readString(), in.readString(),
+ in.readString(), in.readString(), new Date(in.readLong()), in.readString());
+ result.setDuration(in.readInt());
+ result.setPosition(in.readInt());
+ result.setLastPlayedTime(in.readLong());
+ return result;
+ }
+
+ @Override
+ public RemoteMedia[] newArray(int size) {
+ return new RemoteMedia[size];
+ }
+ };
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof RemoteMedia) {
+ RemoteMedia rm = (RemoteMedia) other;
+ return TextUtils.equals(downloadUrl, rm.downloadUrl)
+ && TextUtils.equals(feedUrl, rm.feedUrl)
+ && TextUtils.equals(itemIdentifier, rm.itemIdentifier);
+ }
+ if (other instanceof FeedMedia) {
+ FeedMedia fm = (FeedMedia) other;
+ if (!TextUtils.equals(downloadUrl, fm.getStreamUrl())) {
+ return false;
+ }
+ FeedItem fi = fm.getItem();
+ if (fi == null || !TextUtils.equals(itemIdentifier, fi.getItemIdentifier())) {
+ return false;
+ }
+ Feed feed = fi.getFeed();
+ return feed != null && TextUtils.equals(feedUrl, feed.getDownload_url());
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return new HashCodeBuilder()
+ .append(downloadUrl)
+ .append(feedUrl)
+ .append(itemIdentifier)
+ .toHashCode();
+ }
+}