diff options
Diffstat (limited to 'core/src/main/java/de')
68 files changed, 1498 insertions, 2055 deletions
diff --git a/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java b/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java new file mode 100644 index 000000000..ac67fb042 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java @@ -0,0 +1,50 @@ +package de.danoeh.antennapod.core; + +import android.content.Context; + +import de.danoeh.antennapod.net.ssl.SslProviderInstaller; + +import de.danoeh.antennapod.core.preferences.PlaybackPreferences; +import de.danoeh.antennapod.core.preferences.SleepTimerPreferences; +import de.danoeh.antennapod.core.preferences.UsageStatistics; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; +import de.danoeh.antennapod.core.storage.PodDBAdapter; +import de.danoeh.antennapod.core.util.NetworkUtils; +import de.danoeh.antennapod.core.util.gui.NotificationUtils; + +import java.io.File; + +/** + * Stores callbacks for core classes like Services, DB classes etc. and other configuration variables. + * Apps using the core module of AntennaPod should register implementations of all interfaces here. + */ +public class ClientConfig { + + /** + * Should be used when setting User-Agent header for HTTP-requests. + */ + public static String USER_AGENT; + + public static ApplicationCallbacks applicationCallbacks; + + public static DownloadServiceCallbacks downloadServiceCallbacks; + + private static boolean initialized = false; + + public static synchronized void initialize(Context context) { + if (initialized) { + return; + } + PodDBAdapter.init(context); + UserPreferences.init(context); + UsageStatistics.init(context); + PlaybackPreferences.init(context); + SslProviderInstaller.install(context); + NetworkUtils.init(context); + AntennapodHttpClient.setCacheDirectory(new File(context.getCacheDir(), "okhttp")); + SleepTimerPreferences.init(context); + NotificationUtils.createChannels(context); + initialized = true; + } +} 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 deleted file mode 100644 index f7757935a..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/event/DiscoveryDefaultUpdateEvent.java +++ /dev/null @@ -1,6 +0,0 @@ -package de.danoeh.antennapod.core.event; - -public class DiscoveryDefaultUpdateEvent { - public DiscoveryDefaultUpdateEvent() { - } -} 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 deleted file mode 100644 index cbfcc37e6..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/event/FavoritesEvent.java +++ /dev/null @@ -1,41 +0,0 @@ -package de.danoeh.antennapod.core.event; - -import androidx.annotation.NonNull; - -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; - -import de.danoeh.antennapod.model.feed.FeedItem; - -public class FavoritesEvent { - - public enum Action { - ADDED, REMOVED - } - - private final Action action; - private final FeedItem item; - - private FavoritesEvent(Action action, FeedItem item) { - this.action = action; - this.item = item; - } - - public static FavoritesEvent added(FeedItem item) { - return new FavoritesEvent(Action.ADDED, item); - } - - public static FavoritesEvent removed(FeedItem item) { - return new FavoritesEvent(Action.REMOVED, item); - } - - @NonNull - @Override - public String toString() { - return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) - .append("action", action) - .append("item", item) - .toString(); - } - -} 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 deleted file mode 100644 index 99cb01714..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/event/FeedItemEvent.java +++ /dev/null @@ -1,54 +0,0 @@ -package de.danoeh.antennapod.core.event; - - -import androidx.annotation.NonNull; - -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; - -import java.util.Arrays; -import java.util.List; - -import de.danoeh.antennapod.model.feed.FeedItem; - -public class FeedItemEvent { - - public enum Action { - UPDATE, DELETE_MEDIA - } - - @NonNull - private final Action action; - @NonNull public final List<FeedItem> items; - - private FeedItemEvent(@NonNull Action action, @NonNull List<FeedItem> items) { - this.action = action; - this.items = items; - } - - public static FeedItemEvent deletedMedia(List<FeedItem> items) { - return new FeedItemEvent(Action.DELETE_MEDIA, items); - } - - public static FeedItemEvent deletedMedia(FeedItem... items) { - return deletedMedia(Arrays.asList(items)); - } - - public static FeedItemEvent updated(List<FeedItem> items) { - return new FeedItemEvent(Action.UPDATE, items); - } - - public static FeedItemEvent updated(FeedItem... items) { - return updated(Arrays.asList(items)); - } - - @NonNull - @Override - public String toString() { - return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) - .append("action", action) - .append("items", items) - .toString(); - } - -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/FeedListUpdateEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/FeedListUpdateEvent.java deleted file mode 100644 index 4ed8e33ec..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/event/FeedListUpdateEvent.java +++ /dev/null @@ -1,28 +0,0 @@ -package de.danoeh.antennapod.core.event; - -import de.danoeh.antennapod.model.feed.Feed; - -import java.util.ArrayList; -import java.util.List; - -public class FeedListUpdateEvent { - private final List<Long> feeds = new ArrayList<>(); - - public FeedListUpdateEvent(List<Feed> feeds) { - for (Feed feed : feeds) { - this.feeds.add(feed.getId()); - } - } - - public FeedListUpdateEvent(Feed feed) { - feeds.add(feed.getId()); - } - - public FeedListUpdateEvent(long feedId) { - feeds.add(feedId); - } - - public boolean contains(Feed feed) { - return feeds.contains(feed.getId()); - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/MessageEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/MessageEvent.java deleted file mode 100644 index 9fb22b8ea..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/event/MessageEvent.java +++ /dev/null @@ -1,21 +0,0 @@ -package de.danoeh.antennapod.core.event; - -import androidx.annotation.Nullable; - -public class MessageEvent { - - public final String message; - - @Nullable - public final Runnable action; - - public MessageEvent(String message) { - this(message, null); - } - - public MessageEvent(String message, Runnable action) { - this.message = message; - this.action = action; - } - -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/PlaybackHistoryEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/PlaybackHistoryEvent.java deleted file mode 100644 index cd3f27bf5..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/event/PlaybackHistoryEvent.java +++ /dev/null @@ -1,16 +0,0 @@ -package de.danoeh.antennapod.core.event; - -public class PlaybackHistoryEvent { - - private PlaybackHistoryEvent() { - } - - public static PlaybackHistoryEvent listUpdated() { - return new PlaybackHistoryEvent(); - } - - @Override - public String toString() { - return "PlaybackHistoryEvent"; - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/PlaybackPositionEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/PlaybackPositionEvent.java deleted file mode 100644 index 3327d8a02..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/event/PlaybackPositionEvent.java +++ /dev/null @@ -1,19 +0,0 @@ -package de.danoeh.antennapod.core.event; - -public class PlaybackPositionEvent { - private final int position; - private final int duration; - - public PlaybackPositionEvent(int position, int duration) { - this.position = position; - this.duration = duration; - } - - public int getPosition() { - return position; - } - - public int getDuration() { - return duration; - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/PlayerStatusEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/PlayerStatusEvent.java deleted file mode 100644 index fe7f17968..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/event/PlayerStatusEvent.java +++ /dev/null @@ -1,6 +0,0 @@ -package de.danoeh.antennapod.core.event; - -public class PlayerStatusEvent { - public PlayerStatusEvent() { - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/QueueEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/QueueEvent.java deleted file mode 100644 index c866939bd..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/event/QueueEvent.java +++ /dev/null @@ -1,71 +0,0 @@ -package de.danoeh.antennapod.core.event; - -import androidx.annotation.Nullable; - -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; - -import java.util.List; - -import de.danoeh.antennapod.model.feed.FeedItem; - -public class QueueEvent { - - public enum Action { - ADDED, ADDED_ITEMS, SET_QUEUE, REMOVED, IRREVERSIBLE_REMOVED, CLEARED, DELETED_MEDIA, SORTED, MOVED - } - - public final Action action; - public final FeedItem item; - public final int position; - public final List<FeedItem> items; - - - private QueueEvent(Action action, - @Nullable FeedItem item, - @Nullable List<FeedItem> items, - int position) { - this.action = action; - this.item = item; - this.items = items; - this.position = position; - } - - public static QueueEvent added(FeedItem item, int position) { - return new QueueEvent(Action.ADDED, item, null, position); - } - - public static QueueEvent setQueue(List<FeedItem> queue) { - return new QueueEvent(Action.SET_QUEUE, null, queue, -1); - } - - public static QueueEvent removed(FeedItem item) { - return new QueueEvent(Action.REMOVED, item, null, -1); - } - - public static QueueEvent irreversibleRemoved(FeedItem item) { - return new QueueEvent(Action.IRREVERSIBLE_REMOVED, item, null, -1); - } - - public static QueueEvent cleared() { - return new QueueEvent(Action.CLEARED, null, null, -1); - } - - public static QueueEvent sorted(List<FeedItem> sortedQueue) { - return new QueueEvent(Action.SORTED, null, sortedQueue, -1); - } - - public static QueueEvent moved(FeedItem item, int newPosition) { - return new QueueEvent(Action.MOVED, item, null, newPosition); - } - - @Override - public String toString() { - return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) - .append("action", action) - .append("item", item) - .append("items", items) - .append("position", position) - .toString(); - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/ServiceEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/ServiceEvent.java deleted file mode 100644 index 2230ee84f..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/event/ServiceEvent.java +++ /dev/null @@ -1,14 +0,0 @@ -package de.danoeh.antennapod.core.event; - -public class ServiceEvent { - public enum Action { - SERVICE_STARTED, - SERVICE_SHUT_DOWN - } - - public final Action action; - - public ServiceEvent(Action action) { - this.action = action; - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/SyncServiceEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/SyncServiceEvent.java deleted file mode 100644 index 7aa5f6bf1..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/event/SyncServiceEvent.java +++ /dev/null @@ -1,13 +0,0 @@ -package de.danoeh.antennapod.core.event; - -public class SyncServiceEvent { - private final int messageResId; - - public SyncServiceEvent(int messageResId) { - this.messageResId = messageResId; - } - - public int getMessageResId() { - return messageResId; - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/UnreadItemsUpdateEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/UnreadItemsUpdateEvent.java deleted file mode 100644 index c3efbfe8b..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/event/UnreadItemsUpdateEvent.java +++ /dev/null @@ -1,6 +0,0 @@ -package de.danoeh.antennapod.core.event; - -public class UnreadItemsUpdateEvent { - public UnreadItemsUpdateEvent() { - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/settings/SkipIntroEndingChangedEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/settings/SkipIntroEndingChangedEvent.java deleted file mode 100644 index 583f7b13f..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/event/settings/SkipIntroEndingChangedEvent.java +++ /dev/null @@ -1,25 +0,0 @@ -package de.danoeh.antennapod.core.event.settings; - -public class SkipIntroEndingChangedEvent { - private final int skipIntro; - private final int skipEnding; - private final long feedId; - - public SkipIntroEndingChangedEvent(int skipIntro, int skipEnding, long feedId) { - this.skipIntro= skipIntro; - this.skipEnding = skipEnding; - this.feedId = feedId; - } - - public int getSkipIntro() { - return skipIntro; - } - - public int getSkipEnding() { - return skipEnding; - } - - public long getFeedId() { - return feedId; - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/settings/SpeedPresetChangedEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/settings/SpeedPresetChangedEvent.java deleted file mode 100644 index 0ac7e1316..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/event/settings/SpeedPresetChangedEvent.java +++ /dev/null @@ -1,19 +0,0 @@ -package de.danoeh.antennapod.core.event.settings; - -public class SpeedPresetChangedEvent { - private final float speed; - private final long feedId; - - public SpeedPresetChangedEvent(float speed, long feedId) { - this.speed = speed; - this.feedId = feedId; - } - - public float getSpeed() { - return speed; - } - - public long getFeedId() { - return feedId; - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/settings/VolumeAdaptionChangedEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/settings/VolumeAdaptionChangedEvent.java deleted file mode 100644 index 3905ce68f..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/event/settings/VolumeAdaptionChangedEvent.java +++ /dev/null @@ -1,21 +0,0 @@ -package de.danoeh.antennapod.core.event.settings; - -import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting; - -public class VolumeAdaptionChangedEvent { - private final VolumeAdaptionSetting volumeAdaptionSetting; - private final long feedId; - - public VolumeAdaptionChangedEvent(VolumeAdaptionSetting volumeAdaptionSetting, long feedId) { - this.volumeAdaptionSetting = volumeAdaptionSetting; - this.feedId = feedId; - } - - public VolumeAdaptionSetting getVolumeAdaptionSetting() { - return volumeAdaptionSetting; - } - - public long getFeedId() { - return feedId; - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/export/favorites/FavoritesWriter.java b/core/src/main/java/de/danoeh/antennapod/core/export/favorites/FavoritesWriter.java index 9bc273c9e..29de6ca80 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/export/favorites/FavoritesWriter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/export/favorites/FavoritesWriter.java @@ -117,10 +117,17 @@ public class FavoritesWriter implements ExportWriter { } private void writeFavoriteItem(Writer writer, FeedItem item, String favoriteTemplate) throws IOException { - String favItem = favoriteTemplate - .replace("{FAV_TITLE}", item.getTitle().trim()) - .replace("{FAV_WEBSITE}", item.getLink()) - .replace("{FAV_MEDIA}", item.getMedia().getDownload_url()); + String favItem = favoriteTemplate.replace("{FAV_TITLE}", item.getTitle().trim()); + if (item.getLink() != null) { + favItem = favItem.replace("{FAV_WEBSITE}", item.getLink()); + } else { + favItem = favItem.replace("{FAV_WEBSITE}", ""); + } + if (item.getMedia() != null && item.getMedia().getDownload_url() != null) { + favItem = favItem.replace("{FAV_MEDIA}", item.getMedia().getDownload_url()); + } else { + favItem = favItem.replace("{FAV_MEDIA}", ""); + } writer.append(favItem); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java index 82583b7b5..5d685c24f 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java @@ -178,7 +178,7 @@ public class LocalFeedUpdater { private static FeedItem createFeedItem(Feed feed, DocumentFile file, Context context) { FeedItem item = new FeedItem(0, file.getName(), UUID.randomUUID().toString(), file.getName(), new Date(file.lastModified()), FeedItem.UNPLAYED, feed); - item.setAutoDownload(false); + item.disableAutoDownload(); long size = file.length(); FeedMedia media = new FeedMedia(0, item, 0, 0, size, file.getType(), 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 defe6c9f8..797addcc1 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 @@ -2,6 +2,7 @@ package de.danoeh.antennapod.core.glide; import android.annotation.SuppressLint; import android.content.Context; +import android.graphics.Bitmap; import android.util.Log; import androidx.annotation.NonNull; @@ -11,7 +12,6 @@ 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.module.AppGlideModule; import de.danoeh.antennapod.model.feed.EmbeddedChapterImage; @@ -43,8 +43,9 @@ public class ApGlideModule extends AppGlideModule { public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { 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(String.class, InputStream.class, new NoHttpStringLoader.StreamFactory()); registry.append(EmbeddedChapterImage.class, ByteBuffer.class, new ChapterImageModelLoader.Factory()); + registry.register(Bitmap.class, PaletteBitmap.class, new PaletteBitmapTranscoder()); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/NoHttpStringLoader.java b/core/src/main/java/de/danoeh/antennapod/core/glide/NoHttpStringLoader.java new file mode 100644 index 000000000..9cda3b1aa --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/glide/NoHttpStringLoader.java @@ -0,0 +1,39 @@ +package de.danoeh.antennapod.core.glide; + +import android.net.Uri; +import androidx.annotation.NonNull; +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.load.model.StringLoader; + +import java.io.InputStream; + +/** + * StringLoader that does not handle http/https urls. Used to avoid fallback to StringLoader when + * AntennaPod blocks mobile image loading. + */ +public final class NoHttpStringLoader extends StringLoader<InputStream> { + + public static class StreamFactory implements ModelLoaderFactory<String, InputStream> { + @NonNull + @Override + public ModelLoader<String, InputStream> build(@NonNull MultiModelLoaderFactory multiFactory) { + return new NoHttpStringLoader(multiFactory.build(Uri.class, InputStream.class)); + } + + @Override + public void teardown() { + // Do nothing. + } + } + + public NoHttpStringLoader(ModelLoader<Uri, InputStream> uriLoader) { + super(uriLoader); + } + + @Override + public boolean handles(@NonNull String model) { + return !model.startsWith("http") && super.handles(model); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/PaletteBitmap.java b/core/src/main/java/de/danoeh/antennapod/core/glide/PaletteBitmap.java new file mode 100644 index 000000000..59ecd3d0d --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/glide/PaletteBitmap.java @@ -0,0 +1,20 @@ +/* + * Source: https://github.com/bumptech/glide/wiki/Custom-targets#palette-example + */ + +package de.danoeh.antennapod.core.glide; + +import android.graphics.Bitmap; + +import androidx.annotation.NonNull; +import androidx.palette.graphics.Palette; + +public class PaletteBitmap { + public final Palette palette; + public final Bitmap bitmap; + + public PaletteBitmap(@NonNull Bitmap bitmap, Palette palette) { + this.bitmap = bitmap; + this.palette = palette; + } +}
\ No newline at end of file diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/PaletteBitmapResource.java b/core/src/main/java/de/danoeh/antennapod/core/glide/PaletteBitmapResource.java new file mode 100644 index 000000000..fef0bccd3 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/glide/PaletteBitmapResource.java @@ -0,0 +1,40 @@ +/* + * Source: https://github.com/bumptech/glide/wiki/Custom-targets#palette-example + */ + +package de.danoeh.antennapod.core.glide; + +import androidx.annotation.NonNull; + +import com.bumptech.glide.load.engine.Resource; +import com.bumptech.glide.util.Util; + +public class PaletteBitmapResource implements Resource<PaletteBitmap> { + private final PaletteBitmap paletteBitmap; + + public PaletteBitmapResource(@NonNull PaletteBitmap paletteBitmap) { + this.paletteBitmap = paletteBitmap; + } + + @NonNull + @Override + public Class<PaletteBitmap> getResourceClass() { + return PaletteBitmap.class; + } + + @NonNull + @Override + public PaletteBitmap get() { + return paletteBitmap; + } + + @Override + public int getSize() { + return Util.getBitmapByteSize(paletteBitmap.bitmap); + } + + @Override + public void recycle() { + paletteBitmap.bitmap.recycle(); + } +}
\ No newline at end of file diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/PaletteBitmapTranscoder.java b/core/src/main/java/de/danoeh/antennapod/core/glide/PaletteBitmapTranscoder.java new file mode 100644 index 000000000..a6a606cb8 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/glide/PaletteBitmapTranscoder.java @@ -0,0 +1,32 @@ +/* + * Source: https://github.com/bumptech/glide/wiki/Custom-targets#palette-example + */ + +package de.danoeh.antennapod.core.glide; + +import android.graphics.Bitmap; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.palette.graphics.Palette; + +import com.bumptech.glide.load.Options; +import com.bumptech.glide.load.engine.Resource; +import com.bumptech.glide.load.resource.transcode.ResourceTranscoder; + +import de.danoeh.antennapod.core.preferences.UserPreferences; + +public class PaletteBitmapTranscoder implements ResourceTranscoder<Bitmap, PaletteBitmap> { + + @Nullable + @Override + public Resource<PaletteBitmap> transcode(@NonNull Resource<Bitmap> toTranscode, @NonNull Options options) { + Bitmap bitmap = toTranscode.get(); + Palette palette = null; + if (UserPreferences.shouldShowSubscriptionTitle()) { + palette = new Palette.Builder(bitmap).generate(); + } + PaletteBitmap result = new PaletteBitmap(bitmap, palette); + return new PaletteBitmapResource(result); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/ResizingOkHttpStreamFetcher.java b/core/src/main/java/de/danoeh/antennapod/core/glide/ResizingOkHttpStreamFetcher.java index 11e2f944e..1871723bb 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/glide/ResizingOkHttpStreamFetcher.java +++ b/core/src/main/java/de/danoeh/antennapod/core/glide/ResizingOkHttpStreamFetcher.java @@ -3,12 +3,13 @@ package de.danoeh.antennapod.core.glide; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Build; +import android.util.Log; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.Priority; import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher; import com.bumptech.glide.load.model.GlideUrl; -import com.google.android.exoplayer2.util.Log; import okhttp3.Call; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; @@ -22,7 +23,7 @@ import java.io.InputStream; import java.io.OutputStream; public class ResizingOkHttpStreamFetcher extends OkHttpStreamFetcher { - private static final String TAG = "ResizingOkHttpStreamFetcher"; + private static final String TAG = "ResizingOkHttpStreamFet"; private static final int MAX_DIMENSIONS = 1500; private static final int MAX_FILE_SIZE = 1024 * 1024; // 1 MB diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java deleted file mode 100644 index e338e0d01..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java +++ /dev/null @@ -1,115 +0,0 @@ -package de.danoeh.antennapod.core.preferences; - -import android.content.Context; -import android.content.SharedPreferences; -import android.util.Log; -import de.danoeh.antennapod.core.BuildConfig; -import de.danoeh.antennapod.core.ClientConfig; -import de.danoeh.antennapod.core.sync.SyncService; -import de.danoeh.antennapod.net.sync.gpoddernet.GpodnetService; - -/** - * Manages preferences for accessing gpodder.net service - */ -public class GpodnetPreferences { - - private GpodnetPreferences(){} - - private static final String TAG = "GpodnetPreferences"; - - private static final String PREF_NAME = "gpodder.net"; - private static final String PREF_GPODNET_USERNAME = "de.danoeh.antennapod.preferences.gpoddernet.username"; - private static final String PREF_GPODNET_PASSWORD = "de.danoeh.antennapod.preferences.gpoddernet.password"; - private static final String PREF_GPODNET_DEVICEID = "de.danoeh.antennapod.preferences.gpoddernet.deviceID"; - private static final String PREF_GPODNET_HOSTNAME = "prefGpodnetHostname"; - - private static String username; - private static String password; - private static String deviceID; - private static String hosturl; - - private static boolean preferencesLoaded = false; - - private static SharedPreferences getPreferences() { - return ClientConfig.applicationCallbacks.getApplicationInstance().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); - } - - private static synchronized void ensurePreferencesLoaded() { - if (!preferencesLoaded) { - SharedPreferences prefs = getPreferences(); - username = prefs.getString(PREF_GPODNET_USERNAME, null); - password = prefs.getString(PREF_GPODNET_PASSWORD, null); - deviceID = prefs.getString(PREF_GPODNET_DEVICEID, null); - hosturl = prefs.getString(PREF_GPODNET_HOSTNAME, GpodnetService.DEFAULT_BASE_HOST); - - preferencesLoaded = true; - } - } - - private static void writePreference(String key, String value) { - SharedPreferences.Editor editor = getPreferences().edit(); - editor.putString(key, value); - editor.apply(); - } - - public static String getUsername() { - ensurePreferencesLoaded(); - return username; - } - - public static void setUsername(String username) { - GpodnetPreferences.username = username; - writePreference(PREF_GPODNET_USERNAME, username); - } - - public static String getPassword() { - ensurePreferencesLoaded(); - return password; - } - - public static void setPassword(String password) { - GpodnetPreferences.password = password; - writePreference(PREF_GPODNET_PASSWORD, password); - } - - public static String getDeviceID() { - ensurePreferencesLoaded(); - return deviceID; - } - - public static void setDeviceID(String deviceID) { - GpodnetPreferences.deviceID = deviceID; - writePreference(PREF_GPODNET_DEVICEID, deviceID); - } - - public static String getHosturl() { - ensurePreferencesLoaded(); - return hosturl; - } - - public static void setHosturl(String value) { - if (!value.equals(hosturl)) { - logout(); - writePreference(PREF_GPODNET_HOSTNAME, value); - hosturl = value; - } - } - - /** - * Returns true if device ID, username and password have a non-null value - */ - public static boolean loggedIn() { - ensurePreferencesLoaded(); - return deviceID != null && username != null && password != null; - } - - public static synchronized void logout() { - if (BuildConfig.DEBUG) Log.d(TAG, "Logout: Clearing preferences"); - setUsername(null); - setPassword(null); - setDeviceID(null); - SyncService.clearQueue(ClientConfig.applicationCallbacks.getApplicationInstance()); - UserPreferences.setGpodnetNotificationsEnabled(); - } - -} 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 9c73ed9ae..f0c61403f 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 @@ -5,11 +5,11 @@ import android.content.SharedPreferences; import androidx.preference.PreferenceManager; import android.util.Log; -import de.danoeh.antennapod.core.event.PlayerStatusEvent; +import de.danoeh.antennapod.event.PlayerStatusEvent; import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.model.playback.MediaType; -import de.danoeh.antennapod.core.service.playback.PlayerStatus; import de.danoeh.antennapod.model.playback.Playable; +import de.danoeh.antennapod.playback.base.PlayerStatus; import org.greenrobot.eventbus.EventBus; import static de.danoeh.antennapod.model.feed.FeedPreferences.SPEED_USE_GLOBAL; 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 8b36d88a1..08daf01e2 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 @@ -69,6 +69,7 @@ public class UserPreferences { 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_SUBSCRIPTION_TITLE = "prefSubscriptionTitle"; public static final String PREF_QUEUE_KEEP_SORTED = "prefQueueKeepSorted"; public static final String PREF_QUEUE_KEEP_SORTED_ORDER = "prefQueueKeepSortedOrder"; @@ -85,7 +86,7 @@ public class UserPreferences { private static final String PREF_AUTO_DELETE = "prefAutoDelete"; public static final String PREF_SMART_MARK_AS_PLAYED_SECS = "prefSmartMarkAsPlayedSecs"; private static final String PREF_PLAYBACK_SPEED_ARRAY = "prefPlaybackSpeedArray"; - private static final String PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS = "prefPauseForFocusLoss"; + public static final String PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS = "prefPauseForFocusLoss"; private static final String PREF_RESUME_AFTER_CALL = "prefResumeAfterCall"; public static final String PREF_VIDEO_BEHAVIOR = "prefVideoBehavior"; private static final String PREF_TIME_RESPECTS_SPEED = "prefPlaybackTimeRespectsSpeed"; @@ -207,7 +208,7 @@ public class UserPreferences { public static List<Integer> getCompactNotificationButtons() { String[] buttons = TextUtils.split( prefs.getString(PREF_COMPACT_NOTIFICATION_BUTTONS, - String.valueOf(NOTIFICATION_BUTTON_SKIP)), + NOTIFICATION_BUTTON_REWIND + "," + NOTIFICATION_BUTTON_FAST_FORWARD), ","); List<Integer> notificationButtons = new ArrayList<>(); for (String button : buttons) { @@ -467,7 +468,7 @@ public class UserPreferences { } public static boolean shouldPauseForFocusLoss() { - return prefs.getBoolean(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, false); + return prefs.getBoolean(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, true); } @@ -530,7 +531,8 @@ public class UserPreferences { private static void setAllowMobileFor(String type, boolean allow) { HashSet<String> defaultValue = new HashSet<>(); defaultValue.add("images"); - Set<String> allowed = prefs.getStringSet(PREF_MOBILE_UPDATE, defaultValue); + final Set<String> getValueStringSet = prefs.getStringSet(PREF_MOBILE_UPDATE, defaultValue); + final Set<String> allowed = new HashSet<>(getValueStringSet); if (allow) { allowed.add(type); } else { @@ -609,6 +611,11 @@ public class UserPreferences { public static void setProxyConfig(ProxyConfig config) { SharedPreferences.Editor editor = prefs.edit(); editor.putString(PREF_PROXY_TYPE, config.type.name()); + Proxy.Type type = Proxy.Type.valueOf(config.type.name()); + if (type == Proxy.Type.DIRECT) { + editor.apply(); + return; + } if(TextUtils.isEmpty(config.host)) { editor.remove(PREF_PROXY_HOST); } else { @@ -1084,4 +1091,13 @@ public class UserPreferences { public static void unsetUsageCountingDate() { setUsageCountingDateMillis(-1); } + + public static boolean shouldShowSubscriptionTitle() { + return prefs.getBoolean(PREF_SUBSCRIPTION_TITLE, false); + } + + public static void setSubscriptionTitleSetting(boolean showTitle) { + prefs.edit().putBoolean(PREF_SUBSCRIPTION_TITLE, showTitle).apply(); + } + } 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 26ab4a414..6cf6ce107 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 @@ -8,6 +8,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Binder; +import android.os.Build; import android.os.Handler; import android.os.IBinder; import android.os.Looper; @@ -20,7 +21,6 @@ import androidx.annotation.VisibleForTesting; import androidx.core.app.ServiceCompat; import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.core.sync.SyncService; import org.apache.commons.io.FileUtils; import org.greenrobot.eventbus.EventBus; @@ -40,7 +40,8 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import de.danoeh.antennapod.core.event.DownloadEvent; -import de.danoeh.antennapod.core.event.FeedItemEvent; +import de.danoeh.antennapod.core.util.download.ConnectionStateMonitor; +import de.danoeh.antennapod.event.FeedItemEvent; import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedMedia; @@ -121,7 +122,7 @@ public class DownloadService extends Service { private static final int SCHED_EX_POOL_SIZE = 1; private final ScheduledThreadPoolExecutor schedExecutor; private static DownloaderFactory downloaderFactory = new DefaultDownloaderFactory(); - + private ConnectionStateMonitor connectionMonitor; private final IBinder mBinder = new LocalBinder(); private class LocalBinder extends Binder { @@ -192,6 +193,11 @@ public class DownloadService extends Service { cancelDownloadReceiverFilter.addAction(ACTION_CANCEL_DOWNLOAD); registerReceiver(cancelDownloadReceiver, cancelDownloadReceiverFilter); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + connectionMonitor = new ConnectionStateMonitor(); + connectionMonitor.enable(getApplicationContext()); + } + downloadCompletionThread.start(); } @@ -226,10 +232,9 @@ public class DownloadService extends Service { downloadPostFuture.cancel(true); } unregisterReceiver(cancelDownloadReceiver); - - // if this was the initial gpodder sync, i.e. we just synced the feeds successfully, - // it is now time to sync the episode actions - SyncService.sync(this); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + connectionMonitor.disable(getApplicationContext()); + } // start auto download in case anything new has shown up DBTasks.autodownloadUndownloadedItems(getApplicationContext()); @@ -326,18 +331,8 @@ public class DownloadService extends Service { if (item == null) { return; } - boolean unknownHost = status.getReason() == DownloadError.ERROR_UNKNOWN_HOST; - boolean unsupportedType = status.getReason() == DownloadError.ERROR_UNSUPPORTED_TYPE; - boolean wrongSize = status.getReason() == DownloadError.ERROR_IO_WRONG_SIZE; - - if (! (unknownHost || unsupportedType || wrongSize)) { - try { - DBWriter.saveFeedItemAutoDownloadFailed(item).get(); - } catch (ExecutionException | InterruptedException e) { - Log.d(TAG, "Ignoring exception while setting item download status"); - e.printStackTrace(); - } - } + item.increaseFailedAutoDownloadAttempts(System.currentTimeMillis()); + DBWriter.setFeedItem(item); // to make lists reload the failed item, we fake an item update EventBus.getDefault().post(FeedItemEvent.updated(item)); } 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 781110f82..cbfb2cede 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 @@ -4,6 +4,7 @@ import androidx.annotation.NonNull; import android.text.TextUtils; import android.util.Log; +import de.danoeh.antennapod.core.util.NetworkUtils; import okhttp3.CacheControl; import org.apache.commons.io.IOUtils; @@ -19,8 +20,6 @@ import java.net.URI; import java.net.UnknownHostException; import java.util.Collections; import java.util.Date; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.model.feed.FeedMedia; @@ -39,7 +38,6 @@ public class HttpDownloader extends Downloader { private static final String TAG = "HttpDownloader"; private static final int BUFFER_SIZE = 8 * 1024; - private static final String REGEX_PATTERN_IP_ADDRESS = "([0-9]{1,3}[\\.]){3}[0-9]{1,3}"; public HttpDownloader(@NonNull DownloadRequest request) { super(request); @@ -259,21 +257,14 @@ public class HttpDownloader extends Downloader { onFail(DownloadError.ERROR_UNKNOWN_HOST, e.getMessage()); } catch (IOException e) { e.printStackTrace(); + if (NetworkUtils.wasDownloadBlocked(e)) { + onFail(DownloadError.ERROR_IO_BLOCKED, e.getMessage()); + return; + } String message = e.getMessage(); - if (message != null) { - // Try to parse message for a more detailed error message - Pattern pattern = Pattern.compile(REGEX_PATTERN_IP_ADDRESS); - Matcher matcher = pattern.matcher(message); - if (matcher.find()) { - String ip = matcher.group(); - if (ip.startsWith("127.") || ip.startsWith("0.")) { - onFail(DownloadError.ERROR_IO_BLOCKED, e.getMessage()); - return; - } - } else if (message.contains("Trust anchor for certification path not found")) { - onFail(DownloadError.ERROR_CERTIFICATE, e.getMessage()); - return; - } + if (message != null && message.contains("Trust anchor for certification path not found")) { + onFail(DownloadError.ERROR_CERTIFICATE, e.getMessage()); + return; } onFail(DownloadError.ERROR_IO_ERROR, e.getMessage()); } catch (NullPointerException e) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/NewEpisodesNotification.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/NewEpisodesNotification.java index 869205b64..f7ed049cd 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/NewEpisodesNotification.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/NewEpisodesNotification.java @@ -8,6 +8,7 @@ import android.content.Intent; import android.content.res.Resources; import android.graphics.Bitmap; +import android.os.Build; import android.util.Log; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; @@ -68,7 +69,8 @@ public class NewEpisodesNotification { intent.setComponent(new ComponentName(context, "de.danoeh.antennapod.activity.MainActivity")); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); intent.putExtra("fragment_feed_id", feed.getId()); - PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0); + PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, + (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); Notification notification = new NotificationCompat.Builder( context, NotificationUtils.CHANNEL_ID_EPISODE_NOTIFICATIONS) @@ -79,6 +81,7 @@ public class NewEpisodesNotification { .setContentIntent(pendingIntent) .setGroup(GROUP_KEY) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) + .setOnlyAlertOnce(true) .setAutoCancel(true) .build(); @@ -92,7 +95,8 @@ public class NewEpisodesNotification { intent.setComponent(new ComponentName(context, "de.danoeh.antennapod.activity.MainActivity")); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); intent.putExtra("fragment_tag", "EpisodesFragment"); - PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0); + PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, + (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); Notification notificationGroupSummary = new NotificationCompat.Builder( context, NotificationUtils.CHANNEL_ID_EPISODE_NOTIFICATIONS) @@ -102,6 +106,7 @@ public class NewEpisodesNotification { .setGroup(GROUP_KEY) .setGroupSummary(true) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) + .setOnlyAlertOnce(true) .setAutoCancel(true) .build(); notificationManager.notify(NotificationUtils.CHANNEL_ID_EPISODE_NOTIFICATIONS, 0, notificationGroupSummary); 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 8c9035621..541e17cf6 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 @@ -6,21 +6,22 @@ import android.util.Log; import androidx.annotation.NonNull; +import org.greenrobot.eventbus.EventBus; + import java.io.File; import java.util.concurrent.ExecutionException; -import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; import de.danoeh.antennapod.core.service.download.DownloadRequest; import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink; import de.danoeh.antennapod.core.util.ChapterUtils; import de.danoeh.antennapod.core.util.DownloadError; -import de.danoeh.antennapod.core.sync.SyncService; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.net.sync.model.EpisodeAction; -import org.greenrobot.eventbus.EventBus; /** * Handles a completed media download. @@ -82,7 +83,7 @@ public class MediaDownloadedHandler implements Runnable { // we've received the media, we don't want to autodownload it again if (item != null) { - item.setAutoDownload(false); + item.disableAutoDownload(); // setFeedItem() signals (via EventBus) that the item has been updated, // so we do it after the enclosing media has been updated above, // to ensure subscribers will get the updated FeedMedia as well @@ -103,7 +104,7 @@ public class MediaDownloadedHandler implements Runnable { EpisodeAction action = new EpisodeAction.Builder(item, EpisodeAction.DOWNLOAD) .currentTimestamp() .build(); - SyncService.enqueueEpisodeAction(context, action); + SynchronizationQueueSink.enqueueEpisodeActionIfSynchronizationIsActive(context, action); } } 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 0a9bf5f43..d4008b3f2 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 @@ -5,36 +5,43 @@ import android.net.Uri; import android.text.TextUtils; import android.util.Log; import android.view.SurfaceHolder; +import androidx.annotation.NonNull; +import androidx.core.util.Consumer; 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.Format; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.audio.AudioAttributes; +import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.ExoTrackSelection; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.ui.DefaultTrackNameProvider; import com.google.android.exoplayer2.ui.TrackNameProvider; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; -import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; -import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.exoplayer2.upstream.HttpDataSource; import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; import de.danoeh.antennapod.core.service.download.HttpDownloader; +import de.danoeh.antennapod.core.util.NetworkUtils; import de.danoeh.antennapod.core.util.playback.IPlayer; +import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; @@ -42,6 +49,7 @@ import org.antennapod.audio.MediaPlayer; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.concurrent.TimeUnit; @@ -54,7 +62,7 @@ public class ExoPlayerWrapper implements IPlayer { private MediaSource mediaSource; private MediaPlayer.OnSeekCompleteListener audioSeekCompleteListener; private MediaPlayer.OnCompletionListener audioCompletionListener; - private MediaPlayer.OnErrorListener audioErrorListener; + private Consumer<String> audioErrorListener; private MediaPlayer.OnBufferingUpdateListener bufferingUpdateListener; private PlaybackParameters playbackParameters; private MediaPlayer.OnInfoListener infoListener; @@ -82,12 +90,12 @@ public class ExoPlayerWrapper implements IPlayer { trackSelector = new DefaultTrackSelector(context); exoPlayer = new SimpleExoPlayer.Builder(context, new DefaultRenderersFactory(context)) .setTrackSelector(trackSelector) - .setLoadControl(loadControl.createDefaultLoadControl()) + .setLoadControl(loadControl.build()) .build(); exoPlayer.setSeekParameters(SeekParameters.EXACT); - exoPlayer.addListener(new Player.EventListener() { + exoPlayer.addListener(new Player.Listener() { @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + public void onPlaybackStateChanged(@Player.State int playbackState) { if (audioCompletionListener != null && playbackState == Player.STATE_ENDED) { audioCompletionListener.onCompletion(null); } else if (infoListener != null && playbackState == Player.STATE_BUFFERING) { @@ -98,15 +106,25 @@ public class ExoPlayerWrapper implements IPlayer { } @Override - public void onPlayerError(ExoPlaybackException error) { + public void onPlayerError(@NonNull ExoPlaybackException error) { if (audioErrorListener != null) { - audioErrorListener.onError(null, error.type + ERROR_CODE_OFFSET, 0); + if (NetworkUtils.wasDownloadBlocked(error)) { + audioErrorListener.accept(context.getString(R.string.download_error_blocked)); + } else { + Throwable cause = error.getCause(); + if (cause instanceof HttpDataSource.HttpDataSourceException) { + cause = cause.getCause(); + } + audioErrorListener.accept(cause != null ? cause.getMessage() : error.getMessage()); + } } } @Override - public void onSeekProcessed() { - if (audioSeekCompleteListener != null) { + public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition, + @NonNull Player.PositionInfo newPosition, + @Player.DiscontinuityReason int reason) { + if (audioSeekCompleteListener != null && reason == Player.DISCONTINUITY_REASON_SEEK) { audioSeekCompleteListener.onSeekComplete(null); } } @@ -143,12 +161,13 @@ public class ExoPlayerWrapper implements IPlayer { @Override public void pause() { - exoPlayer.setPlayWhenReady(false); + exoPlayer.pause(); } @Override public void prepare() throws IllegalStateException { - exoPlayer.prepare(mediaSource, false, true); + exoPlayer.setMediaSource(mediaSource, false); + exoPlayer.prepare(); } @Override @@ -184,31 +203,31 @@ public class ExoPlayerWrapper implements IPlayer { b.setContentType(i); b.setFlags(a.flags); b.setUsage(a.usage); - exoPlayer.setAudioAttributes(b.build()); + exoPlayer.setAudioAttributes(b.build(), false); } public void setDataSource(String s, String user, String password) throws IllegalArgumentException, IllegalStateException { Log.d(TAG, "setDataSource: " + s); - DefaultHttpDataSourceFactory httpDataSourceFactory = new DefaultHttpDataSourceFactory( - ClientConfig.USER_AGENT, null, - DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS, - DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, - true); + final OkHttpDataSource.Factory httpDataSourceFactory = + new OkHttpDataSource.Factory(AntennapodHttpClient.getHttpClient()) + .setUserAgent(ClientConfig.USER_AGENT); if (!TextUtils.isEmpty(user) && !TextUtils.isEmpty(password)) { - httpDataSourceFactory.getDefaultRequestProperties().set("Authorization", - HttpDownloader.encodeCredentials( - user, - password, - "ISO-8859-1")); + final HashMap<String, String> requestProperties = new HashMap<>(); + requestProperties.put( + "Authorization", + HttpDownloader.encodeCredentials(user, password, "ISO-8859-1") + ); + httpDataSourceFactory.setDefaultRequestProperties(requestProperties); } DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context, null, httpDataSourceFactory); DefaultExtractorsFactory extractorsFactory = new DefaultExtractorsFactory(); extractorsFactory.setConstantBitrateSeekingEnabled(true); extractorsFactory.setMp3ExtractorFlags(Mp3Extractor.FLAG_DISABLE_ID3_METADATA); ProgressiveMediaSource.Factory f = new ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory); - mediaSource = f.createMediaSource(Uri.parse(s)); + final MediaItem mediaItem = MediaItem.fromUri(Uri.parse(s)); + mediaSource = f.createMediaSource(mediaItem); } @Override @@ -223,7 +242,8 @@ public class ExoPlayerWrapper implements IPlayer { @Override public void setPlaybackParams(float speed, boolean skipSilence) { - playbackParameters = new PlaybackParameters(speed, playbackParameters.pitch, skipSilence); + playbackParameters = new PlaybackParameters(speed, playbackParameters.pitch); + exoPlayer.setSkipSilenceEnabled(skipSilence); exoPlayer.setPlaybackParameters(playbackParameters); } @@ -244,7 +264,7 @@ public class ExoPlayerWrapper implements IPlayer { @Override public void start() { - exoPlayer.setPlayWhenReady(true); + exoPlayer.play(); // Can't set params when paused - so always set it on start in case they changed exoPlayer.setPlaybackParameters(playbackParameters); } @@ -304,7 +324,7 @@ public class ExoPlayerWrapper implements IPlayer { TrackSelectionArray trackSelections = exoPlayer.getCurrentTrackSelections(); List<Format> availableFormats = getFormats(); for (int i = 0; i < trackSelections.length; i++) { - TrackSelection track = trackSelections.get(i); + ExoTrackSelection track = (ExoTrackSelection) trackSelections.get(i); if (track == null) { continue; } @@ -323,7 +343,7 @@ public class ExoPlayerWrapper implements IPlayer { this.audioSeekCompleteListener = audioSeekCompleteListener; } - void setOnErrorListener(MediaPlayer.OnErrorListener audioErrorListener) { + void setOnErrorListener(Consumer<String> audioErrorListener) { this.audioErrorListener = audioErrorListener; } 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 f74e3b9ad..34fc7d699 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 @@ -14,7 +14,12 @@ import android.view.SurfaceHolder; import androidx.media.AudioAttributesCompat; import androidx.media.AudioFocusRequestCompat; import androidx.media.AudioManagerCompat; -import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.event.PlayerErrorEvent; +import de.danoeh.antennapod.event.playback.BufferUpdateEvent; +import de.danoeh.antennapod.event.playback.SpeedChangedEvent; +import de.danoeh.antennapod.core.util.playback.MediaPlayerError; +import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer; +import de.danoeh.antennapod.playback.base.PlayerStatus; import org.antennapod.audio.MediaPlayer; import java.io.File; @@ -35,12 +40,13 @@ import de.danoeh.antennapod.model.playback.MediaType; import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting; import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils; import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.util.RewindAfterPauseUtils; +import de.danoeh.antennapod.playback.base.RewindAfterPauseUtils; import de.danoeh.antennapod.core.util.playback.AudioPlayer; import de.danoeh.antennapod.core.util.playback.IPlayer; import de.danoeh.antennapod.model.playback.Playable; import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter; import de.danoeh.antennapod.core.util.playback.VideoPlayer; +import org.greenrobot.eventbus.EventBus; /** * Manages the MediaPlayer object of the PlaybackService. @@ -68,6 +74,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { private final PlayerLock playerLock; private final PlayerExecutor executor; private boolean useCallerThread = true; + private boolean isShutDown = false; private CountDownLatch seekLatch; @@ -142,7 +149,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { } public LocalPSMP(@NonNull Context context, - @NonNull PSMPCallback callback) { + @NonNull PlaybackServiceMediaPlayer.PSMPCallback callback) { super(context, callback); this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); this.playerLock = new PlayerLock(); @@ -259,9 +266,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { LocalPSMP.this.startWhenPrepared.set(startWhenPrepared); setPlayerStatus(PlayerStatus.INITIALIZING, media); try { - if (media instanceof FeedMedia && ((FeedMedia) media).getItem() == null) { - ((FeedMedia) media).setItem(DBReader.getFeedItem(((FeedMedia) media).getItemId())); - } + callback.ensureMediaInfoLoaded(media); callback.onMediaChanged(false); setPlaybackParams(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media), UserPreferences.isSkipSilence()); if (stream) { @@ -294,6 +299,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { } catch (IOException | IllegalStateException e) { e.printStackTrace(); setPlayerStatus(PlayerStatus.ERROR, null); + EventBus.getDefault().postSticky(new PlayerErrorEvent(e.getLocalizedMessage())); } } @@ -402,6 +408,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { } catch (IOException e) { e.printStackTrace(); setPlayerStatus(PlayerStatus.ERROR, null); + EventBus.getDefault().postSticky(new PlayerErrorEvent(e.getLocalizedMessage())); } } playerLock.unlock(); @@ -611,7 +618,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { private void setSpeedSyncAndSkipSilence(float speed, boolean skipSilence) { playerLock.lock(); Log.d(TAG, "Playback speed was set to " + speed); - callback.playbackSpeedChanged(speed); + EventBus.getDefault().post(new SpeedChangedEvent(speed)); mediaPlayer.setPlaybackParams(speed, skipSilence); playerLock.unlock(); } @@ -712,32 +719,22 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { */ @Override public void shutdown() { - executor.shutdown(); if (mediaPlayer != null) { try { - removeMediaPlayerErrorListener(); + clearMediaPlayerListeners(); if (mediaPlayer.isPlaying()) { mediaPlayer.stop(); } } catch (Exception ignore) { } mediaPlayer.release(); + mediaPlayer = null; } + isShutDown = true; + executor.shutdown(); + abandonAudioFocus(); releaseWifiLockIfNecessary(); } - private void removeMediaPlayerErrorListener() { - if (mediaPlayer instanceof VideoPlayer) { - VideoPlayer vp = (VideoPlayer) mediaPlayer; - vp.setOnErrorListener((mp, what, extra) -> true); - } else if (mediaPlayer instanceof AudioPlayer) { - AudioPlayer ap = (AudioPlayer) mediaPlayer; - ap.setOnErrorListener((mediaPlayer, i, i1) -> true); - } else if (mediaPlayer instanceof ExoPlayerWrapper) { - ExoPlayerWrapper ap = (ExoPlayerWrapper) mediaPlayer; - ap.setOnErrorListener((mediaPlayer, i, i1) -> true); - } - } - /** * Releases internally used resources. This method should only be called when the object is not used anymore. * This method is executed on an internal executor service. @@ -857,10 +854,14 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { @Override public void onAudioFocusChange(final int focusChange) { + if (isShutDown) { + return; + } if (!PlaybackService.isRunning) { abandonAudioFocus(); Log.d(TAG, "onAudioFocusChange: PlaybackService is no longer running"); if (focusChange == AudioManager.AUDIOFOCUS_GAIN && pausedBecauseOfTransientAudiofocusLoss) { + pausedBecauseOfTransientAudiofocusLoss = false; new PlaybackServiceStarter(context, getPlayable()) .startWhenPrepared(true) .streamIfLastWasStream() @@ -1004,9 +1005,9 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { return stream; } - private IPlayer setMediaPlayerListeners(IPlayer mp) { + private void setMediaPlayerListeners(IPlayer mp) { if (mp == null || media == null) { - return mp; + return; } if (mp instanceof VideoPlayer) { if (media.getMediaType() != MediaType.VIDEO) { @@ -1033,12 +1034,36 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { ap.setOnCompletionListener(audioCompletionListener); ap.setOnSeekCompleteListener(audioSeekCompleteListener); ap.setOnBufferingUpdateListener(audioBufferingUpdateListener); - ap.setOnErrorListener(audioErrorListener); + ap.setOnErrorListener(message -> EventBus.getDefault().postSticky(new PlayerErrorEvent(message))); ap.setOnInfoListener(audioInfoListener); } else { Log.w(TAG, "Unknown media player: " + mp); } - return mp; + } + + private void clearMediaPlayerListeners() { + if (mediaPlayer instanceof VideoPlayer) { + VideoPlayer vp = (VideoPlayer) mediaPlayer; + vp.setOnCompletionListener(x -> { }); + vp.setOnSeekCompleteListener(x -> { }); + vp.setOnErrorListener((mediaPlayer, i, i1) -> false); + vp.setOnBufferingUpdateListener((mediaPlayer, i) -> { }); + vp.setOnInfoListener((mediaPlayer, i, i1) -> false); + } else if (mediaPlayer instanceof AudioPlayer) { + AudioPlayer ap = (AudioPlayer) mediaPlayer; + ap.setOnCompletionListener(x -> { }); + ap.setOnSeekCompleteListener(x -> { }); + ap.setOnErrorListener((x, y, z) -> false); + ap.setOnBufferingUpdateListener((arg0, percent) -> { }); + ap.setOnInfoListener((arg0, what, extra) -> false); + } else if (mediaPlayer instanceof ExoPlayerWrapper) { + ExoPlayerWrapper ap = (ExoPlayerWrapper) mediaPlayer; + ap.setOnCompletionListener(x -> { }); + ap.setOnSeekCompleteListener(x -> { }); + ap.setOnBufferingUpdateListener((arg0, percent) -> { }); + ap.setOnErrorListener(x -> { }); + ap.setOnInfoListener((arg0, what, extra) -> false); + } } private final MediaPlayer.OnCompletionListener audioCompletionListener = @@ -1052,14 +1077,10 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { } private final MediaPlayer.OnBufferingUpdateListener audioBufferingUpdateListener = - (mp, percent) -> genericOnBufferingUpdate(percent); + (mp, percent) -> EventBus.getDefault().post(BufferUpdateEvent.progressUpdate(0.01f * percent)); private final android.media.MediaPlayer.OnBufferingUpdateListener videoBufferingUpdateListener = - (mp, percent) -> genericOnBufferingUpdate(percent); - - private void genericOnBufferingUpdate(int percent) { - callback.onBufferingUpdate(percent); - } + (mp, percent) -> EventBus.getDefault().post(BufferUpdateEvent.progressUpdate(0.01f * percent)); private final MediaPlayer.OnInfoListener audioInfoListener = (mp, what, extra) -> genericInfoListener(what); @@ -1068,7 +1089,16 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { (mp, what, extra) -> genericInfoListener(what); private boolean genericInfoListener(int what) { - return callback.onMediaPlayerInfo(what, 0); + switch (what) { + case android.media.MediaPlayer.MEDIA_INFO_BUFFERING_START: + EventBus.getDefault().post(BufferUpdateEvent.started()); + return true; + case android.media.MediaPlayer.MEDIA_INFO_BUFFERING_END: + EventBus.getDefault().post(BufferUpdateEvent.ended()); + return true; + default: + return true; + } } private final MediaPlayer.OnErrorListener audioErrorListener = @@ -1084,7 +1114,8 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { private final android.media.MediaPlayer.OnErrorListener videoErrorListener = this::genericOnError; private boolean genericOnError(Object inObj, int what, int extra) { - return callback.onMediaPlayerError(inObj, what, extra); + EventBus.getDefault().postSticky(new PlayerErrorEvent(MediaPlayerError.getErrorString(context, what))); + return true; } private final MediaPlayer.OnSeekCompleteListener audioSeekCompleteListener = @@ -1116,4 +1147,9 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { executor.submit(r); } } + + @Override + public boolean isCasting() { + return false; + } } 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 8ba5215df..805956094 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 @@ -1,5 +1,7 @@ package de.danoeh.antennapod.core.service.playback; +import static de.danoeh.antennapod.model.feed.FeedPreferences.SPEED_USE_GLOBAL; + import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; @@ -14,20 +16,13 @@ import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.res.Configuration; import android.media.AudioManager; -import android.media.MediaPlayer; import android.net.Uri; import android.os.Binder; import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.os.Vibrator; -import androidx.preference.PreferenceManager; -import androidx.annotation.NonNull; -import androidx.annotation.StringRes; -import androidx.core.app.NotificationCompat; -import androidx.core.app.NotificationManagerCompat; import android.support.v4.media.MediaBrowserCompat; -import androidx.media.MediaBrowserServiceCompat; import android.support.v4.media.MediaDescriptionCompat; import android.support.v4.media.MediaMetadataCompat; import android.support.v4.media.session.MediaSessionCompat; @@ -40,24 +35,38 @@ import android.view.SurfaceHolder; import android.webkit.URLUtil; import android.widget.Toast; +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; +import androidx.media.MediaBrowserServiceCompat; +import androidx.preference.PreferenceManager; + +import de.danoeh.antennapod.event.playback.BufferUpdateEvent; +import de.danoeh.antennapod.event.playback.PlaybackServiceEvent; +import de.danoeh.antennapod.event.PlayerErrorEvent; +import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent; +import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer; +import de.danoeh.antennapod.playback.base.PlayerStatus; +import de.danoeh.antennapod.playback.cast.CastPsmp; +import de.danoeh.antennapod.playback.cast.CastStateListener; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.core.event.MessageEvent; -import de.danoeh.antennapod.core.event.PlaybackPositionEvent; -import de.danoeh.antennapod.core.event.ServiceEvent; -import de.danoeh.antennapod.core.event.settings.SkipIntroEndingChangedEvent; -import de.danoeh.antennapod.core.event.settings.SpeedPresetChangedEvent; -import de.danoeh.antennapod.core.event.settings.VolumeAdaptionChangedEvent; -import de.danoeh.antennapod.model.feed.Chapter; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.model.feed.FeedPreferences; -import de.danoeh.antennapod.model.playback.MediaType; +import de.danoeh.antennapod.event.MessageEvent; +import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; +import de.danoeh.antennapod.event.settings.SkipIntroEndingChangedEvent; +import de.danoeh.antennapod.event.settings.SpeedPresetChangedEvent; +import de.danoeh.antennapod.event.settings.VolumeAdaptionChangedEvent; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.preferences.SleepTimerPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; @@ -66,15 +75,21 @@ import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.FeedSearcher; -import de.danoeh.antennapod.core.sync.SyncService; +import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink; import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.core.util.IntentUtils; import de.danoeh.antennapod.core.util.NetworkUtils; import de.danoeh.antennapod.core.util.gui.NotificationUtils; -import de.danoeh.antennapod.model.playback.Playable; import de.danoeh.antennapod.core.util.playback.PlayableUtils; import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter; import de.danoeh.antennapod.core.widget.WidgetUpdater; +import de.danoeh.antennapod.model.feed.Chapter; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.model.feed.FeedPreferences; +import de.danoeh.antennapod.model.playback.MediaType; +import de.danoeh.antennapod.model.playback.Playable; import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; import de.danoeh.antennapod.ui.appstartintent.VideoPlayerActivityStarter; import io.reactivex.Completable; @@ -83,11 +98,6 @@ 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; - -import static de.danoeh.antennapod.model.feed.FeedPreferences.SPEED_USE_GLOBAL; /** * Controls the MediaPlayer that plays a FeedMedia-file @@ -98,24 +108,10 @@ public class PlaybackService extends MediaBrowserServiceCompat { */ private static final String TAG = "PlaybackService"; - /** - * Parcelable of type Playable. - */ public static final String EXTRA_PLAYABLE = "PlaybackService.PlayableExtra"; - /** - * True if cast session should disconnect. - */ - public static final String EXTRA_CAST_DISCONNECT = "extra.de.danoeh.antennapod.core.service.castDisconnect"; - /** - * True if media should be streamed. - */ public static final String EXTRA_SHOULD_STREAM = "extra.de.danoeh.antennapod.core.service.shouldStream"; public static final String EXTRA_ALLOW_STREAM_THIS_TIME = "extra.de.danoeh.antennapod.core.service.allowStream"; public static final String EXTRA_ALLOW_STREAM_ALWAYS = "extra.de.danoeh.antennapod.core.service.allowStreamAlways"; - /** - * True if playback should be started immediately after media has been - * prepared. - */ public static final String EXTRA_START_WHEN_PREPARED = "extra.de.danoeh.antennapod.core.service.startWhenPrepared"; public static final String EXTRA_PREPARE_IMMEDIATELY = "extra.de.danoeh.antennapod.core.service.prepareImmediately"; @@ -159,30 +155,22 @@ public class PlaybackService extends MediaBrowserServiceCompat { public static final int EXTRA_CODE_VIDEO = 2; public static final int EXTRA_CODE_CAST = 3; - public static final int NOTIFICATION_TYPE_ERROR = 0; - public static final int NOTIFICATION_TYPE_BUFFER_UPDATE = 2; - /** * Receivers of this intent should update their information about the curently playing media */ public static final int NOTIFICATION_TYPE_RELOAD = 3; + /** - * The state of the sleeptimer changed. + * Set a max number of episodes to load for Android Auto, otherwise there could be performance issues */ - public static final int NOTIFICATION_TYPE_SLEEPTIMER_UPDATE = 4; - public static final int NOTIFICATION_TYPE_BUFFER_START = 5; - public static final int NOTIFICATION_TYPE_BUFFER_END = 6; + public static final int MAX_ANDROID_AUTO_EPISODES_PER_FEED = 100; + /** * No more episodes are going to be played. */ public static final int NOTIFICATION_TYPE_PLAYBACK_END = 7; /** - * Playback speed has changed - */ - public static final int NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE = 8; - - /** * Returned by getPositionSafe() or getDurationSafe() if the playbackService * is in an invalid state. */ @@ -203,10 +191,10 @@ public class PlaybackService extends MediaBrowserServiceCompat { private PlaybackServiceMediaPlayer mediaPlayer; private PlaybackServiceTaskManager taskManager; - private PlaybackServiceFlavorHelper flavorHelper; private PlaybackServiceStateManager stateManager; private Disposable positionEventTimer; private PlaybackServiceNotificationBuilder notificationBuilder; + private CastStateListener castStateListener; private String autoSkippedFeedMediaId = null; @@ -283,14 +271,14 @@ public class PlaybackService extends MediaBrowserServiceCompat { EventBus.getDefault().register(this); taskManager = new PlaybackServiceTaskManager(this, taskManagerCallback); - flavorHelper = new PlaybackServiceFlavorHelper(PlaybackService.this, flavorHelperCallback); PreferenceManager.getDefaultSharedPreferences(this) .registerOnSharedPreferenceChangeListener(prefListener); ComponentName eventReceiver = new ComponentName(getApplicationContext(), MediaButtonReceiver.class); Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); mediaButtonIntent.setComponent(eventReceiver); - PendingIntent buttonReceiverIntent = PendingIntent.getBroadcast(this, 0, mediaButtonIntent, PendingIntent.FLAG_UPDATE_CURRENT); + PendingIntent buttonReceiverIntent = PendingIntent.getBroadcast(this, 0, mediaButtonIntent, + PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= 31 ? PendingIntent.FLAG_MUTABLE : 0)); mediaSession = new MediaSessionCompat(getApplicationContext(), TAG, eventReceiver, buttonReceiverIntent); setSessionToken(mediaSession.getSessionToken()); @@ -307,10 +295,34 @@ public class PlaybackService extends MediaBrowserServiceCompat { npe.printStackTrace(); } - flavorHelper.initializeMediaPlayer(PlaybackService.this); + recreateMediaPlayer(); mediaSession.setActive(true); + castStateListener = new CastStateListener(this) { + @Override + public void onSessionStartedOrEnded() { + recreateMediaPlayer(); + } + }; + EventBus.getDefault().post(new PlaybackServiceEvent(PlaybackServiceEvent.Action.SERVICE_STARTED)); + } - EventBus.getDefault().post(new ServiceEvent(ServiceEvent.Action.SERVICE_STARTED)); + void recreateMediaPlayer() { + Playable media = null; + boolean wasPlaying = false; + if (mediaPlayer != null) { + media = mediaPlayer.getPlayable(); + wasPlaying = mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING; + mediaPlayer.pause(true, false); + mediaPlayer.shutdown(); + } + mediaPlayer = CastPsmp.getInstanceIfConnected(this, mediaPlayerCallback); + if (mediaPlayer == null) { + mediaPlayer = new LocalPSMP(this, mediaPlayerCallback); // Cast not supported or not connected + } + if (media != null) { + mediaPlayer.playMediaObject(media, !media.localFileAvailable(), wasPlaying, true); + } + isCasting = mediaPlayer.isCasting(); } @Override @@ -326,6 +338,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { stateManager.stopForeground(!UserPreferences.isPersistNotify()); isRunning = false; currentMediaType = MediaType.UNKNOWN; + castStateListener.destroy(); cancelPositionObserver(); PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(prefListener); @@ -339,8 +352,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { unregisterReceiver(audioBecomingNoisy); unregisterReceiver(skipCurrentEpisodeReceiver); unregisterReceiver(pausePlayCurrentEpisodeReceiver); - flavorHelper.removeCastConsumer(); - flavorHelper.unregisterWifiBroadcastReceiver(); mediaPlayer.shutdown(); taskManager.shutdown(); } @@ -370,30 +381,22 @@ public class PlaybackService extends MediaBrowserServiceCompat { .subscribe(queueItems -> mediaSession.setQueue(queueItems), Throwable::printStackTrace); } - private MediaBrowserCompat.MediaItem createBrowsableMediaItemForRoot() { + private MediaBrowserCompat.MediaItem createBrowsableMediaItem( + @StringRes int title, @DrawableRes int icon, int numEpisodes) { Uri uri = new Uri.Builder() .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) - .authority(getResources().getResourcePackageName(R.drawable.ic_playlist_black)) - .appendPath(getResources().getResourceTypeName(R.drawable.ic_playlist_black)) - .appendPath(getResources().getResourceEntryName(R.drawable.ic_playlist_black)) + .authority(getResources().getResourcePackageName(icon)) + .appendPath(getResources().getResourceTypeName(icon)) + .appendPath(getResources().getResourceEntryName(icon)) .build(); - String subtitle = ""; - try { - int count = taskManager.getQueue().size(); - subtitle = getResources().getQuantityString(R.plurals.num_episodes, count, count); - } catch (InterruptedException e) { - e.printStackTrace(); - } - MediaDescriptionCompat description = new MediaDescriptionCompat.Builder() .setIconUri(uri) - .setMediaId(getResources().getString(R.string.queue_label)) - .setTitle(getResources().getString(R.string.queue_label)) - .setSubtitle(subtitle) + .setMediaId(getResources().getString(title)) + .setTitle(getResources().getString(title)) + .setSubtitle(getResources().getQuantityString(R.plurals.num_episodes, numEpisodes, numEpisodes)) .build(); - return new MediaBrowserCompat.MediaItem(description, - MediaBrowserCompat.MediaItem.FLAG_BROWSABLE); + return new MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE); } private MediaBrowserCompat.MediaItem createBrowsableMediaItemForFeed(Feed feed) { @@ -425,42 +428,47 @@ public class PlaybackService extends MediaBrowserServiceCompat { }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(() -> { }, Throwable::printStackTrace); + .subscribe( + () -> { + }, e -> { + e.printStackTrace(); + result.sendResult(null); + }); } - private List<MediaBrowserCompat.MediaItem> loadChildrenSynchronous(@NonNull String parentId) { + private List<MediaBrowserCompat.MediaItem> loadChildrenSynchronous(@NonNull String parentId) + throws InterruptedException { List<MediaBrowserCompat.MediaItem> mediaItems = new ArrayList<>(); if (parentId.equals(getResources().getString(R.string.app_name))) { - // Root List - try { - if (!(taskManager.getQueue().isEmpty())) { - mediaItems.add(createBrowsableMediaItemForRoot()); - } - } catch (InterruptedException e) { - e.printStackTrace(); - } + mediaItems.add(createBrowsableMediaItem(R.string.queue_label, R.drawable.ic_playlist_black, + taskManager.getQueue().size())); + mediaItems.add(createBrowsableMediaItem(R.string.downloads_label, R.drawable.ic_download_black, + DBReader.getDownloadedItems().size())); List<Feed> feeds = DBReader.getFeedList(); for (Feed feed : feeds) { mediaItems.add(createBrowsableMediaItemForFeed(feed)); } - } else if (parentId.equals(getResources().getString(R.string.queue_label))) { - // Child List - try { - for (FeedItem feedItem : taskManager.getQueue()) { - FeedMedia media = feedItem.getMedia(); - if (media != null) { - mediaItems.add(media.getMediaItem()); - } - } - } catch (InterruptedException e) { - e.printStackTrace(); - } + return mediaItems; + } + + List<FeedItem> feedItems; + if (parentId.equals(getResources().getString(R.string.queue_label))) { + feedItems = taskManager.getQueue(); + } else if (parentId.equals(getResources().getString(R.string.downloads_label))) { + feedItems = DBReader.getDownloadedItems(); } else if (parentId.startsWith("FeedId:")) { 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) { - mediaItems.add(feedItem.getMedia().getMediaItem()); + feedItems = DBReader.getFeedItemList(DBReader.getFeed(feedId)); + } else { + Log.e(TAG, "Parent ID not found: " + parentId); + return null; + } + int count = 0; + for (FeedItem feedItem : feedItems) { + if (feedItem.getMedia() != null && feedItem.getMedia().getMediaItem() != null) { + mediaItems.add(feedItem.getMedia().getMediaItem()); + if (++count >= MAX_ANDROID_AUTO_EPISODES_PER_FEED) { + break; } } } @@ -488,9 +496,8 @@ public class PlaybackService extends MediaBrowserServiceCompat { 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) { + if (keycode == -1 && playable == null) { Log.e(TAG, "PlaybackService was started with no arguments"); stateManager.stopService(); return Service.START_NOT_STICKY; @@ -514,7 +521,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { stateManager.stopService(); return Service.START_NOT_STICKY; } - } else if (!flavorHelper.castDisconnect(castDisconnect) && playable != null) { + } else { stateManager.validStartCommandWasReceived(); boolean stream = intent.getBooleanExtra(EXTRA_SHOULD_STREAM, true); boolean allowStreamThisTime = intent.getBooleanExtra(EXTRA_ALLOW_STREAM_THIS_TIME, false); @@ -558,9 +565,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { stateManager.stopService(); }); return Service.START_NOT_STICKY; - } else { - Log.d(TAG, "Did not handle intent to PlaybackService: " + intent); - Log.d(TAG, "Extras: " + intent.getExtras()); } } @@ -598,10 +602,12 @@ public class PlaybackService extends MediaBrowserServiceCompat { PendingIntent pendingIntentAllowThisTime; if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { pendingIntentAllowThisTime = PendingIntent.getForegroundService(this, - R.id.pending_intent_allow_stream_this_time, intentAllowThisTime, PendingIntent.FLAG_UPDATE_CURRENT); + R.id.pending_intent_allow_stream_this_time, intentAllowThisTime, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); } else { pendingIntentAllowThisTime = PendingIntent.getService(this, - R.id.pending_intent_allow_stream_this_time, intentAllowThisTime, PendingIntent.FLAG_UPDATE_CURRENT); + R.id.pending_intent_allow_stream_this_time, intentAllowThisTime, PendingIntent.FLAG_UPDATE_CURRENT + | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); } Intent intentAlwaysAllow = new Intent(intentAllowThisTime); @@ -610,10 +616,12 @@ public class PlaybackService extends MediaBrowserServiceCompat { PendingIntent pendingIntentAlwaysAllow; if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { pendingIntentAlwaysAllow = PendingIntent.getForegroundService(this, - R.id.pending_intent_allow_stream_always, intentAlwaysAllow, PendingIntent.FLAG_UPDATE_CURRENT); + R.id.pending_intent_allow_stream_always, intentAlwaysAllow, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); } else { pendingIntentAlwaysAllow = PendingIntent.getService(this, - R.id.pending_intent_allow_stream_always, intentAlwaysAllow, PendingIntent.FLAG_UPDATE_CURRENT); + R.id.pending_intent_allow_stream_always, intentAlwaysAllow, PendingIntent.FLAG_UPDATE_CURRENT + | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); } NotificationCompat.Builder builder = new NotificationCompat.Builder(this, @@ -783,26 +791,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { } @Override - 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); - mediaPlayer.setVolume(multiplicator, multiplicator); - } - - @Override - public void onSleepTimerExpired() { - mediaPlayer.pause(true, true); - mediaPlayer.setVolume(1.0f, 1.0f); - sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0); - } - - @Override - public void onSleepTimerReset() { - mediaPlayer.setVolume(1.0f, 1.0f); - } - - @Override public WidgetUpdater.WidgetState requestWidgetState() { return new WidgetUpdater.WidgetState(getPlayable(), getStatus(), getCurrentPosition(), getDuration(), getCurrentPlaybackSpeed(), isCasting()); @@ -834,9 +822,8 @@ public class PlaybackService extends MediaBrowserServiceCompat { taskManager.startChapterLoader(newInfo.playable); break; case PAUSED: - if ((UserPreferences.isPersistNotify() || isCasting) && - android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - // do not remove notification on pause based on user pref and whether android version supports expanded notifications + if (UserPreferences.isPersistNotify() || isCasting) { + // do not remove notification on pause based on user pref // Change [Play] button to [Pause] updateNotificationAndMediaSession(newInfo.playable); } else if (!UserPreferences.isPersistNotify() && !isCasting) { @@ -884,16 +871,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { } @Override - public void playbackSpeedChanged(float s) { - sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE, 0); - } - - @Override - public void onBufferingUpdate(int percent) { - sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_UPDATE, percent); - } - - @Override public void onMediaChanged(boolean reloadUI) { Log.d(TAG, "reloadUI callback reached"); if (reloadUI) { @@ -903,43 +880,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { } @Override - public boolean onMediaPlayerInfo(int code, @StringRes int resourceId) { - switch (code) { - case MediaPlayer.MEDIA_INFO_BUFFERING_START: - sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_START, 0); - return true; - case MediaPlayer.MEDIA_INFO_BUFFERING_END: - sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_END, 0); - - Playable playable = getPlayable(); - if (getPlayable() instanceof FeedMedia - && playable.getDuration() <= 0 && mediaPlayer.getDuration() > 0) { - // Playable is being streamed and does not have a duration specified in the feed - playable.setDuration(mediaPlayer.getDuration()); - DBWriter.setFeedMedia((FeedMedia) playable); - updateNotificationAndMediaSession(playable); - } - - return true; - default: - return flavorHelper.onMediaPlayerInfo(PlaybackService.this, code, resourceId); - } - } - - @Override - public boolean onMediaPlayerError(Object inObj, int what, int extra) { - final String TAG = "PlaybackSvc.onErrorLtsn"; - Log.w(TAG, "An error has occured: " + what + " " + extra); - if (mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING) { - mediaPlayer.pause(true, false); - } - sendNotificationBroadcast(NOTIFICATION_TYPE_ERROR, what); - PlaybackPreferences.writeNoMediaPlaying(); - stateManager.stopService(); - return true; - } - - @Override public void onPostPlayback(@NonNull Playable media, boolean ended, boolean skipped, boolean playingNext) { PlaybackService.this.onPostPlayback(media, ended, skipped, playingNext); @@ -966,7 +906,8 @@ public class PlaybackService extends MediaBrowserServiceCompat { taskManager.cancelWidgetUpdater(); if (playable != null) { if (playable instanceof FeedMedia) { - SyncService.enqueueEpisodePlayed(getApplicationContext(), (FeedMedia) playable, false); + SynchronizationQueueSink.enqueueEpisodePlayedIfSynchronizationIsActive(getApplicationContext(), + (FeedMedia) playable, false); } playable.onPlaybackPause(getApplicationContext()); } @@ -977,12 +918,67 @@ public class PlaybackService extends MediaBrowserServiceCompat { return PlaybackService.this.getNextInQueue(currentMedia); } + @Nullable + @Override + public Playable findMedia(@NonNull String url) { + FeedItem item = DBReader.getFeedItemByGuidOrEpisodeUrl(null, url); + return item != null ? item.getMedia() : null; + } + @Override public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) { PlaybackService.this.onPlaybackEnded(mediaType, stopPlaying); } + + @Override + public void ensureMediaInfoLoaded(@NonNull Playable media) { + if (media instanceof FeedMedia && ((FeedMedia) media).getItem() == null) { + ((FeedMedia) media).setItem(DBReader.getFeedItem(((FeedMedia) media).getItemId())); + } + } }; + @Subscribe(threadMode = ThreadMode.MAIN) + @SuppressWarnings("unused") + public void playerError(PlayerErrorEvent event) { + if (mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING) { + mediaPlayer.pause(true, false); + } + PlaybackPreferences.writeNoMediaPlaying(); + stateManager.stopService(); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + @SuppressWarnings("unused") + public void bufferUpdate(BufferUpdateEvent event) { + if (event.hasEnded()) { + Playable playable = getPlayable(); + if (getPlayable() instanceof FeedMedia + && playable.getDuration() <= 0 && mediaPlayer.getDuration() > 0) { + // Playable is being streamed and does not have a duration specified in the feed + playable.setDuration(mediaPlayer.getDuration()); + DBWriter.setFeedMedia((FeedMedia) playable); + updateNotificationAndMediaSession(playable); + } + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + @SuppressWarnings("unused") + public void sleepTimerUpdate(SleepTimerUpdatedEvent event) { + if (event.isOver()) { + mediaPlayer.pause(true, true); + mediaPlayer.setVolume(1.0f, 1.0f); + } else if (event.getTimeLeft() < PlaybackServiceTaskManager.SleepTimer.NOTIFICATION_THRESHOLD) { + 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) event.getTimeLeft() / 1000)]; + Log.d(TAG, "onSleepTimerAlmostExpired: " + multiplicator); + mediaPlayer.setVolume(multiplicator, multiplicator); + } else if (event.isCancelled()) { + mediaPlayer.setVolume(1.0f, 1.0f); + } + } + private Playable getNextInQueue(final Playable currentMedia) { if (!(currentMedia instanceof FeedMedia)) { Log.d(TAG, "getNextInQueue(), but playable not an instance of FeedMedia, so not proceeding"); @@ -1110,10 +1106,12 @@ public class PlaybackService extends MediaBrowserServiceCompat { } if (ended || smartMarkAsPlayed) { - SyncService.enqueueEpisodePlayed(getApplicationContext(), media, true); + SynchronizationQueueSink.enqueueEpisodePlayedIfSynchronizationIsActive( + getApplicationContext(), media, true); media.onPlaybackCompleted(getApplicationContext()); } else { - SyncService.enqueueEpisodePlayed(getApplicationContext(), media, false); + SynchronizationQueueSink.enqueueEpisodePlayedIfSynchronizationIsActive( + getApplicationContext(), media, false); media.onPlaybackPause(getApplicationContext()); } @@ -1146,12 +1144,10 @@ public class PlaybackService extends MediaBrowserServiceCompat { public void setSleepTimer(long waitingTime) { Log.d(TAG, "Setting sleep timer to " + waitingTime + " milliseconds"); taskManager.setSleepTimer(waitingTime); - sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0); } public void disableSleepTimer() { taskManager.disableSleepTimer(); - sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0); } private void sendNotificationBroadcast(int type, int code) { @@ -1235,7 +1231,8 @@ public class PlaybackService extends MediaBrowserServiceCompat { | PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_FAST_FORWARD | PlaybackStateCompat.ACTION_SKIP_TO_NEXT - | PlaybackStateCompat.ACTION_SEEK_TO; + | PlaybackStateCompat.ACTION_SEEK_TO + | PlaybackStateCompat.ACTION_SET_PLAYBACK_SPEED; if (useSkipToPreviousForRewindInLockscreen()) { // Workaround to fool Android so that Lockscreen will expose a skip-to-previous button, @@ -1268,15 +1265,15 @@ public class PlaybackService extends MediaBrowserServiceCompat { // This would give the PIP of videos a play button capabilities = capabilities | PlaybackStateCompat.ACTION_PLAY; if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_WATCH) { - flavorHelper.sessionStateAddActionForWear(sessionState, + WearMediaSession.sessionStateAddActionForWear(sessionState, CUSTOM_ACTION_REWIND, getString(R.string.rewind_label), android.R.drawable.ic_media_rew); - flavorHelper.sessionStateAddActionForWear(sessionState, + WearMediaSession.sessionStateAddActionForWear(sessionState, CUSTOM_ACTION_FAST_FORWARD, getString(R.string.fast_forward_label), android.R.drawable.ic_media_ff); - flavorHelper.mediaSessionSetExtraForWear(mediaSession); + WearMediaSession.mediaSessionSetExtraForWear(mediaSession); } } @@ -1320,7 +1317,8 @@ public class PlaybackService extends MediaBrowserServiceCompat { if (stateManager.hasReceivedValidStartCommand()) { mediaSession.setSessionActivity(PendingIntent.getActivity(this, R.id.pending_intent_player_activity, - PlaybackService.getPlayerActivityIntent(this), PendingIntent.FLAG_UPDATE_CURRENT)); + PlaybackService.getPlayerActivityIntent(this), PendingIntent.FLAG_UPDATE_CURRENT + | (Build.VERSION.SDK_INT >= 31 ? PendingIntent.FLAG_MUTABLE : 0))); try { mediaSession.setMetadata(builder.build()); } catch (OutOfMemoryError e) { @@ -1357,7 +1355,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { notificationBuilder.setPlayable(playable); notificationBuilder.setMediaSessionToken(mediaSession.getSessionToken()); notificationBuilder.setPlayerStatus(playerStatus); - notificationBuilder.setCasting(isCasting); notificationBuilder.updatePosition(getCurrentPosition(), getCurrentPlaybackSpeed()); NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); @@ -1566,7 +1563,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { @Override public void onReceive(Context context, Intent intent) { if (TextUtils.equals(intent.getAction(), ACTION_SHUTDOWN_PLAYBACK_SERVICE)) { - EventBus.getDefault().post(new ServiceEvent(ServiceEvent.Action.SERVICE_SHUT_DOWN)); + EventBus.getDefault().post(new PlaybackServiceEvent(PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN)); stateManager.stopService(); } } @@ -1892,6 +1889,12 @@ public class PlaybackService extends MediaBrowserServiceCompat { } @Override + public void onSetPlaybackSpeed(float speed) { + Log.d(TAG, "onSetPlaybackSpeed()"); + setSpeed(speed); + } + + @Override public boolean onMediaButtonEvent(final Intent mediaButton) { Log.d(TAG, "onMediaButtonEvent(" + mediaButton + ")"); if (mediaButton != null) { @@ -1920,96 +1923,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { (sharedPreferences, key) -> { if (UserPreferences.PREF_LOCKSCREEN_BACKGROUND.equals(key)) { updateNotificationAndMediaSession(getPlayable()); - } else { - flavorHelper.onSharedPreference(key); } }; - - interface FlavorHelperCallback { - PlaybackServiceMediaPlayer.PSMPCallback getMediaPlayerCallback(); - - void setMediaPlayer(PlaybackServiceMediaPlayer mediaPlayer); - - PlaybackServiceMediaPlayer getMediaPlayer(); - - void setIsCasting(boolean isCasting); - - void sendNotificationBroadcast(int type, int code); - - void saveCurrentPosition(boolean fromMediaPlayer, Playable playable, int position); - - void setupNotification(boolean connected, PlaybackServiceMediaPlayer.PSMPInfo info); - - MediaSessionCompat getMediaSession(); - - Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter); - - void unregisterReceiver(BroadcastReceiver receiver); - } - - private final FlavorHelperCallback flavorHelperCallback = new FlavorHelperCallback() { - @Override - public PlaybackServiceMediaPlayer.PSMPCallback getMediaPlayerCallback() { - return PlaybackService.this.mediaPlayerCallback; - } - - @Override - public void setMediaPlayer(PlaybackServiceMediaPlayer mediaPlayer) { - PlaybackService.this.mediaPlayer = mediaPlayer; - } - - @Override - public PlaybackServiceMediaPlayer getMediaPlayer() { - return PlaybackService.this.mediaPlayer; - } - - @Override - public void setIsCasting(boolean isCasting) { - PlaybackService.isCasting = isCasting; - stateManager.validStartCommandWasReceived(); - } - - @Override - public void sendNotificationBroadcast(int type, int code) { - PlaybackService.this.sendNotificationBroadcast(type, code); - } - - @Override - public void saveCurrentPosition(boolean fromMediaPlayer, Playable playable, int position) { - PlaybackService.this.saveCurrentPosition(fromMediaPlayer, playable, position); - } - - @Override - public void setupNotification(boolean connected, PlaybackServiceMediaPlayer.PSMPInfo info) { - if (connected) { - PlaybackService.this.updateNotificationAndMediaSession(info.playable); - } else { - PlayerStatus status = info.playerStatus; - if ((status == PlayerStatus.PLAYING || - status == PlayerStatus.SEEKING || - status == PlayerStatus.PREPARING || - UserPreferences.isPersistNotify()) && - android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - PlaybackService.this.updateNotificationAndMediaSession(info.playable); - } else if (!UserPreferences.isPersistNotify()) { - stateManager.stopForeground(true); - } - } - } - - @Override - public MediaSessionCompat getMediaSession() { - return PlaybackService.this.mediaSession; - } - - @Override - public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) { - return PlaybackService.this.registerReceiver(receiver, filter); - } - - @Override - public void unregisterReceiver(BroadcastReceiver receiver) { - PlaybackService.this.unregisterReceiver(receiver); - } - }; } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java deleted file mode 100644 index e093383b9..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java +++ /dev/null @@ -1,386 +0,0 @@ -package de.danoeh.antennapod.core.service.playback; - -import android.content.Context; -import android.media.AudioManager; -import android.net.wifi.WifiManager; -import androidx.annotation.NonNull; -import androidx.annotation.StringRes; -import android.util.Log; -import android.util.Pair; -import android.view.SurfaceHolder; - -import java.util.List; -import java.util.concurrent.Future; - -import de.danoeh.antennapod.model.playback.MediaType; -import de.danoeh.antennapod.model.playback.Playable; - - -/* - * An inconvenience of an implementation like this is that some members and methods that once were - * private are now protected, allowing for access from classes of the same package, namely - * PlaybackService. A workaround would be to move this to a dedicated package. - */ -/** - * Abstract class that allows for different implementations of the PlaybackServiceMediaPlayer for local - * and remote (cast devices) playback. - */ -public abstract class PlaybackServiceMediaPlayer { - private static final String TAG = "PlaybackSvcMediaPlayer"; - - /** - * Return value of some PSMP methods if the method call failed. - */ - static final int INVALID_TIME = -1; - - private volatile PlayerStatus oldPlayerStatus; - volatile PlayerStatus playerStatus; - - /** - * A wifi-lock that is acquired if the media file is being streamed. - */ - private WifiManager.WifiLock wifiLock; - - final PSMPCallback callback; - final Context context; - - PlaybackServiceMediaPlayer(@NonNull Context context, - @NonNull PSMPCallback callback){ - this.context = context; - this.callback = callback; - - playerStatus = PlayerStatus.STOPPED; - } - - /** - * Starts or prepares playback of the specified Playable object. If another Playable object is already being played, the currently playing - * episode will be stopped and replaced with the new Playable object. If the Playable object is already being played, the method will - * not do anything. - * Whether playback starts immediately depends on the given parameters. See below for more details. - * <p/> - * States: - * During execution of the method, the object will be in the INITIALIZING state. The end state depends on the given parameters. - * <p/> - * If 'prepareImmediately' is set to true, the method will go into PREPARING state and after that into PREPARED state. If - * 'startWhenPrepared' is set to true, the method will additionally go into PLAYING state. - * <p/> - * If an unexpected error occurs while loading the Playable's metadata or while setting the MediaPlayers data source, the object - * will enter the ERROR state. - * <p/> - * This method is executed on an internal executor service. - * - * @param playable The Playable object that is supposed to be played. This parameter must not be null. - * @param stream The type of playback. If false, the Playable object MUST provide access to a locally available file via - * getLocalMediaUrl. If true, the Playable object MUST provide access to a resource that can be streamed by - * the Android MediaPlayer via getStreamUrl. - * @param startWhenPrepared Sets the 'startWhenPrepared' flag. This flag determines whether playback will start immediately after the - * episode has been prepared for playback. Setting this flag to true does NOT mean that the episode will be prepared - * for playback immediately (see 'prepareImmediately' parameter for more details) - * @param prepareImmediately Set to true if the method should also prepare the episode for playback. - */ - public abstract void playMediaObject(@NonNull Playable playable, boolean stream, boolean startWhenPrepared, boolean prepareImmediately); - - /** - * Resumes playback if the PSMP object is in PREPARED or PAUSED state. If the PSMP object is in an invalid state. - * nothing will happen. - * <p/> - * This method is executed on an internal executor service. - */ - public abstract void resume(); - - /** - * Saves the current position and pauses playback. Note that, if audiofocus - * is abandoned, the lockscreen controls will also disapear. - * <p/> - * This method is executed on an internal executor service. - * - * @param abandonFocus is true if the service should release audio focus - * @param reinit is true if service should reinit after pausing if the media - * file is being streamed - */ - public abstract void pause(boolean abandonFocus, boolean reinit); - - /** - * Prepared media player for playback if the service is in the INITALIZED - * state. - * <p/> - * This method is executed on an internal executor service. - */ - public abstract void prepare(); - - /** - * Resets the media player and moves it into INITIALIZED state. - * <p/> - * This method is executed on an internal executor service. - */ - public abstract void reinit(); - - /** - * Seeks to the specified position. If the PSMP object is in an invalid state, this method will do nothing. - * Invalid time values (< 0) will be ignored. - * <p/> - * This method is executed on an internal executor service. - */ - public abstract void seekTo(int t); - - /** - * Seek a specific position from the current position - * - * @param d offset from current position (positive or negative) - */ - public abstract void seekDelta(int d); - - /** - * Returns the duration of the current media object or INVALID_TIME if the duration could not be retrieved. - */ - public abstract int getDuration(); - - /** - * Returns the position of the current media object or INVALID_TIME if the position could not be retrieved. - */ - public abstract int getPosition(); - - public abstract boolean isStartWhenPrepared(); - - public abstract void setStartWhenPrepared(boolean startWhenPrepared); - - /** - * Sets the playback parameters. - * - Speed - * - SkipSilence (ExoPlayer only) - * This method is executed on an internal executor service. - */ - public abstract void setPlaybackParams(final float speed, final boolean skipSilence); - - /** - * Returns the current playback speed. If the playback speed could not be retrieved, 1 is returned. - */ - public abstract float getPlaybackSpeed(); - - /** - * Sets the playback volume. - * This method is executed on an internal executor service. - */ - public abstract void setVolume(float volumeLeft, float volumeRight); - - /** - * Returns true if the mediaplayer can mix stereo down to mono - */ - public abstract boolean canDownmix(); - - public abstract void setDownmix(boolean enable); - - public abstract MediaType getCurrentMediaType(); - - public abstract boolean isStreaming(); - - /** - * Releases internally used resources. This method should only be called when the object is not used anymore. - */ - public abstract void shutdown(); - - /** - * Releases internally used resources. This method should only be called when the object is not used anymore. - * This method is executed on an internal executor service. - */ - public abstract void shutdownQuietly(); - - public abstract void setVideoSurface(SurfaceHolder surface); - - public abstract void resetVideoSurface(); - - /** - * Return width and height of the currently playing video as a pair. - * - * @return Width and height as a Pair or null if the video size could not be determined. The method might still - * return an invalid non-null value if the getVideoWidth() and getVideoHeight() methods of the media player return - * invalid values. - */ - public abstract Pair<Integer, Integer> getVideoSize(); - - /** - * Returns a PSMInfo object that contains information about the current state of the PSMP object. - * - * @return The PSMPInfo object. - */ - public final synchronized PSMPInfo getPSMPInfo() { - return new PSMPInfo(oldPlayerStatus, playerStatus, getPlayable()); - } - - /** - * Returns the current status, if you need the media and the player status together, you should - * use getPSMPInfo() to make sure they're properly synchronized. Otherwise a race condition - * could result in nonsensical results (like a status of PLAYING, but a null playable) - * @return the current player status - */ - public synchronized PlayerStatus getPlayerStatus() { - return playerStatus; - } - - /** - * Returns the current media, if you need the media and the player status together, you should - * use getPSMPInfo() to make sure they're properly synchronized. Otherwise a race condition - * could result in nonsensical results (like a status of PLAYING, but a null playable) - * @return the current media. May be null - */ - public abstract Playable getPlayable(); - - protected abstract void setPlayable(Playable playable); - - public abstract List<String> getAudioTracks(); - - public abstract void setAudioTrack(int track); - - public abstract int getSelectedAudioTrack(); - - public void skip() { - endPlayback(false, true, true, true); - } - - /** - * Ends playback of current media (if any) and moves into INDETERMINATE state, unless - * {@param toStoppedState} is set to true, in which case it moves into STOPPED state. - * - * @see #endPlayback(boolean, boolean, boolean, boolean) - */ - public Future<?> stopPlayback(boolean toStoppedState) { - return endPlayback(false, false, false, toStoppedState); - } - - /** - * Internal method that handles end of playback. - * - * Currently, it has 5 use cases: - * <ul> - * <li>Media playback has completed: call with (true, false, true, true)</li> - * <li>User asks to skip to next episode: call with (false, true, true, true)</li> - * <li>Skipping to next episode due to playback error: call with (false, false, true, true)</li> - * <li>Stopping the media player: call with (false, false, false, true)</li> - * <li>We want to change the media player implementation: call with (false, false, false, false)</li> - * </ul> - * - * @param hasEnded If true, we assume the current media's playback has ended, for - * purposes of post playback processing. - * @param wasSkipped Whether the user chose to skip the episode (by pressing the skip - * button). - * @param shouldContinue If true, the media player should try to load, and possibly play, - * the next item, based on the user preferences and whether such item - * exists. - * @param toStoppedState If true, the playback state gets set to STOPPED if the media player - * is not loading/playing after this call, and the UI will reflect that. - * Only relevant if {@param shouldContinue} is set to false, otherwise - * this method's behavior defaults as if this parameter was true. - * - * @return a Future, just for the purpose of tracking its execution. - */ - protected abstract Future<?> endPlayback(boolean hasEnded, boolean wasSkipped, - boolean shouldContinue, boolean toStoppedState); - - /** - * @return {@code true} if the WifiLock feature should be used, {@code false} otherwise. - */ - protected abstract boolean shouldLockWifi(); - - final synchronized void acquireWifiLockIfNecessary() { - if (shouldLockWifi()) { - if (wifiLock == null) { - wifiLock = ((WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE)) - .createWifiLock(WifiManager.WIFI_MODE_FULL, TAG); - wifiLock.setReferenceCounted(false); - } - wifiLock.acquire(); - } - } - - final synchronized void releaseWifiLockIfNecessary() { - if (wifiLock != null && wifiLock.isHeld()) { - wifiLock.release(); - } - } - - /** - * Sets the player status of the PSMP object. PlayerStatus and media attributes have to be set at the same time - * so that getPSMPInfo can't return an invalid state (e.g. status is PLAYING, but media is null). - * <p/> - * This method will notify the callback about the change of the player status (even if the new status is the same - * as the old one). - * <p/> - * It will also call {@link PSMPCallback#onPlaybackPause(Playable, int)} or {@link PSMPCallback#onPlaybackStart(Playable, int)} - * depending on the status change. - * - * @param newStatus The new PlayerStatus. This must not be null. - * @param newMedia The new playable object of the PSMP object. This can be null. - * @param position The position to be set to the current Playable object in case playback started or paused. - * Will be ignored if given the value of {@link #INVALID_TIME}. - */ - final synchronized void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia, int position) { - Log.d(TAG, this.getClass().getSimpleName() + ": Setting player status to " + newStatus); - - this.oldPlayerStatus = playerStatus; - this.playerStatus = newStatus; - setPlayable(newMedia); - - if (newMedia != null && newStatus != PlayerStatus.INDETERMINATE) { - if (oldPlayerStatus == PlayerStatus.PLAYING && newStatus != PlayerStatus.PLAYING) { - callback.onPlaybackPause(newMedia, position); - } else if (oldPlayerStatus != PlayerStatus.PLAYING && newStatus == PlayerStatus.PLAYING) { - callback.onPlaybackStart(newMedia, position); - } - } - - callback.statusChanged(new PSMPInfo(oldPlayerStatus, playerStatus, getPlayable())); - } - - public boolean isAudioChannelInUse() { - AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - return (audioManager.getMode() != AudioManager.MODE_NORMAL || audioManager.isMusicActive()); - } - - /** - * @see #setPlayerStatus(PlayerStatus, Playable, int) - */ - final void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia) { - setPlayerStatus(newStatus, newMedia, INVALID_TIME); - } - - public interface PSMPCallback { - void statusChanged(PSMPInfo newInfo); - - void shouldStop(); - - void playbackSpeedChanged(float s); - - void onBufferingUpdate(int percent); - - void onMediaChanged(boolean reloadUI); - - boolean onMediaPlayerInfo(int code, @StringRes int resourceId); - - boolean onMediaPlayerError(Object inObj, int what, int extra); - - void onPostPlayback(@NonNull Playable media, boolean ended, boolean skipped, boolean playingNext); - - void onPlaybackStart(@NonNull Playable playable, int position); - - void onPlaybackPause(Playable playable, int position); - - Playable getNextInQueue(Playable currentMedia); - - void onPlaybackEnded(MediaType mediaType, boolean stopPlaying); - } - - /** - * Holds information about a PSMP object. - */ - public static class PSMPInfo { - public final PlayerStatus oldPlayerStatus; - public PlayerStatus playerStatus; - public Playable playable; - - PSMPInfo(PlayerStatus oldPlayerStatus, PlayerStatus playerStatus, Playable playable) { - this.oldPlayerStatus = oldPlayerStatus; - this.playerStatus = playerStatus; - this.playable = playable; - } - } -} 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 e7dea192a..c348f5773 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 @@ -31,17 +31,17 @@ import de.danoeh.antennapod.model.playback.Playable; import java.util.ArrayList; import java.util.concurrent.ExecutionException; +import de.danoeh.antennapod.playback.base.PlayerStatus; import org.apache.commons.lang3.ArrayUtils; public class PlaybackServiceNotificationBuilder { private static final String TAG = "PlaybackSrvNotification"; private static Bitmap defaultIcon = null; - private Context context; + private final Context context; private Playable playable; private MediaSessionCompat.Token mediaSessionToken; private PlayerStatus playerStatus; - private boolean isCasting; private Bitmap icon; private String position; @@ -140,7 +140,7 @@ public class PlaybackServiceNotificationBuilder { if (playable != null) { notification.setContentTitle(playable.getFeedTitle()); notification.setContentText(playable.getEpisodeTitle()); - addActions(notification, mediaSessionToken, playerStatus, isCasting); + addActions(notification, mediaSessionToken, playerStatus); if (icon != null) { notification.setLargeIcon(icon); @@ -170,26 +170,15 @@ public class PlaybackServiceNotificationBuilder { private PendingIntent getPlayerActivityPendingIntent() { return PendingIntent.getActivity(context, R.id.pending_intent_player_activity, - PlaybackService.getPlayerActivityIntent(context), PendingIntent.FLAG_UPDATE_CURRENT); + PlaybackService.getPlayerActivityIntent(context), PendingIntent.FLAG_UPDATE_CURRENT + | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); } private void addActions(NotificationCompat.Builder notification, MediaSessionCompat.Token mediaSessionToken, - PlayerStatus playerStatus, boolean isCasting) { + PlayerStatus playerStatus) { ArrayList<Integer> compactActionList = new ArrayList<>(); int numActions = 0; // we start and 0 and then increment by 1 for each call to addAction - - if (isCasting) { - Intent stopCastingIntent = new Intent(context, PlaybackService.class); - stopCastingIntent.putExtra(PlaybackService.EXTRA_CAST_DISCONNECT, true); - PendingIntent stopCastingPendingIntent = PendingIntent.getService(context, - numActions, stopCastingIntent, PendingIntent.FLAG_UPDATE_CURRENT); - notification.addAction(R.drawable.ic_notification_cast_off, - context.getString(R.string.cast_disconnect_label), - stopCastingPendingIntent); - numActions++; - } - // always let them rewind PendingIntent rewindButtonPendingIntent = getPendingIntentForMediaAction( KeyEvent.KEYCODE_MEDIA_REWIND, numActions); @@ -252,9 +241,11 @@ public class PlaybackServiceNotificationBuilder { intent.putExtra(MediaButtonReceiver.EXTRA_KEYCODE, keycodeValue); if (Build.VERSION.SDK_INT >= 26) { - return PendingIntent.getForegroundService(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT); + return PendingIntent.getForegroundService(context, requestCode, intent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); } else { - return PendingIntent.getService(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT); + return PendingIntent.getService(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT + | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); } } @@ -266,10 +257,6 @@ public class PlaybackServiceNotificationBuilder { this.playerStatus = playerStatus; } - public void setCasting(boolean casting) { - isCasting = casting; - } - public PlayerStatus getPlayerStatus() { return playerStatus; } 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 a14605e5b..9ca7b6647 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 @@ -7,6 +7,7 @@ import android.os.Vibrator; import androidx.annotation.NonNull; import android.util.Log; +import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent; import de.danoeh.antennapod.core.preferences.SleepTimerPreferences; import de.danoeh.antennapod.core.util.ChapterUtils; import de.danoeh.antennapod.core.widget.WidgetUpdater; @@ -22,10 +23,9 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; -import de.danoeh.antennapod.core.event.FeedItemEvent; -import de.danoeh.antennapod.core.event.QueueEvent; +import de.danoeh.antennapod.event.FeedItemEvent; +import de.danoeh.antennapod.event.QueueEvent; import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.model.playback.Playable; import io.reactivex.Completable; @@ -244,6 +244,7 @@ public class PlaybackServiceTaskManager { } sleepTimer = new SleepTimer(waitingTime); sleepTimerFuture = schedExecutor.schedule(sleepTimer, 0, TimeUnit.MILLISECONDS); + EventBus.getDefault().post(SleepTimerUpdatedEvent.justEnabled(waitingTime)); } /** @@ -349,10 +350,10 @@ public class PlaybackServiceTaskManager { * Cancels all tasks and shuts down the internal executor service of the PSTM. The object should not be used after * execution of this method. */ - public synchronized void shutdown() { + public void shutdown() { EventBus.getDefault().unregister(this); cancelAllTasks(); - schedExecutor.shutdown(); + schedExecutor.shutdownNow(); } private Runnable useMainThreadIfNecessary(Runnable runnable) { @@ -377,27 +378,11 @@ public class PlaybackServiceTaskManager { private final long waitingTime; private long timeLeft; private ShakeListener shakeListener; - private final Handler handler; public SleepTimer(long waitingTime) { super(); this.waitingTime = waitingTime; this.timeLeft = waitingTime; - - if (UserPreferences.useExoplayer() && Looper.myLooper() == Looper.getMainLooper()) { - // Run callbacks in main thread so they can call ExoPlayer methods themselves - this.handler = new Handler(Looper.getMainLooper()); - } else { - this.handler = null; - } - } - - private void postCallback(Runnable r) { - if (handler == null) { - r.run(); - } else { - handler.post(r); - } } @Override @@ -417,6 +402,7 @@ public class PlaybackServiceTaskManager { timeLeft -= now - lastTick; lastTick = now; + EventBus.getDefault().post(SleepTimerUpdatedEvent.updated(timeLeft)); if (timeLeft < NOTIFICATION_THRESHOLD) { Log.d(TAG, "Sleep timer is about to expire"); if (SleepTimerPreferences.vibrate() && !hasVibrated) { @@ -429,7 +415,6 @@ public class PlaybackServiceTaskManager { if (shakeListener == null && SleepTimerPreferences.shakeToReset()) { shakeListener = new ShakeListener(context, this); } - postCallback(() -> callback.onSleepTimerAlmostExpired(timeLeft)); } if (timeLeft <= 0) { Log.d(TAG, "Sleep timer expired"); @@ -438,11 +423,6 @@ public class PlaybackServiceTaskManager { shakeListener = null; } hasVibrated = false; - if (!Thread.currentThread().isInterrupted()) { - postCallback(callback::onSleepTimerExpired); - } else { - Log.d(TAG, "Sleep timer interrupted"); - } } } } @@ -452,10 +432,8 @@ public class PlaybackServiceTaskManager { } public void restart() { - postCallback(() -> { - setSleepTimer(waitingTime); - callback.onSleepTimerReset(); - }); + EventBus.getDefault().post(SleepTimerUpdatedEvent.cancelled()); + setSleepTimer(waitingTime); if (shakeListener != null) { shakeListener.pause(); shakeListener = null; @@ -467,19 +445,13 @@ public class PlaybackServiceTaskManager { if (shakeListener != null) { shakeListener.pause(); } - postCallback(callback::onSleepTimerReset); + EventBus.getDefault().post(SleepTimerUpdatedEvent.cancelled()); } } public interface PSTMCallback { void positionSaverTick(); - void onSleepTimerAlmostExpired(long timeLeft); - - void onSleepTimerExpired(); - - void onSleepTimerReset(); - WidgetUpdater.WidgetState requestWidgetState(); void onChapterLoaded(Playable media); diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdater.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdater.java index edb8bc3a9..43837a473 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdater.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdater.java @@ -4,6 +4,8 @@ import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.model.feed.FeedPreferences; import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting; import de.danoeh.antennapod.model.playback.Playable; +import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer; +import de.danoeh.antennapod.playback.base.PlayerStatus; class PlaybackVolumeUpdater { diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java deleted file mode 100644 index 4f2ae34f8..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java +++ /dev/null @@ -1,33 +0,0 @@ -package de.danoeh.antennapod.core.service.playback; - -public enum PlayerStatus { - INDETERMINATE(0), // player is currently changing its state, listeners should wait until the player has left this state. - ERROR(-1), - PREPARING(19), - PAUSED(30), - PLAYING(40), - STOPPED(5), - PREPARED(20), - SEEKING(29), - INITIALIZING(9), // playback service is loading the Playable's metadata - INITIALIZED(10); // playback service was started, data source of media player was set. - - private final int statusValue; - private static final PlayerStatus[] fromOrdinalLookup; - - static { - fromOrdinalLookup = PlayerStatus.values(); - } - - PlayerStatus(int val) { - statusValue = val; - } - - public static PlayerStatus fromOrdinal(int o) { - return fromOrdinalLookup[o]; - } - - public boolean isAtLeast(PlayerStatus other) { - return other == null || this.statusValue>=other.statusValue; - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java index b5202d79c..cf32eb838 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java @@ -35,7 +35,7 @@ public class AutomaticDownloadAlgorithm { return () -> { // true if we should auto download based on network status - boolean networkShouldAutoDl = NetworkUtils.autodownloadNetworkAvailable() + boolean networkShouldAutoDl = NetworkUtils.isAutoDownloadAllowed() && UserPreferences.isEnableAutodownload(); // true if we should auto download based on power status @@ -65,7 +65,7 @@ public class AutomaticDownloadAlgorithm { Iterator<FeedItem> it = candidates.iterator(); while (it.hasNext()) { FeedItem item = it.next(); - if (!item.isAutoDownloadable() || FeedItemUtil.isPlaying(item.getMedia()) + if (!item.isAutoDownloadable(System.currentTimeMillis()) || FeedItemUtil.isPlaying(item.getMedia()) || item.getFeed().isLocalFeed()) { it.remove(); } 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 49eca1027..f776fe111 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 @@ -575,7 +575,6 @@ public final class DBReader { @Nullable private static FeedItem getFeedItemByGuidOrEpisodeUrl(final String guid, final String episodeUrl, PodDBAdapter adapter) { - Log.d(TAG, "Loading feeditem with guid " + guid + " or episode url " + episodeUrl); try (Cursor cursor = adapter.getFeedItemCursor(guid, episodeUrl)) { if (!cursor.moveToNext()) { return null; @@ -633,8 +632,6 @@ public final class DBReader { * Does NOT load additional attributes like feed or queue state. */ public static FeedItem getFeedItemByGuidOrEpisodeUrl(final String guid, final String episodeUrl) { - Log.d(TAG, "getFeedItem() called with: " + "guid = [" + guid + "], episodeUrl = [" + episodeUrl + "]"); - PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); try { @@ -881,7 +878,7 @@ public final class DBReader { int numDownloadedItems = adapter.getNumberOfDownloadedEpisodes(); List<NavDrawerData.DrawerItem> items = new ArrayList<>(); - Map<String, NavDrawerData.FolderDrawerItem> folders = new HashMap<>(); + Map<String, NavDrawerData.TagDrawerItem> folders = new HashMap<>(); for (Feed feed : feeds) { for (String tag : feed.getPreferences().getTags()) { NavDrawerData.FeedDrawerItem drawerItem = new NavDrawerData.FeedDrawerItem(feed, feed.getId(), @@ -890,18 +887,18 @@ public final class DBReader { items.add(drawerItem); continue; } - NavDrawerData.FolderDrawerItem folder; + NavDrawerData.TagDrawerItem folder; if (folders.containsKey(tag)) { folder = folders.get(tag); } else { - folder = new NavDrawerData.FolderDrawerItem(tag); + folder = new NavDrawerData.TagDrawerItem(tag); folders.put(tag, folder); } drawerItem.id |= folder.id; folder.children.add(drawerItem); } } - List<NavDrawerData.FolderDrawerItem> foldersSorted = new ArrayList<>(folders.values()); + List<NavDrawerData.TagDrawerItem> foldersSorted = new ArrayList<>(folders.values()); Collections.sort(foldersSorted, (o1, o2) -> o1.getTitle().compareToIgnoreCase(o2.getTitle())); items.addAll(foldersSorted); 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 a0c1e54ad..412914329 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 @@ -1,5 +1,7 @@ package de.danoeh.antennapod.core.storage; +import static android.content.Context.MODE_PRIVATE; + import android.content.Context; import android.content.SharedPreferences; import android.database.Cursor; @@ -9,22 +11,6 @@ import android.util.Log; import androidx.annotation.VisibleForTesting; -import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.core.event.FeedItemEvent; -import de.danoeh.antennapod.core.event.FeedListUpdateEvent; -import de.danoeh.antennapod.core.event.MessageEvent; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.core.feed.LocalFeedUpdater; -import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.service.download.DownloadStatus; -import de.danoeh.antennapod.core.storage.mapper.FeedCursorMapper; -import de.danoeh.antennapod.core.sync.SyncService; -import de.danoeh.antennapod.core.util.DownloadError; -import de.danoeh.antennapod.core.util.LongList; -import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator; -import de.danoeh.antennapod.net.sync.model.EpisodeAction; import org.greenrobot.eventbus.EventBus; import java.util.ArrayList; @@ -41,7 +27,23 @@ import java.util.concurrent.Future; import java.util.concurrent.FutureTask; import java.util.concurrent.atomic.AtomicBoolean; -import static android.content.Context.MODE_PRIVATE; +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.event.FeedItemEvent; +import de.danoeh.antennapod.event.FeedListUpdateEvent; +import de.danoeh.antennapod.event.MessageEvent; +import de.danoeh.antennapod.core.feed.LocalFeedUpdater; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.service.download.DownloadStatus; +import de.danoeh.antennapod.core.storage.mapper.FeedCursorMapper; +import de.danoeh.antennapod.core.sync.SyncService; +import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink; +import de.danoeh.antennapod.core.util.DownloadError; +import de.danoeh.antennapod.core.util.LongList; +import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.net.sync.model.EpisodeAction; /** * Provides methods for doing common tasks that use DBReader and DBWriter. @@ -462,7 +464,7 @@ public final class DBTasks { .position(oldItem.getMedia().getDuration() / 1000) .total(oldItem.getMedia().getDuration() / 1000) .build(); - SyncService.enqueueEpisodeAction(context, action); + SynchronizationQueueSink.enqueueEpisodeActionIfSynchronizationIsActive(context, action); } } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBUpgrader.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBUpgrader.java index 46ab7502b..4e0a6aeda 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBUpgrader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBUpgrader.java @@ -73,7 +73,7 @@ class DBUpgrader { } if (oldVersion <= 9) { db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS - + " ADD COLUMN " + PodDBAdapter.KEY_AUTO_DOWNLOAD + + " ADD COLUMN " + PodDBAdapter.KEY_AUTO_DOWNLOAD_ENABLED + " INTEGER DEFAULT 1"); } if (oldVersion <= 10) { @@ -121,10 +121,10 @@ class DBUpgrader { } if (oldVersion <= 14) { db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS - + " ADD COLUMN " + PodDBAdapter.KEY_AUTO_DOWNLOAD + " INTEGER"); + + " ADD COLUMN " + PodDBAdapter.KEY_AUTO_DOWNLOAD_ATTEMPTS + " INTEGER"); db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS - + " SET " + PodDBAdapter.KEY_AUTO_DOWNLOAD + " = " - + "(SELECT " + PodDBAdapter.KEY_AUTO_DOWNLOAD + + " SET " + PodDBAdapter.KEY_AUTO_DOWNLOAD_ATTEMPTS + " = " + + "(SELECT " + PodDBAdapter.KEY_AUTO_DOWNLOAD_ENABLED + " FROM " + PodDBAdapter.TABLE_NAME_FEEDS + " WHERE " + PodDBAdapter.TABLE_NAME_FEEDS + "." + PodDBAdapter.KEY_ID + " = " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + PodDBAdapter.KEY_FEED + ")"); @@ -322,6 +322,10 @@ class DBUpgrader { db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + " ADD COLUMN " + PodDBAdapter.KEY_FEED_TAGS + " TEXT;"); } + if (oldVersion < 2050000) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + PodDBAdapter.KEY_MINIMAL_DURATION_FILTER + " INTEGER DEFAULT -1"); + } } } 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 34ea5e207..280a2f118 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 @@ -6,9 +6,8 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.app.NotificationManagerCompat; -import de.danoeh.antennapod.core.sync.SyncService; -import de.danoeh.antennapod.net.sync.model.EpisodeAction; import org.greenrobot.eventbus.EventBus; import java.io.File; @@ -25,30 +24,31 @@ import java.util.concurrent.TimeUnit; import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.event.DownloadLogEvent; -import de.danoeh.antennapod.core.event.FavoritesEvent; -import de.danoeh.antennapod.core.event.FeedItemEvent; -import de.danoeh.antennapod.core.event.FeedListUpdateEvent; -import de.danoeh.antennapod.core.event.MessageEvent; -import de.danoeh.antennapod.core.event.PlaybackHistoryEvent; -import de.danoeh.antennapod.core.event.QueueEvent; -import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent; -import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.event.FavoritesEvent; +import de.danoeh.antennapod.event.FeedItemEvent; +import de.danoeh.antennapod.event.FeedListUpdateEvent; +import de.danoeh.antennapod.event.MessageEvent; +import de.danoeh.antennapod.event.playback.PlaybackHistoryEvent; +import de.danoeh.antennapod.event.QueueEvent; +import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; import de.danoeh.antennapod.core.feed.FeedEvent; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.model.feed.FeedPreferences; -import de.danoeh.antennapod.core.preferences.GpodnetPreferences; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.service.playback.PlaybackService; +import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink; import de.danoeh.antennapod.core.util.FeedItemPermutors; import de.danoeh.antennapod.core.util.IntentUtils; import de.danoeh.antennapod.core.util.LongList; import de.danoeh.antennapod.core.util.Permutor; +import de.danoeh.antennapod.core.util.playback.PlayableUtils; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.model.feed.FeedPreferences; import de.danoeh.antennapod.model.feed.SortOrder; import de.danoeh.antennapod.model.playback.Playable; -import de.danoeh.antennapod.core.util.playback.PlayableUtils; +import de.danoeh.antennapod.net.sync.model.EpisodeAction; /** * Provides methods for writing data to AntennaPod's database. @@ -129,16 +129,17 @@ public class DBWriter { if (media.getId() == PlaybackPreferences.getCurrentlyPlayingFeedMediaId()) { PlaybackPreferences.writeNoMediaPlaying(); IntentUtils.sendLocalBroadcast(context, PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE); + + NotificationManagerCompat nm = NotificationManagerCompat.from(context); + nm.cancel(R.id.notification_playing); } // Gpodder: queue delete action for synchronization - if (GpodnetPreferences.loggedIn()) { - FeedItem item = media.getItem(); - EpisodeAction action = new EpisodeAction.Builder(item, EpisodeAction.DELETE) - .currentTimestamp() - .build(); - SyncService.enqueueEpisodeAction(context, action); - } + FeedItem item = media.getItem(); + EpisodeAction action = new EpisodeAction.Builder(item, EpisodeAction.DELETE) + .currentTimestamp() + .build(); + SynchronizationQueueSink.enqueueEpisodeActionIfSynchronizationIsActive(context, action); } EventBus.getDefault().post(FeedItemEvent.deletedMedia(Collections.singletonList(media.getItem()))); return true; @@ -152,7 +153,6 @@ public class DBWriter { */ public static Future<?> deleteFeed(final Context context, final long feedId) { return dbExec.submit(() -> { - DownloadRequester requester = DownloadRequester.getInstance(); final Feed feed = DBReader.getFeed(feedId); if (feed == null) { return; @@ -170,7 +170,9 @@ public class DBWriter { adapter.removeFeed(feed); adapter.close(); - SyncService.enqueueFeedRemoved(context, feed.getDownload_url()); + if (!feed.isLocalFeed()) { + SynchronizationQueueSink.enqueueFeedRemovedIfSynchronizationIsActive(context, feed.getDownload_url()); + } EventBus.getDefault().post(new FeedListUpdateEvent(feed)); }); } @@ -782,7 +784,9 @@ public class DBWriter { adapter.close(); for (Feed feed : feeds) { - SyncService.enqueueFeedAdded(context, feed.getDownload_url()); + if (!feed.isLocalFeed()) { + SynchronizationQueueSink.enqueueFeedAddedIfSynchronizationIsActive(context, feed.getDownload_url()); + } } BackupManager backupManager = new BackupManager(context); @@ -953,25 +957,6 @@ public class DBWriter { }); } - public static Future<?> saveFeedItemAutoDownloadFailed(final FeedItem feedItem) { - return dbExec.submit(() -> { - int failedAttempts = feedItem.getFailedAutoDownloadAttempts() + 1; - long autoDownload; - if (!feedItem.getAutoDownload() || failedAttempts >= 10) { - autoDownload = 0; // giving up, disable auto download - feedItem.setAutoDownload(false); - } else { - long now = System.currentTimeMillis(); - autoDownload = (now / 10) * 10 + failedAttempts; - } - final PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setFeedItemAutoDownload(feedItem, autoDownload); - adapter.close(); - EventBus.getDefault().post(new UnreadItemsUpdateEvent()); - }); - } - /** * Set filter of the feed * diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/NavDrawerData.java b/core/src/main/java/de/danoeh/antennapod/core/storage/NavDrawerData.java index 7ca90d687..1ec58216a 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/NavDrawerData.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/NavDrawerData.java @@ -30,7 +30,7 @@ public class NavDrawerData { public abstract static class DrawerItem { public enum Type { - FOLDER, FEED + TAG, FEED } public final Type type; @@ -55,14 +55,14 @@ public class NavDrawerData { public abstract int getCounter(); } - public static class FolderDrawerItem extends DrawerItem { + public static class TagDrawerItem extends DrawerItem { public final List<DrawerItem> children = new ArrayList<>(); public final String name; public boolean isOpen; - public FolderDrawerItem(String name) { + public TagDrawerItem(String name) { // Keep IDs >0 but make room for many feeds - super(DrawerItem.Type.FOLDER, Math.abs((long) name.hashCode()) << 20); + super(DrawerItem.Type.TAG, Math.abs((long) name.hashCode()) << 20); this.name = name; } 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 85ce2dc99..b7e221a33 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 @@ -44,8 +44,6 @@ import de.danoeh.antennapod.model.feed.SortOrder; import static de.danoeh.antennapod.model.feed.FeedPreferences.SPEED_USE_GLOBAL; import static de.danoeh.antennapod.model.feed.SortOrder.toCodeString; -// TODO Remove media column from feeditem table - /** * Implements methods for accessing the database */ @@ -53,7 +51,7 @@ public class PodDBAdapter { private static final String TAG = "PodDBAdapter"; public static final String DATABASE_NAME = "Antennapod.db"; - public static final int VERSION = 2030000; + public static final int VERSION = 2050000; /** * Maximum number of arguments for IN-operator. @@ -97,7 +95,8 @@ public class PodDBAdapter { public static final String KEY_DOWNLOADSTATUS_TITLE = "title"; public static final String KEY_CHAPTER_TYPE = "type"; public static final String KEY_PLAYBACK_COMPLETION_DATE = "playback_completion_date"; - public static final String KEY_AUTO_DOWNLOAD = "auto_download"; + public static final String KEY_AUTO_DOWNLOAD_ATTEMPTS = "auto_download"; + public static final String KEY_AUTO_DOWNLOAD_ENABLED = "auto_download"; // Both tables use the same key public static final String KEY_KEEP_UPDATED = "keep_updated"; public static final String KEY_AUTO_DELETE_ACTION = "auto_delete_action"; public static final String KEY_FEED_VOLUME_ADAPTION = "feed_volume_adaption"; @@ -113,6 +112,7 @@ public class PodDBAdapter { public static final String KEY_LAST_PLAYED_TIME = "last_played_time"; public static final String KEY_INCLUDE_FILTER = "include_filter"; public static final String KEY_EXCLUDE_FILTER = "exclude_filter"; + public static final String KEY_MINIMAL_DURATION_FILTER = "minimal_duration_filter"; public static final String KEY_FEED_PLAYBACK_SPEED = "feed_playback_speed"; public static final String KEY_FEED_SKIP_INTRO = "feed_skip_intro"; public static final String KEY_FEED_SKIP_ENDING = "feed_skip_ending"; @@ -140,11 +140,12 @@ public class PodDBAdapter { + KEY_DESCRIPTION + " TEXT," + KEY_PAYMENT_LINK + " TEXT," + KEY_LASTUPDATE + " TEXT," + KEY_LANGUAGE + " TEXT," + KEY_AUTHOR + " TEXT," + KEY_IMAGE_URL + " TEXT," + KEY_TYPE + " TEXT," - + KEY_FEED_IDENTIFIER + " TEXT," + KEY_AUTO_DOWNLOAD + " INTEGER DEFAULT 1," + + KEY_FEED_IDENTIFIER + " TEXT," + KEY_AUTO_DOWNLOAD_ENABLED + " INTEGER DEFAULT 1," + KEY_USERNAME + " TEXT," + KEY_PASSWORD + " TEXT," + KEY_INCLUDE_FILTER + " TEXT DEFAULT ''," + KEY_EXCLUDE_FILTER + " TEXT DEFAULT ''," + + KEY_MINIMAL_DURATION_FILTER + " INTEGER DEFAULT -1," + KEY_KEEP_UPDATED + " INTEGER DEFAULT 1," + KEY_IS_PAGED + " INTEGER DEFAULT 0," + KEY_NEXT_PAGE_LINK + " TEXT," @@ -167,7 +168,7 @@ public class PodDBAdapter { + KEY_MEDIA + " INTEGER," + KEY_FEED + " INTEGER," + KEY_HAS_CHAPTERS + " INTEGER," + KEY_ITEM_IDENTIFIER + " TEXT," + KEY_IMAGE_URL + " TEXT," - + KEY_AUTO_DOWNLOAD + " INTEGER)"; + + KEY_AUTO_DOWNLOAD_ATTEMPTS + " INTEGER)"; private static final String CREATE_TABLE_FEED_MEDIA = "CREATE TABLE " + TABLE_NAME_FEED_MEDIA + " (" + TABLE_PRIMARY_KEY + KEY_DURATION @@ -244,7 +245,7 @@ public class PodDBAdapter { TABLE_NAME_FEEDS + "." + KEY_IMAGE_URL, TABLE_NAME_FEEDS + "." + KEY_TYPE, TABLE_NAME_FEEDS + "." + KEY_FEED_IDENTIFIER, - TABLE_NAME_FEEDS + "." + KEY_AUTO_DOWNLOAD, + TABLE_NAME_FEEDS + "." + KEY_AUTO_DOWNLOAD_ENABLED, TABLE_NAME_FEEDS + "." + KEY_KEEP_UPDATED, TABLE_NAME_FEEDS + "." + KEY_IS_PAGED, TABLE_NAME_FEEDS + "." + KEY_NEXT_PAGE_LINK, @@ -257,6 +258,7 @@ public class PodDBAdapter { TABLE_NAME_FEEDS + "." + KEY_FEED_VOLUME_ADAPTION, TABLE_NAME_FEEDS + "." + KEY_INCLUDE_FILTER, TABLE_NAME_FEEDS + "." + KEY_EXCLUDE_FILTER, + TABLE_NAME_FEEDS + "." + KEY_MINIMAL_DURATION_FILTER, TABLE_NAME_FEEDS + "." + KEY_FEED_PLAYBACK_SPEED, TABLE_NAME_FEEDS + "." + KEY_FEED_TAGS, TABLE_NAME_FEEDS + "." + KEY_FEED_SKIP_INTRO, @@ -292,7 +294,7 @@ public class PodDBAdapter { + TABLE_NAME_FEED_ITEMS + "." + KEY_HAS_CHAPTERS + ", " + TABLE_NAME_FEED_ITEMS + "." + KEY_ITEM_IDENTIFIER + ", " + TABLE_NAME_FEED_ITEMS + "." + KEY_IMAGE_URL + ", " - + TABLE_NAME_FEED_ITEMS + "." + KEY_AUTO_DOWNLOAD; + + TABLE_NAME_FEED_ITEMS + "." + KEY_AUTO_DOWNLOAD_ATTEMPTS; private static final String KEYS_FEED_MEDIA = TABLE_NAME_FEED_MEDIA + "." + KEY_ID + " AS " + SELECT_KEY_MEDIA_ID + ", " @@ -442,7 +444,7 @@ public class PodDBAdapter { throw new IllegalArgumentException("Feed ID of preference must not be null"); } ContentValues values = new ContentValues(); - values.put(KEY_AUTO_DOWNLOAD, prefs.getAutoDownload()); + values.put(KEY_AUTO_DOWNLOAD_ENABLED, prefs.getAutoDownload()); values.put(KEY_KEEP_UPDATED, prefs.getKeepUpdated()); values.put(KEY_AUTO_DELETE_ACTION, prefs.getAutoDeleteAction().ordinal()); values.put(KEY_FEED_VOLUME_ADAPTION, prefs.getVolumeAdaptionSetting().toInteger()); @@ -450,6 +452,7 @@ public class PodDBAdapter { values.put(KEY_PASSWORD, prefs.getPassword()); values.put(KEY_INCLUDE_FILTER, prefs.getFilter().getIncludeFilter()); values.put(KEY_EXCLUDE_FILTER, prefs.getFilter().getExcludeFilter()); + values.put(KEY_MINIMAL_DURATION_FILTER, prefs.getFilter().getMinimalDurationFilter()); values.put(KEY_FEED_PLAYBACK_SPEED, prefs.getFeedPlaybackSpeed()); values.put(KEY_FEED_TAGS, prefs.getTagsAsString()); values.put(KEY_FEED_SKIP_INTRO, prefs.getFeedSkipIntro()); @@ -645,7 +648,7 @@ public class PodDBAdapter { } values.put(KEY_HAS_CHAPTERS, item.getChapters() != null || item.hasChapters()); values.put(KEY_ITEM_IDENTIFIER, item.getItemIdentifier()); - values.put(KEY_AUTO_DOWNLOAD, item.getAutoDownload()); + values.put(KEY_AUTO_DOWNLOAD_ATTEMPTS, item.getAutoDownloadAttemptsAndTime()); values.put(KEY_IMAGE_URL, item.getImageUrl()); if (item.getId() == 0) { @@ -761,13 +764,6 @@ public class PodDBAdapter { return status.getId(); } - public void setFeedItemAutoDownload(FeedItem feedItem, long autoDownload) { - ContentValues values = new ContentValues(); - values.put(KEY_AUTO_DOWNLOAD, autoDownload); - db.update(TABLE_NAME_FEED_ITEMS, values, KEY_ID + "=?", - new String[]{String.valueOf(feedItem.getId())}); - } - public void setFavorites(List<FeedItem> favorites) { ContentValues values = new ContentValues(); try { @@ -844,43 +840,32 @@ public class PodDBAdapter { db.delete(TABLE_NAME_QUEUE, null, null); } - private void removeFeedMedia(FeedMedia media) { - // delete download log entries for feed media - db.delete(TABLE_NAME_DOWNLOAD_LOG, KEY_FEEDFILE + "=? AND " + KEY_FEEDFILETYPE + "=?", - new String[]{String.valueOf(media.getId()), String.valueOf(FeedMedia.FEEDFILETYPE_FEEDMEDIA)}); - - db.delete(TABLE_NAME_FEED_MEDIA, KEY_ID + "=?", - new String[]{String.valueOf(media.getId())}); - } - - private void removeChaptersOfItem(FeedItem item) { - db.delete(TABLE_NAME_SIMPLECHAPTERS, KEY_FEEDITEM + "=?", - new String[]{String.valueOf(item.getId())}); - } - - /** - * Remove a FeedItem and its FeedMedia entry. - */ - private void removeFeedItem(FeedItem item) { - if (item.getMedia() != null) { - removeFeedMedia(item.getMedia()); - } - if (item.hasChapters() || item.getChapters() != null) { - removeChaptersOfItem(item); - } - db.delete(TABLE_NAME_FEED_ITEMS, KEY_ID + "=?", - new String[]{String.valueOf(item.getId())}); - } - /** * Remove the listed items and their FeedMedia entries. */ public void removeFeedItems(@NonNull List<FeedItem> items) { try { - db.beginTransactionNonExclusive(); + StringBuilder mediaIds = new StringBuilder(); + StringBuilder itemIds = new StringBuilder(); for (FeedItem item : items) { - removeFeedItem(item); + if (item.getMedia() != null) { + if (mediaIds.length() != 0) { + mediaIds.append(","); + } + mediaIds.append(item.getMedia().getId()); + } + if (itemIds.length() != 0) { + itemIds.append(","); + } + itemIds.append(item.getId()); } + + db.beginTransactionNonExclusive(); + db.delete(TABLE_NAME_SIMPLECHAPTERS, KEY_FEEDITEM + " IN (" + itemIds + ")", null); + db.delete(TABLE_NAME_DOWNLOAD_LOG, KEY_FEEDFILETYPE + "=" + FeedMedia.FEEDFILETYPE_FEEDMEDIA + + " AND " + KEY_FEEDFILE + " IN (" + mediaIds + ")", null); + db.delete(TABLE_NAME_FEED_MEDIA, KEY_ID + " IN (" + mediaIds + ")", null); + db.delete(TABLE_NAME_FEED_ITEMS, KEY_ID + " IN (" + itemIds + ")", null); db.setTransactionSuccessful(); } catch (SQLException e) { Log.e(TAG, Log.getStackTraceString(e)); @@ -896,9 +881,7 @@ public class PodDBAdapter { try { db.beginTransactionNonExclusive(); if (feed.getItems() != null) { - for (FeedItem item : feed.getItems()) { - removeFeedItem(item); - } + removeFeedItems(feed.getItems()); } // delete download log entries for feed db.delete(TABLE_NAME_DOWNLOAD_LOG, KEY_FEEDFILE + "=? AND " + KEY_FEEDFILETYPE + "=?", @@ -1123,7 +1106,6 @@ public class PodDBAdapter { + " INNER JOIN " + TABLE_NAME_FEEDS + " ON " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + TABLE_NAME_FEEDS + "." + KEY_ID + " WHERE " + whereClauseCondition; - Log.d(TAG, "SQL: " + query); return db.rawQuery(query, null); } @@ -1362,25 +1344,7 @@ public class PodDBAdapter { } /** - * Select number of items, new items, the date of the latest episode and the number of episodes in progress. The result - * is sorted by the title of the feed. - */ - private static final String FEED_STATISTICS_QUERY = "SELECT Feeds.id, num_items, new_items, latest_episode, in_progress FROM " + - " Feeds LEFT JOIN " + - "(SELECT feed,count(*) AS num_items," + - " COUNT(CASE WHEN read=0 THEN 1 END) AS new_items," + - " MAX(pubDate) AS latest_episode," + - " COUNT(CASE WHEN position>0 THEN 1 END) AS in_progress," + - " COUNT(CASE WHEN downloaded=1 THEN 1 END) AS episodes_downloaded " + - " FROM FeedItems LEFT JOIN FeedMedia ON FeedItems.id=FeedMedia.feeditem GROUP BY FeedItems.feed)" + - " ON Feeds.id = feed ORDER BY Feeds.title COLLATE NOCASE ASC;"; - - public Cursor getFeedStatisticsCursor() { - return db.rawQuery(FEED_STATISTICS_QUERY, null); - } - - /** - * Insert raw data to the database. * + * Insert raw data to the database. * Call method only for unit tests. */ @VisibleForTesting(otherwise = VisibleForTesting.NONE) diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedItemCursorMapper.java b/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedItemCursorMapper.java index 19695ca95..ca0834339 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedItemCursorMapper.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedItemCursorMapper.java @@ -25,7 +25,7 @@ public abstract class FeedItemCursorMapper { int indexHasChapters = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_HAS_CHAPTERS); int indexRead = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_READ); int indexItemIdentifier = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_ITEM_IDENTIFIER); - int indexAutoDownload = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_AUTO_DOWNLOAD); + int indexAutoDownload = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_AUTO_DOWNLOAD_ATTEMPTS); int indexImageUrl = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_IMAGE_URL); long id = cursor.getInt(indexId); diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedMediaCursorMapper.java b/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedMediaCursorMapper.java index 608fce5c4..0dc3dc231 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedMediaCursorMapper.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedMediaCursorMapper.java @@ -36,7 +36,7 @@ public abstract class FeedMediaCursorMapper { } Boolean hasEmbeddedPicture; - switch (cursor.getInt(cursor.getColumnIndex(PodDBAdapter.KEY_HAS_EMBEDDED_PICTURE))) { + switch (cursor.getInt(cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_HAS_EMBEDDED_PICTURE))) { case 1: hasEmbeddedPicture = Boolean.TRUE; break; diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedPreferencesCursorMapper.java b/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedPreferencesCursorMapper.java index cab6ea618..f062609b6 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedPreferencesCursorMapper.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedPreferencesCursorMapper.java @@ -21,7 +21,7 @@ public abstract class FeedPreferencesCursorMapper { @NonNull public static FeedPreferences convert(@NonNull Cursor cursor) { int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID); - int indexAutoDownload = cursor.getColumnIndex(PodDBAdapter.KEY_AUTO_DOWNLOAD); + int indexAutoDownload = cursor.getColumnIndex(PodDBAdapter.KEY_AUTO_DOWNLOAD_ENABLED); int indexAutoRefresh = cursor.getColumnIndex(PodDBAdapter.KEY_KEEP_UPDATED); int indexAutoDeleteAction = cursor.getColumnIndex(PodDBAdapter.KEY_AUTO_DELETE_ACTION); int indexVolumeAdaption = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_VOLUME_ADAPTION); @@ -29,6 +29,7 @@ public abstract class FeedPreferencesCursorMapper { int indexPassword = cursor.getColumnIndex(PodDBAdapter.KEY_PASSWORD); int indexIncludeFilter = cursor.getColumnIndex(PodDBAdapter.KEY_INCLUDE_FILTER); int indexExcludeFilter = cursor.getColumnIndex(PodDBAdapter.KEY_EXCLUDE_FILTER); + int indexMinimalDurationFilter = cursor.getColumnIndex(PodDBAdapter.KEY_MINIMAL_DURATION_FILTER); int indexFeedPlaybackSpeed = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_PLAYBACK_SPEED); int indexAutoSkipIntro = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_SKIP_INTRO); int indexAutoSkipEnding = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_SKIP_ENDING); @@ -47,6 +48,7 @@ public abstract class FeedPreferencesCursorMapper { String password = cursor.getString(indexPassword); String includeFilter = cursor.getString(indexIncludeFilter); String excludeFilter = cursor.getString(indexExcludeFilter); + int minimalDurationFilter = cursor.getInt(indexMinimalDurationFilter); float feedPlaybackSpeed = cursor.getFloat(indexFeedPlaybackSpeed); int feedAutoSkipIntro = cursor.getInt(indexAutoSkipIntro); int feedAutoSkipEnding = cursor.getInt(indexAutoSkipEnding); @@ -62,7 +64,7 @@ public abstract class FeedPreferencesCursorMapper { volumeAdaptionSetting, username, password, - new FeedFilter(includeFilter, excludeFilter), + new FeedFilter(includeFilter, excludeFilter, minimalDurationFilter), feedPlaybackSpeed, feedAutoSkipIntro, feedAutoSkipEnding, diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/EpisodeActionFilter.java b/core/src/main/java/de/danoeh/antennapod/core/sync/EpisodeActionFilter.java index c74356d98..184f24793 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/sync/EpisodeActionFilter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/sync/EpisodeActionFilter.java @@ -22,7 +22,6 @@ public class EpisodeActionFilter { Map<Pair<String, String>, EpisodeAction> localMostRecentPlayActions = createUniqueLocalMostRecentPlayActions(queuedEpisodeActions); for (EpisodeAction remoteAction : remoteActions) { - Log.d(TAG, "Processing remoteAction: " + remoteAction.toString()); Pair<String, String> key = new Pair<>(remoteAction.getPodcast(), remoteAction.getEpisode()); switch (remoteAction.getAction()) { case NEW: diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/LockingAsyncExecutor.java b/core/src/main/java/de/danoeh/antennapod/core/sync/LockingAsyncExecutor.java new file mode 100644 index 000000000..e7dbbbd3c --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/sync/LockingAsyncExecutor.java @@ -0,0 +1,35 @@ +package de.danoeh.antennapod.core.sync; + +import java.util.concurrent.locks.ReentrantLock; + +import io.reactivex.Completable; +import io.reactivex.schedulers.Schedulers; + +public class LockingAsyncExecutor { + + static final ReentrantLock lock = new ReentrantLock(); + + /** + * Take the lock and execute runnable (to prevent changes to preferences being lost when enqueueing while sync is + * in progress). If the lock is free, the runnable is directly executed in the calling thread to prevent overhead. + */ + public static void executeLockedAsync(Runnable runnable) { + if (lock.tryLock()) { + try { + runnable.run(); + } finally { + lock.unlock(); + } + } else { + Completable.fromRunnable(() -> { + lock.lock(); + try { + runnable.run(); + } finally { + lock.unlock(); + } + }).subscribeOn(Schedulers.io()) + .subscribe(); + } + } +} 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 9803a29db..82896382d 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 @@ -5,7 +5,7 @@ import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; +import android.os.Build; import android.util.Log; import androidx.annotation.NonNull; @@ -20,12 +20,16 @@ import androidx.work.WorkManager; import androidx.work.Worker; import androidx.work.WorkerParameters; +import org.apache.commons.lang3.StringUtils; +import org.greenrobot.eventbus.EventBus; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.core.event.SyncServiceEvent; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.core.preferences.GpodnetPreferences; +import de.danoeh.antennapod.event.SyncServiceEvent; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; import de.danoeh.antennapod.core.storage.DBReader; @@ -33,10 +37,14 @@ import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.DownloadRequestException; import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueStorage; import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.core.util.LongList; import de.danoeh.antennapod.core.util.URLChecker; import de.danoeh.antennapod.core.util.gui.NotificationUtils; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.net.sync.gpoddernet.GpodnetService; import de.danoeh.antennapod.net.sync.model.EpisodeAction; import de.danoeh.antennapod.net.sync.model.EpisodeActionChanges; @@ -44,162 +52,67 @@ import de.danoeh.antennapod.net.sync.model.ISyncService; import de.danoeh.antennapod.net.sync.model.SubscriptionChanges; import de.danoeh.antennapod.net.sync.model.SyncServiceException; import de.danoeh.antennapod.net.sync.model.UploadChangesResponse; -import io.reactivex.Completable; -import io.reactivex.schedulers.Schedulers; - -import org.apache.commons.lang3.StringUtils; -import org.greenrobot.eventbus.EventBus; -import org.json.JSONArray; -import org.json.JSONException; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.ReentrantLock; +import de.danoeh.antennapod.net.sync.nextcloud.NextcloudSyncService; public class SyncService extends Worker { - private static final String PREF_NAME = "SyncService"; - private static final String PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP = "last_sync_timestamp"; - private static final String PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP = "last_episode_actions_sync_timestamp"; - private static final String PREF_QUEUED_FEEDS_ADDED = "sync_added"; - private static final String PREF_QUEUED_FEEDS_REMOVED = "sync_removed"; - private static final String PREF_QUEUED_EPISODE_ACTIONS = "sync_queued_episode_actions"; - private static final String PREF_LAST_SYNC_ATTEMPT_TIMESTAMP = "last_sync_attempt_timestamp"; - private static final String PREF_LAST_SYNC_ATTEMPT_SUCCESS = "last_sync_attempt_success"; - private static final String TAG = "SyncService"; + public static final String TAG = "SyncService"; private static final String WORK_ID_SYNC = "SyncServiceWorkId"; - private static final ReentrantLock lock = new ReentrantLock(); - private ISyncService syncServiceImpl; + private static boolean isCurrentlyActive = false; + private final SynchronizationQueueStorage synchronizationQueueStorage; public SyncService(@NonNull Context context, @NonNull WorkerParameters params) { super(context, params); + synchronizationQueueStorage = new SynchronizationQueueStorage(context); } @Override @NonNull public Result doWork() { - if (!GpodnetPreferences.loggedIn()) { + ISyncService activeSyncProvider = getActiveSyncProvider(); + if (activeSyncProvider == null) { return Result.success(); } - syncServiceImpl = new GpodnetService(AntennapodHttpClient.getHttpClient(), - GpodnetPreferences.getHosturl(), GpodnetPreferences.getDeviceID(), - GpodnetPreferences.getUsername(), GpodnetPreferences.getPassword()); - SharedPreferences.Editor prefs = getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) - .edit(); - prefs.putLong(PREF_LAST_SYNC_ATTEMPT_TIMESTAMP, System.currentTimeMillis()).apply(); + + SynchronizationSettings.updateLastSynchronizationAttempt(); + setCurrentlyActive(true); try { - syncServiceImpl.login(); - EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_subscriptions)); - syncSubscriptions(); - syncEpisodeActions(); - syncServiceImpl.logout(); + activeSyncProvider.login(); + syncSubscriptions(activeSyncProvider); + waitForDownloadServiceCompleted(); + syncEpisodeActions(activeSyncProvider); + activeSyncProvider.logout(); clearErrorNotifications(); EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_success)); - prefs.putBoolean(PREF_LAST_SYNC_ATTEMPT_SUCCESS, true).apply(); + SynchronizationSettings.setLastSynchronizationAttemptSuccess(true); return Result.success(); - } catch (SyncServiceException e) { + } catch (Exception e) { EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_error)); - prefs.putBoolean(PREF_LAST_SYNC_ATTEMPT_SUCCESS, false).apply(); + SynchronizationSettings.setLastSynchronizationAttemptSuccess(false); Log.e(TAG, Log.getStackTraceString(e)); - if (getRunAttemptCount() % 3 == 2) { - // Do not spam users with notification and retry before notifying - updateErrorNotification(e); - } - return Result.retry(); - } - } - - public static void clearQueue(Context context) { - executeLockedAsync(() -> - context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE).edit() - .putLong(PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP, 0) - .putLong(PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, 0) - .putLong(PREF_LAST_SYNC_ATTEMPT_TIMESTAMP, 0) - .putString(PREF_QUEUED_EPISODE_ACTIONS, "[]") - .putString(PREF_QUEUED_FEEDS_ADDED, "[]") - .putString(PREF_QUEUED_FEEDS_REMOVED, "[]") - .apply()); - } - - public static void enqueueFeedAdded(Context context, String downloadUrl) { - if (!GpodnetPreferences.loggedIn()) { - return; - } - executeLockedAsync(() -> { - try { - SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); - String json = prefs.getString(PREF_QUEUED_FEEDS_ADDED, "[]"); - JSONArray queue = new JSONArray(json); - queue.put(downloadUrl); - prefs.edit().putString(PREF_QUEUED_FEEDS_ADDED, queue.toString()).apply(); - } catch (JSONException e) { - e.printStackTrace(); - } - sync(context); - }); - } - public static void enqueueFeedRemoved(Context context, String downloadUrl) { - if (!GpodnetPreferences.loggedIn()) { - return; - } - executeLockedAsync(() -> { - try { - SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); - String json = prefs.getString(PREF_QUEUED_FEEDS_REMOVED, "[]"); - JSONArray queue = new JSONArray(json); - queue.put(downloadUrl); - prefs.edit().putString(PREF_QUEUED_FEEDS_REMOVED, queue.toString()).apply(); - } catch (JSONException e) { - e.printStackTrace(); + if (e instanceof SyncServiceException) { + if (getRunAttemptCount() % 3 == 2) { + // Do not spam users with notification and retry before notifying + updateErrorNotification(e); + } + return Result.retry(); + } else { + updateErrorNotification(e); + return Result.failure(); } - sync(context); - }); - } - - public static void enqueueEpisodeAction(Context context, EpisodeAction action) { - if (!GpodnetPreferences.loggedIn()) { - return; + } finally { + setCurrentlyActive(false); } - executeLockedAsync(() -> { - try { - SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); - String json = prefs.getString(PREF_QUEUED_EPISODE_ACTIONS, "[]"); - JSONArray queue = new JSONArray(json); - queue.put(action.writeToJsonObject()); - prefs.edit().putString(PREF_QUEUED_EPISODE_ACTIONS, queue.toString()).apply(); - } catch (JSONException e) { - e.printStackTrace(); - } - sync(context); - }); } - public static void enqueueEpisodePlayed(Context context, FeedMedia media, boolean completed) { - if (!GpodnetPreferences.loggedIn()) { - return; - } - if (media.getItem() == null) { - return; - } - if (media.getStartPosition() < 0 || (!completed && media.getStartPosition() >= media.getPosition())) { - return; - } - EpisodeAction action = new EpisodeAction.Builder(media.getItem(), EpisodeAction.PLAY) - .currentTimestamp() - .started(media.getStartPosition() / 1000) - .position((completed ? media.getDuration() : media.getPosition()) / 1000) - .total(media.getDuration() / 1000) - .build(); - SyncService.enqueueEpisodeAction(context, action); + private static void setCurrentlyActive(boolean active) { + isCurrentlyActive = active; } public static void sync(Context context) { OneTimeWorkRequest workRequest = getWorkRequest().build(); WorkManager.getInstance(context).enqueueUniqueWork(WORK_ID_SYNC, ExistingWorkPolicy.REPLACE, workRequest); - EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_started)); } public static void syncImmediately(Context context) { @@ -207,127 +120,27 @@ public class SyncService extends Worker { .setInitialDelay(0L, TimeUnit.SECONDS) .build(); WorkManager.getInstance(context).enqueueUniqueWork(WORK_ID_SYNC, ExistingWorkPolicy.REPLACE, workRequest); - EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_started)); } public static void fullSync(Context context) { - executeLockedAsync(() -> { - context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE).edit() - .putLong(PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP, 0) - .putLong(PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, 0) - .putLong(PREF_LAST_SYNC_ATTEMPT_TIMESTAMP, 0) - .apply(); - + LockingAsyncExecutor.executeLockedAsync(() -> { + SynchronizationSettings.resetTimestamps(); OneTimeWorkRequest workRequest = getWorkRequest() .setInitialDelay(0L, TimeUnit.SECONDS) .build(); WorkManager.getInstance(context).enqueueUniqueWork(WORK_ID_SYNC, ExistingWorkPolicy.REPLACE, workRequest); - EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_started)); }); } - private static OneTimeWorkRequest.Builder getWorkRequest() { - Constraints.Builder constraints = new Constraints.Builder(); - if (UserPreferences.isAllowMobileFeedRefresh()) { - constraints.setRequiredNetworkType(NetworkType.CONNECTED); - } else { - constraints.setRequiredNetworkType(NetworkType.UNMETERED); - } - - return new OneTimeWorkRequest.Builder(SyncService.class) - .setConstraints(constraints.build()) - .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, TimeUnit.MINUTES) - .setInitialDelay(5L, TimeUnit.SECONDS); // Give it some time, so other actions can be queued - } - - /** - * Take the lock and execute runnable (to prevent changes to preferences being lost when enqueueing while sync is - * in progress). If the lock is free, the runnable is directly executed in the calling thread to prevent overhead. - */ - private static void executeLockedAsync(Runnable runnable) { - if (lock.tryLock()) { - try { - runnable.run(); - } finally { - lock.unlock(); - } - } else { - Completable.fromRunnable(() -> { - lock.lock(); - try { - runnable.run(); - } finally { - lock.unlock(); - } - }).subscribeOn(Schedulers.io()) - .subscribe(); - } - } - - public static boolean isLastSyncSuccessful(Context context) { - return context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) - .getBoolean(PREF_LAST_SYNC_ATTEMPT_SUCCESS, false); - } - - public static long getLastSyncAttempt(Context context) { - return context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) - .getLong(PREF_LAST_SYNC_ATTEMPT_TIMESTAMP, 0); - } - - private List<EpisodeAction> getQueuedEpisodeActions() { - ArrayList<EpisodeAction> actions = new ArrayList<>(); - try { - SharedPreferences prefs = getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); - String json = prefs.getString(PREF_QUEUED_EPISODE_ACTIONS, "[]"); - JSONArray queue = new JSONArray(json); - for (int i = 0; i < queue.length(); i++) { - actions.add(EpisodeAction.readFromJsonObject(queue.getJSONObject(i))); - } - } catch (JSONException e) { - e.printStackTrace(); - } - return actions; - } - - private List<String> getQueuedRemovedFeeds() { - ArrayList<String> actions = new ArrayList<>(); - try { - SharedPreferences prefs = getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); - String json = prefs.getString(PREF_QUEUED_FEEDS_REMOVED, "[]"); - JSONArray queue = new JSONArray(json); - for (int i = 0; i < queue.length(); i++) { - actions.add(queue.getString(i)); - } - } catch (JSONException e) { - e.printStackTrace(); - } - return actions; - } - - private List<String> getQueuedAddedFeeds() { - ArrayList<String> actions = new ArrayList<>(); - try { - SharedPreferences prefs = getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); - String json = prefs.getString(PREF_QUEUED_FEEDS_ADDED, "[]"); - JSONArray queue = new JSONArray(json); - for (int i = 0; i < queue.length(); i++) { - actions.add(queue.getString(i)); - } - } catch (JSONException e) { - e.printStackTrace(); - } - return actions; - } - - private void syncSubscriptions() throws SyncServiceException { - final long lastSync = getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) - .getLong(PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP, 0); + private void syncSubscriptions(ISyncService syncServiceImpl) throws SyncServiceException { + final long lastSync = SynchronizationSettings.getLastSubscriptionSynchronizationTimestamp(); + EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_subscriptions)); final List<String> localSubscriptions = DBReader.getFeedListDownloadUrls(); SubscriptionChanges subscriptionChanges = syncServiceImpl.getSubscriptionChanges(lastSync); long newTimeStamp = subscriptionChanges.getTimestamp(); - List<String> queuedRemovedFeeds = getQueuedRemovedFeeds(); - List<String> queuedAddedFeeds = getQueuedAddedFeeds(); + List<String> queuedRemovedFeeds = synchronizationQueueStorage.getQueuedRemovedFeeds(); + List<String> queuedAddedFeeds = synchronizationQueueStorage.getQueuedAddedFeeds(); Log.d(TAG, "Downloaded subscription changes: " + subscriptionChanges); for (String downloadUrl : subscriptionChanges.getAdded()) { @@ -359,26 +172,33 @@ public class SyncService extends Worker { Log.d(TAG, "Added: " + StringUtils.join(queuedAddedFeeds, ", ")); Log.d(TAG, "Removed: " + StringUtils.join(queuedRemovedFeeds, ", ")); - lock.lock(); + LockingAsyncExecutor.lock.lock(); try { UploadChangesResponse uploadResponse = syncServiceImpl .uploadSubscriptionChanges(queuedAddedFeeds, queuedRemovedFeeds); - getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE).edit() - .putString(PREF_QUEUED_FEEDS_ADDED, "[]").apply(); - getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE).edit() - .putString(PREF_QUEUED_FEEDS_REMOVED, "[]").apply(); + synchronizationQueueStorage.clearFeedQueues(); newTimeStamp = uploadResponse.timestamp; } finally { - lock.unlock(); + LockingAsyncExecutor.lock.unlock(); + } + } + SynchronizationSettings.setLastSubscriptionSynchronizationAttemptTimestamp(newTimeStamp); + } + + private void waitForDownloadServiceCompleted() { + EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_wait_for_downloads)); + try { + while (DownloadRequester.getInstance().isDownloadingFeeds()) { + //noinspection BusyWait + Thread.sleep(1000); } + } catch (InterruptedException e) { + e.printStackTrace(); } - getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE).edit() - .putLong(PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP, newTimeStamp).apply(); } - private void syncEpisodeActions() throws SyncServiceException { - final long lastSync = getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) - .getLong(PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, 0); + private void syncEpisodeActions(ISyncService syncServiceImpl) throws SyncServiceException { + final long lastSync = SynchronizationSettings.getLastEpisodeActionSynchronizationTimestamp(); EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_episodes_download)); EpisodeActionChanges getResponse = syncServiceImpl.getEpisodeActionChanges(lastSync); long newTimeStamp = getResponse.getTimestamp(); @@ -387,7 +207,7 @@ public class SyncService extends Worker { // upload local actions EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_episodes_upload)); - List<EpisodeAction> queuedEpisodeActions = getQueuedEpisodeActions(); + List<EpisodeAction> queuedEpisodeActions = synchronizationQueueStorage.getQueuedEpisodeActions(); if (lastSync == 0) { EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_upload_played)); List<FeedItem> readItems = DBReader.getPlayedItems(); @@ -407,24 +227,21 @@ public class SyncService extends Worker { } } if (queuedEpisodeActions.size() > 0) { - lock.lock(); + LockingAsyncExecutor.lock.lock(); try { Log.d(TAG, "Uploading " + queuedEpisodeActions.size() + " actions: " + StringUtils.join(queuedEpisodeActions, ", ")); UploadChangesResponse postResponse = syncServiceImpl.uploadEpisodeActions(queuedEpisodeActions); newTimeStamp = postResponse.timestamp; Log.d(TAG, "Upload episode response: " + postResponse); - getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE).edit() - .putString(PREF_QUEUED_EPISODE_ACTIONS, "[]").apply(); + synchronizationQueueStorage.clearEpisodeActionQueue(); } finally { - lock.unlock(); + LockingAsyncExecutor.lock.unlock(); } } - getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE).edit() - .putLong(PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, newTimeStamp).apply(); + SynchronizationSettings.setLastEpisodeActionSynchronizationAttemptTimestamp(newTimeStamp); } - private synchronized void processEpisodeActions(List<EpisodeAction> remoteActions) { Log.d(TAG, "Processing " + remoteActions.size() + " actions"); if (remoteActions.size() == 0) { @@ -432,7 +249,8 @@ public class SyncService extends Worker { } Map<Pair<String, String>, EpisodeAction> playActionsToUpdate = EpisodeActionFilter - .getRemoteActionsOverridingLocalActions(remoteActions, getQueuedEpisodeActions()); + .getRemoteActionsOverridingLocalActions(remoteActions, + synchronizationQueueStorage.getQueuedEpisodeActions()); LongList queueToBeRemoved = new LongList(); List<FeedItem> updatedItems = new ArrayList<>(); for (EpisodeAction action : playActionsToUpdate.values()) { @@ -442,20 +260,24 @@ public class SyncService extends Worker { Log.i(TAG, "Unknown feed item: " + action); continue; } + if (feedItem.getMedia() == null) { + Log.i(TAG, "Feed item has no media: " + action); + continue; + } if (action.getAction() == EpisodeAction.NEW) { DBWriter.markItemPlayed(feedItem, FeedItem.UNPLAYED, true); continue; } - Log.d(TAG, "Most recent play action: " + action.toString()); - FeedMedia media = feedItem.getMedia(); - media.setPosition(action.getPosition() * 1000); + feedItem.getMedia().setPosition(action.getPosition() * 1000); if (FeedItemUtil.hasAlmostEnded(feedItem.getMedia())) { - Log.d(TAG, "Marking as played"); + Log.d(TAG, "Marking as played: " + action); feedItem.setPlayed(true); + feedItem.getMedia().setPosition(0); queueToBeRemoved.add(feedItem.getId()); + } else { + Log.d(TAG, "Setting position: " + action); } updatedItems.add(feedItem); - } DBWriter.removeQueueItem(getApplicationContext(), false, queueToBeRemoved.toArray()); DBReader.loadAdditionalFeedItemListData(updatedItems); @@ -469,7 +291,7 @@ public class SyncService extends Worker { nm.cancel(R.id.notification_gpodnet_sync_autherror); } - private void updateErrorNotification(SyncServiceException exception) { + private void updateErrorNotification(Exception exception) { if (!UserPreferences.gpodnetNotificationsEnabled()) { Log.d(TAG, "Skipping sync error notification because of user setting"); return; @@ -481,11 +303,13 @@ public class SyncService extends Worker { Intent intent = getApplicationContext().getPackageManager().getLaunchIntentForPackage( getApplicationContext().getPackageName()); PendingIntent pendingIntent = PendingIntent.getActivity(getApplicationContext(), - R.id.pending_intent_sync_error, intent, PendingIntent.FLAG_UPDATE_CURRENT); + R.id.pending_intent_sync_error, intent, PendingIntent.FLAG_UPDATE_CURRENT + | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); Notification notification = new NotificationCompat.Builder(getApplicationContext(), NotificationUtils.CHANNEL_ID_SYNC_ERROR) .setContentTitle(getApplicationContext().getString(R.string.gpodnetsync_error_title)) .setContentText(description) + .setStyle(new NotificationCompat.BigTextStyle().bigText(description)) .setContentIntent(pendingIntent) .setSmallIcon(R.drawable.ic_notification_sync_error) .setAutoCancel(true) @@ -495,4 +319,48 @@ public class SyncService extends Worker { .getSystemService(Context.NOTIFICATION_SERVICE); nm.notify(R.id.notification_gpodnet_sync_error, notification); } + + private static OneTimeWorkRequest.Builder getWorkRequest() { + Constraints.Builder constraints = new Constraints.Builder(); + if (UserPreferences.isAllowMobileFeedRefresh()) { + constraints.setRequiredNetworkType(NetworkType.CONNECTED); + } else { + constraints.setRequiredNetworkType(NetworkType.UNMETERED); + } + + OneTimeWorkRequest.Builder builder = new OneTimeWorkRequest.Builder(SyncService.class) + .setConstraints(constraints.build()) + .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, TimeUnit.MINUTES); + + if (isCurrentlyActive) { + // Debounce: don't start sync again immediately after it was finished. + builder.setInitialDelay(2L, TimeUnit.MINUTES); + } else { + // Give it some time, so other possible actions can be queued. + builder.setInitialDelay(20L, TimeUnit.SECONDS); + EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_started)); + } + return builder; + } + + private ISyncService getActiveSyncProvider() { + String selectedSyncProviderKey = SynchronizationSettings.getSelectedSyncProviderKey(); + SynchronizationProviderViewData selectedService = SynchronizationProviderViewData + .fromIdentifier(selectedSyncProviderKey); + if (selectedService == null) { + return null; + } + switch (selectedService) { + case GPODDER_NET: + return new GpodnetService(AntennapodHttpClient.getHttpClient(), + SynchronizationCredentials.getHosturl(), SynchronizationCredentials.getDeviceID(), + SynchronizationCredentials.getUsername(), SynchronizationCredentials.getPassword()); + case NEXTCLOUD_GPODDER: + return new NextcloudSyncService(AntennapodHttpClient.getHttpClient(), + SynchronizationCredentials.getHosturl(), SynchronizationCredentials.getUsername(), + SynchronizationCredentials.getPassword()); + default: + return null; + } + } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/SynchronizationCredentials.java b/core/src/main/java/de/danoeh/antennapod/core/sync/SynchronizationCredentials.java new file mode 100644 index 000000000..e08bc66ad --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/sync/SynchronizationCredentials.java @@ -0,0 +1,67 @@ +package de.danoeh.antennapod.core.sync; + +import android.content.Context; +import android.content.SharedPreferences; +import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink; + +/** + * Manages preferences for accessing gpodder.net service and other sync providers + */ +public class SynchronizationCredentials { + + private SynchronizationCredentials() { + } + + private static final String PREF_NAME = "gpodder.net"; + private static final String PREF_USERNAME = "de.danoeh.antennapod.preferences.gpoddernet.username"; + private static final String PREF_PASSWORD = "de.danoeh.antennapod.preferences.gpoddernet.password"; + private static final String PREF_DEVICEID = "de.danoeh.antennapod.preferences.gpoddernet.deviceID"; + private static final String PREF_HOSTNAME = "prefGpodnetHostname"; + + private static SharedPreferences getPreferences() { + return ClientConfig.applicationCallbacks.getApplicationInstance() + .getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + } + + public static String getUsername() { + return getPreferences().getString(PREF_USERNAME, null); + } + + public static void setUsername(String username) { + getPreferences().edit().putString(PREF_USERNAME, username).apply(); + } + + public static String getPassword() { + return getPreferences().getString(PREF_PASSWORD, null); + } + + public static void setPassword(String password) { + getPreferences().edit().putString(PREF_PASSWORD, password).apply(); + } + + public static String getDeviceID() { + return getPreferences().getString(PREF_DEVICEID, null); + } + + public static void setDeviceID(String deviceID) { + getPreferences().edit().putString(PREF_DEVICEID, deviceID).apply(); + } + + public static String getHosturl() { + return getPreferences().getString(PREF_HOSTNAME, null); + } + + public static void setHosturl(String value) { + getPreferences().edit().putString(PREF_HOSTNAME, value).apply(); + } + + public static synchronized void clear(Context context) { + setUsername(null); + setPassword(null); + setDeviceID(null); + SynchronizationQueueSink.clearQueue(context); + UserPreferences.setGpodnetNotificationsEnabled(); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/SynchronizationProviderViewData.java b/core/src/main/java/de/danoeh/antennapod/core/sync/SynchronizationProviderViewData.java new file mode 100644 index 000000000..cba713f60 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/sync/SynchronizationProviderViewData.java @@ -0,0 +1,47 @@ +package de.danoeh.antennapod.core.sync; + +import de.danoeh.antennapod.core.R; + +public enum SynchronizationProviderViewData { + GPODDER_NET( + "GPODDER_NET", + R.string.gpodnet_description, + R.drawable.gpodder_icon + ), + NEXTCLOUD_GPODDER( + "NEXTCLOUD_GPODDER", + R.string.synchronization_summary_nextcloud, + R.drawable.nextcloud_logo + ); + + public static SynchronizationProviderViewData fromIdentifier(String provider) { + for (SynchronizationProviderViewData synchronizationProvider : SynchronizationProviderViewData.values()) { + if (synchronizationProvider.getIdentifier().equals(provider)) { + return synchronizationProvider; + } + } + return null; + } + + private final String identifier; + private final int iconResource; + private final int summaryResource; + + SynchronizationProviderViewData(String identifier, int summaryResource, int iconResource) { + this.identifier = identifier; + this.iconResource = iconResource; + this.summaryResource = summaryResource; + } + + public String getIdentifier() { + return identifier; + } + + public int getIconResource() { + return iconResource; + } + + public int getSummaryResource() { + return summaryResource; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/SynchronizationSettings.java b/core/src/main/java/de/danoeh/antennapod/core/sync/SynchronizationSettings.java new file mode 100644 index 000000000..1a53ac0fb --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/sync/SynchronizationSettings.java @@ -0,0 +1,83 @@ +package de.danoeh.antennapod.core.sync; + +import android.content.Context; +import android.content.SharedPreferences; + +import de.danoeh.antennapod.core.ClientConfig; + +public class SynchronizationSettings { + + public static final String LAST_SYNC_ATTEMPT_TIMESTAMP = "last_sync_attempt_timestamp"; + private static final String NAME = "synchronization"; + private static final String SELECTED_SYNC_PROVIDER = "selected_sync_provider"; + private static final String LAST_SYNC_ATTEMPT_SUCCESS = "last_sync_attempt_success"; + private static final String LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP = "last_episode_actions_sync_timestamp"; + private static final String LAST_SUBSCRIPTION_SYNC_TIMESTAMP = "last_sync_timestamp"; + + public static boolean isProviderConnected() { + return getSelectedSyncProviderKey() != null; + } + + public static void resetTimestamps() { + getSharedPreferences().edit() + .putLong(LAST_SUBSCRIPTION_SYNC_TIMESTAMP, 0) + .putLong(LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, 0) + .putLong(LAST_SYNC_ATTEMPT_TIMESTAMP, 0) + .apply(); + } + + public static boolean isLastSyncSuccessful() { + return getSharedPreferences().getBoolean(LAST_SYNC_ATTEMPT_SUCCESS, false); + } + + public static long getLastSyncAttempt() { + return getSharedPreferences().getLong(LAST_SYNC_ATTEMPT_TIMESTAMP, 0); + } + + public static void setSelectedSyncProvider(SynchronizationProviderViewData provider) { + getSharedPreferences() + .edit() + .putString(SELECTED_SYNC_PROVIDER, provider == null ? null : provider.getIdentifier()) + .apply(); + } + + public static String getSelectedSyncProviderKey() { + return getSharedPreferences().getString(SELECTED_SYNC_PROVIDER, null); + } + + public static void updateLastSynchronizationAttempt() { + getSharedPreferences().edit() + .putLong(LAST_SYNC_ATTEMPT_TIMESTAMP, System.currentTimeMillis()) + .apply(); + } + + public static void setLastSynchronizationAttemptSuccess(boolean isSuccess) { + getSharedPreferences().edit() + .putBoolean(LAST_SYNC_ATTEMPT_SUCCESS, isSuccess) + .apply(); + } + + public static long getLastSubscriptionSynchronizationTimestamp() { + return getSharedPreferences().getLong(LAST_SUBSCRIPTION_SYNC_TIMESTAMP, 0); + } + + public static void setLastSubscriptionSynchronizationAttemptTimestamp(long newTimeStamp) { + getSharedPreferences().edit() + .putLong(LAST_SUBSCRIPTION_SYNC_TIMESTAMP, newTimeStamp).apply(); + } + + public static long getLastEpisodeActionSynchronizationTimestamp() { + return getSharedPreferences() + .getLong(LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, 0); + } + + public static void setLastEpisodeActionSynchronizationAttemptTimestamp(long timestamp) { + getSharedPreferences().edit() + .putLong(LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, timestamp).apply(); + } + + private static SharedPreferences getSharedPreferences() { + return ClientConfig.applicationCallbacks.getApplicationInstance() + .getSharedPreferences(NAME, Context.MODE_PRIVATE); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/queue/SynchronizationQueueSink.java b/core/src/main/java/de/danoeh/antennapod/core/sync/queue/SynchronizationQueueSink.java new file mode 100644 index 000000000..445faf60f --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/sync/queue/SynchronizationQueueSink.java @@ -0,0 +1,67 @@ +package de.danoeh.antennapod.core.sync.queue; + +import android.content.Context; + +import de.danoeh.antennapod.core.sync.LockingAsyncExecutor; +import de.danoeh.antennapod.core.sync.SyncService; +import de.danoeh.antennapod.core.sync.SynchronizationSettings; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.net.sync.model.EpisodeAction; + +public class SynchronizationQueueSink { + + public static void clearQueue(Context context) { + LockingAsyncExecutor.executeLockedAsync(new SynchronizationQueueStorage(context)::clearQueue); + } + + public static void enqueueFeedAddedIfSynchronizationIsActive(Context context, String downloadUrl) { + if (!SynchronizationSettings.isProviderConnected()) { + return; + } + LockingAsyncExecutor.executeLockedAsync(() -> { + new SynchronizationQueueStorage(context).enqueueFeedAdded(downloadUrl); + SyncService.sync(context); + }); + } + + public static void enqueueFeedRemovedIfSynchronizationIsActive(Context context, String downloadUrl) { + if (!SynchronizationSettings.isProviderConnected()) { + return; + } + LockingAsyncExecutor.executeLockedAsync(() -> { + new SynchronizationQueueStorage(context).enqueueFeedRemoved(downloadUrl); + SyncService.sync(context); + }); + } + + public static void enqueueEpisodeActionIfSynchronizationIsActive(Context context, EpisodeAction action) { + if (!SynchronizationSettings.isProviderConnected()) { + return; + } + LockingAsyncExecutor.executeLockedAsync(() -> { + new SynchronizationQueueStorage(context).enqueueEpisodeAction(action); + SyncService.sync(context); + }); + } + + public static void enqueueEpisodePlayedIfSynchronizationIsActive(Context context, FeedMedia media, + boolean completed) { + if (!SynchronizationSettings.isProviderConnected()) { + return; + } + if (media.getItem() == null) { + return; + } + if (media.getStartPosition() < 0 || (!completed && media.getStartPosition() >= media.getPosition())) { + return; + } + EpisodeAction action = new EpisodeAction.Builder(media.getItem(), EpisodeAction.PLAY) + .currentTimestamp() + .started(media.getStartPosition() / 1000) + .position((completed ? media.getDuration() : media.getPosition()) / 1000) + .total(media.getDuration() / 1000) + .build(); + enqueueEpisodeActionIfSynchronizationIsActive(context, action); + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/queue/SynchronizationQueueStorage.java b/core/src/main/java/de/danoeh/antennapod/core/sync/queue/SynchronizationQueueStorage.java new file mode 100644 index 000000000..5c6d58fe3 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/sync/queue/SynchronizationQueueStorage.java @@ -0,0 +1,140 @@ +package de.danoeh.antennapod.core.sync.queue; + +import android.content.Context; +import android.content.SharedPreferences; + +import org.json.JSONArray; +import org.json.JSONException; + +import java.util.ArrayList; + +import de.danoeh.antennapod.core.sync.SynchronizationSettings; +import de.danoeh.antennapod.net.sync.model.EpisodeAction; + +public class SynchronizationQueueStorage { + + private static final String NAME = "synchronization"; + private static final String QUEUED_EPISODE_ACTIONS = "sync_queued_episode_actions"; + private static final String QUEUED_FEEDS_REMOVED = "sync_removed"; + private static final String QUEUED_FEEDS_ADDED = "sync_added"; + private final SharedPreferences sharedPreferences; + + public SynchronizationQueueStorage(Context context) { + this.sharedPreferences = context.getSharedPreferences(NAME, Context.MODE_PRIVATE); + } + + public ArrayList<EpisodeAction> getQueuedEpisodeActions() { + ArrayList<EpisodeAction> actions = new ArrayList<>(); + try { + String json = getSharedPreferences() + .getString(QUEUED_EPISODE_ACTIONS, "[]"); + JSONArray queue = new JSONArray(json); + for (int i = 0; i < queue.length(); i++) { + actions.add(EpisodeAction.readFromJsonObject(queue.getJSONObject(i))); + } + } catch (JSONException e) { + e.printStackTrace(); + } + return actions; + } + + public ArrayList<String> getQueuedRemovedFeeds() { + ArrayList<String> removedFeedUrls = new ArrayList<>(); + try { + String json = getSharedPreferences() + .getString(QUEUED_FEEDS_REMOVED, "[]"); + JSONArray queue = new JSONArray(json); + for (int i = 0; i < queue.length(); i++) { + removedFeedUrls.add(queue.getString(i)); + } + } catch (JSONException e) { + e.printStackTrace(); + } + return removedFeedUrls; + + } + + public ArrayList<String> getQueuedAddedFeeds() { + ArrayList<String> addedFeedUrls = new ArrayList<>(); + try { + String json = getSharedPreferences() + .getString(QUEUED_FEEDS_ADDED, "[]"); + JSONArray queue = new JSONArray(json); + for (int i = 0; i < queue.length(); i++) { + addedFeedUrls.add(queue.getString(i)); + } + } catch (JSONException e) { + e.printStackTrace(); + } + return addedFeedUrls; + } + + public void clearEpisodeActionQueue() { + getSharedPreferences().edit() + .putString(QUEUED_EPISODE_ACTIONS, "[]").apply(); + + } + + public void clearFeedQueues() { + getSharedPreferences().edit() + .putString(QUEUED_FEEDS_ADDED, "[]") + .putString(QUEUED_FEEDS_REMOVED, "[]") + .apply(); + } + + protected void clearQueue() { + SynchronizationSettings.resetTimestamps(); + getSharedPreferences().edit() + .putString(QUEUED_EPISODE_ACTIONS, "[]") + .putString(QUEUED_FEEDS_ADDED, "[]") + .putString(QUEUED_FEEDS_REMOVED, "[]") + .apply(); + + } + + protected void enqueueFeedAdded(String downloadUrl) { + SharedPreferences sharedPreferences = getSharedPreferences(); + String json = sharedPreferences + .getString(QUEUED_FEEDS_ADDED, "[]"); + try { + JSONArray queue = new JSONArray(json); + queue.put(downloadUrl); + sharedPreferences + .edit().putString(QUEUED_FEEDS_ADDED, queue.toString()).apply(); + + } catch (JSONException jsonException) { + jsonException.printStackTrace(); + } + } + + protected void enqueueFeedRemoved(String downloadUrl) { + SharedPreferences sharedPreferences = getSharedPreferences(); + String json = sharedPreferences.getString(QUEUED_FEEDS_REMOVED, "[]"); + try { + JSONArray queue = new JSONArray(json); + queue.put(downloadUrl); + sharedPreferences.edit().putString(QUEUED_FEEDS_REMOVED, queue.toString()) + .apply(); + } catch (JSONException jsonException) { + jsonException.printStackTrace(); + } + } + + protected void enqueueEpisodeAction(EpisodeAction action) { + SharedPreferences sharedPreferences = getSharedPreferences(); + String json = sharedPreferences.getString(QUEUED_EPISODE_ACTIONS, "[]"); + try { + JSONArray queue = new JSONArray(json); + queue.put(action.writeToJsonObject()); + sharedPreferences.edit().putString( + QUEUED_EPISODE_ACTIONS, queue.toString() + ).apply(); + } catch (JSONException jsonException) { + jsonException.printStackTrace(); + } + } + + private SharedPreferences getSharedPreferences() { + return sharedPreferences; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/FeedItemPermutors.java b/core/src/main/java/de/danoeh/antennapod/core/util/FeedItemPermutors.java index e5f60d64b..09161ca7b 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/FeedItemPermutors.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/FeedItemPermutors.java @@ -9,6 +9,7 @@ import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import de.danoeh.antennapod.model.feed.FeedItem; @@ -77,25 +78,22 @@ public class FeedItemPermutors { @NonNull private static Date pubDate(@Nullable FeedItem item) { - return (item != null && item.getPubDate() != null) ? - item.getPubDate() : new Date(0); + return (item != null && item.getPubDate() != null) ? item.getPubDate() : new Date(0); } @NonNull private static String itemTitle(@Nullable FeedItem item) { - return (item != null && item.getTitle() != null) ? - item.getTitle() : ""; + return (item != null && item.getTitle() != null) ? item.getTitle().toLowerCase(Locale.getDefault()) : ""; } private static int duration(@Nullable FeedItem item) { - return (item != null && item.getMedia() != null) ? - item.getMedia().getDuration() : 0; + return (item != null && item.getMedia() != null) ? item.getMedia().getDuration() : 0; } @NonNull private static String feedTitle(@Nullable FeedItem item) { - return (item != null && item.getFeed() != null && item.getFeed().getTitle() != null) ? - item.getFeed().getTitle() : ""; + return (item != null && item.getFeed() != null && item.getFeed().getTitle() != null) + ? item.getFeed().getTitle().toLowerCase(Locale.getDefault()) : ""; } /** 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 12f1e98f9..efc7845a4 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 @@ -8,7 +8,6 @@ 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; @@ -16,7 +15,11 @@ import java.io.File; import java.io.IOException; import java.util.Arrays; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; @@ -30,6 +33,8 @@ import okhttp3.Request; import okhttp3.Response; public class NetworkUtils { + private static final String REGEX_PATTERN_IP_ADDRESS = "([0-9]{1,3}[\\.]){3}[0-9]{1,3}"; + private NetworkUtils(){} private static final String TAG = NetworkUtils.class.getSimpleName(); @@ -40,56 +45,23 @@ public class NetworkUtils { 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); + public static boolean isAutoDownloadAllowed() { + 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; - } + if (networkInfo == null) { + return false; + } + if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { + if (UserPreferences.isEnableAutodownloadWifiFilter()) { + return isInAllowedWifiNetwork(); } 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; + return !isNetworkMetered(); } + } else if (networkInfo.getType() == ConnectivityManager.TYPE_ETHERNET) { + return true; + } else { + return UserPreferences.isAllowMobileAutoDownload() || !NetworkUtils.isNetworkRestricted(); } - Log.d(TAG, "Network for auto-dl is not available"); - return false; } public static boolean networkAvailable() { @@ -126,7 +98,18 @@ public class NetworkUtils { private static boolean isNetworkMetered() { ConnectivityManager connManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - return ConnectivityManagerCompat.isActiveNetworkMetered(connManager); + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + NetworkCapabilities capabilities = connManager.getNetworkCapabilities( + connManager.getActiveNetwork()); + + if (capabilities != null + && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) + && capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) { + return false; + } + } + return connManager.isActiveNetworkMetered(); } private static boolean isNetworkCellular() { @@ -157,6 +140,12 @@ public class NetworkUtils { } } + private static boolean isInAllowedWifiNetwork() { + WifiManager wm = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE); + List<String> selectedNetworks = Arrays.asList(UserPreferences.getAutodownloadSelectedNetworks()); + return selectedNetworks.contains(Integer.toString(wm.getConnectionInfo().getNetworkId())); + } + /** * Returns the SSID of the wifi connection, or <code>null</code> if there is no wifi. */ @@ -169,6 +158,22 @@ public class NetworkUtils { return null; } + public static boolean wasDownloadBlocked(Throwable throwable) { + String message = throwable.getMessage(); + if (message != null) { + Pattern pattern = Pattern.compile(REGEX_PATTERN_IP_ADDRESS); + Matcher matcher = pattern.matcher(message); + if (matcher.find()) { + String ip = matcher.group(); + return ip.startsWith("127.") || ip.startsWith("0."); + } + } + if (throwable.getCause() != null) { + return wasDownloadBlocked(throwable.getCause()); + } + return false; + } + public static Single<Long> getFeedMediaSizeObservable(FeedMedia media) { return Single.create((SingleOnSubscribe<Long>) emitter -> { if (!NetworkUtils.isEpisodeHeadDownloadAllowed()) { @@ -225,4 +230,16 @@ public class NetworkUtils { .observeOn(AndroidSchedulers.mainThread()); } + public static void networkChangedDetected() { + if (NetworkUtils.isAutoDownloadAllowed()) { + Log.d(TAG, "auto-dl network available, starting auto-download"); + DBTasks.autodownloadUndownloadedItems(context); + } else { // if new network is Wi-Fi, finish ongoing downloads, + // otherwise cancel all downloads + if (NetworkUtils.isNetworkRestricted()) { + Log.i(TAG, "Device is no longer connected to Wi-Fi. Cancelling ongoing downloads"); + DownloadRequester.getInstance().cancelAllDownloads(context); + } + } + } } 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 deleted file mode 100644 index 813c6d0f7..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtils.java +++ /dev/null @@ -1,47 +0,0 @@ -package de.danoeh.antennapod.core.util; - -import java.util.concurrent.TimeUnit; - -/** - * This class calculates the proper rewind time after the pause and resume. - * <p> - * User might loose context if he/she pauses and resumes the media after longer time. - * Media file should be "rewinded" x seconds after user resumes the playback. - */ -public class RewindAfterPauseUtils { - private RewindAfterPauseUtils(){} - - public static final long ELAPSED_TIME_FOR_SHORT_REWIND = TimeUnit.MINUTES.toMillis(1); - public static final long ELAPSED_TIME_FOR_MEDIUM_REWIND = TimeUnit.HOURS.toMillis(1); - public static final long ELAPSED_TIME_FOR_LONG_REWIND = TimeUnit.DAYS.toMillis(1); - - public static final long SHORT_REWIND = TimeUnit.SECONDS.toMillis(3); - public static final long MEDIUM_REWIND = TimeUnit.SECONDS.toMillis(10); - public static final long LONG_REWIND = TimeUnit.SECONDS.toMillis(20); - - /** - * @param currentPosition current position in a media file in ms - * @param lastPlayedTime timestamp when was media paused - * @return new rewinded position for playback in milliseconds - */ - public static int calculatePositionWithRewind(int currentPosition, long lastPlayedTime) { - if (currentPosition > 0 && lastPlayedTime > 0) { - long elapsedTime = System.currentTimeMillis() - lastPlayedTime; - long rewindTime = 0; - - if (elapsedTime > ELAPSED_TIME_FOR_LONG_REWIND) { - rewindTime = LONG_REWIND; - } else if (elapsedTime > ELAPSED_TIME_FOR_MEDIUM_REWIND) { - rewindTime = MEDIUM_REWIND; - } else if (elapsedTime > ELAPSED_TIME_FOR_SHORT_REWIND) { - rewindTime = SHORT_REWIND; - } - - int newPosition = currentPosition - (int) rewindTime; - - return Math.max(newPosition, 0); - } else { - return currentPosition; - } - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java index c1c48f70d..34b9d294d 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java @@ -5,10 +5,12 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.net.Uri; -import android.os.Build; -import androidx.core.content.FileProvider; import android.util.Log; +import androidx.annotation.NonNull; +import androidx.core.app.ShareCompat; +import androidx.core.content.FileProvider; + import java.io.File; import java.util.List; @@ -24,11 +26,13 @@ public class ShareUtils { private ShareUtils() { } - public static void shareLink(Context context, String text) { - Intent i = new Intent(Intent.ACTION_SEND); - i.setType("text/plain"); - i.putExtra(Intent.EXTRA_TEXT, text); - context.startActivity(Intent.createChooser(i, context.getString(R.string.share_url_label))); + public static void shareLink(@NonNull Context context, @NonNull String text) { + Intent intent = new ShareCompat.IntentBuilder(context) + .setType("text/plain") + .setText(text) + .setChooserTitle(R.string.share_url_label) + .createChooserIntent(); + context.startActivity(intent); } public static void shareFeedlink(Context context, Feed feed) { @@ -75,21 +79,20 @@ public class ShareUtils { } public static void shareFeedItemFile(Context context, FeedMedia media) { - Intent i = new Intent(Intent.ACTION_SEND); - i.setType(media.getMime_type()); + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType(media.getMime_type()); Uri fileUri = FileProvider.getUriForFile(context, context.getString(R.string.provider_authority), new File(media.getLocalMediaUrl())); - i.putExtra(Intent.EXTRA_STREAM, fileUri); - i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { - List<ResolveInfo> resInfoList = context.getPackageManager() - .queryIntentActivities(i, PackageManager.MATCH_DEFAULT_ONLY); - for (ResolveInfo resolveInfo : resInfoList) { - String packageName = resolveInfo.activityInfo.packageName; - context.grantUriPermission(packageName, fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); - } + intent.putExtra(Intent.EXTRA_STREAM, fileUri); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + Intent chooserIntent = Intent.createChooser(intent, context.getString(R.string.share_file_label)); + List<ResolveInfo> resInfoList = context.getPackageManager() + .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); + for (ResolveInfo resolveInfo : resInfoList) { + String packageName = resolveInfo.activityInfo.packageName; + context.grantUriPermission(packageName, fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); } - context.startActivity(Intent.createChooser(i, context.getString(R.string.share_file_label))); + context.startActivity(chooserIntent); Log.e(TAG, "shareFeedItemFile called"); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/StorageUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/StorageUtils.java index 414b5c781..cf049ed80 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/StorageUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/StorageUtils.java @@ -1,7 +1,6 @@ package de.danoeh.antennapod.core.util; import android.app.Activity; -import android.os.Build; import android.os.StatFs; import android.util.Log; @@ -63,29 +62,15 @@ public class StorageUtils { */ public static long getFreeSpaceAvailable(String path) { StatFs stat = new StatFs(path); - long availableBlocks; - long blockSize; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { - availableBlocks = stat.getAvailableBlocksLong(); - blockSize = stat.getBlockSizeLong(); - } else { - availableBlocks = stat.getAvailableBlocks(); - blockSize = stat.getBlockSize(); - } + long availableBlocks = stat.getAvailableBlocksLong(); + long blockSize = stat.getBlockSizeLong(); return availableBlocks * blockSize; } public static long getTotalSpaceAvailable(String path) { StatFs stat = new StatFs(path); - long blockCount; - long blockSize; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { - blockCount = stat.getBlockCountLong(); - blockSize = stat.getBlockSizeLong(); - } else { - blockCount = stat.getBlockCount(); - blockSize = stat.getBlockSize(); - } + long blockCount = stat.getBlockCountLong(); + long blockSize = stat.getBlockSizeLong(); return blockCount * blockSize; } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/CompareCompat.java b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/CompareCompat.java deleted file mode 100644 index c189f2389..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/CompareCompat.java +++ /dev/null @@ -1,29 +0,0 @@ -package de.danoeh.antennapod.core.util.comparator; - -/** - * Some compare() methods are not available before API 19. - * This class provides fallbacks - */ -public class CompareCompat { - - private CompareCompat() { - // Must not be instantiated - } - - /** - * Compares two {@code long} values. Long.compare() is not available before API 19 - * - * @return 0 if long1 = long2, less than 0 if long1 < long2, - * and greater than 0 if long1 > long2. - */ - public static int compareLong(long long1, long long2) { - //noinspection UseCompareMethod - if (long1 > long2) { - return -1; - } else if (long1 < long2) { - return 1; - } else { - return 0; - } - } -} 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 bb0a71744..dbad1f63e 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 @@ -1,9 +1,10 @@ package de.danoeh.antennapod.core.util.download; import android.content.Context; -import androidx.annotation.NonNull; import android.util.Log; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; import androidx.work.Constraints; import androidx.work.Data; import androidx.work.ExistingPeriodicWorkPolicy; @@ -17,6 +18,7 @@ import java.util.Arrays; import java.util.Calendar; import java.util.concurrent.TimeUnit; +import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.FeedUpdateWorker; import de.danoeh.antennapod.core.storage.DBTasks; @@ -70,7 +72,7 @@ public class AutoUpdateManager { Log.d(TAG, "Restarting update alarm."); Calendar now = Calendar.getInstance(); - Calendar alarm = (Calendar)now.clone(); + Calendar alarm = (Calendar) now.clone(); alarm.set(Calendar.HOUR_OF_DAY, hoursOfDay); alarm.set(Calendar.MINUTE, minute); if (alarm.before(now) || alarm.equals(now)) { @@ -121,8 +123,24 @@ public class AutoUpdateManager { Log.d(TAG, "Run auto update immediately in background."); if (!NetworkUtils.networkAvailable()) { Log.d(TAG, "Ignoring: No network connection."); - return; + } else if (NetworkUtils.isEpisodeDownloadAllowed()) { + startRefreshAllFeeds(context); + } else { + confirmMobileAllFeedsRefresh(context); } + } + + private static void confirmMobileAllFeedsRefresh(final Context context) { + AlertDialog.Builder builder = new AlertDialog.Builder(context) + .setTitle(R.string.feed_refresh_title) + .setMessage(R.string.confirm_mobile_feed_refresh_dialog_message) + .setPositiveButton(R.string.yes, + (dialog, which) -> startRefreshAllFeeds(context)) + .setNegativeButton(R.string.no, null); + builder.show(); + } + + private static void startRefreshAllFeeds(final Context context) { new Thread(() -> DBTasks.refreshAllFeeds( context.getApplicationContext(), true), "ManualRefreshAllFeeds").start(); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/download/ConnectionStateMonitor.java b/core/src/main/java/de/danoeh/antennapod/core/util/download/ConnectionStateMonitor.java new file mode 100644 index 000000000..5c543bf5a --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/download/ConnectionStateMonitor.java @@ -0,0 +1,46 @@ +package de.danoeh.antennapod.core.util.download; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.os.Build; +import android.util.Log; + +import androidx.annotation.RequiresApi; +import de.danoeh.antennapod.core.util.NetworkUtils; + +@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) +public class ConnectionStateMonitor + extends ConnectivityManager.NetworkCallback + implements ConnectivityManager.OnNetworkActiveListener { + private static final String TAG = "ConnectionStateMonitor"; + final NetworkRequest networkRequest; + + public ConnectionStateMonitor() { + networkRequest = new NetworkRequest.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + .build(); + } + + @Override + public void onNetworkActive() { + Log.d(TAG, "ConnectionStateMonitor::onNetworkActive network connection changed"); + NetworkUtils.networkChangedDetected(); + } + + public void enable(Context context) { + ConnectivityManager connectivityManager = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + connectivityManager.registerNetworkCallback(networkRequest, this); + connectivityManager.addDefaultNetworkActiveListener(this); + } + + public void disable(Context context) { + ConnectivityManager connectivityManager = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + connectivityManager.unregisterNetworkCallback(this); + connectivityManager.removeDefaultNetworkActiveListener(this); + } +}
\ No newline at end of file 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 7f4c1ceaf..549171c76 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 @@ -7,23 +7,24 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; -import android.media.MediaPlayer; import android.os.IBinder; import android.util.Log; import android.util.Pair; import android.view.SurfaceHolder; import androidx.annotation.NonNull; -import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.core.event.MessageEvent; -import de.danoeh.antennapod.core.event.ServiceEvent; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.event.playback.PlaybackServiceEvent; +import de.danoeh.antennapod.event.playback.SpeedChangedEvent; import de.danoeh.antennapod.model.playback.MediaType; import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.playback.PlaybackService; -import de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer; -import de.danoeh.antennapod.core.service.playback.PlayerStatus; import de.danoeh.antennapod.model.playback.Playable; +import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer; +import de.danoeh.antennapod.playback.base.PlayerStatus; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; @@ -71,8 +72,8 @@ public abstract class PlaybackController { } @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(ServiceEvent event) { - if (event.action == ServiceEvent.Action.SERVICE_STARTED) { + public void onEventMainThread(PlaybackServiceEvent event) { + if (event.action == PlaybackServiceEvent.Action.SERVICE_STARTED) { init(); } } @@ -209,13 +210,6 @@ public abstract class PlaybackController { return; } switch (type) { - case PlaybackService.NOTIFICATION_TYPE_ERROR: - handleError(code); - break; - case PlaybackService.NOTIFICATION_TYPE_BUFFER_UPDATE: - float progress = ((float) code) / 100; - onBufferUpdate(progress); - break; case PlaybackService.NOTIFICATION_TYPE_RELOAD: if (playbackService == null && PlaybackService.isRunning) { bindToService(); @@ -226,21 +220,9 @@ public abstract class PlaybackController { onReloadNotification(intent.getIntExtra( PlaybackService.EXTRA_NOTIFICATION_CODE, -1)); break; - case PlaybackService.NOTIFICATION_TYPE_SLEEPTIMER_UPDATE: - onSleepTimerUpdate(); - break; - case PlaybackService.NOTIFICATION_TYPE_BUFFER_START: - onBufferStart(); - break; - case PlaybackService.NOTIFICATION_TYPE_BUFFER_END: - onBufferEnd(); - break; case PlaybackService.NOTIFICATION_TYPE_PLAYBACK_END: onPlaybackEnd(); break; - case PlaybackService.NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE: - onPlaybackSpeedChange(); - break; } } @@ -248,24 +230,11 @@ public abstract class PlaybackController { public void onPositionObserverUpdate() {} - - public void onPlaybackSpeedChange() {} - /** * Called when the currently displayed information should be refreshed. */ public void onReloadNotification(int code) {} - public void onBufferStart() {} - - public void onBufferEnd() {} - - public void onBufferUpdate(float progress) {} - - public void onSleepTimerUpdate() {} - - public void handleError(int code) {} - public void onPlaybackEnd() {} /** @@ -276,10 +245,6 @@ public abstract class PlaybackController { Log.d(TAG, "status: " + status.toString()); checkMediaInfoLoaded(); switch (status) { - case ERROR: - EventBus.getDefault().post(new MessageEvent(activity.getString(R.string.player_error_msg))); - handleError(MediaPlayer.MEDIA_ERROR_UNKNOWN); - break; case PAUSED: onPositionObserverUpdate(); updatePlayButtonShowsPlay(true); @@ -458,6 +423,11 @@ public abstract class PlaybackController { public void seekTo(int time) { if (playbackService != null) { playbackService.seekTo(time); + } else if (getMedia() instanceof FeedMedia) { + FeedMedia media = (FeedMedia) getMedia(); + media.setPosition(time); + DBWriter.setFeedItem(media.getItem()); + EventBus.getDefault().post(new PlaybackPositionEvent(time, getMedia().getDuration())); } } @@ -482,7 +452,7 @@ public abstract class PlaybackController { if (playbackService != null) { playbackService.setSpeed(speed); } else { - onPlaybackSpeedChange(); + EventBus.getDefault().post(new SpeedChangedEvent(speed)); } } @@ -555,20 +525,6 @@ public abstract class PlaybackController { } } - /** - * Move service into INITIALIZED state if it's paused to save bandwidth - */ - public void reinitServiceIfPaused() { - if (playbackService != null - && playbackService.isStreaming() - && !PlaybackService.isCasting() - && (playbackService.getStatus() == PlayerStatus.PAUSED || - (playbackService.getStatus() == PlayerStatus.PREPARING && - !playbackService.isStartWhenPrepared()))) { - playbackService.reinit(); - } - } - public boolean isStreaming() { return playbackService != null && playbackService.isStreaming(); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdater.java b/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdater.java index cecd4b3b6..2762fb9fe 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdater.java +++ b/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdater.java @@ -7,6 +7,7 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.Bitmap; +import android.os.Build; import android.os.Bundle; import android.util.Log; import android.view.KeyEvent; @@ -19,15 +20,16 @@ import com.bumptech.glide.request.RequestOptions; import java.util.concurrent.TimeUnit; import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.model.playback.MediaType; import de.danoeh.antennapod.core.glide.ApGlideSettings; import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; import de.danoeh.antennapod.core.receiver.PlayerWidget; -import de.danoeh.antennapod.core.service.playback.PlayerStatus; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.feed.util.ImageResourceUtils; import de.danoeh.antennapod.core.util.TimeSpeedConverter; import de.danoeh.antennapod.model.playback.Playable; +import de.danoeh.antennapod.playback.base.PlayerStatus; import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; import de.danoeh.antennapod.ui.appstartintent.VideoPlayerActivityStarter; @@ -67,9 +69,6 @@ public abstract class WidgetUpdater { if (!PlayerWidget.isEnabled(context) || widgetState == null) { return; } - ComponentName playerWidget = new ComponentName(context, PlayerWidget.class); - AppWidgetManager manager = AppWidgetManager.getInstance(context); - int[] widgetIds = manager.getAppWidgetIds(playerWidget); PendingIntent startMediaPlayer; if (widgetState.media != null && widgetState.media.getMediaType() == MediaType.VIDEO @@ -156,36 +155,36 @@ public abstract class WidgetUpdater { views.setImageViewResource(R.id.butPlayExtended, R.drawable.ic_widget_play); } - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { - for (int id : widgetIds) { - Bundle options = manager.getAppWidgetOptions(id); - SharedPreferences prefs = context.getSharedPreferences(PlayerWidget.PREFS_NAME, Context.MODE_PRIVATE); - int minWidth = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH); - int columns = getCellsForSize(minWidth); - if (columns < 3) { - views.setViewVisibility(R.id.layout_center, View.INVISIBLE); - } else { - views.setViewVisibility(R.id.layout_center, View.VISIBLE); - } - boolean showRewind = prefs.getBoolean(PlayerWidget.KEY_WIDGET_REWIND + id, false); - boolean showFastForward = prefs.getBoolean(PlayerWidget.KEY_WIDGET_FAST_FORWARD + id, false); - boolean showSkip = prefs.getBoolean(PlayerWidget.KEY_WIDGET_SKIP + id, false); - - if (showRewind || showSkip || showFastForward) { - views.setInt(R.id.extendedButtonsContainer, "setVisibility", View.VISIBLE); - views.setInt(R.id.butPlay, "setVisibility", View.GONE); - views.setInt(R.id.butRew, "setVisibility", showRewind ? View.VISIBLE : View.GONE); - views.setInt(R.id.butFastForward, "setVisibility", showFastForward ? View.VISIBLE : View.GONE); - views.setInt(R.id.butSkip, "setVisibility", showSkip ? View.VISIBLE : View.GONE); - } - - int backgroundColor = prefs.getInt(PlayerWidget.KEY_WIDGET_COLOR + id, PlayerWidget.DEFAULT_COLOR); - views.setInt(R.id.widgetLayout, "setBackgroundColor", backgroundColor); + ComponentName playerWidget = new ComponentName(context, PlayerWidget.class); + AppWidgetManager manager = AppWidgetManager.getInstance(context); + int[] widgetIds = manager.getAppWidgetIds(playerWidget); - manager.updateAppWidget(id, views); + for (int id : widgetIds) { + Bundle options = manager.getAppWidgetOptions(id); + SharedPreferences prefs = context.getSharedPreferences(PlayerWidget.PREFS_NAME, Context.MODE_PRIVATE); + int minWidth = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH); + int columns = getCellsForSize(minWidth); + if (columns < 3) { + views.setViewVisibility(R.id.layout_center, View.INVISIBLE); + } else { + views.setViewVisibility(R.id.layout_center, View.VISIBLE); } - } else { - manager.updateAppWidget(playerWidget, views); + boolean showRewind = prefs.getBoolean(PlayerWidget.KEY_WIDGET_REWIND + id, false); + boolean showFastForward = prefs.getBoolean(PlayerWidget.KEY_WIDGET_FAST_FORWARD + id, false); + boolean showSkip = prefs.getBoolean(PlayerWidget.KEY_WIDGET_SKIP + id, false); + + if (showRewind || showSkip || showFastForward) { + views.setInt(R.id.extendedButtonsContainer, "setVisibility", View.VISIBLE); + views.setInt(R.id.butPlay, "setVisibility", View.GONE); + views.setInt(R.id.butRew, "setVisibility", showRewind ? View.VISIBLE : View.GONE); + views.setInt(R.id.butFastForward, "setVisibility", showFastForward ? View.VISIBLE : View.GONE); + views.setInt(R.id.butSkip, "setVisibility", showSkip ? View.VISIBLE : View.GONE); + } + + int backgroundColor = prefs.getInt(PlayerWidget.KEY_WIDGET_COLOR + id, PlayerWidget.DEFAULT_COLOR); + views.setInt(R.id.widgetLayout, "setBackgroundColor", backgroundColor); + + manager.updateAppWidget(id, views); } } @@ -212,18 +211,21 @@ public abstract class WidgetUpdater { startingIntent.setAction(MediaButtonReceiver.NOTIFY_BUTTON_RECEIVER); startingIntent.putExtra(Intent.EXTRA_KEY_EVENT, event); - return PendingIntent.getBroadcast(context, eventCode, startingIntent, 0); + return PendingIntent.getBroadcast(context, eventCode, startingIntent, + (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); } private static String getProgressString(int position, int duration, float speed) { - if (position >= 0 && duration > 0) { - TimeSpeedConverter converter = new TimeSpeedConverter(speed); - position = converter.convert(position); - duration = converter.convert(duration); - return Converter.getDurationStringLong(position) + " / " - + Converter.getDurationStringLong(duration); - } else { + if (position < 0 || duration <= 0) { return null; } + TimeSpeedConverter converter = new TimeSpeedConverter(speed); + if (UserPreferences.shouldShowRemainingTime()) { + return Converter.getDurationStringLong(converter.convert(position)) + " / -" + + Converter.getDurationStringLong(converter.convert(Math.max(0, duration - position))); + } else { + return Converter.getDurationStringLong(converter.convert(position)) + " / " + + Converter.getDurationStringLong(converter.convert(duration)); + } } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdaterJobService.java b/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdaterJobService.java index b14fb3b0b..325c508c5 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdaterJobService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdaterJobService.java @@ -6,9 +6,9 @@ import androidx.annotation.NonNull; import androidx.core.app.SafeJobIntentService; import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; -import de.danoeh.antennapod.core.service.playback.PlayerStatus; import de.danoeh.antennapod.model.playback.Playable; import de.danoeh.antennapod.core.util.playback.PlayableUtils; +import de.danoeh.antennapod.playback.base.PlayerStatus; public class WidgetUpdaterJobService extends SafeJobIntentService { private static final int JOB_ID = -17001; |