summaryrefslogtreecommitdiff
path: root/core/src/main/java/de/danoeh/antennapod
diff options
context:
space:
mode:
authorByteHamster <info@bytehamster.com>2021-03-05 10:09:10 +0100
committerByteHamster <info@bytehamster.com>2021-03-05 10:12:35 +0100
commitf76d3ad09e41c544b8af2f33db0529e3bcdabc0e (patch)
treef33d371de5a74e3ac75ff9431168b4a7c76a9246 /core/src/main/java/de/danoeh/antennapod
parent5a8bfc0ea483d0af4db8f266969f1e52c2cd529d (diff)
parentc58aa40b212c7ff5a798c2b3faafabbaaeac0b3f (diff)
downloadAntennaPod-f76d3ad09e41c544b8af2f33db0529e3bcdabc0e.zip
Merge branch 'develop' into folders
Diffstat (limited to 'core/src/main/java/de/danoeh/antennapod')
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/PlaybackServiceCallbacks.java22
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/asynctask/ImageResource.java15
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java90
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java74
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java148
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java78
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java58
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java48
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/util/ImageResourceUtils.java55
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java24
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java39
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/receiver/PlayerWidget.java17
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/PlayerWidgetJobService.java243
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java35
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java84
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java32
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java24
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/NewEpisodesNotification.java132
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java3
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java8
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java20
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java24
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java112
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java16
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java25
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/ssl/BackportCaCerts.java105
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/ssl/BackportTrustManager.java60
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/ssl/CompositeX509TrustManager.java60
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/ssl/NoV1SslSocketFactory.java100
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/APDownloadAlgorithm.java103
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java90
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java70
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java123
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DBUpgrader.java8
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java4
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java31
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/ExceptFavoriteCleanupAlgorithm.java99
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java98
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedCursorMapper.java70
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedItemFilterQuery.java76
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java3
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/sync/gpoddernet/GpodnetService.java174
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/syndication/handler/UnsupportedFeedtypeException.java3
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSContent.java32
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSITunes.java18
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java5
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java10
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java8
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java239
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java7
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/DownloadError.java93
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/Flavors.java24
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/Optional.java213
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/ShownotesProvider.java16
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/ThemeUtils.java25
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/gui/NotificationUtils.java11
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ChapterReader.java193
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3Reader.java299
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/FrameHeader.java20
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/TagHeader.java37
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java8
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java264
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java133
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/PlayableException.java13
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/PlayableUtils.java73
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java81
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/RemoteMedia.java11
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/Timeline.java30
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java6
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdater.java229
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdaterJobService.java35
75 files changed, 2116 insertions, 2728 deletions
diff --git a/core/src/main/java/de/danoeh/antennapod/core/PlaybackServiceCallbacks.java b/core/src/main/java/de/danoeh/antennapod/core/PlaybackServiceCallbacks.java
deleted file mode 100644
index 3dcaac4dc..000000000
--- a/core/src/main/java/de/danoeh/antennapod/core/PlaybackServiceCallbacks.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package de.danoeh.antennapod.core;
-
-import android.content.Context;
-import android.content.Intent;
-
-import de.danoeh.antennapod.core.feed.MediaType;
-
-/**
- * Callbacks for the PlaybackService of the core module
- */
-public interface PlaybackServiceCallbacks {
-
- /**
- * Returns an intent which starts an audio- or videoplayer, depending on the
- * type of media that is being played.
- *
- * @param mediaType The type of media that is being played.
- * @param remotePlayback true if the media is played on a remote device.
- * @return A non-null activity intent.
- */
- Intent getPlayerActivityIntent(Context context, MediaType mediaType, boolean remotePlayback);
-}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/asynctask/ImageResource.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/ImageResource.java
deleted file mode 100644
index b01e3f3ba..000000000
--- a/core/src/main/java/de/danoeh/antennapod/core/asynctask/ImageResource.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package de.danoeh.antennapod.core.asynctask;
-
-/**
- * Classes that implement this interface provide access to an image resource that can
- * be loaded by the Picasso library.
- */
-public interface ImageResource {
-
- /**
- * Returns the location of the image or null if no image is available.
- * <p/>
- * The location can either be an URL or a local path
- */
- String getImageLocation();
-}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java b/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java
index a3b66c951..dd8a466eb 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java
@@ -1,6 +1,5 @@
package de.danoeh.antennapod.core.feed;
-import android.database.Cursor;
import android.text.TextUtils;
import androidx.annotation.Nullable;
@@ -9,26 +8,28 @@ import java.util.ArrayList;
import java.util.Date;
import java.util.List;
-import de.danoeh.antennapod.core.asynctask.ImageResource;
-import de.danoeh.antennapod.core.storage.DBWriter;
-import de.danoeh.antennapod.core.storage.PodDBAdapter;
import de.danoeh.antennapod.core.util.SortOrder;
/**
- * Data Object for a whole feed
+ * Data Object for a whole feed.
*
* @author daniel
*/
-public class Feed extends FeedFile implements ImageResource {
+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 */
+ /**
+ * title as defined by the feed.
+ */
private String feedTitle;
- /* custom title set by the user */
+
+ /**
+ * custom title set by the user.
+ */
private String customTitle;
/**
@@ -42,25 +43,25 @@ public class Feed extends FeedFile implements ImageResource {
private String description;
private String language;
/**
- * Name of the author
+ * 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)
+ * String that identifies the last update (adopted from Last-Modified or ETag header).
*/
private String lastUpdate;
private String paymentLink;
/**
- * Feed type, for example RSS 2 or Atom
+ * Feed type, for example RSS 2 or Atom.
*/
private String type;
/**
- * Feed preferences
+ * Feed preferences.
*/
private FeedPreferences preferences;
@@ -122,7 +123,7 @@ public class Feed extends FeedFile implements ImageResource {
this.paged = paged;
this.nextPageLink = nextPageLink;
this.items = new ArrayList<>();
- if(filter != null) {
+ if (filter != null) {
this.itemfilter = new FeedItemFilter(filter);
} else {
this.itemfilter = new FeedItemFilter(new String[0]);
@@ -132,7 +133,7 @@ public class Feed extends FeedFile implements ImageResource {
}
/**
- * This constructor is used for test purposes
+ * 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,
@@ -175,56 +176,6 @@ public class Feed extends FeedFile implements ImageResource {
preferences = new FeedPreferences(0, true, FeedPreferences.AutoDeleteAction.GLOBAL, VolumeAdaptionSetting.OFF, username, password);
}
- public static Feed fromCursor(Cursor cursor) {
- int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID);
- int indexLastUpdate = cursor.getColumnIndex(PodDBAdapter.KEY_LASTUPDATE);
- int indexTitle = cursor.getColumnIndex(PodDBAdapter.KEY_TITLE);
- int indexCustomTitle = cursor.getColumnIndex(PodDBAdapter.KEY_CUSTOM_TITLE);
- int indexLink = cursor.getColumnIndex(PodDBAdapter.KEY_LINK);
- int indexDescription = cursor.getColumnIndex(PodDBAdapter.KEY_DESCRIPTION);
- int indexPaymentLink = cursor.getColumnIndex(PodDBAdapter.KEY_PAYMENT_LINK);
- int indexAuthor = cursor.getColumnIndex(PodDBAdapter.KEY_AUTHOR);
- int indexLanguage = cursor.getColumnIndex(PodDBAdapter.KEY_LANGUAGE);
- int indexType = cursor.getColumnIndex(PodDBAdapter.KEY_TYPE);
- int indexFeedIdentifier = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_IDENTIFIER);
- int indexFileUrl = cursor.getColumnIndex(PodDBAdapter.KEY_FILE_URL);
- int indexDownloadUrl = cursor.getColumnIndex(PodDBAdapter.KEY_DOWNLOAD_URL);
- int indexDownloaded = cursor.getColumnIndex(PodDBAdapter.KEY_DOWNLOADED);
- int indexIsPaged = cursor.getColumnIndex(PodDBAdapter.KEY_IS_PAGED);
- int indexNextPageLink = cursor.getColumnIndex(PodDBAdapter.KEY_NEXT_PAGE_LINK);
- int indexHide = cursor.getColumnIndex(PodDBAdapter.KEY_HIDE);
- int indexSortOrder = cursor.getColumnIndex(PodDBAdapter.KEY_SORT_ORDER);
- int indexLastUpdateFailed = cursor.getColumnIndex(PodDBAdapter.KEY_LAST_UPDATE_FAILED);
- int indexImageUrl = cursor.getColumnIndex(PodDBAdapter.KEY_IMAGE_URL);
-
- Feed feed = new Feed(
- cursor.getLong(indexId),
- cursor.getString(indexLastUpdate),
- cursor.getString(indexTitle),
- cursor.getString(indexCustomTitle),
- cursor.getString(indexLink),
- cursor.getString(indexDescription),
- cursor.getString(indexPaymentLink),
- cursor.getString(indexAuthor),
- cursor.getString(indexLanguage),
- cursor.getString(indexType),
- cursor.getString(indexFeedIdentifier),
- cursor.getString(indexImageUrl),
- cursor.getString(indexFileUrl),
- cursor.getString(indexDownloadUrl),
- cursor.getInt(indexDownloaded) > 0,
- cursor.getInt(indexIsPaged) > 0,
- cursor.getString(indexNextPageLink),
- cursor.getString(indexHide),
- SortOrder.fromCodeString(cursor.getString(indexSortOrder)),
- cursor.getInt(indexLastUpdateFailed) > 0
- );
-
- FeedPreferences preferences = FeedPreferences.fromCursor(cursor);
- feed.setPreferences(preferences);
- return feed;
- }
-
/**
* Returns the item at the specified index.
*
@@ -384,7 +335,7 @@ public class Feed extends FeedFile implements ImageResource {
}
public void setCustomTitle(String customTitle) {
- if(customTitle == null || customTitle.equals(feedTitle)) {
+ if (customTitle == null || customTitle.equals(feedTitle)) {
this.customTitle = null;
} else {
this.customTitle = customTitle;
@@ -479,10 +430,6 @@ public class Feed extends FeedFile implements ImageResource {
return preferences;
}
- public void savePreferences() {
- DBWriter.setFeedPreferences(preferences);
- }
-
@Override
public void setId(long id) {
super.setId(id);
@@ -491,11 +438,6 @@ public class Feed extends FeedFile implements ImageResource {
}
}
- @Override
- public String getImageLocation() {
- return imageUrl;
- }
-
public int getPageNr() {
return pageNr;
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java
index 131cbe563..d6926385e 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java
@@ -4,7 +4,6 @@ import android.database.Cursor;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import android.text.TextUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
@@ -14,20 +13,16 @@ import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
-import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
-import de.danoeh.antennapod.core.asynctask.ImageResource;
-import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.PodDBAdapter;
-import de.danoeh.antennapod.core.util.ShownotesProvider;
/**
- * Data Object for a XML message
+ * Item (episode) within a feed.
*
* @author daniel
*/
-public class FeedItem extends FeedComponent implements ShownotesProvider, ImageResource, Serializable {
+public class FeedItem extends FeedComponent implements Serializable {
/** tag that indicates this item is in the queue */
public static final String TAG_QUEUE = "Queue";
@@ -43,10 +38,6 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, ImageR
* The description of a feeditem.
*/
private String description;
- /**
- * The content of the content-encoded tag of a feeditem.
- */
- private String contentEncoded;
private String link;
private Date pubDate;
@@ -182,9 +173,6 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, ImageR
if (other.getDescription() != null) {
description = other.getDescription();
}
- if (other.getContentEncoded() != null) {
- contentEncoded = other.contentEncoded;
- }
if (other.link != null) {
link = other.link;
}
@@ -240,10 +228,6 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, ImageR
return description;
}
- public void setDescription(String description) {
- this.description = description;
- }
-
public String getLink() {
return link;
}
@@ -307,7 +291,7 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, ImageR
}
public void setPlayed(boolean played) {
- if(played) {
+ if (played) {
state = PLAYED;
} else {
state = UNPLAYED;
@@ -318,12 +302,19 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, ImageR
return (media != null && media.isInProgress());
}
- public String getContentEncoded() {
- return contentEncoded;
- }
-
- public void setContentEncoded(String contentEncoded) {
- this.contentEncoded = contentEncoded;
+ /**
+ * 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() {
@@ -358,32 +349,13 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, ImageR
return media != null && media.isPlaying();
}
- @Override
- public Callable<String> loadShownotes() {
- return () -> {
- if (contentEncoded == null || description == null) {
- DBReader.loadDescriptionOfFeedItem(FeedItem.this);
- }
- if (TextUtils.isEmpty(contentEncoded)) {
- return description;
- } else if (TextUtils.isEmpty(description)) {
- return contentEncoded;
- } else if (description.length() > 1.25 * contentEncoded.length()) {
- return description;
- } else {
- return contentEncoded;
- }
- };
- }
-
- @Override
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.getImageLocation();
+ return feed.getImageUrl();
} else {
return null;
}
@@ -472,17 +444,23 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, ImageR
/**
* @return true if the item has this tag
*/
- public boolean isTagged(String tag) { return tags.contains(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); }
+ public void addTag(String tag) {
+ tags.add(tag);
+ }
/**
* @param tag the to remove
*/
- public void removeTag(String tag) { tags.remove(tag); }
+ public void removeTag(String tag) {
+ tags.remove(tag);
+ }
@NonNull
@Override
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java
index 787f0e5e7..ac742e765 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java
@@ -1,134 +1,60 @@
package de.danoeh.antennapod.core.feed;
import android.text.TextUtils;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import de.danoeh.antennapod.core.storage.DBReader;
-import de.danoeh.antennapod.core.util.LongList;
-
-import static de.danoeh.antennapod.core.feed.FeedItem.TAG_FAVORITE;
+import java.util.Arrays;
public class FeedItemFilter {
- private final String[] mProperties;
-
- private boolean showPlayed = false;
- private boolean showUnplayed = false;
- private boolean showPaused = false;
- private boolean showNotPaused = false;
- private boolean showQueued = false;
- private boolean showNotQueued = false;
- private boolean showDownloaded = false;
- private boolean showNotDownloaded = false;
- private boolean showHasMedia = false;
- private boolean showNoMedia = false;
- private boolean showIsFavorite = false;
- private boolean showNotFavorite = false;
+ 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.mProperties = properties;
- for (String property : properties) {
- // see R.arrays.feed_filter_values
- switch (property) {
- case "unplayed":
- showUnplayed = true;
- break;
- case "paused":
- showPaused = true;
- break;
- case "not_paused":
- showNotPaused = true;
- break;
- case "played":
- showPlayed = true;
- break;
- case "queued":
- showQueued = true;
- break;
- case "not_queued":
- showNotQueued = true;
- break;
- case "downloaded":
- showDownloaded = true;
- break;
- case "not_downloaded":
- showNotDownloaded = true;
- break;
- case "has_media":
- showHasMedia = true;
- break;
- case "no_media":
- showNoMedia = true;
- break;
- case "is_favorite":
- showIsFavorite = true;
- break;
- case "not_favorite":
- showNotFavorite = true;
- break;
- default:
- break;
- }
- }
+ 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");
}
- /**
- * Run a list of feed items through the filter.
- */
- public List<FeedItem> filter(List<FeedItem> items) {
- if(mProperties.length == 0) return items;
-
- List<FeedItem> result = new ArrayList<>();
-
- // Check for filter combinations that will always return an empty list
- // (e.g. requiring played and unplayed at the same time)
- if (showPlayed && showUnplayed) return result;
- if (showQueued && showNotQueued) return result;
- if (showDownloaded && showNotDownloaded) return result;
-
- final LongList queuedIds = DBReader.getQueueIDList();
- for (FeedItem item : items) {
- // If the item does not meet a requirement, skip it.
-
- if (showPlayed && !item.isPlayed()) continue;
- if (showUnplayed && item.isPlayed()) continue;
-
- if (showPaused && item.getState() != FeedItem.State.IN_PROGRESS) continue;
- if (showNotPaused && item.getState() == FeedItem.State.IN_PROGRESS) continue;
-
- boolean queued = queuedIds.contains(item.getId());
- if (showQueued && !queued) continue;
- if (showNotQueued && queued) continue;
-
- boolean downloaded = item.getMedia() != null && item.getMedia().isDownloaded();
- if (showDownloaded && !downloaded) continue;
- if (showNotDownloaded && downloaded) continue;
-
- if (showHasMedia && !item.hasMedia()) continue;
- if (showNoMedia && item.hasMedia()) continue;
-
- if (showIsFavorite && !item.isTagged(TAG_FAVORITE)) continue;
- if (showNotFavorite && item.isTagged(TAG_FAVORITE)) continue;
-
- // If the item reaches here, it meets all criteria
- result.add(item);
- }
-
- return result;
+ private boolean hasProperty(String property) {
+ return Arrays.asList(properties).contains(property);
}
public String[] getValues() {
- return mProperties.clone();
+ return properties.clone();
}
public boolean isShowDownloaded() {
return showDownloaded;
}
-
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java
index 4857e899d..3070f882c 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java
@@ -12,10 +12,8 @@ import androidx.annotation.Nullable;
import android.support.v4.media.MediaBrowserCompat;
import android.support.v4.media.MediaDescriptionCompat;
-import java.util.Collections;
import java.util.Date;
import java.util.List;
-import java.util.concurrent.Callable;
import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
@@ -24,10 +22,10 @@ import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.storage.PodDBAdapter;
-import de.danoeh.antennapod.core.util.ChapterUtils;
import de.danoeh.antennapod.core.util.playback.Playable;
import de.danoeh.antennapod.core.sync.SyncService;
import de.danoeh.antennapod.core.sync.model.EpisodeAction;
+import de.danoeh.antennapod.core.util.playback.PlayableException;
public class FeedMedia extends FeedFile implements Playable {
private static final String TAG = "FeedMedia";
@@ -175,8 +173,8 @@ public class FeedMedia extends FeedFile implements Playable {
// 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().getImageLocation() != null) {
- builder.setIconUri(Uri.parse(item.getFeed().getImageLocation()));
+ } 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);
@@ -287,6 +285,14 @@ public class FeedMedia extends FeedFile implements Playable {
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.
@@ -385,40 +391,6 @@ public class FeedMedia extends FeedFile implements Playable {
}
@Override
- public void loadChapterMarks(Context context) {
- if (item == null && itemID != 0) {
- item = DBReader.getFeedItem(itemID);
- }
- if (item == null || item.getChapters() != null) {
- return;
- }
-
- List<Chapter> chapters = loadChapters(context);
- if (chapters == null) {
- // Do not try loading again. There are no chapters.
- item.setChapters(Collections.emptyList());
- } else {
- item.setChapters(chapters);
- }
- }
-
- private List<Chapter> loadChapters(Context context) {
- List<Chapter> chaptersFromDatabase = null;
- if (item.hasChapters()) {
- chaptersFromDatabase = DBReader.loadChaptersOfFeedItem(item);
- }
-
- List<Chapter> chaptersFromMediaFile;
- if (localFileAvailable()) {
- chaptersFromMediaFile = ChapterUtils.loadChaptersFromFileUrl(this);
- } else {
- chaptersFromMediaFile = ChapterUtils.loadChaptersFromStreamUrl(this, context);
- }
-
- return ChapterMerger.merge(chaptersFromDatabase, chaptersFromMediaFile);
- }
-
- @Override
public String getEpisodeTitle() {
if (item == null) {
return null;
@@ -478,6 +450,18 @@ public class FeedMedia extends FeedFile implements Playable {
}
@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;
}
@@ -487,6 +471,10 @@ public class FeedMedia extends FeedFile implements Playable {
return download_url != null;
}
+ public long getItemId() {
+ return itemID;
+ }
+
@Override
public void saveCurrentPosition(SharedPreferences pref, int newPosition, long timeStamp) {
if(item != null && item.isNew()) {
@@ -543,21 +531,11 @@ public class FeedMedia extends FeedFile implements Playable {
@Override
public void setChapters(List<Chapter> chapters) {
- if(item != null) {
+ if (item != null) {
item.setChapters(chapters);
}
}
- @Override
- public Callable<String> loadShownotes() {
- return () -> {
- if (item == null) {
- item = DBReader.getFeedItem(itemID);
- }
- return item.loadShownotes().call();
- };
- }
-
public static final Parcelable.Creator<FeedMedia> CREATOR = new Parcelable.Creator<FeedMedia>() {
public FeedMedia createFromParcel(Parcel in) {
final long id = in.readLong();
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java
index bd4690684..794c71cf3 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java
@@ -1,12 +1,10 @@
package de.danoeh.antennapod.core.feed;
-import android.content.Context;
import android.database.Cursor;
import androidx.annotation.NonNull;
import android.text.TextUtils;
import de.danoeh.antennapod.core.preferences.UserPreferences;
-import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.storage.PodDBAdapter;
import java.util.Arrays;
@@ -22,36 +20,37 @@ public class FeedPreferences {
public static final String TAG_ROOT = "#root";
public static final String TAG_SEPARATOR = ",";
- @NonNull
- private FeedFilter filter;
- private long feedID;
- private boolean autoDownload;
- private boolean keepUpdated;
-
public enum AutoDeleteAction {
GLOBAL,
YES,
NO
}
- private AutoDeleteAction autoDeleteAction;
+ @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) {
+ 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, new HashSet<>());
+ username, password, new FeedFilter(), SPEED_USE_GLOBAL, 0, 0, false, new HashSet<>());
}
- private FeedPreferences(long feedID, boolean autoDownload, boolean keepUpdated, AutoDeleteAction autoDeleteAction,
- VolumeAdaptionSetting volumeAdaptionSetting, String username, String password,
- @NonNull FeedFilter filter, float feedPlaybackSpeed, int feedSkipIntro, int feedSkipEnding,
+ private 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;
@@ -64,6 +63,7 @@ public class FeedPreferences {
this.feedPlaybackSpeed = feedPlaybackSpeed;
this.feedSkipIntro = feedSkipIntro;
this.feedSkipEnding = feedSkipEnding;
+ this.showEpisodeNotification = showEpisodeNotification;
this.tags.addAll(tags);
}
@@ -80,6 +80,7 @@ public class FeedPreferences {
int indexFeedPlaybackSpeed = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_PLAYBACK_SPEED);
int indexAutoSkipIntro = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_SKIP_INTRO);
int indexAutoSkipEnding = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_SKIP_ENDING);
+ int indexEpisodeNotification = cursor.getColumnIndex(PodDBAdapter.KEY_EPISODE_NOTIFICATION);
int indexTags = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_TAGS);
long feedId = cursor.getLong(indexId);
@@ -96,11 +97,11 @@ public class FeedPreferences {
float feedPlaybackSpeed = cursor.getFloat(indexFeedPlaybackSpeed);
int feedAutoSkipIntro = cursor.getInt(indexAutoSkipIntro);
int feedAutoSkipEnding = cursor.getInt(indexAutoSkipEnding);
+ boolean showNotification = cursor.getInt(indexEpisodeNotification) > 0;
String tagsString = cursor.getString(indexTags);
if (TextUtils.isEmpty(tagsString)) {
tagsString = TAG_ROOT;
}
-
return new FeedPreferences(feedId,
autoDownload,
autoRefresh,
@@ -112,6 +113,7 @@ public class FeedPreferences {
feedPlaybackSpeed,
feedAutoSkipIntro,
feedAutoSkipEnding,
+ showNotification,
new HashSet<>(Arrays.asList(tagsString.split(TAG_SEPARATOR))));
}
@@ -192,8 +194,8 @@ public class FeedPreferences {
return volumeAdaptionSetting;
}
- public void setAutoDeleteAction(AutoDeleteAction auto_delete_action) {
- this.autoDeleteAction = auto_delete_action;
+ public void setAutoDeleteAction(AutoDeleteAction autoDeleteAction) {
+ this.autoDeleteAction = autoDeleteAction;
}
public void setVolumeAdaptionSetting(VolumeAdaptionSetting volumeAdaptionSetting) {
@@ -204,18 +206,12 @@ public class FeedPreferences {
switch (autoDeleteAction) {
case GLOBAL:
return UserPreferences.isAutoDelete();
-
case YES:
return true;
-
case NO:
+ default: // fall-through
return false;
}
- return false; // TODO - add exceptions here
- }
-
- public void save(Context context) {
- DBWriter.setFeedPreferences(this);
}
public String getUsername() {
@@ -265,4 +261,16 @@ public class FeedPreferences {
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/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java
index 4e59fd750..1418a4e78 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java
@@ -6,15 +6,13 @@ import android.media.MediaMetadataRetriever;
import android.net.Uri;
import android.text.TextUtils;
+import androidx.annotation.NonNull;
import androidx.documentfile.provider.DocumentFile;
-import org.apache.commons.lang3.StringUtils;
-
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
@@ -34,6 +32,8 @@ import de.danoeh.antennapod.core.util.DownloadError;
public class LocalFeedUpdater {
+ static final String[] PREFERRED_FEED_IMAGE_FILENAMES = { "folder.jpg", "Folder.jpg", "folder.png", "Folder.png" };
+
public static void updateFeed(Feed feed, Context context) {
try {
tryUpdateFeed(feed, context);
@@ -97,18 +97,7 @@ public class LocalFeedUpdater {
}
}
- List<String> iconLocations = Arrays.asList("folder.jpg", "Folder.jpg", "folder.png", "Folder.png");
- for (String iconLocation : iconLocations) {
- DocumentFile image = documentFolder.findFile(iconLocation);
- if (image != null) {
- feed.setImageUrl(image.getUri().toString());
- break;
- }
- }
- if (StringUtils.isBlank(feed.getImageUrl())) {
- // set default feed image
- feed.setImageUrl(getDefaultIconUrl(context));
- }
+ feed.setImageUrl(getImageUrl(context, documentFolder));
feed.getPreferences().setAutoDownload(false);
feed.getPreferences().setAutoDeleteAction(FeedPreferences.AutoDeleteAction.NO);
@@ -123,6 +112,31 @@ public class LocalFeedUpdater {
}
/**
+ * Returns the image URL for the local feed.
+ */
+ @NonNull
+ static String getImageUrl(@NonNull Context context, @NonNull DocumentFile documentFolder) {
+ // look for special file names
+ for (String iconLocation : PREFERRED_FEED_IMAGE_FILENAMES) {
+ DocumentFile image = documentFolder.findFile(iconLocation);
+ if (image != null) {
+ return image.getUri().toString();
+ }
+ }
+
+ // use the first image in the folder if existing
+ for (DocumentFile file : documentFolder.listFiles()) {
+ String mime = file.getType();
+ if (mime != null && (mime.startsWith("image/jpeg") || mime.startsWith("image/png"))) {
+ return file.getUri().toString();
+ }
+ }
+
+ // use default icon as fallback
+ return getDefaultIconUrl(context);
+ }
+
+ /**
* Returns the URL of the default icon for a local feed. The URL refers to an app resource file.
*/
public static String getDefaultIconUrl(Context context) {
@@ -155,13 +169,13 @@ public class LocalFeedUpdater {
try {
loadMetadata(item, file, context);
} catch (Exception e) {
- item.setDescription(e.getMessage());
+ item.setDescriptionIfLonger(e.getMessage());
}
return item;
}
- private static void loadMetadata(FeedItem item, DocumentFile file, Context context) throws Exception {
+ private static void loadMetadata(FeedItem item, DocumentFile file, Context context) {
MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
mediaMetadataRetriever.setDataSource(context, file.getUri());
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/util/ImageResourceUtils.java b/core/src/main/java/de/danoeh/antennapod/core/feed/util/ImageResourceUtils.java
index 674663a6d..b0aee3d77 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/feed/util/ImageResourceUtils.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/util/ImageResourceUtils.java
@@ -1,45 +1,66 @@
package de.danoeh.antennapod.core.feed.util;
-import de.danoeh.antennapod.core.asynctask.ImageResource;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.util.playback.Playable;
/**
- * Utility class to use the appropriate image resource based on {@link UserPreferences}
+ * Utility class to use the appropriate image resource based on {@link UserPreferences}.
*/
public final class ImageResourceUtils {
private ImageResourceUtils() {
}
- public static String getImageLocation(ImageResource resource) {
+ /**
+ * returns the image location, does prefer the episode cover if available and enabled in settings.
+ */
+ @Nullable
+ public static String getEpisodeListImageLocation(@NonNull Playable playable) {
if (UserPreferences.getUseEpisodeCoverSetting()) {
- return resource.getImageLocation();
+ return playable.getImageLocation();
} else {
- return getShowImageLocation(resource);
+ return getFallbackImageLocation(playable);
}
}
- private static String getShowImageLocation(ImageResource resource) {
+ /**
+ * returns the image location, does prefer the episode cover if available and enabled in settings.
+ */
+ @Nullable
+ public static String getEpisodeListImageLocation(@NonNull FeedItem feedItem) {
+ if (UserPreferences.getUseEpisodeCoverSetting()) {
+ return feedItem.getImageLocation();
+ } else {
+ return getFallbackImageLocation(feedItem);
+ }
+ }
- if (resource instanceof FeedItem) {
- FeedItem item = (FeedItem) resource;
- if (item.getFeed() != null) {
- return item.getFeed().getImageLocation();
- } else {
- return null;
- }
- } else if (resource instanceof FeedMedia) {
- FeedMedia media = (FeedMedia) resource;
+ @Nullable
+ public static String getFallbackImageLocation(@NonNull Playable playable) {
+ if (playable instanceof FeedMedia) {
+ FeedMedia media = (FeedMedia) playable;
FeedItem item = media.getItem();
if (item != null && item.getFeed() != null) {
- return item.getFeed().getImageLocation();
+ return item.getFeed().getImageUrl();
} else {
return null;
}
} else {
- return resource.getImageLocation();
+ return playable.getImageLocation();
+ }
+ }
+
+ @Nullable
+ public static String getFallbackImageLocation(@NonNull FeedItem feedItem) {
+ if (feedItem.getFeed() != null) {
+ return feedItem.getFeed().getImageUrl();
+ } else {
+ return null;
}
}
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java
index 0a72b5d5c..209558b19 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java
@@ -26,7 +26,7 @@ public class GpodnetPreferences {
private static String username;
private static String password;
private static String deviceID;
- private static String hostname;
+ private static String hosturl;
private static boolean preferencesLoaded = false;
@@ -40,7 +40,7 @@ public class GpodnetPreferences {
username = prefs.getString(PREF_GPODNET_USERNAME, null);
password = prefs.getString(PREF_GPODNET_PASSWORD, null);
deviceID = prefs.getString(PREF_GPODNET_DEVICEID, null);
- hostname = checkGpodnetHostname(prefs.getString(PREF_GPODNET_HOSTNAME, GpodnetService.DEFAULT_BASE_HOST));
+ hosturl = prefs.getString(PREF_GPODNET_HOSTNAME, GpodnetService.DEFAULT_BASE_HOST);
preferencesLoaded = true;
}
@@ -82,17 +82,16 @@ public class GpodnetPreferences {
writePreference(PREF_GPODNET_DEVICEID, deviceID);
}
- public static String getHostname() {
+ public static String getHosturl() {
ensurePreferencesLoaded();
- return hostname;
+ return hosturl;
}
- public static void setHostname(String value) {
- value = checkGpodnetHostname(value);
- if (!value.equals(hostname)) {
+ public static void setHosturl(String value) {
+ if (!value.equals(hosturl)) {
logout();
writePreference(PREF_GPODNET_HOSTNAME, value);
- hostname = value;
+ hosturl = value;
}
}
@@ -113,13 +112,4 @@ public class GpodnetPreferences {
UserPreferences.setGpodnetNotificationsEnabled();
}
- private static String checkGpodnetHostname(String value) {
- int startIndex = 0;
- if (value.startsWith("http://")) {
- startIndex = "http://".length();
- } else if (value.startsWith("https://")) {
- startIndex = "https://".length();
- }
- return value.substring(startIndex);
- }
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java
index 08ea27434..95b828e28 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java
@@ -100,7 +100,7 @@ public class PlaybackPreferences implements SharedPreferences.OnSharedPreference
}
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
- if (key.equals(PREF_CURRENT_PLAYER_STATUS)) {
+ if (PREF_CURRENT_PLAYER_STATUS.equals(key)) {
EventBus.getDefault().post(new PlayerStatusEvent());
}
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java
index ed9c519a6..cbfe28ded 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java
@@ -6,6 +6,7 @@ import android.content.res.Configuration;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
+import android.view.KeyEvent;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
@@ -35,6 +36,7 @@ import de.danoeh.antennapod.core.feed.MediaType;
import de.danoeh.antennapod.core.feed.SubscriptionsFilter;
import de.danoeh.antennapod.core.service.download.ProxyConfig;
import de.danoeh.antennapod.core.storage.APCleanupAlgorithm;
+import de.danoeh.antennapod.core.storage.ExceptFavoriteCleanupAlgorithm;
import de.danoeh.antennapod.core.storage.APNullCleanupAlgorithm;
import de.danoeh.antennapod.core.storage.APQueueCleanupAlgorithm;
import de.danoeh.antennapod.core.storage.EpisodeCleanupAlgorithm;
@@ -60,6 +62,7 @@ public class UserPreferences {
private static final String PREF_DRAWER_FEED_COUNTER = "prefDrawerFeedIndicator";
public static final String PREF_EXPANDED_NOTIFICATION = "prefExpandNotify";
public static final String PREF_USE_EPISODE_COVER = "prefEpisodeCover";
+ public static final String PREF_SHOW_TIME_LEFT = "showTimeLeft";
private static final String PREF_PERSISTENT_NOTIFICATION = "prefPersistNotify";
public static final String PREF_COMPACT_NOTIFICATION_BUTTONS = "prefCompactNotificationButtons";
public static final String PREF_LOCKSCREEN_BACKGROUND = "prefLockscreenBackground";
@@ -76,8 +79,8 @@ public class UserPreferences {
public static final String PREF_PAUSE_ON_HEADSET_DISCONNECT = "prefPauseOnHeadsetDisconnect";
public static final String PREF_UNPAUSE_ON_HEADSET_RECONNECT = "prefUnpauseOnHeadsetReconnect";
private static final String PREF_UNPAUSE_ON_BLUETOOTH_RECONNECT = "prefUnpauseOnBluetoothReconnect";
- private static final String PREF_HARDWARE_FOWARD_BUTTON_SKIPS = "prefHardwareForwardButtonSkips";
- private static final String PREF_HARDWARE_PREVIOUS_BUTTON_RESTARTS = "prefHardwarePreviousButtonRestarts";
+ public static final String PREF_HARDWARE_FORWARD_BUTTON = "prefHardwareForwardButton";
+ public static final String PREF_HARDWARE_PREVIOUS_BUTTON = "prefHardwarePreviousButton";
public static final String PREF_FOLLOW_QUEUE = "prefFollowQueue";
public static final String PREF_SKIP_KEEPS_EPISODE = "prefSkipKeepsEpisode";
private static final String PREF_FAVORITE_KEEPS_EPISODE = "prefFavoriteKeepsEpisode";
@@ -136,6 +139,7 @@ public class UserPreferences {
public static final String PREF_CAST_ENABLED = "prefCast"; //Used for enabling Chromecast support
public static final int EPISODE_CLEANUP_QUEUE = -1;
public static final int EPISODE_CLEANUP_NULL = -2;
+ public static final int EPISODE_CLEANUP_EXCEPT_FAVORITE = -3;
public static final int EPISODE_CLEANUP_DEFAULT = 0;
// Constants
@@ -265,6 +269,23 @@ public class UserPreferences {
}
/**
+ * @return {@code true} if we should show remaining time or the duration
+ */
+ public static boolean shouldShowRemainingTime() {
+ return prefs.getBoolean(PREF_SHOW_TIME_LEFT, false);
+ }
+
+ /**
+ * Sets the preference for whether we show the remain time, if not show the duration. This will
+ * send out events so the current playing screen, queue and the episode list would refresh
+ *
+ * @return {@code true} if we should show remaining time or the duration
+ */
+ public static void setShowRemainTimeSetting(Boolean showRemain) {
+ prefs.edit().putBoolean(PREF_SHOW_TIME_LEFT, showRemain).apply();
+ }
+
+ /**
* Returns notification priority.
*
* @return NotificationCompat.PRIORITY_MAX or NotificationCompat.PRIORITY_DEFAULT
@@ -373,12 +394,14 @@ public class UserPreferences {
return prefs.getBoolean(PREF_UNPAUSE_ON_BLUETOOTH_RECONNECT, false);
}
- public static boolean shouldHardwareButtonSkip() {
- return prefs.getBoolean(PREF_HARDWARE_FOWARD_BUTTON_SKIPS, false);
+ public static int getHardwareForwardButton() {
+ return Integer.parseInt(prefs.getString(PREF_HARDWARE_FORWARD_BUTTON,
+ String.valueOf(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD)));
}
- public static boolean shouldHardwarePreviousButtonRestart() {
- return prefs.getBoolean(PREF_HARDWARE_PREVIOUS_BUTTON_RESTARTS, false);
+ public static int getHardwarePreviousButton() {
+ return Integer.parseInt(prefs.getString(PREF_HARDWARE_PREVIOUS_BUTTON,
+ String.valueOf(KeyEvent.KEYCODE_MEDIA_REWIND)));
}
@@ -879,7 +902,9 @@ public class UserPreferences {
return new APNullCleanupAlgorithm();
}
int cleanupValue = getEpisodeCleanupValue();
- if (cleanupValue == EPISODE_CLEANUP_QUEUE) {
+ if (cleanupValue == EPISODE_CLEANUP_EXCEPT_FAVORITE) {
+ return new ExceptFavoriteCleanupAlgorithm();
+ } else if (cleanupValue == EPISODE_CLEANUP_QUEUE) {
return new APQueueCleanupAlgorithm();
} else if (cleanupValue == EPISODE_CLEANUP_NULL) {
return new APNullCleanupAlgorithm();
diff --git a/core/src/main/java/de/danoeh/antennapod/core/receiver/PlayerWidget.java b/core/src/main/java/de/danoeh/antennapod/core/receiver/PlayerWidget.java
index 2e592bdf5..cf0debed2 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/receiver/PlayerWidget.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/receiver/PlayerWidget.java
@@ -9,20 +9,23 @@ import android.util.Log;
import java.util.Arrays;
-import de.danoeh.antennapod.core.service.PlayerWidgetJobService;
+import de.danoeh.antennapod.core.widget.WidgetUpdaterJobService;
public class PlayerWidget extends AppWidgetProvider {
private static final String TAG = "PlayerWidget";
public static final String PREFS_NAME = "PlayerWidgetPrefs";
private static final String KEY_ENABLED = "WidgetEnabled";
public static final String KEY_WIDGET_COLOR = "widget_color";
+ public static final String KEY_WIDGET_SKIP = "widget_skip";
+ public static final String KEY_WIDGET_FAST_FORWARD = "widget_fast_forward";
+ public static final String KEY_WIDGET_REWIND = "widget_rewind";
public static final int DEFAULT_COLOR = 0x00262C31;
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "onReceive");
super.onReceive(context, intent);
- PlayerWidgetJobService.updateWidget(context);
+ WidgetUpdaterJobService.performBackgroundUpdate(context);
}
@Override
@@ -30,13 +33,14 @@ public class PlayerWidget extends AppWidgetProvider {
super.onEnabled(context);
Log.d(TAG, "Widget enabled");
setEnabled(context, true);
- PlayerWidgetJobService.updateWidget(context);
+ WidgetUpdaterJobService.performBackgroundUpdate(context);
}
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
- Log.d(TAG, "onUpdate() called with: " + "context = [" + context + "], appWidgetManager = [" + appWidgetManager + "], appWidgetIds = [" + Arrays.toString(appWidgetIds) + "]");
- PlayerWidgetJobService.updateWidget(context);
+ Log.d(TAG, "onUpdate() called with: " + "context = [" + context + "], appWidgetManager = ["
+ + appWidgetManager + "], appWidgetIds = [" + Arrays.toString(appWidgetIds) + "]");
+ WidgetUpdaterJobService.performBackgroundUpdate(context);
}
@Override
@@ -52,6 +56,9 @@ public class PlayerWidget extends AppWidgetProvider {
for (int appWidgetId : appWidgetIds) {
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
prefs.edit().remove(KEY_WIDGET_COLOR + appWidgetId).apply();
+ prefs.edit().remove(KEY_WIDGET_REWIND + appWidgetId).apply();
+ prefs.edit().remove(KEY_WIDGET_FAST_FORWARD + appWidgetId).apply();
+ prefs.edit().remove(KEY_WIDGET_SKIP + appWidgetId).apply();
}
super.onDeleted(context, appWidgetIds);
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/PlayerWidgetJobService.java b/core/src/main/java/de/danoeh/antennapod/core/service/PlayerWidgetJobService.java
deleted file mode 100644
index 74735a264..000000000
--- a/core/src/main/java/de/danoeh/antennapod/core/service/PlayerWidgetJobService.java
+++ /dev/null
@@ -1,243 +0,0 @@
-package de.danoeh.antennapod.core.service;
-
-import android.app.PendingIntent;
-import android.appwidget.AppWidgetManager;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.ServiceConnection;
-import android.content.SharedPreferences;
-import android.graphics.Bitmap;
-import android.os.Bundle;
-import android.os.IBinder;
-import androidx.annotation.NonNull;
-import androidx.core.app.SafeJobIntentService;
-import android.util.Log;
-import android.view.KeyEvent;
-import android.view.View;
-import android.widget.RemoteViews;
-
-import com.bumptech.glide.Glide;
-import com.bumptech.glide.request.RequestOptions;
-
-import java.util.concurrent.TimeUnit;
-
-import de.danoeh.antennapod.core.R;
-import de.danoeh.antennapod.core.glide.ApGlideSettings;
-import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils;
-import de.danoeh.antennapod.core.receiver.MediaButtonReceiver;
-import de.danoeh.antennapod.core.receiver.PlayerWidget;
-import de.danoeh.antennapod.core.service.playback.PlaybackService;
-import de.danoeh.antennapod.core.service.playback.PlayerStatus;
-import de.danoeh.antennapod.core.util.Converter;
-import de.danoeh.antennapod.core.feed.util.ImageResourceUtils;
-import de.danoeh.antennapod.core.util.TimeSpeedConverter;
-import de.danoeh.antennapod.core.util.playback.Playable;
-
-/**
- * Updates the state of the player widget
- */
-public class PlayerWidgetJobService extends SafeJobIntentService {
-
- private static final String TAG = "PlayerWidgetJobService";
-
- private PlaybackService playbackService;
- private final Object waitForService = new Object();
- private final Object waitUsingService = new Object();
-
- private static final int JOB_ID = -17001;
-
- public static void updateWidget(Context context) {
- enqueueWork(context, PlayerWidgetJobService.class, JOB_ID, new Intent(context, PlayerWidgetJobService.class));
- }
-
- @Override
- protected void onHandleWork(@NonNull Intent intent) {
- if (!PlayerWidget.isEnabled(getApplicationContext())) {
- return;
- }
-
- synchronized (waitForService) {
- if (PlaybackService.isRunning && playbackService == null) {
- bindService(new Intent(this, PlaybackService.class), mConnection, 0);
- while (playbackService == null) {
- try {
- waitForService.wait();
- } catch (InterruptedException e) {
- return;
- }
- }
- }
- }
-
- synchronized (waitUsingService) {
- updateViews();
- }
-
- if (playbackService != null) {
- try {
- unbindService(mConnection);
- } catch (IllegalArgumentException e) {
- Log.w(TAG, "IllegalArgumentException when trying to unbind service");
- }
- }
- }
-
- /**
- * Returns number of cells needed for given size of the widget.
- *
- * @param size Widget size in dp.
- * @return Size in number of cells.
- */
- private static int getCellsForSize(int size) {
- int n = 2;
- while (70 * n - 30 < size) {
- ++n;
- }
- return n - 1;
- }
-
- private void updateViews() {
-
- ComponentName playerWidget = new ComponentName(this, PlayerWidget.class);
- AppWidgetManager manager = AppWidgetManager.getInstance(this);
- int[] widgetIds = manager.getAppWidgetIds(playerWidget);
- RemoteViews views = new RemoteViews(getPackageName(), R.layout.player_widget);
- final PendingIntent startMediaPlayer = PendingIntent.getActivity(this, R.id.pending_intent_player_activity,
- PlaybackService.getPlayerActivityIntent(this), PendingIntent.FLAG_UPDATE_CURRENT);
-
- boolean nothingPlaying = false;
- Playable media;
- PlayerStatus status;
- if (playbackService != null) {
- media = playbackService.getPlayable();
- status = playbackService.getStatus();
- } else {
- media = Playable.PlayableUtils.createInstanceFromPreferences(getApplicationContext());
- status = PlayerStatus.STOPPED;
- }
-
- if (media != null) {
- views.setOnClickPendingIntent(R.id.layout_left, startMediaPlayer);
-
- try {
- Bitmap icon;
- int iconSize = getResources().getDimensionPixelSize(android.R.dimen.app_icon_size);
- icon = Glide.with(PlayerWidgetJobService.this)
- .asBitmap()
- .load(ImageResourceUtils.getImageLocation(media))
- .apply(RequestOptions.diskCacheStrategyOf(ApGlideSettings.AP_DISK_CACHE_STRATEGY))
- .submit(iconSize, iconSize)
- .get(500, TimeUnit.MILLISECONDS);
- views.setImageViewBitmap(R.id.imgvCover, icon);
- } catch (Throwable tr) {
- Log.e(TAG, "Error loading the media icon for the widget", tr);
- views.setImageViewResource(R.id.imgvCover, R.mipmap.ic_launcher_round);
- }
-
- views.setTextViewText(R.id.txtvTitle, media.getEpisodeTitle());
- views.setViewVisibility(R.id.txtvTitle, View.VISIBLE);
- views.setViewVisibility(R.id.txtNoPlaying, View.GONE);
-
- String progressString;
- if (playbackService != null) {
- progressString = getProgressString(playbackService.getCurrentPosition(),
- playbackService.getDuration(), playbackService.getCurrentPlaybackSpeed());
- } else {
- progressString = getProgressString(media.getPosition(), media.getDuration(), PlaybackSpeedUtils.getCurrentPlaybackSpeed(media));
- }
-
- if (progressString != null) {
- views.setViewVisibility(R.id.txtvProgress, View.VISIBLE);
- views.setTextViewText(R.id.txtvProgress, progressString);
- }
-
- if (status == PlayerStatus.PLAYING) {
- views.setImageViewResource(R.id.butPlay, R.drawable.ic_av_pause_white_48dp);
- views.setContentDescription(R.id.butPlay, getString(R.string.pause_label));
- } else {
- views.setImageViewResource(R.id.butPlay, R.drawable.ic_av_play_white_48dp);
- views.setContentDescription(R.id.butPlay, getString(R.string.play_label));
- }
- views.setOnClickPendingIntent(R.id.butPlay, createMediaButtonIntent());
- } else {
- nothingPlaying = true;
- }
-
- if (nothingPlaying) {
- // start the app if they click anything
- views.setOnClickPendingIntent(R.id.layout_left, startMediaPlayer);
- views.setOnClickPendingIntent(R.id.butPlay, startMediaPlayer);
- views.setViewVisibility(R.id.txtvProgress, View.GONE);
- views.setViewVisibility(R.id.txtvTitle, View.GONE);
- views.setViewVisibility(R.id.txtNoPlaying, View.VISIBLE);
- views.setImageViewResource(R.id.imgvCover, R.mipmap.ic_launcher_round);
- views.setImageViewResource(R.id.butPlay, R.drawable.ic_av_play_white_48dp);
- }
-
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
- for (int id : widgetIds) {
- Bundle options = manager.getAppWidgetOptions(id);
- int minWidth = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH);
- int columns = getCellsForSize(minWidth);
- if (columns < 3) {
- views.setViewVisibility(R.id.layout_center, View.INVISIBLE);
- } else {
- views.setViewVisibility(R.id.layout_center, View.VISIBLE);
- }
-
- SharedPreferences prefs = getSharedPreferences(PlayerWidget.PREFS_NAME, Context.MODE_PRIVATE);
- int backgroundColor = prefs.getInt(PlayerWidget.KEY_WIDGET_COLOR + id, PlayerWidget.DEFAULT_COLOR);
- views.setInt(R.id.widgetLayout, "setBackgroundColor", backgroundColor);
-
- manager.updateAppWidget(id, views);
- }
- } else {
- manager.updateAppWidget(playerWidget, views);
- }
- }
-
- /**
- * Creates an intent which fakes a mediabutton press
- */
- private PendingIntent createMediaButtonIntent() {
- KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE);
- Intent startingIntent = new Intent(getBaseContext(), MediaButtonReceiver.class);
- startingIntent.setAction(MediaButtonReceiver.NOTIFY_BUTTON_RECEIVER);
- startingIntent.putExtra(Intent.EXTRA_KEY_EVENT, event);
-
- return PendingIntent.getBroadcast(this, 0, startingIntent, 0);
- }
-
- private String getProgressString(int position, int duration, float speed) {
- if (position >= 0 && duration > 0) {
- TimeSpeedConverter converter = new TimeSpeedConverter(speed);
- position = converter.convert(position);
- duration = converter.convert(duration);
- return Converter.getDurationStringLong(position) + " / "
- + Converter.getDurationStringLong(duration);
- } else {
- return null;
- }
- }
-
- private final ServiceConnection mConnection = new ServiceConnection() {
- public void onServiceConnected(ComponentName className, IBinder service) {
- Log.d(TAG, "Connection to service established");
- if (service instanceof PlaybackService.LocalBinder) {
- synchronized (waitForService) {
- playbackService = ((PlaybackService.LocalBinder) service).getService();
- waitForService.notifyAll();
- }
- }
- }
-
- @Override
- public void onServiceDisconnected(ComponentName name) {
- synchronized (waitUsingService) {
- playbackService = null;
- }
- Log.d(TAG, "Disconnected from service");
- }
- };
-}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java
index a01b3cb52..c4029d57f 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java
@@ -1,19 +1,14 @@
package de.danoeh.antennapod.core.service.download;
-import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.BasicAuthorizationInterceptor;
import de.danoeh.antennapod.core.service.UserAgentInterceptor;
-import de.danoeh.antennapod.core.ssl.BackportTrustManager;
-import de.danoeh.antennapod.core.ssl.NoV1SslSocketFactory;
import de.danoeh.antennapod.core.storage.DBWriter;
-import de.danoeh.antennapod.core.util.Flavors;
+import de.danoeh.antennapod.net.ssl.SslClientSetup;
import okhttp3.Cache;
-import okhttp3.CipherSuite;
-import okhttp3.ConnectionSpec;
import okhttp3.Credentials;
import okhttp3.HttpUrl;
import okhttp3.JavaNetCookieJar;
@@ -21,8 +16,6 @@ import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.internal.http.StatusLine;
-
-import javax.net.ssl.X509TrustManager;
import java.io.File;
import java.net.CookieManager;
import java.net.CookiePolicy;
@@ -30,9 +23,6 @@ import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.SocketAddress;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
import java.util.concurrent.TimeUnit;
/**
@@ -140,28 +130,7 @@ public class AntennapodHttpClient {
}
}
- if (Flavors.FLAVOR == Flavors.FREE) {
- // The Free flavor bundles a modern conscrypt (security provider), so CustomSslSocketFactory
- // is only used to make sure that modern protocols (TLSv1.3 and TLSv1.2) are enabled and
- // that old, deprecated, protocols (like SSLv3, TLSv1.0 and TLSv1.1) are disabled.
- X509TrustManager trustManager = BackportTrustManager.create();
- builder.sslSocketFactory(new NoV1SslSocketFactory(trustManager), trustManager);
- } else if (Build.VERSION.SDK_INT < 21) {
- X509TrustManager trustManager = BackportTrustManager.create();
- builder.sslSocketFactory(new NoV1SslSocketFactory(trustManager), trustManager);
-
- // workaround for Android 4.x for certain web sites.
- // see: https://github.com/square/okhttp/issues/4053#issuecomment-402579554
- List<CipherSuite> cipherSuites = new ArrayList<>(ConnectionSpec.MODERN_TLS.cipherSuites());
- cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA);
- cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA);
-
- ConnectionSpec legacyTls = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
- .cipherSuites(cipherSuites.toArray(new CipherSuite[0]))
- .build();
- builder.connectionSpecs(Arrays.asList(legacyTls, ConnectionSpec.CLEARTEXT));
- }
-
+ SslClientSetup.installCertificates(builder);
return builder;
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java
index 5a2c653d6..2e0cb705b 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java
@@ -25,7 +25,6 @@ import org.greenrobot.eventbus.EventBus;
import java.io.File;
import java.io.IOException;
-import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -96,6 +95,7 @@ public class DownloadService extends Service {
private final CompletionService<Downloader> downloadExecutor;
private final DownloadRequester requester;
private DownloadServiceNotification notificationManager;
+ private final NewEpisodesNotification newEpisodesNotification;
/**
* Currently running downloads.
@@ -118,7 +118,7 @@ public class DownloadService extends Service {
private ScheduledFuture<?> notificationUpdaterFuture;
private ScheduledFuture<?> downloadPostFuture;
private static final int SCHED_EX_POOL_SIZE = 1;
- private ScheduledThreadPoolExecutor schedExecutor;
+ private final ScheduledThreadPoolExecutor schedExecutor;
private static DownloaderFactory downloaderFactory = new DefaultDownloaderFactory();
private final IBinder mBinder = new LocalBinder();
@@ -134,12 +134,16 @@ public class DownloadService extends Service {
downloads = Collections.synchronizedList(new ArrayList<>());
numberOfDownloads = new AtomicInteger(0);
requester = DownloadRequester.getInstance();
+ newEpisodesNotification = new NewEpisodesNotification();
syncExecutor = Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r, "SyncThread");
t.setPriority(Thread.MIN_PRIORITY);
return t;
});
+ // Must be the first runnable in syncExecutor
+ syncExecutor.execute(newEpisodesNotification::loadCountersBeforeRefresh);
+
Log.d(TAG, "parallel downloads: " + UserPreferences.getParallelDownloads());
downloadExecutor = new ExecutorCompletionService<>(
Executors.newFixedThreadPool(UserPreferences.getParallelDownloads(),
@@ -165,10 +169,10 @@ public class DownloadService extends Service {
Notification notification = notificationManager.updateNotifications(
requester.getNumberOfDownloads(), downloads);
startForeground(R.id.notification_downloading, notification);
+ setupNotificationUpdaterIfNecessary();
syncExecutor.execute(() -> onDownloadQueued(intent));
} else if (numberOfDownloads.get() == 0) {
- stopForeground(true);
- stopSelf();
+ shutdown();
} else {
Log.d(TAG, "onStartCommand: Unknown intent");
}
@@ -188,10 +192,6 @@ public class DownloadService extends Service {
registerReceiver(cancelDownloadReceiver, cancelDownloadReceiverFilter);
downloadCompletionThread.start();
-
- Notification notification = notificationManager.updateNotifications(
- requester.getNumberOfDownloads(), downloads);
- startForeground(R.id.notification_downloading, notification);
}
@Override
@@ -226,10 +226,6 @@ public class DownloadService extends Service {
}
unregisterReceiver(cancelDownloadReceiver);
- stopForeground(true);
- NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
- nm.cancel(R.id.notification_downloading);
-
// if this was the initial gpodder sync, i.e. we just synced the feeds successfully,
// it is now time to sync the episode actions
SyncService.sync(this);
@@ -254,13 +250,13 @@ public class DownloadService extends Service {
handleSuccessfulDownload(downloader);
removeDownload(downloader);
numberOfDownloads.decrementAndGet();
- queryDownloadsAsync();
+ stopServiceIfEverythingDoneAsync();
});
} else {
handleFailedDownload(downloader);
removeDownload(downloader);
numberOfDownloads.decrementAndGet();
- queryDownloadsAsync();
+ stopServiceIfEverythingDoneAsync();
}
} catch (InterruptedException e) {
Log.e(TAG, "DownloadCompletionThread was interrupted");
@@ -290,6 +286,10 @@ public class DownloadService extends Service {
if (log.size() > 0 && !log.get(0).isSuccessful()) {
saveDownloadStatus(task.getDownloadStatus());
}
+ if (request.getFeedfileId() != 0 && !request.isInitiatedByUser()) {
+ // Was stored in the database before and not initiated manually
+ newEpisodesNotification.showIfNeeded(DownloadService.this, task.getSavedFeed());
+ }
} else {
DBWriter.setFeedLastUpdateFailed(request.getFeedfileId(), true);
saveDownloadStatus(task.getDownloadStatus());
@@ -325,18 +325,11 @@ public class DownloadService extends Service {
if (item == null) {
return;
}
- boolean httpNotFound = status.getReason() == DownloadError.ERROR_HTTP_DATA_ERROR
- && String.valueOf(HttpURLConnection.HTTP_NOT_FOUND).equals(status.getReasonDetailed());
- boolean forbidden = status.getReason() == DownloadError.ERROR_FORBIDDEN
- && String.valueOf(HttpURLConnection.HTTP_FORBIDDEN).equals(status.getReasonDetailed());
- boolean notEnoughSpace = status.getReason() == DownloadError.ERROR_NOT_ENOUGH_SPACE;
- boolean wrongFileType = status.getReason() == DownloadError.ERROR_FILE_TYPE;
- boolean httpGone = status.getReason() == DownloadError.ERROR_HTTP_DATA_ERROR
- && String.valueOf(HttpURLConnection.HTTP_GONE).equals(status.getReasonDetailed());
- boolean httpBadReq = status.getReason() == DownloadError.ERROR_HTTP_DATA_ERROR
- && String.valueOf(HttpURLConnection.HTTP_BAD_REQUEST).equals(status.getReasonDetailed());
-
- if (httpNotFound || forbidden || notEnoughSpace || wrongFileType || httpGone || httpBadReq ) {
+ boolean unknownHost = status.getReason() == DownloadError.ERROR_UNKNOWN_HOST;
+ boolean unsupportedType = status.getReason() == DownloadError.ERROR_UNSUPPORTED_TYPE;
+ boolean wrongSize = status.getReason() == DownloadError.ERROR_IO_WRONG_SIZE;
+
+ if (! (unknownHost || unsupportedType || wrongSize)) {
try {
DBWriter.saveFeedItemAutoDownloadFailed(item).get();
} catch (ExecutionException | InterruptedException e) {
@@ -412,7 +405,7 @@ public class DownloadService extends Service {
}
postDownloaders();
}
- queryDownloads();
+ stopServiceIfEverythingDone();
}
};
@@ -483,7 +476,7 @@ public class DownloadService extends Service {
postDownloaders();
});
}
- handler.post(this::queryDownloads);
+ handler.post(this::stopServiceIfEverythingDone);
}
private static boolean isEnqueued(@NonNull DownloadRequest request,
@@ -540,30 +533,19 @@ public class DownloadService extends Service {
* Calls query downloads on the services main thread. This method should be used instead of queryDownloads if it is
* used from a thread other than the main thread.
*/
- private void queryDownloadsAsync() {
- handler.post(DownloadService.this::queryDownloads);
+ private void stopServiceIfEverythingDoneAsync() {
+ handler.post(DownloadService.this::stopServiceIfEverythingDone);
}
/**
* Check if there's something else to download, otherwise stop.
*/
- private void queryDownloads() {
+ private void stopServiceIfEverythingDone() {
Log.d(TAG, numberOfDownloads.get() + " downloads left");
if (numberOfDownloads.get() <= 0 && DownloadRequester.getInstance().hasNoDownloads()) {
- Log.d(TAG, "Number of downloads is " + numberOfDownloads.get() + ", attempting shutdown");
- stopForeground(true);
- stopSelf();
- if (notificationUpdater != null) {
- notificationUpdater.run();
- } else {
- Log.d(TAG, "Skipping notification update");
- }
- } else {
- setupNotificationUpdater();
- Notification notification = notificationManager.updateNotifications(
- requester.getNumberOfDownloads(), downloads);
- startForeground(R.id.notification_downloading, notification);
+ Log.d(TAG, "Attempting shutdown");
+ shutdown();
}
}
@@ -616,7 +598,7 @@ public class DownloadService extends Service {
/**
* Schedules the notification updater task if it hasn't been scheduled yet.
*/
- private void setupNotificationUpdater() {
+ private void setupNotificationUpdaterIfNecessary() {
if (notificationUpdater == null) {
Log.d(TAG, "Setting up notification updater");
notificationUpdater = new NotificationUpdater();
@@ -653,4 +635,16 @@ public class DownloadService extends Service {
new PostDownloaderTask(downloads), 1, 1, TimeUnit.SECONDS);
}
}
+
+ private void shutdown() {
+ // If the service was run for a very short time, the system may delay closing
+ // the notification. Set the notification text now so that a misleading message
+ // is not left on the notification.
+ if (notificationUpdater != null) {
+ notificationUpdater.run();
+ }
+ cancelNotificationUpdater();
+ stopForeground(true);
+ stopSelf();
+ }
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java
index fb6009c02..7b7879409 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java
@@ -28,7 +28,10 @@ public class DownloadServiceNotification {
private void setupNotificationBuilders() {
notificationCompatBuilder = new NotificationCompat.Builder(context, NotificationUtils.CHANNEL_ID_DOWNLOADING)
- .setOngoing(true)
+ .setOngoing(false)
+ .setWhen(0)
+ .setOnlyAlertOnce(true)
+ .setShowWhen(false)
.setContentIntent(ClientConfig.downloadServiceCallbacks.getNotificationContentIntent(context))
.setSmallIcon(R.drawable.ic_notification_sync);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
@@ -50,7 +53,7 @@ public class DownloadServiceNotification {
String contentTitle = context.getString(R.string.download_notification_title);
String downloadsLeft = (numDownloads > 0)
? context.getResources().getQuantityString(R.plurals.downloads_left, numDownloads, numDownloads)
- : context.getString(R.string.downloads_processing);
+ : context.getString(R.string.service_shutting_down);
String bigText = compileNotificationString(downloads);
notificationCompatBuilder.setContentTitle(contentTitle);
@@ -106,6 +109,23 @@ public class DownloadServiceNotification {
return sb.toString();
}
+ private String createFailedDownloadNotificationContent(List<DownloadStatus> statuses) {
+ StringBuilder sb = new StringBuilder();
+
+ for (int i = 0; i < statuses.size(); i++) {
+ if (statuses.get(i).isSuccessful()) {
+ continue;
+ }
+ sb.append("• ").append(statuses.get(i).getTitle());
+ sb.append(": ").append(statuses.get(i).getReason().getErrorString(context));
+ if (i != statuses.size() - 1) {
+ sb.append("\n");
+ }
+ }
+
+ return sb.toString();
+ }
+
/**
* Creates a notification at the end of the service lifecycle to notify the
* user about the number of completed downloads. A report will only be
@@ -143,7 +163,7 @@ public class DownloadServiceNotification {
// We are generating an auto-download report
channelId = NotificationUtils.CHANNEL_ID_AUTO_DOWNLOAD;
titleId = R.string.auto_download_report_title;
- iconId = R.drawable.ic_notification_auto_download_complete;
+ iconId = R.drawable.ic_notification_new;
intent = ClientConfig.downloadServiceCallbacks.getAutoDownloadReportNotificationContentIntent(context);
id = R.id.notification_auto_download_report;
content = createAutoDownloadNotificationContent(reportQueue);
@@ -153,11 +173,7 @@ public class DownloadServiceNotification {
iconId = R.drawable.ic_notification_sync_error;
intent = ClientConfig.downloadServiceCallbacks.getReportNotificationContentIntent(context);
id = R.id.notification_download_report;
- content = context.getResources()
- .getQuantityString(R.plurals.download_report_content,
- successfulDownloads,
- successfulDownloads,
- failedDownloads);
+ content = createFailedDownloadNotificationContent(reportQueue);
}
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId);
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java
index 393592cf9..2d955859f 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java
@@ -19,6 +19,8 @@ import java.net.URI;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.Date;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.core.feed.FeedMedia;
@@ -37,6 +39,7 @@ public class HttpDownloader extends Downloader {
private static final String TAG = "HttpDownloader";
private static final int BUFFER_SIZE = 8 * 1024;
+ private static final String REGEX_PATTERN_IP_ADDRESS = "([0-9]{1,3}[\\.]){3}[0-9]{1,3}";
public HttpDownloader(@NonNull DownloadRequest request) {
super(request);
@@ -134,6 +137,9 @@ public class HttpDownloader extends Downloader {
} else if (response.code() == HttpURLConnection.HTTP_FORBIDDEN) {
error = DownloadError.ERROR_FORBIDDEN;
details = String.valueOf(response.code());
+ } else if (response.code() == HttpURLConnection.HTTP_NOT_FOUND) {
+ error = DownloadError.ERROR_NOT_FOUND;
+ details = String.valueOf(response.code());
} else {
error = DownloadError.ERROR_HTTP_DATA_ERROR;
details = String.valueOf(response.code());
@@ -223,7 +229,7 @@ public class HttpDownloader extends Downloader {
// written file. This check cannot be made if compression was used
if (!isGzip && request.getSize() != DownloadStatus.SIZE_UNKNOWN &&
request.getSoFar() != request.getSize()) {
- onFail(DownloadError.ERROR_IO_ERROR, "Download completed but size: " +
+ onFail(DownloadError.ERROR_IO_WRONG_SIZE, "Download completed but size: " +
request.getSoFar() + " does not equal expected size " + request.getSize());
return;
} else if (request.getSize() > 0 && request.getSoFar() == 0) {
@@ -250,6 +256,22 @@ public class HttpDownloader extends Downloader {
onFail(DownloadError.ERROR_UNKNOWN_HOST, e.getMessage());
} catch (IOException e) {
e.printStackTrace();
+ String message = e.getMessage();
+ if (message != null) {
+ // Try to parse message for a more detailed error message
+ Pattern pattern = Pattern.compile(REGEX_PATTERN_IP_ADDRESS);
+ Matcher matcher = pattern.matcher(message);
+ if (matcher.find()) {
+ String ip = matcher.group();
+ if (ip.startsWith("127.") || ip.startsWith("0.")) {
+ onFail(DownloadError.ERROR_IO_BLOCKED, e.getMessage());
+ return;
+ }
+ } else if (message.contains("Trust anchor for certification path not found")) {
+ onFail(DownloadError.ERROR_CERTIFICATE, e.getMessage());
+ return;
+ }
+ }
onFail(DownloadError.ERROR_IO_ERROR, e.getMessage());
} catch (NullPointerException e) {
// might be thrown by connection.getInputStream()
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/NewEpisodesNotification.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/NewEpisodesNotification.java
new file mode 100644
index 000000000..799a68037
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/NewEpisodesNotification.java
@@ -0,0 +1,132 @@
+package de.danoeh.antennapod.core.service.download;
+
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+
+import android.graphics.Bitmap;
+import android.util.Log;
+import androidx.core.app.NotificationCompat;
+import androidx.core.app.NotificationManagerCompat;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.request.RequestOptions;
+import de.danoeh.antennapod.core.R;
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.feed.FeedPreferences;
+import de.danoeh.antennapod.core.glide.ApGlideSettings;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.storage.PodDBAdapter;
+import de.danoeh.antennapod.core.util.LongIntMap;
+import de.danoeh.antennapod.core.util.gui.NotificationUtils;
+
+public class NewEpisodesNotification {
+ private static final String TAG = "NewEpisodesNotification";
+ private static final String GROUP_KEY = "de.danoeh.antennapod.EPISODES";
+
+ private LongIntMap countersBefore;
+
+ public NewEpisodesNotification() {
+ }
+
+ public void loadCountersBeforeRefresh() {
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ countersBefore = adapter.getFeedCounters(UserPreferences.FEED_COUNTER_SHOW_NEW);
+ adapter.close();
+ }
+
+ public void showIfNeeded(Context context, Feed feed) {
+ FeedPreferences prefs = feed.getPreferences();
+ if (!prefs.getKeepUpdated() || !prefs.getShowEpisodeNotification()) {
+ return;
+ }
+
+ int newEpisodesBefore = countersBefore.get(feed.getId());
+ int newEpisodesAfter = getNewEpisodeCount(feed.getId());
+
+ Log.d(TAG, "New episodes before: " + newEpisodesBefore + ", after: " + newEpisodesAfter);
+ if (newEpisodesAfter > newEpisodesBefore) {
+ NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
+ showNotification(newEpisodesAfter, feed, context, notificationManager);
+ }
+ }
+
+ private static void showNotification(int newEpisodes, Feed feed, Context context,
+ NotificationManagerCompat notificationManager) {
+ Resources res = context.getResources();
+ String text = res.getQuantityString(
+ R.plurals.new_episode_notification_message, newEpisodes, newEpisodes, feed.getTitle()
+ );
+ String title = res.getQuantityString(R.plurals.new_episode_notification_title, newEpisodes);
+
+ Intent intent = new Intent();
+ intent.setAction("NewEpisodes" + feed.getId());
+ intent.setComponent(new ComponentName(context, "de.danoeh.antennapod.activity.MainActivity"));
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ intent.putExtra("fragment_feed_id", feed.getId());
+ PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
+
+ Notification notification = new NotificationCompat.Builder(
+ context, NotificationUtils.CHANNEL_ID_EPISODE_NOTIFICATIONS)
+ .setSmallIcon(R.drawable.ic_notification_new)
+ .setContentTitle(title)
+ .setLargeIcon(loadIcon(context, feed))
+ .setContentText(text)
+ .setContentIntent(pendingIntent)
+ .setGroup(GROUP_KEY)
+ .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
+ .setAutoCancel(true)
+ .build();
+
+ notificationManager.notify(NotificationUtils.CHANNEL_ID_EPISODE_NOTIFICATIONS, feed.hashCode(), notification);
+ showGroupSummaryNotification(context, notificationManager);
+ }
+
+ private static void showGroupSummaryNotification(Context context, NotificationManagerCompat notificationManager) {
+ Intent intent = new Intent();
+ intent.setAction("NewEpisodes");
+ intent.setComponent(new ComponentName(context, "de.danoeh.antennapod.activity.MainActivity"));
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ intent.putExtra("fragment_tag", "EpisodesFragment");
+ PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
+
+ Notification notificationGroupSummary = new NotificationCompat.Builder(
+ context, NotificationUtils.CHANNEL_ID_EPISODE_NOTIFICATIONS)
+ .setSmallIcon(R.drawable.ic_notification_new)
+ .setContentTitle(context.getString(R.string.new_episode_notification_group_text))
+ .setContentIntent(pendingIntent)
+ .setGroup(GROUP_KEY)
+ .setGroupSummary(true)
+ .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
+ .setAutoCancel(true)
+ .build();
+ notificationManager.notify(NotificationUtils.CHANNEL_ID_EPISODE_NOTIFICATIONS, 0, notificationGroupSummary);
+ }
+
+ private static Bitmap loadIcon(Context context, Feed feed) {
+ int iconSize = (int) (128 * context.getResources().getDisplayMetrics().density);
+ try {
+ return Glide.with(context)
+ .asBitmap()
+ .load(feed.getImageUrl())
+ .apply(RequestOptions.diskCacheStrategyOf(ApGlideSettings.AP_DISK_CACHE_STRATEGY))
+ .apply(new RequestOptions().centerCrop())
+ .submit(iconSize, iconSize)
+ .get();
+ } catch (Throwable tr) {
+ return null;
+ }
+ }
+
+ private static int getNewEpisodeCount(long feedId) {
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ int episodeCount = adapter.getFeedCounters(UserPreferences.FEED_COUNTER_SHOW_NEW, feedId).get(feedId);
+ adapter.close();
+ return episodeCount;
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java
index 18c5fce27..d07018f13 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java
@@ -58,6 +58,9 @@ public class FeedParserTask implements Callable<FeedHandlerResult> {
e.printStackTrace();
successful = false;
reason = DownloadError.ERROR_UNSUPPORTED_TYPE;
+ if ("html".equalsIgnoreCase(e.getRootElement())) {
+ reason = DownloadError.ERROR_UNSUPPORTED_TYPE_HTML;
+ }
reasonDetailed = e.getMessage();
} catch (InvalidFeedException e) {
e.printStackTrace();
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java
index 483a2aa56..e2d9ee614 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java
@@ -2,6 +2,7 @@ package de.danoeh.antennapod.core.service.download.handler;
import android.content.Context;
import android.util.Log;
+
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.service.download.DownloadRequest;
import de.danoeh.antennapod.core.service.download.DownloadStatus;
@@ -15,6 +16,7 @@ public class FeedSyncTask {
private final DownloadRequest request;
private final Context context;
private DownloadStatus downloadStatus;
+ private Feed savedFeed;
public FeedSyncTask(Context context, DownloadRequest request) {
this.request = request;
@@ -30,7 +32,7 @@ public class FeedSyncTask {
return false;
}
- Feed savedFeed = DBTasks.updateFeed(context, result.feed, false);
+ savedFeed = DBTasks.updateFeed(context, result.feed, false);
// If loadAllPages=true, check if another page is available and queue it for download
final boolean loadAllPages = request.getArguments().getBoolean(DownloadRequester.REQUEST_ARG_LOAD_ALL_PAGES);
final Feed feed = result.feed;
@@ -48,4 +50,8 @@ public class FeedSyncTask {
public DownloadStatus getDownloadStatus() {
return downloadStatus;
}
+
+ public Feed getSavedFeed() {
+ return savedFeed;
+ }
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java
index 501214399..7712ca36b 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java
@@ -56,7 +56,7 @@ public class MediaDownloadedHandler implements Runnable {
// check if file has chapters
if (media.getItem() != null && !media.getItem().hasChapters()) {
- media.setChapters(ChapterUtils.loadChaptersFromFileUrl(media));
+ media.setChapters(ChapterUtils.loadChaptersFromMediaFile(media, context));
}
// Get duration
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java
index 71bbf2efd..9a8248984 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java
@@ -2,6 +2,7 @@ package de.danoeh.antennapod.core.service.playback;
import android.content.Context;
import android.net.Uri;
+import android.text.TextUtils;
import android.util.Log;
import android.view.SurfaceHolder;
import com.google.android.exoplayer2.C;
@@ -28,8 +29,10 @@ import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
+
import de.danoeh.antennapod.core.ClientConfig;
import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.service.download.HttpDownloader;
import de.danoeh.antennapod.core.util.playback.IPlayer;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
@@ -184,14 +187,22 @@ public class ExoPlayerWrapper implements IPlayer {
exoPlayer.setAudioAttributes(b.build());
}
- @Override
- public void setDataSource(String s) throws IllegalArgumentException, IllegalStateException {
+ public void setDataSource(String s, String user, String password)
+ throws IllegalArgumentException, IllegalStateException {
Log.d(TAG, "setDataSource: " + s);
DefaultHttpDataSourceFactory httpDataSourceFactory = new DefaultHttpDataSourceFactory(
ClientConfig.USER_AGENT, null,
DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS,
DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS,
true);
+
+ if (!TextUtils.isEmpty(user) && !TextUtils.isEmpty(password)) {
+ httpDataSourceFactory.getDefaultRequestProperties().set("Authorization",
+ HttpDownloader.encodeCredentials(
+ user,
+ password,
+ "ISO-8859-1"));
+ }
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context, null, httpDataSourceFactory);
DefaultExtractorsFactory extractorsFactory = new DefaultExtractorsFactory();
extractorsFactory.setConstantBitrateSeekingEnabled(true);
@@ -200,6 +211,11 @@ public class ExoPlayerWrapper implements IPlayer {
}
@Override
+ public void setDataSource(String s) throws IllegalArgumentException, IllegalStateException {
+ setDataSource(s, null, null);
+ }
+
+ @Override
public void setDisplay(SurfaceHolder sh) {
exoPlayer.setVideoSurfaceHolder(sh);
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java
index 325b04e9a..28d8a0e29 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java
@@ -1,6 +1,8 @@
package de.danoeh.antennapod.core.service.playback;
+import android.app.UiModeManager;
import android.content.Context;
+import android.content.res.Configuration;
import android.media.AudioManager;
import android.os.PowerManager;
import androidx.annotation.NonNull;
@@ -36,6 +38,7 @@ import de.danoeh.antennapod.core.util.RewindAfterPauseUtils;
import de.danoeh.antennapod.core.util.playback.AudioPlayer;
import de.danoeh.antennapod.core.util.playback.IPlayer;
import de.danoeh.antennapod.core.util.playback.Playable;
+import de.danoeh.antennapod.core.util.playback.PlayableException;
import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter;
import de.danoeh.antennapod.core.util.playback.VideoPlayer;
@@ -260,13 +263,25 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
callback.onMediaChanged(false);
setPlaybackParams(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media), UserPreferences.isSkipSilence());
if (stream) {
- mediaPlayer.setDataSource(media.getStreamUrl());
+ if (playable instanceof FeedMedia) {
+ FeedMedia feedMedia = (FeedMedia) playable;
+ FeedPreferences preferences = feedMedia.getItem().getFeed().getPreferences();
+ mediaPlayer.setDataSource(
+ media.getStreamUrl(),
+ preferences.getUsername(),
+ preferences.getPassword());
+ } else {
+ mediaPlayer.setDataSource(media.getStreamUrl());
+ }
} else if (media.getLocalMediaUrl() != null && new File(media.getLocalMediaUrl()).canRead()) {
mediaPlayer.setDataSource(media.getLocalMediaUrl());
} else {
throw new IOException("Unable to read local file " + media.getLocalMediaUrl());
}
- setPlayerStatus(PlayerStatus.INITIALIZED, media);
+ UiModeManager uiModeManager = (UiModeManager) context.getSystemService(Context.UI_MODE_SERVICE);
+ if (uiModeManager.getCurrentModeType() != Configuration.UI_MODE_TYPE_CAR) {
+ setPlayerStatus(PlayerStatus.INITIALIZED, media);
+ }
if (prepareImmediately) {
setPlayerStatus(PlayerStatus.PREPARING, media);
@@ -274,7 +289,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
onPrepared(startWhenPrepared);
}
- } catch (Playable.PlayableException | IOException | IllegalStateException e) {
+ } catch (PlayableException | IOException | IllegalStateException e) {
e.printStackTrace();
setPlayerStatus(PlayerStatus.ERROR, null);
}
@@ -924,9 +939,6 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
boolean isPlaying = playerStatus == PlayerStatus.PLAYING;
- if (playerStatus != PlayerStatus.INDETERMINATE) {
- setPlayerStatus(PlayerStatus.INDETERMINATE, media);
- }
// we're relying on the position stored in the Playable object for post-playback processing
if (media != null) {
int position = getPosition();
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java
index c1500d78b..9430e2e3c 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java
@@ -50,7 +50,6 @@ import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
-import de.danoeh.antennapod.core.ClientConfig;
import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.core.event.MessageEvent;
import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
@@ -69,7 +68,6 @@ import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.preferences.SleepTimerPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.receiver.MediaButtonReceiver;
-import de.danoeh.antennapod.core.service.PlayerWidgetJobService;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBTasks;
import de.danoeh.antennapod.core.storage.DBWriter;
@@ -78,9 +76,13 @@ import de.danoeh.antennapod.core.feed.util.ImageResourceUtils;
import de.danoeh.antennapod.core.util.IntentUtils;
import de.danoeh.antennapod.core.util.NetworkUtils;
import de.danoeh.antennapod.core.util.gui.NotificationUtils;
-import de.danoeh.antennapod.core.util.playback.ExternalMedia;
import de.danoeh.antennapod.core.util.playback.Playable;
+import de.danoeh.antennapod.core.util.playback.PlayableException;
+import de.danoeh.antennapod.core.util.playback.PlayableUtils;
import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter;
+import de.danoeh.antennapod.core.widget.WidgetUpdater;
+import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter;
+import de.danoeh.antennapod.ui.appstartintent.VideoPlayerActivityStarter;
import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
@@ -245,24 +247,31 @@ public class PlaybackService extends MediaBrowserServiceCompat {
* running, the type of the last played media will be looked up.
*/
public static Intent getPlayerActivityIntent(Context context) {
+ boolean showVideoPlayer;
+
if (isRunning) {
- return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, currentMediaType, isCasting);
+ showVideoPlayer = currentMediaType == MediaType.VIDEO && !isCasting;
} else {
- if (PlaybackPreferences.getCurrentEpisodeIsVideo()) {
- return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, MediaType.VIDEO, isCasting);
- } else {
- return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, MediaType.AUDIO, isCasting);
- }
+ showVideoPlayer = PlaybackPreferences.getCurrentEpisodeIsVideo();
+ }
+
+ if (showVideoPlayer) {
+ return new VideoPlayerActivityStarter(context).getIntent();
+ } else {
+ return new MainActivityStarter(context).withOpenPlayer().getIntent();
}
}
/**
- * Same as getPlayerActivityIntent(context), but here the type of activity
+ * Same as {@link #getPlayerActivityIntent(Context)}, but here the type of activity
* depends on the FeedMedia that is provided as an argument.
*/
public static Intent getPlayerActivityIntent(Context context, Playable media) {
- MediaType mt = media.getMediaType();
- return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, mt, isCasting);
+ if (media.getMediaType() == MediaType.VIDEO && !isCasting) {
+ return new VideoPlayerActivityStarter(context).getIntent();
+ } else {
+ return new MainActivityStarter(context).withOpenPlayer().getIntent();
+ }
}
@Override
@@ -401,8 +410,8 @@ public class PlaybackService extends MediaBrowserServiceCompat {
.setTitle(feed.getTitle())
.setDescription(feed.getDescription())
.setSubtitle(feed.getCustomTitle());
- if (feed.getImageLocation() != null) {
- builder.setIconUri(Uri.parse(feed.getImageLocation()));
+ if (feed.getImageUrl() != null) {
+ builder.setIconUri(Uri.parse(feed.getImageUrl()));
}
if (feed.getLink() != null) {
builder.setMediaUri(Uri.parse(feed.getLink()));
@@ -509,8 +518,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
boolean startWhenPrepared = intent.getBooleanExtra(EXTRA_START_WHEN_PREPARED, false);
boolean prepareImmediately = intent.getBooleanExtra(EXTRA_PREPARE_IMMEDIATELY, false);
sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0);
- //If the user asks to play External Media, the casting session, if on, should end.
- flavorHelper.castDisconnect(playable instanceof ExternalMedia);
if (allowStreamAlways) {
UserPreferences.setAllowMobileStreaming(true);
}
@@ -668,18 +675,14 @@ public class PlaybackService extends MediaBrowserServiceCompat {
}
return false;
case KeyEvent.KEYCODE_MEDIA_NEXT:
- if (getStatus() != PlayerStatus.PLAYING && getStatus() != PlayerStatus.PAUSED) {
- return false;
- } else if (notificationButton || UserPreferences.shouldHardwareButtonSkip()) {
- // assume the skip command comes from a notification or the lockscreen
- // a >| skip button should actually skip
+ if (!notificationButton) {
+ // Handle remapped button as notification button which is not remapped again.
+ return handleKeycode(UserPreferences.getHardwareForwardButton(), true);
+ } else if (getStatus() == PlayerStatus.PLAYING || getStatus() == PlayerStatus.PAUSED) {
mediaPlayer.skip();
- } else {
- // assume skip command comes from a (bluetooth) media button
- // user actually wants to fast-forward
- seekDelta(UserPreferences.getFastForwardSecs() * 1000);
+ return true;
}
- return true;
+ return false;
case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
if (getStatus() == PlayerStatus.PLAYING || getStatus() == PlayerStatus.PAUSED) {
mediaPlayer.seekDelta(UserPreferences.getFastForwardSecs() * 1000);
@@ -687,23 +690,20 @@ public class PlaybackService extends MediaBrowserServiceCompat {
}
return false;
case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
- if (getStatus() != PlayerStatus.PLAYING && getStatus() != PlayerStatus.PAUSED) {
- return false;
- } else if (UserPreferences.shouldHardwarePreviousButtonRestart()) {
- // user wants to restart current episode
+ if (!notificationButton) {
+ // Handle remapped button as notification button which is not remapped again.
+ return handleKeycode(UserPreferences.getHardwarePreviousButton(), true);
+ } else if (getStatus() == PlayerStatus.PLAYING || getStatus() == PlayerStatus.PAUSED) {
mediaPlayer.seekTo(0);
- } else {
- // user wants to rewind current episode
- mediaPlayer.seekDelta(-UserPreferences.getRewindSecs() * 1000);
+ return true;
}
- return true;
+ return false;
case KeyEvent.KEYCODE_MEDIA_REWIND:
if (getStatus() == PlayerStatus.PLAYING || getStatus() == PlayerStatus.PAUSED) {
mediaPlayer.seekDelta(-UserPreferences.getRewindSecs() * 1000);
- } else {
- return false;
+ return true;
}
- return true;
+ return false;
case KeyEvent.KEYCODE_MEDIA_STOP:
if (status == PlayerStatus.PLAYING) {
mediaPlayer.pause(true, true);
@@ -722,7 +722,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
}
private void startPlayingFromPreferences() {
- Observable.fromCallable(() -> Playable.PlayableUtils.createInstanceFromPreferences(getApplicationContext()))
+ Observable.fromCallable(() -> PlayableUtils.createInstanceFromPreferences(getApplicationContext()))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
@@ -801,8 +801,9 @@ public class PlaybackService extends MediaBrowserServiceCompat {
}
@Override
- public void onWidgetUpdaterTick() {
- PlayerWidgetJobService.updateWidget(getBaseContext());
+ public WidgetUpdater.WidgetState requestWidgetState() {
+ return new WidgetUpdater.WidgetState(getPlayable(), getStatus(),
+ getCurrentPosition(), getDuration(), getCurrentPlaybackSpeed(), isCasting());
}
@Override
@@ -873,9 +874,9 @@ public class PlaybackService extends MediaBrowserServiceCompat {
}
IntentUtils.sendLocalBroadcast(getApplicationContext(), ACTION_PLAYER_STATUS_CHANGED);
- PlayerWidgetJobService.updateWidget(getBaseContext());
bluetoothNotifyChange(newInfo, AVRCP_ACTION_PLAYER_STATUS_CHANGED);
bluetoothNotifyChange(newInfo, AVRCP_ACTION_META_CHANGED);
+ taskManager.requestWidgetUpdate();
}
@Override
@@ -994,7 +995,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
FeedMedia media = (FeedMedia) currentMedia;
try {
media.loadMetadata();
- } catch (Playable.PlayableException e) {
+ } catch (PlayableException e) {
Log.e(TAG, "Unable to load metadata to get next in queue", e);
return null;
}
@@ -1240,7 +1241,8 @@ public class PlaybackService extends MediaBrowserServiceCompat {
capabilities = capabilities | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
}
- UiModeManager uiModeManager = (UiModeManager) getApplicationContext().getSystemService(Context.UI_MODE_SERVICE);
+ UiModeManager uiModeManager = (UiModeManager) getApplicationContext()
+ .getSystemService(Context.UI_MODE_SERVICE);
if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR) {
sessionState.addCustomAction(
new PlaybackStateCompat.CustomAction.Builder(
@@ -1303,21 +1305,32 @@ public class PlaybackService extends MediaBrowserServiceCompat {
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, p.getEpisodeTitle());
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, p.getFeedTitle());
- String imageLocation = ImageResourceUtils.getImageLocation(p);
+ String imageLocation = p.getImageLocation();
if (!TextUtils.isEmpty(imageLocation)) {
if (UserPreferences.setLockscreenBackground()) {
+ Bitmap art;
builder.putString(MediaMetadataCompat.METADATA_KEY_ART_URI, imageLocation);
try {
- Bitmap art = Glide.with(this)
+ art = Glide.with(this)
.asBitmap()
.load(imageLocation)
.apply(RequestOptions.diskCacheStrategyOf(ApGlideSettings.AP_DISK_CACHE_STRATEGY))
.submit(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
.get();
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, art);
- } catch (Throwable tr) {
- Log.e(TAG, Log.getStackTraceString(tr));
+ } catch (Throwable tr1) {
+ try {
+ art = Glide.with(this)
+ .asBitmap()
+ .load(ImageResourceUtils.getFallbackImageLocation(p))
+ .apply(RequestOptions.diskCacheStrategyOf(ApGlideSettings.AP_DISK_CACHE_STRATEGY))
+ .submit(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
+ .get();
+ builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, art);
+ } catch (Throwable tr2) {
+ Log.e(TAG, Log.getStackTraceString(tr2));
+ }
}
} else if (isCasting) {
// In the absence of metadata art, the controller dialog takes care of creating it.
@@ -1897,7 +1910,10 @@ public class PlaybackService extends MediaBrowserServiceCompat {
@Override
public void onSkipToNext() {
Log.d(TAG, "onSkipToNext()");
- if (UserPreferences.shouldHardwareButtonSkip()) {
+ UiModeManager uiModeManager = (UiModeManager) getApplicationContext()
+ .getSystemService(Context.UI_MODE_SERVICE);
+ if (UserPreferences.getHardwareForwardButton() == KeyEvent.KEYCODE_MEDIA_NEXT
+ || uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR) {
mediaPlayer.skip();
} else {
seekDelta(UserPreferences.getFastForwardSecs() * 1000);
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java
index 9d249620d..cbfc36266 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java
@@ -29,6 +29,8 @@ import de.danoeh.antennapod.core.util.TimeSpeedConverter;
import de.danoeh.antennapod.core.util.gui.NotificationUtils;
import de.danoeh.antennapod.core.util.playback.Playable;
import java.util.ArrayList;
+import java.util.concurrent.ExecutionException;
+
import org.apache.commons.lang3.ArrayUtils;
public class PlaybackServiceNotificationBuilder {
@@ -73,11 +75,23 @@ public class PlaybackServiceNotificationBuilder {
try {
icon = Glide.with(context)
.asBitmap()
- .load(ImageResourceUtils.getImageLocation(playable))
+ .load(playable.getImageLocation())
.apply(RequestOptions.diskCacheStrategyOf(ApGlideSettings.AP_DISK_CACHE_STRATEGY))
.apply(new RequestOptions().centerCrop())
.submit(iconSize, iconSize)
.get();
+ } catch (ExecutionException e) {
+ try {
+ icon = Glide.with(context)
+ .asBitmap()
+ .load(ImageResourceUtils.getFallbackImageLocation(playable))
+ .apply(RequestOptions.diskCacheStrategyOf(ApGlideSettings.AP_DISK_CACHE_STRATEGY))
+ .apply(new RequestOptions().centerCrop())
+ .submit(iconSize, iconSize)
+ .get();
+ } catch (Throwable tr) {
+ Log.e(TAG, "Error loading the media icon for the notification", tr);
+ }
} catch (Throwable tr) {
Log.e(TAG, "Error loading the media icon for the notification", tr);
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java
index 05d64ea3e..556d9b3c0 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java
@@ -8,6 +8,8 @@ import androidx.annotation.NonNull;
import android.util.Log;
import de.danoeh.antennapod.core.preferences.SleepTimerPreferences;
+import de.danoeh.antennapod.core.util.ChapterUtils;
+import de.danoeh.antennapod.core.widget.WidgetUpdater;
import io.reactivex.disposables.Disposable;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
@@ -199,11 +201,10 @@ public class PlaybackServiceTaskManager {
*/
public synchronized void startWidgetUpdater() {
if (!isWidgetUpdaterActive() && !schedExecutor.isShutdown()) {
- Runnable widgetUpdater = callback::onWidgetUpdaterTick;
+ Runnable widgetUpdater = this::requestWidgetUpdate;
widgetUpdater = useMainThreadIfNecessary(widgetUpdater);
- widgetUpdaterFuture = schedExecutor.scheduleWithFixedDelay(widgetUpdater, WIDGET_UPDATER_NOTIFICATION_INTERVAL,
- WIDGET_UPDATER_NOTIFICATION_INTERVAL, TimeUnit.MILLISECONDS);
-
+ widgetUpdaterFuture = schedExecutor.scheduleWithFixedDelay(widgetUpdater,
+ WIDGET_UPDATER_NOTIFICATION_INTERVAL, WIDGET_UPDATER_NOTIFICATION_INTERVAL, TimeUnit.MILLISECONDS);
Log.d(TAG, "Started WidgetUpdater");
} else {
Log.d(TAG, "Call to startWidgetUpdater was ignored.");
@@ -211,6 +212,18 @@ public class PlaybackServiceTaskManager {
}
/**
+ * Retrieves information about the widget state in the calling thread and then displays it in a background thread.
+ */
+ public synchronized void requestWidgetUpdate() {
+ WidgetUpdater.WidgetState state = callback.requestWidgetState();
+ if (!schedExecutor.isShutdown()) {
+ schedExecutor.execute(() -> WidgetUpdater.updateWidget(context, state));
+ } else {
+ Log.d(TAG, "Call to requestWidgetUpdate was ignored.");
+ }
+ }
+
+ /**
* Starts a new sleep timer with the given waiting time. If another sleep timer is already active, it will be
* cancelled first.
* After waitingTime has elapsed, onSleepTimerExpired() will be called.
@@ -303,7 +316,7 @@ public class PlaybackServiceTaskManager {
if (media.getChapters() == null) {
chapterLoaderFuture = Completable.create(emitter -> {
- media.loadChapterMarks(context);
+ ChapterUtils.loadChapters(media, context);
emitter.onComplete();
})
.subscribeOn(Schedulers.io())
@@ -464,7 +477,7 @@ public class PlaybackServiceTaskManager {
void onSleepTimerReset();
- void onWidgetUpdaterTick();
+ WidgetUpdater.WidgetState requestWidgetState();
void onChapterLoaded(Playable media);
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/ssl/BackportCaCerts.java b/core/src/main/java/de/danoeh/antennapod/core/ssl/BackportCaCerts.java
deleted file mode 100644
index 78c105e38..000000000
--- a/core/src/main/java/de/danoeh/antennapod/core/ssl/BackportCaCerts.java
+++ /dev/null
@@ -1,105 +0,0 @@
-package de.danoeh.antennapod.core.ssl;
-
-public class BackportCaCerts {
- public static final String SECTIGO_USER_TRUST = "-----BEGIN CERTIFICATE-----\n"
- + "MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB\n"
- + "iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl\n"
- + "cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV\n"
- + "BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw\n"
- + "MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV\n"
- + "BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU\n"
- + "aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy\n"
- + "dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK\n"
- + "AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B\n"
- + "3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY\n"
- + "tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/\n"
- + "Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2\n"
- + "VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT\n"
- + "79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6\n"
- + "c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT\n"
- + "Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l\n"
- + "c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee\n"
- + "UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE\n"
- + "Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd\n"
- + "BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G\n"
- + "A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF\n"
- + "Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO\n"
- + "VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3\n"
- + "ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs\n"
- + "8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR\n"
- + "iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze\n"
- + "Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ\n"
- + "XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/\n"
- + "qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB\n"
- + "VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB\n"
- + "L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG\n"
- + "jjxDah2nGN59PRbxYvnKkKj9\n"
- + "-----END CERTIFICATE-----\n";
-
- public static final String COMODO = "-----BEGIN CERTIFICATE-----\n"
- + "MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB\n"
- + "hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G\n"
- + "A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV\n"
- + "BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5\n"
- + "MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT\n"
- + "EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR\n"
- + "Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh\n"
- + "dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR\n"
- + "6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X\n"
- + "pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC\n"
- + "9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV\n"
- + "/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf\n"
- + "Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z\n"
- + "+pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w\n"
- + "qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah\n"
- + "SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC\n"
- + "u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf\n"
- + "Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq\n"
- + "crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E\n"
- + "FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB\n"
- + "/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl\n"
- + "wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM\n"
- + "4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV\n"
- + "2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna\n"
- + "FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ\n"
- + "CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK\n"
- + "boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke\n"
- + "jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL\n"
- + "S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb\n"
- + "QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl\n"
- + "0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB\n"
- + "NVOFBkpdn627G190\n"
- + "-----END CERTIFICATE-----";
-
- public static final String LETSENCRYPT_ISRG = "-----BEGIN CERTIFICATE-----\n"
- + "MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw\n"
- + "TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh\n"
- + "cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4\n"
- + "WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu\n"
- + "ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY\n"
- + "MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc\n"
- + "h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+\n"
- + "0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U\n"
- + "A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW\n"
- + "T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH\n"
- + "B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC\n"
- + "B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv\n"
- + "KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn\n"
- + "OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn\n"
- + "jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw\n"
- + "qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI\n"
- + "rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV\n"
- + "HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq\n"
- + "hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL\n"
- + "ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ\n"
- + "3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK\n"
- + "NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5\n"
- + "ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur\n"
- + "TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC\n"
- + "jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc\n"
- + "oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq\n"
- + "4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA\n"
- + "mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d\n"
- + "emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=\n"
- + "-----END CERTIFICATE-----";
-}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/ssl/BackportTrustManager.java b/core/src/main/java/de/danoeh/antennapod/core/ssl/BackportTrustManager.java
deleted file mode 100644
index 81d2a0709..000000000
--- a/core/src/main/java/de/danoeh/antennapod/core/ssl/BackportTrustManager.java
+++ /dev/null
@@ -1,60 +0,0 @@
-package de.danoeh.antennapod.core.ssl;
-
-import android.util.Log;
-
-import javax.net.ssl.TrustManager;
-import javax.net.ssl.TrustManagerFactory;
-import javax.net.ssl.X509TrustManager;
-import java.io.ByteArrayInputStream;
-import java.nio.charset.Charset;
-import java.security.KeyStore;
-import java.security.KeyStoreException;
-import java.security.NoSuchAlgorithmException;
-import java.security.cert.CertificateFactory;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * SSL trust manager that allows old Android systems to use modern certificates.
- */
-public class BackportTrustManager {
- private static final String TAG = "BackportTrustManager";
-
- private static X509TrustManager getSystemTrustManager(KeyStore keystore) {
- TrustManagerFactory factory;
- try {
- factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
- factory.init(keystore);
- for (TrustManager manager : factory.getTrustManagers()) {
- if (manager instanceof X509TrustManager) {
- return (X509TrustManager) manager;
- }
- }
- } catch (NoSuchAlgorithmException | KeyStoreException e) {
- e.printStackTrace();
- }
- throw new IllegalStateException("Unexpected default trust managers");
- }
-
- public static X509TrustManager create() {
- try {
- KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
- keystore.load(null); // Clear
- CertificateFactory cf = CertificateFactory.getInstance("X.509");
- keystore.setCertificateEntry("BACKPORT_COMODO_ROOT_CA", cf.generateCertificate(
- new ByteArrayInputStream(BackportCaCerts.COMODO.getBytes(Charset.forName("UTF-8")))));
- keystore.setCertificateEntry("SECTIGO_USER_TRUST_CA", cf.generateCertificate(
- new ByteArrayInputStream(BackportCaCerts.SECTIGO_USER_TRUST.getBytes(Charset.forName("UTF-8")))));
- keystore.setCertificateEntry("LETSENCRYPT_ISRG_CA", cf.generateCertificate(
- new ByteArrayInputStream(BackportCaCerts.LETSENCRYPT_ISRG.getBytes(Charset.forName("UTF-8")))));
-
- List<X509TrustManager> managers = new ArrayList<>();
- managers.add(getSystemTrustManager(keystore));
- managers.add(getSystemTrustManager(null));
- return new CompositeX509TrustManager(managers);
- } catch (Exception e) {
- Log.e(TAG, Log.getStackTraceString(e));
- return null;
- }
- }
-}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/ssl/CompositeX509TrustManager.java b/core/src/main/java/de/danoeh/antennapod/core/ssl/CompositeX509TrustManager.java
deleted file mode 100644
index 7af96a492..000000000
--- a/core/src/main/java/de/danoeh/antennapod/core/ssl/CompositeX509TrustManager.java
+++ /dev/null
@@ -1,60 +0,0 @@
-package de.danoeh.antennapod.core.ssl;
-
-import javax.net.ssl.X509TrustManager;
-import java.security.cert.CertificateException;
-import java.security.cert.X509Certificate;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-/**
- * Represents an ordered list of {@link X509TrustManager}s with additive trust. If any one of the composed managers
- * trusts a certificate chain, then it is trusted by the composite manager.
- * Based on https://stackoverflow.com/a/16229909
- */
-public class CompositeX509TrustManager implements X509TrustManager {
- private final List<X509TrustManager> trustManagers;
-
- public CompositeX509TrustManager(List<X509TrustManager> trustManagers) {
- this.trustManagers = trustManagers;
- }
-
- @Override
- public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
- CertificateException reason = null;
- for (X509TrustManager trustManager : trustManagers) {
- try {
- trustManager.checkClientTrusted(chain, authType);
- return; // someone trusts them. success!
- } catch (CertificateException e) {
- // maybe someone else will trust them
- reason = e;
- }
- }
- throw reason;
- }
-
- @Override
- public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
- CertificateException reason = null;
- for (X509TrustManager trustManager : trustManagers) {
- try {
- trustManager.checkServerTrusted(chain, authType);
- return; // someone trusts them. success!
- } catch (CertificateException e) {
- // maybe someone else will trust them
- reason = e;
- }
- }
- throw reason;
- }
-
- @Override
- public X509Certificate[] getAcceptedIssuers() {
- List<X509Certificate> certificates = new ArrayList<>();
- for (X509TrustManager trustManager : trustManagers) {
- certificates.addAll(Arrays.asList(trustManager.getAcceptedIssuers()));
- }
- return certificates.toArray(new X509Certificate[0]);
- }
-}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/ssl/NoV1SslSocketFactory.java b/core/src/main/java/de/danoeh/antennapod/core/ssl/NoV1SslSocketFactory.java
deleted file mode 100644
index 96a42f22d..000000000
--- a/core/src/main/java/de/danoeh/antennapod/core/ssl/NoV1SslSocketFactory.java
+++ /dev/null
@@ -1,100 +0,0 @@
-package de.danoeh.antennapod.core.ssl;
-
-import de.danoeh.antennapod.core.util.Flavors;
-
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLSocket;
-import javax.net.ssl.SSLSocketFactory;
-import javax.net.ssl.TrustManager;
-import java.io.IOException;
-import java.net.InetAddress;
-import java.net.Socket;
-import java.security.GeneralSecurityException;
-
-/**
- * SSLSocketFactory that does not use TLS 1.0
- * This fixes issues with old Android versions that abort if the server does not know TLS 1.0
- */
-public class NoV1SslSocketFactory extends SSLSocketFactory {
- private SSLSocketFactory factory;
-
- public NoV1SslSocketFactory(TrustManager trustManager) {
- try {
- SSLContext sslContext;
-
- if (Flavors.FLAVOR == Flavors.FREE) {
- // Free flavor (bundles modern conscrypt): support for TLSv1.3 is guaranteed.
- sslContext = SSLContext.getInstance("TLSv1.3");
- } else {
- // Play flavor (security provider can vary): only TLSv1.2 is guaranteed.
- sslContext = SSLContext.getInstance("TLSv1.2");
- }
-
- sslContext.init(null, new TrustManager[] {trustManager}, null);
- factory = sslContext.getSocketFactory();
- } catch (GeneralSecurityException e) {
- e.printStackTrace();
- }
- }
-
- @Override
- public String[] getDefaultCipherSuites() {
- return factory.getDefaultCipherSuites();
- }
-
- @Override
- public String[] getSupportedCipherSuites() {
- return factory.getSupportedCipherSuites();
- }
-
- public Socket createSocket() throws IOException {
- SSLSocket result = (SSLSocket) factory.createSocket();
- configureSocket(result);
- return result;
- }
-
- public Socket createSocket(String var1, int var2) throws IOException {
- SSLSocket result = (SSLSocket) factory.createSocket(var1, var2);
- configureSocket(result);
- return result;
- }
-
- public Socket createSocket(Socket var1, String var2, int var3, boolean var4) throws IOException {
- SSLSocket result = (SSLSocket) factory.createSocket(var1, var2, var3, var4);
- configureSocket(result);
- return result;
- }
-
- public Socket createSocket(InetAddress var1, int var2) throws IOException {
- SSLSocket result = (SSLSocket) factory.createSocket(var1, var2);
- configureSocket(result);
- return result;
- }
-
- public Socket createSocket(String var1, int var2, InetAddress var3, int var4) throws IOException {
- SSLSocket result = (SSLSocket) factory.createSocket(var1, var2, var3, var4);
- configureSocket(result);
- return result;
- }
-
- public Socket createSocket(InetAddress var1, int var2, InetAddress var3, int var4) throws IOException {
- SSLSocket result = (SSLSocket) factory.createSocket(var1, var2, var3, var4);
- configureSocket(result);
- return result;
- }
-
- private void configureSocket(SSLSocket s) {
- if (Flavors.FLAVOR == Flavors.FREE) {
- // Free flavor (bundles modern conscrypt): TLSv1.3 and modern cipher suites are
- // guaranteed. Protocols older than TLSv1.2 are now deprecated and can be disabled.
- s.setEnabledProtocols(new String[] { "TLSv1.3", "TLSv1.2" });
- } else {
- // Play flavor (security provider can vary): only TLSv1.2 is guaranteed, supported
- // cipher suites may vary. Old protocols might be necessary to keep things working.
-
- // TLS 1.0 is enabled by default on some old systems, which causes connection errors.
- // This disables that.
- s.setEnabledProtocols(new String[] { "TLSv1.2", "TLSv1.1", "TLSv1" });
- }
- }
-} \ No newline at end of file
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/APDownloadAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/APDownloadAlgorithm.java
deleted file mode 100644
index 061d6cf3f..000000000
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/APDownloadAlgorithm.java
+++ /dev/null
@@ -1,103 +0,0 @@
-package de.danoeh.antennapod.core.storage;
-
-import android.content.Context;
-import android.util.Log;
-
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
-
-import de.danoeh.antennapod.core.feed.FeedFilter;
-import de.danoeh.antennapod.core.feed.FeedItem;
-import de.danoeh.antennapod.core.feed.FeedPreferences;
-import de.danoeh.antennapod.core.preferences.UserPreferences;
-import de.danoeh.antennapod.core.util.NetworkUtils;
-import de.danoeh.antennapod.core.util.PowerUtils;
-
-/**
- * Implements the automatic download algorithm used by AntennaPod. This class assumes that
- * the client uses the APEpisodeCleanupAlgorithm.
- */
-public class APDownloadAlgorithm implements AutomaticDownloadAlgorithm {
- private static final String TAG = "APDownloadAlgorithm";
-
- /**
- * Looks for undownloaded episodes in the queue or list of new items and request a download if
- * 1. Network is available
- * 2. The device is charging or the user allows auto download on battery
- * 3. There is free space in the episode cache
- * This method is executed on an internal single thread executor.
- *
- * @param context Used for accessing the DB.
- * @return A Runnable that will be submitted to an ExecutorService.
- */
- @Override
- public Runnable autoDownloadUndownloadedItems(final Context context) {
- return () -> {
-
- // true if we should auto download based on network status
- boolean networkShouldAutoDl = NetworkUtils.autodownloadNetworkAvailable()
- && UserPreferences.isEnableAutodownload();
-
- // true if we should auto download based on power status
- boolean powerShouldAutoDl = PowerUtils.deviceCharging(context)
- || UserPreferences.isEnableAutodownloadOnBattery();
-
- // we should only auto download if both network AND power are happy
- if (networkShouldAutoDl && powerShouldAutoDl) {
-
- Log.d(TAG, "Performing auto-dl of undownloaded episodes");
-
- List<FeedItem> candidates;
- final List<FeedItem> queue = DBReader.getQueue();
- final List<FeedItem> newItems = DBReader.getNewItemsList(0, Integer.MAX_VALUE);
- candidates = new ArrayList<>(queue.size() + newItems.size());
- candidates.addAll(queue);
- for (FeedItem newItem : newItems) {
- FeedPreferences feedPrefs = newItem.getFeed().getPreferences();
- FeedFilter feedFilter = feedPrefs.getFilter();
- if (!candidates.contains(newItem) && feedFilter.shouldAutoDownload(newItem)) {
- candidates.add(newItem);
- }
- }
-
- // filter items that are not auto downloadable
- Iterator<FeedItem> it = candidates.iterator();
- while (it.hasNext()) {
- FeedItem item = it.next();
- if (!item.isAutoDownloadable() || item.getFeed().isLocalFeed()) {
- it.remove();
- }
- }
-
- int autoDownloadableEpisodes = candidates.size();
- int downloadedEpisodes = DBReader.getNumberOfDownloadedEpisodes();
- int deletedEpisodes = UserPreferences.getEpisodeCleanupAlgorithm()
- .makeRoomForEpisodes(context, autoDownloadableEpisodes);
- boolean cacheIsUnlimited =
- UserPreferences.getEpisodeCacheSize() == UserPreferences.getEpisodeCacheSizeUnlimited();
- int episodeCacheSize = UserPreferences.getEpisodeCacheSize();
-
- int episodeSpaceLeft;
- if (cacheIsUnlimited || episodeCacheSize >= downloadedEpisodes + autoDownloadableEpisodes) {
- episodeSpaceLeft = autoDownloadableEpisodes;
- } else {
- episodeSpaceLeft = episodeCacheSize - (downloadedEpisodes - deletedEpisodes);
- }
-
- FeedItem[] itemsToDownload = candidates.subList(0, episodeSpaceLeft)
- .toArray(new FeedItem[episodeSpaceLeft]);
-
- if (itemsToDownload.length > 0) {
- Log.d(TAG, "Enqueueing " + itemsToDownload.length + " items for download");
-
- try {
- DownloadRequester.getInstance().downloadMedia(false, context, false, itemsToDownload);
- } catch (DownloadRequestException e) {
- e.printStackTrace();
- }
- }
- }
- };
- }
-}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java
index dbb77e19c..f8b643ccf 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java
@@ -1,11 +1,28 @@
package de.danoeh.antennapod.core.storage;
import android.content.Context;
+import android.util.Log;
-public interface AutomaticDownloadAlgorithm {
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import de.danoeh.antennapod.core.feed.FeedFilter;
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.feed.FeedPreferences;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.util.NetworkUtils;
+import de.danoeh.antennapod.core.util.PowerUtils;
+
+/**
+ * Implements the automatic download algorithm used by AntennaPod. This class assumes that
+ * the client uses the {@link EpisodeCleanupAlgorithm}.
+ */
+public class AutomaticDownloadAlgorithm {
+ private static final String TAG = "DownloadAlgorithm";
/**
- * Looks for undownloaded episodes and request a download if
+ * Looks for undownloaded episodes in the queue or list of new items and request a download if
* 1. Network is available
* 2. The device is charging or the user allows auto download on battery
* 3. There is free space in the episode cache
@@ -14,5 +31,72 @@ public interface AutomaticDownloadAlgorithm {
* @param context Used for accessing the DB.
* @return A Runnable that will be submitted to an ExecutorService.
*/
- Runnable autoDownloadUndownloadedItems(Context context);
+ public Runnable autoDownloadUndownloadedItems(final Context context) {
+ return () -> {
+
+ // true if we should auto download based on network status
+ boolean networkShouldAutoDl = NetworkUtils.autodownloadNetworkAvailable()
+ && UserPreferences.isEnableAutodownload();
+
+ // true if we should auto download based on power status
+ boolean powerShouldAutoDl = PowerUtils.deviceCharging(context)
+ || UserPreferences.isEnableAutodownloadOnBattery();
+
+ // we should only auto download if both network AND power are happy
+ if (networkShouldAutoDl && powerShouldAutoDl) {
+
+ Log.d(TAG, "Performing auto-dl of undownloaded episodes");
+
+ List<FeedItem> candidates;
+ final List<FeedItem> queue = DBReader.getQueue();
+ final List<FeedItem> newItems = DBReader.getNewItemsList(0, Integer.MAX_VALUE);
+ candidates = new ArrayList<>(queue.size() + newItems.size());
+ candidates.addAll(queue);
+ for (FeedItem newItem : newItems) {
+ FeedPreferences feedPrefs = newItem.getFeed().getPreferences();
+ FeedFilter feedFilter = feedPrefs.getFilter();
+ if (!candidates.contains(newItem) && feedFilter.shouldAutoDownload(newItem)) {
+ candidates.add(newItem);
+ }
+ }
+
+ // filter items that are not auto downloadable
+ Iterator<FeedItem> it = candidates.iterator();
+ while (it.hasNext()) {
+ FeedItem item = it.next();
+ if (!item.isAutoDownloadable() || item.getFeed().isLocalFeed()) {
+ it.remove();
+ }
+ }
+
+ int autoDownloadableEpisodes = candidates.size();
+ int downloadedEpisodes = DBReader.getNumberOfDownloadedEpisodes();
+ int deletedEpisodes = UserPreferences.getEpisodeCleanupAlgorithm()
+ .makeRoomForEpisodes(context, autoDownloadableEpisodes);
+ boolean cacheIsUnlimited =
+ UserPreferences.getEpisodeCacheSize() == UserPreferences.getEpisodeCacheSizeUnlimited();
+ int episodeCacheSize = UserPreferences.getEpisodeCacheSize();
+
+ int episodeSpaceLeft;
+ if (cacheIsUnlimited || episodeCacheSize >= downloadedEpisodes + autoDownloadableEpisodes) {
+ episodeSpaceLeft = autoDownloadableEpisodes;
+ } else {
+ episodeSpaceLeft = episodeCacheSize - (downloadedEpisodes - deletedEpisodes);
+ }
+
+ FeedItem[] itemsToDownload = candidates.subList(0, episodeSpaceLeft)
+ .toArray(new FeedItem[episodeSpaceLeft]);
+
+ if (itemsToDownload.length > 0) {
+ Log.d(TAG, "Enqueueing " + itemsToDownload.length + " items for download");
+
+ try {
+ DownloadRequester.getInstance().downloadMedia(false, context, false, itemsToDownload);
+ } catch (DownloadRequestException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ };
+ }
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java
index 74e8e23cb..e45d53af3 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java
@@ -18,11 +18,13 @@ import java.util.Map;
import de.danoeh.antennapod.core.feed.Chapter;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.feed.FeedItemFilter;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.FeedPreferences;
import de.danoeh.antennapod.core.feed.SubscriptionsFilter;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.DownloadStatus;
+import de.danoeh.antennapod.core.storage.mapper.FeedCursorMapper;
import de.danoeh.antennapod.core.util.LongIntMap;
import de.danoeh.antennapod.core.util.LongList;
import de.danoeh.antennapod.core.util.comparator.DownloadStatusComparator;
@@ -160,11 +162,15 @@ public final class DBReader {
* The method does NOT change the items-attribute of the feed.
*/
public static List<FeedItem> getFeedItemList(final Feed feed) {
+ return getFeedItemList(feed, FeedItemFilter.unfiltered());
+ }
+
+ public static List<FeedItem> getFeedItemList(final Feed feed, final FeedItemFilter filter) {
Log.d(TAG, "getFeedItemList() called with: " + "feed = [" + feed + "]");
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
- try (Cursor cursor = adapter.getAllItemsOfFeedCursor(feed)) {
+ try (Cursor cursor = adapter.getItemsOfFeedCursor(feed, filter)) {
List<FeedItem> items = extractItemlistFromCursor(adapter, cursor);
Collections.sort(items, new FeedItemPubdateComparator());
for (FeedItem item : items) {
@@ -204,7 +210,7 @@ public final class DBReader {
}
private static Feed extractFeedFromCursorRow(Cursor cursor) {
- Feed feed = Feed.fromCursor(cursor);
+ Feed feed = FeedCursorMapper.convert(cursor);
FeedPreferences preferences = FeedPreferences.fromCursor(cursor);
feed.setPreferences(preferences);
return feed;
@@ -367,18 +373,19 @@ public final class DBReader {
}
/**
- * Loads a list of FeedItems sorted by pubDate in descending order.
+ * Loads a filtered list of FeedItems sorted by pubDate in descending order.
*
* @param offset The first episode that should be loaded.
* @param limit The maximum number of episodes that should be loaded.
+ * @param filter The filter describing which episodes to filter out.
*/
@NonNull
- public static List<FeedItem> getRecentlyPublishedEpisodes(int offset, int limit) {
- Log.d(TAG, "getRecentlyPublishedEpisodes() called with: " + "offset = [" + offset + "]" + " limit = [" + limit + "]" );
+ public static List<FeedItem> getRecentlyPublishedEpisodes(int offset, int limit, FeedItemFilter filter) {
+ Log.d(TAG, "getRecentlyPublishedEpisodes() called with: offset=" + offset + ", limit=" + limit);
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
- try (Cursor cursor = adapter.getRecentlyPublishedItemsCursor(offset, limit)) {
+ try (Cursor cursor = adapter.getRecentlyPublishedItemsCursor(offset, limit, filter)) {
List<FeedItem> items = extractItemlistFromCursor(adapter, cursor);
loadAdditionalFeedItemListData(items);
return items;
@@ -478,31 +485,41 @@ public final class DBReader {
*
* @param feedId The ID of the Feed
* @return The Feed or null if the Feed could not be found. The Feeds FeedItems will also be loaded from the
- * database and the items-attribute will be set correctly.
+ * database and the items-attribute will be set correctly.
*/
+ @Nullable
public static Feed getFeed(final long feedId) {
- Log.d(TAG, "getFeed() called with: " + "feedId = [" + feedId + "]");
-
- PodDBAdapter adapter = PodDBAdapter.getInstance();
- adapter.open();
- try {
- return getFeed(feedId, adapter);
- } finally {
- adapter.close();
- }
+ return getFeed(feedId, false);
}
+ /**
+ * Loads a specific Feed from the database.
+ *
+ * @param feedId The ID of the Feed
+ * @param filtered <code>true</code> if only the visible items should be loaded according to the feed filter.
+ * @return The Feed or null if the Feed could not be found. The Feeds FeedItems will also be loaded from the
+ * database and the items-attribute will be set correctly.
+ */
@Nullable
- static Feed getFeed(final long feedId, PodDBAdapter adapter) {
+ public static Feed getFeed(final long feedId, boolean filtered) {
+ Log.d(TAG, "getFeed() called with: " + "feedId = [" + feedId + "]");
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
Feed feed = null;
try (Cursor cursor = adapter.getFeedCursor(feedId)) {
if (cursor.moveToNext()) {
feed = extractFeedFromCursorRow(cursor);
- feed.setItems(getFeedItemList(feed));
+ if (filtered) {
+ feed.setItems(getFeedItemList(feed, feed.getItemFilter()));
+ } else {
+ feed.setItems(getFeedItemList(feed));
+ }
} else {
Log.e(TAG, "getFeed could not find feed with id " + feedId);
}
return feed;
+ } finally {
+ adapter.close();
}
}
@@ -635,10 +652,7 @@ public final class DBReader {
if (cursor.moveToFirst()) {
int indexDescription = cursor.getColumnIndex(PodDBAdapter.KEY_DESCRIPTION);
String description = cursor.getString(indexDescription);
- int indexContentEncoded = cursor.getColumnIndex(PodDBAdapter.KEY_CONTENT_ENCODED);
- String contentEncoded = cursor.getString(indexContentEncoded);
- item.setDescription(description);
- item.setContentEncoded(contentEncoded);
+ item.setDescriptionIfLonger(description);
}
} finally {
adapter.close();
@@ -801,15 +815,9 @@ public final class DBReader {
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
- List<Feed> feeds = getFeedList(adapter);
- long[] feedIds = new long[feeds.size()];
- for (int i = 0; i < feeds.size(); i++) {
- feedIds[i] = feeds.get(i).getId();
- }
- final LongIntMap feedCounters = adapter.getFeedCounters(feedIds);
-
+ final LongIntMap feedCounters = adapter.getFeedCounters();
SubscriptionsFilter subscriptionsFilter = UserPreferences.getSubscriptionsFilter();
- feeds = subscriptionsFilter.filter(getFeedList(adapter), feedCounters);
+ List<Feed> feeds = subscriptionsFilter.filter(getFeedList(adapter), feedCounters);
Comparator<Feed> comparator;
int feedOrder = UserPreferences.getFeedOrder();
@@ -839,7 +847,7 @@ public final class DBReader {
}
};
} else if (feedOrder == UserPreferences.FEED_ORDER_MOST_PLAYED) {
- final LongIntMap playedCounters = adapter.getPlayedEpisodesCounters(feedIds);
+ final LongIntMap playedCounters = adapter.getPlayedEpisodesCounters();
comparator = (lhs, rhs) -> {
long counterLhs = playedCounters.get(lhs.getId());
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java
index ec39e7144..d16432cd6 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java
@@ -6,7 +6,9 @@ import android.database.Cursor;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Log;
-import de.danoeh.antennapod.core.ClientConfig;
+
+import androidx.annotation.VisibleForTesting;
+
import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.core.event.FeedItemEvent;
import de.danoeh.antennapod.core.event.FeedListUpdateEvent;
@@ -14,10 +16,10 @@ import de.danoeh.antennapod.core.event.MessageEvent;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
-import de.danoeh.antennapod.core.feed.FeedPreferences;
import de.danoeh.antennapod.core.feed.LocalFeedUpdater;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.DownloadStatus;
+import de.danoeh.antennapod.core.storage.mapper.FeedCursorMapper;
import de.danoeh.antennapod.core.sync.SyncService;
import de.danoeh.antennapod.core.util.DownloadError;
import de.danoeh.antennapod.core.util.LongList;
@@ -29,6 +31,7 @@ import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
+import java.util.ListIterator;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
@@ -53,6 +56,8 @@ public final class DBTasks {
*/
private static final ExecutorService autodownloadExec;
+ private static AutomaticDownloadAlgorithm downloadAlgorithm = new AutomaticDownloadAlgorithm();
+
static {
autodownloadExec = Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r);
@@ -117,7 +122,18 @@ public final class DBTasks {
throw new IllegalStateException("DBTasks.refreshAllFeeds() must not be called from the main thread.");
}
- refreshFeeds(context, DBReader.getFeedList(), initiatedByUser);
+ List<Feed> feeds = DBReader.getFeedList();
+ ListIterator<Feed> iterator = feeds.listIterator();
+ while (iterator.hasNext()) {
+ if (!iterator.next().getPreferences().getKeepUpdated()) {
+ iterator.remove();
+ }
+ }
+ try {
+ refreshFeeds(context, feeds, false, false, false);
+ } catch (DownloadRequestException e) {
+ e.printStackTrace();
+ }
isRefreshing.set(false);
SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, MODE_PRIVATE);
@@ -131,38 +147,6 @@ public final class DBTasks {
}
/**
- * @param context
- * @param feedList the list of feeds to refresh
- * @param initiatedByUser a boolean indicating if the refresh was triggered by user action.
- */
- private static void refreshFeeds(final Context context,
- final List<Feed> feedList,
- boolean initiatedByUser) {
-
- for (Feed feed : feedList) {
- FeedPreferences prefs = feed.getPreferences();
- // feeds with !getKeepUpdated can only be refreshed
- // directly from the FeedActivity
- if (prefs.getKeepUpdated()) {
- try {
- refreshFeed(context, feed);
- } catch (DownloadRequestException e) {
- e.printStackTrace();
- DBWriter.addDownloadStatus(
- new DownloadStatus(feed,
- feed.getHumanReadableIdentifier(),
- DownloadError.ERROR_REQUEST_ERROR,
- false,
- e.getMessage(),
- initiatedByUser)
- );
- }
- }
- }
-
- }
-
- /**
* Downloads all pages of the given feed even if feed has not been modified since last refresh
*
* @param context Used for requesting the download.
@@ -170,7 +154,7 @@ public final class DBTasks {
*/
public static void forceRefreshCompleteFeed(final Context context, final Feed feed) {
try {
- refreshFeed(context, feed, true, true, false);
+ refreshFeeds(context, Collections.singletonList(feed), true, true, false);
} catch (DownloadRequestException e) {
e.printStackTrace();
DBWriter.addDownloadStatus(
@@ -206,19 +190,6 @@ public final class DBTasks {
}
/**
- * Refresh a specific Feed. The refresh may get canceled if the feed does not seem to be modified
- * and the last update was only few days ago.
- *
- * @param context Used for requesting the download.
- * @param feed The Feed object.
- */
- private static void refreshFeed(Context context, Feed feed)
- throws DownloadRequestException {
- Log.d(TAG, "refreshFeed(feed.id: " + feed.getId() +")");
- refreshFeed(context, feed, false, false, false);
- }
-
- /**
* Refresh a specific feed even if feed has not been modified since last refresh
*
* @param context Used for requesting the download.
@@ -226,26 +197,32 @@ public final class DBTasks {
*/
public static void forceRefreshFeed(Context context, Feed feed, boolean initiatedByUser)
throws DownloadRequestException {
- Log.d(TAG, "refreshFeed(feed.id: " + feed.getId() +")");
- refreshFeed(context, feed, false, true, initiatedByUser);
+ Log.d(TAG, "refreshFeed(feed.id: " + feed.getId() + ")");
+ refreshFeeds(context, Collections.singletonList(feed), false, true, initiatedByUser);
}
- private static void refreshFeed(Context context, Feed feed, boolean loadAllPages, boolean force, boolean initiatedByUser)
- throws DownloadRequestException {
- Feed f;
- String lastUpdate = feed.hasLastUpdateFailed() ? null : feed.getLastUpdate();
- if (feed.getPreferences() == null) {
- f = new Feed(feed.getDownload_url(), lastUpdate, feed.getTitle());
- } else {
- f = new Feed(feed.getDownload_url(), lastUpdate, feed.getTitle(),
- feed.getPreferences().getUsername(), feed.getPreferences().getPassword());
+ private static void refreshFeeds(Context context, List<Feed> feeds, boolean loadAllPages,
+ boolean force, boolean initiatedByUser) throws DownloadRequestException {
+ List<Feed> localFeeds = new ArrayList<>();
+ List<Feed> normalFeeds = new ArrayList<>();
+
+ for (Feed feed : feeds) {
+ if (feed.isLocalFeed()) {
+ localFeeds.add(feed);
+ } else {
+ normalFeeds.add(feed);
+ }
}
- f.setId(feed.getId());
- if (f.isLocalFeed()) {
- new Thread(() -> LocalFeedUpdater.updateFeed(f, context)).start();
- } else {
- DownloadRequester.getInstance().downloadFeed(context, f, loadAllPages, force, initiatedByUser);
+ if (!localFeeds.isEmpty()) {
+ new Thread(() -> {
+ for (Feed feed : localFeeds) {
+ LocalFeedUpdater.updateFeed(feed, context);
+ }
+ }).start();
+ }
+ if (!normalFeeds.isEmpty()) {
+ DownloadRequester.getInstance().downloadFeeds(context, feeds, loadAllPages, force, initiatedByUser);
}
}
@@ -278,7 +255,7 @@ public final class DBTasks {
}
/**
- * Looks for undownloaded episodes in the queue or list of unread items and request a download if
+ * Looks for non-downloaded episodes in the queue or list of unread items and request a download if
* 1. Network is available
* 2. The device is charging or the user allows auto download on battery
* 3. There is free space in the episode cache
@@ -289,9 +266,15 @@ public final class DBTasks {
*/
public static Future<?> autodownloadUndownloadedItems(final Context context) {
Log.d(TAG, "autodownloadUndownloadedItems");
- return autodownloadExec.submit(ClientConfig.automaticDownloadAlgorithm
- .autoDownloadUndownloadedItems(context));
+ return autodownloadExec.submit(downloadAlgorithm.autoDownloadUndownloadedItems(context));
+ }
+ /**
+ * For testing purpose only.
+ */
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ public static void setDownloadAlgorithm(AutomaticDownloadAlgorithm newDownloadAlgorithm) {
+ downloadAlgorithm = newDownloadAlgorithm;
}
/**
@@ -337,7 +320,7 @@ public final class DBTasks {
private static Feed searchFeedByIdentifyingValueOrID(PodDBAdapter adapter,
Feed feed) {
if (feed.getId() != 0) {
- return DBReader.getFeed(feed.getId(), adapter);
+ return DBReader.getFeed(feed.getId());
} else {
List<Feed> feeds = DBReader.getFeedList();
for (Feed f : feeds) {
@@ -534,7 +517,7 @@ public final class DBTasks {
List<Feed> items = new ArrayList<>();
if (cursor.moveToFirst()) {
do {
- items.add(Feed.fromCursor(cursor));
+ items.add(FeedCursorMapper.convert(cursor));
} while (cursor.moveToNext());
}
setResult(items);
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBUpgrader.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBUpgrader.java
index 622389ed8..4e2eb6e5a 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBUpgrader.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBUpgrader.java
@@ -312,6 +312,14 @@ class DBUpgrader {
}
if (oldVersion < 2020000) {
db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
+ + " ADD COLUMN " + PodDBAdapter.KEY_EPISODE_NOTIFICATION + " INTEGER DEFAULT 0;");
+ }
+ if (oldVersion < 2030000) {
+ db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS
+ + " SET " + PodDBAdapter.KEY_DESCRIPTION + " = content_encoded, content_encoded = NULL "
+ + "WHERE length(" + PodDBAdapter.KEY_DESCRIPTION + ") < length(content_encoded)");
+ db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + " SET content_encoded = NULL");
+ db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
+ " ADD COLUMN " + PodDBAdapter.KEY_FEED_TAGS + " TEXT;");
}
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java
index 84cc4b6a8..a86bdaa65 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java
@@ -48,6 +48,7 @@ import de.danoeh.antennapod.core.util.LongList;
import de.danoeh.antennapod.core.util.Permutor;
import de.danoeh.antennapod.core.util.SortOrder;
import de.danoeh.antennapod.core.util.playback.Playable;
+import de.danoeh.antennapod.core.util.playback.PlayableUtils;
/**
* Provides methods for writing data to AntennaPod's database.
@@ -382,7 +383,7 @@ public class DBWriter {
List<FeedItem> updatedItems = new ArrayList<>();
ItemEnqueuePositionCalculator positionCalculator =
new ItemEnqueuePositionCalculator(UserPreferences.getEnqueueLocation());
- Playable currentlyPlaying = Playable.PlayableUtils.createInstanceFromPreferences(context);
+ Playable currentlyPlaying = PlayableUtils.createInstanceFromPreferences(context);
int insertPosition = positionCalculator.calcPosition(queue, currentlyPlaying);
for (long itemId : itemIds) {
if (!itemListContains(queue, itemId)) {
@@ -789,6 +790,7 @@ public class DBWriter {
adapter.open();
adapter.setFeedItemlist(items);
adapter.close();
+ EventBus.getDefault().post(FeedItemEvent.updated(items));
});
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java
index e3121caa2..638c1bef5 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java
@@ -17,6 +17,7 @@ import org.apache.commons.io.FilenameUtils;
import java.io.File;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -184,16 +185,31 @@ public class DownloadRequester implements DownloadStateProvider {
}
/**
- * Downloads a feed
+ * Downloads a feed.
*
* @param context The application's environment.
- * @param feed Feed to download
+ * @param feed Feeds to download
* @param loadAllPages Set to true to download all pages
*/
public synchronized void downloadFeed(Context context, Feed feed, boolean loadAllPages,
- boolean force, boolean initiatedByUser)
- throws DownloadRequestException {
- if (feedFileValid(feed)) {
+ boolean force, boolean initiatedByUser) throws DownloadRequestException {
+ downloadFeeds(context, Collections.singletonList(feed), loadAllPages, force, initiatedByUser);
+ }
+
+ /**
+ * Downloads a list of feeds.
+ *
+ * @param context The application's environment.
+ * @param feeds Feeds to download
+ * @param loadAllPages Set to true to download all pages
+ */
+ public synchronized void downloadFeeds(Context context, List<Feed> feeds, boolean loadAllPages,
+ boolean force, boolean initiatedByUser) throws DownloadRequestException {
+ List<DownloadRequest> requests = new ArrayList<>();
+ for (Feed feed : feeds) {
+ if (!feedFileValid(feed)) {
+ continue;
+ }
String username = (feed.getPreferences() != null) ? feed.getPreferences().getUsername() : null;
String password = (feed.getPreferences() != null) ? feed.getPreferences().getPassword() : null;
String lastModified = feed.isPaged() || force ? null : feed.getLastUpdate();
@@ -206,9 +222,12 @@ public class DownloadRequester implements DownloadStateProvider {
true, username, password, lastModified, true, args, initiatedByUser
);
if (request != null) {
- download(context, request);
+ requests.add(request);
}
}
+ if (!requests.isEmpty()) {
+ download(context, requests.toArray(new DownloadRequest[0]));
+ }
}
public synchronized void downloadFeed(Context context, Feed feed) throws DownloadRequestException {
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/ExceptFavoriteCleanupAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/ExceptFavoriteCleanupAlgorithm.java
new file mode 100644
index 000000000..f0788db33
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/ExceptFavoriteCleanupAlgorithm.java
@@ -0,0 +1,99 @@
+package de.danoeh.antennapod.core.storage;
+
+import android.content.Context;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.ExecutionException;
+
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+
+/**
+ * A cleanup algorithm that removes any item that isn't a favorite but only if space is needed.
+ */
+public class ExceptFavoriteCleanupAlgorithm extends EpisodeCleanupAlgorithm {
+
+ private static final String TAG = "ExceptFavCleanupAlgo";
+
+ /**
+ * The maximum number of episodes that could be cleaned up.
+ *
+ * @return the number of episodes that *could* be cleaned up, if needed
+ */
+ public int getReclaimableItems() {
+ return getCandidates().size();
+ }
+
+ @Override
+ public int performCleanup(Context context, int numberOfEpisodesToDelete) {
+ List<FeedItem> candidates = getCandidates();
+ List<FeedItem> delete;
+
+ // in the absence of better data, we'll sort by item publication date
+ Collections.sort(candidates, (lhs, rhs) -> {
+ Date l = lhs.getPubDate();
+ Date r = rhs.getPubDate();
+
+ if (l != null && r != null) {
+ return l.compareTo(r);
+ } else {
+ // No date - compare by id which should be always incremented
+ return Long.compare(lhs.getId(), rhs.getId());
+ }
+ });
+
+ if (candidates.size() > numberOfEpisodesToDelete) {
+ delete = candidates.subList(0, numberOfEpisodesToDelete);
+ } else {
+ delete = candidates;
+ }
+
+ for (FeedItem item : delete) {
+ try {
+ DBWriter.deleteFeedMediaOfItem(context, item.getMedia().getId()).get();
+ } catch (InterruptedException | ExecutionException e) {
+ e.printStackTrace();
+ }
+ }
+
+ int counter = delete.size();
+ Log.i(TAG, String.format(Locale.US,
+ "Auto-delete deleted %d episodes (%d requested)", counter,
+ numberOfEpisodesToDelete));
+
+ return counter;
+ }
+
+ @NonNull
+ private List<FeedItem> getCandidates() {
+ List<FeedItem> candidates = new ArrayList<>();
+ List<FeedItem> downloadedItems = DBReader.getDownloadedItems();
+ for (FeedItem item : downloadedItems) {
+ if (item.hasMedia()
+ && item.getMedia().isDownloaded()
+ && !item.isTagged(FeedItem.TAG_FAVORITE)) {
+ candidates.add(item);
+ }
+ }
+ return candidates;
+ }
+
+ @Override
+ public int getDefaultCleanupParameter() {
+ int cacheSize = UserPreferences.getEpisodeCacheSize();
+ if (cacheSize != UserPreferences.getEpisodeCacheSizeUnlimited()) {
+ int downloadedEpisodes = DBReader.getNumberOfDownloadedEpisodes();
+ if (downloadedEpisodes > cacheSize) {
+ return downloadedEpisodes - cacheSize;
+ }
+ }
+ return 0;
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java
index 8f47675a8..98d5e6310 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java
@@ -15,7 +15,9 @@ import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import de.danoeh.antennapod.core.storage.mapper.FeedItemFilterQuery;
import org.apache.commons.io.FileUtils;
import java.io.File;
@@ -30,6 +32,7 @@ import java.util.Set;
import de.danoeh.antennapod.core.feed.Chapter;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.feed.FeedItemFilter;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.FeedPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
@@ -49,7 +52,7 @@ public class PodDBAdapter {
private static final String TAG = "PodDBAdapter";
public static final String DATABASE_NAME = "Antennapod.db";
- public static final int VERSION = 2020000;
+ public static final int VERSION = 2030000;
/**
* Maximum number of arguments for IN-operator.
@@ -81,7 +84,6 @@ public class PodDBAdapter {
public static final String KEY_FEEDFILETYPE = "feedfile_type";
public static final String KEY_COMPLETION_DATE = "completion_date";
public static final String KEY_FEEDITEM = "feeditem";
- public static final String KEY_CONTENT_ENCODED = "content_encoded";
public static final String KEY_PAYMENT_LINK = "payment_link";
public static final String KEY_START = "start";
public static final String KEY_LANGUAGE = "language";
@@ -114,16 +116,17 @@ public class PodDBAdapter {
public static final String KEY_FEED_SKIP_INTRO = "feed_skip_intro";
public static final String KEY_FEED_SKIP_ENDING = "feed_skip_ending";
public static final String KEY_FEED_TAGS = "tags";
+ public static final String KEY_EPISODE_NOTIFICATION = "episode_notification";
// Table names
- static final String TABLE_NAME_FEEDS = "Feeds";
- static final String TABLE_NAME_FEED_ITEMS = "FeedItems";
- static final String TABLE_NAME_FEED_IMAGES = "FeedImages";
- static final String TABLE_NAME_FEED_MEDIA = "FeedMedia";
- static final String TABLE_NAME_DOWNLOAD_LOG = "DownloadLog";
- static final String TABLE_NAME_QUEUE = "Queue";
- static final String TABLE_NAME_SIMPLECHAPTERS = "SimpleChapters";
- static final String TABLE_NAME_FAVORITES = "Favorites";
+ public static final String TABLE_NAME_FEEDS = "Feeds";
+ public static final String TABLE_NAME_FEED_ITEMS = "FeedItems";
+ public static final String TABLE_NAME_FEED_IMAGES = "FeedImages";
+ public static final String TABLE_NAME_FEED_MEDIA = "FeedMedia";
+ public static final String TABLE_NAME_DOWNLOAD_LOG = "DownloadLog";
+ public static final String TABLE_NAME_QUEUE = "Queue";
+ public static final String TABLE_NAME_SIMPLECHAPTERS = "SimpleChapters";
+ public static final String TABLE_NAME_FAVORITES = "Favorites";
// SQL Statements for creating new tables
private static final String TABLE_PRIMARY_KEY = KEY_ID
@@ -152,12 +155,13 @@ public class PodDBAdapter {
+ KEY_FEED_VOLUME_ADAPTION + " INTEGER DEFAULT 0,"
+ KEY_FEED_TAGS + " TEXT,"
+ KEY_FEED_SKIP_INTRO + " INTEGER DEFAULT 0,"
- + KEY_FEED_SKIP_ENDING + " INTEGER DEFAULT 0)";
+ + KEY_FEED_SKIP_ENDING + " INTEGER DEFAULT 0,"
+ + KEY_EPISODE_NOTIFICATION + " INTEGER DEFAULT 0)";
private static final String CREATE_TABLE_FEED_ITEMS = "CREATE TABLE "
- + TABLE_NAME_FEED_ITEMS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE
- + " TEXT," + KEY_CONTENT_ENCODED + " TEXT," + KEY_PUBDATE
- + " INTEGER," + KEY_READ + " INTEGER," + KEY_LINK + " TEXT,"
+ + TABLE_NAME_FEED_ITEMS + " (" + TABLE_PRIMARY_KEY
+ + KEY_TITLE + " TEXT," + KEY_PUBDATE + " INTEGER,"
+ + KEY_READ + " INTEGER," + KEY_LINK + " TEXT,"
+ KEY_DESCRIPTION + " TEXT," + KEY_PAYMENT_LINK + " TEXT,"
+ KEY_MEDIA + " INTEGER," + KEY_FEED + " INTEGER,"
+ KEY_HAS_CHAPTERS + " INTEGER," + KEY_ITEM_IDENTIFIER + " TEXT,"
@@ -255,7 +259,8 @@ public class PodDBAdapter {
TABLE_NAME_FEEDS + "." + KEY_FEED_PLAYBACK_SPEED,
TABLE_NAME_FEEDS + "." + KEY_FEED_TAGS,
TABLE_NAME_FEEDS + "." + KEY_FEED_SKIP_INTRO,
- TABLE_NAME_FEEDS + "." + KEY_FEED_SKIP_ENDING
+ TABLE_NAME_FEEDS + "." + KEY_FEED_SKIP_ENDING,
+ TABLE_NAME_FEEDS + "." + KEY_EPISODE_NOTIFICATION
};
/**
@@ -308,8 +313,7 @@ public class PodDBAdapter {
private static final String SELECT_FEED_ITEMS_AND_MEDIA_WITH_DESCRIPTION =
"SELECT " + KEYS_FEED_ITEM_WITHOUT_DESCRIPTION + ", " + KEYS_FEED_MEDIA + ", "
- + TABLE_NAME_FEED_ITEMS + "." + KEY_DESCRIPTION + ", "
- + TABLE_NAME_FEED_ITEMS + "." + KEY_CONTENT_ENCODED
+ + TABLE_NAME_FEED_ITEMS + "." + KEY_DESCRIPTION
+ " FROM " + TABLE_NAME_FEED_ITEMS
+ JOIN_FEED_ITEM_AND_MEDIA;
private static final String SELECT_FEED_ITEMS_AND_MEDIA =
@@ -370,6 +374,7 @@ public class PodDBAdapter {
* For more information see
* <a href="https://github.com/robolectric/robolectric/issues/1890">robolectric/robolectric#1890</a>.</p>
*/
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
public static void tearDownTests() {
getInstance().dbHelper.close();
instance = null;
@@ -448,6 +453,7 @@ public class PodDBAdapter {
values.put(KEY_FEED_TAGS, prefs.getTagsAsString());
values.put(KEY_FEED_SKIP_INTRO, prefs.getFeedSkipIntro());
values.put(KEY_FEED_SKIP_ENDING, prefs.getFeedSkipEnding());
+ values.put(KEY_EPISODE_NOTIFICATION, prefs.getShowEpisodeNotification());
db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(prefs.getFeedID())});
}
@@ -623,9 +629,6 @@ public class PodDBAdapter {
if (item.getDescription() != null) {
values.put(KEY_DESCRIPTION, item.getDescription());
}
- if (item.getContentEncoded() != null) {
- values.put(KEY_CONTENT_ENCODED, item.getContentEncoded());
- }
values.put(KEY_PUBDATE, item.getPubDate().getTime());
values.put(KEY_PAYMENT_LINK, item.getPaymentLink());
if (saveFeed && item.getFeed() != null) {
@@ -947,9 +950,12 @@ public class PodDBAdapter {
* @param feed The feed you want to get the FeedItems from.
* @return The cursor of the query
*/
- public final Cursor getAllItemsOfFeedCursor(final Feed feed) {
+ public final Cursor getItemsOfFeedCursor(final Feed feed, FeedItemFilter filter) {
+ String filterQuery = FeedItemFilterQuery.generateFrom(filter);
+ String whereClauseAnd = "".equals(filterQuery) ? "" : " AND " + filterQuery;
final String query = SELECT_FEED_ITEMS_AND_MEDIA
- + " WHERE " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + feed.getId();
+ + " WHERE " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + feed.getId()
+ + whereClauseAnd;
return db.rawQuery(query, null);
}
@@ -957,7 +963,7 @@ public class PodDBAdapter {
* Return the description and content_encoded of item
*/
public final Cursor getDescriptionOfItem(final FeedItem item) {
- final String query = "SELECT " + KEY_DESCRIPTION + ", " + KEY_CONTENT_ENCODED
+ final String query = "SELECT " + KEY_DESCRIPTION
+ " FROM " + TABLE_NAME_FEED_ITEMS
+ " WHERE " + KEY_ID + "=" + item.getId();
return db.rawQuery(query, null);
@@ -1048,9 +1054,11 @@ public class PodDBAdapter {
return db.rawQuery(query, null);
}
- public final Cursor getRecentlyPublishedItemsCursor(int offset, int limit) {
- final String query = SELECT_FEED_ITEMS_AND_MEDIA
- + "ORDER BY " + KEY_PUBDATE + " DESC LIMIT " + offset + ", " + limit;
+ public final Cursor getRecentlyPublishedItemsCursor(int offset, int limit, FeedItemFilter filter) {
+ String filterQuery = FeedItemFilterQuery.generateFrom(filter);
+ String whereClause = "".equals(filterQuery) ? "" : " WHERE " + filterQuery;
+ final String query = SELECT_FEED_ITEMS_AND_MEDIA + whereClause
+ + " ORDER BY " + KEY_PUBDATE + " DESC LIMIT " + offset + ", " + limit;
return db.rawQuery(query, null);
}
@@ -1164,6 +1172,11 @@ public class PodDBAdapter {
public final LongIntMap getFeedCounters(long... feedIds) {
int setting = UserPreferences.getFeedCounterSetting();
+
+ return getFeedCounters(setting, feedIds);
+ }
+
+ public final LongIntMap getFeedCounters(int setting, long... feedIds) {
String whereRead;
switch (setting) {
case UserPreferences.FEED_COUNTER_SHOW_NEW_UNPLAYED_SUM:
@@ -1188,24 +1201,26 @@ public class PodDBAdapter {
}
private LongIntMap conditionalFeedCounterRead(String whereRead, long... feedIds) {
- // work around TextUtils.join wanting only boxed items
- // and StringUtils.join() causing NoSuchMethodErrors on MIUI
- StringBuilder builder = new StringBuilder();
- for (long id : feedIds) {
- builder.append(id);
- builder.append(',');
- }
+ String limitFeeds = "";
if (feedIds.length > 0) {
+ // work around TextUtils.join wanting only boxed items
+ // and StringUtils.join() causing NoSuchMethodErrors on MIUI
+ StringBuilder builder = new StringBuilder();
+ for (long id : feedIds) {
+ builder.append(id);
+ builder.append(',');
+ }
// there's an extra ',', get rid of it
builder.deleteCharAt(builder.length() - 1);
+ limitFeeds = KEY_FEED + " IN (" + builder.toString() + ") AND ";
}
final String query = "SELECT " + KEY_FEED + ", COUNT(" + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + ") AS count "
+ " FROM " + TABLE_NAME_FEED_ITEMS
+ " LEFT JOIN " + TABLE_NAME_FEED_MEDIA + " ON "
+ TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "=" + TABLE_NAME_FEED_MEDIA + "." + KEY_FEEDITEM
- + " WHERE " + KEY_FEED + " IN (" + builder.toString() + ") "
- + " AND " + whereRead + " GROUP BY " + KEY_FEED;
+ + " WHERE " + limitFeeds + " "
+ + whereRead + " GROUP BY " + KEY_FEED;
Cursor c = db.rawQuery(query, null);
LongIntMap result = new LongIntMap(c.getCount());
@@ -1301,8 +1316,6 @@ public class PodDBAdapter {
.append("(")
.append(KEY_DESCRIPTION + " LIKE '%").append(queryWords[i])
.append("%' OR ")
- .append(KEY_CONTENT_ENCODED).append(" LIKE '%").append(queryWords[i])
- .append("%' OR ")
.append(KEY_TITLE).append(" LIKE '%").append(queryWords[i])
.append("%') ");
@@ -1368,7 +1381,16 @@ public class PodDBAdapter {
}
/**
- * Called when a database corruption happens
+ * Insert raw data to the database. *
+ * Call method only for unit tests.
+ */
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ public void insertTestData(@NonNull String table, @NonNull ContentValues values) {
+ db.insert(table, null, values);
+ }
+
+ /**
+ * Called when a database corruption happens.
*/
public static class PodDbErrorHandler implements DatabaseErrorHandler {
@Override
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedCursorMapper.java b/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedCursorMapper.java
new file mode 100644
index 000000000..783fba596
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedCursorMapper.java
@@ -0,0 +1,70 @@
+package de.danoeh.antennapod.core.storage.mapper;
+
+import android.database.Cursor;
+
+import androidx.annotation.NonNull;
+
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.feed.FeedPreferences;
+import de.danoeh.antennapod.core.storage.PodDBAdapter;
+import de.danoeh.antennapod.core.util.SortOrder;
+
+/**
+ * Converts a {@link Cursor} to a {@link Feed} object.
+ */
+public abstract class FeedCursorMapper {
+
+ /**
+ * Create a {@link Feed} instance from a database row (cursor).
+ */
+ @NonNull
+ public static Feed convert(@NonNull Cursor cursor) {
+ int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID);
+ int indexLastUpdate = cursor.getColumnIndex(PodDBAdapter.KEY_LASTUPDATE);
+ int indexTitle = cursor.getColumnIndex(PodDBAdapter.KEY_TITLE);
+ int indexCustomTitle = cursor.getColumnIndex(PodDBAdapter.KEY_CUSTOM_TITLE);
+ int indexLink = cursor.getColumnIndex(PodDBAdapter.KEY_LINK);
+ int indexDescription = cursor.getColumnIndex(PodDBAdapter.KEY_DESCRIPTION);
+ int indexPaymentLink = cursor.getColumnIndex(PodDBAdapter.KEY_PAYMENT_LINK);
+ int indexAuthor = cursor.getColumnIndex(PodDBAdapter.KEY_AUTHOR);
+ int indexLanguage = cursor.getColumnIndex(PodDBAdapter.KEY_LANGUAGE);
+ int indexType = cursor.getColumnIndex(PodDBAdapter.KEY_TYPE);
+ int indexFeedIdentifier = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_IDENTIFIER);
+ int indexFileUrl = cursor.getColumnIndex(PodDBAdapter.KEY_FILE_URL);
+ int indexDownloadUrl = cursor.getColumnIndex(PodDBAdapter.KEY_DOWNLOAD_URL);
+ int indexDownloaded = cursor.getColumnIndex(PodDBAdapter.KEY_DOWNLOADED);
+ int indexIsPaged = cursor.getColumnIndex(PodDBAdapter.KEY_IS_PAGED);
+ int indexNextPageLink = cursor.getColumnIndex(PodDBAdapter.KEY_NEXT_PAGE_LINK);
+ int indexHide = cursor.getColumnIndex(PodDBAdapter.KEY_HIDE);
+ int indexSortOrder = cursor.getColumnIndex(PodDBAdapter.KEY_SORT_ORDER);
+ int indexLastUpdateFailed = cursor.getColumnIndex(PodDBAdapter.KEY_LAST_UPDATE_FAILED);
+ int indexImageUrl = cursor.getColumnIndex(PodDBAdapter.KEY_IMAGE_URL);
+
+ Feed feed = new Feed(
+ cursor.getLong(indexId),
+ cursor.getString(indexLastUpdate),
+ cursor.getString(indexTitle),
+ cursor.getString(indexCustomTitle),
+ cursor.getString(indexLink),
+ cursor.getString(indexDescription),
+ cursor.getString(indexPaymentLink),
+ cursor.getString(indexAuthor),
+ cursor.getString(indexLanguage),
+ cursor.getString(indexType),
+ cursor.getString(indexFeedIdentifier),
+ cursor.getString(indexImageUrl),
+ cursor.getString(indexFileUrl),
+ cursor.getString(indexDownloadUrl),
+ cursor.getInt(indexDownloaded) > 0,
+ cursor.getInt(indexIsPaged) > 0,
+ cursor.getString(indexNextPageLink),
+ cursor.getString(indexHide),
+ SortOrder.fromCodeString(cursor.getString(indexSortOrder)),
+ cursor.getInt(indexLastUpdateFailed) > 0
+ );
+
+ FeedPreferences preferences = FeedPreferences.fromCursor(cursor);
+ feed.setPreferences(preferences);
+ return feed;
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedItemFilterQuery.java b/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedItemFilterQuery.java
new file mode 100644
index 000000000..f6963b5ac
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedItemFilterQuery.java
@@ -0,0 +1,76 @@
+package de.danoeh.antennapod.core.storage.mapper;
+
+import de.danoeh.antennapod.core.feed.FeedItemFilter;
+import de.danoeh.antennapod.core.storage.PodDBAdapter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class FeedItemFilterQuery {
+ private FeedItemFilterQuery() {
+ // Must not be instantiated
+ }
+
+ /**
+ * Express the filter using an SQL boolean statement that can be inserted into an SQL WHERE clause
+ * to yield output filtered according to the rules of this filter.
+ *
+ * @return An SQL boolean statement that matches the desired items,
+ * empty string if there is nothing to filter
+ */
+ public static String generateFrom(FeedItemFilter filter) {
+ // The keys used within this method, but explicitly combined with their table
+ String keyRead = PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + PodDBAdapter.KEY_READ;
+ String keyPosition = PodDBAdapter.TABLE_NAME_FEED_MEDIA + "." + PodDBAdapter.KEY_POSITION;
+ String keyDownloaded = PodDBAdapter.TABLE_NAME_FEED_MEDIA + "." + PodDBAdapter.KEY_DOWNLOADED;
+ String keyMediaId = PodDBAdapter.TABLE_NAME_FEED_MEDIA + "." + PodDBAdapter.KEY_ID;
+ String keyItemId = PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + PodDBAdapter.KEY_ID;
+ String keyFeedItem = PodDBAdapter.KEY_FEEDITEM;
+ String tableQueue = PodDBAdapter.TABLE_NAME_QUEUE;
+ String tableFavorites = PodDBAdapter.TABLE_NAME_FAVORITES;
+
+ List<String> statements = new ArrayList<>();
+ if (filter.showPlayed) {
+ statements.add(keyRead + " = 1 ");
+ } else if (filter.showUnplayed) {
+ statements.add(" NOT " + keyRead + " = 1 "); // Match "New" items (read = -1) as well
+ }
+ if (filter.showPaused) {
+ statements.add(" (" + keyPosition + " NOT NULL AND " + keyPosition + " > 0 " + ") ");
+ } else if (filter.showNotPaused) {
+ statements.add(" (" + keyPosition + " IS NULL OR " + keyPosition + " = 0 " + ") ");
+ }
+ if (filter.showQueued) {
+ statements.add(keyItemId + " IN (SELECT " + keyFeedItem + " FROM " + tableQueue + ") ");
+ } else if (filter.showNotQueued) {
+ statements.add(keyItemId + " NOT IN (SELECT " + keyFeedItem + " FROM " + tableQueue + ") ");
+ }
+ if (filter.showDownloaded) {
+ statements.add(keyDownloaded + " = 1 ");
+ } else if (filter.showNotDownloaded) {
+ statements.add(keyDownloaded + " = 0 ");
+ }
+ if (filter.showHasMedia) {
+ statements.add(keyMediaId + " NOT NULL ");
+ } else if (filter.showNoMedia) {
+ statements.add(keyMediaId + " IS NULL ");
+ }
+ if (filter.showIsFavorite) {
+ statements.add(keyItemId + " IN (SELECT " + keyFeedItem + " FROM " + tableFavorites + ") ");
+ } else if (filter.showNotFavorite) {
+ statements.add(keyItemId + " NOT IN (SELECT " + keyFeedItem + " FROM " + tableFavorites + ") ");
+ }
+
+ if (statements.isEmpty()) {
+ return "";
+ }
+
+ StringBuilder query = new StringBuilder(" (" + statements.get(0));
+ for (String r : statements.subList(1, statements.size())) {
+ query.append(" AND ");
+ query.append(r);
+ }
+ query.append(") ");
+ return query.toString();
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java b/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java
index 7563ab715..670a65e44 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java
@@ -80,7 +80,7 @@ public class SyncService extends Worker {
if (!GpodnetPreferences.loggedIn()) {
return Result.success();
}
- syncServiceImpl = new GpodnetService(AntennapodHttpClient.getHttpClient(), GpodnetPreferences.getHostname());
+ syncServiceImpl = new GpodnetService(AntennapodHttpClient.getHttpClient(), GpodnetPreferences.getHosturl());
SharedPreferences.Editor prefs = getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
.edit();
prefs.putLong(PREF_LAST_SYNC_ATTEMPT_TIMESTAMP, System.currentTimeMillis()).apply();
@@ -474,6 +474,7 @@ public class SyncService extends Worker {
}
}
DBWriter.removeQueueItem(getApplicationContext(), false, queueToBeRemoved.toArray());
+ DBReader.loadAdditionalFeedItemListData(updatedItems);
DBWriter.setItemList(updatedItems);
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/gpoddernet/GpodnetService.java b/core/src/main/java/de/danoeh/antennapod/core/sync/gpoddernet/GpodnetService.java
index 62c8ce5f3..cecfc0d2c 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/sync/gpoddernet/GpodnetService.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/sync/gpoddernet/GpodnetService.java
@@ -2,6 +2,7 @@ package de.danoeh.antennapod.core.sync.gpoddernet;
import android.util.Log;
import androidx.annotation.NonNull;
+import de.danoeh.antennapod.core.BuildConfig;
import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetDevice;
import de.danoeh.antennapod.core.sync.model.EpisodeAction;
@@ -36,27 +37,63 @@ import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
/**
* Communicates with the gpodder.net service.
*/
public class GpodnetService implements ISyncService {
public static final String TAG = "GpodnetService";
public static final String DEFAULT_BASE_HOST = "gpodder.net";
- private static final String BASE_SCHEME = "https";
private static final int UPLOAD_BULK_SIZE = 30;
private static final MediaType TEXT = MediaType.parse("plain/text; charset=utf-8");
private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
- private final String baseHost;
+ private String baseScheme;
+ private String baseHost;
+ private int basePort;
+
private final OkHttpClient httpClient;
private String username = null;
- public GpodnetService(OkHttpClient httpClient, String baseHost) {
+ // split into schema, host and port - missing parts are null
+ private static Pattern urlsplit_regex = Pattern.compile("(?:(https?)://)?([^:]+)(?::(\\d+))?");
+
+ public GpodnetService(OkHttpClient httpClient, String baseHosturl) {
this.httpClient = httpClient;
- this.baseHost = baseHost;
+
+ Matcher m = urlsplit_regex.matcher(baseHosturl);
+ if (m.matches()) {
+ this.baseScheme = m.group(1);
+ this.baseHost = m.group(2);
+ if (m.group(3) == null) {
+ this.basePort = -1;
+ } else {
+ this.basePort = Integer.parseInt(m.group(3)); // regex -> can only be digits
+ }
+ } else {
+ // URL does not match regex: use it anyway -> this will cause an exception on connect
+ this.baseScheme = "https";
+ this.baseHost = baseHosturl;
+ this.basePort = 443;
+ }
+
+ if (this.baseScheme == null) { // assume https
+ this.baseScheme = "https";
+ }
+
+ if (this.baseScheme.equals("https") && this.basePort == -1) {
+ this.basePort = 443;
+ }
+
+ if (this.baseScheme.equals("http") && this.basePort == -1) {
+ this.basePort = 80;
+ }
}
private void requireLoggedIn() {
@@ -71,7 +108,8 @@ public class GpodnetService implements ISyncService {
public List<GpodnetTag> getTopTags(int count) throws GpodnetServiceException {
URL url;
try {
- url = new URI(BASE_SCHEME, baseHost, String.format(Locale.US, "/api/2/tags/%d.json", count), null).toURL();
+ url = new URI(baseScheme, null, baseHost, basePort,
+ String.format(Locale.US, "/api/2/tags/%d.json", count), null, null).toURL();
} catch (MalformedURLException | URISyntaxException e) {
e.printStackTrace();
throw new GpodnetServiceException(e);
@@ -104,8 +142,8 @@ public class GpodnetService implements ISyncService {
public List<GpodnetPodcast> getPodcastsForTag(@NonNull GpodnetTag tag, int count)
throws GpodnetServiceException {
try {
- URL url = new URI(BASE_SCHEME, baseHost, String.format(Locale.US,
- "/api/2/tag/%s/%d.json", tag.getTag(), count), null).toURL();
+ URL url = new URI(baseScheme, null, baseHost, basePort,
+ String.format(Locale.US, "/api/2/tag/%s/%d.json", tag.getTag(), count), null, null).toURL();
Request.Builder request = new Request.Builder().url(url);
String response = executeRequest(request);
@@ -130,7 +168,8 @@ public class GpodnetService implements ISyncService {
}
try {
- URL url = new URI(BASE_SCHEME, baseHost, String.format(Locale.US, "/toplist/%d.json", count), null).toURL();
+ URL url = new URI(baseScheme, null, baseHost, basePort,
+ String.format(Locale.US, "/toplist/%d.json", count), null, null).toURL();
Request.Builder request = new Request.Builder().url(url);
String response = executeRequest(request);
@@ -161,8 +200,8 @@ public class GpodnetService implements ISyncService {
}
try {
- URL url = new URI(BASE_SCHEME, baseHost,
- String.format(Locale.US, "/suggestions/%d.json", count), null).toURL();
+ URL url = new URI(baseScheme, null, baseHost, basePort,
+ String.format(Locale.US, "/suggestions/%d.json", count), null, null).toURL();
Request.Builder request = new Request.Builder().url(url);
String response = executeRequest(request);
@@ -187,7 +226,7 @@ public class GpodnetService implements ISyncService {
.format(Locale.US, "q=%s&scale_logo=%d", query, scaledLogoSize) : String
.format("q=%s", query);
try {
- URL url = new URI(BASE_SCHEME, null, baseHost, -1, "/search.json",
+ URL url = new URI(baseScheme, null, baseHost, basePort, "/search.json",
parameters, null).toURL();
Request.Builder request = new Request.Builder().url(url);
String response = executeRequest(request);
@@ -214,7 +253,8 @@ public class GpodnetService implements ISyncService {
public List<GpodnetDevice> getDevices() throws GpodnetServiceException {
requireLoggedIn();
try {
- URL url = new URI(BASE_SCHEME, baseHost, String.format("/api/2/devices/%s.json", username), null).toURL();
+ URL url = new URI(baseScheme, null, baseHost, basePort,
+ String.format("/api/2/devices/%s.json", username), null, null).toURL();
Request.Builder request = new Request.Builder().url(url);
String response = executeRequest(request);
JSONArray devicesArray = new JSONArray(response);
@@ -226,6 +266,45 @@ public class GpodnetService implements ISyncService {
}
/**
+ * Returns synchronization status of devices.
+ * <p/>
+ * This method requires authentication.
+ *
+ * @throws GpodnetServiceAuthenticationException If there is an authentication error.
+ */
+ public List<List<String>> getSynchronizedDevices() throws GpodnetServiceException {
+ requireLoggedIn();
+ try {
+ URL url = new URI(baseScheme, null, baseHost, basePort,
+ String.format("/api/2/sync-devices/%s.json", username), null, null).toURL();
+ Request.Builder request = new Request.Builder().url(url);
+ String response = executeRequest(request);
+ JSONObject syncStatus = new JSONObject(response);
+ List<List<String>> result = new ArrayList<>();
+
+ JSONArray synchronizedDevices = syncStatus.getJSONArray("synchronized");
+ for (int i = 0; i < synchronizedDevices.length(); i++) {
+ JSONArray groupDevices = synchronizedDevices.getJSONArray(i);
+ List<String> group = new ArrayList<>();
+ for (int j = 0; j < groupDevices.length(); j++) {
+ group.add(groupDevices.getString(j));
+ }
+ result.add(group);
+ }
+
+ JSONArray notSynchronizedDevices = syncStatus.getJSONArray("not-synchronized");
+ for (int i = 0; i < notSynchronizedDevices.length(); i++) {
+ result.add(Collections.singletonList(notSynchronizedDevices.getString(i)));
+ }
+
+ return result;
+ } catch (JSONException | MalformedURLException | URISyntaxException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException(e);
+ }
+ }
+
+ /**
* Configures the device of a given user.
* <p/>
* This method requires authentication.
@@ -237,8 +316,8 @@ public class GpodnetService implements ISyncService {
throws GpodnetServiceException {
requireLoggedIn();
try {
- URL url = new URI(BASE_SCHEME, baseHost, String.format(
- "/api/2/devices/%s/%s.json", username, deviceId), null).toURL();
+ URL url = new URI(baseScheme, null, baseHost, basePort,
+ String.format("/api/2/devices/%s/%s.json", username, deviceId), null, null).toURL();
String content;
if (caption != null || type != null) {
JSONObject jsonContent = new JSONObject();
@@ -262,6 +341,39 @@ public class GpodnetService implements ISyncService {
}
/**
+ * Links devices for synchronization.
+ * <p/>
+ * This method requires authentication.
+ *
+ * @throws GpodnetServiceAuthenticationException If there is an authentication error.
+ */
+ public void linkDevices(@NonNull List<String> deviceIds) throws GpodnetServiceException {
+ requireLoggedIn();
+ try {
+ final URL url = new URI(baseScheme, null, baseHost, basePort,
+ String.format("/api/2/sync-devices/%s.json", username), null, null).toURL();
+ JSONObject jsonContent = new JSONObject();
+ JSONArray group = new JSONArray();
+ for (String deviceId : deviceIds) {
+ group.put(deviceId);
+ }
+
+ JSONArray synchronizedGroups = new JSONArray();
+ synchronizedGroups.put(group);
+ jsonContent.put("synchronize", synchronizedGroups);
+ jsonContent.put("stop-synchronize", new JSONArray());
+
+ Log.d("aaaa", jsonContent.toString());
+ RequestBody body = RequestBody.create(JSON, jsonContent.toString());
+ Request.Builder request = new Request.Builder().post(body).url(url);
+ executeRequest(request);
+ } catch (JSONException | MalformedURLException | URISyntaxException e) {
+ e.printStackTrace();
+ throw new GpodnetServiceException(e);
+ }
+ }
+
+ /**
* Returns the subscriptions of a specific device.
* <p/>
* This method requires authentication.
@@ -273,8 +385,8 @@ public class GpodnetService implements ISyncService {
public String getSubscriptionsOfDevice(@NonNull String deviceId) throws GpodnetServiceException {
requireLoggedIn();
try {
- URL url = new URI(BASE_SCHEME, baseHost, String.format(
- "/subscriptions/%s/%s.opml", username, deviceId), null).toURL();
+ URL url = new URI(baseScheme, null, baseHost, basePort,
+ String.format("/subscriptions/%s/%s.opml", username, deviceId), null, null).toURL();
Request.Builder request = new Request.Builder().url(url);
return executeRequest(request);
} catch (MalformedURLException | URISyntaxException e) {
@@ -295,7 +407,8 @@ public class GpodnetService implements ISyncService {
public String getSubscriptionsOfUser() throws GpodnetServiceException {
requireLoggedIn();
try {
- URL url = new URI(BASE_SCHEME, baseHost, String.format("/subscriptions/%s.opml", username), null).toURL();
+ URL url = new URI(baseScheme, null, baseHost, basePort,
+ String.format("/subscriptions/%s.opml", username), null, null).toURL();
Request.Builder request = new Request.Builder().url(url);
return executeRequest(request);
} catch (MalformedURLException | URISyntaxException e) {
@@ -319,8 +432,8 @@ public class GpodnetService implements ISyncService {
throws GpodnetServiceException {
requireLoggedIn();
try {
- URL url = new URI(BASE_SCHEME, baseHost, String.format(
- "/subscriptions/%s/%s.txt", username, deviceId), null).toURL();
+ URL url = new URI(baseScheme, null, baseHost, basePort,
+ String.format("/subscriptions/%s/%s.txt", username, deviceId), null, null).toURL();
StringBuilder builder = new StringBuilder();
for (String s : subscriptions) {
builder.append(s);
@@ -353,8 +466,8 @@ public class GpodnetService implements ISyncService {
@NonNull Collection<String> removed) throws GpodnetServiceException {
requireLoggedIn();
try {
- URL url = new URI(BASE_SCHEME, baseHost, String.format(
- "/api/2/subscriptions/%s/%s.json", username, deviceId), null).toURL();
+ URL url = new URI(baseScheme, null, baseHost, basePort,
+ String.format("/api/2/subscriptions/%s/%s.json", username, deviceId), null, null).toURL();
final JSONObject requestObject = new JSONObject();
requestObject.put("add", new JSONArray(added));
@@ -389,8 +502,7 @@ public class GpodnetService implements ISyncService {
String params = String.format(Locale.US, "since=%d", timestamp);
String path = String.format("/api/2/subscriptions/%s/%s.json", username, deviceId);
try {
- URL url = new URI(BASE_SCHEME, null, baseHost, -1, path, params,
- null).toURL();
+ URL url = new URI(baseScheme, null, baseHost, basePort, path, params, null).toURL();
Request.Builder request = new Request.Builder().url(url);
String response = executeRequest(request);
@@ -432,8 +544,8 @@ public class GpodnetService implements ISyncService {
throws SyncServiceException {
try {
Log.d(TAG, "Uploading partial actions " + from + " to " + to + " of " + episodeActions.size());
- URL url = new URI(BASE_SCHEME, baseHost, String.format(
- "/api/2/episodes/%s.json", username), null).toURL();
+ URL url = new URI(baseScheme, null, baseHost, basePort,
+ String.format("/api/2/episodes/%s.json", username), null, null).toURL();
final JSONArray list = new JSONArray();
for (int i = from; i < to; i++) {
@@ -471,7 +583,7 @@ public class GpodnetService implements ISyncService {
String params = String.format(Locale.US, "since=%d", timestamp);
String path = String.format("/api/2/episodes/%s.json", username);
try {
- URL url = new URI(BASE_SCHEME, null, baseHost, -1, path, params, null).toURL();
+ URL url = new URI(baseScheme, null, baseHost, basePort, path, params, null).toURL();
Request.Builder request = new Request.Builder().url(url);
String response = executeRequest(request);
@@ -497,7 +609,8 @@ public class GpodnetService implements ISyncService {
public void authenticate(@NonNull String username, @NonNull String password) throws GpodnetServiceException {
URL url;
try {
- url = new URI(BASE_SCHEME, baseHost, String.format("/api/2/auth/%s/login.json", username), null).toURL();
+ url = new URI(baseScheme, null, baseHost, basePort,
+ String.format("/api/2/auth/%s/login.json", username), null, null).toURL();
} catch (MalformedURLException | URISyntaxException e) {
e.printStackTrace();
throw new GpodnetServiceException(e);
@@ -567,6 +680,13 @@ public class GpodnetService implements ISyncService {
if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) {
throw new GpodnetServiceAuthenticationException("Wrong username or password");
} else {
+ if (BuildConfig.DEBUG) {
+ try {
+ Log.d(TAG, response.body().string());
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
throw new GpodnetServiceBadStatusCodeException("Bad response code: " + responseCode, responseCode);
}
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/UnsupportedFeedtypeException.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/UnsupportedFeedtypeException.java
index 11588967a..c9f9f19c8 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/UnsupportedFeedtypeException.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/UnsupportedFeedtypeException.java
@@ -36,9 +36,6 @@ public class UnsupportedFeedtypeException extends Exception {
if (message != null) {
return message;
} else if (type == TypeGetter.Type.INVALID) {
- if ("html".equals(rootElement)) {
- return "The server returned a website, not a podcast feed";
- }
return "Invalid type";
} else {
return "Type " + type + " not supported";
diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSContent.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSContent.java
index 306b79c15..bedf377aa 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSContent.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSContent.java
@@ -5,23 +5,21 @@ import org.xml.sax.Attributes;
import de.danoeh.antennapod.core.syndication.handler.HandlerState;
public class NSContent extends Namespace {
- public static final String NSTAG = "content";
- public static final String NSURI = "http://purl.org/rss/1.0/modules/content/";
-
- private static final String ENCODED = "encoded";
-
- @Override
- public SyndElement handleElementStart(String localName, HandlerState state,
- Attributes attributes) {
- return new SyndElement(localName, this);
- }
+ public static final String NSTAG = "content";
+ public static final String NSURI = "http://purl.org/rss/1.0/modules/content/";
- @Override
- public void handleElementEnd(String localName, HandlerState state) {
- if (ENCODED.equals(localName) && state.getCurrentItem() != null &&
- state.getContentBuf() != null) {
- state.getCurrentItem().setContentEncoded(state.getContentBuf().toString());
- }
- }
+ private static final String ENCODED = "encoded";
+
+ @Override
+ public SyndElement handleElementStart(String localName, HandlerState state, Attributes attributes) {
+ return new SyndElement(localName, this);
+ }
+
+ @Override
+ public void handleElementEnd(String localName, HandlerState state) {
+ if (ENCODED.equals(localName) && state.getCurrentItem() != null && state.getContentBuf() != null) {
+ state.getCurrentItem().setDescriptionIfLonger(state.getContentBuf().toString());
+ }
+ }
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSITunes.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSITunes.java
index 1e069a1f0..1dc8d8af3 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSITunes.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSITunes.java
@@ -3,9 +3,10 @@ package de.danoeh.antennapod.core.syndication.namespace;
import android.text.TextUtils;
import android.util.Log;
+import androidx.core.text.HtmlCompat;
+
import org.xml.sax.Attributes;
-import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.syndication.handler.HandlerState;
import de.danoeh.antennapod.core.syndication.parsers.DurationParser;
@@ -62,7 +63,8 @@ public class NSITunes extends Namespace {
private void parseAuthor(HandlerState state) {
if (state.getFeed() != null) {
String author = state.getContentBuf().toString();
- state.getFeed().setAuthor(author);
+ state.getFeed().setAuthor(HtmlCompat.fromHtml(author,
+ HtmlCompat.FROM_HTML_MODE_LEGACY).toString());
}
}
@@ -87,7 +89,7 @@ public class NSITunes extends Namespace {
}
if (state.getCurrentItem() != null) {
if (TextUtils.isEmpty(state.getCurrentItem().getDescription())) {
- state.getCurrentItem().setDescription(subtitle);
+ state.getCurrentItem().setDescriptionIfLonger(subtitle);
}
} else {
if (state.getFeed() != null && TextUtils.isEmpty(state.getFeed().getDescription())) {
@@ -102,16 +104,10 @@ public class NSITunes extends Namespace {
return;
}
- FeedItem currentItem = state.getCurrentItem();
- String description = getDescription(currentItem);
- if (currentItem != null && description.length() * 1.25 < summary.length()) {
- currentItem.setDescription(summary);
+ if (state.getCurrentItem() != null) {
+ state.getCurrentItem().setDescriptionIfLonger(summary);
} else if (NSRSS20.CHANNEL.equals(secondElementName) && state.getFeed() != null) {
state.getFeed().setDescription(summary);
}
}
-
- private String getDescription(FeedItem item) {
- return (item != null && item.getDescription() != null) ? item.getDescription() : "";
- }
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java
index 30b01f0bc..b5d5a1b3f 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java
@@ -121,9 +121,8 @@ public class NSMedia extends Namespace {
public void handleElementEnd(String localName, HandlerState state) {
if (DESCRIPTION.equals(localName)) {
String content = state.getContentBuf().toString();
- if (state.getCurrentItem() != null && content != null
- && state.getCurrentItem().getDescription() == null) {
- state.getCurrentItem().setDescription(content);
+ if (state.getCurrentItem() != null) {
+ state.getCurrentItem().setDescriptionIfLonger(content);
}
}
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java
index 45c5d4884..b1cd6d1c2 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java
@@ -13,10 +13,7 @@ import de.danoeh.antennapod.core.syndication.util.SyndTypeUtils;
import de.danoeh.antennapod.core.util.DateUtils;
/**
- * SAX-Parser for reading RSS-Feeds
- *
- * @author daniel
- *
+ * SAX-Parser for reading RSS-Feeds.
*/
public class NSRSS20 extends Namespace {
@@ -83,8 +80,7 @@ public class NSRSS20 extends Namespace {
if (state.getCurrentItem() != null) {
FeedItem currentItem = state.getCurrentItem();
// the title tag is optional in RSS 2.0. The description is used
- // as a
- // title if the item has no title-tag.
+ // as a title if the item has no title-tag.
if (currentItem.getTitle() == null) {
currentItem.setTitle(currentItem.getDescription());
}
@@ -138,7 +134,7 @@ public class NSRSS20 extends Namespace {
if (CHANNEL.equals(second) && state.getFeed() != null) {
state.getFeed().setDescription(content);
} else if (ITEM.equals(second) && state.getCurrentItem() != null) {
- state.getCurrentItem().setDescription(content);
+ state.getCurrentItem().setDescriptionIfLonger(content);
}
} else if (LANGUAGE.equals(localName) && state.getFeed() != null) {
state.getFeed().setLanguage(content.toLowerCase());
diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java
index 7e4350fd4..42f787d98 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java
@@ -198,10 +198,10 @@ public class NSAtom extends Namespace {
state.getFeed().setDescription(textElement.getProcessedContent());
} else if (CONTENT.equals(top) && ENTRY.equals(second) && textElement != null &&
state.getCurrentItem() != null) {
- state.getCurrentItem().setDescription(textElement.getProcessedContent());
- } else if (SUMMARY.equals(top) && ENTRY.equals(second) && textElement != null &&
- state.getCurrentItem() != null && state.getCurrentItem().getDescription() == null) {
- state.getCurrentItem().setDescription(textElement.getProcessedContent());
+ state.getCurrentItem().setDescriptionIfLonger(textElement.getProcessedContent());
+ } else if (SUMMARY.equals(top) && ENTRY.equals(second) && textElement != null
+ && state.getCurrentItem() != null) {
+ state.getCurrentItem().setDescriptionIfLonger(textElement.getProcessedContent());
} else if (UPDATED.equals(top) && ENTRY.equals(second) && state.getCurrentItem() != null &&
state.getCurrentItem().getPubDate() == null) {
state.getCurrentItem().setPubDate(DateUtils.parse(content));
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java
index d4a2cdca6..ca9689048 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java
@@ -3,32 +3,30 @@ package de.danoeh.antennapod.core.util;
import android.content.ContentResolver;
import android.content.Context;
import android.net.Uri;
-import androidx.annotation.NonNull;
import android.util.Log;
-
-import java.net.URLConnection;
-import de.danoeh.antennapod.core.ClientConfig;
-import org.apache.commons.io.IOUtils;
-
-import java.io.BufferedInputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.URL;
-import java.util.Collections;
-import java.util.List;
-
+import androidx.annotation.NonNull;
import de.danoeh.antennapod.core.feed.Chapter;
+import de.danoeh.antennapod.core.feed.ChapterMerger;
+import de.danoeh.antennapod.core.feed.FeedMedia;
+import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
+import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.util.comparator.ChapterStartTimeComparator;
import de.danoeh.antennapod.core.util.id3reader.ChapterReader;
import de.danoeh.antennapod.core.util.id3reader.ID3ReaderException;
import de.danoeh.antennapod.core.util.playback.Playable;
import de.danoeh.antennapod.core.util.vorbiscommentreader.VorbisCommentChapterReader;
import de.danoeh.antennapod.core.util.vorbiscommentreader.VorbisCommentReaderException;
+import okhttp3.Request;
+import okhttp3.Response;
import org.apache.commons.io.input.CountingInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.List;
+
/**
* Utility class for getting chapter data from media files.
*/
@@ -52,101 +50,84 @@ public class ChapterUtils {
return chapters.size() - 1;
}
- public static List<Chapter> loadChaptersFromStreamUrl(Playable media, Context context) {
- List<Chapter> chapters = ChapterUtils.readID3ChaptersFromPlayableStreamUrl(media, context);
- if (chapters == null) {
- chapters = ChapterUtils.readOggChaptersFromPlayableStreamUrl(media, context);
+ public static void loadChapters(Playable playable, Context context) {
+ if (playable.getChapters() != null) {
+ // Already loaded
+ return;
}
- return chapters;
- }
- public static List<Chapter> loadChaptersFromFileUrl(Playable media) {
- if (!media.localFileAvailable()) {
- Log.e(TAG, "Could not load chapters from file url: local file not available");
- return null;
+ List<Chapter> chaptersFromDatabase = null;
+ if (playable instanceof FeedMedia) {
+ FeedMedia feedMedia = (FeedMedia) playable;
+ if (feedMedia.getItem() == null) {
+ feedMedia.setItem(DBReader.getFeedItem(feedMedia.getItemId()));
+ }
+ if (feedMedia.getItem().hasChapters()) {
+ chaptersFromDatabase = DBReader.loadChaptersOfFeedItem(feedMedia.getItem());
+ }
}
- List<Chapter> chapters = ChapterUtils.readID3ChaptersFromPlayableFileUrl(media);
+
+ List<Chapter> chaptersFromMediaFile = ChapterUtils.loadChaptersFromMediaFile(playable, context);
+ List<Chapter> chapters = ChapterMerger.merge(chaptersFromDatabase, chaptersFromMediaFile);
if (chapters == null) {
- chapters = ChapterUtils.readOggChaptersFromPlayableFileUrl(media);
+ // Do not try loading again. There are no chapters.
+ playable.setChapters(Collections.emptyList());
+ } else {
+ playable.setChapters(chapters);
}
- return chapters;
}
- /**
- * Uses the download URL of a media object of a feeditem to read its ID3
- * chapters.
- */
- private static List<Chapter> readID3ChaptersFromPlayableStreamUrl(Playable p, Context context) {
- if (p == null || p.getStreamUrl() == null) {
- Log.e(TAG, "Unable to read ID3 chapters: media or download URL was null");
- return null;
- }
- Log.d(TAG, "Reading id3 chapters from item " + p.getEpisodeTitle());
- CountingInputStream in = null;
- try {
- if (p.getStreamUrl().startsWith(ContentResolver.SCHEME_CONTENT)) {
- Uri uri = Uri.parse(p.getStreamUrl());
- in = new CountingInputStream(context.getContentResolver().openInputStream(uri));
- } else {
- URL url = new URL(p.getStreamUrl());
- URLConnection urlConnection = url.openConnection();
- urlConnection.setRequestProperty("User-Agent", ClientConfig.USER_AGENT);
- in = new CountingInputStream(urlConnection.getInputStream());
- }
- List<Chapter> chapters = readChaptersFrom(in);
+ public static List<Chapter> loadChaptersFromMediaFile(Playable playable, Context context) {
+ try (CountingInputStream in = openStream(playable, context)) {
+ List<Chapter> chapters = readId3ChaptersFrom(in);
if (!chapters.isEmpty()) {
+ Log.i(TAG, "Chapters loaded");
return chapters;
}
- Log.i(TAG, "Chapters loaded");
- } catch (IOException | ID3ReaderException | IllegalArgumentException e) {
- Log.e(TAG, Log.getStackTraceString(e));
- } finally {
- IOUtils.closeQuietly(in);
- }
- return null;
- }
-
- /**
- * Uses the file URL of a media object of a feeditem to read its ID3
- * chapters.
- */
- private static List<Chapter> readID3ChaptersFromPlayableFileUrl(Playable p) {
- if (p == null || !p.localFileAvailable() || p.getLocalMediaUrl() == null) {
- return null;
- }
- Log.d(TAG, "Reading id3 chapters from item " + p.getEpisodeTitle());
- File source = new File(p.getLocalMediaUrl());
- if (!source.exists()) {
- Log.e(TAG, "Unable to read id3 chapters: Source doesn't exist");
- return null;
+ } catch (IOException | ID3ReaderException e) {
+ Log.e(TAG, "Unable to load ID3 chapters: " + e.getMessage());
}
- CountingInputStream in = null;
- try {
- in = new CountingInputStream(new BufferedInputStream(new FileInputStream(source)));
- List<Chapter> chapters = readChaptersFrom(in);
+ try (CountingInputStream in = openStream(playable, context)) {
+ List<Chapter> chapters = readOggChaptersFromInputStream(in);
if (!chapters.isEmpty()) {
+ Log.i(TAG, "Chapters loaded");
return chapters;
}
- Log.i(TAG, "Chapters loaded");
- } catch (IOException | ID3ReaderException e) {
- Log.e(TAG, Log.getStackTraceString(e));
- } finally {
- IOUtils.closeQuietly(in);
+ } catch (IOException | VorbisCommentReaderException e) {
+ Log.e(TAG, "Unable to load vorbis chapters: " + e.getMessage());
}
return null;
}
+ private static CountingInputStream openStream(Playable playable, Context context) throws IOException {
+ if (playable.localFileAvailable()) {
+ if (playable.getLocalMediaUrl() == null) {
+ throw new IOException("No local url");
+ }
+ File source = new File(playable.getLocalMediaUrl());
+ if (!source.exists()) {
+ throw new IOException("Local file does not exist");
+ }
+ return new CountingInputStream(new FileInputStream(source));
+ } else if (playable.getStreamUrl().startsWith(ContentResolver.SCHEME_CONTENT)) {
+ Uri uri = Uri.parse(playable.getStreamUrl());
+ return new CountingInputStream(context.getContentResolver().openInputStream(uri));
+ } else {
+ Request request = new Request.Builder().url(playable.getStreamUrl()).build();
+ Response response = AntennapodHttpClient.getHttpClient().newCall(request).execute();
+ if (response.body() == null) {
+ throw new IOException("Body is null");
+ }
+ return new CountingInputStream(response.body().byteStream());
+ }
+ }
+
@NonNull
- private static List<Chapter> readChaptersFrom(CountingInputStream in) throws IOException, ID3ReaderException {
- ChapterReader reader = new ChapterReader();
- reader.readInputStream(in);
+ private static List<Chapter> readId3ChaptersFrom(CountingInputStream in) throws IOException, ID3ReaderException {
+ ChapterReader reader = new ChapterReader(in);
+ reader.readInputStream();
List<Chapter> chapters = reader.getChapters();
-
- if (chapters == null) {
- Log.i(TAG, "ChapterReader could not find any ID3 chapters");
- return Collections.emptyList();
- }
Collections.sort(chapters, new ChapterStartTimeComparator());
enumerateEmptyChapterTitles(chapters);
if (!chaptersValid(chapters)) {
@@ -156,73 +137,20 @@ public class ChapterUtils {
return chapters;
}
- private static List<Chapter> readOggChaptersFromPlayableStreamUrl(Playable media, Context context) {
- if (media == null || !media.streamAvailable()) {
- return null;
- }
- InputStream input = null;
- try {
- if (media.getStreamUrl().startsWith(ContentResolver.SCHEME_CONTENT)) {
- Uri uri = Uri.parse(media.getStreamUrl());
- input = context.getContentResolver().openInputStream(uri);
- } else {
- URL url = new URL(media.getStreamUrl());
- URLConnection urlConnection = url.openConnection();
- urlConnection.setRequestProperty("User-Agent", ClientConfig.USER_AGENT);
- input = urlConnection.getInputStream();
- }
- if (input != null) {
- return readOggChaptersFromInputStream(media, input);
- }
- } catch (IOException | IllegalArgumentException e) {
- Log.e(TAG, Log.getStackTraceString(e));
- } finally {
- IOUtils.closeQuietly(input);
- }
- return null;
- }
-
- private static List<Chapter> readOggChaptersFromPlayableFileUrl(Playable media) {
- if (media == null || media.getLocalMediaUrl() == null) {
- return null;
- }
- File source = new File(media.getLocalMediaUrl());
- if (source.exists()) {
- InputStream input = null;
- try {
- input = new BufferedInputStream(new FileInputStream(source));
- return readOggChaptersFromInputStream(media, input);
- } catch (FileNotFoundException e) {
- Log.e(TAG, Log.getStackTraceString(e));
- } finally {
- IOUtils.closeQuietly(input);
- }
+ @NonNull
+ private static List<Chapter> readOggChaptersFromInputStream(InputStream input) throws VorbisCommentReaderException {
+ VorbisCommentChapterReader reader = new VorbisCommentChapterReader();
+ reader.readInputStream(input);
+ List<Chapter> chapters = reader.getChapters();
+ if (chapters == null) {
+ return Collections.emptyList();
}
- return null;
- }
-
- private static List<Chapter> readOggChaptersFromInputStream(Playable p, InputStream input) {
- Log.d(TAG, "Trying to read chapters from item with title " + p.getEpisodeTitle());
- try {
- VorbisCommentChapterReader reader = new VorbisCommentChapterReader();
- reader.readInputStream(input);
- List<Chapter> chapters = reader.getChapters();
- if (chapters == null) {
- Log.i(TAG, "ChapterReader could not find any Ogg vorbis chapters");
- return null;
- }
- Collections.sort(chapters, new ChapterStartTimeComparator());
- enumerateEmptyChapterTitles(chapters);
- if (chaptersValid(chapters)) {
- Log.i(TAG, "Chapters loaded");
- return chapters;
- } else {
- Log.e(TAG, "Chapter data was invalid");
- }
- } catch (VorbisCommentReaderException e) {
- e.printStackTrace();
+ Collections.sort(chapters, new ChapterStartTimeComparator());
+ enumerateEmptyChapterTitles(chapters);
+ if (chaptersValid(chapters)) {
+ return chapters;
}
- return null;
+ return Collections.emptyList();
}
/**
@@ -248,5 +176,4 @@ public class ChapterUtils {
}
return true;
}
-
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java
index 833ff33f1..196583bcd 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java
@@ -30,9 +30,12 @@ public class DateUtils {
}
String date = input.trim().replace('/', '-').replaceAll("( ){2,}+", " ");
+ // remove colon from timezone to avoid differences between Android and Java SimpleDateFormat
+ date = date.replaceAll("([+-]\\d\\d):(\\d\\d)$", "$1$2");
+
// CEST is widely used but not in the "ISO 8601 Time zone" list. Let's hack around.
- date = date.replaceAll("CEST$", "+02:00");
- date = date.replaceAll("CET$", "+01:00");
+ date = date.replaceAll("CEST$", "+0200");
+ date = date.replaceAll("CET$", "+0100");
// some generators use "Sept" for September
date = date.replaceAll("\\bSept\\b", "Sep");
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/DownloadError.java b/core/src/main/java/de/danoeh/antennapod/core/util/DownloadError.java
index 0c9989b43..9c4a61cd8 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/DownloadError.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/DownloadError.java
@@ -6,51 +6,54 @@ import de.danoeh.antennapod.core.R;
/** Utility class for Download Errors. */
public enum DownloadError {
- SUCCESS(0, R.string.download_successful),
- ERROR_PARSER_EXCEPTION(1, R.string.download_error_parser_exception),
- ERROR_UNSUPPORTED_TYPE(2, R.string.download_error_unsupported_type),
- ERROR_CONNECTION_ERROR(3, R.string.download_error_connection_error),
- ERROR_MALFORMED_URL(4, R.string.download_error_error_unknown),
- ERROR_IO_ERROR(5, R.string.download_error_io_error),
- ERROR_FILE_EXISTS(6, R.string.download_error_error_unknown),
- ERROR_DOWNLOAD_CANCELLED(7, R.string.download_error_error_unknown),
- ERROR_DEVICE_NOT_FOUND(8, R.string.download_error_device_not_found),
- ERROR_HTTP_DATA_ERROR(9, R.string.download_error_http_data_error),
- ERROR_NOT_ENOUGH_SPACE(10, R.string.download_error_insufficient_space),
- ERROR_UNKNOWN_HOST(11, R.string.download_error_unknown_host),
- ERROR_REQUEST_ERROR(12, R.string.download_error_request_error),
+ SUCCESS(0, R.string.download_successful),
+ ERROR_PARSER_EXCEPTION(1, R.string.download_error_parser_exception),
+ ERROR_UNSUPPORTED_TYPE(2, R.string.download_error_unsupported_type),
+ ERROR_CONNECTION_ERROR(3, R.string.download_error_connection_error),
+ ERROR_MALFORMED_URL(4, R.string.download_error_error_unknown),
+ ERROR_IO_ERROR(5, R.string.download_error_io_error),
+ ERROR_FILE_EXISTS(6, R.string.download_error_error_unknown),
+ ERROR_DOWNLOAD_CANCELLED(7, R.string.download_canceled_msg),
+ ERROR_DEVICE_NOT_FOUND(8, R.string.download_error_device_not_found),
+ ERROR_HTTP_DATA_ERROR(9, R.string.download_error_http_data_error),
+ ERROR_NOT_ENOUGH_SPACE(10, R.string.download_error_insufficient_space),
+ ERROR_UNKNOWN_HOST(11, R.string.download_error_unknown_host),
+ ERROR_REQUEST_ERROR(12, R.string.download_error_request_error),
ERROR_DB_ACCESS_ERROR(13, R.string.download_error_db_access),
ERROR_UNAUTHORIZED(14, R.string.download_error_unauthorized),
- ERROR_FILE_TYPE(15, R.string.download_error_file_type_type),
- ERROR_FORBIDDEN(16, R.string.download_error_forbidden);
-
-
- private final int code;
- private final int resId;
-
- DownloadError(int code, int resId) {
- this.code = code;
- this.resId = resId;
- }
-
- /** Return DownloadError from its associated code. */
- public static DownloadError fromCode(int code) {
- for (DownloadError reason : values()) {
- if (reason.getCode() == code) {
- return reason;
- }
- }
- throw new IllegalArgumentException("unknown code: " + code);
- }
-
- /** Get machine-readable code. */
- public int getCode() {
- return code;
- }
-
- /** Get a human-readable string. */
- public String getErrorString(Context context) {
- return context.getString(resId);
- }
-
+ ERROR_FILE_TYPE(15, R.string.download_error_file_type_type),
+ ERROR_FORBIDDEN(16, R.string.download_error_forbidden),
+ ERROR_IO_WRONG_SIZE(17, R.string.download_error_wrong_size),
+ ERROR_IO_BLOCKED(18, R.string.download_error_blocked),
+ ERROR_UNSUPPORTED_TYPE_HTML(19, R.string.download_error_unsupported_type_html),
+ ERROR_NOT_FOUND(20, R.string.download_error_not_found),
+ ERROR_CERTIFICATE(21, R.string.download_error_certificate);
+
+ private final int code;
+ private final int resId;
+
+ DownloadError(int code, int resId) {
+ this.code = code;
+ this.resId = resId;
+ }
+
+ /** Return DownloadError from its associated code. */
+ public static DownloadError fromCode(int code) {
+ for (DownloadError reason : values()) {
+ if (reason.getCode() == code) {
+ return reason;
+ }
+ }
+ throw new IllegalArgumentException("unknown code: " + code);
+ }
+
+ /** Get machine-readable code. */
+ public int getCode() {
+ return code;
+ }
+
+ /** Get a human-readable string. */
+ public String getErrorString(Context context) {
+ return context.getString(resId);
+ }
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java b/core/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java
index 2a387b7b0..69c23efc2 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java
@@ -13,7 +13,7 @@ import java.security.NoSuchAlgorithmException;
/** Generates valid filenames for a given string. */
public class FileNameGenerator {
@VisibleForTesting
- public static final int MAX_FILENAME_LENGTH = 255; // Limited by ext4
+ public static final int MAX_FILENAME_LENGTH = 242; // limited by CircleCI
private static final int MD5_HEX_LENGTH = 32;
private static final char[] validChars =
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/Flavors.java b/core/src/main/java/de/danoeh/antennapod/core/util/Flavors.java
deleted file mode 100644
index 5feb232e7..000000000
--- a/core/src/main/java/de/danoeh/antennapod/core/util/Flavors.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package de.danoeh.antennapod.core.util;
-
-import de.danoeh.antennapod.core.BuildConfig;
-
-/**
- * Helper class to handle the different build flavors.
- */
-public enum Flavors {
- FREE,
- PLAY,
- UNKNOWN;
-
- public static final Flavors FLAVOR;
-
- static {
- if (BuildConfig.FLAVOR.equals("free")) {
- FLAVOR = FREE;
- } else if (BuildConfig.FLAVOR.equals("play")) {
- FLAVOR = PLAY;
- } else {
- FLAVOR = UNKNOWN;
- }
- }
-}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/Optional.java b/core/src/main/java/de/danoeh/antennapod/core/util/Optional.java
deleted file mode 100644
index 37f12c01c..000000000
--- a/core/src/main/java/de/danoeh/antennapod/core/util/Optional.java
+++ /dev/null
@@ -1,213 +0,0 @@
-/*
- * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation. Oracle designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-package de.danoeh.antennapod.core.util;
-
-import java.util.NoSuchElementException;
-import java.util.Objects;
-
-// AntennaPod's stripped-down version of Java/Android platform's java.util.Optional
-// so that it can be used on lower API level (API level 14)
-
-// Android-changed: removed ValueBased paragraph.
-/**
- * A container object which may or may not contain a non-null value.
- * If a value is present, {@code isPresent()} will return {@code true} and
- * {@code get()} will return the value.
- *
- * <p>Additional methods that depend on the presence or absence of a contained
- * value are provided, such as {@link #orElse(java.lang.Object) orElse()}
- * (return a default value if value not present) and
- * {@link #ifPresent(java.util.function.Consumer) ifPresent()} (execute a block
- * of code if the value is present).
- *
- * @since 1.8
- */
-public final class Optional<T> {
- /**
- * Common instance for {@code empty()}.
- */
- private static final Optional<?> EMPTY = new Optional<>();
-
- /**
- * If non-null, the value; if null, indicates no value is present
- */
- private final T value;
-
- /**
- * Constructs an empty instance.
- *
- * @implNote Generally only one empty instance, {@link Optional#EMPTY},
- * should exist per VM.
- */
- private Optional() {
- this.value = null;
- }
-
- /**
- * Returns an empty {@code Optional} instance. No value is present for this
- * Optional.
- *
- * @apiNote Though it may be tempting to do so, avoid testing if an object
- * is empty by comparing with {@code ==} against instances returned by
- * {@code Option.empty()}. There is no guarantee that it is a singleton.
- * Instead, use {@link #isPresent()}.
- *
- * @param <T> Type of the non-existent value
- * @return an empty {@code Optional}
- */
- public static <T> Optional<T> empty() {
- @SuppressWarnings("unchecked")
- Optional<T> t = (Optional<T>) EMPTY;
- return t;
- }
-
- /**
- * Constructs an instance with the value present.
- *
- * @param value the non-null value to be present
- * @throws NullPointerException if value is null
- */
- private Optional(T value) {
- this.value = Objects.requireNonNull(value);
- }
-
- /**
- * Returns an {@code Optional} with the specified present non-null value.
- *
- * @param <T> the class of the value
- * @param value the value to be present, which must be non-null
- * @return an {@code Optional} with the value present
- * @throws NullPointerException if value is null
- */
- public static <T> Optional<T> of(T value) {
- return new Optional<>(value);
- }
-
- /**
- * Returns an {@code Optional} describing the specified value, if non-null,
- * otherwise returns an empty {@code Optional}.
- *
- * @param <T> the class of the value
- * @param value the possibly-null value to describe
- * @return an {@code Optional} with a present value if the specified value
- * is non-null, otherwise an empty {@code Optional}
- */
- public static <T> Optional<T> ofNullable(T value) {
- return value == null ? empty() : of(value);
- }
-
- /**
- * If a value is present in this {@code Optional}, returns the value,
- * otherwise throws {@code NoSuchElementException}.
- *
- * @return the non-null value held by this {@code Optional}
- * @throws NoSuchElementException if there is no value present
- *
- * @see Optional#isPresent()
- */
- public T get() {
- if (value == null) {
- throw new NoSuchElementException("No value present");
- }
- return value;
- }
-
- /**
- * Return {@code true} if there is a value present, otherwise {@code false}.
- *
- * @return {@code true} if there is a value present, otherwise {@code false}
- */
- public boolean isPresent() {
- return value != null;
- }
-
-
- /**
- * Return the value if present, otherwise return {@code other}.
- *
- * @param other the value to be returned if there is no value present, may
- * be null
- * @return the value, if present, otherwise {@code other}
- */
- public T orElse(T other) {
- return value != null ? value : other;
- }
-
- /**
- * Indicates whether some other object is "equal to" this Optional. The
- * other object is considered equal if:
- * <ul>
- * <li>it is also an {@code Optional} and;
- * <li>both instances have no value present or;
- * <li>the present values are "equal to" each other via {@code equals()}.
- * </ul>
- *
- * @param obj an object to be tested for equality
- * @return {code true} if the other object is "equal to" this object
- * otherwise {@code false}
- */
- @Override
- public boolean equals(Object obj) {
- if (this == obj) {
- return true;
- }
-
- if (!(obj instanceof Optional)) {
- return false;
- }
-
- Optional<?> other = (Optional<?>) obj;
- return (value == other.value) || (value != null && value.equals(other.value));
- }
-
- /**
- * Returns the hash code value of the present value, if any, or 0 (zero) if
- * no value is present.
- *
- * @return hash code value of the present value or 0 if no value is present
- */
- @Override
- public int hashCode() {
- return value != null ? value.hashCode() : 0;
- }
-
- /**
- * Returns a non-empty string representation of this Optional suitable for
- * debugging. The exact presentation format is unspecified and may vary
- * between implementations and versions.
- *
- * @implSpec If a value is present the result must include its string
- * representation in the result. Empty and present Optionals must be
- * unambiguously differentiable.
- *
- * @return the string representation of this instance
- */
- @Override
- public String toString() {
- return value != null
- ? String.format("Optional[%s]", value)
- : "Optional.empty";
- }
-}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/ShownotesProvider.java b/core/src/main/java/de/danoeh/antennapod/core/util/ShownotesProvider.java
deleted file mode 100644
index a4cd83f70..000000000
--- a/core/src/main/java/de/danoeh/antennapod/core/util/ShownotesProvider.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package de.danoeh.antennapod.core.util;
-
-import java.util.concurrent.Callable;
-
-/**
- * Created by daniel on 04.08.13.
- */
-public interface ShownotesProvider {
- /**
- * Loads shownotes. If the shownotes have to be loaded from a file or from a
- * database, it should be done in a separate thread. After the shownotes
- * have been loaded, callback.onShownotesLoaded should be called.
- */
- Callable<String> loadShownotes();
-
-}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/ThemeUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/ThemeUtils.java
deleted file mode 100644
index 44b31f0be..000000000
--- a/core/src/main/java/de/danoeh/antennapod/core/util/ThemeUtils.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package de.danoeh.antennapod.core.util;
-
-import android.content.Context;
-import androidx.annotation.AttrRes;
-import androidx.annotation.ColorInt;
-import android.util.TypedValue;
-import androidx.annotation.DrawableRes;
-
-public class ThemeUtils {
- private ThemeUtils() {
-
- }
-
- public static @ColorInt int getColorFromAttr(Context context, @AttrRes int attr) {
- TypedValue typedValue = new TypedValue();
- context.getTheme().resolveAttribute(attr, typedValue, true);
- return typedValue.data;
- }
-
- public static @DrawableRes int getDrawableFromAttr(Context context, @AttrRes int attr) {
- TypedValue typedValue = new TypedValue();
- context.getTheme().resolveAttribute(attr, typedValue, true);
- return typedValue.resourceId;
- }
-}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/gui/NotificationUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/gui/NotificationUtils.java
index 3101eac34..5895c5933 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/gui/NotificationUtils.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/gui/NotificationUtils.java
@@ -18,6 +18,7 @@ public class NotificationUtils {
public static final String CHANNEL_ID_DOWNLOAD_ERROR = "error";
public static final String CHANNEL_ID_SYNC_ERROR = "sync_error";
public static final String CHANNEL_ID_AUTO_DOWNLOAD = "auto_download";
+ public static final String CHANNEL_ID_EPISODE_NOTIFICATIONS = "episode_notifications";
public static final String GROUP_ID_ERRORS = "group_errors";
public static final String GROUP_ID_NEWS = "group_news";
@@ -38,6 +39,7 @@ public class NotificationUtils {
mNotificationManager.createNotificationChannel(createChannelError(context));
mNotificationManager.createNotificationChannel(createChannelSyncError(context));
mNotificationManager.createNotificationChannel(createChannelAutoDownload(context));
+ mNotificationManager.createNotificationChannel(createChannelEpisodeNotification(context));
}
}
@@ -111,6 +113,15 @@ public class NotificationUtils {
}
@RequiresApi(api = Build.VERSION_CODES.O)
+ private static NotificationChannel createChannelEpisodeNotification(Context c) {
+ NotificationChannel channel = new NotificationChannel(CHANNEL_ID_EPISODE_NOTIFICATIONS,
+ c.getString(R.string.notification_channel_new_episode), NotificationManager.IMPORTANCE_DEFAULT);
+ channel.setDescription(c.getString(R.string.notification_channel_new_episode_description));
+ channel.setGroup(GROUP_ID_NEWS);
+ return channel;
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.O)
private static NotificationChannelGroup createGroupErrors(Context c) {
return new NotificationChannelGroup(GROUP_ID_ERRORS,
c.getString(R.string.notification_group_errors));
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ChapterReader.java b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ChapterReader.java
index ce3577a9e..69d8316c2 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ChapterReader.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ChapterReader.java
@@ -2,149 +2,114 @@ package de.danoeh.antennapod.core.util.id3reader;
import android.text.TextUtils;
import android.util.Log;
+import androidx.annotation.NonNull;
import de.danoeh.antennapod.core.feed.Chapter;
import de.danoeh.antennapod.core.feed.ID3Chapter;
import de.danoeh.antennapod.core.util.EmbeddedChapterImage;
import de.danoeh.antennapod.core.util.id3reader.model.FrameHeader;
-import de.danoeh.antennapod.core.util.id3reader.model.TagHeader;
+import org.apache.commons.io.input.CountingInputStream;
import java.io.IOException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.List;
-import org.apache.commons.io.input.CountingInputStream;
+/**
+ * Reads ID3 chapters.
+ * See https://id3.org/id3v2-chapters-1.0
+ */
public class ChapterReader extends ID3Reader {
private static final String TAG = "ID3ChapterReader";
- private static final String FRAME_ID_CHAPTER = "CHAP";
- private static final String FRAME_ID_TITLE = "TIT2";
- private static final String FRAME_ID_LINK = "WXXX";
- private static final String FRAME_ID_PICTURE = "APIC";
- private static final int IMAGE_TYPE_COVER = 3;
+ public static final String FRAME_ID_CHAPTER = "CHAP";
+ public static final String FRAME_ID_TITLE = "TIT2";
+ public static final String FRAME_ID_LINK = "WXXX";
+ public static final String FRAME_ID_PICTURE = "APIC";
+ public static final String MIME_IMAGE_URL = "-->";
+ public static final int IMAGE_TYPE_COVER = 3;
- private List<Chapter> chapters;
- private ID3Chapter currentChapter;
+ private final List<Chapter> chapters = new ArrayList<>();
- @Override
- public int onStartTagHeader(TagHeader header) {
- chapters = new ArrayList<>();
- Log.d(TAG, "header: " + header);
- return ID3Reader.ACTION_DONT_SKIP;
+ public ChapterReader(CountingInputStream input) {
+ super(input);
}
@Override
- public int onStartFrameHeader(FrameHeader header, CountingInputStream input) throws IOException, ID3ReaderException {
- Log.d(TAG, "header: " + header);
- switch (header.getId()) {
- case FRAME_ID_CHAPTER:
- if (currentChapter != null) {
- if (!hasId3Chapter(currentChapter)) {
- chapters.add(currentChapter);
- Log.d(TAG, "Found chapter: " + currentChapter);
- currentChapter = null;
- }
- }
- StringBuilder elementId = new StringBuilder();
- readISOString(elementId, input, Integer.MAX_VALUE);
- char[] startTimeSource = readChars(input, 4);
- long startTime = ((int) startTimeSource[0] << 24)
- | ((int) startTimeSource[1] << 16)
- | ((int) startTimeSource[2] << 8) | startTimeSource[3];
- currentChapter = new ID3Chapter(elementId.toString(), startTime);
- skipBytes(input, 12);
- return ID3Reader.ACTION_DONT_SKIP;
- case FRAME_ID_TITLE:
- if (currentChapter != null && currentChapter.getTitle() == null) {
- StringBuilder title = new StringBuilder();
- readString(title, input, header.getSize());
- currentChapter
- .setTitle(title.toString());
- Log.d(TAG, "Found title: " + currentChapter.getTitle());
+ protected void readFrame(@NonNull FrameHeader frameHeader) throws IOException, ID3ReaderException {
+ if (FRAME_ID_CHAPTER.equals(frameHeader.getId())) {
+ Log.d(TAG, "Handling frame: " + frameHeader.toString());
+ Chapter chapter = readChapter(frameHeader);
+ Log.d(TAG, "Chapter done: " + chapter);
+ chapters.add(chapter);
+ } else {
+ super.readFrame(frameHeader);
+ }
+ }
- return ID3Reader.ACTION_DONT_SKIP;
- }
+ public Chapter readChapter(@NonNull FrameHeader frameHeader) throws IOException, ID3ReaderException {
+ int chapterStartedPosition = getPosition();
+ String elementId = readIsoStringNullTerminated(100);
+ long startTime = readInt();
+ skipBytes(12); // Ignore end time, start offset, end offset
+ ID3Chapter chapter = new ID3Chapter(elementId, startTime);
+
+ // Read sub-frames
+ while (getPosition() < chapterStartedPosition + frameHeader.getSize()) {
+ FrameHeader subFrameHeader = readFrameHeader();
+ readChapterSubFrame(subFrameHeader, chapter);
+ }
+ return chapter;
+ }
+
+ public void readChapterSubFrame(@NonNull FrameHeader frameHeader, @NonNull Chapter chapter)
+ throws IOException, ID3ReaderException {
+ Log.d(TAG, "Handling subframe: " + frameHeader.toString());
+ int frameStartPosition = getPosition();
+ switch (frameHeader.getId()) {
+ case FRAME_ID_TITLE:
+ chapter.setTitle(readEncodingAndString(frameHeader.getSize()));
+ Log.d(TAG, "Found title: " + chapter.getTitle());
break;
case FRAME_ID_LINK:
- if (currentChapter != null) {
- // skip description
- int descriptionLength = readString(null, input, header.getSize());
- StringBuilder link = new StringBuilder();
- readISOString(link, input, header.getSize() - descriptionLength);
- try {
- String decodedLink = URLDecoder.decode(link.toString(), "UTF-8");
- currentChapter.setLink(decodedLink);
- Log.d(TAG, "Found link: " + currentChapter.getLink());
- } catch (IllegalArgumentException iae) {
- Log.w(TAG, "Bad URL found in ID3 data");
- }
-
- return ID3Reader.ACTION_DONT_SKIP;
+ readEncodingAndString(frameHeader.getSize()); // skip description
+ String url = readIsoStringNullTerminated(frameStartPosition + frameHeader.getSize() - getPosition());
+ try {
+ String decodedLink = URLDecoder.decode(url, "ISO-8859-1");
+ chapter.setLink(decodedLink);
+ Log.d(TAG, "Found link: " + chapter.getLink());
+ } catch (IllegalArgumentException iae) {
+ Log.w(TAG, "Bad URL found in ID3 data");
}
break;
case FRAME_ID_PICTURE:
- if (currentChapter != null) {
- Log.d(TAG, header.toString());
- StringBuilder mime = new StringBuilder();
- int read = readString(mime, input, header.getSize());
- byte type = (byte) readChars(input, 1)[0];
- read++;
- StringBuilder description = new StringBuilder();
- read += readISOString(description, input, header.getSize()); // Should use same encoding as mime
-
- Log.d(TAG, "Found apic: " + mime + "," + description);
- if (mime.toString().equals("-->")) {
- // Data contains a link to a picture
- StringBuilder link = new StringBuilder();
- readISOString(link, input, header.getSize());
- Log.d(TAG, "link: " + link.toString());
- if (TextUtils.isEmpty(currentChapter.getImageUrl()) || type == IMAGE_TYPE_COVER) {
- currentChapter.setImageUrl(link.toString());
- }
- } else {
- // Data contains the picture
- int length = header.getSize() - read;
- if (TextUtils.isEmpty(currentChapter.getImageUrl()) || type == IMAGE_TYPE_COVER) {
- currentChapter.setImageUrl(EmbeddedChapterImage.makeUrl(input.getCount(), length));
- }
- skipBytes(input, length);
+ byte encoding = readByte();
+ String mime = readEncodedString(encoding, frameHeader.getSize());
+ byte type = readByte();
+ String description = readEncodedString(encoding, frameHeader.getSize());
+ Log.d(TAG, "Found apic: " + mime + "," + description);
+ if (MIME_IMAGE_URL.equals(mime)) {
+ String link = readIsoStringNullTerminated(frameHeader.getSize());
+ Log.d(TAG, "Link: " + link);
+ if (TextUtils.isEmpty(chapter.getImageUrl()) || type == IMAGE_TYPE_COVER) {
+ chapter.setImageUrl(link);
+ }
+ } else {
+ int alreadyConsumed = getPosition() - frameStartPosition;
+ int rawImageDataLength = frameHeader.getSize() - alreadyConsumed;
+ if (TextUtils.isEmpty(chapter.getImageUrl()) || type == IMAGE_TYPE_COVER) {
+ chapter.setImageUrl(EmbeddedChapterImage.makeUrl(getPosition(), rawImageDataLength));
}
- return ID3Reader.ACTION_DONT_SKIP;
}
break;
+ default:
+ Log.d(TAG, "Unknown chapter sub-frame.");
+ break;
}
- return super.onStartFrameHeader(header, input);
- }
-
- private boolean hasId3Chapter(ID3Chapter chapter) {
- for (Chapter c : chapters) {
- if (((ID3Chapter) c).getId3ID().equals(chapter.getId3ID())) {
- return true;
- }
- }
- return false;
- }
-
- @Override
- public void onEndTag() {
- if (currentChapter != null) {
- if (!hasId3Chapter(currentChapter)) {
- chapters.add(currentChapter);
- }
- }
- Log.d(TAG, "Reached end of tag");
- if (chapters != null) {
- for (Chapter c : chapters) {
- Log.d(TAG, "chapter: " + c);
- }
- }
- }
-
- @Override
- public void onNoTagHeaderFound() {
- Log.d(TAG, "No tag header found");
- super.onNoTagHeaderFound();
+ // Skip garbage to fill frame completely
+ // This also asserts that we are not reading too many bytes from this frame.
+ int alreadyConsumed = getPosition() - frameStartPosition;
+ skipBytes(frameHeader.getSize() - alreadyConsumed);
}
public List<Chapter> getChapters() {
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3Reader.java b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3Reader.java
index 124388254..17313ca14 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3Reader.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3Reader.java
@@ -1,151 +1,112 @@
package de.danoeh.antennapod.core.util.id3reader;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import de.danoeh.antennapod.core.util.id3reader.model.FrameHeader;
+import de.danoeh.antennapod.core.util.id3reader.model.TagHeader;
import org.apache.commons.io.IOUtils;
+import org.apache.commons.io.input.CountingInputStream;
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
-import de.danoeh.antennapod.core.util.id3reader.model.FrameHeader;
-import de.danoeh.antennapod.core.util.id3reader.model.TagHeader;
-import org.apache.commons.io.input.CountingInputStream;
-
/**
- * Reads the ID3 Tag of a given file. In order to use this class, you should
- * create a subclass of it and overwrite the onStart* - or onEnd* - methods.
+ * Reads the ID3 Tag of a given file.
+ * See https://id3.org/id3v2.3.0
*/
public class ID3Reader {
- private static final int HEADER_LENGTH = 10;
- private static final int ID3_LENGTH = 3;
+ private static final String TAG = "ID3Reader";
private static final int FRAME_ID_LENGTH = 4;
-
- private static final int ACTION_SKIP = 1;
- static final int ACTION_DONT_SKIP = 2;
-
- private int readerPosition;
-
- private static final byte ENCODING_UTF16_WITH_BOM = 1;
- private static final byte ENCODING_UTF16_WITHOUT_BOM = 2;
- private static final byte ENCODING_UTF8 = 3;
+ public static final byte ENCODING_ISO = 0;
+ public static final byte ENCODING_UTF16_WITH_BOM = 1;
+ public static final byte ENCODING_UTF16_WITHOUT_BOM = 2;
+ public static final byte ENCODING_UTF8 = 3;
private TagHeader tagHeader;
+ private final CountingInputStream inputStream;
- ID3Reader() {
+ public ID3Reader(CountingInputStream input) {
+ inputStream = input;
}
- public final void readInputStream(CountingInputStream input) throws IOException, ID3ReaderException {
- int rc;
- readerPosition = 0;
- char[] tagHeaderSource = readChars(input, HEADER_LENGTH);
- tagHeader = createTagHeader(tagHeaderSource);
- if (tagHeader == null) {
- onNoTagHeaderFound();
- } else {
- rc = onStartTagHeader(tagHeader);
- if (rc != ACTION_SKIP) {
- while (readerPosition < tagHeader.getSize()) {
- FrameHeader frameHeader = createFrameHeader(readChars(input, HEADER_LENGTH));
- if (checkForNullString(frameHeader.getId())) {
- break;
- }
- rc = onStartFrameHeader(frameHeader, input);
- if (rc == ACTION_SKIP) {
- if (frameHeader.getSize() + readerPosition > tagHeader.getSize()) {
- break;
- }
- skipBytes(input, frameHeader.getSize());
- }
- }
+ public void readInputStream() throws IOException, ID3ReaderException {
+ tagHeader = readTagHeader();
+ int tagContentStartPosition = getPosition();
+ while (getPosition() < tagContentStartPosition + tagHeader.getSize()) {
+ FrameHeader frameHeader = readFrameHeader();
+ if (frameHeader.getId().charAt(0) < '0' || frameHeader.getId().charAt(0) > 'z') {
+ Log.d(TAG, "Stopping because of invalid frame: " + frameHeader.toString());
+ return;
}
- onEndTag();
+ readFrame(frameHeader);
}
}
- /** Returns true if string only contains null-bytes. */
- private boolean checkForNullString(String s) {
- if (!s.isEmpty()) {
- int i = 0;
- if (s.charAt(i) == 0) {
- for (i = 1; i < s.length(); i++) {
- if (s.charAt(i) != 0) {
- return false;
- }
- }
- return true;
- }
- return false;
- } else {
- return true;
- }
+ protected void readFrame(@NonNull FrameHeader frameHeader) throws IOException, ID3ReaderException {
+ Log.d(TAG, "Skipping frame: " + frameHeader.toString());
+ skipBytes(frameHeader.getSize());
+ }
+ int getPosition() {
+ return inputStream.getCount();
}
/**
- * Read a certain number of chars from the given input stream. This method
- * changes the readerPosition-attribute.
+ * Skip a certain number of bytes on the given input stream.
*/
- char[] readChars(InputStream input, int number) throws IOException, ID3ReaderException {
- char[] header = new char[number];
- for (int i = 0; i < number; i++) {
- int b = input.read();
- readerPosition++;
- if (b != -1) {
- header[i] = (char) b;
- } else {
- throw new ID3ReaderException("Unexpected end of stream");
- }
+ void skipBytes(int number) throws IOException, ID3ReaderException {
+ if (number < 0) {
+ throw new ID3ReaderException("Trying to read a negative number of bytes");
}
- return header;
+ IOUtils.skipFully(inputStream, number);
}
- /**
- * Skip a certain number of bytes on the given input stream. This method
- * changes the readerPosition-attribute.
- */
- void skipBytes(InputStream input, int number) throws IOException {
- if (number <= 0) {
- number = 1;
- }
- IOUtils.skipFully(input, number);
+ byte readByte() throws IOException {
+ return (byte) inputStream.read();
+ }
- readerPosition += number;
+ short readShort() throws IOException {
+ char firstByte = (char) inputStream.read();
+ char secondByte = (char) inputStream.read();
+ return (short) ((firstByte << 8) | secondByte);
}
- private TagHeader createTagHeader(char[] source) throws ID3ReaderException {
- boolean hasTag = (source[0] == 0x49) && (source[1] == 0x44)
- && (source[2] == 0x33);
- if (source.length != HEADER_LENGTH) {
- throw new ID3ReaderException("Length of header must be "
- + HEADER_LENGTH);
- }
- if (hasTag) {
- String id = new String(source, 0, ID3_LENGTH);
- char version = (char) ((source[3] << 8) | source[4]);
- byte flags = (byte) source[5];
- int size = (source[6] << 24) | (source[7] << 16) | (source[8] << 8)
- | source[9];
- size = unsynchsafe(size);
- return new TagHeader(id, size, version, flags);
- } else {
- return null;
- }
+ int readInt() throws IOException {
+ char firstByte = (char) inputStream.read();
+ char secondByte = (char) inputStream.read();
+ char thirdByte = (char) inputStream.read();
+ char fourthByte = (char) inputStream.read();
+ return (firstByte << 24) | (secondByte << 16) | (thirdByte << 8) | fourthByte;
}
- private FrameHeader createFrameHeader(char[] source)
- throws ID3ReaderException {
- if (source.length != HEADER_LENGTH) {
- throw new ID3ReaderException("Length of header must be "
- + HEADER_LENGTH);
+ void expectChar(char expected) throws ID3ReaderException, IOException {
+ char read = (char) inputStream.read();
+ if (read != expected) {
+ throw new ID3ReaderException("Expected " + expected + " and got " + read);
}
- String id = new String(source, 0, FRAME_ID_LENGTH);
+ }
- int size = (((int) source[4]) << 24) | (((int) source[5]) << 16)
- | (((int) source[6]) << 8) | source[7];
+ @NonNull
+ TagHeader readTagHeader() throws ID3ReaderException, IOException {
+ expectChar('I');
+ expectChar('D');
+ expectChar('3');
+ short version = readShort();
+ byte flags = readByte();
+ int size = unsynchsafe(readInt());
+ return new TagHeader("ID3", size, version, flags);
+ }
+
+ @NonNull
+ FrameHeader readFrameHeader() throws IOException {
+ String id = readIsoStringFixed(FRAME_ID_LENGTH);
+ int size = readInt();
if (tagHeader != null && tagHeader.getVersion() >= 0x0400) {
size = unsynchsafe(size);
}
- char flags = (char) ((source[8] << 8) | source[9]);
+ short flags = readShort();
return new FrameHeader(id, size, flags);
}
@@ -162,81 +123,73 @@ public class ID3Reader {
return out;
}
- protected int readString(StringBuilder buffer, InputStream input, int max) throws IOException,
- ID3ReaderException {
- if (max > 0) {
- char[] encoding = readChars(input, 1);
- max--;
-
- if (encoding[0] == ENCODING_UTF16_WITH_BOM || encoding[0] == ENCODING_UTF16_WITHOUT_BOM) {
- return readUnicodeString(buffer, input, max, Charset.forName("UTF-16")) + 1; // take encoding byte into account
- } else if (encoding[0] == ENCODING_UTF8) {
- return readUnicodeString(buffer, input, max, Charset.forName("UTF-8")) + 1; // take encoding byte into account
- } else {
- return readISOString(buffer, input, max) + 1; // take encoding byte into account
- }
- } else {
- if (buffer != null) {
- buffer.append("");
- }
- return 0;
- }
+ /**
+ * Reads a null-terminated string with encoding.
+ */
+ protected String readEncodingAndString(int max) throws IOException {
+ byte encoding = readByte();
+ return readEncodedString(encoding, max - 1);
}
- protected int readISOString(StringBuilder buffer, InputStream input, int max)
- throws IOException, ID3ReaderException {
+ @SuppressWarnings("CharsetObjectCanBeUsed")
+ protected String readIsoStringFixed(int length) throws IOException {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
int bytesRead = 0;
- char c;
- while (++bytesRead <= max && (c = (char) input.read()) > 0) {
- if (buffer != null) {
- buffer.append(c);
- }
+ while (bytesRead < length) {
+ bytes.write(readByte());
+ bytesRead++;
}
- return bytesRead;
- }
-
- private int readUnicodeString(StringBuilder strBuffer, InputStream input, int max, Charset charset)
- throws IOException, ID3ReaderException {
- byte[] buffer = new byte[max];
- int c;
- int cZero = -1;
- int i = 0;
- for (; i < max; i++) {
- c = input.read();
- if (c == -1) {
- break;
- } else if (c == 0) {
- if (cZero == 0) {
- // termination character found
- break;
- } else {
- cZero = 0;
- }
- } else {
- buffer[i] = (byte) c;
- cZero = -1;
- }
- }
- if (strBuffer != null) {
- strBuffer.append(charset.newDecoder().decode(ByteBuffer.wrap(buffer)).toString());
- }
- return i;
+ return Charset.forName("ISO-8859-1").newDecoder().decode(ByteBuffer.wrap(bytes.toByteArray())).toString();
}
- int onStartTagHeader(TagHeader header) {
- return ACTION_SKIP;
+ protected String readIsoStringNullTerminated(int max) throws IOException {
+ return readEncodedString(ENCODING_ISO, max);
}
- int onStartFrameHeader(FrameHeader header, CountingInputStream input) throws IOException, ID3ReaderException {
- return ACTION_SKIP;
+ @SuppressWarnings("CharsetObjectCanBeUsed")
+ String readEncodedString(int encoding, int max) throws IOException {
+ if (encoding == ENCODING_UTF16_WITH_BOM || encoding == ENCODING_UTF16_WITHOUT_BOM) {
+ return readEncodedString2(Charset.forName("UTF-16"), max);
+ } else if (encoding == ENCODING_UTF8) {
+ return readEncodedString2(Charset.forName("UTF-8"), max);
+ } else {
+ return readEncodedString1(Charset.forName("ISO-8859-1"), max);
+ }
}
- void onEndTag() {
-
+ /**
+ * Reads chars where the encoding uses 1 char per symbol.
+ */
+ private String readEncodedString1(Charset charset, int max) throws IOException {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ int bytesRead = 0;
+ while (bytesRead < max) {
+ byte c = readByte();
+ bytesRead++;
+ if (c == 0) {
+ break;
+ }
+ bytes.write(c);
+ }
+ return charset.newDecoder().decode(ByteBuffer.wrap(bytes.toByteArray())).toString();
}
- void onNoTagHeaderFound() {
-
+ /**
+ * Reads chars where the encoding uses 2 chars per symbol.
+ */
+ private String readEncodedString2(Charset charset, int max) throws IOException {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ int bytesRead = 0;
+ while (bytesRead + 1 < max) {
+ byte c1 = readByte();
+ byte c2 = readByte();
+ if (c1 == 0 && c2 == 0) {
+ break;
+ }
+ bytesRead += 2;
+ bytes.write(c1);
+ bytes.write(c2);
+ }
+ return charset.newDecoder().decode(ByteBuffer.wrap(bytes.toByteArray())).toString();
}
-
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/FrameHeader.java b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/FrameHeader.java
index 2f3f378ab..e4af89a86 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/FrameHeader.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/FrameHeader.java
@@ -1,17 +1,19 @@
package de.danoeh.antennapod.core.util.id3reader.model;
-public class FrameHeader extends Header {
+import androidx.annotation.NonNull;
- private final char flags;
+public class FrameHeader extends Header {
+ private final short flags;
- public FrameHeader(String id, int size, char flags) {
- super(id, size);
- this.flags = flags;
- }
+ public FrameHeader(String id, int size, short flags) {
+ super(id, size);
+ this.flags = flags;
+ }
- @Override
- public String toString() {
- return String.format("FrameHeader [flags=%s, id=%s, size=%s]", Integer.toBinaryString(flags), id, Integer.toBinaryString(size));
+ @Override
+ @NonNull
+ public String toString() {
+ return String.format("FrameHeader [flags=%s, id=%s, size=%s]", Integer.toBinaryString(flags), id, size);
}
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/TagHeader.java b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/TagHeader.java
index b652a139c..2590db029 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/TagHeader.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/TagHeader.java
@@ -1,26 +1,25 @@
package de.danoeh.antennapod.core.util.id3reader.model;
-public class TagHeader extends Header {
-
- private final char version;
- private final byte flags;
+import androidx.annotation.NonNull;
- public TagHeader(String id, int size, char version, byte flags) {
- super(id, size);
- this.version = version;
- this.flags = flags;
- }
-
- @Override
- public String toString() {
- return "TagHeader [version=" + version + ", flags=" + flags + ", id="
- + id + ", size=" + size + "]";
- }
+public class TagHeader extends Header {
+ private final short version;
+ private final byte flags;
- public char getVersion() {
- return version;
- }
+ public TagHeader(String id, int size, short version, byte flags) {
+ super(id, size);
+ this.version = version;
+ this.flags = flags;
+ }
-
+ @Override
+ @NonNull
+ public String toString() {
+ return "TagHeader [version=" + version + ", flags=" + flags + ", id="
+ + id + ", size=" + size + "]";
+ }
+ public short getVersion() {
+ return version;
+ }
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java
index 0467c0a78..c948d98a3 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java
@@ -10,6 +10,7 @@ import org.antennapod.audio.MediaPlayer;
import de.danoeh.antennapod.core.preferences.UserPreferences;
+import java.io.IOException;
import java.util.Collections;
import java.util.List;
@@ -20,7 +21,7 @@ public class AudioPlayer extends MediaPlayer implements IPlayer {
super(context, true, ClientConfig.USER_AGENT);
PreferenceManager.getDefaultSharedPreferences(context)
.registerOnSharedPreferenceChangeListener((sharedPreferences, key) -> {
- if (key.equals(UserPreferences.PREF_MEDIA_PLAYER)) {
+ if (UserPreferences.PREF_MEDIA_PLAYER.equals(key)) {
checkMpi();
}
});
@@ -64,4 +65,9 @@ public class AudioPlayer extends MediaPlayer implements IPlayer {
public int getSelectedAudioTrack() {
return -1;
}
+
+ @Override
+ public void setDataSource(String streamUrl, String username, String password) throws IOException {
+ setDataSource(streamUrl);
+ }
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java
deleted file mode 100644
index 6c107996f..000000000
--- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java
+++ /dev/null
@@ -1,264 +0,0 @@
-package de.danoeh.antennapod.core.util.playback;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.content.SharedPreferences.Editor;
-import android.media.MediaMetadataRetriever;
-import android.os.Parcel;
-import android.os.Parcelable;
-import de.danoeh.antennapod.core.feed.Chapter;
-import de.danoeh.antennapod.core.feed.MediaType;
-import de.danoeh.antennapod.core.util.ChapterUtils;
-import java.util.List;
-import java.util.concurrent.Callable;
-import org.apache.commons.io.FilenameUtils;
-
-/** Represents a media file that is stored on the local storage device. */
-public class ExternalMedia implements Playable {
- public static final int PLAYABLE_TYPE_EXTERNAL_MEDIA = 2;
- public static final String PREF_SOURCE_URL = "ExternalMedia.PrefSourceUrl";
- public static final String PREF_POSITION = "ExternalMedia.PrefPosition";
- public static final String PREF_MEDIA_TYPE = "ExternalMedia.PrefMediaType";
- public static final String PREF_LAST_PLAYED_TIME = "ExternalMedia.PrefLastPlayedTime";
-
- private final String source;
- private String episodeTitle;
- private String feedTitle;
- private MediaType mediaType;
- private List<Chapter> chapters;
- private int duration;
- private int position;
- private long lastPlayedTime;
-
- /**
- * Creates a new playable for files on the sd card.
- * @param source File path of the file
- * @param mediaType Type of the file
- */
- public ExternalMedia(String source, MediaType mediaType) {
- super();
- this.source = source;
- this.mediaType = mediaType;
- }
-
- /**
- * Creates a new playable for files on the sd card.
- * @param source File path of the file
- * @param mediaType Type of the file
- * @param position Position to start from
- * @param lastPlayedTime Timestamp when it was played last
- */
- public ExternalMedia(String source, MediaType mediaType, int position, long lastPlayedTime) {
- this(source, mediaType);
- this.position = position;
- this.lastPlayedTime = lastPlayedTime;
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeString(source);
- dest.writeString(mediaType.toString());
- dest.writeInt(position);
- dest.writeLong(lastPlayedTime);
- }
-
- @Override
- public void writeToPreferences(Editor prefEditor) {
- prefEditor.putString(PREF_SOURCE_URL, source);
- prefEditor.putString(PREF_MEDIA_TYPE, mediaType.toString());
- prefEditor.putInt(PREF_POSITION, position);
- prefEditor.putLong(PREF_LAST_PLAYED_TIME, lastPlayedTime);
- }
-
- @Override
- public void loadMetadata() throws PlayableException {
- MediaMetadataRetriever mmr = new MediaMetadataRetriever();
- try {
- mmr.setDataSource(source);
- } catch (IllegalArgumentException e) {
- e.printStackTrace();
- throw new PlayableException("IllegalArgumentException when setting up MediaMetadataReceiver");
- } catch (RuntimeException e) {
- // http://code.google.com/p/android/issues/detail?id=39770
- e.printStackTrace();
- throw new PlayableException("RuntimeException when setting up MediaMetadataRetriever");
- }
- episodeTitle = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
- if (episodeTitle == null) {
- episodeTitle = FilenameUtils.getName(source);
- }
- feedTitle = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM);
- try {
- duration = Integer.parseInt(mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION));
- } catch (NumberFormatException e) {
- e.printStackTrace();
- throw new PlayableException("NumberFormatException when reading duration of media file");
- }
- setChapters(ChapterUtils.loadChaptersFromFileUrl(this));
- }
-
- @Override
- public void loadChapterMarks(Context context) {
-
- }
-
- @Override
- public String getEpisodeTitle() {
- return episodeTitle;
- }
-
- @Override
- public Callable<String> loadShownotes() {
- return () -> "";
- }
-
- @Override
- public List<Chapter> getChapters() {
- return chapters;
- }
-
- @Override
- public String getWebsiteLink() {
- return null;
- }
-
- @Override
- public String getPaymentLink() {
- return null;
- }
-
- @Override
- public String getFeedTitle() {
- return feedTitle;
- }
-
- @Override
- public Object getIdentifier() {
- return source;
- }
-
- @Override
- public int getDuration() {
- return duration;
- }
-
- @Override
- public int getPosition() {
- return position;
- }
-
- @Override
- public long getLastPlayedTime() {
- return lastPlayedTime;
- }
-
- @Override
- public MediaType getMediaType() {
- return mediaType;
- }
-
- @Override
- public String getLocalMediaUrl() {
- return source;
- }
-
- @Override
- public String getStreamUrl() {
- return null;
- }
-
- @Override
- public boolean localFileAvailable() {
- return true;
- }
-
- @Override
- public boolean streamAvailable() {
- return false;
- }
-
- @Override
- public void saveCurrentPosition(SharedPreferences pref, int newPosition, long timestamp) {
- SharedPreferences.Editor editor = pref.edit();
- editor.putInt(PREF_POSITION, newPosition);
- editor.putLong(PREF_LAST_PLAYED_TIME, timestamp);
- position = newPosition;
- lastPlayedTime = timestamp;
- editor.apply();
- }
-
- @Override
- public void setPosition(int newPosition) {
- position = newPosition;
- }
-
- @Override
- public void setDuration(int newDuration) {
- duration = newDuration;
- }
-
- @Override
- public void setLastPlayedTime(long lastPlayedTime) {
- this.lastPlayedTime = lastPlayedTime;
- }
-
- @Override
- public void onPlaybackStart() {
-
- }
-
- @Override
- public void onPlaybackPause(Context context) {
-
- }
-
- @Override
- public void onPlaybackCompleted(Context context) {
-
- }
-
- @Override
- public int getPlayableType() {
- return PLAYABLE_TYPE_EXTERNAL_MEDIA;
- }
-
- @Override
- public void setChapters(List<Chapter> chapters) {
- this.chapters = chapters;
- }
-
- public static final Parcelable.Creator<ExternalMedia> CREATOR = new Parcelable.Creator<ExternalMedia>() {
- public ExternalMedia createFromParcel(Parcel in) {
- String source = in.readString();
- MediaType type = MediaType.valueOf(in.readString());
- int position = 0;
- if (in.dataAvail() > 0) {
- position = in.readInt();
- }
- long lastPlayedTime = 0;
- if (in.dataAvail() > 0) {
- lastPlayedTime = in.readLong();
- }
-
- return new ExternalMedia(source, type, position, lastPlayedTime);
- }
-
- public ExternalMedia[] newArray(int size) {
- return new ExternalMedia[size];
- }
- };
-
- @Override
- public String getImageLocation() {
- if (localFileAvailable()) {
- return getLocalMediaUrl();
- } else {
- return null;
- }
- }
-}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java
index 363004709..a511916fa 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java
@@ -35,6 +35,8 @@ public interface IPlayer {
void setDataSource(String path) throws IllegalStateException, IOException,
IllegalArgumentException, SecurityException;
+ void setDataSource(String streamUrl, String username, String password) throws IOException;
+
void setDisplay(SurfaceHolder sh);
void setPlaybackParams(float speed, boolean skipSilence);
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java
index 5b15913c8..feba6db1c 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java
@@ -3,24 +3,18 @@ package de.danoeh.antennapod.core.util.playback;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Parcelable;
-import androidx.preference.PreferenceManager;
-import android.util.Log;
+
import androidx.annotation.Nullable;
-import de.danoeh.antennapod.core.asynctask.ImageResource;
import de.danoeh.antennapod.core.feed.Chapter;
-import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.MediaType;
-import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
-import de.danoeh.antennapod.core.storage.DBReader;
-import de.danoeh.antennapod.core.util.ShownotesProvider;
-
+import java.util.Date;
import java.util.List;
/**
* Interface for objects that can be played by the PlaybackService.
*/
-public interface Playable extends Parcelable,
- ShownotesProvider, ImageResource {
+public interface Playable extends Parcelable {
+ public static final int INVALID_TIME = -1;
/**
* Save information about the playable in a preference so that it can be
@@ -39,13 +33,6 @@ public interface Playable extends Parcelable,
void loadMetadata() throws PlayableException;
/**
- * This method is called from a separate thread by the PlaybackService.
- * Playable objects should load their chapter marks in this method if no
- * local file was available when loadMetadata() was called.
- */
- void loadChapterMarks(Context context);
-
- /**
* Returns the title of the episode that this playable represents
*/
String getEpisodeTitle();
@@ -68,6 +55,11 @@ public interface Playable extends Parcelable,
String getFeedTitle();
/**
+ * Returns the published date
+ */
+ Date getPubDate();
+
+ /**
* Returns a unique identifier, for example a file url or an ID from a
* database.
*/
@@ -90,6 +82,13 @@ public interface Playable extends Parcelable,
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. This method should return the correct value
* BEFORE loadMetadata() is called.
*/
@@ -172,99 +171,11 @@ public interface Playable extends Parcelable,
void setChapters(List<Chapter> chapters);
/**
- * Provides utility methods for Playable objects.
+ * 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.
*/
- class PlayableUtils {
- private PlayableUtils(){}
-
- private static final String TAG = "PlayableUtils";
-
- /**
- * Restores a playable object from a sharedPreferences file. This method might load data from the database,
- * depending on the type of playable that was restored.
- *
- * @return The restored Playable object
- */
- @Nullable
- public static Playable createInstanceFromPreferences(Context context) {
- long currentlyPlayingMedia = PlaybackPreferences.getCurrentlyPlayingMediaType();
- if (currentlyPlayingMedia != PlaybackPreferences.NO_MEDIA_PLAYING) {
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
- return PlayableUtils.createInstanceFromPreferences(context,
- (int) currentlyPlayingMedia, prefs);
- }
- return null;
- }
-
- /**
- * Restores a playable object from a sharedPreferences file. This method might load data from the database,
- * depending on the type of playable that was restored.
- *
- * @param type An integer that represents the type of the Playable object
- * that is restored.
- * @param pref The SharedPreferences file from which the Playable object
- * is restored
- * @return The restored Playable object
- */
- public static Playable createInstanceFromPreferences(Context context, int type,
- SharedPreferences pref) {
- Playable result = null;
- // ADD new Playable types here:
- switch (type) {
- case FeedMedia.PLAYABLE_TYPE_FEEDMEDIA:
- result = createFeedMediaInstance(pref);
- break;
- case ExternalMedia.PLAYABLE_TYPE_EXTERNAL_MEDIA:
- result = createExternalMediaInstance(pref);
- break;
- }
- if (result == null) {
- Log.e(TAG, "Could not restore Playable object from preferences");
- }
- return result;
- }
-
- private static Playable createFeedMediaInstance(SharedPreferences pref) {
- Playable result = null;
- long mediaId = pref.getLong(FeedMedia.PREF_MEDIA_ID, -1);
- if (mediaId != -1) {
- result = DBReader.getFeedMedia(mediaId);
- }
- return result;
- }
-
- private static Playable createExternalMediaInstance(SharedPreferences pref) {
- Playable result = null;
- String source = pref.getString(ExternalMedia.PREF_SOURCE_URL, null);
- String mediaType = pref.getString(ExternalMedia.PREF_MEDIA_TYPE, null);
- if (source != null && mediaType != null) {
- int position = pref.getInt(ExternalMedia.PREF_POSITION, 0);
- long lastPlayedTime = pref.getLong(ExternalMedia.PREF_LAST_PLAYED_TIME, 0);
- result = new ExternalMedia(source, MediaType.valueOf(mediaType),
- position, lastPlayedTime);
- }
- return result;
- }
- }
-
- class PlayableException extends Exception {
- private static final long serialVersionUID = 1L;
-
- public PlayableException() {
- super();
- }
-
- public PlayableException(String detailMessage, Throwable throwable) {
- super(detailMessage, throwable);
- }
-
- public PlayableException(String detailMessage) {
- super(detailMessage);
- }
-
- public PlayableException(Throwable throwable) {
- super(throwable);
- }
-
- }
+ @Nullable
+ String getImageLocation();
+
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlayableException.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlayableException.java
new file mode 100644
index 000000000..c0c21d647
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlayableException.java
@@ -0,0 +1,13 @@
+package de.danoeh.antennapod.core.util.playback;
+
+/**
+ * Exception thrown by {@link Playable} implementations.
+ */
+public class PlayableException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ public PlayableException(String detailMessage) {
+ super(detailMessage);
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlayableUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlayableUtils.java
new file mode 100644
index 000000000..861d42c1b
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlayableUtils.java
@@ -0,0 +1,73 @@
+package de.danoeh.antennapod.core.util.playback;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.preference.PreferenceManager;
+
+import de.danoeh.antennapod.core.feed.FeedMedia;
+import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
+import de.danoeh.antennapod.core.storage.DBReader;
+
+/**
+ * Provides utility methods for Playable objects.
+ */
+public abstract class PlayableUtils {
+
+ private static final String TAG = "PlayableUtils";
+
+ /**
+ * Restores a playable object from a sharedPreferences file. This method might load data from the database,
+ * depending on the type of playable that was restored.
+ *
+ * @return The restored Playable object
+ */
+ @Nullable
+ public static Playable createInstanceFromPreferences(@NonNull Context context) {
+ long currentlyPlayingMedia = PlaybackPreferences.getCurrentlyPlayingMediaType();
+ if (currentlyPlayingMedia != PlaybackPreferences.NO_MEDIA_PLAYING) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
+ return PlayableUtils.createInstanceFromPreferences((int) currentlyPlayingMedia, prefs);
+ }
+ return null;
+ }
+
+ /**
+ * Restores a playable object from a sharedPreferences file. This method might load data from the database,
+ * depending on the type of playable that was restored.
+ *
+ * @param type An integer that represents the type of the Playable object
+ * that is restored.
+ * @param pref The SharedPreferences file from which the Playable object
+ * is restored
+ * @return The restored Playable object
+ */
+ private static Playable createInstanceFromPreferences(int type, SharedPreferences pref) {
+ Playable result;
+ // ADD new Playable types here:
+ switch (type) {
+ case FeedMedia.PLAYABLE_TYPE_FEEDMEDIA:
+ result = createFeedMediaInstance(pref);
+ break;
+ default:
+ result = null;
+ break;
+ }
+ if (result == null) {
+ Log.e(TAG, "Could not restore Playable object from preferences");
+ }
+ return result;
+ }
+
+ private static Playable createFeedMediaInstance(SharedPreferences pref) {
+ Playable result = null;
+ long mediaId = pref.getLong(FeedMedia.PREF_MEDIA_ID, -1);
+ if (mediaId != -1) {
+ result = DBReader.getFeedMedia(mediaId);
+ }
+ return result;
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java
index e1b4c967c..117e32cd4 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java
@@ -17,12 +17,10 @@ import android.util.Pair;
import android.view.SurfaceHolder;
import android.widget.ImageButton;
import androidx.annotation.NonNull;
-import androidx.core.content.ContextCompat;
import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.core.event.MessageEvent;
import de.danoeh.antennapod.core.event.ServiceEvent;
import de.danoeh.antennapod.core.feed.Chapter;
-import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.MediaType;
import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
@@ -30,13 +28,9 @@ import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer;
import de.danoeh.antennapod.core.service.playback.PlayerStatus;
-import de.danoeh.antennapod.core.storage.DBTasks;
-import de.danoeh.antennapod.core.util.Optional;
-import de.danoeh.antennapod.core.util.ThemeUtils;
-import de.danoeh.antennapod.core.util.playback.Playable.PlayableUtils;
+import de.danoeh.antennapod.ui.common.ThemeUtils;
import io.reactivex.Maybe;
import io.reactivex.MaybeOnSubscribe;
-import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
@@ -51,7 +45,7 @@ import java.util.List;
* Communicates with the playback service. GUI classes should use this class to
* control playback instead of communicating with the PlaybackService directly.
*/
-public class PlaybackController {
+public abstract class PlaybackController {
private static final String TAG = "PlaybackController";
private static final int INVALID_TIME = -1;
@@ -66,7 +60,6 @@ public class PlaybackController {
private boolean initialized = false;
private boolean eventsRegistered = false;
- private Disposable serviceBinder;
private Disposable mediaLoader;
public PlaybackController(@NonNull Activity activity) {
@@ -153,9 +146,6 @@ public class PlaybackController {
}
private void unbind() {
- if (serviceBinder != null) {
- serviceBinder.dispose();
- }
try {
activity.unbindService(mConnection);
} catch (IllegalArgumentException e) {
@@ -178,56 +168,11 @@ public class PlaybackController {
*/
private void bindToService() {
Log.d(TAG, "Trying to connect to service");
- if (serviceBinder != null) {
- serviceBinder.dispose();
- }
- serviceBinder = Observable.fromCallable(this::getPlayLastPlayedMediaIntent)
- .subscribeOn(Schedulers.io())
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(optionalIntent -> {
- boolean bound = false;
- if (!PlaybackService.isRunning) {
- if (optionalIntent.isPresent()) {
- Log.d(TAG, "Calling start service");
- ContextCompat.startForegroundService(activity, optionalIntent.get());
- bound = activity.bindService(optionalIntent.get(), mConnection, 0);
- } else {
- status = PlayerStatus.STOPPED;
- setupGUI();
- handleStatus();
- }
- } else {
- Log.d(TAG, "PlaybackService is running, trying to connect without start command.");
- bound = activity.bindService(new Intent(activity, PlaybackService.class),
- mConnection, 0);
- }
- Log.d(TAG, "Result for service binding: " + bound);
- }, error -> Log.e(TAG, Log.getStackTraceString(error)));
- }
-
- /**
- * Returns an intent that starts the PlaybackService and plays the last
- * played media or null if no last played media could be found.
- */
- @NonNull
- private Optional<Intent> getPlayLastPlayedMediaIntent() {
- Log.d(TAG, "Trying to restore last played media");
- Playable media = PlayableUtils.createInstanceFromPreferences(activity);
- if (media == null) {
- Log.d(TAG, "No last played media found");
- return Optional.empty();
+ if (!PlaybackService.isRunning) {
+ throw new IllegalStateException("Trying to bind but service is not running");
}
-
- boolean fileExists = media.localFileAvailable();
- boolean lastIsStream = PlaybackPreferences.getCurrentEpisodeIsStream();
- if (!fileExists && !lastIsStream && media instanceof FeedMedia) {
- DBTasks.notifyMissingFeedMediaFile(activity, (FeedMedia) media);
- }
-
- return Optional.of(new PlaybackServiceStarter(activity, media)
- .startWhenPrepared(false)
- .shouldStream(lastIsStream || !fileExists)
- .getIntent());
+ boolean bound = activity.bindService(new Intent(activity, PlaybackService.class), mConnection, 0);
+ Log.d(TAG, "Result for service binding: " + bound);
}
private final ServiceConnection mConnection = new ServiceConnection() {
@@ -331,8 +276,6 @@ public class PlaybackController {
}
};
- public void setupGUI() {}
-
public void onPositionObserverUpdate() {}
@@ -431,7 +374,10 @@ public class PlaybackController {
}
private void checkMediaInfoLoaded() {
- mediaInfoLoaded = (mediaInfoLoaded || loadMediaInfo());
+ if (!mediaInfoLoaded) {
+ loadMediaInfo();
+ }
+ mediaInfoLoaded = true;
}
private void updatePlayButtonAppearance(int resource, CharSequence contentDescription) {
@@ -446,9 +392,7 @@ public class PlaybackController {
return null;
}
- public boolean loadMediaInfo() {
- return false;
- }
+ public abstract void loadMediaInfo();
public void onAwaitingVideoSurface() {}
@@ -463,10 +407,9 @@ public class PlaybackController {
status = info.playerStatus;
media = info.playable;
- setupGUI();
- handleStatus();
// make sure that new media is loaded if it's available
mediaInfoLoaded = false;
+ handleStatus();
} else {
Log.e(TAG,
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/RemoteMedia.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/RemoteMedia.java
index 29eb20aca..926eaa315 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/RemoteMedia.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/RemoteMedia.java
@@ -11,10 +11,8 @@ import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.MediaType;
-import de.danoeh.antennapod.core.util.ChapterUtils;
import java.util.Date;
import java.util.List;
-import java.util.concurrent.Callable;
import org.apache.commons.lang3.builder.HashCodeBuilder;
/**
@@ -129,11 +127,6 @@ public class RemoteMedia implements Playable {
}
@Override
- public void loadChapterMarks(Context context) {
- setChapters(ChapterUtils.loadChaptersFromStreamUrl(this, context));
- }
-
- @Override
public String getEpisodeTitle() {
return episodeTitle;
}
@@ -266,8 +259,8 @@ public class RemoteMedia implements Playable {
}
@Override
- public Callable<String> loadShownotes() {
- return () -> (notes != null) ? notes : "";
+ public String getDescription() {
+ return notes;
}
@Override
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Timeline.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Timeline.java
index 40849a262..e125c7e66 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Timeline.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Timeline.java
@@ -9,7 +9,7 @@ import android.text.TextUtils;
import android.util.Log;
import android.util.TypedValue;
-import de.danoeh.antennapod.core.feed.FeedItem;
+import androidx.annotation.Nullable;
import org.apache.commons.io.IOUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
@@ -24,7 +24,6 @@ import java.util.regex.Pattern;
import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.core.util.Converter;
-import de.danoeh.antennapod.core.util.ShownotesProvider;
/**
* Connects chapter information and shownotes of a shownotesProvider, for example by making it possible to use the
@@ -42,17 +41,16 @@ public class Timeline {
private static final Pattern TIMECODE_REGEX = Pattern.compile("\\b((\\d+):)?(\\d+):(\\d{2})\\b");
private static final Pattern LINE_BREAK_REGEX = Pattern.compile("<br */?>");
- private final ShownotesProvider shownotesProvider;
+ private final String rawShownotes;
private final String noShownotesLabel;
+ private final int playableDuration;
private final String webviewStyle;
- public Timeline(Context context, ShownotesProvider shownotesProvider) {
- if (shownotesProvider == null) {
- throw new IllegalArgumentException("shownotesProvider = null");
- }
- this.shownotesProvider = shownotesProvider;
+ public Timeline(Context context, @Nullable String rawShownotes, int playableDuration) {
+ this.rawShownotes = rawShownotes;
noShownotesLabel = context.getString(R.string.no_shownotes_label);
+ this.playableDuration = playableDuration;
final String colorPrimary = colorToHtml(context, android.R.attr.textColorPrimary);
final String colorAccent = colorToHtml(context, R.attr.colorAccent);
final int margin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8,
@@ -87,13 +85,7 @@ public class Timeline {
*/
@NonNull
public String processShownotes() {
- String shownotes;
- try {
- shownotes = shownotesProvider.loadShownotes().call();
- } catch (Exception e) {
- Log.e(TAG, "processShownotes() - encounters exceptions unexpectedly in load, treat as if no shownotes.", e);
- shownotes = "";
- }
+ String shownotes = rawShownotes;
if (TextUtils.isEmpty(shownotes)) {
Log.d(TAG, "shownotesProvider contained no shownotes. Returning 'no shownotes' message");
@@ -147,14 +139,6 @@ public class Timeline {
// No elements with timecodes
return;
}
-
- int playableDuration = Integer.MAX_VALUE;
- if (shownotesProvider instanceof Playable) {
- playableDuration = ((Playable) shownotesProvider).getDuration();
- } else if (shownotesProvider instanceof FeedItem && ((FeedItem) shownotesProvider).getMedia() != null) {
- playableDuration = ((FeedItem) shownotesProvider).getMedia().getDuration();
- }
-
boolean useHourFormat = true;
if (playableDuration != Integer.MAX_VALUE) {
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java
index d18801870..6728c027d 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java
@@ -3,6 +3,7 @@ package de.danoeh.antennapod.core.util.playback;
import android.media.MediaPlayer;
import android.util.Log;
+import java.io.IOException;
import java.util.Collections;
import java.util.List;
@@ -52,4 +53,9 @@ public class VideoPlayer extends MediaPlayer implements IPlayer {
public int getSelectedAudioTrack() {
return -1;
}
+
+ @Override
+ public void setDataSource(String streamUrl, String username, String password) throws IOException {
+ setDataSource(streamUrl);
+ }
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdater.java b/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdater.java
new file mode 100644
index 000000000..afbe6526b
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdater.java
@@ -0,0 +1,229 @@
+package de.danoeh.antennapod.core.widget;
+
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.RemoteViews;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.request.RequestOptions;
+
+import java.util.concurrent.TimeUnit;
+
+import de.danoeh.antennapod.core.R;
+import de.danoeh.antennapod.core.feed.MediaType;
+import de.danoeh.antennapod.core.glide.ApGlideSettings;
+import de.danoeh.antennapod.core.receiver.MediaButtonReceiver;
+import de.danoeh.antennapod.core.receiver.PlayerWidget;
+import de.danoeh.antennapod.core.service.playback.PlayerStatus;
+import de.danoeh.antennapod.core.util.Converter;
+import de.danoeh.antennapod.core.feed.util.ImageResourceUtils;
+import de.danoeh.antennapod.core.util.TimeSpeedConverter;
+import de.danoeh.antennapod.core.util.playback.Playable;
+import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter;
+import de.danoeh.antennapod.ui.appstartintent.VideoPlayerActivityStarter;
+
+/**
+ * Updates the state of the player widget.
+ */
+public abstract class WidgetUpdater {
+ private static final String TAG = "WidgetUpdater";
+
+ public static class WidgetState {
+ final Playable media;
+ final PlayerStatus status;
+ final int position;
+ final int duration;
+ final float playbackSpeed;
+ final boolean isCasting;
+
+ public WidgetState(Playable media, PlayerStatus status, int position, int duration,
+ float playbackSpeed, boolean isCasting) {
+ this.media = media;
+ this.status = status;
+ this.position = position;
+ this.duration = duration;
+ this.playbackSpeed = playbackSpeed;
+ this.isCasting = isCasting;
+ }
+
+ public WidgetState(PlayerStatus status) {
+ this(null, status, Playable.INVALID_TIME, Playable.INVALID_TIME, 1.0f, false);
+ }
+ }
+
+ /**
+ * Update the widgets with the given parameters. Must be called in a background thread.
+ */
+ public static void updateWidget(Context context, WidgetState widgetState) {
+ if (!PlayerWidget.isEnabled(context) || widgetState == null) {
+ return;
+ }
+ ComponentName playerWidget = new ComponentName(context, PlayerWidget.class);
+ AppWidgetManager manager = AppWidgetManager.getInstance(context);
+ int[] widgetIds = manager.getAppWidgetIds(playerWidget);
+
+ PendingIntent startMediaPlayer;
+ if (widgetState.media != null && widgetState.media.getMediaType() == MediaType.VIDEO
+ && !widgetState.isCasting) {
+ startMediaPlayer = new VideoPlayerActivityStarter(context).getPendingIntent();
+ } else {
+ startMediaPlayer = new MainActivityStarter(context).withOpenPlayer().getPendingIntent();
+ }
+ RemoteViews views;
+ views = new RemoteViews(context.getPackageName(), R.layout.player_widget);
+
+ if (widgetState.media != null) {
+ Bitmap icon;
+ int iconSize = context.getResources().getDimensionPixelSize(android.R.dimen.app_icon_size);
+ views.setOnClickPendingIntent(R.id.layout_left, startMediaPlayer);
+ views.setOnClickPendingIntent(R.id.imgvCover, startMediaPlayer);
+
+ try {
+ icon = Glide.with(context)
+ .asBitmap()
+ .load(widgetState.media.getImageLocation())
+ .apply(RequestOptions.diskCacheStrategyOf(ApGlideSettings.AP_DISK_CACHE_STRATEGY))
+ .submit(iconSize, iconSize)
+ .get(500, TimeUnit.MILLISECONDS);
+ views.setImageViewBitmap(R.id.imgvCover, icon);
+ } catch (Throwable tr1) {
+ try {
+ icon = Glide.with(context)
+ .asBitmap()
+ .load(ImageResourceUtils.getFallbackImageLocation(widgetState.media))
+ .apply(RequestOptions.diskCacheStrategyOf(ApGlideSettings.AP_DISK_CACHE_STRATEGY))
+ .submit(iconSize, iconSize)
+ .get(500, TimeUnit.MILLISECONDS);
+ views.setImageViewBitmap(R.id.imgvCover, icon);
+ } catch (Throwable tr2) {
+ Log.e(TAG, "Error loading the media icon for the widget", tr2);
+ views.setImageViewResource(R.id.imgvCover, R.mipmap.ic_launcher_round);
+ }
+ }
+
+ views.setTextViewText(R.id.txtvTitle, widgetState.media.getEpisodeTitle());
+ views.setViewVisibility(R.id.txtvTitle, View.VISIBLE);
+ views.setViewVisibility(R.id.txtNoPlaying, View.GONE);
+
+ String progressString = getProgressString(widgetState.position,
+ widgetState.duration, widgetState.playbackSpeed);
+ if (progressString != null) {
+ views.setViewVisibility(R.id.txtvProgress, View.VISIBLE);
+ views.setTextViewText(R.id.txtvProgress, progressString);
+ }
+
+ if (widgetState.status == PlayerStatus.PLAYING) {
+ views.setImageViewResource(R.id.butPlay, R.drawable.ic_av_pause_white_48dp);
+ views.setContentDescription(R.id.butPlay, context.getString(R.string.pause_label));
+ views.setImageViewResource(R.id.butPlayExtended, R.drawable.ic_av_pause_white_48dp);
+ views.setContentDescription(R.id.butPlayExtended, context.getString(R.string.pause_label));
+ } else {
+ views.setImageViewResource(R.id.butPlay, R.drawable.ic_av_play_white_48dp);
+ views.setContentDescription(R.id.butPlay, context.getString(R.string.play_label));
+ views.setImageViewResource(R.id.butPlayExtended, R.drawable.ic_av_play_white_48dp);
+ views.setContentDescription(R.id.butPlayExtended, context.getString(R.string.play_label));
+ }
+ views.setOnClickPendingIntent(R.id.butPlay,
+ createMediaButtonIntent(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE));
+ views.setOnClickPendingIntent(R.id.butPlayExtended,
+ createMediaButtonIntent(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE));
+ views.setOnClickPendingIntent(R.id.butRew,
+ createMediaButtonIntent(context, KeyEvent.KEYCODE_MEDIA_REWIND));
+ views.setOnClickPendingIntent(R.id.butFastForward,
+ createMediaButtonIntent(context, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD));
+ views.setOnClickPendingIntent(R.id.butSkip,
+ createMediaButtonIntent(context, KeyEvent.KEYCODE_MEDIA_NEXT));
+ } else {
+ // start the app if they click anything
+ views.setOnClickPendingIntent(R.id.layout_left, startMediaPlayer);
+ views.setOnClickPendingIntent(R.id.butPlay, startMediaPlayer);
+ views.setOnClickPendingIntent(R.id.butPlayExtended,
+ createMediaButtonIntent(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE));
+ views.setViewVisibility(R.id.txtvProgress, View.GONE);
+ views.setViewVisibility(R.id.txtvTitle, View.GONE);
+ views.setViewVisibility(R.id.txtNoPlaying, View.VISIBLE);
+ views.setImageViewResource(R.id.imgvCover, R.mipmap.ic_launcher_round);
+ views.setImageViewResource(R.id.butPlay, R.drawable.ic_av_play_white_48dp);
+ views.setImageViewResource(R.id.butPlayExtended, R.drawable.ic_av_play_white_48dp);
+ }
+
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
+ for (int id : widgetIds) {
+ Bundle options = manager.getAppWidgetOptions(id);
+ SharedPreferences prefs = context.getSharedPreferences(PlayerWidget.PREFS_NAME, Context.MODE_PRIVATE);
+ int minWidth = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH);
+ int columns = getCellsForSize(minWidth);
+ if (columns < 3) {
+ views.setViewVisibility(R.id.layout_center, View.INVISIBLE);
+ } else {
+ views.setViewVisibility(R.id.layout_center, View.VISIBLE);
+ }
+ boolean showRewind = prefs.getBoolean(PlayerWidget.KEY_WIDGET_REWIND + id, false);
+ boolean showFastForward = prefs.getBoolean(PlayerWidget.KEY_WIDGET_FAST_FORWARD + id, false);
+ boolean showSkip = prefs.getBoolean(PlayerWidget.KEY_WIDGET_SKIP + id, false);
+
+ if (showRewind || showSkip || showFastForward) {
+ views.setInt(R.id.extendedButtonsContainer, "setVisibility", View.VISIBLE);
+ views.setInt(R.id.butPlay, "setVisibility", View.GONE);
+ views.setInt(R.id.butRew, "setVisibility", showRewind ? View.VISIBLE : View.GONE);
+ views.setInt(R.id.butFastForward, "setVisibility", showFastForward ? View.VISIBLE : View.GONE);
+ views.setInt(R.id.butSkip, "setVisibility", showSkip ? View.VISIBLE : View.GONE);
+ }
+
+ int backgroundColor = prefs.getInt(PlayerWidget.KEY_WIDGET_COLOR + id, PlayerWidget.DEFAULT_COLOR);
+ views.setInt(R.id.widgetLayout, "setBackgroundColor", backgroundColor);
+
+ manager.updateAppWidget(id, views);
+ }
+ } else {
+ manager.updateAppWidget(playerWidget, views);
+ }
+ }
+
+ /**
+ * Returns number of cells needed for given size of the widget.
+ *
+ * @param size Widget size in dp.
+ * @return Size in number of cells.
+ */
+ private static int getCellsForSize(int size) {
+ int n = 2;
+ while (70 * n - 30 < size) {
+ ++n;
+ }
+ return n - 1;
+ }
+
+ /**
+ * Creates an intent which fakes a mediabutton press.
+ */
+ private static PendingIntent createMediaButtonIntent(Context context, int eventCode) {
+ KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, eventCode);
+ Intent startingIntent = new Intent(context, MediaButtonReceiver.class);
+ startingIntent.setAction(MediaButtonReceiver.NOTIFY_BUTTON_RECEIVER);
+ startingIntent.putExtra(Intent.EXTRA_KEY_EVENT, event);
+
+ return PendingIntent.getBroadcast(context, eventCode, startingIntent, 0);
+ }
+
+ private static String getProgressString(int position, int duration, float speed) {
+ if (position >= 0 && duration > 0) {
+ TimeSpeedConverter converter = new TimeSpeedConverter(speed);
+ position = converter.convert(position);
+ duration = converter.convert(duration);
+ return Converter.getDurationStringLong(position) + " / "
+ + Converter.getDurationStringLong(duration);
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdaterJobService.java b/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdaterJobService.java
new file mode 100644
index 000000000..004588945
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdaterJobService.java
@@ -0,0 +1,35 @@
+package de.danoeh.antennapod.core.widget;
+
+import android.content.Context;
+import android.content.Intent;
+import androidx.annotation.NonNull;
+import androidx.core.app.SafeJobIntentService;
+import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils;
+import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
+import de.danoeh.antennapod.core.service.playback.PlayerStatus;
+import de.danoeh.antennapod.core.util.playback.Playable;
+import de.danoeh.antennapod.core.util.playback.PlayableUtils;
+
+public class WidgetUpdaterJobService extends SafeJobIntentService {
+ private static final int JOB_ID = -17001;
+
+ /**
+ * Loads the current media from the database and updates the widget in a background job.
+ */
+ public static void performBackgroundUpdate(Context context) {
+ enqueueWork(context, WidgetUpdaterJobService.class,
+ WidgetUpdaterJobService.JOB_ID, new Intent(context, WidgetUpdaterJobService.class));
+ }
+
+ @Override
+ protected void onHandleWork(@NonNull Intent intent) {
+ Playable media = PlayableUtils.createInstanceFromPreferences(getApplicationContext());
+ if (media != null) {
+ WidgetUpdater.updateWidget(this, new WidgetUpdater.WidgetState(media, PlayerStatus.STOPPED,
+ media.getPosition(), media.getDuration(), PlaybackSpeedUtils.getCurrentPlaybackSpeed(media),
+ PlaybackPreferences.getCurrentEpisodeIsStream()));
+ } else {
+ WidgetUpdater.updateWidget(this, new WidgetUpdater.WidgetState(PlayerStatus.STOPPED));
+ }
+ }
+} \ No newline at end of file