summaryrefslogtreecommitdiff
path: root/core/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'core/src/main')
-rw-r--r--core/src/main/AndroidManifest.xml14
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/event/DiscoveryDefaultUpdateEvent.java6
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/event/DownloadEvent.java3
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/event/DownloadLogEvent.java3
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/event/DownloaderUpdate.java1
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/event/FavoritesEvent.java3
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/event/FeedItemEvent.java3
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/ChapterMerger.java55
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java6
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/FeedComponent.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/FeedEvent.java3
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java8
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java34
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilterGroup.java36
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java39
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java7
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java162
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/SubscriptionsFilter.java106
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/SubscriptionsFilterGroup.java30
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideModule.java7
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java28
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/glide/AudioCoverFetcher.java14
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/glide/ChapterImageModelLoader.java10
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/glide/FastBlurTransformation.java5
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/glide/MetadataRetrieverLoader.java57
-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.java92
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java7
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/PlayerWidgetJobService.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequest.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java3
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java6
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java8
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java3
-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.java9
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java45
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java142
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java4
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java49
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/ShakeListener.java1
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java178
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java200
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java98
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DatabaseExporter.java12
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java146
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/StatisticsItem.java8
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java7
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/sync/gpoddernet/GpodnetService.java8
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/sync/model/EpisodeAction.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/sync/model/EpisodeActionChanges.java1
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/AtomText.java10
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.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/NetworkUtils.java172
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtils.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/comparator/ChapterStartTimeComparator.java8
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java22
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/download/AutoUpdateManager.java5
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/gui/NotificationUtils.java10
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java4
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java11
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java7
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/RemoteMedia.java4
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/OggInputStream.java2
-rw-r--r--core/src/main/res/color/filter_dialog_button_text.xml5
-rw-r--r--core/src/main/res/color/filter_dialog_clear_dark.xml5
-rw-r--r--core/src/main/res/color/filter_dialog_clear_light.xml5
-rw-r--r--core/src/main/res/drawable/filter_dialog_background_dark.xml5
-rw-r--r--core/src/main/res/drawable/filter_dialog_background_light.xml5
-rw-r--r--core/src/main/res/drawable/ic_filter_close.xml55
-rw-r--r--core/src/main/res/drawable/ic_notifications_black.xml9
-rw-r--r--core/src/main/res/drawable/ic_notifications_white.xml5
-rw-r--r--core/src/main/res/layout/player_widget.xml6
-rw-r--r--core/src/main/res/raw/local_feed_default_icon.pngbin0 -> 1240 bytes
-rw-r--r--core/src/main/res/values/arrays.xml24
-rw-r--r--core/src/main/res/values/attrs.xml3
-rw-r--r--core/src/main/res/values/colors.xml11
-rw-r--r--core/src/main/res/values/dimens.xml2
-rw-r--r--core/src/main/res/values/strings.xml105
-rw-r--r--core/src/main/res/values/styles.xml18
84 files changed, 1574 insertions, 726 deletions
diff --git a/core/src/main/AndroidManifest.xml b/core/src/main/AndroidManifest.xml
index 1f6c36c40..ae5e56e55 100644
--- a/core/src/main/AndroidManifest.xml
+++ b/core/src/main/AndroidManifest.xml
@@ -1,5 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="de.danoeh.antennapod.core">
+ xmlns:tools="http://schemas.android.com/tools" package="de.danoeh.antennapod.core">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
@@ -12,15 +12,19 @@
<application
android:allowBackup="true"
- android:icon="@mipmap/ic_launcher">
+ android:icon="@mipmap/ic_launcher"
+ android:supportsRtl="true">
<service
android:name=".service.download.DownloadService"
android:enabled="true" />
+
<service android:name=".service.playback.PlaybackService"
android:label="@string/app_name"
android:enabled="true"
- android:exported="true">
+ android:exported="true"
+ tools:ignore="ExportedService">
+
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService"/>
</intent-filter>
@@ -39,8 +43,8 @@
<receiver android:name=".receiver.FeedUpdateReceiver"
android:label="@string/feed_update_receiver_name"
- android:exported="true"> <!-- allow feeds update to be triggered by external apps -->
- </receiver>
+ android:exported="true"
+ tools:ignore="ExportedReceiver" /> <!-- allow feeds update to be triggered by external apps -->
</application>
</manifest>
diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/DiscoveryDefaultUpdateEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/DiscoveryDefaultUpdateEvent.java
new file mode 100644
index 000000000..f7757935a
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/event/DiscoveryDefaultUpdateEvent.java
@@ -0,0 +1,6 @@
+package de.danoeh.antennapod.core.event;
+
+public class DiscoveryDefaultUpdateEvent {
+ public DiscoveryDefaultUpdateEvent() {
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/DownloadEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/DownloadEvent.java
index 24a71ec96..efd53ab9d 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/event/DownloadEvent.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/event/DownloadEvent.java
@@ -1,5 +1,7 @@
package de.danoeh.antennapod.core.event;
+import androidx.annotation.NonNull;
+
import java.util.ArrayList;
import java.util.List;
@@ -19,6 +21,7 @@ public class DownloadEvent {
return new DownloadEvent(update);
}
+ @NonNull
@Override
public String toString() {
return "DownloadEvent{" +
diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/DownloadLogEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/DownloadLogEvent.java
index 7428c5b00..5ab5decf9 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/event/DownloadLogEvent.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/event/DownloadLogEvent.java
@@ -1,5 +1,7 @@
package de.danoeh.antennapod.core.event;
+import androidx.annotation.NonNull;
+
public class DownloadLogEvent {
private DownloadLogEvent() {
@@ -9,6 +11,7 @@ public class DownloadLogEvent {
return new DownloadLogEvent();
}
+ @NonNull
@Override
public String toString() {
return "DownloadLogEvent";
diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/DownloaderUpdate.java b/core/src/main/java/de/danoeh/antennapod/core/event/DownloaderUpdate.java
index f549940b7..10992408d 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/event/DownloaderUpdate.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/event/DownloaderUpdate.java
@@ -46,6 +46,7 @@ public class DownloaderUpdate {
this.mediaIds = mediaIds1.toArray();
}
+ @NonNull
@Override
public String toString() {
return "DownloaderUpdate{" +
diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/FavoritesEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/FavoritesEvent.java
index 578007561..d3be8fac0 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/event/FavoritesEvent.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/event/FavoritesEvent.java
@@ -1,5 +1,7 @@
package de.danoeh.antennapod.core.event;
+import androidx.annotation.NonNull;
+
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
@@ -27,6 +29,7 @@ public class FavoritesEvent {
return new FavoritesEvent(Action.REMOVED, item);
}
+ @NonNull
@Override
public String toString() {
return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/FeedItemEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/FeedItemEvent.java
index 4b14a72d2..02559b2f5 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/event/FeedItemEvent.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/event/FeedItemEvent.java
@@ -21,7 +21,7 @@ public class FeedItemEvent {
private final Action action;
@NonNull public final List<FeedItem> items;
- private FeedItemEvent(Action action, List<FeedItem> items) {
+ private FeedItemEvent(@NonNull Action action, @NonNull List<FeedItem> items) {
this.action = action;
this.items = items;
}
@@ -42,6 +42,7 @@ public class FeedItemEvent {
return updated(Arrays.asList(items));
}
+ @NonNull
@Override
public String toString() {
return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/ChapterMerger.java b/core/src/main/java/de/danoeh/antennapod/core/feed/ChapterMerger.java
new file mode 100644
index 000000000..f33fa7511
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/ChapterMerger.java
@@ -0,0 +1,55 @@
+package de.danoeh.antennapod.core.feed;
+
+import android.text.TextUtils;
+import android.util.Log;
+import androidx.annotation.Nullable;
+
+import java.util.List;
+
+public class ChapterMerger {
+ private static final String TAG = "ChapterMerger";
+
+ private ChapterMerger() {
+
+ }
+
+ /**
+ * This method might modify the input data.
+ */
+ @Nullable
+ public static List<Chapter> merge(@Nullable List<Chapter> chapters1, @Nullable List<Chapter> chapters2) {
+ Log.d(TAG, "Merging chapters");
+ if (chapters1 == null) {
+ return chapters2;
+ } else if (chapters2 == null) {
+ return chapters1;
+ } else if (chapters2.size() > chapters1.size()) {
+ return chapters2;
+ } else if (chapters2.size() < chapters1.size()) {
+ return chapters1;
+ } else {
+ // Merge chapter lists of same length. Store in chapters2 array.
+ // In case the lists can not be merged, return chapters1 array.
+ for (int i = 0; i < chapters2.size(); i++) {
+ Chapter chapterTarget = chapters2.get(i);
+ Chapter chapterOther = chapters1.get(i);
+
+ if (Math.abs(chapterTarget.start - chapterOther.start) > 1000) {
+ Log.e(TAG, "Chapter lists are too different. Cancelling merge.");
+ return chapters1;
+ }
+
+ if (TextUtils.isEmpty(chapterTarget.imageUrl)) {
+ chapterTarget.imageUrl = chapterOther.imageUrl;
+ }
+ if (TextUtils.isEmpty(chapterTarget.link)) {
+ chapterTarget.link = chapterOther.link;
+ }
+ if (TextUtils.isEmpty(chapterTarget.title)) {
+ chapterTarget.title = chapterOther.title;
+ }
+ }
+ return chapters2;
+ }
+ }
+}
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 0889e5182..a3b66c951 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
@@ -24,6 +24,7 @@ public class Feed extends FeedFile implements ImageResource {
public static final int FEEDFILETYPE_FEED = 0;
public static final String TYPE_RSS2 = "rss";
public static final String TYPE_ATOM1 = "atom";
+ public static final String PREFIX_LOCAL_FOLDER = "antennapod_local:";
/* title as defined by the feed */
private String feedTitle;
@@ -352,7 +353,7 @@ public class Feed extends FeedFile implements ImageResource {
Date mostRecentDate = new Date(0);
FeedItem mostRecentItem = null;
for (FeedItem item : items) {
- if (item.getPubDate().after(mostRecentDate)) {
+ if (item.getPubDate() != null && item.getPubDate().after(mostRecentDate)) {
mostRecentDate = item.getPubDate();
mostRecentItem = item;
}
@@ -551,4 +552,7 @@ public class Feed extends FeedFile implements ImageResource {
this.lastUpdateFailed = lastUpdateFailed;
}
+ public boolean isLocalFeed() {
+ return download_url.startsWith(PREFIX_LOCAL_FOLDER);
+ }
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedComponent.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedComponent.java
index 2610d253f..3edecd35c 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedComponent.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedComponent.java
@@ -50,7 +50,7 @@ public abstract class FeedComponent {
@Override
public boolean equals(Object o) {
if (this == o) return true;
- if (o == null || !(o instanceof FeedComponent)) return false;
+ if (!(o instanceof FeedComponent)) return false;
FeedComponent that = (FeedComponent) o;
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedEvent.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedEvent.java
index 15cdf92dc..044554451 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedEvent.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedEvent.java
@@ -1,5 +1,7 @@
package de.danoeh.antennapod.core.feed;
+import androidx.annotation.NonNull;
+
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
@@ -18,6 +20,7 @@ public class FeedEvent {
this.feedId = feedId;
}
+ @NonNull
@Override
public String toString() {
return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
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 20ed402fc..131cbe563 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
@@ -1,12 +1,15 @@
package de.danoeh.antennapod.core.feed;
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;
+import java.io.Serializable;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
@@ -24,7 +27,7 @@ import de.danoeh.antennapod.core.util.ShownotesProvider;
*
* @author daniel
*/
-public class FeedItem extends FeedComponent implements ShownotesProvider, ImageResource {
+public class FeedItem extends FeedComponent implements ShownotesProvider, ImageResource, Serializable {
/** tag that indicates this item is in the queue */
public static final String TAG_QUEUE = "Queue";
@@ -378,7 +381,7 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, ImageR
if (imageUrl != null) {
return imageUrl;
} else if (media != null && media.hasEmbeddedPicture()) {
- return media.getLocalMediaUrl();
+ return FeedMedia.FILENAME_PREFIX_EMBEDDED_COVER + media.getLocalMediaUrl();
} else if (feed != null) {
return feed.getImageLocation();
} else {
@@ -481,6 +484,7 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, ImageR
*/
public void removeTag(String tag) { tags.remove(tag); }
+ @NonNull
@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
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 719383d23..e8e478a86 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,8 +1,10 @@
package de.danoeh.antennapod.core.feed;
import android.text.TextUtils;
+import android.util.Log;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import de.danoeh.antennapod.core.storage.DBReader;
@@ -11,17 +13,21 @@ import de.danoeh.antennapod.core.util.LongList;
import static de.danoeh.antennapod.core.feed.FeedItem.TAG_FAVORITE;
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;
public FeedItemFilter(String properties) {
this(TextUtils.split(properties, ","));
@@ -29,15 +35,18 @@ public class FeedItemFilter {
public FeedItemFilter(String[] properties) {
this.mProperties = properties;
- for(String property : properties) {
+ for (String property : properties) {
// see R.arrays.feed_filter_values
- switch(property) {
+ switch (property) {
case "unplayed":
showUnplayed = true;
break;
case "paused":
showPaused = true;
break;
+ case "not_paused":
+ showNotPaused = true;
+ break;
case "played":
showPlayed = true;
break;
@@ -56,9 +65,17 @@ public class FeedItemFilter {
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;
}
}
}
@@ -77,12 +94,15 @@ public class FeedItemFilter {
if (showQueued && showNotQueued) return result;
if (showDownloaded && showNotDownloaded) return result;
- final LongList queuedIds = DBReader.getQueueIDList();
- for(FeedItem item : items) {
+ 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;
@@ -93,8 +113,10 @@ public class FeedItemFilter {
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);
@@ -107,4 +129,8 @@ public class FeedItemFilter {
return mProperties.clone();
}
+ public boolean isShowDownloaded() {
+ return showDownloaded;
+ }
+
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilterGroup.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilterGroup.java
new file mode 100644
index 000000000..fcbe2e4ab
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilterGroup.java
@@ -0,0 +1,36 @@
+package de.danoeh.antennapod.core.feed;
+
+import de.danoeh.antennapod.core.R;
+
+public enum FeedItemFilterGroup {
+ PLAYED(new ItemProperties(R.string.hide_played_episodes_label, "played"),
+ new ItemProperties(R.string.not_played, "unplayed")),
+ PAUSED(new ItemProperties(R.string.hide_paused_episodes_label, "paused"),
+ new ItemProperties(R.string.not_paused, "not_paused")),
+ FAVORITE(new ItemProperties(R.string.hide_is_favorite_label, "is_favorite"),
+ new ItemProperties(R.string.not_favorite, "not_favorite")),
+ MEDIA(new ItemProperties(R.string.has_media, "has_media"),
+ new ItemProperties(R.string.no_media, "no_media")),
+ QUEUED(new ItemProperties(R.string.queued_label, "queued"),
+ new ItemProperties(R.string.not_queued_label, "not_queued")),
+ DOWNLOADED(new ItemProperties(R.string.hide_downloaded_episodes_label, "downloaded"),
+ new ItemProperties(R.string.hide_not_downloaded_episodes_label, "not_downloaded"));
+
+ public final ItemProperties[] values;
+
+ FeedItemFilterGroup(ItemProperties... values) {
+ this.values = values;
+ }
+
+ public static class ItemProperties {
+
+ public final int displayName;
+ public final String filterId;
+
+ public ItemProperties(int displayName, String filterId) {
+ this.displayName = displayName;
+ this.filterId = filterId;
+ }
+
+ }
+}
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 7e1a5fd9b..88945b930 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
@@ -11,6 +11,7 @@ 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;
@@ -32,6 +33,7 @@ public class FeedMedia extends FeedFile implements Playable {
public static final int FEEDFILETYPE_FEEDMEDIA = 2;
public static final int PLAYABLE_TYPE_FEEDMEDIA = 1;
+ public static final String FILENAME_PREFIX_EMBEDDED_COVER = "metadata-retriever:";
public static final String PREF_MEDIA_ID = "FeedMedia.PrefMediaId";
private static final String PREF_FEED_ID = "FeedMedia.PrefFeedId";
@@ -375,26 +377,37 @@ public class FeedMedia extends FeedFile implements Playable {
}
@Override
- public void loadChapterMarks() {
+ public void loadChapterMarks(Context context) {
if (item == null && itemID != 0) {
item = DBReader.getFeedItem(itemID);
}
if (item == null || item.getChapters() != null) {
return;
}
- // check if chapters are stored in db and not loaded yet.
+
+ 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()) {
- DBReader.loadChaptersOfFeedItem(item);
+ chaptersFromDatabase = DBReader.loadChaptersOfFeedItem(item);
+ }
+
+ List<Chapter> chaptersFromMediaFile;
+ if (localFileAvailable()) {
+ chaptersFromMediaFile = ChapterUtils.loadChaptersFromFileUrl(this);
} else {
- if(localFileAvailable()) {
- ChapterUtils.loadChaptersFromFileUrl(this);
- } else {
- ChapterUtils.loadChaptersFromStreamUrl(this);
- }
- if (item.getChapters() != null) {
- DBWriter.setFeedItem(item);
- }
+ chaptersFromMediaFile = ChapterUtils.loadChaptersFromStreamUrl(this, context);
}
+
+ return ChapterMerger.merge(chaptersFromDatabase, chaptersFromMediaFile);
}
@Override
@@ -481,7 +494,7 @@ public class FeedMedia extends FeedFile implements Playable {
@Override
public void onPlaybackStart() {
- startPosition = (position > 0) ? position : 0;
+ startPosition = Math.max(position, 0);
playedDurationWhenStarted = played_duration;
}
@@ -557,7 +570,7 @@ public class FeedMedia extends FeedFile implements Playable {
if (item != null) {
return item.getImageLocation();
} else if (hasEmbeddedPicture()) {
- return getLocalMediaUrl();
+ return FILENAME_PREFIX_EMBEDDED_COVER + getLocalMediaUrl();
} else {
return null;
}
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 2a2568f28..5ffee0d62 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
@@ -38,11 +38,8 @@ public class FeedPreferences {
private int feedSkipEnding;
public FeedPreferences(long feedID, boolean autoDownload, AutoDeleteAction auto_delete_action, VolumeAdaptionSetting volumeAdaptionSetting, String username, String password) {
- this(feedID, autoDownload, true, auto_delete_action, volumeAdaptionSetting, username, password, new FeedFilter(), SPEED_USE_GLOBAL);
- }
-
- private FeedPreferences(long feedID, boolean autoDownload, boolean keepUpdated, AutoDeleteAction auto_delete_action, VolumeAdaptionSetting volumeAdaptionSetting, String username, String password, @NonNull FeedFilter filter, float feedPlaybackSpeed) {
- this(feedID, autoDownload, true, auto_delete_action, volumeAdaptionSetting, username, password, new FeedFilter(), feedPlaybackSpeed, 0, 0);
+ this(feedID, autoDownload, true, auto_delete_action, volumeAdaptionSetting,
+ username, password, new FeedFilter(), SPEED_USE_GLOBAL, 0, 0);
}
private FeedPreferences(long feedID, boolean autoDownload, boolean keepUpdated, AutoDeleteAction auto_delete_action, VolumeAdaptionSetting volumeAdaptionSetting, String username, String password, @NonNull FeedFilter filter, float feedPlaybackSpeed, int feedSkipIntro, int feedSkipEnding) {
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
new file mode 100644
index 000000000..2791be08c
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java
@@ -0,0 +1,162 @@
+package de.danoeh.antennapod.core.feed;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.media.MediaMetadataRetriever;
+import android.net.Uri;
+import android.text.TextUtils;
+
+import androidx.documentfile.provider.DocumentFile;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+
+import de.danoeh.antennapod.core.R;
+import de.danoeh.antennapod.core.service.download.DownloadStatus;
+import de.danoeh.antennapod.core.storage.DBTasks;
+import de.danoeh.antennapod.core.storage.DBWriter;
+import de.danoeh.antennapod.core.util.DownloadError;
+
+public class LocalFeedUpdater {
+
+ public static void updateFeed(Feed feed, Context context) {
+ String uriString = feed.getDownload_url().replace(Feed.PREFIX_LOCAL_FOLDER, "");
+ DocumentFile documentFolder = DocumentFile.fromTreeUri(context, Uri.parse(uriString));
+ if (documentFolder == null) {
+ reportError(feed, "Unable to retrieve document tree."
+ + "Try re-connecting the folder on the podcast info page.");
+ return;
+ }
+ if (!documentFolder.exists() || !documentFolder.canRead()) {
+ reportError(feed, "Cannot read local directory. Try re-connecting the folder on the podcast info page.");
+ return;
+ }
+
+ if (feed.getItems() == null) {
+ feed.setItems(new ArrayList<>());
+ }
+ //make sure it is the latest 'version' of this feed from the db (all items etc)
+ feed = DBTasks.updateFeed(context, feed, false);
+
+ // list files in feed folder
+ List<DocumentFile> mediaFiles = new ArrayList<>();
+ Set<String> mediaFileNames = new HashSet<>();
+ for (DocumentFile file : documentFolder.listFiles()) {
+ String mime = file.getType();
+ if (mime != null && (mime.startsWith("audio/") || mime.startsWith("video/"))) {
+ mediaFiles.add(file);
+ mediaFileNames.add(file.getName());
+ }
+ }
+
+ // add new files to feed and update item data
+ List<FeedItem> newItems = feed.getItems();
+ for (DocumentFile f : mediaFiles) {
+ FeedItem oldItem = feedContainsFile(feed, f.getName());
+ FeedItem newItem = createFeedItem(feed, f, context);
+ if (oldItem == null) {
+ newItems.add(newItem);
+ } else {
+ oldItem.updateFromOther(newItem);
+ }
+ }
+
+ // remove feed items without corresponding file
+ Iterator<FeedItem> it = newItems.iterator();
+ while (it.hasNext()) {
+ FeedItem feedItem = it.next();
+ if (!mediaFileNames.contains(feedItem.getLink())) {
+ it.remove();
+ }
+ }
+
+ 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));
+ }
+ if (feed.getPreferences().getAutoDownload()) {
+ feed.getPreferences().setAutoDownload(false);
+ feed.getPreferences().setAutoDeleteAction(FeedPreferences.AutoDeleteAction.NO);
+ try {
+ DBWriter.setFeedPreferences(feed.getPreferences()).get();
+ } catch (ExecutionException | InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+
+ // update items, delete items without existing file;
+ // only delete items if the folder contains at least one element to avoid accidentally
+ // deleting played state or position in case the folder is temporarily unavailable.
+ boolean removeUnlistedItems = (newItems.size() >= 1);
+ DBTasks.updateFeed(context, feed, removeUnlistedItems);
+ }
+
+ /**
+ * 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) {
+ String resourceEntryName = context.getResources().getResourceEntryName(R.raw.local_feed_default_icon);
+ return ContentResolver.SCHEME_ANDROID_RESOURCE + "://"
+ + context.getPackageName() + "/raw/"
+ + resourceEntryName;
+ }
+
+ private static FeedItem feedContainsFile(Feed feed, String filename) {
+ List<FeedItem> items = feed.getItems();
+ for (FeedItem i : items) {
+ if (i.getMedia() != null && i.getLink().equals(filename)) {
+ return i;
+ }
+ }
+ return null;
+ }
+
+ private static FeedItem createFeedItem(Feed feed, DocumentFile file, Context context) {
+ String uuid = UUID.randomUUID().toString();
+ FeedItem item = new FeedItem(0, file.getName(), uuid, file.getName(), new Date(),
+ FeedItem.UNPLAYED, feed);
+ item.setAutoDownload(false);
+
+ MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
+ mediaMetadataRetriever.setDataSource(context, file.getUri());
+ String durationStr = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
+ String title = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
+ if (!TextUtils.isEmpty(title)) {
+ item.setTitle(title);
+ }
+
+ //add the media to the item
+ long duration = Long.parseLong(durationStr);
+ long size = file.length();
+ FeedMedia media = new FeedMedia(0, item, (int) duration, 0, size, file.getType(),
+ file.getUri().toString(), file.getUri().toString(), false, null, 0, 0);
+ media.setHasEmbeddedPicture(mediaMetadataRetriever.getEmbeddedPicture() != null);
+ item.setMedia(media);
+
+ return item;
+ }
+
+ private static void reportError(Feed feed, String reasonDetailed) {
+ DownloadStatus status = new DownloadStatus(feed, feed.getTitle(),
+ DownloadError.ERROR_IO_ERROR, false, reasonDetailed, true);
+ DBWriter.addDownloadStatus(status);
+ DBWriter.setFeedLastUpdateFailed(feed.getId(), true);
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/SubscriptionsFilter.java b/core/src/main/java/de/danoeh/antennapod/core/feed/SubscriptionsFilter.java
new file mode 100644
index 000000000..93f098ecf
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/SubscriptionsFilter.java
@@ -0,0 +1,106 @@
+package de.danoeh.antennapod.core.feed;
+
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import de.danoeh.antennapod.core.util.LongIntMap;
+
+public class SubscriptionsFilter {
+ private static final String divider = ",";
+
+ private final String[] properties;
+
+ private boolean showIfCounterGreaterZero = false;
+
+ private boolean showAutoDownloadEnabled = false;
+ private boolean showAutoDownloadDisabled = false;
+
+ private boolean showUpdatedEnabled = false;
+ private boolean showUpdatedDisabled = false;
+
+ public SubscriptionsFilter(String properties) {
+ this(TextUtils.split(properties, divider));
+ }
+
+
+ public SubscriptionsFilter(String[] properties) {
+ this.properties = properties;
+ for (String property : properties) {
+ // see R.arrays.feed_filter_values
+ switch (property) {
+ case "counter_greater_zero":
+ showIfCounterGreaterZero = true;
+ break;
+ case "enabled_auto_download":
+ showAutoDownloadEnabled = true;
+ break;
+ case "disabled_auto_download":
+ showAutoDownloadDisabled = true;
+ break;
+ case "enabled_updates":
+ showUpdatedEnabled = true;
+ break;
+ case "disabled_updates":
+ showUpdatedDisabled = true;
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ public boolean isEnabled() {
+ return properties.length > 0;
+ }
+
+ /**
+ * Run a list of feed items through the filter.
+ */
+ public List<Feed> filter(List<Feed> items, LongIntMap feedCounters) {
+ if (properties.length == 0) {
+ return items;
+ }
+
+ List<Feed> result = new ArrayList<>();
+
+ for (Feed item : items) {
+ FeedPreferences itemPreferences = item.getPreferences();
+
+ // If the item does not meet a requirement, skip it.
+ if (showAutoDownloadEnabled && !itemPreferences.getAutoDownload()) {
+ continue;
+ } else if (showAutoDownloadDisabled && itemPreferences.getAutoDownload()) {
+ continue;
+ }
+
+ if (showUpdatedEnabled && !itemPreferences.getKeepUpdated()) {
+ continue;
+ } else if (showUpdatedDisabled && itemPreferences.getKeepUpdated()) {
+ continue;
+ }
+
+ // If the item reaches here, it meets all criteria (except counter > 0)
+ result.add(item);
+ }
+
+ if (showIfCounterGreaterZero) {
+ for (int i = result.size() - 1; i >= 0; i--) {
+ if (feedCounters.get(result.get(i).getId()) <= 0) {
+ result.remove(i);
+ }
+ }
+ }
+
+ return result;
+ }
+
+ public String[] getValues() {
+ return properties.clone();
+ }
+
+ public String serialize() {
+ return TextUtils.join(divider, getValues());
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/SubscriptionsFilterGroup.java b/core/src/main/java/de/danoeh/antennapod/core/feed/SubscriptionsFilterGroup.java
new file mode 100644
index 000000000..7db0456a0
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/SubscriptionsFilterGroup.java
@@ -0,0 +1,30 @@
+package de.danoeh.antennapod.core.feed;
+
+import de.danoeh.antennapod.core.R;
+
+public enum SubscriptionsFilterGroup {
+ COUNTER_GREATER_ZERO(new ItemProperties(R.string.subscriptions_counter_greater_zero, "counter_greater_zero")),
+ AUTO_DOWNLOAD(new ItemProperties(R.string.auto_downloaded, "enabled_auto_download"),
+ new ItemProperties(R.string.not_auto_downloaded, "disabled_auto_download")),
+ UPDATED(new ItemProperties(R.string.kept_updated, "enabled_updates"),
+ new ItemProperties(R.string.not_kept_updated, "disabled_updates"));
+
+
+ public final ItemProperties[] values;
+
+ SubscriptionsFilterGroup(ItemProperties... values) {
+ this.values = values;
+ }
+
+ public static class ItemProperties {
+
+ public final int displayName;
+ public final String filterId;
+
+ public ItemProperties(int displayName, String filterId) {
+ this.displayName = displayName;
+ this.filterId = filterId;
+ }
+
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideModule.java b/core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideModule.java
index 50511526f..ab4247cef 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideModule.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideModule.java
@@ -9,6 +9,8 @@ import com.bumptech.glide.Registry;
import com.bumptech.glide.annotation.GlideModule;
import com.bumptech.glide.load.DecodeFormat;
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory;
+import com.bumptech.glide.load.model.StringLoader;
+import com.bumptech.glide.load.model.UriLoader;
import com.bumptech.glide.module.AppGlideModule;
import de.danoeh.antennapod.core.util.EmbeddedChapterImage;
@@ -33,7 +35,10 @@ public class ApGlideModule extends AppGlideModule {
@Override
public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
- registry.replace(String.class, InputStream.class, new ApOkHttpUrlLoader.Factory());
+ registry.replace(String.class, InputStream.class, new MetadataRetrieverLoader.Factory(context));
+ registry.append(String.class, InputStream.class, new ApOkHttpUrlLoader.Factory());
+ registry.append(String.class, InputStream.class, new StringLoader.StreamFactory());
+
registry.append(EmbeddedChapterImage.class, ByteBuffer.class, new ChapterImageModelLoader.Factory());
}
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java b/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java
index 071b1d0c9..8c80e9151 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java
@@ -1,5 +1,6 @@
package de.danoeh.antennapod.core.glide;
+import android.content.ContentResolver;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -22,7 +23,7 @@ import java.io.IOException;
import java.io.InputStream;
/**
- * @see com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
+ * {@see com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader}.
*/
class ApOkHttpUrlLoader implements ModelLoader<String, InputStream> {
@@ -52,14 +53,7 @@ class ApOkHttpUrlLoader implements ModelLoader<String, InputStream> {
* Constructor for a new Factory that runs requests using a static singleton client.
*/
Factory() {
- this(getInternalClient());
- }
-
- /**
- * Constructor for a new Factory that runs requests using given client.
- */
- Factory(OkHttpClient client) {
- this.client = client;
+ this.client = getInternalClient();
}
@NonNull
@@ -83,19 +77,15 @@ class ApOkHttpUrlLoader implements ModelLoader<String, InputStream> {
@Nullable
@Override
public LoadData<InputStream> buildLoadData(@NonNull String model, int width, int height, @NonNull Options options) {
- if (TextUtils.isEmpty(model)) {
- return null;
- } else if (model.startsWith("/")) {
- return new LoadData<>(new ObjectKey(model), new AudioCoverFetcher(model));
- } else {
- GlideUrl url = new GlideUrl(model);
- return new LoadData<>(new ObjectKey(model), new OkHttpStreamFetcher(client, url));
- }
+ return new LoadData<>(new ObjectKey(model), new OkHttpStreamFetcher(client, new GlideUrl(model)));
}
@Override
- public boolean handles(@NonNull String s) {
- return true;
+ public boolean handles(@NonNull String model) {
+ // Leave content URIs to Glide's default loaders
+ return !TextUtils.isEmpty(model)
+ && !model.startsWith(ContentResolver.SCHEME_CONTENT)
+ && !model.startsWith(ContentResolver.SCHEME_ANDROID_RESOURCE);
}
private static class NetworkAllowanceInterceptor implements Interceptor {
diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/AudioCoverFetcher.java b/core/src/main/java/de/danoeh/antennapod/core/glide/AudioCoverFetcher.java
index 6a237573b..b6b607904 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/glide/AudioCoverFetcher.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/glide/AudioCoverFetcher.java
@@ -1,7 +1,10 @@
package de.danoeh.antennapod.core.glide;
+import android.content.ContentResolver;
+import android.content.Context;
import android.media.MediaMetadataRetriever;
+import android.net.Uri;
import androidx.annotation.NonNull;
import com.bumptech.glide.Priority;
import com.bumptech.glide.load.DataSource;
@@ -17,16 +20,22 @@ class AudioCoverFetcher implements DataFetcher<InputStream> {
private static final String TAG = "AudioCoverFetcher";
private final String path;
+ private final Context context;
- public AudioCoverFetcher(String path) {
+ public AudioCoverFetcher(String path, Context context) {
this.path = path;
+ this.context = context;
}
@Override
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
try {
- retriever.setDataSource(path);
+ if (path.startsWith(ContentResolver.SCHEME_CONTENT)) {
+ retriever.setDataSource(context, Uri.parse(path));
+ } else {
+ retriever.setDataSource(path);
+ }
byte[] picture = retriever.getEmbeddedPicture();
if (picture != null) {
callback.onDataReady(new ByteArrayInputStream(picture));
@@ -41,6 +50,7 @@ class AudioCoverFetcher implements DataFetcher<InputStream> {
@Override public void cleanup() {
// nothing to clean up
}
+
@Override public void cancel() {
// cannot cancel
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/ChapterImageModelLoader.java b/core/src/main/java/de/danoeh/antennapod/core/glide/ChapterImageModelLoader.java
index 36da11eca..35a9d987b 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/glide/ChapterImageModelLoader.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/glide/ChapterImageModelLoader.java
@@ -28,8 +28,9 @@ import org.apache.commons.io.IOUtils;
public final class ChapterImageModelLoader implements ModelLoader<EmbeddedChapterImage, ByteBuffer> {
public static class Factory implements ModelLoaderFactory<EmbeddedChapterImage, ByteBuffer> {
+ @NonNull
@Override
- public ModelLoader<EmbeddedChapterImage, ByteBuffer> build(MultiModelLoaderFactory unused) {
+ public ModelLoader<EmbeddedChapterImage, ByteBuffer> build(@NonNull MultiModelLoaderFactory unused) {
return new ChapterImageModelLoader();
}
@@ -41,12 +42,15 @@ public final class ChapterImageModelLoader implements ModelLoader<EmbeddedChapte
@Nullable
@Override
- public LoadData<ByteBuffer> buildLoadData(EmbeddedChapterImage model, int width, int height, Options options) {
+ public LoadData<ByteBuffer> buildLoadData(@NonNull EmbeddedChapterImage model,
+ int width,
+ int height,
+ @NonNull Options options) {
return new LoadData<>(new ObjectKey(model), new EmbeddedImageFetcher(model));
}
@Override
- public boolean handles(EmbeddedChapterImage model) {
+ public boolean handles(@NonNull EmbeddedChapterImage model) {
return true;
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/FastBlurTransformation.java b/core/src/main/java/de/danoeh/antennapod/core/glide/FastBlurTransformation.java
index d0301db2f..1f8ae5ad9 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/glide/FastBlurTransformation.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/glide/FastBlurTransformation.java
@@ -21,7 +21,10 @@ public class FastBlurTransformation extends BitmapTransformation {
}
@Override
- protected Bitmap transform(BitmapPool pool, Bitmap source, int outWidth, int outHeight) {
+ protected Bitmap transform(@NonNull BitmapPool pool,
+ @NonNull Bitmap source,
+ int outWidth,
+ int outHeight) {
int targetWidth = outWidth / 3;
int targetHeight = (int) (1.0 * outHeight * targetWidth / outWidth);
Bitmap resized = ThumbnailUtils.extractThumbnail(source, targetWidth, targetHeight);
diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/MetadataRetrieverLoader.java b/core/src/main/java/de/danoeh/antennapod/core/glide/MetadataRetrieverLoader.java
new file mode 100644
index 000000000..baa06e722
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/glide/MetadataRetrieverLoader.java
@@ -0,0 +1,57 @@
+package de.danoeh.antennapod.core.glide;
+
+import android.content.Context;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.bumptech.glide.load.Options;
+import com.bumptech.glide.load.model.ModelLoader;
+import com.bumptech.glide.load.model.ModelLoaderFactory;
+import com.bumptech.glide.load.model.MultiModelLoaderFactory;
+import com.bumptech.glide.signature.ObjectKey;
+import de.danoeh.antennapod.core.feed.FeedMedia;
+
+import java.io.InputStream;
+
+class MetadataRetrieverLoader implements ModelLoader<String, InputStream> {
+
+ /**
+ * The default factory for {@link MetadataRetrieverLoader}s.
+ */
+ public static class Factory implements ModelLoaderFactory<String, InputStream> {
+ private final Context context;
+
+ Factory(Context context) {
+ this.context = context;
+ }
+
+ @NonNull
+ @Override
+ public ModelLoader<String, InputStream> build(@NonNull MultiModelLoaderFactory multiFactory) {
+ return new MetadataRetrieverLoader(context);
+ }
+
+ @Override
+ public void teardown() {
+ // Do nothing, this instance doesn't own the client.
+ }
+ }
+
+ private final Context context;
+
+ private MetadataRetrieverLoader(Context context) {
+ this.context = context;
+ }
+
+ @Nullable
+ @Override
+ public LoadData<InputStream> buildLoadData(@NonNull String model,
+ int width, int height, @NonNull Options options) {
+ return new LoadData<>(new ObjectKey(model),
+ new AudioCoverFetcher(model.replace(FeedMedia.FILENAME_PREFIX_EMBEDDED_COVER, ""), context));
+ }
+
+ @Override
+ public boolean handles(@NonNull String model) {
+ return model.startsWith(FeedMedia.FILENAME_PREFIX_EMBEDDED_COVER);
+ }
+}
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 a4612d857..08ea27434 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
@@ -2,7 +2,7 @@ package de.danoeh.antennapod.core.preferences;
import android.content.Context;
import android.content.SharedPreferences;
-import android.preference.PreferenceManager;
+import androidx.preference.PreferenceManager;
import android.util.Log;
import de.danoeh.antennapod.core.event.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 876251563..56dd95fe6 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
@@ -4,7 +4,6 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.os.Build;
-import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.Log;
@@ -12,6 +11,7 @@ import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.core.app.NotificationCompat;
+import androidx.preference.PreferenceManager;
import org.json.JSONArray;
import org.json.JSONException;
@@ -19,15 +19,20 @@ import org.json.JSONException;
import java.io.File;
import java.io.IOException;
import java.net.Proxy;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Calendar;
import java.util.HashSet;
import java.util.List;
+import java.util.Locale;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import de.danoeh.antennapod.core.R;
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.APNullCleanupAlgorithm;
@@ -46,14 +51,12 @@ import de.danoeh.antennapod.core.util.download.AutoUpdateManager;
public class UserPreferences {
private UserPreferences(){}
- private static final String IMPORT_DIR = "import/";
-
private static final String TAG = "UserPreferences";
// User Interface
public static final String PREF_THEME = "prefTheme";
public static final String PREF_HIDDEN_DRAWER_ITEMS = "prefHiddenDrawerItems";
- private static final String PREF_DRAWER_FEED_ORDER = "prefDrawerFeedOrder";
+ public static final String PREF_DRAWER_FEED_ORDER = "prefDrawerFeedOrder";
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";
@@ -64,6 +67,7 @@ public class UserPreferences {
private static final String PREF_SHOW_AUTO_DOWNLOAD_REPORT = "prefShowAutoDownloadReport";
public static final String PREF_BACK_BUTTON_BEHAVIOR = "prefBackButtonBehavior";
private static final String PREF_BACK_BUTTON_GO_TO_PAGE = "prefBackButtonGoToPage";
+ public static final String PREF_FILTER_FEED = "prefSubscriptionsFilter";
public static final String PREF_QUEUE_KEEP_SORTED = "prefQueueKeepSorted";
public static final String PREF_QUEUE_KEEP_SORTED_ORDER = "prefQueueKeepSortedOrder";
@@ -111,6 +115,7 @@ public class UserPreferences {
private static final String PREF_DATA_FOLDER = "prefDataFolder";
public static final String PREF_IMAGE_CACHE_SIZE = "prefImageCacheSize";
public static final String PREF_DELETE_REMOVES_FROM_QUEUE = "prefDeleteRemovesFromQueue";
+ public static final String PREF_USAGE_COUNTING_DATE = "prefUsageCounting";
// Mediaplayer
public static final String PREF_MEDIA_PLAYER = "prefMediaPlayer";
@@ -161,7 +166,6 @@ public class UserPreferences {
UserPreferences.context = context.getApplicationContext();
UserPreferences.prefs = PreferenceManager.getDefaultSharedPreferences(context);
- createImportDirectory();
createNoMediaFile();
}
@@ -242,6 +246,12 @@ public class UserPreferences {
return Integer.parseInt(value);
}
+ public static void setFeedOrder(String selected) {
+ prefs.edit()
+ .putString(PREF_DRAWER_FEED_ORDER, selected)
+ .apply();
+ }
+
public static int getFeedCounterSetting() {
String value = prefs.getString(PREF_DRAWER_FEED_COUNTER, "" + FEED_COUNTER_SHOW_NEW);
return Integer.parseInt(value);
@@ -414,7 +424,7 @@ public class UserPreferences {
return prefs.getBoolean(PREF_PLAYBACK_SKIP_SILENCE, false);
}
- public static float[] getPlaybackSpeedArray() {
+ public static List<Float> getPlaybackSpeedArray() {
return readPlaybackSpeedArray(prefs.getString(PREF_PLAYBACK_SPEED_ARRAY, null));
}
@@ -628,8 +638,7 @@ public class UserPreferences {
}
public static boolean isQueueLocked() {
- return prefs.getBoolean(PREF_QUEUE_LOCKED, false)
- || isQueueKeepSorted();
+ return prefs.getBoolean(PREF_QUEUE_LOCKED, false);
}
public static void setFastForwardSecs(int secs) {
@@ -662,10 +671,13 @@ public class UserPreferences {
.apply();
}
- public static void setPlaybackSpeedArray(String[] speeds) {
+ public static void setPlaybackSpeedArray(List<Float> speeds) {
+ DecimalFormatSymbols format = new DecimalFormatSymbols(Locale.US);
+ format.setDecimalSeparator('.');
+ DecimalFormat speedFormat = new DecimalFormat("0.00", format);
JSONArray jsonArray = new JSONArray();
- for (String speed : speeds) {
- jsonArray.put(speed);
+ for (float speed : speeds) {
+ jsonArray.put(speedFormat.format(speed));
}
prefs.edit()
.putString(PREF_PLAYBACK_SPEED_ARRAY, jsonArray.toString())
@@ -775,13 +787,13 @@ public class UserPreferences {
}
}
- private static float[] readPlaybackSpeedArray(String valueFromPrefs) {
+ private static List<Float> readPlaybackSpeedArray(String valueFromPrefs) {
if (valueFromPrefs != null) {
try {
JSONArray jsonArray = new JSONArray(valueFromPrefs);
- float[] selectedSpeeds = new float[jsonArray.length()];
+ List<Float> selectedSpeeds = new ArrayList<>();
for (int i = 0; i < jsonArray.length(); i++) {
- selectedSpeeds[i] = (float) jsonArray.getDouble(i);
+ selectedSpeeds.add((float) jsonArray.getDouble(i));
}
return selectedSpeeds;
} catch (JSONException e) {
@@ -790,7 +802,7 @@ public class UserPreferences {
}
}
// If this preference hasn't been set yet, return the default options
- return new float[] { 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f };
+ return Arrays.asList(0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f);
}
public static String getMediaPlayer() {
@@ -826,8 +838,8 @@ public class UserPreferences {
public static VideoBackgroundBehavior getVideoBackgroundBehavior() {
switch (prefs.getString(PREF_VIDEO_BEHAVIOR, "pip")) {
case "stop": return VideoBackgroundBehavior.STOP;
- case "pip": return VideoBackgroundBehavior.PICTURE_IN_PICTURE;
case "continue": return VideoBackgroundBehavior.CONTINUE_PLAYING;
+ case "pip": //Deliberate fall-through
default: return VideoBackgroundBehavior.PICTURE_IN_PICTURE;
}
}
@@ -914,7 +926,6 @@ public class UserPreferences {
prefs.edit()
.putString(PREF_DATA_FOLDER, dir)
.apply();
- createImportDirectory();
}
/**
@@ -934,24 +945,6 @@ public class UserPreferences {
}
/**
- * Creates the import directory if it doesn't exist and if storage is
- * available
- */
- private static void createImportDirectory() {
- File importDir = getDataFolder(IMPORT_DIR);
- if (importDir != null) {
- if (importDir.exists()) {
- Log.d(TAG, "Import directory already exists");
- } else {
- Log.d(TAG, "Creating import directory");
- importDir.mkdir();
- }
- } else {
- Log.d(TAG, "Could not access external storage.");
- }
- }
-
- /**
*
* @return true if auto update is set to a specific time
* false if auto update is set to interval
@@ -977,11 +970,11 @@ public class UserPreferences {
public static BackButtonBehavior getBackButtonBehavior() {
switch (prefs.getString(PREF_BACK_BUTTON_BEHAVIOR, "default")) {
- case "default": return BackButtonBehavior.DEFAULT;
case "drawer": return BackButtonBehavior.OPEN_DRAWER;
case "doubletap": return BackButtonBehavior.DOUBLE_TAP;
case "prompt": return BackButtonBehavior.SHOW_PROMPT;
case "page": return BackButtonBehavior.GO_TO_PAGE;
+ case "default": // Deliberate fall-through
default: return BackButtonBehavior.DEFAULT;
}
}
@@ -1052,4 +1045,31 @@ public class UserPreferences {
.putString(PREF_QUEUE_KEEP_SORTED_ORDER, sortOrder.name())
.apply();
}
+
+ public static SubscriptionsFilter getSubscriptionsFilter() {
+ String value = prefs.getString(PREF_FILTER_FEED, "");
+ return new SubscriptionsFilter(value);
+ }
+
+ public static void setSubscriptionsFilter(SubscriptionsFilter value) {
+ prefs.edit()
+ .putString(PREF_FILTER_FEED, value.serialize())
+ .apply();
+ }
+
+ public static long getUsageCountingDateMillis() {
+ return prefs.getLong(PREF_USAGE_COUNTING_DATE, -1);
+ }
+
+ private static void setUsageCountingDateMillis(long value) {
+ prefs.edit().putLong(PREF_USAGE_COUNTING_DATE, value).apply();
+ }
+
+ public static void resetUsageCountingDate() {
+ setUsageCountingDateMillis(Calendar.getInstance().getTimeInMillis());
+ }
+
+ public static void unsetUsageCountingDate() {
+ setUsageCountingDateMillis(-1);
+ }
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java b/core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java
index b683f849c..abee9d8d3 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java
@@ -15,6 +15,7 @@ public class MediaButtonReceiver extends BroadcastReceiver {
private static final String TAG = "MediaButtonReceiver";
public static final String EXTRA_KEYCODE = "de.danoeh.antennapod.core.service.extra.MediaButtonReceiver.KEYCODE";
public static final String EXTRA_SOURCE = "de.danoeh.antennapod.core.service.extra.MediaButtonReceiver.SOURCE";
+ public static final String EXTRA_HARDWAREBUTTON = "de.danoeh.antennapod.core.service.extra.MediaButtonReceiver.HARDWAREBUTTON";
public static final String NOTIFY_BUTTON_RECEIVER = "de.danoeh.antennapod.NOTIFY_BUTTON_RECEIVER";
@@ -30,6 +31,12 @@ public class MediaButtonReceiver extends BroadcastReceiver {
Intent serviceIntent = new Intent(context, PlaybackService.class);
serviceIntent.putExtra(EXTRA_KEYCODE, event.getKeyCode());
serviceIntent.putExtra(EXTRA_SOURCE, event.getSource());
+ //detect if this is a hardware button press
+ if (event.getEventTime() > 0 || event.getDownTime() > 0) {
+ serviceIntent.putExtra(EXTRA_HARDWAREBUTTON, true);
+ } else {
+ serviceIntent.putExtra(EXTRA_HARDWAREBUTTON, false);
+ }
ContextCompat.startForegroundService(context, serviceIntent);
}
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
index 7bf1a5df1..74735a264 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/PlayerWidgetJobService.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/PlayerWidgetJobService.java
@@ -121,7 +121,7 @@ public class PlayerWidgetJobService extends SafeJobIntentService {
views.setOnClickPendingIntent(R.id.layout_left, startMediaPlayer);
try {
- Bitmap icon = null;
+ Bitmap icon;
int iconSize = getResources().getDimensionPixelSize(android.R.dimen.app_icon_size);
icon = Glide.with(PlayerWidgetJobService.this)
.asBitmap()
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequest.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequest.java
index 78c4d3f48..3f503c6b4 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequest.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequest.java
@@ -115,7 +115,7 @@ public class DownloadRequest implements Parcelable {
@Override
public boolean equals(Object o) {
if (this == o) return true;
- if (o == null || !(o instanceof DownloadRequest)) return false;
+ if (!(o instanceof DownloadRequest)) return false;
DownloadRequest that = (DownloadRequest) o;
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 e44aa716a..f1b35fe23 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
@@ -10,6 +10,7 @@ import android.content.IntentFilter;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
+import android.os.Looper;
import android.text.TextUtils;
import android.util.Log;
@@ -178,7 +179,7 @@ public class DownloadService extends Service {
public void onCreate() {
Log.d(TAG, "Service started");
isRunning = true;
- handler = new Handler();
+ handler = new Handler(Looper.getMainLooper());
notificationManager = new DownloadServiceNotification(this);
IntentFilter cancelDownloadReceiverFilter = new IntentFilter();
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 64666d25d..975bc3cb3 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
@@ -153,7 +153,11 @@ public class DownloadServiceNotification {
iconId = R.drawable.ic_notification_sync_error;
intent = ClientConfig.downloadServiceCallbacks.getReportNotificationContentIntent(context);
id = R.id.notification_download_report;
- content = String.format(context.getString(R.string.download_report_content), successfulDownloads, failedDownloads);
+ content = context.getResources()
+ .getQuantityString(R.plurals.download_report_content,
+ successfulDownloads,
+ successfulDownloads,
+ failedDownloads);
}
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 ef86c9024..65b7ed7d1 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
@@ -191,7 +191,7 @@ public class HttpDownloader extends Downloader {
}
byte[] buffer = new byte[BUFFER_SIZE];
- int count = 0;
+ int count;
request.setStatusMsg(R.string.download_running);
Log.d(TAG, "Getting size of download");
request.setSize(responseBody.contentLength() + request.getSoFar());
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 c50162788..18c5fce27 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
@@ -18,7 +18,6 @@ import org.xml.sax.SAXException;
import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.io.IOException;
-import java.util.Date;
import java.util.concurrent.Callable;
public class FeedParserTask implements Callable<FeedHandlerResult> {
@@ -104,13 +103,6 @@ public class FeedParserTask implements Callable<FeedHandlerResult> {
if (item.getTitle() == null) {
throw new InvalidFeedException("Item has no title: " + item);
}
- if (item.getPubDate() == null) {
- Log.e(TAG, "Item has no pubDate. Using current time as pubDate");
- if (item.getTitle() != null) {
- Log.e(TAG, "Title of invalid item: " + item.getTitle());
- }
- item.setPubDate(new Date());
- }
}
}
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 8be3d2980..483a2aa56 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
@@ -30,8 +30,7 @@ public class FeedSyncTask {
return false;
}
- Feed[] savedFeeds = DBTasks.updateFeed(context, result.feed);
- Feed savedFeed = savedFeeds[0];
+ Feed 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;
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 9e2b69810..501214399 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()) {
- ChapterUtils.loadChaptersFromFileUrl(media);
+ media.setChapters(ChapterUtils.loadChaptersFromFileUrl(media));
}
// 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 dddf442f3..71bbf2efd 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
@@ -8,7 +8,6 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultLoadControl;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlaybackException;
-import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
@@ -76,9 +75,11 @@ public class ExoPlayerWrapper implements IPlayer {
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS);
loadControl.setBackBuffer(UserPreferences.getRewindSecs() * 1000 + 500, true);
- trackSelector = new DefaultTrackSelector();
- exoPlayer = ExoPlayerFactory.newSimpleInstance(context, new DefaultRenderersFactory(context),
- trackSelector, loadControl.createDefaultLoadControl());
+ trackSelector = new DefaultTrackSelector(context);
+ exoPlayer = new SimpleExoPlayer.Builder(context, new DefaultRenderersFactory(context))
+ .setTrackSelector(trackSelector)
+ .setLoadControl(loadControl.createDefaultLoadControl())
+ .build();
exoPlayer.setSeekParameters(SeekParameters.EXACT);
exoPlayer.addListener(new Player.EventListener() {
@Override
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 fbdc9a52b..ae5d62872 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
@@ -2,9 +2,7 @@ package de.danoeh.antennapod.core.service.playback;
import android.content.Context;
import android.media.AudioAttributes;
-import android.media.AudioFocusRequest;
import android.media.AudioManager;
-import android.os.Build;
import android.os.PowerManager;
import androidx.annotation.NonNull;
import android.telephony.TelephonyManager;
@@ -12,11 +10,13 @@ import android.util.Log;
import android.util.Pair;
import android.view.SurfaceHolder;
+import androidx.media.AudioAttributesCompat;
+import androidx.media.AudioFocusRequestCompat;
+import androidx.media.AudioManagerCompat;
import org.antennapod.audio.MediaPlayer;
import java.io.File;
import java.io.IOException;
-import java.util.EnumSet;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
@@ -57,6 +57,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
private final AtomicBoolean startWhenPrepared;
private volatile boolean pausedBecauseOfTransientAudiofocusLoss;
private volatile Pair<Integer, Integer> videoSize;
+ private final AudioFocusRequestCompat audioFocusRequest;
/**
* Some asynchronous calls might change the state of the MediaPlayer object. Therefore calls in other threads
@@ -154,6 +155,16 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
pausedBecauseOfTransientAudiofocusLoss = false;
mediaType = MediaType.UNKNOWN;
videoSize = null;
+
+ AudioAttributesCompat audioAttributes = new AudioAttributesCompat.Builder()
+ .setUsage(AudioAttributesCompat.USAGE_MEDIA)
+ .setContentType(AudioAttributesCompat.CONTENT_TYPE_SPEECH)
+ .build();
+ audioFocusRequest = new AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN)
+ .setAudioAttributes(audioAttributes)
+ .setOnAudioFocusChangeListener(audioFocusChangeListener)
+ .setWillPauseWhenDucked(true)
+ .build();
}
/**
@@ -287,25 +298,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
private void resumeSync() {
if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) {
- int focusGained;
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- AudioAttributes audioAttributes = new AudioAttributes.Builder()
- .setUsage(AudioAttributes.USAGE_MEDIA)
- .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
- .build();
- AudioFocusRequest audioFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
- .setAudioAttributes(audioAttributes)
- .setOnAudioFocusChangeListener(audioFocusChangeListener)
- .setAcceptsDelayedFocusGain(true)
- .setWillPauseWhenDucked(true)
- .build();
- focusGained = audioManager.requestAudioFocus(audioFocusRequest);
- } else {
- focusGained = audioManager.requestAudioFocus(
- audioFocusChangeListener, AudioManager.STREAM_MUSIC,
- AudioManager.AUDIOFOCUS_GAIN);
- }
+ int focusGained = AudioManagerCompat.requestAudioFocus(audioManager, audioFocusRequest);
if (focusGained == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
Log.d(TAG, "Audiofocus successfully requested");
@@ -373,13 +366,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
}
private void abandonAudioFocus() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- AudioFocusRequest.Builder builder = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
- .setOnAudioFocusChangeListener(audioFocusChangeListener);
- audioManager.abandonAudioFocusRequest(builder.build());
- } else {
- audioManager.abandonAudioFocus(audioFocusChangeListener);
- }
+ AudioManagerCompat.abandonAudioFocusRequest(audioManager, audioFocusRequest);
}
/**
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 e9c8e1bbb..60075dda6 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
@@ -3,6 +3,7 @@ package de.danoeh.antennapod.core.service.playback;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
+import android.app.UiModeManager;
import android.bluetooth.BluetoothA2dp;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
@@ -10,6 +11,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
+import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.media.AudioManager;
import android.media.MediaPlayer;
@@ -19,7 +21,7 @@ import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Vibrator;
-import android.preference.PreferenceManager;
+import androidx.preference.PreferenceManager;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.core.app.NotificationCompat;
@@ -35,6 +37,7 @@ import android.util.Log;
import android.util.Pair;
import android.view.KeyEvent;
import android.view.SurfaceHolder;
+import android.webkit.URLUtil;
import android.widget.Toast;
import com.bumptech.glide.Glide;
@@ -82,6 +85,7 @@ import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
+import io.reactivex.schedulers.Schedulers;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
@@ -313,7 +317,10 @@ public class PlaybackService extends MediaBrowserServiceCompat {
}
}
emitter.onSuccess(queueItems);
- }).subscribe(queueItems -> mediaSession.setQueue(queueItems), Throwable::printStackTrace);
+ })
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(queueItems -> mediaSession.setQueue(queueItems), Throwable::printStackTrace);
flavorHelper.initializeMediaPlayer(PlaybackService.this);
mediaSession.setActive(true);
@@ -419,7 +426,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
e.printStackTrace();
}
} else if (parentId.startsWith("FeedId:")) {
- Long feedId = Long.parseLong(parentId.split(":")[1]);
+ long feedId = Long.parseLong(parentId.split(":")[1]);
List<FeedItem> feedItems = DBReader.getFeedItemList(DBReader.getFeed(feedId));
for (FeedItem feedItem : feedItems) {
if (feedItem.getMedia() != null && feedItem.getMedia().getMediaItem() != null) {
@@ -450,6 +457,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
notificationManager.cancel(R.id.notification_streaming_confirmation);
final int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1);
+ final boolean hardwareButton = intent.getBooleanExtra(MediaButtonReceiver.EXTRA_HARDWAREBUTTON, false);
final boolean castDisconnect = intent.getBooleanExtra(EXTRA_CAST_DISCONNECT, false);
Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE);
if (keycode == -1 && playable == null && !castDisconnect) {
@@ -463,8 +471,15 @@ public class PlaybackService extends MediaBrowserServiceCompat {
stateManager.stopForeground(true);
} else {
if (keycode != -1) {
- Log.d(TAG, "Received media button event");
- boolean handled = handleKeycode(keycode, true);
+ boolean notificationButton;
+ if (hardwareButton) {
+ Log.d(TAG, "Received hardware button event");
+ notificationButton = false;
+ } else {
+ Log.d(TAG, "Received media button event");
+ notificationButton = true;
+ }
+ boolean handled = handleKeycode(keycode, notificationButton);
if (!handled && !stateManager.hasReceivedValidStartCommand()) {
stateManager.stopService();
return Service.START_NOT_STICKY;
@@ -482,22 +497,34 @@ public class PlaybackService extends MediaBrowserServiceCompat {
if (allowStreamAlways) {
UserPreferences.setAllowMobileStreaming(true);
}
- if (stream && !NetworkUtils.isStreamingAllowed() && !allowStreamThisTime) {
+ boolean localFeed = URLUtil.isContentUrl(playable.getStreamUrl());
+ if (stream && !NetworkUtils.isStreamingAllowed() && !allowStreamThisTime && !localFeed) {
displayStreamingNotAllowedNotification(intent);
PlaybackPreferences.writeNoMediaPlaying();
stateManager.stopService();
return Service.START_NOT_STICKY;
}
- if (playable instanceof FeedMedia) {
- playable = DBReader.getFeedMedia(((FeedMedia) playable).getId());
- }
- if (playable == null) {
- Log.d(TAG, "Playable was not found. Stopping service.");
- stateManager.stopService();
- return Service.START_NOT_STICKY;
- }
- mediaPlayer.playMediaObject(playable, stream, startWhenPrepared, prepareImmediately);
- addPlayableToQueue(playable);
+
+ Observable.fromCallable(
+ () -> {
+ if (playable instanceof FeedMedia) {
+ return DBReader.getFeedMedia(((FeedMedia) playable).getId());
+ } else {
+ return playable;
+ }
+ })
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ playableLoaded -> {
+ mediaPlayer.playMediaObject(playable, stream, startWhenPrepared,
+ prepareImmediately);
+ addPlayableToQueue(playable);
+ }, error -> {
+ Log.d(TAG, "Playable was not found. Stopping service.");
+ stateManager.stopService();
+ });
+ return Service.START_NOT_STICKY;
} else {
Log.d(TAG, "Did not handle intent to PlaybackService: " + intent);
Log.d(TAG, "Extras: " + intent.getExtras());
@@ -566,7 +593,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntentAllowThisTime)
.addAction(R.drawable.ic_stream_white,
- getString(R.string.stream_label),
+ getString(R.string.confirm_mobile_streaming_button_once),
pendingIntentAllowThisTime)
.addAction(R.drawable.ic_stream_white,
getString(R.string.confirm_mobile_streaming_button_always),
@@ -677,26 +704,33 @@ public class PlaybackService extends MediaBrowserServiceCompat {
}
private void startPlayingFromPreferences() {
- Playable playable = Playable.PlayableUtils.createInstanceFromPreferences(getApplicationContext());
- if (playable != null) {
- if (PlaybackPreferences.getCurrentEpisodeIsStream() && !NetworkUtils.isStreamingAllowed()) {
- displayStreamingNotAllowedNotification(
- new PlaybackServiceStarter(this, playable)
- .prepareImmediately(true)
- .startWhenPrepared(true)
- .shouldStream(true)
- .getIntent());
- PlaybackPreferences.writeNoMediaPlaying();
- stateManager.stopService();
- return;
- }
- mediaPlayer.playMediaObject(playable, PlaybackPreferences.getCurrentEpisodeIsStream(), true, true);
- stateManager.validStartCommandWasReceived();
- PlaybackService.this.updateMediaSessionMetadata(playable);
- addPlayableToQueue(playable);
- } else {
- stateManager.stopService();
- }
+ Observable.fromCallable(() -> Playable.PlayableUtils.createInstanceFromPreferences(getApplicationContext()))
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ playable -> {
+ boolean localFeed = URLUtil.isContentUrl(playable.getStreamUrl());
+ if (PlaybackPreferences.getCurrentEpisodeIsStream()
+ && !NetworkUtils.isStreamingAllowed() && !localFeed) {
+ displayStreamingNotAllowedNotification(
+ new PlaybackServiceStarter(this, playable)
+ .prepareImmediately(true)
+ .startWhenPrepared(true)
+ .shouldStream(true)
+ .getIntent());
+ PlaybackPreferences.writeNoMediaPlaying();
+ stateManager.stopService();
+ return;
+ }
+ mediaPlayer.playMediaObject(playable, PlaybackPreferences.getCurrentEpisodeIsStream(),
+ true, true);
+ stateManager.validStartCommandWasReceived();
+ PlaybackService.this.updateMediaSessionMetadata(playable);
+ addPlayableToQueue(playable);
+ }, error -> {
+ Log.d(TAG, "Playable was not loaded from preferences. Stopping service.");
+ stateManager.stopService();
+ });
}
/**
@@ -722,9 +756,12 @@ public class PlaybackService extends MediaBrowserServiceCompat {
}
@Override
- public void onSleepTimerAlmostExpired() {
- float leftVolume = 0.1f * UserPreferences.getLeftVolume();
- float rightVolume = 0.1f * UserPreferences.getRightVolume();
+ public void onSleepTimerAlmostExpired(long timeLeft) {
+ final float[] multiplicators = {0.1f, 0.2f, 0.3f, 0.3f, 0.3f, 0.4f, 0.4f, 0.4f, 0.6f, 0.8f};
+ float multiplicator = multiplicators[Math.max(0, (int) timeLeft / 1000)];
+ Log.d(TAG, "onSleepTimerAlmostExpired: " + multiplicator);
+ float leftVolume = multiplicator * UserPreferences.getLeftVolume();
+ float rightVolume = multiplicator * UserPreferences.getRightVolume();
mediaPlayer.setVolume(leftVolume, rightVolume);
}
@@ -965,7 +1002,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
}
if (!nextItem.getMedia().localFileAvailable() && !NetworkUtils.isStreamingAllowed()
- && UserPreferences.isFollowQueue()) {
+ && UserPreferences.isFollowQueue() && !nextItem.getFeed().isLocalFeed()) {
displayStreamingNotAllowedNotification(
new PlaybackServiceStarter(this, nextItem.getMedia())
.prepareImmediately(true)
@@ -1154,13 +1191,11 @@ public class PlaybackService extends MediaBrowserServiceCompat {
case INITIALIZING:
state = PlaybackStateCompat.STATE_CONNECTING;
break;
- case INITIALIZED:
- case INDETERMINATE:
- state = PlaybackStateCompat.STATE_NONE;
- break;
case ERROR:
state = PlaybackStateCompat.STATE_ERROR;
break;
+ case INITIALIZED: // Deliberate fall-through
+ case INDETERMINATE:
default:
state = PlaybackStateCompat.STATE_NONE;
break;
@@ -1172,7 +1207,8 @@ public class PlaybackService extends MediaBrowserServiceCompat {
long capabilities = PlaybackStateCompat.ACTION_PLAY_PAUSE
| PlaybackStateCompat.ACTION_REWIND
| PlaybackStateCompat.ACTION_FAST_FORWARD
- | PlaybackStateCompat.ACTION_SKIP_TO_NEXT;
+ | PlaybackStateCompat.ACTION_SKIP_TO_NEXT
+ | PlaybackStateCompat.ACTION_SEEK_TO;
if (useSkipToPreviousForRewindInLockscreen()) {
// Workaround to fool Android so that Lockscreen will expose a skip-to-previous button,
@@ -1188,6 +1224,20 @@ public class PlaybackService extends MediaBrowserServiceCompat {
capabilities = capabilities | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
}
+ UiModeManager uiModeManager = (UiModeManager) getApplicationContext().getSystemService(Context.UI_MODE_SERVICE);
+ if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR) {
+ sessionState.addCustomAction(
+ new PlaybackStateCompat.CustomAction.Builder(
+ CUSTOM_ACTION_REWIND,
+ getString(R.string.rewind_label), R.drawable.ic_notification_fast_rewind)
+ .build());
+ sessionState.addCustomAction(
+ new PlaybackStateCompat.CustomAction.Builder(
+ CUSTOM_ACTION_FAST_FORWARD,
+ getString(R.string.fast_forward_label), R.drawable.ic_notification_fast_forward)
+ .build());
+ }
+
sessionState.setActions(capabilities);
flavorHelper.sessionStateAddActionForWear(sessionState,
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 6a892cc1c..632ac07ea 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
@@ -139,6 +139,7 @@ public class PlaybackServiceNotificationBuilder {
notification.setSmallIcon(R.drawable.ic_notification);
notification.setOngoing(false);
notification.setOnlyAlertOnce(true);
+ notification.setShowWhen(false);
notification.setPriority(UserPreferences.getNotifyPriority());
notification.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
notification.setColor(NotificationCompat.COLOR_DEFAULT);
@@ -183,15 +184,14 @@ public class PlaybackServiceNotificationBuilder {
notification.addAction(R.drawable.ic_notification_pause, //pause action
context.getString(R.string.pause_label),
pauseButtonPendingIntent);
- compactActionList.add(numActions++);
} else {
PendingIntent playButtonPendingIntent = getPendingIntentForMediaAction(
KeyEvent.KEYCODE_MEDIA_PLAY, numActions);
notification.addAction(R.drawable.ic_notification_play, //play action
context.getString(R.string.play_label),
playButtonPendingIntent);
- compactActionList.add(numActions++);
}
+ compactActionList.add(numActions++);
// ff follows play, then we have skip (if it's present)
PendingIntent ffButtonPendingIntent = getPendingIntentForMediaAction(
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 afa7fcebf..05d64ea3e 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,7 @@ import androidx.annotation.NonNull;
import android.util.Log;
import de.danoeh.antennapod.core.preferences.SleepTimerPreferences;
+import io.reactivex.disposables.Disposable;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
@@ -57,7 +58,7 @@ public class PlaybackServiceTaskManager {
private ScheduledFuture<?> widgetUpdaterFuture;
private ScheduledFuture<?> sleepTimerFuture;
private volatile Future<List<FeedItem>> queueFuture;
- private volatile Future<?> chapterLoaderFuture;
+ private volatile Disposable chapterLoaderFuture;
private SleepTimer sleepTimer;
@@ -102,7 +103,7 @@ public class PlaybackServiceTaskManager {
private synchronized void loadQueue() {
if (!isQueueLoaderActive()) {
- queueFuture = schedExecutor.submit(DBReader::getQueue);
+ queueFuture = schedExecutor.submit(() -> DBReader.getQueue());
}
}
@@ -289,29 +290,20 @@ public class PlaybackServiceTaskManager {
}
}
- private synchronized void cancelChapterLoader() {
- if (isChapterLoaderActive()) {
- chapterLoaderFuture.cancel(true);
- }
- }
-
- private synchronized boolean isChapterLoaderActive() {
- return chapterLoaderFuture != null && !chapterLoaderFuture.isDone();
- }
-
/**
* Starts a new thread that loads the chapter marks from a playable object. If another chapter loader is already active,
* it will be cancelled first.
* On completion, the callback's onChapterLoaded method will be called.
*/
public synchronized void startChapterLoader(@NonNull final Playable media) {
- if (isChapterLoaderActive()) {
- cancelChapterLoader();
+ if (chapterLoaderFuture != null) {
+ chapterLoaderFuture.dispose();
+ chapterLoaderFuture = null;
}
if (media.getChapters() == null) {
- Completable.create(emitter -> {
- media.loadChapterMarks();
+ chapterLoaderFuture = Completable.create(emitter -> {
+ media.loadChapterMarks(context);
emitter.onComplete();
})
.subscribeOn(Schedulers.io())
@@ -330,7 +322,11 @@ public class PlaybackServiceTaskManager {
cancelWidgetUpdater();
disableSleepTimer();
cancelQueueLoader();
- cancelChapterLoader();
+
+ if (chapterLoaderFuture != null) {
+ chapterLoaderFuture.dispose();
+ chapterLoaderFuture = null;
+ }
}
/**
@@ -347,7 +343,7 @@ public class PlaybackServiceTaskManager {
if (Looper.myLooper() == Looper.getMainLooper()) {
// Called in main thread => ExoPlayer is used
// Run on ui thread even if called from schedExecutor
- Handler handler = new Handler();
+ Handler handler = new Handler(Looper.getMainLooper());
return () -> handler.post(runnable);
} else {
return runnable;
@@ -360,7 +356,8 @@ public class PlaybackServiceTaskManager {
class SleepTimer implements Runnable {
private static final String TAG = "SleepTimer";
private static final long UPDATE_INTERVAL = 1000L;
- private static final long NOTIFICATION_THRESHOLD = 10000;
+ public static final long NOTIFICATION_THRESHOLD = 10000;
+ private boolean hasVibrated = false;
private final long waitingTime;
private long timeLeft;
private ShakeListener shakeListener;
@@ -373,7 +370,7 @@ public class PlaybackServiceTaskManager {
if (UserPreferences.useExoplayer() && Looper.myLooper() == Looper.getMainLooper()) {
// Run callbacks in main thread so they can call ExoPlayer methods themselves
- this.handler = new Handler();
+ this.handler = new Handler(Looper.getMainLooper());
} else {
this.handler = null;
}
@@ -390,7 +387,6 @@ public class PlaybackServiceTaskManager {
@Override
public void run() {
Log.d(TAG, "Starting");
- boolean notifiedAlmostExpired = false;
long lastTick = System.currentTimeMillis();
while (timeLeft > 0) {
try {
@@ -405,19 +401,19 @@ public class PlaybackServiceTaskManager {
timeLeft -= now - lastTick;
lastTick = now;
- if (timeLeft < NOTIFICATION_THRESHOLD && !notifiedAlmostExpired) {
+ if (timeLeft < NOTIFICATION_THRESHOLD) {
Log.d(TAG, "Sleep timer is about to expire");
- if (SleepTimerPreferences.vibrate()) {
+ if (SleepTimerPreferences.vibrate() && !hasVibrated) {
Vibrator v = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
if (v != null) {
v.vibrate(500);
+ hasVibrated = true;
}
}
if (shakeListener == null && SleepTimerPreferences.shakeToReset()) {
shakeListener = new ShakeListener(context, this);
}
- postCallback(callback::onSleepTimerAlmostExpired);
- notifiedAlmostExpired = true;
+ postCallback(() -> callback.onSleepTimerAlmostExpired(timeLeft));
}
if (timeLeft <= 0) {
Log.d(TAG, "Sleep timer expired");
@@ -425,6 +421,7 @@ public class PlaybackServiceTaskManager {
shakeListener.pause();
shakeListener = null;
}
+ hasVibrated = false;
if (!Thread.currentThread().isInterrupted()) {
postCallback(callback::onSleepTimerExpired);
} else {
@@ -461,7 +458,7 @@ public class PlaybackServiceTaskManager {
public interface PSTMCallback {
void positionSaverTick();
- void onSleepTimerAlmostExpired();
+ void onSleepTimerAlmostExpired(long timeLeft);
void onSleepTimerExpired();
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/ShakeListener.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/ShakeListener.java
index b0b6e164d..b967577af 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/ShakeListener.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/ShakeListener.java
@@ -58,7 +58,6 @@ class ShakeListener implements SensorEventListener
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
- return;
}
} \ No newline at end of file
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 7330a6c80..b218a73f9 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
@@ -19,6 +19,7 @@ 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.SubscriptionsFilter;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.DownloadStatus;
import de.danoeh.antennapod.core.util.LongIntMap;
@@ -72,19 +73,13 @@ public final class DBReader {
@NonNull
private static List<Feed> getFeedList(PodDBAdapter adapter) {
- Cursor cursor = null;
- try {
- cursor = adapter.getAllFeedsCursor();
+ try (Cursor cursor = adapter.getAllFeedsCursor()) {
List<Feed> feeds = new ArrayList<>(cursor.getCount());
while (cursor.moveToNext()) {
Feed feed = extractFeedFromCursorRow(cursor);
feeds.add(feed);
}
return feeds;
- } finally {
- if (cursor != null) {
- cursor.close();
- }
}
}
@@ -96,18 +91,13 @@ public final class DBReader {
public static List<String> getFeedListDownloadUrls() {
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
- Cursor cursor = null;
- try {
- cursor = adapter.getFeedCursorDownloadUrls();
+ try (Cursor cursor = adapter.getFeedCursorDownloadUrls()) {
List<String> result = new ArrayList<>(cursor.getCount());
while (cursor.moveToNext()) {
result.add(cursor.getString(1));
}
return result;
} finally {
- if (cursor != null) {
- cursor.close();
- }
adapter.close();
}
}
@@ -173,9 +163,7 @@ public final class DBReader {
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
- Cursor cursor = null;
- try {
- cursor = adapter.getAllItemsOfFeedCursor(feed);
+ try (Cursor cursor = adapter.getAllItemsOfFeedCursor(feed)) {
List<FeedItem> items = extractItemlistFromCursor(adapter, cursor);
Collections.sort(items, new FeedItemPubdateComparator());
for (FeedItem item : items) {
@@ -183,9 +171,6 @@ public final class DBReader {
}
return items;
} finally {
- if (cursor != null) {
- cursor.close();
- }
adapter.close();
}
}
@@ -227,16 +212,10 @@ public final class DBReader {
@NonNull
static List<FeedItem> getQueue(PodDBAdapter adapter) {
Log.d(TAG, "getQueue()");
- Cursor cursor = null;
- try {
- cursor = adapter.getQueueCursor();
+ try (Cursor cursor = adapter.getQueueCursor()) {
List<FeedItem> items = extractItemlistFromCursor(adapter, cursor);
loadAdditionalFeedItemListData(items);
return items;
- } finally {
- if (cursor != null) {
- cursor.close();
- }
}
}
@@ -258,18 +237,12 @@ public final class DBReader {
}
private static LongList getQueueIDList(PodDBAdapter adapter) {
- Cursor cursor = null;
- try {
- cursor = adapter.getQueueIDCursor();
+ try (Cursor cursor = adapter.getQueueIDCursor()) {
LongList queueIds = new LongList(cursor.getCount());
while (cursor.moveToNext()) {
queueIds.add(cursor.getLong(0));
}
return queueIds;
- } finally {
- if (cursor != null) {
- cursor.close();
- }
}
}
@@ -303,17 +276,12 @@ public final class DBReader {
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
- Cursor cursor = null;
- try {
- cursor = adapter.getDownloadedItemsCursor();
+ try (Cursor cursor = adapter.getDownloadedItemsCursor()) {
List<FeedItem> items = extractItemlistFromCursor(adapter, cursor);
loadAdditionalFeedItemListData(items);
Collections.sort(items, new FeedItemPubdateComparator());
return items;
} finally {
- if (cursor != null) {
- cursor.close();
- }
adapter.close();
}
}
@@ -329,16 +297,11 @@ public final class DBReader {
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
- Cursor cursor = null;
- try {
- cursor = adapter.getPlayedItemsCursor();
+ try (Cursor cursor = adapter.getPlayedItemsCursor()) {
List<FeedItem> items = extractItemlistFromCursor(adapter, cursor);
loadAdditionalFeedItemListData(items);
return items;
} finally {
- if (cursor != null) {
- cursor.close();
- }
adapter.close();
}
}
@@ -356,16 +319,11 @@ public final class DBReader {
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
- Cursor cursor = null;
- try {
- cursor = adapter.getNewItemsCursor(offset, limit);
+ try (Cursor cursor = adapter.getNewItemsCursor(offset, limit)) {
List<FeedItem> items = extractItemlistFromCursor(adapter, cursor);
loadAdditionalFeedItemListData(items);
return items;
} finally {
- if (cursor != null) {
- cursor.close();
- }
adapter.close();
}
}
@@ -382,16 +340,11 @@ public final class DBReader {
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
- Cursor cursor = null;
- try {
- cursor = adapter.getFavoritesCursor(offset, limit);
+ try (Cursor cursor = adapter.getFavoritesCursor(offset, limit)) {
List<FeedItem> items = extractItemlistFromCursor(adapter, cursor);
loadAdditionalFeedItemListData(items);
return items;
} finally {
- if (cursor != null) {
- cursor.close();
- }
adapter.close();
}
}
@@ -401,18 +354,13 @@ public final class DBReader {
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
- Cursor cursor = null;
- try {
- cursor = adapter.getFavoritesCursor(0, Integer.MAX_VALUE);
+ try (Cursor cursor = adapter.getFavoritesCursor(0, Integer.MAX_VALUE)) {
LongList favoriteIDs = new LongList(cursor.getCount());
while (cursor.moveToNext()) {
favoriteIDs.add(cursor.getLong(0));
}
return favoriteIDs;
} finally {
- if (cursor != null) {
- cursor.close();
- }
adapter.close();
}
}
@@ -429,16 +377,11 @@ public final class DBReader {
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
- Cursor cursor = null;
- try {
- cursor = adapter.getRecentlyPublishedItemsCursor(offset, limit);
+ try (Cursor cursor = adapter.getRecentlyPublishedItemsCursor(offset, limit)) {
List<FeedItem> items = extractItemlistFromCursor(adapter, cursor);
loadAdditionalFeedItemListData(items);
return items;
} finally {
- if (cursor != null) {
- cursor.close();
- }
adapter.close();
}
}
@@ -493,9 +436,7 @@ public final class DBReader {
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
- Cursor cursor = null;
- try {
- cursor = adapter.getDownloadLogCursor(DOWNLOAD_LOG_SIZE);
+ try (Cursor cursor = adapter.getDownloadLogCursor(DOWNLOAD_LOG_SIZE)) {
List<DownloadStatus> downloadLog = new ArrayList<>(cursor.getCount());
while (cursor.moveToNext()) {
downloadLog.add(DownloadStatus.fromCursor(cursor));
@@ -503,9 +444,6 @@ public final class DBReader {
Collections.sort(downloadLog, new DownloadStatusComparator());
return downloadLog;
} finally {
- if (cursor != null) {
- cursor.close();
- }
adapter.close();
}
}
@@ -522,9 +460,7 @@ public final class DBReader {
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
- Cursor cursor = null;
- try {
- cursor = adapter.getDownloadLog(Feed.FEEDFILETYPE_FEED, feedId);
+ try (Cursor cursor = adapter.getDownloadLog(Feed.FEEDFILETYPE_FEED, feedId)) {
List<DownloadStatus> downloadLog = new ArrayList<>(cursor.getCount());
while (cursor.moveToNext()) {
downloadLog.add(DownloadStatus.fromCursor(cursor));
@@ -532,9 +468,6 @@ public final class DBReader {
Collections.sort(downloadLog, new DownloadStatusComparator());
return downloadLog;
} finally {
- if (cursor != null) {
- cursor.close();
- }
adapter.close();
}
}
@@ -561,9 +494,7 @@ public final class DBReader {
@Nullable
static Feed getFeed(final long feedId, PodDBAdapter adapter) {
Feed feed = null;
- Cursor cursor = null;
- try {
- cursor = adapter.getFeedCursor(feedId);
+ try (Cursor cursor = adapter.getFeedCursor(feedId)) {
if (cursor.moveToNext()) {
feed = extractFeedFromCursorRow(cursor);
feed.setItems(getFeedItemList(feed));
@@ -571,10 +502,6 @@ public final class DBReader {
Log.e(TAG, "getFeed could not find feed with id " + feedId);
}
return feed;
- } finally {
- if (cursor != null) {
- cursor.close();
- }
}
}
@@ -583,9 +510,7 @@ public final class DBReader {
Log.d(TAG, "Loading feeditem with id " + itemId);
FeedItem item = null;
- Cursor cursor = null;
- try {
- cursor = adapter.getFeedItemCursor(Long.toString(itemId));
+ try (Cursor cursor = adapter.getFeedItemCursor(Long.toString(itemId))) {
if (cursor.moveToNext()) {
List<FeedItem> list = extractItemlistFromCursor(adapter, cursor);
if (!list.isEmpty()) {
@@ -594,10 +519,6 @@ public final class DBReader {
}
}
return item;
- } finally {
- if (cursor != null) {
- cursor.close();
- }
}
}
@@ -632,9 +553,7 @@ public final class DBReader {
@Nullable
private static FeedItem getFeedItemByUrl(final String podcastUrl, final String episodeUrl, PodDBAdapter adapter) {
Log.d(TAG, "Loading feeditem with podcast url " + podcastUrl + " and episode url " + episodeUrl);
- Cursor cursor = null;
- try {
- cursor = adapter.getFeedItemCursor(podcastUrl, episodeUrl);
+ try (Cursor cursor = adapter.getFeedItemCursor(podcastUrl, episodeUrl)) {
if (!cursor.moveToNext()) {
return null;
}
@@ -643,10 +562,6 @@ public final class DBReader {
return list.get(0);
}
return null;
- } finally {
- if (cursor != null) {
- cursor.close();
- }
}
}
@@ -669,10 +584,8 @@ public final class DBReader {
}
private static String getImageAuthentication(final String imageUrl, PodDBAdapter adapter) {
- String credentials = null;
- Cursor cursor = null;
- try {
- cursor = adapter.getImageAuthenticationCursor(imageUrl);
+ String credentials;
+ try (Cursor cursor = adapter.getImageAuthenticationCursor(imageUrl)) {
if (cursor.moveToFirst()) {
String username = cursor.getString(0);
String password = cursor.getString(1);
@@ -684,10 +597,6 @@ public final class DBReader {
} else {
credentials = "";
}
- } finally {
- if (cursor != null) {
- cursor.close();
- }
}
return credentials;
}
@@ -721,9 +630,7 @@ public final class DBReader {
Log.d(TAG, "loadDescriptionOfFeedItem() called with: " + "item = [" + item + "]");
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
- Cursor cursor = null;
- try {
- cursor = adapter.getDescriptionOfItem(item);
+ try (Cursor cursor = adapter.getDescriptionOfItem(item)) {
if (cursor.moveToFirst()) {
int indexDescription = cursor.getColumnIndex(PodDBAdapter.KEY_DESCRIPTION);
String description = cursor.getString(indexDescription);
@@ -733,9 +640,6 @@ public final class DBReader {
item.setContentEncoded(contentEncoded);
}
} finally {
- if (cursor != null) {
- cursor.close();
- }
adapter.close();
}
}
@@ -747,29 +651,30 @@ public final class DBReader {
*
* @param item The FeedItem
*/
- public static void loadChaptersOfFeedItem(final FeedItem item) {
+ public static List<Chapter> loadChaptersOfFeedItem(final FeedItem item) {
Log.d(TAG, "loadChaptersOfFeedItem() called with: " + "item = [" + item + "]");
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
try {
- loadChaptersOfFeedItem(adapter, item);
+ return loadChaptersOfFeedItem(adapter, item);
} finally {
adapter.close();
}
}
- private static void loadChaptersOfFeedItem(PodDBAdapter adapter, FeedItem item) {
+ private static List<Chapter> loadChaptersOfFeedItem(PodDBAdapter adapter, FeedItem item) {
try (Cursor cursor = adapter.getSimpleChaptersOfFeedItemCursor(item)) {
int chaptersCount = cursor.getCount();
if (chaptersCount == 0) {
item.setChapters(null);
- return;
+ return null;
}
- item.setChapters(new ArrayList<>(chaptersCount));
+ ArrayList<Chapter> chapters = new ArrayList<>();
while (cursor.moveToNext()) {
- item.getChapters().add(Chapter.fromCursor(cursor));
+ chapters.add(Chapter.fromCursor(cursor));
}
+ return chapters;
}
}
@@ -842,6 +747,7 @@ public final class DBReader {
long episodesStarted = 0;
long episodesStartedIncludingMarked = 0;
long totalDownloadSize = 0;
+ long episodesDownloadCount = 0;
List<FeedItem> items = getFeed(feed.getId()).getItems();
for (FeedItem item : items) {
FeedMedia media = item.getMedia();
@@ -869,13 +775,14 @@ public final class DBReader {
if (media.isDownloaded()) {
totalDownloadSize = totalDownloadSize + media.getSize();
+ episodesDownloadCount++;
}
episodes++;
}
feedTime.add(new StatisticsItem(
feed, feedTotalTime, feedPlayedTime, feedPlayedTimeCountAll, episodes,
- episodesStarted, episodesStartedIncludingMarked, totalDownloadSize));
+ episodesStarted, episodesStartedIncludingMarked, totalDownloadSize, episodesDownloadCount));
}
adapter.close();
@@ -892,6 +799,7 @@ public final class DBReader {
Log.d(TAG, "getNavDrawerData() called with: " + "");
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++) {
@@ -899,6 +807,9 @@ public final class DBReader {
}
final LongIntMap feedCounters = adapter.getFeedCounters(feedIds);
+ SubscriptionsFilter subscriptionsFilter = UserPreferences.getSubscriptionsFilter();
+ feeds = subscriptionsFilter.filter(getFeedList(adapter), feedCounters);
+
Comparator<Feed> comparator;
int feedOrder = UserPreferences.getFeedOrder();
if (feedOrder == UserPreferences.FEED_ORDER_COUNTER) {
@@ -942,24 +853,11 @@ public final class DBReader {
}
};
} else {
+ final Map<Long, Long> recentPubDates = adapter.getMostRecentItemDates();
comparator = (lhs, rhs) -> {
- if (lhs.getItems() == null || lhs.getItems().size() == 0) {
- List<FeedItem> items = DBReader.getFeedItemList(lhs);
- lhs.setItems(items);
- }
- if (rhs.getItems() == null || rhs.getItems().size() == 0) {
- List<FeedItem> items = DBReader.getFeedItemList(rhs);
- rhs.setItems(items);
- }
- if (lhs.getMostRecentItem() == null) {
- return 1;
- } else if (rhs.getMostRecentItem() == null) {
- return -1;
- } else {
- Date d1 = lhs.getMostRecentItem().getPubDate();
- Date d2 = rhs.getMostRecentItem().getPubDate();
- return d2.compareTo(d1);
- }
+ long dateLhs = recentPubDates.containsKey(lhs.getId()) ? recentPubDates.get(lhs.getId()) : 0;
+ long dateRhs = recentPubDates.containsKey(rhs.getId()) ? recentPubDates.get(rhs.getId()) : 0;
+ return Long.compare(dateRhs, dateLhs);
};
}
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 16e2825b4..c059e696a 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
@@ -16,6 +16,7 @@ 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.sync.SyncService;
@@ -241,7 +242,12 @@ public final class DBTasks {
feed.getPreferences().getUsername(), feed.getPreferences().getPassword());
}
f.setId(feed.getId());
- DownloadRequester.getInstance().downloadFeed(context, f, loadAllPages, force, initiatedByUser);
+
+ if (f.isLocalFeed()) {
+ new Thread(() -> LocalFeedUpdater.updateFeed(f, context)).start();
+ } else {
+ DownloadRequester.getInstance().downloadFeed(context, f, loadAllPages, force, initiatedByUser);
+ }
}
/**
@@ -257,7 +263,6 @@ public final class DBTasks {
EventBus.getDefault().post(new MessageEvent(context.getString(R.string.error_file_not_found)));
}
- @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public static List<? extends FeedItem> enqueueFeedItemsToDownload(final Context context,
List<? extends FeedItem> items) throws InterruptedException, ExecutionException {
List<FeedItem> itemsToEnqueue = new ArrayList<>();
@@ -367,118 +372,135 @@ public final class DBTasks {
* <p/>
* This method should NOT be executed on the GUI thread.
*
- * @param context Used for accessing the DB.
- * @param newFeeds The new Feed objects.
- * @return The updated Feeds from the database if it already existed, or the new Feed from the parameters otherwise.
+ * @param context Used for accessing the DB.
+ * @param newFeed The new Feed object.
+ * @param removeUnlistedItems The item list in the new Feed object is considered to be exhaustive.
+ * I.e. items are removed from the database if they are not in this item list.
+ * @return The updated Feed from the database if it already existed, or the new Feed from the parameters otherwise.
*/
- public static synchronized Feed[] updateFeed(final Context context,
- final Feed... newFeeds) {
- List<Feed> newFeedsList = new ArrayList<>();
- List<Feed> updatedFeedsList = new ArrayList<>();
- Feed[] resultFeeds = new Feed[newFeeds.length];
+ public static synchronized Feed updateFeed(Context context, Feed newFeed, boolean removeUnlistedItems) {
+ Feed resultFeed;
+ List<FeedItem> unlistedItems = new ArrayList<>();
+
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
- for (int feedIdx = 0; feedIdx < newFeeds.length; feedIdx++) {
+ // Look up feed in the feedslist
+ final Feed savedFeed = searchFeedByIdentifyingValueOrID(adapter, newFeed);
+ if (savedFeed == null) {
+ Log.d(TAG, "Found no existing Feed with title "
+ + newFeed.getTitle() + ". Adding as new one.");
+
+ // Add a new Feed
+ // all new feeds will have the most recent item marked as unplayed
+ FeedItem mostRecent = newFeed.getMostRecentItem();
+ if (mostRecent != null) {
+ mostRecent.setNew();
+ }
- final Feed newFeed = newFeeds[feedIdx];
+ resultFeed = newFeed;
+ } else {
+ Log.d(TAG, "Feed with title " + newFeed.getTitle()
+ + " already exists. Syncing new with existing one.");
- // Look up feed in the feedslist
- final Feed savedFeed = searchFeedByIdentifyingValueOrID(adapter,
- newFeed);
- if (savedFeed == null) {
- Log.d(TAG, "Found no existing Feed with title "
- + newFeed.getTitle() + ". Adding as new one.");
-
- // Add a new Feed
- // all new feeds will have the most recent item marked as unplayed
- FeedItem mostRecent = newFeed.getMostRecentItem();
- if (mostRecent != null) {
- mostRecent.setNew();
- }
+ Collections.sort(newFeed.getItems(), new FeedItemPubdateComparator());
- newFeedsList.add(newFeed);
- resultFeeds[feedIdx] = newFeed;
+ if (newFeed.getPageNr() == savedFeed.getPageNr()) {
+ if (savedFeed.compareWithOther(newFeed)) {
+ Log.d(TAG, "Feed has updated attribute values. Updating old feed's attributes");
+ savedFeed.updateFromOther(newFeed);
+ }
} else {
- Log.d(TAG, "Feed with title " + newFeed.getTitle()
- + " already exists. Syncing new with existing one.");
+ Log.d(TAG, "New feed has a higher page number.");
+ savedFeed.setNextPageLink(newFeed.getNextPageLink());
+ }
+ if (savedFeed.getPreferences().compareWithOther(newFeed.getPreferences())) {
+ Log.d(TAG, "Feed has updated preferences. Updating old feed's preferences");
+ savedFeed.getPreferences().updateFromOther(newFeed.getPreferences());
+ }
- Collections.sort(newFeed.getItems(), new FeedItemPubdateComparator());
+ // get the most recent date now, before we start changing the list
+ FeedItem priorMostRecent = savedFeed.getMostRecentItem();
+ Date priorMostRecentDate = null;
+ if (priorMostRecent != null) {
+ priorMostRecentDate = priorMostRecent.getPubDate();
+ }
- if (newFeed.getPageNr() == savedFeed.getPageNr()) {
- if (savedFeed.compareWithOther(newFeed)) {
- Log.d(TAG, "Feed has updated attribute values. Updating old feed's attributes");
- savedFeed.updateFromOther(newFeed);
+ // Look for new or updated Items
+ for (int idx = 0; idx < newFeed.getItems().size(); idx++) {
+ final FeedItem item = newFeed.getItems().get(idx);
+ FeedItem oldItem = searchFeedItemByIdentifyingValue(savedFeed, item.getIdentifyingValue());
+ if (oldItem == null) {
+ // item is new
+ item.setFeed(savedFeed);
+ item.setAutoDownload(savedFeed.getPreferences().getAutoDownload());
+
+ if (idx >= savedFeed.getItems().size()) {
+ savedFeed.getItems().add(item);
+ } else {
+ savedFeed.getItems().add(idx, item);
}
- } else {
- Log.d(TAG, "New feed has a higher page number.");
- savedFeed.setNextPageLink(newFeed.getNextPageLink());
- }
- if (savedFeed.getPreferences().compareWithOther(newFeed.getPreferences())) {
- Log.d(TAG, "Feed has updated preferences. Updating old feed's preferences");
- savedFeed.getPreferences().updateFromOther(newFeed.getPreferences());
- }
- // get the most recent date now, before we start changing the list
- FeedItem priorMostRecent = savedFeed.getMostRecentItem();
- Date priorMostRecentDate = null;
- if (priorMostRecent != null) {
- priorMostRecentDate = priorMostRecent.getPubDate();
+ // only mark the item new if it was published after or at the same time
+ // as the most recent item
+ // (if the most recent date is null then we can assume there are no items
+ // and this is the first, hence 'new')
+ if (priorMostRecentDate == null
+ || priorMostRecentDate.before(item.getPubDate())
+ || priorMostRecentDate.equals(item.getPubDate())) {
+ Log.d(TAG, "Marking item published on " + item.getPubDate()
+ + " new, prior most recent date = " + priorMostRecentDate);
+ item.setNew();
+ }
+ } else {
+ oldItem.updateFromOther(item);
}
+ }
- // Look for new or updated Items
- for (int idx = 0; idx < newFeed.getItems().size(); idx++) {
- final FeedItem item = newFeed.getItems().get(idx);
- FeedItem oldItem = searchFeedItemByIdentifyingValue(savedFeed,
- item.getIdentifyingValue());
- if (oldItem == null) {
- // item is new
- item.setFeed(savedFeed);
- item.setAutoDownload(savedFeed.getPreferences().getAutoDownload());
-
- if (idx >= savedFeed.getItems().size()) {
- savedFeed.getItems().add(item);
- } else {
- savedFeed.getItems().add(idx, item);
- }
-
- // only mark the item new if it was published after or at the same time
- // as the most recent item
- // (if the most recent date is null then we can assume there are no items
- // and this is the first, hence 'new')
- if (priorMostRecentDate == null
- || priorMostRecentDate.before(item.getPubDate())
- || priorMostRecentDate.equals(item.getPubDate())) {
- Log.d(TAG, "Marking item published on " + item.getPubDate()
- + " new, prior most recent date = " + priorMostRecentDate);
- item.setNew();
- }
- } else {
- oldItem.updateFromOther(item);
+ // identify items to be removed
+ if (removeUnlistedItems) {
+ Iterator<FeedItem> it = savedFeed.getItems().iterator();
+ while (it.hasNext()) {
+ FeedItem feedItem = it.next();
+ if (searchFeedItemByIdentifyingValue(newFeed, feedItem.getIdentifyingValue()) == null) {
+ unlistedItems.add(feedItem);
+ it.remove();
}
}
- // update attributes
- savedFeed.setLastUpdate(newFeed.getLastUpdate());
- savedFeed.setType(newFeed.getType());
- savedFeed.setLastUpdateFailed(false);
-
- updatedFeedsList.add(savedFeed);
- resultFeeds[feedIdx] = savedFeed;
}
- }
- adapter.close();
+ // update attributes
+ savedFeed.setLastUpdate(newFeed.getLastUpdate());
+ savedFeed.setType(newFeed.getType());
+ savedFeed.setLastUpdateFailed(false);
+
+ resultFeed = savedFeed;
+ }
try {
- DBWriter.addNewFeed(context, newFeedsList.toArray(new Feed[0])).get();
- DBWriter.setCompleteFeed(updatedFeedsList.toArray(new Feed[0])).get();
+ if (savedFeed == null) {
+ DBWriter.addNewFeed(context, newFeed).get();
+ // Update with default values that are set in database
+ resultFeed = searchFeedByIdentifyingValueOrID(adapter, newFeed);
+ } else {
+ DBWriter.setCompleteFeed(savedFeed).get();
+ }
+ if (removeUnlistedItems) {
+ DBWriter.deleteFeedItems(context, unlistedItems).get();
+ }
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
- EventBus.getDefault().post(new FeedListUpdateEvent(updatedFeedsList));
+ adapter.close();
+
+ if (savedFeed != null) {
+ EventBus.getDefault().post(new FeedListUpdateEvent(savedFeed));
+ } else {
+ EventBus.getDefault().post(new FeedListUpdateEvent(Collections.emptyList()));
+ }
- return resultFeeds;
+ return resultFeed;
}
/**
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 e33b67719..9e6041df3 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
@@ -141,48 +141,74 @@ public class DBWriter {
return dbExec.submit(() -> {
DownloadRequester requester = DownloadRequester.getInstance();
final Feed feed = DBReader.getFeed(feedId);
+ if (feed == null) {
+ return;
+ }
- if (feed != null) {
- // delete stored media files and mark them as read
- List<FeedItem> queue = DBReader.getQueue();
- List<FeedItem> removed = new ArrayList<>();
- if (feed.getItems() == null) {
- DBReader.getFeedItemList(feed);
- }
+ // delete stored media files and mark them as read
+ if (feed.getItems() == null) {
+ DBReader.getFeedItemList(feed);
+ }
+ deleteFeedItemsSynchronous(context, feed.getItems());
- for (FeedItem item : feed.getItems()) {
- if (queue.remove(item)) {
- removed.add(item);
- }
- if (item.getMedia() != null && item.getMedia().isDownloaded()) {
- deleteFeedMediaSynchronous(context, item.getMedia());
- } else if (item.getMedia() != null && requester.isDownloadingFile(item.getMedia())) {
- requester.cancelDownload(context, item.getMedia());
- }
- }
- PodDBAdapter adapter = PodDBAdapter.getInstance();
- adapter.open();
- if (removed.size() > 0) {
- adapter.setQueue(queue);
- for (FeedItem item : removed) {
- EventBus.getDefault().post(QueueEvent.irreversibleRemoved(item));
- }
- }
- adapter.removeFeed(feed);
- adapter.close();
+ // delete feed
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.removeFeed(feed);
+ adapter.close();
- SyncService.enqueueFeedRemoved(context, feed.getDownload_url());
- EventBus.getDefault().post(new FeedListUpdateEvent(feed));
+ SyncService.enqueueFeedRemoved(context, feed.getDownload_url());
+ EventBus.getDefault().post(new FeedListUpdateEvent(feed));
+ });
+ }
- // we assume we also removed download log entries for the feed or its media files.
- // especially important if download or refresh failed, as the user should not be able
- // to retry these
- EventBus.getDefault().post(DownloadLogEvent.listUpdated());
+ /**
+ * Remove the listed items and their FeedMedia entries.
+ * Deleting media also removes the download log entries.
+ */
+ @NonNull
+ public static Future<?> deleteFeedItems(@NonNull Context context, @NonNull List<FeedItem> items) {
+ return dbExec.submit(() -> deleteFeedItemsSynchronous(context, items));
+ }
- BackupManager backupManager = new BackupManager(context);
- backupManager.dataChanged();
+ /**
+ * Remove the listed items and their FeedMedia entries.
+ * Deleting media also removes the download log entries.
+ */
+ private static void deleteFeedItemsSynchronous(@NonNull Context context, @NonNull List<FeedItem> items) {
+ DownloadRequester requester = DownloadRequester.getInstance();
+ List<FeedItem> queue = DBReader.getQueue();
+ List<FeedItem> removedFromQueue = new ArrayList<>();
+ for (FeedItem item : items) {
+ if (queue.remove(item)) {
+ removedFromQueue.add(item);
}
- });
+ if (item.getMedia() != null && item.getMedia().isDownloaded()) {
+ deleteFeedMediaSynchronous(context, item.getMedia());
+ } else if (item.getMedia() != null && requester.isDownloadingFile(item.getMedia())) {
+ requester.cancelDownload(context, item.getMedia());
+ }
+ }
+
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ if (!removedFromQueue.isEmpty()) {
+ adapter.setQueue(queue);
+ }
+ adapter.removeFeedItems(items);
+ adapter.close();
+
+ for (FeedItem item : removedFromQueue) {
+ EventBus.getDefault().post(QueueEvent.irreversibleRemoved(item));
+ }
+
+ // we assume we also removed download log entries for the feed or its media files.
+ // especially important if download or refresh failed, as the user should not be able
+ // to retry these
+ EventBus.getDefault().post(DownloadLogEvent.listUpdated());
+
+ BackupManager backupManager = new BackupManager(context);
+ backupManager.dataChanged();
}
/**
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DatabaseExporter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DatabaseExporter.java
index 234c01b20..271babc6e 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/DatabaseExporter.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DatabaseExporter.java
@@ -5,6 +5,7 @@ import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
+import android.text.format.Formatter;
import android.util.Log;
import de.danoeh.antennapod.core.R;
import org.apache.commons.io.FileUtils;
@@ -53,7 +54,16 @@ public class DatabaseExporter {
if (currentDB.exists()) {
src = new FileInputStream(currentDB).getChannel();
dst = outFileStream.getChannel();
- dst.transferFrom(src, 0, src.size());
+ long srcSize = src.size();
+ dst.transferFrom(src, 0, srcSize);
+
+ long newDstSize = dst.size();
+ if (newDstSize != srcSize) {
+ throw new IOException(String.format(
+ "Unable to write entire database. Expected to write %s, but wrote %s.",
+ Formatter.formatShortFileSize(context, srcSize),
+ Formatter.formatShortFileSize(context, newDstSize)));
+ }
} else {
throw new IOException("Can not access current database");
}
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 f10dde65f..e3121caa2 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
@@ -340,7 +340,7 @@ public class DownloadRequester implements DownloadStateProvider {
/**
* Checks if feedfile is in the downloads list
*/
- public synchronized boolean isDownloadingFile(FeedFile item) {
+ public synchronized boolean isDownloadingFile(@NonNull FeedFile item) {
return item.getDownload_url() != null && downloads.containsKey(item.getDownload_url());
}
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 e6d47b32a..935b06cd6 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
@@ -14,15 +14,18 @@ import android.database.sqlite.SQLiteOpenHelper;
import android.text.TextUtils;
import android.util.Log;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
-import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
import java.util.Set;
import de.danoeh.antennapod.core.feed.Chapter;
@@ -357,6 +360,21 @@ public class PodDBAdapter {
// do nothing
}
+ /**
+ * <p>Resets all database connections to ensure new database connections for
+ * the next test case. Call method only for unit tests.</p>
+ *
+ * <p>That's a workaround for a Robolectric issue in ShadowSQLiteConnection
+ * that leads to an error <tt>IllegalStateException: Illegal connection
+ * pointer</tt> if several threads try to use the same database connection.
+ * For more information see
+ * <a href="https://github.com/robolectric/robolectric/issues/1890">robolectric/robolectric#1890</a>.</p>
+ */
+ public static void tearDownTests() {
+ db = null;
+ SingletonHolder.dbHelper.close();
+ }
+
public static boolean deleteDatabase() {
PodDBAdapter adapter = getInstance();
adapter.open();
@@ -593,6 +611,11 @@ public class PodDBAdapter {
* @return the id of the entry
*/
private long setFeedItem(FeedItem item, boolean saveFeed) {
+ if (item.getId() == 0 && item.getPubDate() == null) {
+ Log.e(TAG, "Newly saved item has no pubDate. Using current date as pubDate");
+ item.setPubDate(new Date());
+ }
+
ContentValues values = new ContentValues();
values.put(KEY_TITLE, item.getTitle());
values.put(KEY_LINK, item.getLink());
@@ -852,6 +875,23 @@ public class PodDBAdapter {
}
/**
+ * Remove the listed items and their FeedMedia entries.
+ */
+ public void removeFeedItems(@NonNull List<FeedItem> items) {
+ try {
+ db.beginTransactionNonExclusive();
+ for (FeedItem item : items) {
+ removeFeedItem(item);
+ }
+ db.setTransactionSuccessful();
+ } catch (SQLException e) {
+ Log.e(TAG, Log.getStackTraceString(e));
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ /**
* Remove a feed with all its FeedItems and Media entries.
*/
public void removeFeed(Feed feed) {
@@ -1184,6 +1224,25 @@ public class PodDBAdapter {
return conditionalFeedCounterRead(whereRead, feedIds);
}
+ public final Map<Long, Long> getMostRecentItemDates() {
+ final String query = "SELECT " + KEY_FEED + ","
+ + " MAX(" + TABLE_NAME_FEED_ITEMS + "." + KEY_PUBDATE + ") AS most_recent_pubdate"
+ + " FROM " + TABLE_NAME_FEED_ITEMS
+ + " GROUP BY " + KEY_FEED;
+
+ Cursor c = db.rawQuery(query, null);
+ Map<Long, Long> result = new HashMap<>();
+ if (c.moveToFirst()) {
+ do {
+ long feedId = c.getLong(0);
+ long date = c.getLong(1);
+ result.put(feedId, date);
+ } while (c.moveToNext());
+ }
+ c.close();
+ return result;
+ }
+
public final int getNumberOfDownloadedEpisodes() {
final String query = "SELECT COUNT(DISTINCT " + KEY_ID + ") AS count FROM " + TABLE_NAME_FEED_MEDIA +
" WHERE " + KEY_DOWNLOADED + " > 0";
@@ -1201,12 +1260,17 @@ public class PodDBAdapter {
* Uses DatabaseUtils to escape a search query and removes ' at the
* beginning and the end of the string returned by the escape method.
*/
- private String prepareSearchQuery(String query) {
- StringBuilder builder = new StringBuilder();
- DatabaseUtils.appendEscapedSQLString(builder, query);
- builder.deleteCharAt(0);
- builder.deleteCharAt(builder.length() - 1);
- return builder.toString();
+ private String[] prepareSearchQuery(String query) {
+ String[] queryWords = query.split("\\s+");
+ for (int i = 0; i < queryWords.length; ++i) {
+ StringBuilder builder = new StringBuilder();
+ DatabaseUtils.appendEscapedSQLString(builder, queryWords[i]);
+ builder.deleteCharAt(0);
+ builder.deleteCharAt(builder.length() - 1);
+ queryWords[i] = builder.toString();
+ }
+
+ return queryWords;
}
/**
@@ -1216,9 +1280,9 @@ public class PodDBAdapter {
* @return A cursor with all search results in SEL_FI_EXTRA selection.
*/
public Cursor searchItems(long feedID, String searchQuery) {
- String preparedQuery = prepareSearchQuery(searchQuery);
+ String[] queryWords = prepareSearchQuery(searchQuery);
- String queryFeedId = "";
+ String queryFeedId;
if (feedID != 0) {
// search items in specific feed
queryFeedId = KEY_FEED + " = " + feedID;
@@ -1227,14 +1291,28 @@ public class PodDBAdapter {
queryFeedId = "1 = 1";
}
- String query = SELECT_FEED_ITEMS_AND_MEDIA_WITH_DESCRIPTION
- + " WHERE " + queryFeedId + " AND ("
- + KEY_DESCRIPTION + " LIKE '%" + preparedQuery + "%' OR "
- + KEY_CONTENT_ENCODED + " LIKE '%" + preparedQuery + "%' OR "
- + KEY_TITLE + " LIKE '%" + preparedQuery + "%'"
- + ") ORDER BY " + KEY_PUBDATE + " DESC "
- + "LIMIT 300";
- return db.rawQuery(query, null);
+ String queryStart = SELECT_FEED_ITEMS_AND_MEDIA_WITH_DESCRIPTION
+ + " WHERE " + queryFeedId + " AND (";
+ StringBuilder sb = new StringBuilder(queryStart);
+
+ for (int i = 0; i < queryWords.length; i++) {
+ sb
+ .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("%') ");
+
+ if (i != queryWords.length - 1) {
+ sb.append("AND ");
+ }
+ }
+
+ sb.append(") ORDER BY " + KEY_PUBDATE + " DESC LIMIT 300");
+
+ return db.rawQuery(sb.toString(), null);
}
/**
@@ -1243,15 +1321,31 @@ public class PodDBAdapter {
* @return A cursor with all search results in SEL_FI_EXTRA selection.
*/
public Cursor searchFeeds(String searchQuery) {
- String preparedQuery = prepareSearchQuery(searchQuery);
- String query = "SELECT * FROM " + TABLE_NAME_FEEDS + " WHERE "
- + KEY_TITLE + " LIKE '%" + preparedQuery + "%' OR "
- + KEY_CUSTOM_TITLE + " LIKE '%" + preparedQuery + "%' OR "
- + KEY_AUTHOR + " LIKE '%" + preparedQuery + "%' OR "
- + KEY_DESCRIPTION + " LIKE '%" + preparedQuery + "%' "
- + "ORDER BY " + KEY_TITLE + " ASC "
- + "LIMIT 300";
- return db.rawQuery(query, null);
+ String[] queryWords = prepareSearchQuery(searchQuery);
+
+ String queryStart = "SELECT * FROM " + TABLE_NAME_FEEDS + " WHERE ";
+ StringBuilder sb = new StringBuilder(queryStart);
+
+ for (int i = 0; i < queryWords.length; i++) {
+ sb
+ .append("(")
+ .append(KEY_TITLE).append(" LIKE '%").append(queryWords[i])
+ .append("%' OR ")
+ .append(KEY_CUSTOM_TITLE).append(" LIKE '%").append(queryWords[i])
+ .append("%' OR ")
+ .append(KEY_AUTHOR).append(" LIKE '%").append(queryWords[i])
+ .append("%' OR ")
+ .append(KEY_DESCRIPTION).append(" LIKE '%").append(queryWords[i])
+ .append("%') ");
+
+ if (i != queryWords.length - 1) {
+ sb.append("AND ");
+ }
+ }
+
+ sb.append("ORDER BY " + KEY_TITLE + " ASC LIMIT 300");
+
+ return db.rawQuery(sb.toString(), null);
}
/**
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/StatisticsItem.java b/core/src/main/java/de/danoeh/antennapod/core/storage/StatisticsItem.java
index f96af185b..18a5403a7 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/StatisticsItem.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/StatisticsItem.java
@@ -36,9 +36,14 @@ public class StatisticsItem {
*/
public final long totalDownloadSize;
+ /**
+ * Stores the number of episodes downloaded.
+ */
+ public final long episodesDownloadCount;
+
public StatisticsItem(Feed feed, long time, long timePlayed, long timePlayedCountAll,
long episodes, long episodesStarted, long episodesStartedIncludingMarked,
- long totalDownloadSize) {
+ long totalDownloadSize, long episodesDownloadCount) {
this.feed = feed;
this.time = time;
this.timePlayed = timePlayed;
@@ -47,5 +52,6 @@ public class StatisticsItem {
this.episodesStarted = episodesStarted;
this.episodesStartedIncludingMarked = episodesStartedIncludingMarked;
this.totalDownloadSize = totalDownloadSize;
+ this.episodesDownloadCount = episodesDownloadCount;
}
}
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 4c89ebc19..1f5d9b75f 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
@@ -39,6 +39,7 @@ import de.danoeh.antennapod.core.sync.model.ISyncService;
import de.danoeh.antennapod.core.sync.model.SubscriptionChanges;
import de.danoeh.antennapod.core.sync.model.SyncServiceException;
import de.danoeh.antennapod.core.sync.model.UploadChangesResponse;
+import de.danoeh.antennapod.core.util.LongList;
import de.danoeh.antennapod.core.util.URLChecker;
import de.danoeh.antennapod.core.util.gui.NotificationUtils;
import io.reactivex.Completable;
@@ -456,7 +457,7 @@ public class SyncService extends Worker {
break;
}
}
-
+ LongList queueToBeRemoved = new LongList();
List<FeedItem> updatedItems = new ArrayList<>();
for (EpisodeAction action : mostRecentPlayAction.values()) {
FeedItem playItem = DBReader.getFeedItemByUrl(action.getPodcast(), action.getEpisode());
@@ -467,10 +468,12 @@ public class SyncService extends Worker {
if (playItem.getMedia().hasAlmostEnded()) {
Log.d(TAG, "Marking as played");
playItem.setPlayed(true);
+ queueToBeRemoved.add(playItem.getId());
}
updatedItems.add(playItem);
}
}
+ DBWriter.removeQueueItem(getApplicationContext(), false, queueToBeRemoved.toArray());
DBWriter.setItemList(updatedItems);
}
@@ -491,7 +494,7 @@ public class SyncService extends Worker {
PendingIntent pendingIntent = PendingIntent.getActivity(getApplicationContext(),
R.id.pending_intent_sync_error, intent, PendingIntent.FLAG_UPDATE_CURRENT);
Notification notification = new NotificationCompat.Builder(getApplicationContext(),
- NotificationUtils.CHANNEL_ID_ERROR)
+ NotificationUtils.CHANNEL_ID_SYNC_ERROR)
.setContentTitle(getApplicationContext().getString(R.string.gpodnetsync_error_title))
.setContentText(description)
.setContentIntent(pendingIntent)
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 eae7a08af..62c8ce5f3 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
@@ -21,7 +21,6 @@ import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
-import org.apache.commons.io.Charsets;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@@ -34,6 +33,7 @@ import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
+import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
@@ -505,7 +505,7 @@ public class GpodnetService implements ISyncService {
RequestBody requestBody = RequestBody.create(TEXT, "");
Request request = new Request.Builder().url(url).post(requestBody).build();
try {
- String credential = Credentials.basic(username, password, Charsets.UTF_8);
+ String credential = Credentials.basic(username, password, Charset.forName("UTF-8"));
Request authRequest = request.newBuilder().header("Authorization", credential).build();
Response response = httpClient.newCall(authRequest).execute();
checkStatusCode(response);
@@ -519,8 +519,8 @@ public class GpodnetService implements ISyncService {
private String executeRequest(@NonNull Request.Builder requestB) throws GpodnetServiceException {
Request request = requestB.build();
- String responseString = null;
- Response response = null;
+ String responseString;
+ Response response;
ResponseBody body = null;
try {
diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/model/EpisodeAction.java b/core/src/main/java/de/danoeh/antennapod/core/sync/model/EpisodeAction.java
index 6154ccc84..798be8d96 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/sync/model/EpisodeAction.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/sync/model/EpisodeAction.java
@@ -3,6 +3,7 @@ package de.danoeh.antennapod.core.sync.model;
import android.text.TextUtils;
import android.util.Log;
+import androidx.annotation.NonNull;
import androidx.core.util.ObjectsCompat;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.util.DateUtils;
@@ -179,6 +180,7 @@ public class EpisodeAction {
return obj;
}
+ @NonNull
@Override
public String toString() {
return "EpisodeAction{"
diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/model/EpisodeActionChanges.java b/core/src/main/java/de/danoeh/antennapod/core/sync/model/EpisodeActionChanges.java
index 77942ffa0..90af585af 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/sync/model/EpisodeActionChanges.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/sync/model/EpisodeActionChanges.java
@@ -23,6 +23,7 @@ public class EpisodeActionChanges {
return this.timestamp;
}
+ @NonNull
@Override
public String toString() {
return "EpisodeActionGetResponse{"
diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/AtomText.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/AtomText.java
index e85d5fae1..0c0561279 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/AtomText.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/AtomText.java
@@ -1,7 +1,7 @@
package de.danoeh.antennapod.core.syndication.namespace.atom;
-import android.os.Build;
-import android.text.Html;
+import androidx.core.text.HtmlCompat;
+
import de.danoeh.antennapod.core.syndication.namespace.Namespace;
import de.danoeh.antennapod.core.syndication.namespace.SyndElement;
@@ -24,11 +24,7 @@ public class AtomText extends SyndElement {
if (type == null) {
return content;
} else if (type.equals(TYPE_HTML)) {
- if (Build.VERSION.SDK_INT >= 24) {
- return Html.fromHtml(content, Html.FROM_HTML_MODE_LEGACY).toString();
- } else {
- return Html.fromHtml(content).toString();
- }
+ return HtmlCompat.fromHtml(content, HtmlCompat.FROM_HTML_MODE_LEGACY).toString();
} else if (type.equals(TYPE_XHTML)) {
return content;
} else { // Handle as text by default
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 737f902b7..d4a2cdca6 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
@@ -1,12 +1,12 @@
package de.danoeh.antennapod.core.util;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.net.Uri;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import android.util.Log;
import java.net.URLConnection;
-import java.util.zip.CheckedOutputStream;
-
import de.danoeh.antennapod.core.ClientConfig;
import org.apache.commons.io.IOUtils;
@@ -52,65 +52,73 @@ public class ChapterUtils {
return chapters.size() - 1;
}
- public static void loadChaptersFromStreamUrl(Playable media) {
- ChapterUtils.readID3ChaptersFromPlayableStreamUrl(media);
- if (media.getChapters() == null) {
- ChapterUtils.readOggChaptersFromPlayableStreamUrl(media);
+ public static List<Chapter> loadChaptersFromStreamUrl(Playable media, Context context) {
+ List<Chapter> chapters = ChapterUtils.readID3ChaptersFromPlayableStreamUrl(media, context);
+ if (chapters == null) {
+ chapters = ChapterUtils.readOggChaptersFromPlayableStreamUrl(media, context);
}
+ return chapters;
}
- public static void loadChaptersFromFileUrl(Playable media) {
+ 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;
+ return null;
}
- ChapterUtils.readID3ChaptersFromPlayableFileUrl(media);
- if (media.getChapters() == null) {
- ChapterUtils.readOggChaptersFromPlayableFileUrl(media);
+ List<Chapter> chapters = ChapterUtils.readID3ChaptersFromPlayableFileUrl(media);
+ if (chapters == null) {
+ chapters = ChapterUtils.readOggChaptersFromPlayableFileUrl(media);
}
+ return chapters;
}
/**
* Uses the download URL of a media object of a feeditem to read its ID3
* chapters.
*/
- private static void readID3ChaptersFromPlayableStreamUrl(Playable p) {
+ 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;
+ return null;
}
Log.d(TAG, "Reading id3 chapters from item " + p.getEpisodeTitle());
CountingInputStream in = null;
try {
- URL url = new URL(p.getStreamUrl());
- URLConnection urlConnection = url.openConnection();
- urlConnection.setRequestProperty("User-Agent", ClientConfig.USER_AGENT);
- in = new CountingInputStream(urlConnection.getInputStream());
+ 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);
if (!chapters.isEmpty()) {
- p.setChapters(chapters);
+ return chapters;
}
Log.i(TAG, "Chapters loaded");
- } catch (IOException | ID3ReaderException e) {
+ } 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 void readID3ChaptersFromPlayableFileUrl(Playable p) {
+ private static List<Chapter> readID3ChaptersFromPlayableFileUrl(Playable p) {
if (p == null || !p.localFileAvailable() || p.getLocalMediaUrl() == null) {
- return;
+ 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;
+ return null;
}
CountingInputStream in = null;
@@ -118,7 +126,7 @@ public class ChapterUtils {
in = new CountingInputStream(new BufferedInputStream(new FileInputStream(source)));
List<Chapter> chapters = readChaptersFrom(in);
if (!chapters.isEmpty()) {
- p.setChapters(chapters);
+ return chapters;
}
Log.i(TAG, "Chapters loaded");
} catch (IOException | ID3ReaderException e) {
@@ -126,6 +134,7 @@ public class ChapterUtils {
} finally {
IOUtils.closeQuietly(in);
}
+ return null;
}
@NonNull
@@ -147,45 +156,52 @@ public class ChapterUtils {
return chapters;
}
- private static void readOggChaptersFromPlayableStreamUrl(Playable media) {
+ private static List<Chapter> readOggChaptersFromPlayableStreamUrl(Playable media, Context context) {
if (media == null || !media.streamAvailable()) {
- return;
+ return null;
}
InputStream input = null;
try {
- URL url = new URL(media.getStreamUrl());
- URLConnection urlConnection = url.openConnection();
- urlConnection.setRequestProperty("User-Agent", ClientConfig.USER_AGENT);
- input = urlConnection.getInputStream();
+ 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) {
- readOggChaptersFromInputStream(media, input);
+ return readOggChaptersFromInputStream(media, input);
}
- } catch (IOException e) {
+ } catch (IOException | IllegalArgumentException e) {
Log.e(TAG, Log.getStackTraceString(e));
} finally {
IOUtils.closeQuietly(input);
}
+ return null;
}
- private static void readOggChaptersFromPlayableFileUrl(Playable media) {
+ private static List<Chapter> readOggChaptersFromPlayableFileUrl(Playable media) {
if (media == null || media.getLocalMediaUrl() == null) {
- return;
+ return null;
}
File source = new File(media.getLocalMediaUrl());
if (source.exists()) {
InputStream input = null;
try {
input = new BufferedInputStream(new FileInputStream(source));
- readOggChaptersFromInputStream(media, input);
+ return readOggChaptersFromInputStream(media, input);
} catch (FileNotFoundException e) {
Log.e(TAG, Log.getStackTraceString(e));
} finally {
IOUtils.closeQuietly(input);
}
}
+ return null;
}
- private static void readOggChaptersFromInputStream(Playable p, InputStream input) {
+ 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();
@@ -193,19 +209,20 @@ public class ChapterUtils {
List<Chapter> chapters = reader.getChapters();
if (chapters == null) {
Log.i(TAG, "ChapterReader could not find any Ogg vorbis chapters");
- return;
+ return null;
}
Collections.sort(chapters, new ChapterStartTimeComparator());
enumerateEmptyChapterTitles(chapters);
if (chaptersValid(chapters)) {
- p.setChapters(chapters);
Log.i(TAG, "Chapters loaded");
+ return chapters;
} else {
Log.e(TAG, "Chapter data was invalid");
}
} catch (VorbisCommentReaderException e) {
e.printStackTrace();
}
+ return null;
}
/**
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 220a783f3..2a387b7b0 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
@@ -4,6 +4,7 @@ import android.text.TextUtils;
import androidx.annotation.VisibleForTesting;
import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.StringUtils;
import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;
@@ -29,6 +30,7 @@ public class FileNameGenerator {
* characters of the given string.
*/
public static String generateFileName(String string) {
+ string = StringUtils.stripAccents(string);
StringBuilder buf = new StringBuilder();
for (int i = 0; i < string.length(); i++) {
char c = string.charAt(i);
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java
index a9e46e42c..8cca2f28f 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java
@@ -2,9 +2,12 @@ package de.danoeh.antennapod.core.util;
import android.content.Context;
import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
+import android.os.Build;
import androidx.core.net.ConnectivityManagerCompat;
import android.text.TextUtils;
import android.util.Log;
@@ -29,65 +32,65 @@ import okhttp3.Response;
public class NetworkUtils {
private NetworkUtils(){}
- private static final String TAG = NetworkUtils.class.getSimpleName();
-
- private static Context context;
-
- public static void init(Context context) {
- NetworkUtils.context = context;
- }
-
- /**
- * Returns true if the device is connected to Wi-Fi and the Wi-Fi filter for
- * automatic downloads is disabled or the device is connected to a Wi-Fi
- * network that is on the 'selected networks' list of the Wi-Fi filter for
- * automatic downloads and false otherwise.
- * */
- public static boolean autodownloadNetworkAvailable() {
- ConnectivityManager cm = (ConnectivityManager) context
- .getSystemService(Context.CONNECTIVITY_SERVICE);
- NetworkInfo networkInfo = cm.getActiveNetworkInfo();
- if (networkInfo != null) {
- if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
- Log.d(TAG, "Device is connected to Wi-Fi");
- if (networkInfo.isConnected()) {
- if (!UserPreferences.isEnableAutodownloadWifiFilter()) {
- Log.d(TAG, "Auto-dl filter is disabled");
- return true;
- } else {
- WifiManager wm = (WifiManager) context.getApplicationContext()
- .getSystemService(Context.WIFI_SERVICE);
- WifiInfo wifiInfo = wm.getConnectionInfo();
- List<String> selectedNetworks = Arrays
- .asList(UserPreferences
- .getAutodownloadSelectedNetworks());
- if (selectedNetworks.contains(Integer.toString(wifiInfo
- .getNetworkId()))) {
- Log.d(TAG, "Current network is on the selected networks list");
- return true;
- }
- }
- }
- } else if (networkInfo.getType() == ConnectivityManager.TYPE_ETHERNET) {
- Log.d(TAG, "Device is connected to Ethernet");
- if (networkInfo.isConnected()) {
- return true;
- }
- } else {
- if (!UserPreferences.isAllowMobileAutoDownload()) {
- Log.d(TAG, "Auto Download not enabled on Mobile");
- return false;
- }
- if (networkInfo.isRoaming()) {
- Log.d(TAG, "Roaming on foreign network");
- return false;
- }
- return true;
- }
- }
- Log.d(TAG, "Network for auto-dl is not available");
- return false;
- }
+ private static final String TAG = NetworkUtils.class.getSimpleName();
+
+ private static Context context;
+
+ public static void init(Context context) {
+ NetworkUtils.context = context;
+ }
+
+ /**
+ * Returns true if the device is connected to Wi-Fi and the Wi-Fi filter for
+ * automatic downloads is disabled or the device is connected to a Wi-Fi
+ * network that is on the 'selected networks' list of the Wi-Fi filter for
+ * automatic downloads and false otherwise.
+ * */
+ public static boolean autodownloadNetworkAvailable() {
+ ConnectivityManager cm = (ConnectivityManager) context
+ .getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo networkInfo = cm.getActiveNetworkInfo();
+ if (networkInfo != null) {
+ if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
+ Log.d(TAG, "Device is connected to Wi-Fi");
+ if (networkInfo.isConnected()) {
+ if (!UserPreferences.isEnableAutodownloadWifiFilter()) {
+ Log.d(TAG, "Auto-dl filter is disabled");
+ return true;
+ } else {
+ WifiManager wm = (WifiManager) context.getApplicationContext()
+ .getSystemService(Context.WIFI_SERVICE);
+ WifiInfo wifiInfo = wm.getConnectionInfo();
+ List<String> selectedNetworks = Arrays
+ .asList(UserPreferences
+ .getAutodownloadSelectedNetworks());
+ if (selectedNetworks.contains(Integer.toString(wifiInfo
+ .getNetworkId()))) {
+ Log.d(TAG, "Current network is on the selected networks list");
+ return true;
+ }
+ }
+ }
+ } else if (networkInfo.getType() == ConnectivityManager.TYPE_ETHERNET) {
+ Log.d(TAG, "Device is connected to Ethernet");
+ if (networkInfo.isConnected()) {
+ return true;
+ }
+ } else {
+ if (!UserPreferences.isAllowMobileAutoDownload()) {
+ Log.d(TAG, "Auto Download not enabled on Mobile");
+ return false;
+ }
+ if (networkInfo.isRoaming()) {
+ Log.d(TAG, "Roaming on foreign network");
+ return false;
+ }
+ return true;
+ }
+ }
+ Log.d(TAG, "Network for auto-dl is not available");
+ return false;
+ }
public static boolean networkAvailable() {
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
@@ -96,7 +99,7 @@ public class NetworkUtils {
}
public static boolean isEpisodeDownloadAllowed() {
- return UserPreferences.isAllowMobileEpisodeDownload() || !NetworkUtils.isNetworkMetered();
+ return UserPreferences.isAllowMobileEpisodeDownload() || !NetworkUtils.isNetworkRestricted();
}
public static boolean isEpisodeHeadDownloadAllowed() {
@@ -106,22 +109,53 @@ public class NetworkUtils {
}
public static boolean isImageAllowed() {
- return UserPreferences.isAllowMobileImages() || !NetworkUtils.isNetworkMetered();
+ return UserPreferences.isAllowMobileImages() || !NetworkUtils.isNetworkRestricted();
}
public static boolean isStreamingAllowed() {
- return UserPreferences.isAllowMobileStreaming() || !NetworkUtils.isNetworkMetered();
+ return UserPreferences.isAllowMobileStreaming() || !NetworkUtils.isNetworkRestricted();
}
public static boolean isFeedRefreshAllowed() {
- return UserPreferences.isAllowMobileFeedRefresh() || !NetworkUtils.isNetworkMetered();
+ return UserPreferences.isAllowMobileFeedRefresh() || !NetworkUtils.isNetworkRestricted();
+ }
+
+ public static boolean isNetworkRestricted() {
+ return isNetworkMetered() || isNetworkCellular();
}
- private static boolean isNetworkMetered() {
- ConnectivityManager connManager = (ConnectivityManager) context
- .getSystemService(Context.CONNECTIVITY_SERVICE);
+ private static boolean isNetworkMetered() {
+ ConnectivityManager connManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
return ConnectivityManagerCompat.isActiveNetworkMetered(connManager);
- }
+ }
+
+ private static boolean isNetworkCellular() {
+ ConnectivityManager connManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ if (Build.VERSION.SDK_INT >= 23) {
+ Network network = connManager.getActiveNetwork();
+ if (network == null) {
+ return false; // Nothing connected
+ }
+ NetworkInfo info = connManager.getNetworkInfo(network);
+ if (info == null) {
+ return true; // Better be safe than sorry
+ }
+ NetworkCapabilities capabilities = connManager.getNetworkCapabilities(network);
+ if (capabilities == null) {
+ return true; // Better be safe than sorry
+ }
+ return capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR);
+ } else {
+ // if the default network is a VPN,
+ // this method will return the NetworkInfo for one of its underlying networks
+ NetworkInfo info = connManager.getActiveNetworkInfo();
+ if (info == null) {
+ return false; // Nothing connected
+ }
+ //noinspection deprecation
+ return info.getType() == ConnectivityManager.TYPE_MOBILE;
+ }
+ }
/**
* Returns the SSID of the wifi connection, or <code>null</code> if there is no wifi.
@@ -135,7 +169,7 @@ public class NetworkUtils {
return null;
}
- public static Single<Long> getFeedMediaSizeObservable(FeedMedia media) {
+ public static Single<Long> getFeedMediaSizeObservable(FeedMedia media) {
return Single.create((SingleOnSubscribe<Long>) emitter -> {
if (!NetworkUtils.isEpisodeHeadDownloadAllowed()) {
emitter.onSuccess(0L);
@@ -188,7 +222,7 @@ public class NetworkUtils {
DBWriter.setFeedMedia(media);
})
.subscribeOn(Schedulers.io())
- .observeOn(AndroidSchedulers.mainThread());
+ .observeOn(AndroidSchedulers.mainThread());
}
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtils.java
index 366f86707..813c6d0f7 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtils.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtils.java
@@ -39,7 +39,7 @@ public class RewindAfterPauseUtils {
int newPosition = currentPosition - (int) rewindTime;
- return newPosition > 0 ? newPosition : 0;
+ return Math.max(newPosition, 0);
} else {
return currentPosition;
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/ChapterStartTimeComparator.java b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/ChapterStartTimeComparator.java
index 8bd23c2ed..920a1ef8a 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/ChapterStartTimeComparator.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/ChapterStartTimeComparator.java
@@ -8,13 +8,7 @@ public class ChapterStartTimeComparator implements Comparator<Chapter> {
@Override
public int compare(Chapter lhs, Chapter rhs) {
- if (lhs.getStart() == rhs.getStart()) {
- return 0;
- } else if (lhs.getStart() < rhs.getStart()) {
- return -1;
- } else {
- return 1;
- }
+ return Long.compare(lhs.getStart(), rhs.getStart());
}
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java
index 51fe2da78..ad81a1d17 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java
@@ -4,16 +4,20 @@ import java.util.Comparator;
import de.danoeh.antennapod.core.feed.FeedItem;
-/** Compares the pubDate of two FeedItems for sorting*/
+/**
+ * Compares the pubDate of two FeedItems for sorting.
+ */
public class FeedItemPubdateComparator implements Comparator<FeedItem> {
- /** Returns a new instance of this comparator in reverse order.
- public static FeedItemPubdateComparator newInstance() {
- FeedItemPubdateComparator
- }*/
- @Override
- public int compare(FeedItem lhs, FeedItem rhs) {
- return rhs.getPubDate().compareTo(lhs.getPubDate());
- }
+ /**
+ * Returns a new instance of this comparator in reverse order.
+ */
+ @Override
+ public int compare(FeedItem lhs, FeedItem rhs) {
+ if (rhs.getPubDate() == null || lhs.getPubDate() == null) {
+ return 0;
+ }
+ return rhs.getPubDate().compareTo(lhs.getPubDate());
+ }
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/download/AutoUpdateManager.java b/core/src/main/java/de/danoeh/antennapod/core/util/download/AutoUpdateManager.java
index 991089910..a8ca43ccb 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/download/AutoUpdateManager.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/download/AutoUpdateManager.java
@@ -118,9 +118,8 @@ public class AutoUpdateManager {
*/
public static void runImmediate(@NonNull Context context) {
Log.d(TAG, "Run auto update immediately in background.");
- new Thread(() -> {
- DBTasks.refreshAllFeeds(context.getApplicationContext(), true);
- }, "ManualRefreshAllFeeds").start();
+ new Thread(() -> DBTasks.refreshAllFeeds(
+ context.getApplicationContext(), true), "ManualRefreshAllFeeds").start();
}
public static void disableAutoUpdate(Context context) {
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 f546ca019..ddbe68938 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
@@ -14,6 +14,7 @@ public class NotificationUtils {
public static final String CHANNEL_ID_DOWNLOADING = "downloading";
public static final String CHANNEL_ID_PLAYING = "playing";
public static final String CHANNEL_ID_ERROR = "error";
+ public static final String CHANNEL_ID_SYNC_ERROR = "sync_error";
public static final String CHANNEL_ID_AUTO_DOWNLOAD = "auto_download";
public static void createChannels(Context context) {
@@ -27,6 +28,7 @@ public class NotificationUtils {
mNotificationManager.createNotificationChannel(createChannelDownloading(context));
mNotificationManager.createNotificationChannel(createChannelPlaying(context));
mNotificationManager.createNotificationChannel(createChannelError(context));
+ mNotificationManager.createNotificationChannel(createChannelSyncError(context));
mNotificationManager.createNotificationChannel(createChannelAutoDownload(context));
}
}
@@ -66,6 +68,14 @@ public class NotificationUtils {
}
@RequiresApi(api = Build.VERSION_CODES.O)
+ private static NotificationChannel createChannelSyncError(Context c) {
+ NotificationChannel notificationChannel = new NotificationChannel(CHANNEL_ID_SYNC_ERROR,
+ c.getString(R.string.notification_channel_sync_error), NotificationManager.IMPORTANCE_HIGH);
+ notificationChannel.setDescription(c.getString(R.string.notification_channel_sync_error_description));
+ return notificationChannel;
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.O)
private static NotificationChannel createChannelAutoDownload(Context c) {
NotificationChannel mChannel = new NotificationChannel(CHANNEL_ID_AUTO_DOWNLOAD,
c.getString(R.string.notification_channel_auto_download), NotificationManager.IMPORTANCE_DEFAULT);
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 aec53da4c..fecb14d25 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
@@ -2,7 +2,7 @@ package de.danoeh.antennapod.core.util.playback;
import android.content.Context;
import android.content.SharedPreferences;
-import android.preference.PreferenceManager;
+import androidx.preference.PreferenceManager;
import android.util.Log;
import android.view.SurfaceHolder;
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
index b55091009..6c107996f 100644
--- 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
@@ -99,11 +99,11 @@ public class ExternalMedia implements Playable {
e.printStackTrace();
throw new PlayableException("NumberFormatException when reading duration of media file");
}
- ChapterUtils.loadChaptersFromFileUrl(this);
+ setChapters(ChapterUtils.loadChaptersFromFileUrl(this));
}
@Override
- public void loadChapterMarks() {
+ public void loadChapterMarks(Context context) {
}
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 24aabf212..5b15913c8 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,12 +3,9 @@ package de.danoeh.antennapod.core.util.playback;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Parcelable;
-import android.preference.PreferenceManager;
-import androidx.annotation.Nullable;
+import androidx.preference.PreferenceManager;
import android.util.Log;
-
-import java.util.List;
-
+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;
@@ -17,6 +14,8 @@ import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.util.ShownotesProvider;
+import java.util.List;
+
/**
* Interface for objects that can be played by the PlaybackService.
*/
@@ -44,7 +43,7 @@ public interface Playable extends Parcelable,
* Playable objects should load their chapter marks in this method if no
* local file was available when loadMetadata() was called.
*/
- void loadChapterMarks();
+ void loadChapterMarks(Context context);
/**
* Returns the title of the episode that this playable represents
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 77525e1e5..425a07f4a 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
@@ -600,6 +600,13 @@ public class PlaybackController {
}
public void setPlaybackSpeed(float speed) {
+ PlaybackPreferences.setCurrentlyPlayingTemporaryPlaybackSpeed(speed);
+ if (getMedia() != null && getMedia().getMediaType() == MediaType.VIDEO) {
+ UserPreferences.setVideoPlaybackSpeed(speed);
+ } else {
+ UserPreferences.setPlaybackSpeed(speed);
+ }
+
if (playbackService != null) {
playbackService.setSpeed(speed);
} else {
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 ca09cda4b..29eb20aca 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
@@ -129,8 +129,8 @@ public class RemoteMedia implements Playable {
}
@Override
- public void loadChapterMarks() {
- ChapterUtils.loadChaptersFromStreamUrl(this);
+ public void loadChapterMarks(Context context) {
+ setChapters(ChapterUtils.loadChaptersFromStreamUrl(this, context));
}
@Override
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/OggInputStream.java b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/OggInputStream.java
index cdf171299..9277af6e6 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/OggInputStream.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/OggInputStream.java
@@ -39,7 +39,7 @@ class OggInputStream extends InputStream {
private void readOggPage() throws IOException {
// find OggS
int[] buffer = new int[4];
- int c = 0;
+ int c;
boolean isInOggS = false;
while ((c = input.read()) != -1) {
switch (c) {
diff --git a/core/src/main/res/color/filter_dialog_button_text.xml b/core/src/main/res/color/filter_dialog_button_text.xml
new file mode 100644
index 000000000..fea8b3e74
--- /dev/null
+++ b/core/src/main/res/color/filter_dialog_button_text.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="?attr/colorOnSecondary" android:state_checked="true" />
+ <item android:color="?android:textColorPrimary" />
+</selector> \ No newline at end of file
diff --git a/core/src/main/res/color/filter_dialog_clear_dark.xml b/core/src/main/res/color/filter_dialog_clear_dark.xml
new file mode 100644
index 000000000..88e022d0f
--- /dev/null
+++ b/core/src/main/res/color/filter_dialog_clear_dark.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@color/dialog_filter_inactive_dark" android:state_checked="true" />
+ <item android:color="@color/dialog_filter_clear_inactive_dark" />
+</selector> \ No newline at end of file
diff --git a/core/src/main/res/color/filter_dialog_clear_light.xml b/core/src/main/res/color/filter_dialog_clear_light.xml
new file mode 100644
index 000000000..9d513f72a
--- /dev/null
+++ b/core/src/main/res/color/filter_dialog_clear_light.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@color/dialog_filter_inactive_light" android:state_checked="true" />
+ <item android:color="@color/dialog_filter_clear_inactive_light" />
+</selector> \ No newline at end of file
diff --git a/core/src/main/res/drawable/filter_dialog_background_dark.xml b/core/src/main/res/drawable/filter_dialog_background_dark.xml
new file mode 100644
index 000000000..9ea827147
--- /dev/null
+++ b/core/src/main/res/drawable/filter_dialog_background_dark.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@color/accent_dark" android:state_checked="true"/>
+ <item android:drawable="@color/dialog_filter_inactive_dark" />
+</selector> \ No newline at end of file
diff --git a/core/src/main/res/drawable/filter_dialog_background_light.xml b/core/src/main/res/drawable/filter_dialog_background_light.xml
new file mode 100644
index 000000000..e0a80737c
--- /dev/null
+++ b/core/src/main/res/drawable/filter_dialog_background_light.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@color/accent_light" android:state_checked="true" />
+ <item android:drawable="@color/dialog_filter_inactive_light" />
+</selector> \ No newline at end of file
diff --git a/core/src/main/res/drawable/ic_filter_close.xml b/core/src/main/res/drawable/ic_filter_close.xml
new file mode 100644
index 000000000..9e0a26905
--- /dev/null
+++ b/core/src/main/res/drawable/ic_filter_close.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:bottom="5dp"
+ android:left="5dp"
+ android:right="5dp"
+ android:top="5dp">
+
+ <shape android:shape="oval">
+ <stroke
+ android:width="4dp"
+ android:color="?attr/filter_dialog_clear" />
+ </shape>
+ </item>
+
+ <!-- x -->
+ <item
+ android:bottom="12dp"
+ android:left="12dp"
+ android:right="12dp"
+ android:top="12dp">
+ <rotate
+ android:fromDegrees="135"
+ android:pivotX="50%"
+ android:pivotY="50%"
+ android:toDegrees="135">
+ <shape android:shape="line">
+ <stroke
+ android:width="4dp"
+ android:color="?attr/filter_dialog_clear" />
+ </shape>
+ </rotate>
+ </item>
+
+ <item
+ android:bottom="12dp"
+ android:left="12dp"
+ android:right="12dp"
+ android:top="12dp">
+ <rotate
+ android:fromDegrees="45"
+ android:pivotX="50%"
+ android:pivotY="50%"
+ android:toDegrees="45">
+ <shape android:shape="line">
+ <stroke
+ android:width="4dp"
+ android:color="?attr/filter_dialog_clear" />
+ </shape>
+
+ </rotate>
+ </item>
+
+</layer-list> \ No newline at end of file
diff --git a/core/src/main/res/drawable/ic_notifications_black.xml b/core/src/main/res/drawable/ic_notifications_black.xml
new file mode 100644
index 000000000..7009a6763
--- /dev/null
+++ b/core/src/main/res/drawable/ic_notifications_black.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.89,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z"/>
+</vector>
diff --git a/core/src/main/res/drawable/ic_notifications_white.xml b/core/src/main/res/drawable/ic_notifications_white.xml
new file mode 100644
index 000000000..10239aadd
--- /dev/null
+++ b/core/src/main/res/drawable/ic_notifications_white.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#FFFFFF"
+ android:viewportHeight="24.0" android:viewportWidth="24.0"
+ android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="#FFFFFFFF" android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.89,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z"/>
+</vector>
diff --git a/core/src/main/res/layout/player_widget.xml b/core/src/main/res/layout/player_widget.xml
index 6e463e9cd..8e38d7f6e 100644
--- a/core/src/main/res/layout/player_widget.xml
+++ b/core/src/main/res/layout/player_widget.xml
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/widget_margin" >
@@ -8,8 +9,8 @@
android:id="@+id/widgetLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:background="#262C31" >
-
+ android:background="#262C31"
+ tools:ignore="UselessParent">
<ImageButton
android:id="@+id/butPlay"
@@ -41,6 +42,7 @@
android:layout_width="@android:dimen/app_icon_size"
android:layout_height="match_parent"
android:src="@mipmap/ic_launcher_round"
+ android:importantForAccessibility="no"
android:layout_margin="12dp" />
<LinearLayout
diff --git a/core/src/main/res/raw/local_feed_default_icon.png b/core/src/main/res/raw/local_feed_default_icon.png
new file mode 100644
index 000000000..c1b24a729
--- /dev/null
+++ b/core/src/main/res/raw/local_feed_default_icon.png
Binary files differ
diff --git a/core/src/main/res/values/arrays.xml b/core/src/main/res/values/arrays.xml
index dc79905cd..13ff092b0 100644
--- a/core/src/main/res/values/arrays.xml
+++ b/core/src/main/res/values/arrays.xml
@@ -245,30 +245,6 @@
<item>exoplayer</item>
</string-array>
- <string-array name="episode_filter_options">
- <item>@string/hide_unplayed_episodes_label</item>
- <item>@string/hide_paused_episodes_label</item>
- <item>@string/hide_played_episodes_label</item>
- <item>@string/hide_queued_episodes_label</item>
- <item>@string/hide_not_queued_episodes_label</item>
- <item>@string/hide_downloaded_episodes_label</item>
- <item>@string/hide_not_downloaded_episodes_label</item>
- <item>@string/hide_has_media_label</item>
- <item>@string/hide_is_favorite_label</item>
- </string-array>
-
- <string-array name="episode_filter_values">
- <item>unplayed</item>
- <item>paused</item>
- <item>played</item>
- <item>queued</item>
- <item>not_queued</item>
- <item>downloaded</item>
- <item>not_downloaded</item>
- <item>has_media</item>
- <item>is_favorite</item>
- </string-array>
-
<!-- sort for podcast screen, not for queue -->
<string-array name="feed_episodes_sort_options">
<item>@string/sort_date_new_old</item>
diff --git a/core/src/main/res/values/attrs.xml b/core/src/main/res/values/attrs.xml
index b89a819f1..a78f837bf 100644
--- a/core/src/main/res/values/attrs.xml
+++ b/core/src/main/res/values/attrs.xml
@@ -58,6 +58,9 @@
<attr name="action_icon_color" format="color"/>
<attr name="scrollbar_thumb" format="reference"/>
<attr name="background_elevated" format="color"/>
+ <attr name="filter_dialog_clear" format="reference"/>
+ <attr name="filter_dialog_button_background" format="reference"/>
+ <attr name="ic_notifications" format="reference"/>
<declare-styleable name="SquareImageView">
<attr name="direction" format="enum">
diff --git a/core/src/main/res/values/colors.xml b/core/src/main/res/values/colors.xml
index 8cb386fcf..d09f53d64 100644
--- a/core/src/main/res/values/colors.xml
+++ b/core/src/main/res/values/colors.xml
@@ -10,6 +10,7 @@
<color name="download_failed_red">#B00020</color>
<color name="image_readability_tint">#80000000</color>
<color name="feed_image_bg">#50000000</color>
+ <color name="feed_text_bg">#ccbfbfbf</color>
<!-- Theme colors -->
<color name="background_light">#FFFFFF</color>
@@ -23,11 +24,15 @@
<color name="accent_light">#0078C2</color>
<color name="accent_dark">#3D8BFF</color>
+ <color name="icon_background_gradient_start">#0ba2ff</color>
+ <color name="icon_background_gradient_end">#0878ff</color>
<color name="master_switch_background_light">#DDDDDD</color>
<color name="master_switch_background_dark">#191919</color>
- <color name="icon_background_gradient_start">#0ba2ff</color>
- <color name="icon_background_gradient_end">#0878ff</color>
-
+ <!-- filter dialog -->
+ <color name="dialog_filter_clear_inactive_light">#666666</color>
+ <color name="dialog_filter_clear_inactive_dark">#bbbbbb</color>
+ <color name="dialog_filter_inactive_light">#eeeeee</color>
+ <color name="dialog_filter_inactive_dark">#555555</color>
</resources>
diff --git a/core/src/main/res/values/dimens.xml b/core/src/main/res/values/dimens.xml
index 41a24f6fa..4702a5302 100644
--- a/core/src/main/res/values/dimens.xml
+++ b/core/src/main/res/values/dimens.xml
@@ -38,4 +38,6 @@
<dimen name="media_router_controller_playback_control_start_padding">24dp</dimen>
<dimen name="media_router_controller_bottom_margin">8dp</dimen>
+ <dimen name="nav_drawer_max_screen_size">480dp</dimen>
+
</resources>
diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml
index b08d97e61..542b90120 100644
--- a/core/src/main/res/values/strings.xml
+++ b/core/src/main/res/values/strings.xml
@@ -30,6 +30,7 @@
<string name="episode_cache_full_message">The episode cache limit has been reached. You can increase the cache size in the Settings.</string>
<string name="playback_statistics_label">Playback</string>
<string name="download_statistics_label">Downloads</string>
+ <string name="notification_pref_fragment">Notifications</string>
<!-- Statistics fragment -->
<string name="total_time_listened_to_podcasts">Total time of podcasts played:</string>
@@ -40,9 +41,10 @@
<string name="statistics_speed_not_counted">Notice: Playback speed is never taken into account.</string>
<string name="statistics_reset_data">Reset statistics data</string>
<string name="statistics_reset_data_msg">This will erase the history of duration played for all episodes. Are you sure you want to proceed?</string>
+ <string name="statistics_counting_since">Since %s,\nyou played</string>
<!-- Download Statistics fragment -->
- <string name="total_size_downloaded_podcasts">Total size of downloaded podcasts:</string>
+ <string name="total_size_downloaded_podcasts">Total size of episodes on the device:</string>
<!-- Main activity -->
<string name="drawer_open">Open menu</string>
@@ -58,6 +60,9 @@
<string name="drawer_feed_counter_downloaded">Number of downloaded episodes</string>
<string name="drawer_feed_counter_none">None</string>
+ <!-- Bug report activity -->
+ <string name="log_file_share_exception">No compatible apps found</string>
+
<!-- Webview actions -->
<string name="open_in_browser_label">Open in Browser</string>
<string name="copy_url_label">Copy URL</string>
@@ -136,29 +141,26 @@
<string name="feed_settings_label">Podcast settings</string>
<string name="rename_feed_label">Rename podcast</string>
<string name="remove_feed_label">Remove podcast</string>
- <string name="share_label">Share&#8230;</string>
+ <string name="share_label">Share</string>
+ <string name="share_label_with_ellipses">Share…</string>
<string name="share_link_label">Share Episode URL</string>
<string name="share_link_with_position_label">Share Episode URL with Position</string>
<string name="share_file_label">Share File</string>
- <string name="share_website_url_label">Share Website URL</string>
- <string name="share_feed_url_label">Share Podcast URL</string>
+ <string name="share_website_url_label">Website address</string>
+ <string name="share_feed_url_label">Podcast feed URL</string>
<string name="share_item_url_label">Share Media File URL</string>
<string name="share_item_url_with_position_label">Share Media File URL with Position</string>
<string name="feed_delete_confirmation_msg">Please confirm that you want to delete the podcast \"%1$s\" and ALL its episodes (including downloaded episodes).</string>
+ <string name="feed_delete_confirmation_local_msg">Please confirm that you want to remove the podcast \"%1$s\". The files in the local source folder will not be deleted.</string>
<string name="feed_remover_msg">Removing podcast</string>
<string name="load_complete_feed">Refresh complete podcast</string>
<string name="multi_select">Multi select</string>
<string name="select_all_above">Select all above</string>
<string name="select_all_below">Select all below</string>
<string name="hide_unplayed_episodes_label">Unplayed</string>
- <string name="hide_paused_episodes_label">Paused</string>
- <string name="hide_played_episodes_label">Played</string>
<string name="hide_queued_episodes_label">Queued</string>
<string name="hide_not_queued_episodes_label">Not queued</string>
- <string name="hide_downloaded_episodes_label">Downloaded</string>
- <string name="hide_not_downloaded_episodes_label">Not downloaded</string>
<string name="hide_has_media_label">Has media</string>
- <string name="hide_is_favorite_label">Is favorite</string>
<string name="filtered_label">Filtered</string>
<string name="refresh_failed_msg">{fa-exclamation-circle} Last Refresh failed</string>
<string name="open_podcast">Open Podcast</string>
@@ -252,7 +254,10 @@
</plurals>
<string name="downloads_processing">Processing downloads</string>
<string name="download_notification_title">Downloading podcast data</string>
- <string name="download_report_content">%1$d downloads succeeded, %2$d failed</string>
+ <plurals name="download_report_content">
+ <item quantity="one">%d download succeeded, %d failed</item>
+ <item quantity="other">%d downloads succeeded, %d failed</item>
+ </plurals>
<string name="download_log_title_unknown">Unknown Title</string>
<string name="download_type_feed">Feed</string>
<string name="download_type_media">Media file</string>
@@ -265,7 +270,8 @@
<string name="confirm_mobile_download_dialog_message">Downloading over mobile data connection is disabled in the settings.\n\nDo you want to allow downloading temporarily?\n\n<small>Your choice will be remembered for 10 minutes.</small></string>
<string name="confirm_mobile_streaming_notification_title">Confirm Mobile streaming</string>
<string name="confirm_mobile_streaming_notification_message">Streaming over mobile data connection is disabled in the settings. Tap to stream anyway.</string>
- <string name="confirm_mobile_streaming_button_always">Always allow</string>
+ <string name="confirm_mobile_streaming_button_always">Always</string>
+ <string name="confirm_mobile_streaming_button_once">Once</string>
<string name="confirm_mobile_download_dialog_only_add_to_queue">Enqueue</string>
<string name="confirm_mobile_download_dialog_enable_temporarily">Allow temporarily</string>
@@ -315,8 +321,9 @@
<string name="download_plugin_label">Download Plugin</string>
<string name="no_playback_plugin_title">Plugin Not Installed</string>
<string name="no_playback_plugin_or_sonic_msg">For variable speed playback to work, we recommend to enable the built-in Sonic mediaplayer.</string>
- <string name="set_playback_speed_label">Playback Speeds</string>
<string name="enable_sonic">Enable Sonic</string>
+ <string name="speed_presets">Presets</string>
+ <string name="preset_already_exists">%1$.2fx is already saved as a preset.</string>
<!-- Empty list labels -->
<string name="no_items_header_label">No queued episodes</string>
@@ -391,7 +398,7 @@
<string name="pref_autoUpdateIntervallOrTime_every">every %1$s</string>
<string name="pref_autoUpdateIntervallOrTime_at">at %1$s</string>
<string name="pref_followQueue_title">Continuous Playback</string>
- <string name="pref_pauseOnHeadsetDisconnect_title">Headphones Disconnect</string>
+ <string name="pref_pauseOnHeadsetDisconnect_title">Headphones or Bluetooth disconnect</string>
<string name="pref_unpauseOnHeadsetReconnect_title">Headphones Reconnect</string>
<string name="pref_unpauseOnBluetoothReconnect_title">Bluetooth Reconnect</string>
<string name="pref_stream_over_download_title">Prefer Streaming</string>
@@ -404,7 +411,7 @@
<string name="pref_mobileUpdate_episode_download">Episode download</string>
<string name="pref_mobileUpdate_streaming">Streaming</string>
<string name="user_interface_label">User Interface</string>
- <string name="user_interface_sum">Appearance, Subscription order, Lockscreen</string>
+ <string name="user_interface_sum">Appearance, Subscriptions, Lockscreen</string>
<string name="pref_set_theme_title">Select Theme</string>
<string name="pref_nav_drawer_items_title">Set Navigation Drawer items</string>
<string name="pref_nav_drawer_items_sum">Change which items appear in the navigation drawer.</string>
@@ -443,10 +450,9 @@
<string name="pref_gpodnet_full_sync_title">Force full synchronization</string>
<string name="pref_gpodnet_full_sync_sum">Sync all subscriptions and episode states with gpodder.net.</string>
<string name="pref_gpodnet_login_status"><![CDATA[Logged in as <i>%1$s</i> with device <i>%2$s</i>]]></string>
- <string name="pref_gpodnet_notifications_title">Show sync error notifications</string>
+ <string name="pref_gpodnet_notifications_title">Synchronization failed</string>
<string name="pref_gpodnet_notifications_sum">This setting does not apply to authentication errors.</string>
- <string name="pref_playback_speed_title">Playback Speeds</string>
- <string name="pref_playback_speed_sum">Customize the speeds available for variable speed audio playback</string>
+ <string name="pref_playback_speed_sum">Customize the speeds available for variable speed playback</string>
<string name="pref_feed_playback_speed_sum">The speed to use when starting audio playback for episodes in this podcast</string>
<string name="pref_feed_skip">Auto Skip</string>
<string name="pref_feed_skip_sum">Skip introductions and ending credits.</string>
@@ -466,15 +472,15 @@
<string name="pref_expandNotify_sum">This usually expands the notification to show playback buttons.</string>
<string name="pref_persistNotify_title">Persistent Playback Controls</string>
<string name="pref_persistNotify_sum">Keep notification and lockscreen controls when playback is paused.</string>
- <string name="pref_compact_notification_buttons_title">Set Lockscreen Buttons</string>
- <string name="pref_compact_notification_buttons_sum">Change the playback buttons on the lockscreen. The play/pause button is always included.</string>
+ <string name="pref_compact_notification_buttons_title">Set Compact Notification Buttons</string>
+ <string name="pref_compact_notification_buttons_sum">Change the playback buttons when the notification is collapsed. The play/pause button is always included.</string>
<string name="pref_compact_notification_buttons_dialog_title">Select a maximum of %1$d items</string>
<string name="pref_compact_notification_buttons_dialog_error">You can only select a maximum of %1$d items.</string>
<string name="pref_lockscreen_background_title">Set Lockscreen Background</string>
<string name="pref_lockscreen_background_sum">Set the lockscreen background to the current episode\'s image. As a side effect, this will also show the image in third party apps.</string>
- <string name="pref_showDownloadReport_title">Show Download Report</string>
+ <string name="pref_showDownloadReport_title">Download failed</string>
<string name="pref_showDownloadReport_sum">If downloads fail, generate a report that shows the details of the failure.</string>
- <string name="pref_showAutoDownloadReport_title">Show Auto Download Report</string>
+ <string name="pref_showAutoDownloadReport_title">Automatic download completed</string>
<string name="pref_showAutoDownloadReport_sum">Show a notification for automatically downloaded episodes.</string>
<string name="pref_expand_notify_unsupport_toast">Android versions before 4.1 do not support expanded notifications.</string>
<string name="pref_enqueue_location_title">Enqueue Location</string>
@@ -526,6 +532,15 @@
<string name="back_button_go_to_page_title">Select page</string>
<string name="pref_delete_removes_from_queue_title">Delete removes from Queue</string>
<string name="pref_delete_removes_from_queue_sum">Automatically remove an episode from the queue when it is deleted.</string>
+ <string name="pref_filter_feed_title">Subscription Filter</string>
+ <string name="pref_filter_feed_sum">Filter your subscriptions in navigation drawer and subscriptions screen.</string>
+ <string name="no_filter_label">None</string>
+ <string name="subscriptions_are_filtered">Subscriptions are filtered.</string>
+ <string name="subscriptions_counter_greater_zero">Counter greater zero</string>
+ <string name="auto_downloaded">Auto downloaded</string>
+ <string name="not_auto_downloaded">Not auto downloaded</string>
+ <string name="kept_updated">Kept updated</string>
+ <string name="not_kept_updated">Not kept updated</string>
<!-- About screen -->
<string name="about_pref">About</string>
@@ -564,6 +579,7 @@
<string name="database_export_summary">Transfer subscriptions, listened episodes and queue to AntennaPod on another device</string>
<string name="database_import_summary">Import AntennaPod database from another device</string>
<string name="opml_import_label">OPML Import</string>
+ <string name="opml_add_podcast_label">Import podcast list (OPML)</string>
<string name="opml_reader_error">An error has occurred while reading the OPML document:</string>
<string name="opml_import_error_no_file">No file selected!</string>
<string name="select_all_label">Select all</string>
@@ -579,7 +595,8 @@
<string name="export_success_sum">The exported file was written to:\n\n%1$s</string>
<string name="opml_import_ask_read_permission">Access to external storage is required to read the OPML file</string>
<string name="import_select_file">Select file to import</string>
- <string name="import_ok">Import successful.\n\nPlease press OK to restart AntennaPod</string>
+ <string name="successful_import_label">Import successful</string>
+ <string name="import_ok">Please press OK to restart AntennaPod</string>
<string name="import_no_downgrade">This database was exported with a newer version of AntennaPod. Your current installation does not yet know how to handle this file.</string>
<string name="favorites_export_label">Favorites export</string>
<string name="favorites_export_summary">Export saved favorites to file</string>
@@ -681,7 +698,6 @@
<string name="decrease_speed">Decrease speed</string>
<string name="media_type_audio_label">Audio</string>
<string name="media_type_video_label">Video</string>
- <string name="navigate_upwards_label">Navigate upwards</string>
<string name="status_downloading_label">Episode is being downloaded</string>
<string name="in_queue_label">Episode is in the queue</string>
<string name="is_favorite_label">Episode is marked as favorite</string>
@@ -713,21 +729,30 @@
<!-- Add podcast fragment -->
<string name="search_podcast_hint">Search podcast…</string>
<string name="search_itunes_label">Search iTunes</string>
+ <string name="search_podcastindex_label">Search Podcastindex.org</string>
<string name="search_fyyd_label">Search fyyd</string>
<string name="advanced">Advanced</string>
- <string name="add_podcast_by_url">Add Podcast by URL</string>
+ <string name="add_podcast_by_url">Add Podcast by RSS address</string>
<string name="add_podcast_by_url_hint" translatable="false">www.example.com/feed</string>
<string name="browse_gpoddernet_label">Browse gpodder.net</string>
<string name="discover">Discover</string>
+ <string name="discover_hide">Hide</string>
+ <string name="discover_is_hidden">You selected to hide suggestions.</string>
<string name="discover_more">more »</string>
- <string name="search_powered_by">Search powered by %1$s</string>
+ <string name="discover_powered_by_itunes">Suggestions by iTunes</string>
+ <string name="search_powered_by">Results by %1$s</string>
+ <string name="add_local_folder">Add local folder</string>
+ <string name="add_local_folder_success">Adding local folder succeeded</string>
+ <string name="reconnect_local_folder">Re-connect local folder</string>
+ <string name="reconnect_local_folder_warning">In case of permission denials, you can use this to re-connect to the exact same folder. Do not select another folder.</string>
+ <string name="local_feed_description">This virtual podcast was created by adding a folder to AntennaPod.</string>
<string name="filter">Filter</string>
-
+
<!-- Episodes apply actions -->
<string name="all_label">All</string>
<string name="selected_all_label">Selected all Episodes</string>
- <string name="none_label">None</string>
+ <string name="select_none_label">None</string>
<string name="deselected_all_label">Deselected all Episodes</string>
<string name="played_label">Played</string>
<string name="selected_played_label">Selected played Episodes</string>
@@ -737,13 +762,23 @@
<string name="selected_downloaded_label">Selected downloaded Episodes</string>
<string name="not_downloaded_label">Not downloaded</string>
<string name="selected_not_downloaded_label">Selected not downloaded Episodes</string>
- <string name="queued_label">Queued</string>
<string name="selected_queued_label">Selected queued Episodes</string>
- <string name="not_queued_label">Not queued</string>
<string name="selected_not_queued_label">Selected not queued Episodes</string>
- <string name="has_media">Has media</string>
<string name="selected_has_media_label">Selected episodes with media</string>
+ <string name="hide_is_favorite_label">Is favorite</string>
+ <string name="not_favorite">Not favorite</string>
+ <string name="hide_downloaded_episodes_label">Downloaded</string>
+ <string name="hide_not_downloaded_episodes_label">Not downloaded</string>
+ <string name="queued_label">Queued</string>
+ <string name="not_queued_label">Not queued</string>
+ <string name="has_media">Has media</string>
+ <string name="no_media">No media</string>
+ <string name="hide_paused_episodes_label">Paused</string>
+ <string name="not_paused">Not paused</string>
+ <string name="hide_played_episodes_label">Played</string>
+ <string name="not_played">Not played</string>
+
<!-- Sort -->
<string name="sort_title_a_z">Title (A \u2192 Z)</string>
<string name="sort_title_z_a">Title (Z \u2192 A)</string>
@@ -766,6 +801,12 @@
<string name="rating_later_label">Remind me later</string>
<string name="rating_now_label">Sure, let\'s do this!</string>
+ <!-- Share episode dialog -->
+ <string name="share_dialog_include_label">Include:</string>
+ <string name="share_playback_position_dialog_label">Playback position</string>
+ <string name="share_dialog_media_file_label">Media file URL</string>
+ <string name="share_dialog_episode_website_label">Episode webpage</string>
+
<!-- Audio controls -->
<string name="audio_controls">Audio controls</string>
<string name="playback_speed">Playback Speed</string>
@@ -816,7 +857,9 @@
<string name="notification_channel_playing">Currently playing</string>
<string name="notification_channel_playing_description">Allows to control playback. This is the main notification you see while playing a podcast.</string>
<string name="notification_channel_error">Errors</string>
- <string name="notification_channel_error_description">Shown if something went wrong, for example if download or gpodder sync fails.</string>
+ <string name="notification_channel_error_description">Shown if something went wrong, for example if download or feed update fails.</string>
+ <string name="notification_channel_sync_error">Synchronization Errors</string>
+ <string name="notification_channel_sync_error_description">Shown when gpodder synchronization fails.</string>
<string name="notification_channel_auto_download">Auto Downloads</string>
<string name="notification_channel_episode_auto_download">Shown when episodes have been automatically downloaded.</string>
diff --git a/core/src/main/res/values/styles.xml b/core/src/main/res/values/styles.xml
index b7e482a91..9ec82215a 100644
--- a/core/src/main/res/values/styles.xml
+++ b/core/src/main/res/values/styles.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<resources xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android">
+<resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.AntennaPod.Light" parent="Theme.Base.AntennaPod.Light">
<!-- Room for API dependent attributes -->
@@ -73,6 +73,9 @@
<item name="ic_key">@drawable/ic_key_black</item>
<item name="ic_volume_adaption">@drawable/ic_volume_adaption_black</item>
<item name="scrollbar_thumb">@drawable/scrollbar_thumb_light</item>
+ <item name="filter_dialog_clear">@color/filter_dialog_clear_light</item>
+ <item name="filter_dialog_button_background">@drawable/filter_dialog_background_light</item>
+ <item name="ic_notifications">@drawable/ic_notifications_black</item>
</style>
<style name="Theme.AntennaPod.Dark" parent="Theme.Base.AntennaPod.Dark">
@@ -148,6 +151,9 @@
<item name="ic_key">@drawable/ic_key_white</item>
<item name="ic_volume_adaption">@drawable/ic_volume_adaption_white</item>
<item name="scrollbar_thumb">@drawable/scrollbar_thumb_dark</item>
+ <item name="filter_dialog_clear">@color/filter_dialog_clear_dark</item>
+ <item name="filter_dialog_button_background">@drawable/filter_dialog_background_dark</item>
+ <item name="ic_notifications">@drawable/ic_notifications_white</item>
</style>
<style name="Theme.AntennaPod.TrueBlack" parent="Theme.Base.AntennaPod.TrueBlack">
@@ -302,4 +308,14 @@
<style name="Widget.AntennaPod.ActionBar.Black" parent="Widget.MaterialComponents.Light.ActionBar.Solid">
<item name="background">@color/black</item>
</style>
+
+ <style name="AddPodcastTextView">
+ <item name="android:drawablePadding">8dp</item>
+ <item name="android:paddingTop">8dp</item>
+ <item name="android:paddingBottom">8dp</item>
+ <item name="android:background">?android:attr/selectableItemBackground</item>
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
+ <item name="android:clickable">true</item>
+ </style>
+
</resources>